@tangle-network/agent-app 0.3.1 → 0.5.0

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.
@@ -4,123 +4,6 @@ import {
4
4
  isAppToolName
5
5
  } from "./chunk-LT2YIMEB.js";
6
6
 
7
- // src/runtime/model.ts
8
- var DEFAULT_TANGLE_ROUTER_BASE_URL = "https://router.tangle.tools/v1";
9
- var DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR = "TANGLE_BILLING_ENFORCEMENT";
10
- function requireEnv(env, name) {
11
- const value = env[name]?.trim();
12
- if (!value) throw new Error(`${name} is required`);
13
- return value;
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
- }
101
- function resolveTangleModelConfig(opts = {}) {
102
- const env = opts.env ?? process.env;
103
- const provider = env.MODEL_PROVIDER?.trim() || "openai-compat";
104
- const model = requireEnv(env, "MODEL_NAME");
105
- if (provider === "openai-compat" || provider === "tangle-router" || provider === "tcloud") {
106
- return {
107
- provider: "openai-compat",
108
- model,
109
- apiKey: requireEnv(env, "TANGLE_API_KEY"),
110
- baseUrl: (env.TANGLE_ROUTER_BASE_URL?.trim() || opts.defaultRouterBaseUrl || DEFAULT_TANGLE_ROUTER_BASE_URL).replace(/\/+$/, "")
111
- };
112
- }
113
- if (provider === "anthropic") {
114
- return {
115
- provider,
116
- model,
117
- apiKey: requireEnv(env, "ANTHROPIC_API_KEY"),
118
- baseUrl: requireEnv(env, "ANTHROPIC_BASE_URL")
119
- };
120
- }
121
- throw new Error(`Unsupported MODEL_PROVIDER: ${provider} (use openai-compat for the Tangle Router, or anthropic for BYOK)`);
122
- }
123
-
124
7
  // src/runtime/openai-stream.ts
125
8
  async function* toLoopEvents(chunks) {
126
9
  const calls = /* @__PURE__ */ new Map();
@@ -258,10 +141,27 @@ function createAgentRuntime(opts) {
258
141
  }
259
142
 
260
143
  // src/runtime/index.ts
144
+ function toolCallId(call) {
145
+ return call.toolCallId ?? `call_${call.toolName}`;
146
+ }
147
+ function assistantToolCallMessage(turnText, pending) {
148
+ return {
149
+ role: "assistant",
150
+ content: turnText.trim() || null,
151
+ tool_calls: pending.map((call) => ({
152
+ id: toolCallId(call),
153
+ type: "function",
154
+ function: { name: call.toolName, arguments: JSON.stringify(call.args) }
155
+ }))
156
+ };
157
+ }
158
+ function toolResultMessage(call, content) {
159
+ return { role: "tool", tool_call_id: toolCallId(call), content };
160
+ }
261
161
  var DEFAULT_MAX_TOOL_TURNS = 8;
262
162
  function defaultRender(label, outcome) {
263
- if (outcome.ok) return `- ${label} \u2192 ok: ${JSON.stringify(outcome.result)}`;
264
- return `- ${label} \u2192 failed (${outcome.code}): ${outcome.message}`;
163
+ if (outcome.ok) return `${label} \u2192 ok: ${JSON.stringify(outcome.result)}`;
164
+ return `${label} \u2192 failed (${outcome.code}): ${outcome.message}`;
265
165
  }
266
166
  async function runAppToolLoop(opts) {
267
167
  const maxTurns = opts.maxToolTurns ?? DEFAULT_MAX_TOOL_TURNS;
@@ -291,8 +191,7 @@ async function runAppToolLoop(opts) {
291
191
  if (toolTurn >= maxTurns) {
292
192
  return { finalText, toolResults, turns, cappedOut: true };
293
193
  }
294
- if (turnText.trim()) messages.push({ role: "assistant", content: turnText });
295
- const lines = [];
194
+ messages.push(assistantToolCallMessage(turnText, pending));
296
195
  for (const call of pending) {
297
196
  let outcome;
298
197
  try {
@@ -302,10 +201,8 @@ async function runAppToolLoop(opts) {
302
201
  }
303
202
  const label = labelFor(call);
304
203
  toolResults.push({ call, label, outcome });
305
- lines.push(render(label, outcome));
204
+ messages.push(toolResultMessage(call, render(label, outcome)));
306
205
  }
307
- messages.push({ role: "user", content: `Tool results:
308
- ${lines.join("\n")}` });
309
206
  }
310
207
  return { finalText, toolResults, turns, cappedOut: false };
311
208
  }
@@ -332,8 +229,7 @@ async function* streamAppToolLoop(opts) {
332
229
  yield { kind: "capped", pending: pending.length };
333
230
  return;
334
231
  }
335
- if (turnText.trim()) messages.push({ role: "assistant", content: turnText });
336
- const lines = [];
232
+ messages.push(assistantToolCallMessage(turnText, pending));
337
233
  for (const call of pending) {
338
234
  let outcome;
339
235
  try {
@@ -343,29 +239,16 @@ async function* streamAppToolLoop(opts) {
343
239
  }
344
240
  const label = labelFor(call);
345
241
  yield { kind: "tool_result", toolName: call.toolName, toolCallId: call.toolCallId, label, outcome };
346
- lines.push(render(label, outcome));
242
+ messages.push(toolResultMessage(call, render(label, outcome)));
347
243
  }
348
- messages.push({ role: "user", content: `Tool results:
349
- ${lines.join("\n")}` });
350
244
  }
351
245
  }
352
246
 
353
247
  export {
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,
364
- resolveTangleModelConfig,
365
248
  toLoopEvents,
366
249
  createOpenAICompatStreamTurn,
367
250
  createAgentRuntime,
368
251
  runAppToolLoop,
369
252
  streamAppToolLoop
370
253
  };
371
- //# sourceMappingURL=chunk-SVCJYRVM.js.map
254
+ //# sourceMappingURL=chunk-7PXRACS2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runtime/openai-stream.ts","../src/runtime/agent.ts","../src/runtime/index.ts"],"sourcesContent":["/**\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, LoopMessage, 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: LoopMessage[]) => 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, append the results to history in OpenAI function-calling shape,\n * and re-run so the model reads them. Bounded by `maxToolTurns` so a model\n * looping on a failing action can't run forever.\n *\n * The history shape is the OpenAI function-calling contract: the assistant turn\n * that emitted tool calls is preserved as an `assistant` message carrying its\n * `tool_calls` array, and each result is its own `{ role: 'tool', tool_call_id,\n * content }` message keyed to the call. A strict model (Claude, and any model\n * that validates tool history) needs this to read its own tool use back; folding\n * results into a `user` message instead makes such models re-issue the same call\n * in a loop.\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/** One OpenAI-shaped tool-call entry on an assistant message. */\nexport interface LoopAssistantToolCall {\n id: string\n type: 'function'\n function: { name: string; arguments: string }\n}\n\n/**\n * A message in the running conversation the loop sends to `streamTurn`.\n *\n * The base `{ role, content }` covers `system` / `user` / plain `assistant`\n * turns. Two optional fields carry the OpenAI function-calling contract so the\n * model reads its own tool use back correctly instead of re-issuing it:\n *\n * - an assistant turn that emitted tool calls carries `tool_calls`, and its\n * `content` is `null` when the turn was tool-only;\n * - each tool result is its own `{ role: 'tool', tool_call_id, content }`\n * message keyed to the call that produced it.\n *\n * Widening is additive: a `streamTurn` that reads only `role` + `content` still\n * works; one that forwards the whole message to an OpenAI-compatible endpoint\n * now gets correct tool history. */\nexport interface LoopMessage {\n role: string\n content: string | null\n tool_calls?: LoopAssistantToolCall[]\n tool_call_id?: string\n}\n\n/** A tool-call id is required to key a `role: 'tool'` result back to its call.\n * When the model omitted one, derive a stable id from the tool name so the\n * assistant `tool_calls` entry and its `tool` result still match. */\nfunction toolCallId(call: LoopToolCall): string {\n return call.toolCallId ?? `call_${call.toolName}`\n}\n\n/** The assistant turn that emitted `pending`, in OpenAI shape: text content\n * (null when the turn was tool-only) plus its `tool_calls` array. */\nfunction assistantToolCallMessage(turnText: string, pending: LoopToolCall[]): LoopMessage {\n return {\n role: 'assistant',\n content: turnText.trim() || null,\n tool_calls: pending.map((call) => ({\n id: toolCallId(call),\n type: 'function',\n function: { name: call.toolName, arguments: JSON.stringify(call.args) },\n })),\n }\n}\n\n/** One `role: 'tool'` result message keyed to its call by `tool_call_id`. */\nfunction toolResultMessage(call: LoopToolCall, content: string): LoopMessage {\n return { role: 'tool', tool_call_id: toolCallId(call), content }\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. Messages follow {@link LoopMessage}: a tool-calling assistant\n * turn carries `tool_calls`, and each tool result is a `role: 'tool'` message.\n * A backend that reads only `role` + `content` is unaffected. */\n streamTurn: (messages: LoopMessage[]) => 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 the `content` of its `role: 'tool'` message.\n * Default 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: LoopMessage[] = [\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 // The assistant turn that emitted the calls — with its tool_calls array —\n // so the model sees its own tool use in history.\n messages.push(assistantToolCallMessage(turnText, pending))\n\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 // One role:'tool' message per result, keyed to its call by tool_call_id.\n messages.push(toolResultMessage(call, render(label, outcome)))\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 → append → 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 * Messages follow {@link LoopMessage}: a tool-calling assistant turn carries\n * `tool_calls`, and each tool result is a `role: 'tool'` message. */\n streamTurn: (messages: LoopMessage[]) => 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: LoopMessage[] = [\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 // The assistant turn that emitted the calls — with its tool_calls array.\n messages.push(assistantToolCallMessage(turnText, pending))\n\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 // One role:'tool' message per result, keyed to its call by tool_call_id.\n messages.push(toolResultMessage(call, render(label, outcome)))\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,131 @@
1
+ // src/runtime/model.ts
2
+ var DEFAULT_TANGLE_ROUTER_BASE_URL = "https://router.tangle.tools/v1";
3
+ var DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR = "TANGLE_BILLING_ENFORCEMENT";
4
+ function requireEnv(env, name) {
5
+ const value = env[name]?.trim();
6
+ if (!value) throw new Error(`${name} is required`);
7
+ return value;
8
+ }
9
+ function trimOrNull(value) {
10
+ const trimmed = value?.trim();
11
+ return trimmed ? trimmed : null;
12
+ }
13
+ function isTangleExecutionKeyErrorCode(value) {
14
+ return value === "local_tangle_api_key_required" || value === "tangle_account_not_connected";
15
+ }
16
+ var TangleExecutionKeyError = class extends Error {
17
+ code;
18
+ status;
19
+ constructor(code, message, status) {
20
+ super(message);
21
+ this.name = "TangleExecutionKeyError";
22
+ this.code = code;
23
+ this.status = status;
24
+ }
25
+ };
26
+ function isTangleExecutionKeyError(error) {
27
+ return error instanceof TangleExecutionKeyError || typeof error === "object" && error !== null && error.name === "TangleExecutionKeyError" && typeof error.message === "string" && isTangleExecutionKeyErrorCode(error.code) && typeof error.status === "number";
28
+ }
29
+ function resolveTangleExecutionEnvironment(env = process.env) {
30
+ const raw = (env.APP_ENV ?? env.NODE_ENV ?? "").trim().toLowerCase();
31
+ if (raw === "development" || raw === "dev" || raw === "local") return "development";
32
+ if (raw === "staging") return "staging";
33
+ if (raw === "test") return "test";
34
+ return "production";
35
+ }
36
+ function isTangleBillingEnforcementDisabled(opts = {}) {
37
+ const env = opts.env ?? process.env;
38
+ const enforcementEnvVar = opts.enforcementEnvVar ?? DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR;
39
+ const override = env[enforcementEnvVar]?.trim().toLowerCase();
40
+ if (override === "disabled") return true;
41
+ if (override === "enabled") return false;
42
+ return resolveTangleExecutionEnvironment(env) === "development";
43
+ }
44
+ function tangleExecutionKeyHttpError(error) {
45
+ if (!isTangleExecutionKeyError(error)) return null;
46
+ return {
47
+ status: error.status,
48
+ body: {
49
+ error: error.message,
50
+ code: error.code
51
+ }
52
+ };
53
+ }
54
+ async function resolveUserTangleExecutionKey(opts) {
55
+ const env = opts.env ?? process.env;
56
+ const environment = opts.environment ?? resolveTangleExecutionEnvironment(env);
57
+ if (environment === "development") {
58
+ const apiKey2 = trimOrNull(env.TANGLE_API_KEY);
59
+ if (apiKey2) return { apiKey: apiKey2, source: "local-env" };
60
+ }
61
+ const apiKey = trimOrNull(await opts.getUserApiKey());
62
+ if (apiKey) return { apiKey, source: "user" };
63
+ if (environment === "development") {
64
+ throw new TangleExecutionKeyError(
65
+ "local_tangle_api_key_required",
66
+ "TANGLE_API_KEY or a linked Tangle account is required for local Tangle model execution.",
67
+ 503
68
+ );
69
+ }
70
+ throw new TangleExecutionKeyError(
71
+ "tangle_account_not_connected",
72
+ "Connect your Tangle account before invoking this agent.",
73
+ 401
74
+ );
75
+ }
76
+ async function resolveUserTangleExecutionKeyForUser(opts) {
77
+ return resolveUserTangleExecutionKey({
78
+ environment: opts.environment,
79
+ env: opts.env,
80
+ getUserApiKey: () => opts.getUserApiKey(opts.userId)
81
+ });
82
+ }
83
+ function createTangleRouterModelConfig(opts) {
84
+ const apiKey = opts.apiKey.trim();
85
+ if (!apiKey) throw new Error("apiKey is required");
86
+ const model = opts.model.trim();
87
+ if (!model) throw new Error("model is required");
88
+ return {
89
+ provider: "openai-compat",
90
+ model,
91
+ apiKey,
92
+ baseUrl: (opts.baseUrl?.trim() || DEFAULT_TANGLE_ROUTER_BASE_URL).replace(/\/+$/, "")
93
+ };
94
+ }
95
+ function resolveTangleModelConfig(opts = {}) {
96
+ const env = opts.env ?? process.env;
97
+ const provider = env.MODEL_PROVIDER?.trim() || "openai-compat";
98
+ const model = requireEnv(env, "MODEL_NAME");
99
+ if (provider === "openai-compat" || provider === "tangle-router" || provider === "tcloud") {
100
+ return {
101
+ provider: "openai-compat",
102
+ model,
103
+ apiKey: requireEnv(env, "TANGLE_API_KEY"),
104
+ baseUrl: (env.TANGLE_ROUTER_BASE_URL?.trim() || opts.defaultRouterBaseUrl || DEFAULT_TANGLE_ROUTER_BASE_URL).replace(/\/+$/, "")
105
+ };
106
+ }
107
+ if (provider === "anthropic") {
108
+ return {
109
+ provider,
110
+ model,
111
+ apiKey: requireEnv(env, "ANTHROPIC_API_KEY"),
112
+ baseUrl: requireEnv(env, "ANTHROPIC_BASE_URL")
113
+ };
114
+ }
115
+ throw new Error(`Unsupported MODEL_PROVIDER: ${provider} (use openai-compat for the Tangle Router, or anthropic for BYOK)`);
116
+ }
117
+
118
+ export {
119
+ DEFAULT_TANGLE_ROUTER_BASE_URL,
120
+ DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR,
121
+ TangleExecutionKeyError,
122
+ isTangleExecutionKeyError,
123
+ resolveTangleExecutionEnvironment,
124
+ isTangleBillingEnforcementDisabled,
125
+ tangleExecutionKeyHttpError,
126
+ resolveUserTangleExecutionKey,
127
+ resolveUserTangleExecutionKeyForUser,
128
+ createTangleRouterModelConfig,
129
+ resolveTangleModelConfig
130
+ };
131
+ //# sourceMappingURL=chunk-EHPK7GKR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runtime/model.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"],"mappings":"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","names":["apiKey"]}
@@ -39,6 +39,34 @@ async function checkRateLimit(kv, key, limit, windowSeconds) {
39
39
  await kv.put(kvKey, JSON.stringify(valid), { expirationTtl: windowSeconds * 2 });
40
40
  return { allowed: true, remaining: limit - valid.length, resetAt: now + windowSeconds };
41
41
  }
42
+ function serializeCookie(value, opts) {
43
+ if (opts.sameSite === "None" && !opts.secure) {
44
+ throw new Error("SameSite=None cookies require secure: true (browsers reject them otherwise)");
45
+ }
46
+ const parts = [`${opts.name}=${encodeURIComponent(value)}`, `Path=${opts.path ?? "/"}`];
47
+ if (opts.httpOnly !== false) parts.push("HttpOnly");
48
+ parts.push(`SameSite=${opts.sameSite ?? "Lax"}`);
49
+ if (opts.maxAgeSeconds !== void 0) parts.push(`Max-Age=${opts.maxAgeSeconds}`);
50
+ if (opts.secure) parts.push("Secure");
51
+ return parts.join("; ");
52
+ }
53
+ function clearCookieHeader(opts) {
54
+ return serializeCookie("", { ...opts, maxAgeSeconds: 0 });
55
+ }
56
+ function readCookieValue(cookieHeader, name) {
57
+ if (!cookieHeader) return null;
58
+ for (const part of cookieHeader.split(/;\s*/)) {
59
+ const [cookieName, ...rest] = part.split("=");
60
+ if (cookieName === name) {
61
+ try {
62
+ return decodeURIComponent(rest.join("="));
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+ }
68
+ return null;
69
+ }
42
70
  function addSecurityHeaders(response, opts = {}) {
43
71
  response.headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload");
44
72
  response.headers.set("X-Content-Type-Options", "nosniff");
@@ -56,6 +84,9 @@ export {
56
84
  requireString,
57
85
  extractRequestContext,
58
86
  checkRateLimit,
87
+ serializeCookie,
88
+ clearCookieHeader,
89
+ readCookieValue,
59
90
  addSecurityHeaders
60
91
  };
61
- //# sourceMappingURL=chunk-CN75FIPT.js.map
92
+ //# sourceMappingURL=chunk-SCG5JJ4C.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/web/index.ts"],"sourcesContent":["/**\n * Web-boundary utilities every agent app's routes hand-roll: JSON body parsing\n * + narrowing, request-context extraction (real client IP behind Cloudflare),\n * a KV-backed sliding-window rate limiter, and security response headers. Pure\n * mechanism — no DB, no domain. The KV is a structural interface so this needs\n * no `@cloudflare/workers-types` dependency.\n */\n\nexport type JsonObject = Record<string, unknown>\n\n/** Parse + object-narrow a Request body. `[body, null]` on success, `[null,\n * errorResponse]` on a non-object body (callers `if (err) return err`). */\nexport async function parseJsonObjectBody(request: Request): Promise<[JsonObject, null] | [null, Response]> {\n let raw: unknown\n try {\n raw = await request.json()\n } catch {\n return [null, Response.json({ error: 'Invalid JSON body' }, { status: 400 })]\n }\n if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {\n return [null, Response.json({ error: 'Body must be a JSON object' }, { status: 400 })]\n }\n return [raw as JsonObject, null]\n}\n\n/** Narrow one required string field, 400 if missing/empty. */\nexport function requireString(body: JsonObject, field: string): string | Response {\n const v = body[field]\n if (typeof v !== 'string' || v.length === 0) {\n return Response.json({ error: `Missing or non-string field: ${field}` }, { status: 400 })\n }\n return v\n}\n\nexport interface RequestContext {\n ipAddress: string\n userAgent: string\n timestamp: string\n requestId: string\n}\n\n/** Extract request context for audit trails. Uses `CF-Connecting-IP` for the\n * real client IP behind Cloudflare. */\nexport function extractRequestContext(request: Request): RequestContext {\n const ipAddress =\n request.headers.get('CF-Connecting-IP') ??\n request.headers.get('X-Forwarded-For')?.split(',')[0]?.trim() ??\n '0.0.0.0'\n return {\n ipAddress,\n userAgent: request.headers.get('User-Agent') ?? '',\n timestamp: new Date().toISOString(),\n requestId: crypto.randomUUID(),\n }\n}\n\n/** Minimal KV contract (Cloudflare `KVNamespace` satisfies it structurally). */\nexport interface KvLike {\n get(key: string): Promise<string | null>\n put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>\n}\n\nexport interface RateLimitResult {\n allowed: boolean\n remaining: number\n resetAt: number\n}\n\n/** KV-backed sliding-window rate limit. Stores recent timestamps per key,\n * prunes the window, allows until `limit` is hit. */\nexport async function checkRateLimit(kv: KvLike, key: string, limit: number, windowSeconds: number): Promise<RateLimitResult> {\n const now = Math.floor(Date.now() / 1000)\n const windowStart = now - windowSeconds\n const kvKey = `rl:${key}`\n const raw = await kv.get(kvKey)\n const timestamps: number[] = raw ? JSON.parse(raw) : []\n const valid = timestamps.filter((t) => t > windowStart)\n if (valid.length >= limit) return { allowed: false, remaining: 0, resetAt: (valid[0] ?? now) + windowSeconds }\n valid.push(now)\n await kv.put(kvKey, JSON.stringify(valid), { expirationTtl: windowSeconds * 2 })\n return { allowed: true, remaining: limit - valid.length, resetAt: now + windowSeconds }\n}\n\nexport interface CookieOptions {\n name: string\n /** Default '/'. */\n path?: string\n /** Default true. */\n httpOnly?: boolean\n /** Adds the `Secure` attribute. Default false. */\n secure?: boolean\n /** Default 'Lax'. */\n sameSite?: 'Lax' | 'Strict' | 'None'\n maxAgeSeconds?: number\n}\n\n/** Serialize a Set-Cookie header value: `name=encodeURIComponent(value)` plus\n * attributes in Path / HttpOnly / SameSite / Max-Age / Secure order.\n * Throws on `SameSite=None` without `secure` — browsers silently drop that\n * combination, which would otherwise fail invisibly. */\nexport function serializeCookie(value: string, opts: CookieOptions): string {\n if (opts.sameSite === 'None' && !opts.secure) {\n throw new Error('SameSite=None cookies require secure: true (browsers reject them otherwise)')\n }\n const parts = [`${opts.name}=${encodeURIComponent(value)}`, `Path=${opts.path ?? '/'}`]\n if (opts.httpOnly !== false) parts.push('HttpOnly')\n parts.push(`SameSite=${opts.sameSite ?? 'Lax'}`)\n if (opts.maxAgeSeconds !== undefined) parts.push(`Max-Age=${opts.maxAgeSeconds}`)\n if (opts.secure) parts.push('Secure')\n return parts.join('; ')\n}\n\n/** Set-Cookie header value that deletes the cookie (empty value, Max-Age=0). */\nexport function clearCookieHeader(opts: Omit<CookieOptions, 'maxAgeSeconds'>): string {\n return serializeCookie('', { ...opts, maxAgeSeconds: 0 })\n}\n\n/** Read + decode one cookie from a Cookie request header; null when absent. */\nexport function readCookieValue(cookieHeader: string | null, name: string): string | null {\n if (!cookieHeader) return null\n for (const part of cookieHeader.split(/;\\s*/)) {\n const [cookieName, ...rest] = part.split('=')\n if (cookieName === name) {\n try {\n return decodeURIComponent(rest.join('='))\n } catch {\n return null\n }\n }\n }\n return null\n}\n\nexport interface SecurityHeaderOptions {\n /** Product disclaimer (e.g. \"AI-powered tool. Not legal advice.\"). Omitted if absent. */\n disclaimer?: string\n /** Data-retention label (e.g. \"7-years\"). Omitted if absent. */\n retention?: string\n /** Extra headers to set. */\n extra?: Record<string, string>\n}\n\n/** Set standard security headers on a response (HSTS, nosniff, frame-options,\n * referrer-policy, XSS) + optional product disclaimer/retention. The security\n * set is generic; the disclaimer/retention are the product's. */\nexport function addSecurityHeaders(response: Response, opts: SecurityHeaderOptions = {}): Response {\n response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload')\n response.headers.set('X-Content-Type-Options', 'nosniff')\n response.headers.set('X-Frame-Options', 'SAMEORIGIN')\n response.headers.set('Referrer-Policy', 'same-origin')\n response.headers.set('X-XSS-Protection', '1; mode=block')\n if (opts.disclaimer) response.headers.set('X-AI-Disclaimer', opts.disclaimer)\n if (opts.retention) response.headers.set('X-Data-Retention', opts.retention)\n for (const [k, v] of Object.entries(opts.extra ?? {})) response.headers.set(k, v)\n return response\n}\n"],"mappings":";AAYA,eAAsB,oBAAoB,SAAkE;AAC1G,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,QAAQ,KAAK;AAAA,EAC3B,QAAQ;AACN,WAAO,CAAC,MAAM,SAAS,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC,CAAC;AAAA,EAC9E;AACA,MAAI,QAAQ,QAAQ,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG;AACjE,WAAO,CAAC,MAAM,SAAS,KAAK,EAAE,OAAO,6BAA6B,GAAG,EAAE,QAAQ,IAAI,CAAC,CAAC;AAAA,EACvF;AACA,SAAO,CAAC,KAAmB,IAAI;AACjC;AAGO,SAAS,cAAc,MAAkB,OAAkC;AAChF,QAAM,IAAI,KAAK,KAAK;AACpB,MAAI,OAAO,MAAM,YAAY,EAAE,WAAW,GAAG;AAC3C,WAAO,SAAS,KAAK,EAAE,OAAO,gCAAgC,KAAK,GAAG,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC1F;AACA,SAAO;AACT;AAWO,SAAS,sBAAsB,SAAkC;AACtE,QAAM,YACJ,QAAQ,QAAQ,IAAI,kBAAkB,KACtC,QAAQ,QAAQ,IAAI,iBAAiB,GAAG,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,KAC5D;AACF,SAAO;AAAA,IACL;AAAA,IACA,WAAW,QAAQ,QAAQ,IAAI,YAAY,KAAK;AAAA,IAChD,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC,WAAW,OAAO,WAAW;AAAA,EAC/B;AACF;AAgBA,eAAsB,eAAe,IAAY,KAAa,OAAe,eAAiD;AAC5H,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,cAAc,MAAM;AAC1B,QAAM,QAAQ,MAAM,GAAG;AACvB,QAAM,MAAM,MAAM,GAAG,IAAI,KAAK;AAC9B,QAAM,aAAuB,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AACtD,QAAM,QAAQ,WAAW,OAAO,CAAC,MAAM,IAAI,WAAW;AACtD,MAAI,MAAM,UAAU,MAAO,QAAO,EAAE,SAAS,OAAO,WAAW,GAAG,UAAU,MAAM,CAAC,KAAK,OAAO,cAAc;AAC7G,QAAM,KAAK,GAAG;AACd,QAAM,GAAG,IAAI,OAAO,KAAK,UAAU,KAAK,GAAG,EAAE,eAAe,gBAAgB,EAAE,CAAC;AAC/E,SAAO,EAAE,SAAS,MAAM,WAAW,QAAQ,MAAM,QAAQ,SAAS,MAAM,cAAc;AACxF;AAmBO,SAAS,gBAAgB,OAAe,MAA6B;AAC1E,MAAI,KAAK,aAAa,UAAU,CAAC,KAAK,QAAQ;AAC5C,UAAM,IAAI,MAAM,6EAA6E;AAAA,EAC/F;AACA,QAAM,QAAQ,CAAC,GAAG,KAAK,IAAI,IAAI,mBAAmB,KAAK,CAAC,IAAI,QAAQ,KAAK,QAAQ,GAAG,EAAE;AACtF,MAAI,KAAK,aAAa,MAAO,OAAM,KAAK,UAAU;AAClD,QAAM,KAAK,YAAY,KAAK,YAAY,KAAK,EAAE;AAC/C,MAAI,KAAK,kBAAkB,OAAW,OAAM,KAAK,WAAW,KAAK,aAAa,EAAE;AAChF,MAAI,KAAK,OAAQ,OAAM,KAAK,QAAQ;AACpC,SAAO,MAAM,KAAK,IAAI;AACxB;AAGO,SAAS,kBAAkB,MAAoD;AACpF,SAAO,gBAAgB,IAAI,EAAE,GAAG,MAAM,eAAe,EAAE,CAAC;AAC1D;AAGO,SAAS,gBAAgB,cAA6B,MAA6B;AACxF,MAAI,CAAC,aAAc,QAAO;AAC1B,aAAW,QAAQ,aAAa,MAAM,MAAM,GAAG;AAC7C,UAAM,CAAC,YAAY,GAAG,IAAI,IAAI,KAAK,MAAM,GAAG;AAC5C,QAAI,eAAe,MAAM;AACvB,UAAI;AACF,eAAO,mBAAmB,KAAK,KAAK,GAAG,CAAC;AAAA,MAC1C,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAcO,SAAS,mBAAmB,UAAoB,OAA8B,CAAC,GAAa;AACjG,WAAS,QAAQ,IAAI,6BAA6B,8CAA8C;AAChG,WAAS,QAAQ,IAAI,0BAA0B,SAAS;AACxD,WAAS,QAAQ,IAAI,mBAAmB,YAAY;AACpD,WAAS,QAAQ,IAAI,mBAAmB,aAAa;AACrD,WAAS,QAAQ,IAAI,oBAAoB,eAAe;AACxD,MAAI,KAAK,WAAY,UAAS,QAAQ,IAAI,mBAAmB,KAAK,UAAU;AAC5E,MAAI,KAAK,UAAW,UAAS,QAAQ,IAAI,oBAAoB,KAAK,SAAS;AAC3E,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,EAAG,UAAS,QAAQ,IAAI,GAAG,CAAC;AAChF,SAAO;AACT;","names":[]}
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { APP_TOOL_NAMES, AppToolMcpServer, AppToolName, AppToolRuntimeExecutor,
2
2
  export { A as AddCitationArgs, a as AddCitationResult, b as AppToolContext, c as AppToolHandlers, d as AppToolOutcome, e as AppToolProducedEvent, f as AppToolTaxonomy, R as RenderUiArgs, g as RenderUiResult, S as ScheduleFollowupArgs, h as ScheduleFollowupResult, i as SubmitProposalArgs, j as SubmitProposalResult } from './types-CeWor4bQ.js';
3
3
  export { BuildDelegationOptions, DELEGATION_MCP_SERVER_KEY, DELEGATION_TOOLS, DelegationMcpServer, buildDelegationMcpServer, delegationMcpForConfig } from './delegation/index.js';
4
4
  export { BrokerToken, BrokerTokenMinter, BrokerTokenProvider, BrokerTokenProviderOptions, ConsentUrlInput, buildConsentUrl, createBrokerTokenProvider } from './tangle/index.js';
5
- export { AgentRuntime, AgentRuntimeModelConfig, AgentTurnOptions, AppToolLoopOptions, CreateAgentRuntimeOptions, LoopEvent, LoopToolCall, OpenAICompatStreamTurnOptions, OpenAIStreamChunk, StreamAppToolLoopOptions, StreamLoopYield, ToolLoopResult, createAgentRuntime, createOpenAICompatStreamTurn, runAppToolLoop, streamAppToolLoop, toLoopEvents } from './runtime/index.js';
5
+ export { AgentRuntime, AgentRuntimeModelConfig, AgentTurnOptions, AppToolLoopOptions, CreateAgentRuntimeOptions, LoopAssistantToolCall, LoopEvent, LoopMessage, LoopToolCall, OpenAICompatStreamTurnOptions, OpenAIStreamChunk, StreamAppToolLoopOptions, StreamLoopYield, ToolLoopResult, createAgentRuntime, createOpenAICompatStreamTurn, runAppToolLoop, streamAppToolLoop, toLoopEvents } from './runtime/index.js';
6
6
  export { createTokenRecallChecker, producedFromToolEvents } from './eval/index.js';
7
7
  export { KnowledgeRequirementSpec, KnowledgeSignal, KnowledgeStateAccessor, SatisfiedByRule, buildKnowledgeRequirements, deriveSignals } from './knowledge/index.js';
8
8
  export { CreateKnowledgeLoopDeps, KnowledgeCandidate, KnowledgeDecider, KnowledgeDeciderInput, KnowledgeDecision, KnowledgeGateVerdict, KnowledgeLoop, KnowledgeLoopDriver, createKnowledgeLoop, createReviewerDecider, reviewCandidate } from './knowledge-loop/index.js';
@@ -13,7 +13,7 @@ export { KeyCrypto, KeyProvisioner, PlanLimit, PlatformBalanceInfo, PlatformBala
13
13
  export { DeriveKeyOptions, createFieldCrypto, decodeHexKey, decryptAesGcm, decryptBytes, decryptWithKey, deriveKey, encryptAesGcm, encryptBytes, encryptWithKey } from './crypto/index.js';
14
14
  export { JsonRecord, PersistedChatMessageForTurn, ResolvedChatTurn, StreamEvent, asRecord, asString, buildUserTextParts, encodeEvent, finalizeAssistantParts, getPartKey, mergePersistedPart, messageHasTurnId, normalizeClientTurnId, normalizePersistedPart, normalizeTime, normalizeToolEvent, resolveChatTurn, resolveToolId, resolveToolName } from './stream/index.js';
15
15
  export { HubExecClient, HubExecClientOptions, HubExecErrorCode, HubExecResult, HubInvokeDeps, HubInvokeInput, HubInvokeOutcome, ParsedIntegrationAction, invokeIntegrationHub, resolveIntegrationAction } from './integrations/index.js';
16
- export { JsonObject, KvLike, RateLimitResult, RequestContext, SecurityHeaderOptions, addSecurityHeaders, checkRateLimit, extractRequestContext, parseJsonObjectBody, requireString } from './web/index.js';
16
+ export { CookieOptions, JsonObject, KvLike, RateLimitResult, RequestContext, SecurityHeaderOptions, addSecurityHeaders, checkRateLimit, clearCookieHeader, extractRequestContext, parseJsonObjectBody, readCookieValue, requireString, serializeCookie } 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
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';
package/dist/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import {
2
2
  addSecurityHeaders,
3
3
  checkRateLimit,
4
+ clearCookieHeader,
4
5
  extractRequestContext,
5
6
  parseJsonObjectBody,
6
- requireString
7
- } from "./chunk-CN75FIPT.js";
7
+ readCookieValue,
8
+ requireString,
9
+ serializeCookie
10
+ } from "./chunk-SCG5JJ4C.js";
8
11
  import {
9
12
  DEFAULT_REDACTION_PATTERNS,
10
13
  buildRedactedDocument,
@@ -98,12 +101,17 @@ import {
98
101
  buildConsentUrl,
99
102
  createBrokerTokenProvider
100
103
  } from "./chunk-YGUNTIT5.js";
104
+ import {
105
+ createAgentRuntime,
106
+ createOpenAICompatStreamTurn,
107
+ runAppToolLoop,
108
+ streamAppToolLoop,
109
+ toLoopEvents
110
+ } from "./chunk-7PXRACS2.js";
101
111
  import {
102
112
  DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR,
103
113
  DEFAULT_TANGLE_ROUTER_BASE_URL,
104
114
  TangleExecutionKeyError,
105
- createAgentRuntime,
106
- createOpenAICompatStreamTurn,
107
115
  createTangleRouterModelConfig,
108
116
  isTangleBillingEnforcementDisabled,
109
117
  isTangleExecutionKeyError,
@@ -111,11 +119,8 @@ import {
111
119
  resolveTangleModelConfig,
112
120
  resolveUserTangleExecutionKey,
113
121
  resolveUserTangleExecutionKeyForUser,
114
- runAppToolLoop,
115
- streamAppToolLoop,
116
- tangleExecutionKeyHttpError,
117
- toLoopEvents
118
- } from "./chunk-SVCJYRVM.js";
122
+ tangleExecutionKeyHttpError
123
+ } from "./chunk-EHPK7GKR.js";
119
124
  import {
120
125
  APP_TOOL_NAMES,
121
126
  ToolInputError,
@@ -167,6 +172,7 @@ export {
167
172
  buildRedactedDocument,
168
173
  buildUserTextParts,
169
174
  checkRateLimit,
175
+ clearCookieHeader,
170
176
  coerceHarness,
171
177
  createAgentRuntime,
172
178
  createAppToolRuntimeExecutor,
@@ -222,6 +228,7 @@ export {
222
228
  outcomeStatus,
223
229
  parseJsonObjectBody,
224
230
  producedFromToolEvents,
231
+ readCookieValue,
225
232
  readToolArgs,
226
233
  redactForIngestion,
227
234
  requireString,
@@ -237,6 +244,7 @@ export {
237
244
  revealSpan,
238
245
  reviewCandidate,
239
246
  runAppToolLoop,
247
+ serializeCookie,
240
248
  streamAppToolLoop,
241
249
  tangleExecutionKeyHttpError,
242
250
  toLoopEvents,