@tangle-network/agent-app 0.7.0 → 0.7.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.
@@ -326,13 +326,20 @@ function assistantToolCallMessage(turnText, pending) {
326
326
  function toolResultMessage(call, content) {
327
327
  return { role: "tool", tool_call_id: toolCallId(call), content };
328
328
  }
329
- var DEFAULT_MAX_TOOL_TURNS = 8;
329
+ var RUNAWAY_BACKSTOP_TURNS = 200;
330
+ var STUCK_LOOP_THRESHOLD = 3;
331
+ function canonicalCallHash(call) {
332
+ const sortedArgs = Object.fromEntries(
333
+ Object.entries(call.args).sort(([a], [b]) => a.localeCompare(b))
334
+ );
335
+ return `${call.toolName}:${JSON.stringify(sortedArgs)}`;
336
+ }
330
337
  function defaultRender(label, outcome) {
331
338
  if (outcome.ok) return `${label} \u2192 ok: ${JSON.stringify(outcome.result)}`;
332
339
  return `${label} \u2192 failed (${outcome.code}): ${outcome.message}`;
333
340
  }
334
341
  async function runAppToolLoop(opts) {
335
- const maxTurns = opts.maxToolTurns ?? DEFAULT_MAX_TOOL_TURNS;
342
+ const backstop = opts.maxToolTurns ?? RUNAWAY_BACKSTOP_TURNS;
336
343
  const render = opts.renderResult ?? defaultRender;
337
344
  const labelFor = opts.labelFor ?? ((c) => c.toolName);
338
345
  const messages = [
@@ -343,8 +350,14 @@ async function runAppToolLoop(opts) {
343
350
  const toolResults = [];
344
351
  let finalText = "";
345
352
  let turns = 0;
353
+ let accumulatedCostUsd = 0;
354
+ let lastCallHash = null;
355
+ let consecutiveCount = 0;
346
356
  for (let toolTurn = 0; ; toolTurn++) {
347
357
  turns++;
358
+ if (opts.deadlineMs !== void 0 && Date.now() >= opts.deadlineMs) {
359
+ return { finalText, toolResults, turns, stopReason: "deadline", cappedOut: true };
360
+ }
348
361
  let turnText = "";
349
362
  const pending = [];
350
363
  for await (const ev of opts.streamTurn([...messages])) {
@@ -356,26 +369,45 @@ async function runAppToolLoop(opts) {
356
369
  }
357
370
  }
358
371
  if (pending.length === 0) break;
359
- if (toolTurn >= maxTurns) {
360
- return { finalText, toolResults, turns, cappedOut: true };
372
+ if (toolTurn >= backstop) {
373
+ return { finalText, toolResults, turns, stopReason: "backstop", cappedOut: true };
361
374
  }
362
375
  messages.push(assistantToolCallMessage(turnText, pending));
363
376
  for (const call of pending) {
377
+ const callHash = canonicalCallHash(call);
378
+ if (callHash === lastCallHash) {
379
+ consecutiveCount++;
380
+ } else {
381
+ lastCallHash = callHash;
382
+ consecutiveCount = 1;
383
+ }
384
+ if (consecutiveCount >= STUCK_LOOP_THRESHOLD) {
385
+ return { finalText, toolResults, turns, stopReason: "stuck-loop", cappedOut: true };
386
+ }
364
387
  let outcome;
365
388
  try {
366
389
  outcome = await opts.executeToolCall(call);
367
390
  } catch (err) {
368
391
  outcome = { ok: false, code: "executor_error", message: err instanceof Error ? err.message : String(err) };
369
392
  }
393
+ if (opts.maxCostUsd !== void 0 && opts.costOf !== void 0) {
394
+ accumulatedCostUsd += opts.costOf(call, outcome);
395
+ if (accumulatedCostUsd >= opts.maxCostUsd) {
396
+ const label2 = labelFor(call);
397
+ toolResults.push({ call, label: label2, outcome });
398
+ messages.push(toolResultMessage(call, render(label2, outcome)));
399
+ return { finalText, toolResults, turns, stopReason: "budget", cappedOut: true };
400
+ }
401
+ }
370
402
  const label = labelFor(call);
371
403
  toolResults.push({ call, label, outcome });
372
404
  messages.push(toolResultMessage(call, render(label, outcome)));
373
405
  }
374
406
  }
375
- return { finalText, toolResults, turns, cappedOut: false };
407
+ return { finalText, toolResults, turns, stopReason: "completed", cappedOut: false };
376
408
  }
377
409
  async function* streamAppToolLoop(opts) {
378
- const maxTurns = opts.maxToolTurns ?? DEFAULT_MAX_TOOL_TURNS;
410
+ const backstop = opts.maxToolTurns ?? RUNAWAY_BACKSTOP_TURNS;
379
411
  const render = opts.renderResult ?? defaultRender;
380
412
  const labelFor = opts.labelFor ?? ((c) => c.toolName);
381
413
  const messages = [
@@ -383,7 +415,14 @@ async function* streamAppToolLoop(opts) {
383
415
  ...opts.priorMessages ?? [],
384
416
  { role: "user", content: opts.userMessage }
385
417
  ];
418
+ let accumulatedCostUsd = 0;
419
+ let lastCallHash = null;
420
+ let consecutiveCount = 0;
386
421
  for (let toolTurn = 0; ; toolTurn++) {
422
+ if (opts.deadlineMs !== void 0 && Date.now() >= opts.deadlineMs) {
423
+ yield { kind: "capped", pending: 0, stopReason: "deadline" };
424
+ return;
425
+ }
387
426
  let turnText = "";
388
427
  const pending = [];
389
428
  for await (const event of opts.streamTurn([...messages])) {
@@ -393,18 +432,39 @@ async function* streamAppToolLoop(opts) {
393
432
  if (call && opts.isExecutableTool(call.toolName)) pending.push(call);
394
433
  }
395
434
  if (pending.length === 0) return;
396
- if (toolTurn >= maxTurns) {
397
- yield { kind: "capped", pending: pending.length };
435
+ if (toolTurn >= backstop) {
436
+ yield { kind: "capped", pending: pending.length, stopReason: "backstop" };
398
437
  return;
399
438
  }
400
439
  messages.push(assistantToolCallMessage(turnText, pending));
401
440
  for (const call of pending) {
441
+ const callHash = canonicalCallHash(call);
442
+ if (callHash === lastCallHash) {
443
+ consecutiveCount++;
444
+ } else {
445
+ lastCallHash = callHash;
446
+ consecutiveCount = 1;
447
+ }
448
+ if (consecutiveCount >= STUCK_LOOP_THRESHOLD) {
449
+ yield { kind: "capped", pending: pending.length, stopReason: "stuck-loop" };
450
+ return;
451
+ }
402
452
  let outcome;
403
453
  try {
404
454
  outcome = await opts.executeToolCall(call);
405
455
  } catch (err) {
406
456
  outcome = { ok: false, code: "executor_error", message: err instanceof Error ? err.message : String(err) };
407
457
  }
458
+ if (opts.maxCostUsd !== void 0 && opts.costOf !== void 0) {
459
+ accumulatedCostUsd += opts.costOf(call, outcome);
460
+ if (accumulatedCostUsd >= opts.maxCostUsd) {
461
+ const label2 = labelFor(call);
462
+ yield { kind: "tool_result", toolName: call.toolName, toolCallId: call.toolCallId, label: label2, outcome };
463
+ messages.push(toolResultMessage(call, render(label2, outcome)));
464
+ yield { kind: "capped", pending: pending.length, stopReason: "budget" };
465
+ return;
466
+ }
467
+ }
408
468
  const label = labelFor(call);
409
469
  yield { kind: "tool_result", toolName: call.toolName, toolCallId: call.toolCallId, label, outcome };
410
470
  messages.push(toolResultMessage(call, render(label, outcome)));
@@ -423,4 +483,4 @@ export {
423
483
  runAppToolLoop,
424
484
  streamAppToolLoop
425
485
  };
426
- //# sourceMappingURL=chunk-TH2AOJJM.js.map
486
+ //# sourceMappingURL=chunk-4YTWB5MG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runtime/model-catalog.ts","../src/runtime/openai-stream.ts","../src/runtime/agent.ts","../src/runtime/index.ts"],"sourcesContent":["/**\n * Model catalogue — computed live from the Tangle Router, never hand-curated.\n * Lifted from tuner-agent so every agent app's model picker shares one\n * filter/dedupe/rank/feature pipeline instead of re-deriving it.\n *\n * The router's /models endpoint returns every routeable model (~200), which is\n * unusable as a picker list: it mixes chat models with TTS/embedding/realtime\n * endpoints, dated snapshots alias their parents, and provider-prefixed ids\n * duplicate canonical ones. This module turns that into a product catalogue:\n *\n * filter (chat-capable, routeable) → dedupe (snapshot/prefix/:free aliases)\n * → rank (provider tier, family, version) → feature (best model per family)\n * → default (env override or first featured)\n *\n * Freshness is automatic: everything is derived from the live router response,\n * so new models surface as soon as the router lists them. The only static\n * knowledge here is slow-moving: provider display order and family name\n * patterns (e.g. \"claude-sonnet-*\", \"gpt-N\"). A new Sonnet or GPT release\n * outranks its predecessor by version comparison with zero code change; only\n * a brand-new *family name* (rare) needs a one-line rule addition.\n */\n\nexport interface RouterModel {\n id: string\n name?: string\n description?: string\n _provider?: string\n pricing?: { prompt?: string | null; completion?: string | null }\n context_length?: number\n architecture?: {\n modality?: string\n input_modalities?: string[]\n output_modalities?: string[]\n }\n supported_parameters?: string[]\n routeability?: {\n status?: string\n routeable?: boolean\n provider?: string\n }\n}\n\nexport interface CatalogModel {\n id: string\n name: string\n provider: string\n description?: string\n contextLength?: number\n pricing?: { prompt?: string; completion?: string }\n supportsTools: boolean\n supportsReasoning: boolean\n featured: boolean\n}\n\nexport interface ModelCatalog {\n defaultModelId: string | null\n fetchedAt: string\n models: CatalogModel[]\n}\n\n/** Display order. Unlisted providers sort after these, alphabetically. */\nconst PROVIDER_TIER: string[] = [\n 'anthropic',\n 'openai',\n 'google',\n 'xai',\n 'deepseek',\n 'moonshotai',\n 'moonshot',\n 'zai',\n 'z-ai',\n 'mistral',\n 'groq',\n 'nvidia',\n 'cohere',\n 'cerebras',\n]\n\n/** Non-chat endpoints that pollute the router list (matched on normalized id). */\nconst EXCLUDED_ID = /(embedding|tts|transcribe|whisper|audio|realtime|image|lyria|sora|dall-e|moderation|content-safety|search-preview|search-api|deep-research)/\n\n/**\n * Featured families, in display order. Each rule surfaces the highest-version\n * routeable model whose normalized id matches. Patterns anchor on the family\n * name and stop before specialty suffixes (codex, nano, lite, …) so the\n * mainline model wins.\n */\nconst FEATURED_RULES: Array<{ providers: string[]; match: RegExp }> = [\n { providers: ['anthropic'], match: /^claude-sonnet-[\\d-]+$/ },\n { providers: ['anthropic'], match: /^claude-opus-[\\d-]+$/ },\n { providers: ['anthropic'], match: /^claude-haiku-[\\d-]+$/ },\n { providers: ['openai'], match: /^gpt-\\d+(\\.\\d+)?$/ },\n { providers: ['openai'], match: /^gpt-\\d+(\\.\\d+)?-mini$/ },\n { providers: ['google'], match: /^gemini-[\\d.]+-pro(-preview)?$/ },\n { providers: ['google'], match: /^gemini-[\\d.]+-flash(-preview)?$/ },\n { providers: ['xai'], match: /^grok-[\\d.]+$/ },\n { providers: ['deepseek'], match: /^deepseek-(chat|v[\\d.]+(-\\w+)?)$/ },\n { providers: ['moonshotai', 'moonshot'], match: /^kimi-k[\\d.]+$/ },\n { providers: ['zai', 'z-ai'], match: /^glm-[\\d.]+$/ },\n { providers: ['mistral'], match: /^mistral-(large|medium)-?[\\d.-]*$/ },\n]\n\n/** Families known to support tool calls even when router metadata omits it\n * (dated snapshots often lack the supported_parameters of their parent). */\nconst TOOL_CAPABLE_FAMILY = /^(claude|gpt-[45]|gpt-oss|o[134]|gemini|grok|deepseek|glm|kimi|mistral|ministral|magistral|command|nemotron|llama)/\n\n/** Strip provider prefix, :free suffix, and trailing date stamps. */\nexport function normalizeModelId(id: string): string {\n let tail = id.split('/').pop() ?? id\n tail = tail.replace(/:free$/, '')\n tail = tail.replace(/-\\d{8}$/, '')\n tail = tail.replace(/-\\d{4}-\\d{2}-\\d{2}$/, '')\n return tail\n}\n\n/** All numeric groups in a normalized id, for version comparison. */\nfunction versionOf(normId: string): number[] {\n return (normId.match(/\\d+/g) ?? []).map(Number)\n}\n\nfunction compareVersions(a: number[], b: number[]): number {\n const len = Math.max(a.length, b.length)\n for (let i = 0; i < len; i++) {\n const d = (a[i] ?? -1) - (b[i] ?? -1)\n if (d !== 0) return d\n }\n return 0\n}\n\n/** Lower = preferred representative for an alias group. */\nfunction aliasPenalty(id: string): number {\n let p = 0\n if (id.includes('/')) p += 4\n if (/-\\d{8}$|-\\d{4}-\\d{2}-\\d{2}$/.test(id.replace(/:free$/, ''))) p += 2\n if (id.endsWith(':free')) p += 1\n return p\n}\n\nfunction providerRank(provider: string): number {\n const i = PROVIDER_TIER.indexOf(provider)\n return i === -1 ? PROVIDER_TIER.length : i\n}\n\nfunction isChatModel(m: RouterModel): boolean {\n const arch = m.architecture\n if (!arch?.input_modalities || !arch?.output_modalities) return true\n return arch.input_modalities.includes('text') && arch.output_modalities.includes('text')\n}\n\nfunction isRouteable(m: RouterModel): boolean {\n return m.routeability?.routeable !== false && m.routeability?.status !== 'unavailable'\n}\n\nfunction familyOf(normId: string): string {\n return normId.replace(/[\\d.]+/g, '').replace(/-+/g, '-').replace(/-$/, '')\n}\n\n/**\n * Pure catalogue pipeline. `preferredDefault` (typically the MODEL_NAME env\n * var) wins when it survives filtering; otherwise the first featured model.\n */\nexport function buildCatalog(raw: RouterModel[], opts?: { preferredDefault?: string }): ModelCatalog {\n // Filter to chat-capable, routeable, non-specialty models\n const candidates = raw.filter(\n (m) => m.id && isRouteable(m) && isChatModel(m) && !EXCLUDED_ID.test(normalizeModelId(m.id)),\n )\n\n // Dedupe alias groups (dated snapshots, provider prefixes, :free variants).\n // Within a group, merge metadata so the representative keeps the richest\n // supported_parameters claim (snapshots often omit what the parent lists).\n const groups = new Map<string, RouterModel[]>()\n for (const m of candidates) {\n const key = `${m._provider ?? ''}::${normalizeModelId(m.id)}`\n const g = groups.get(key)\n if (g) g.push(m)\n else groups.set(key, [m])\n }\n\n const reps: Array<{ model: RouterModel; normId: string; mergedParams: Set<string> }> = []\n for (const group of groups.values()) {\n group.sort((a, b) => aliasPenalty(a.id) - aliasPenalty(b.id) || a.id.length - b.id.length)\n const rep = group[0]!\n const mergedParams = new Set<string>(group.flatMap((m) => m.supported_parameters ?? []))\n reps.push({ model: rep, normId: normalizeModelId(rep.id), mergedParams })\n }\n\n // Featured: best version per family rule, in rule order\n const featuredIds: string[] = []\n for (const rule of FEATURED_RULES) {\n const matches = reps.filter(\n (r) =>\n rule.providers.includes(r.model._provider ?? '') &&\n rule.match.test(r.normId) &&\n !featuredIds.includes(r.model.id),\n )\n if (!matches.length) continue\n matches.sort(\n (a, b) =>\n compareVersions(versionOf(b.normId), versionOf(a.normId)) ||\n Number(a.normId.includes('preview')) - Number(b.normId.includes('preview')) ||\n a.model.id.length - b.model.id.length,\n )\n featuredIds.push(matches[0]!.model.id)\n }\n\n const toCatalogModel = (r: (typeof reps)[number]): CatalogModel => {\n const m = r.model\n const provider = m._provider ?? 'unknown'\n return {\n id: m.id,\n name: m.name ?? m.id,\n provider,\n description: m.description ? m.description.slice(0, 160) : undefined,\n contextLength: m.context_length,\n pricing:\n m.pricing?.prompt || m.pricing?.completion\n ? { prompt: m.pricing.prompt ?? undefined, completion: m.pricing.completion ?? undefined }\n : undefined,\n supportsTools: r.mergedParams.has('tools') || TOOL_CAPABLE_FAMILY.test(r.normId),\n supportsReasoning: r.mergedParams.has('reasoning') || r.mergedParams.has('include_reasoning'),\n featured: featuredIds.includes(m.id),\n }\n }\n\n // Sort: featured first (rule order), then provider tier → family → version desc\n const featured = featuredIds\n .map((id) => reps.find((r) => r.model.id === id)!)\n .map(toCatalogModel)\n const rest = reps\n .filter((r) => !featuredIds.includes(r.model.id))\n .sort((a, b) => {\n const pa = providerRank(a.model._provider ?? '')\n const pb = providerRank(b.model._provider ?? '')\n if (pa !== pb) return pa - pb\n const fa = familyOf(a.normId)\n const fb = familyOf(b.normId)\n if (fa !== fb) return fa.localeCompare(fb)\n return compareVersions(versionOf(b.normId), versionOf(a.normId)) || a.model.id.localeCompare(b.model.id)\n })\n .map(toCatalogModel)\n\n const models = [...featured, ...rest]\n\n const preferred = opts?.preferredDefault\n const defaultModelId =\n (preferred && models.find((m) => m.id === preferred || normalizeModelId(m.id) === normalizeModelId(preferred))?.id) ||\n featured.find((m) => m.supportsTools)?.id ||\n models[0]?.id ||\n null\n\n return { defaultModelId, fetchedAt: new Date().toISOString(), models }\n}\n\n// ── Cached fetch ─────────────────────────────────────────────────────────\n\nconst CATALOG_TTL_MS = 5 * 60 * 1000\n\nlet _cache: { catalog: ModelCatalog; at: number } | null = null\n\n/**\n * Fetch the router model list and build the catalogue, with an in-isolate\n * cache (TTL 5 min). On router failure a stale catalogue is served rather\n * than erroring the picker.\n */\nexport async function fetchModelCatalog(cfg: {\n baseUrl: string\n apiKey: string\n preferredDefault?: string\n}): Promise<ModelCatalog> {\n if (_cache && Date.now() - _cache.at < CATALOG_TTL_MS) {\n return _cache.catalog\n }\n try {\n const res = await fetch(`${cfg.baseUrl}/models`, {\n headers: { Authorization: `Bearer ${cfg.apiKey}` },\n })\n if (!res.ok) throw new Error(`Router /models returned ${res.status}`)\n const data = (await res.json()) as { data?: RouterModel[] }\n const catalog = buildCatalog(data.data ?? [], { preferredDefault: cfg.preferredDefault })\n _cache = { catalog, at: Date.now() }\n return catalog\n } catch (err) {\n if (_cache) return _cache.catalog\n throw err\n }\n}\n\n/** Test-only: clear the catalogue cache. */\nexport function __resetCatalogCache(): void {\n _cache = null\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, 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 /** Reasoning deltas — DeepSeek/router use `reasoning_content`; some proxies use `thinking`. */\n reasoning_content?: string | null\n thinking?: 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 /** Final-chunk token accounting (requires `stream_options.include_usage`). */\n usage?: {\n prompt_tokens?: number\n completion_tokens?: number\n } | null\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 // Usage rides the final chunk, which has an empty choices array — handle\n // it before the choice guard.\n if (chunk.usage?.prompt_tokens != null || chunk.usage?.completion_tokens != null) {\n yield {\n type: 'usage',\n usage: {\n promptTokens: chunk.usage.prompt_tokens ?? 0,\n completionTokens: chunk.usage.completion_tokens ?? 0,\n },\n }\n }\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 const reasoning = choice.delta?.reasoning_content ?? choice.delta?.thinking\n if (reasoning) yield { type: 'reasoning', text: reasoning }\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 stream_options: { include_usage: 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 /** Runaway-backstop cap. Default 200 — set far above any legitimate workflow.\n * For per-workflow limits use `deadlineMs` or `maxCostUsd` on the loop options. */\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-catalog'\nexport * 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; `reasoning` and `usage` pass through\n * for UIs that render thinking sections and per-message token/cost metrics.\n * Extra event types pass through untouched (the caller re-emits them to its\n * own UI stream). */\nexport type LoopEvent =\n | { type: 'text'; text: string }\n | { type: 'reasoning'; text: string }\n | { type: 'tool_call'; call: LoopToolCall }\n | { type: 'usage'; usage: { promptTokens: number; completionTokens: number } }\n | { type: 'other'; event: unknown }\n\n/** Why the loop stopped. `completed` = model finished naturally; `stuck-loop` =\n * ≥3 consecutive identical tool calls (same tool + args); `backstop` = hit the\n * runaway-backstop cap (200 by default); `deadline` = wall-clock deadlineMs\n * exceeded; `budget` = maxCostUsd exhausted. Non-`completed` stops are infra /\n * resource outcomes — eval scoring must distinguish them from capability failure. */\nexport type ToolLoopStopReason = 'completed' | 'stuck-loop' | 'backstop' | 'deadline' | 'budget'\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 /** Why the loop stopped. */\n stopReason: ToolLoopStopReason\n /** @deprecated Use `stopReason !== 'completed'` instead. */\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 /** Runaway-backstop cap. Default 200 — set far above any legitimate workflow.\n * For per-workflow limits use `maxCostUsd` or `deadlineMs` instead. */\n maxToolTurns?: number\n /** Wall-clock deadline in ms since epoch (Date.now()-based). When exceeded the\n * loop stops with stopReason `deadline`. */\n deadlineMs?: number\n /** Maximum total cost in USD. Requires `costOf` to meter each tool call. */\n maxCostUsd?: number\n /** Return the USD cost of one outcome. Required for `maxCostUsd` to work. */\n costOf?: (call: LoopToolCall, outcome: AppToolOutcome) => 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\n/** Runaway-backstop: stops an infinite tool loop where cost is unmetered. Set\n * far above any legitimate workflow — this is a watchdog, not a policy cap. */\nconst RUNAWAY_BACKSTOP_TURNS = 200\n/** Consecutive identical calls (same tool + canonical-JSON args) that trigger\n * stuck-loop detection. The window resets on any different call. */\nconst STUCK_LOOP_THRESHOLD = 3\n\n/** Canonical identifier for a tool call used by stuck-loop detection.\n * Keys are sorted so {b:1,a:2} and {a:2,b:1} produce the same hash. */\nfunction canonicalCallHash(call: LoopToolCall): string {\n const sortedArgs = Object.fromEntries(\n Object.entries(call.args).sort(([a], [b]) => a.localeCompare(b)),\n )\n return `${call.toolName}:${JSON.stringify(sortedArgs)}`\n}\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 backstop = opts.maxToolTurns ?? RUNAWAY_BACKSTOP_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 let accumulatedCostUsd = 0\n let lastCallHash: string | null = null\n let consecutiveCount = 0\n\n for (let toolTurn = 0; ; toolTurn++) {\n turns++\n\n // Wall-clock deadline check before every new turn.\n if (opts.deadlineMs !== undefined && Date.now() >= opts.deadlineMs) {\n return { finalText, toolResults, turns, stopReason: 'deadline', cappedOut: true }\n }\n\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\n // Runaway backstop — the model keeps emitting calls past the safety ceiling.\n if (toolTurn >= backstop) {\n return { finalText, toolResults, turns, stopReason: 'backstop', 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 // Stuck-loop detection.\n const callHash = canonicalCallHash(call)\n if (callHash === lastCallHash) {\n consecutiveCount++\n } else {\n lastCallHash = callHash\n consecutiveCount = 1\n }\n if (consecutiveCount >= STUCK_LOOP_THRESHOLD) {\n return { finalText, toolResults, turns, stopReason: 'stuck-loop', cappedOut: true }\n }\n\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\n // Budget check after each tool call.\n if (opts.maxCostUsd !== undefined && opts.costOf !== undefined) {\n accumulatedCostUsd += opts.costOf(call, outcome)\n if (accumulatedCostUsd >= opts.maxCostUsd) {\n const label = labelFor(call)\n toolResults.push({ call, label, outcome })\n messages.push(toolResultMessage(call, render(label, outcome)))\n return { finalText, toolResults, turns, stopReason: 'budget', cappedOut: true }\n }\n }\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, stopReason: 'completed', 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; stopReason: Exclude<ToolLoopStopReason, 'completed'> }\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 /** Runaway-backstop cap. Default 200 — set far above any legitimate workflow. */\n maxToolTurns?: number\n /** Wall-clock deadline in ms since epoch (Date.now()-based). */\n deadlineMs?: number\n /** Maximum total cost in USD. Requires `costOf` to meter each tool call. */\n maxCostUsd?: number\n /** Return the USD cost of one outcome. Required for `maxCostUsd` to work. */\n costOf?: (call: LoopToolCall, outcome: AppToolOutcome) => 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` (with stopReason)\n * when it stops for any non-completed reason. 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 backstop = opts.maxToolTurns ?? RUNAWAY_BACKSTOP_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 let accumulatedCostUsd = 0\n let lastCallHash: string | null = null\n let consecutiveCount = 0\n\n for (let toolTurn = 0; ; toolTurn++) {\n // Wall-clock deadline check before every new turn.\n if (opts.deadlineMs !== undefined && Date.now() >= opts.deadlineMs) {\n yield { kind: 'capped', pending: 0, stopReason: 'deadline' }\n return\n }\n\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\n // Runaway backstop.\n if (toolTurn >= backstop) {\n yield { kind: 'capped', pending: pending.length, stopReason: 'backstop' }\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 // Stuck-loop detection.\n const callHash = canonicalCallHash(call)\n if (callHash === lastCallHash) {\n consecutiveCount++\n } else {\n lastCallHash = callHash\n consecutiveCount = 1\n }\n if (consecutiveCount >= STUCK_LOOP_THRESHOLD) {\n yield { kind: 'capped', pending: pending.length, stopReason: 'stuck-loop' }\n return\n }\n\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\n // Budget check after each tool call.\n if (opts.maxCostUsd !== undefined && opts.costOf !== undefined) {\n accumulatedCostUsd += opts.costOf(call, outcome)\n if (accumulatedCostUsd >= opts.maxCostUsd) {\n const label = labelFor(call)\n yield { kind: 'tool_result', toolName: call.toolName, toolCallId: call.toolCallId, label, outcome }\n messages.push(toolResultMessage(call, render(label, outcome)))\n yield { kind: 'capped', pending: pending.length, stopReason: 'budget' }\n return\n }\n }\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":["label"]}