@agentfield/sdk 0.1.111-rc.1 → 0.1.111-rc.3

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.
package/dist/index.js CHANGED
@@ -142,6 +142,34 @@ var init_openrouterAttribution = __esm({
142
142
  DEFAULT_OPENROUTER_APP_NAME = "AgentField AI";
143
143
  }
144
144
  });
145
+
146
+ // src/harness/modelVariant.ts
147
+ function splitModelVariant(model) {
148
+ if (typeof model !== "string" || !model.trim()) {
149
+ return {};
150
+ }
151
+ const sepIndex = model.indexOf(MODEL_VARIANT_SEP);
152
+ const base = sepIndex === -1 ? model : model.slice(0, sepIndex);
153
+ const variant = sepIndex === -1 ? "" : model.slice(sepIndex + 1);
154
+ return {
155
+ model: base.trim() || void 0,
156
+ variant: variant.trim() || void 0
157
+ };
158
+ }
159
+ function resolveModelAndVariant(options) {
160
+ const { model, variant } = splitModelVariant(options.model);
161
+ const explicit = options.variant;
162
+ if (typeof explicit === "string" && explicit.trim()) {
163
+ return { model, variant: explicit.trim() };
164
+ }
165
+ return { model, variant };
166
+ }
167
+ var MODEL_VARIANT_SEP;
168
+ var init_modelVariant = __esm({
169
+ "src/harness/modelVariant.ts"() {
170
+ MODEL_VARIANT_SEP = "#";
171
+ }
172
+ });
145
173
  function getZodConverter() {
146
174
  if (zodConverter !== void 0) {
147
175
  return zodConverter;
@@ -155,14 +183,14 @@ function getZodConverter() {
155
183
  }
156
184
  return zodConverter;
157
185
  }
158
- function isRecord(value) {
186
+ function isRecord2(value) {
159
187
  return typeof value === "object" && value !== null && !Array.isArray(value);
160
188
  }
161
189
  function hasJsonSchema(value) {
162
- return isRecord(value) && typeof value.jsonSchema === "function";
190
+ return isRecord2(value) && typeof value.jsonSchema === "function";
163
191
  }
164
192
  function hasParse(value) {
165
- return isRecord(value) && typeof value.parse === "function";
193
+ return isRecord2(value) && typeof value.parse === "function";
166
194
  }
167
195
  function estimateTokens(text2) {
168
196
  return Math.floor(text2.length / 4);
@@ -191,7 +219,7 @@ function getSchemaPath(cwd) {
191
219
  return path2__default.join(cwd, SCHEMA_FILENAME);
192
220
  }
193
221
  function schemaToJsonSchema(schema) {
194
- if (isRecord(schema)) {
222
+ if (isRecord2(schema)) {
195
223
  if ("type" in schema || "properties" in schema || "$schema" in schema) {
196
224
  return schema;
197
225
  }
@@ -339,7 +367,7 @@ var claude_exports = {};
339
367
  __export(claude_exports, {
340
368
  ClaudeCodeProvider: () => ClaudeCodeProvider
341
369
  });
342
- function isRecord2(value) {
370
+ function isRecord3(value) {
343
371
  return typeof value === "object" && value !== null;
344
372
  }
345
373
  function getString(record, key) {
@@ -354,6 +382,7 @@ var ClaudeCodeProvider;
354
382
  var init_claude = __esm({
355
383
  "src/harness/providers/claude.ts"() {
356
384
  init_types();
385
+ init_modelVariant();
357
386
  ClaudeCodeProvider = class {
358
387
  async execute(prompt, options) {
359
388
  let sdk;
@@ -366,7 +395,8 @@ var init_claude = __esm({
366
395
  );
367
396
  }
368
397
  const agentOptions = {};
369
- if (options.model !== void 0) agentOptions.model = options.model;
398
+ const { model: modelValue } = resolveModelAndVariant(options);
399
+ if (modelValue !== void 0) agentOptions.model = modelValue;
370
400
  if (options.cwd !== void 0) agentOptions.cwd = options.cwd;
371
401
  if (options.maxTurns !== void 0) agentOptions.max_turns = options.maxTurns;
372
402
  if (options.tools !== void 0) agentOptions.allowed_tools = options.tools;
@@ -383,10 +413,16 @@ var init_claude = __esm({
383
413
  let totalCost;
384
414
  let numTurns = 0;
385
415
  let sessionId = "";
416
+ let usageObj;
417
+ let inputTokens;
418
+ let outputTokens;
419
+ let cacheReadTokens;
420
+ let cacheCreationTokens;
421
+ let modelName;
386
422
  const startApi = Date.now();
387
423
  try {
388
424
  for await (const msg of sdk.query({ prompt, options: agentOptions })) {
389
- const msgObj = isRecord2(msg) ? msg : { raw: String(msg) };
425
+ const msgObj = isRecord3(msg) ? msg : { raw: String(msg) };
390
426
  messages.push(msgObj);
391
427
  const msgType = getString(msgObj, "type") ?? "";
392
428
  if (msgType === "result") {
@@ -405,16 +441,28 @@ var init_claude = __esm({
405
441
  }
406
442
  const turns = getNumber(msgObj, "num_turns");
407
443
  numTurns = turns === void 0 ? messages.length : Math.trunc(turns);
408
- } else if (msgType === "assistant" && resultText === void 0) {
444
+ if (isRecord3(msgObj.usage)) {
445
+ usageObj = msgObj.usage;
446
+ inputTokens = getNumber(usageObj, "input_tokens");
447
+ outputTokens = getNumber(usageObj, "output_tokens");
448
+ cacheReadTokens = getNumber(usageObj, "cache_read_input_tokens");
449
+ cacheCreationTokens = getNumber(usageObj, "cache_creation_input_tokens");
450
+ }
451
+ modelName = getString(msgObj, "model") ?? modelName;
452
+ } else if (msgType === "assistant") {
453
+ if (modelName === void 0 && isRecord3(msgObj.message)) {
454
+ modelName = getString(msgObj.message, "model");
455
+ }
456
+ if (resultText !== void 0) continue;
409
457
  let content = msgObj.content;
410
- if (content === void 0 && isRecord2(msgObj.message)) {
458
+ if (content === void 0 && isRecord3(msgObj.message)) {
411
459
  content = msgObj.message.content;
412
460
  }
413
461
  if (typeof content === "string") {
414
462
  resultText = content;
415
463
  } else if (Array.isArray(content)) {
416
464
  for (const block of content) {
417
- if (isRecord2(block) && block.type === "text" && typeof block.text === "string") {
465
+ if (isRecord3(block) && block.type === "text" && typeof block.text === "string") {
418
466
  resultText = block.text;
419
467
  }
420
468
  }
@@ -428,7 +476,13 @@ var init_claude = __esm({
428
476
  durationApiMs: Date.now() - startApi,
429
477
  numTurns,
430
478
  totalCostUsd: totalCost,
431
- sessionId
479
+ sessionId,
480
+ usage: usageObj,
481
+ inputTokens,
482
+ outputTokens,
483
+ cacheReadTokens,
484
+ cacheCreationTokens,
485
+ model: modelName
432
486
  }),
433
487
  isError: false
434
488
  });
@@ -586,6 +640,7 @@ var init_codex = __esm({
586
640
  "src/harness/providers/codex.ts"() {
587
641
  init_types();
588
642
  init_cli();
643
+ init_modelVariant();
589
644
  CodexProvider = class {
590
645
  bin;
591
646
  constructor(binPath = "codex") {
@@ -599,6 +654,13 @@ var init_codex = __esm({
599
654
  if (options.permissionMode === "auto") {
600
655
  cmd.push("--full-auto");
601
656
  }
657
+ const { model: modelValue, variant: variantValue } = resolveModelAndVariant(options);
658
+ if (modelValue) {
659
+ cmd.push("-m", modelValue);
660
+ }
661
+ if (variantValue) {
662
+ cmd.push("-c", `model_reasoning_effort=${variantValue}`);
663
+ }
602
664
  cmd.push(prompt);
603
665
  const startApi = Date.now();
604
666
  try {
@@ -625,7 +687,7 @@ var init_codex = __esm({
625
687
  return createRawResult({
626
688
  result: resultText,
627
689
  messages: events,
628
- metrics: createMetrics({ durationApiMs: Date.now() - startApi, numTurns, sessionId }),
690
+ metrics: createMetrics({ durationApiMs: Date.now() - startApi, numTurns, sessionId, model: modelValue }),
629
691
  isError,
630
692
  errorMessage: isError ? stderr.trim() : void 0
631
693
  });
@@ -659,6 +721,7 @@ var init_gemini = __esm({
659
721
  "src/harness/providers/gemini.ts"() {
660
722
  init_types();
661
723
  init_cli();
724
+ init_modelVariant();
662
725
  GeminiProvider = class {
663
726
  bin;
664
727
  constructor(binPath = "gemini") {
@@ -672,8 +735,9 @@ var init_gemini = __esm({
672
735
  if (options.permissionMode === "auto") {
673
736
  cmd.push("--sandbox");
674
737
  }
675
- if (options.model) {
676
- cmd.push("-m", String(options.model));
738
+ const { model: modelValue } = resolveModelAndVariant(options);
739
+ if (modelValue) {
740
+ cmd.push("-m", modelValue);
677
741
  }
678
742
  cmd.push("-p", prompt);
679
743
  const startApi = Date.now();
@@ -725,6 +789,7 @@ var init_opencode = __esm({
725
789
  "src/harness/providers/opencode.ts"() {
726
790
  init_types();
727
791
  init_cli();
792
+ init_modelVariant();
728
793
  init_openrouterAttribution();
729
794
  OpenCodeProvider = class {
730
795
  bin;
@@ -739,8 +804,12 @@ var init_opencode = __esm({
739
804
  cmd.push("--dir", options.project_dir);
740
805
  }
741
806
  const env = { ...options.env };
742
- if (options.model) {
743
- cmd.push("-m", String(options.model));
807
+ const { model: modelValue, variant: variantValue } = resolveModelAndVariant(options);
808
+ if (modelValue) {
809
+ cmd.push("-m", modelValue);
810
+ }
811
+ if (variantValue) {
812
+ cmd.push("--variant", variantValue);
744
813
  }
745
814
  let effectivePrompt = prompt;
746
815
  if (options.system_prompt && typeof options.system_prompt === "string" && options.system_prompt.trim()) {
@@ -753,9 +822,8 @@ USER REQUEST:
753
822
  ${prompt}`;
754
823
  }
755
824
  cmd.push(effectivePrompt);
756
- const explicitModel = typeof options.model === "string" ? options.model : void 0;
757
- if (explicitModel && isOpenRouterRequest({ model: explicitModel }) && !env.OPENCODE_CONFIG_CONTENT && !process.env.OPENCODE_CONFIG_CONTENT) {
758
- const modelSlug = explicitModel.slice("openrouter/".length);
825
+ if (modelValue && isOpenRouterRequest({ model: modelValue }) && !env.OPENCODE_CONFIG_CONTENT && !process.env.OPENCODE_CONFIG_CONTENT) {
826
+ const modelSlug = modelValue.slice("openrouter/".length);
759
827
  const headers = openRouterAttributionHeaders({ env: { ...process.env, ...env } });
760
828
  if (modelSlug && Object.keys(headers).length > 0) {
761
829
  env.OPENCODE_CONFIG_CONTENT = JSON.stringify({
@@ -780,7 +848,8 @@ ${prompt}`;
780
848
  metrics: createMetrics({
781
849
  durationApiMs: Date.now() - startApi,
782
850
  numTurns: resultText ? 1 : 0,
783
- sessionId: ""
851
+ sessionId: "",
852
+ model: modelValue
784
853
  }),
785
854
  isError,
786
855
  errorMessage: isError ? stderr.trim() : void 0
@@ -842,6 +911,10 @@ var runner_exports = {};
842
911
  __export(runner_exports, {
843
912
  HarnessRunner: () => HarnessRunner
844
913
  });
914
+ function tokenMetrics(raw) {
915
+ const { inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, totalTokens, model } = raw.metrics;
916
+ return { inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, totalTokens, model };
917
+ }
845
918
  var TRANSIENT_PATTERNS, HarnessRunner;
846
919
  var init_runner = __esm({
847
920
  "src/harness/runner.ts"() {
@@ -892,7 +965,8 @@ var init_runner = __esm({
892
965
  numTurns: raw.metrics.numTurns,
893
966
  durationMs: Date.now() - startTime,
894
967
  sessionId: raw.metrics.sessionId,
895
- messages: raw.messages
968
+ messages: raw.messages,
969
+ ...tokenMetrics(raw)
896
970
  });
897
971
  } finally {
898
972
  if (schema !== void 0) {
@@ -906,6 +980,7 @@ var init_runner = __esm({
906
980
  for (const key of [
907
981
  "provider",
908
982
  "model",
983
+ "variant",
909
984
  "maxTurns",
910
985
  "maxBudgetUsd",
911
986
  "maxRetries",
@@ -985,7 +1060,8 @@ var init_runner = __esm({
985
1060
  numTurns: raw.metrics.numTurns,
986
1061
  durationMs: Date.now() - startTime,
987
1062
  sessionId: raw.metrics.sessionId,
988
- messages: raw.messages
1063
+ messages: raw.messages,
1064
+ ...tokenMetrics(raw)
989
1065
  });
990
1066
  }
991
1067
  return createHarnessResult({
@@ -996,7 +1072,8 @@ var init_runner = __esm({
996
1072
  numTurns: raw.metrics.numTurns,
997
1073
  durationMs: Date.now() - startTime,
998
1074
  sessionId: raw.metrics.sessionId,
999
- messages: raw.messages
1075
+ messages: raw.messages,
1076
+ ...tokenMetrics(raw)
1000
1077
  });
1001
1078
  }
1002
1079
  async buildProvider(providerName, options) {
@@ -1553,6 +1630,134 @@ function applyTriggerTransform(triggerContext, bindings, input) {
1553
1630
  }
1554
1631
  return input;
1555
1632
  }
1633
+
1634
+ // src/usage/costTracker.ts
1635
+ var USAGE_ENVELOPE_KEY = "__agentfield_usage__";
1636
+ function deriveProvider(model) {
1637
+ if (!model) return null;
1638
+ const slug = String(model).trim();
1639
+ const slash = slug.indexOf("/");
1640
+ if (slash < 0) return null;
1641
+ return slug.slice(0, slash).toLowerCase() || null;
1642
+ }
1643
+ function toCount(value) {
1644
+ if (typeof value !== "number" || !Number.isFinite(value)) return 0;
1645
+ return Math.trunc(value);
1646
+ }
1647
+ function toCost(value) {
1648
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
1649
+ return value;
1650
+ }
1651
+ function round6(value) {
1652
+ return Math.round(value * 1e6) / 1e6;
1653
+ }
1654
+ var CostTracker = class {
1655
+ entries = [];
1656
+ /**
1657
+ * Record a single call's usage.
1658
+ *
1659
+ * Cost is optional: a call with known token counts but unknown price is
1660
+ * still recorded (`costUsd: null`) so tokens are never discarded.
1661
+ */
1662
+ record(init) {
1663
+ this.entries.push({
1664
+ model: init.model,
1665
+ inputTokens: toCount(init.inputTokens),
1666
+ outputTokens: toCount(init.outputTokens),
1667
+ totalTokens: toCount(init.totalTokens),
1668
+ costUsd: toCost(init.costUsd),
1669
+ reasonerName: init.reasonerName ?? null,
1670
+ source: init.source ?? "llm",
1671
+ provider: init.provider ?? deriveProvider(init.model),
1672
+ harness: init.harness ?? null,
1673
+ cacheReadTokens: toCount(init.cacheReadTokens),
1674
+ cacheCreationTokens: toCount(init.cacheCreationTokens),
1675
+ costSource: init.costSource ?? null
1676
+ });
1677
+ }
1678
+ /** Total accumulated cost in USD (unknown costs count as zero). */
1679
+ get totalCostUsd() {
1680
+ return this.entries.reduce((sum, e) => sum + (e.costUsd ?? 0), 0);
1681
+ }
1682
+ /** Total tokens used across all calls (per-entry total, no fallback). */
1683
+ get totalTokens() {
1684
+ return this.entries.reduce((sum, e) => sum + e.totalTokens, 0);
1685
+ }
1686
+ /** Number of calls tracked. */
1687
+ get callCount() {
1688
+ return this.entries.length;
1689
+ }
1690
+ get hasEntries() {
1691
+ return this.entries.length > 0;
1692
+ }
1693
+ /**
1694
+ * Return the transport contract form attached to execution envelopes.
1695
+ *
1696
+ * Matches the Python SDK's `CostTracker.serialize()` byte-for-byte in shape:
1697
+ * unset string fields serialize as null, `total_cost_usd` is null when no
1698
+ * entry had a known cost, and a per-entry `total_tokens` of zero falls back
1699
+ * to input + output.
1700
+ */
1701
+ serialize() {
1702
+ const entries = [];
1703
+ let totalInput = 0;
1704
+ let totalOutput = 0;
1705
+ let totalTokens = 0;
1706
+ let totalCost = 0;
1707
+ let anyCost = false;
1708
+ for (const e of this.entries) {
1709
+ const entryTotal = e.totalTokens || e.inputTokens + e.outputTokens;
1710
+ entries.push({
1711
+ source: e.source,
1712
+ provider: e.provider,
1713
+ model: e.model,
1714
+ harness: e.harness,
1715
+ reasoner: e.reasonerName,
1716
+ input_tokens: e.inputTokens,
1717
+ output_tokens: e.outputTokens,
1718
+ cache_read_tokens: e.cacheReadTokens,
1719
+ cache_creation_tokens: e.cacheCreationTokens,
1720
+ total_tokens: entryTotal,
1721
+ cost_usd: e.costUsd,
1722
+ cost_source: e.costSource
1723
+ });
1724
+ totalInput += e.inputTokens;
1725
+ totalOutput += e.outputTokens;
1726
+ totalTokens += entryTotal;
1727
+ if (e.costUsd !== null) {
1728
+ totalCost += e.costUsd;
1729
+ anyCost = true;
1730
+ }
1731
+ }
1732
+ return {
1733
+ total_cost_usd: anyCost ? round6(totalCost) : null,
1734
+ total_input_tokens: totalInput,
1735
+ total_output_tokens: totalOutput,
1736
+ total_tokens: totalTokens,
1737
+ entries
1738
+ };
1739
+ }
1740
+ /** Clear all tracked entries. */
1741
+ reset() {
1742
+ this.entries = [];
1743
+ }
1744
+ };
1745
+ function usageSummaryOrNull(tracker) {
1746
+ if (!tracker || !tracker.hasEntries) return null;
1747
+ return tracker.serialize();
1748
+ }
1749
+ function isPlainObject(value) {
1750
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1751
+ const proto = Object.getPrototypeOf(value);
1752
+ return proto === Object.prototype || proto === null;
1753
+ }
1754
+ function attachUsageToSyncResult(result, tracker) {
1755
+ const usage = usageSummaryOrNull(tracker);
1756
+ if (usage === null || !isPlainObject(result)) return result;
1757
+ return { ...result, [USAGE_ENVELOPE_KEY]: usage };
1758
+ }
1759
+
1760
+ // src/context/ExecutionContext.ts
1556
1761
  var store = new AsyncLocalStorage();
1557
1762
  var ExecutionContext = class {
1558
1763
  input;
@@ -1560,12 +1765,21 @@ var ExecutionContext = class {
1560
1765
  req;
1561
1766
  res;
1562
1767
  agent;
1768
+ /**
1769
+ * Per-execution LLM/harness usage accumulator. Each top-level execution
1770
+ * binds a fresh tracker (isolated across concurrent executions via the
1771
+ * AsyncLocalStorage this context lives in); nested local `agent.call()`
1772
+ * executions inherit the parent's tracker so their usage rolls up into the
1773
+ * parent's report.
1774
+ */
1775
+ costTracker;
1563
1776
  constructor(params) {
1564
1777
  this.input = params.input;
1565
1778
  this.metadata = params.metadata;
1566
1779
  this.req = params.req;
1567
1780
  this.res = params.res;
1568
1781
  this.agent = params.agent;
1782
+ this.costTracker = params.costTracker ?? new CostTracker();
1569
1783
  }
1570
1784
  get logger() {
1571
1785
  return this.agent.getExecutionLogger();
@@ -1577,6 +1791,79 @@ var ExecutionContext = class {
1577
1791
  return store.getStore();
1578
1792
  }
1579
1793
  };
1794
+
1795
+ // src/usage/aiUsage.ts
1796
+ function isRecord(value) {
1797
+ return typeof value === "object" && value !== null;
1798
+ }
1799
+ function num(value) {
1800
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1801
+ }
1802
+ function extractTokenUsage(usage) {
1803
+ if (!isRecord(usage)) {
1804
+ return { inputTokens: 0, outputTokens: 0, totalTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0 };
1805
+ }
1806
+ const inputDetails = isRecord(usage.inputTokenDetails) ? usage.inputTokenDetails : void 0;
1807
+ return {
1808
+ inputTokens: num(usage.inputTokens),
1809
+ outputTokens: num(usage.outputTokens),
1810
+ totalTokens: num(usage.totalTokens),
1811
+ // `cachedInputTokens` is the deprecated pre-details alias — keep it as a
1812
+ // fallback so older provider adapters still report cache reads.
1813
+ cacheReadTokens: num(inputDetails?.cacheReadTokens) || num(usage.cachedInputTokens),
1814
+ cacheCreationTokens: num(inputDetails?.cacheWriteTokens)
1815
+ };
1816
+ }
1817
+ function costFromUsageRaw(usage) {
1818
+ if (!isRecord(usage) || !isRecord(usage.raw)) return null;
1819
+ const cost = usage.raw.cost;
1820
+ return typeof cost === "number" && Number.isFinite(cost) && cost >= 0 ? cost : null;
1821
+ }
1822
+ function costFromProviderMetadata(providerMetadata) {
1823
+ if (!isRecord(providerMetadata)) return null;
1824
+ const openrouter = providerMetadata.openrouter;
1825
+ if (!isRecord(openrouter) || !isRecord(openrouter.usage)) return null;
1826
+ const cost = openrouter.usage.cost;
1827
+ return typeof cost === "number" && Number.isFinite(cost) && cost >= 0 ? cost : null;
1828
+ }
1829
+ function extractProviderCostUsd(source) {
1830
+ if (Array.isArray(source.steps) && source.steps.length > 0) {
1831
+ let sum = 0;
1832
+ let any = false;
1833
+ for (const step of source.steps) {
1834
+ const cost = costFromUsageRaw(isRecord(step) ? step.usage : void 0);
1835
+ if (cost !== null) {
1836
+ sum += cost;
1837
+ any = true;
1838
+ }
1839
+ }
1840
+ if (any) return sum;
1841
+ }
1842
+ return costFromUsageRaw(source.totalUsage) ?? costFromUsageRaw(source.usage) ?? costFromProviderMetadata(source.providerMetadata);
1843
+ }
1844
+ function recordAiSdkUsage(params) {
1845
+ try {
1846
+ const current = ExecutionContext.getCurrent();
1847
+ const tracker = params.tracker ?? current?.costTracker;
1848
+ if (!tracker) return;
1849
+ const tokens = extractTokenUsage(params.source.totalUsage ?? params.source.usage);
1850
+ const cost = extractProviderCostUsd(params.source);
1851
+ const hasTokens = tokens.inputTokens > 0 || tokens.outputTokens > 0 || tokens.totalTokens > 0 || tokens.cacheReadTokens > 0 || tokens.cacheCreationTokens > 0;
1852
+ if (!hasTokens && cost === null) return;
1853
+ tracker.record({
1854
+ model: params.model,
1855
+ ...tokens,
1856
+ costUsd: cost,
1857
+ costSource: cost !== null ? "provider" : null,
1858
+ reasonerName: params.reasonerName !== void 0 ? params.reasonerName : current?.metadata.reasonerId ?? null,
1859
+ source: "llm",
1860
+ provider: params.provider ?? void 0
1861
+ });
1862
+ } catch {
1863
+ }
1864
+ }
1865
+
1866
+ // src/ai/ToolCalling.ts
1580
1867
  var DEFAULT_MAX_TURNS = 10;
1581
1868
  var DEFAULT_MAX_TOOL_CALLS = 25;
1582
1869
  var DEFAULT_HEALTH_STATUS = void 0;
@@ -1794,9 +2081,15 @@ function wrapToolsWithObservability(toolMap, agent, trace, maxToolCalls, getCurr
1794
2081
  }
1795
2082
  return { tools: observableTools, getTotalCalls: () => totalCalls };
1796
2083
  }
1797
- async function executeToolCallLoop(agent, prompt, toolMap, config, needsLazyHydration, buildModel, options = {}) {
2084
+ async function executeToolCallLoop(agent, prompt, toolMap, config, needsLazyHydration, buildModel, options = {}, modelChoice) {
1798
2085
  const maxTurns = config.maxTurns ?? DEFAULT_MAX_TURNS;
1799
2086
  const maxToolCalls = config.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS;
2087
+ const usageModel = modelChoice?.modelName ?? options.model;
2088
+ const recordLoopUsage = (result2) => {
2089
+ if (usageModel) {
2090
+ recordAiSdkUsage({ source: result2, model: usageModel, provider: modelChoice?.provider });
2091
+ }
2092
+ };
1800
2093
  const trace = {
1801
2094
  calls: [],
1802
2095
  totalTurns: 0,
@@ -1823,6 +2116,7 @@ async function executeToolCallLoop(agent, prompt, toolMap, config, needsLazyHydr
1823
2116
  stopWhen: stepCountIs(1)
1824
2117
  // Stop after LLM's first response (tool selection)
1825
2118
  });
2119
+ recordLoopUsage(selectionResult);
1826
2120
  const selectedNames = /* @__PURE__ */ new Set();
1827
2121
  for (const step of selectionResult.steps) {
1828
2122
  for (const tc of step.toolCalls) {
@@ -1866,6 +2160,7 @@ async function executeToolCallLoop(agent, prompt, toolMap, config, needsLazyHydr
1866
2160
  trace.totalTurns = currentTurn;
1867
2161
  }
1868
2162
  });
2163
+ recordLoopUsage(result);
1869
2164
  trace.finalResponse = result.text;
1870
2165
  trace.totalTurns = result.steps.length;
1871
2166
  return { text: result.text, trace };
@@ -1893,6 +2188,13 @@ var ReasonerContext = class {
1893
2188
  memory;
1894
2189
  workflow;
1895
2190
  did;
2191
+ /**
2192
+ * Per-execution token/cost usage accumulator. LLM calls made through
2193
+ * `ctx.ai()` / `ctx.aiWithTools()` and harness runs record into it
2194
+ * automatically; reasoner authors may also `record()` custom entries. Its
2195
+ * serialized summary is attached to the execution's terminal report.
2196
+ */
2197
+ costTracker;
1896
2198
  /**
1897
2199
  * AbortSignal that fires when the control plane cancels this execution
1898
2200
  * (per-execution cancel, the bottom-up cancel-tree endpoint, or any
@@ -1930,6 +2232,7 @@ var ReasonerContext = class {
1930
2232
  this.workflow = params.workflow;
1931
2233
  this.did = params.did;
1932
2234
  this.signal = params.signal ?? new AbortController().signal;
2235
+ this.costTracker = params.costTracker ?? ExecutionContext.getCurrent()?.costTracker ?? new CostTracker();
1933
2236
  this.trigger = params.trigger;
1934
2237
  }
1935
2238
  ai(prompt, options) {
@@ -1954,6 +2257,7 @@ var ReasonerContext = class {
1954
2257
  maxTurns: options.maxTurns ?? config.maxTurns ?? 10,
1955
2258
  maxToolCalls: options.maxToolCalls ?? config.maxToolCalls ?? 25
1956
2259
  };
2260
+ const modelChoice = typeof this.aiClient.resolveModelChoice === "function" ? this.aiClient.resolveModelChoice(options) : void 0;
1957
2261
  return executeToolCallLoop(
1958
2262
  this.agent,
1959
2263
  prompt,
@@ -1961,7 +2265,8 @@ var ReasonerContext = class {
1961
2265
  mergedConfig,
1962
2266
  needsLazyHydration,
1963
2267
  () => this.aiClient.getModel(options),
1964
- options
2268
+ options,
2269
+ modelChoice
1965
2270
  );
1966
2271
  }
1967
2272
  aiStream(prompt, options) {
@@ -2026,7 +2331,8 @@ function getCurrentContext() {
2026
2331
  aiClient: agent.getAIClient(),
2027
2332
  memory: agent.getMemoryInterface(metadata),
2028
2333
  workflow: agent.getWorkflowReporter(metadata),
2029
- did: agent.getDidInterface(metadata, input)
2334
+ did: agent.getDidInterface(metadata, input),
2335
+ costTracker: execution.costTracker
2030
2336
  });
2031
2337
  }
2032
2338
 
@@ -2288,6 +2594,26 @@ function toError(error) {
2288
2594
 
2289
2595
  // src/ai/AIClient.ts
2290
2596
  init_openrouterAttribution();
2597
+
2598
+ // src/ai/openrouterUsage.ts
2599
+ function withOpenRouterUsageInclude(baseFetch) {
2600
+ return (input, init) => {
2601
+ const impl = ((...args) => globalThis.fetch(...args));
2602
+ try {
2603
+ if (init && typeof init.body === "string") {
2604
+ const parsed = JSON.parse(init.body);
2605
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && parsed.usage === void 0) {
2606
+ parsed.usage = { include: true };
2607
+ return impl(input, { ...init, body: JSON.stringify(parsed) });
2608
+ }
2609
+ }
2610
+ } catch {
2611
+ }
2612
+ return impl(input, init);
2613
+ };
2614
+ }
2615
+
2616
+ // src/ai/AIClient.ts
2291
2617
  function repairJsonText(text2) {
2292
2618
  let cleaned = text2.trim();
2293
2619
  const codeBlockMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/);
@@ -2322,6 +2648,7 @@ var AIClient = class {
2322
2648
  };
2323
2649
  }
2324
2650
  async generate(prompt, options = {}) {
2651
+ const { provider, modelName } = this.resolveModelChoice(options);
2325
2652
  const model = this.buildModel(options);
2326
2653
  if (options.schema) {
2327
2654
  const schema = options.schema;
@@ -2336,6 +2663,7 @@ var AIClient = class {
2336
2663
  experimental_repairText: async ({ text: text2 }) => repairJsonText(text2)
2337
2664
  });
2338
2665
  const response2 = await this.withRateLimitRetry(call2);
2666
+ recordAiSdkUsage({ source: response2, model: modelName, provider });
2339
2667
  return response2.object;
2340
2668
  }
2341
2669
  const call = async () => generateText({
@@ -2346,8 +2674,13 @@ var AIClient = class {
2346
2674
  maxOutputTokens: options.maxTokens ?? this.config.maxTokens
2347
2675
  });
2348
2676
  const response = await this.withRateLimitRetry(call);
2677
+ recordAiSdkUsage({ source: response, model: modelName, provider });
2349
2678
  return response.text;
2350
2679
  }
2680
+ // NOTE: stream() usage is deliberately NOT captured. The AI SDK's
2681
+ // streamResult.usage/.totalUsage promises "automatically consume the
2682
+ // stream": attaching to them would force full background consumption of a
2683
+ // stream the caller may abandon early, changing stream semantics.
2351
2684
  async stream(prompt, options = {}) {
2352
2685
  const model = this.buildModel(options);
2353
2686
  const streamResult = streamText({
@@ -2386,9 +2719,19 @@ var AIClient = class {
2386
2719
  getModel(options = {}) {
2387
2720
  return this.buildModel(options);
2388
2721
  }
2722
+ /**
2723
+ * Resolve the effective provider/model pair for a request without building
2724
+ * the model. Used by usage tracking to attribute token/cost entries to the
2725
+ * model actually called.
2726
+ */
2727
+ resolveModelChoice(options = {}) {
2728
+ return {
2729
+ provider: options.provider ?? this.config.provider ?? "openai",
2730
+ modelName: options.model ?? this.config.model ?? "gpt-4o"
2731
+ };
2732
+ }
2389
2733
  buildModel(options) {
2390
- const provider = options.provider ?? this.config.provider ?? "openai";
2391
- const modelName = options.model ?? this.config.model ?? "gpt-4o";
2734
+ const { provider, modelName } = this.resolveModelChoice(options);
2392
2735
  const openRouterHeaders = this.openRouterHeaders(provider, modelName);
2393
2736
  switch (provider) {
2394
2737
  case "anthropic": {
@@ -2444,7 +2787,8 @@ var AIClient = class {
2444
2787
  const openrouter = createOpenAI({
2445
2788
  apiKey: this.config.apiKey,
2446
2789
  baseURL: this.config.baseUrl ?? "https://openrouter.ai/api/v1",
2447
- headers: openRouterHeaders
2790
+ headers: openRouterHeaders,
2791
+ fetch: withOpenRouterUsageInclude()
2448
2792
  });
2449
2793
  return openrouter.chat(modelName);
2450
2794
  }
@@ -2461,7 +2805,7 @@ var AIClient = class {
2461
2805
  const openai = createOpenAI({
2462
2806
  apiKey: this.config.apiKey,
2463
2807
  baseURL: this.config.baseUrl,
2464
- ...openRouterHeaders ? { headers: openRouterHeaders } : {}
2808
+ ...openRouterHeaders ? { headers: openRouterHeaders, fetch: withOpenRouterUsageInclude() } : {}
2465
2809
  });
2466
2810
  return openai(modelName);
2467
2811
  }
@@ -3167,6 +3511,7 @@ var AgentFieldClient = class {
3167
3511
  if (payload.result !== void 0) body.result = wrapResult(payload.result);
3168
3512
  if (payload.error !== void 0) body.error = payload.error;
3169
3513
  if (payload.errorDetails !== void 0) body.error_details = payload.errorDetails;
3514
+ if (payload.usage !== void 0) body.usage = payload.usage;
3170
3515
  const bodyStr = JSON.stringify(body);
3171
3516
  const authHeaders = this.didAuthenticator.signRequest(Buffer.from(bodyStr));
3172
3517
  for (let attempt = 0; attempt < maxRetries; attempt++) {
@@ -3386,6 +3731,9 @@ function wrapResult(result) {
3386
3731
  }
3387
3732
  return { result };
3388
3733
  }
3734
+
3735
+ // src/agent/Agent.ts
3736
+ init_modelVariant();
3389
3737
  var MemoryClientBase = class {
3390
3738
  http;
3391
3739
  defaultHeaders;
@@ -4918,7 +5266,54 @@ var Agent = class {
4918
5266
  }
4919
5267
  async harness(prompt, options) {
4920
5268
  const runner = await this.getHarnessRunner();
4921
- return runner.run(prompt, options ?? {});
5269
+ const result = await runner.run(prompt, options ?? {});
5270
+ this.recordHarnessUsage(result, options);
5271
+ return result;
5272
+ }
5273
+ /**
5274
+ * Record a harness run's token/cost usage into the current execution's
5275
+ * tracker. Mirrors the Python SDK's `_record_harness_usage`: a no-op when
5276
+ * the harness reported neither tokens nor cost (the common case for
5277
+ * providers that don't expose usage) so empty entries are never emitted;
5278
+ * cost is threaded even when tokens are unknown, and vice versa. Never
5279
+ * throws — usage capture is best-effort.
5280
+ */
5281
+ recordHarnessUsage(result, options) {
5282
+ try {
5283
+ const current = ExecutionContext.getCurrent();
5284
+ const tracker = current?.costTracker;
5285
+ if (!tracker) return;
5286
+ const inputTokens = result.inputTokens ?? 0;
5287
+ const outputTokens = result.outputTokens ?? 0;
5288
+ const cacheRead = result.cacheReadTokens ?? 0;
5289
+ const cacheCreation = result.cacheCreationTokens ?? 0;
5290
+ const reportedTotal = result.totalTokens ?? 0;
5291
+ const cost = typeof result.costUsd === "number" && Number.isFinite(result.costUsd) ? result.costUsd : null;
5292
+ if (!inputTokens && !outputTokens && !cacheRead && !cacheCreation && !reportedTotal && cost === null) {
5293
+ return;
5294
+ }
5295
+ const providerName = options?.provider ?? this.config.harnessConfig?.provider;
5296
+ const harnessName = providerName ? String(providerName).replace(/-/g, "_") : null;
5297
+ const rawModelName = String(
5298
+ result.model ?? options?.model ?? this.config.harnessConfig?.model ?? providerName ?? "harness"
5299
+ );
5300
+ const modelName = splitModelVariant(rawModelName).model ?? rawModelName;
5301
+ tracker.record({
5302
+ model: modelName,
5303
+ inputTokens,
5304
+ outputTokens,
5305
+ totalTokens: reportedTotal || inputTokens + outputTokens,
5306
+ costUsd: cost,
5307
+ reasonerName: current.metadata.reasonerId ?? null,
5308
+ source: "harness",
5309
+ provider: deriveProvider(modelName),
5310
+ harness: harnessName,
5311
+ cacheReadTokens: cacheRead,
5312
+ cacheCreationTokens: cacheCreation,
5313
+ costSource: cost !== null ? "provider" : null
5314
+ });
5315
+ } catch {
5316
+ }
4922
5317
  }
4923
5318
  getMemoryInterface(metadata) {
4924
5319
  const defaultScope = this.config.memoryConfig?.defaultScope ?? "workflow";
@@ -5111,7 +5506,8 @@ var Agent = class {
5111
5506
  }
5112
5507
  async call(target, input) {
5113
5508
  const { agentId, name } = this.parseTarget(target);
5114
- const parentMetadata = ExecutionContext.getCurrent()?.metadata;
5509
+ const parentContext = ExecutionContext.getCurrent();
5510
+ const parentMetadata = parentContext?.metadata;
5115
5511
  if (!agentId || agentId === this.config.nodeId) {
5116
5512
  const local = this.reasoners.get(name);
5117
5513
  if (!local) throw new Error(`Reasoner not found: ${name}`);
@@ -5142,7 +5538,11 @@ var Agent = class {
5142
5538
  },
5143
5539
  req: dummyReq,
5144
5540
  res: dummyRes,
5145
- agent: this
5541
+ agent: this,
5542
+ // Nested local calls inherit the parent's cost tracker so their LLM /
5543
+ // harness usage rolls up into the parent execution's usage report
5544
+ // (entries still carry the child reasoner's name).
5545
+ costTracker: parentContext?.costTracker
5146
5546
  });
5147
5547
  const startTime = Date.now();
5148
5548
  this.executionLogger.system("agent.call.started", "Local agent call started", {
@@ -5212,7 +5612,8 @@ var Agent = class {
5212
5612
  memory: this.getMemoryInterface(execCtx.metadata),
5213
5613
  workflow: this.getWorkflowReporter(execCtx.metadata),
5214
5614
  did: this.getDidInterface(execCtx.metadata, resolvedInput, name),
5215
- trigger: triggerContext
5615
+ trigger: triggerContext,
5616
+ costTracker: execCtx.costTracker
5216
5617
  })
5217
5618
  );
5218
5619
  this.executionLogger.system("reasoner.completed", "Reasoner execution completed", {
@@ -5914,6 +6315,8 @@ var Agent = class {
5914
6315
  watchdog.catch(() => {
5915
6316
  });
5916
6317
  const completedAt = () => (/* @__PURE__ */ new Date()).toISOString();
6318
+ const usageTracker = new CostTracker();
6319
+ const usage = () => usageSummaryOrNull(usageTracker) ?? void 0;
5917
6320
  try {
5918
6321
  const result = await Promise.race([
5919
6322
  this.runReasoner(reasoner, {
@@ -5921,7 +6324,8 @@ var Agent = class {
5921
6324
  input: params.input,
5922
6325
  metadata: params.metadata,
5923
6326
  respond: false,
5924
- controller
6327
+ controller,
6328
+ costTracker: usageTracker
5925
6329
  }),
5926
6330
  watchdog
5927
6331
  ]);
@@ -5930,7 +6334,8 @@ var Agent = class {
5930
6334
  result,
5931
6335
  durationMs: Date.now() - start,
5932
6336
  completedAt: completedAt(),
5933
- reasoner: reasonerName
6337
+ reasoner: reasonerName,
6338
+ usage: usage()
5934
6339
  });
5935
6340
  } catch (err) {
5936
6341
  const durationMs = Date.now() - start;
@@ -5941,7 +6346,8 @@ var Agent = class {
5941
6346
  errorDetails: { reason: "reasoner_timeout" },
5942
6347
  durationMs,
5943
6348
  completedAt: completedAt(),
5944
- reasoner: reasonerName
6349
+ reasoner: reasonerName,
6350
+ usage: usage()
5945
6351
  });
5946
6352
  } else if (controller.signal.aborted) {
5947
6353
  await this.agentFieldClient.reportExecutionResult(executionId, {
@@ -5950,7 +6356,8 @@ var Agent = class {
5950
6356
  errorDetails: { reason: "cancelled" },
5951
6357
  durationMs,
5952
6358
  completedAt: completedAt(),
5953
- reasoner: reasonerName
6359
+ reasoner: reasonerName,
6360
+ usage: usage()
5954
6361
  });
5955
6362
  } else {
5956
6363
  await this.agentFieldClient.reportExecutionResult(executionId, {
@@ -5959,7 +6366,8 @@ var Agent = class {
5959
6366
  errorDetails: err?.responseData,
5960
6367
  durationMs,
5961
6368
  completedAt: completedAt(),
5962
- reasoner: reasonerName
6369
+ reasoner: reasonerName,
6370
+ usage: usage()
5963
6371
  });
5964
6372
  }
5965
6373
  } finally {
@@ -5975,6 +6383,7 @@ var Agent = class {
5975
6383
  rootWorkflowId: params.metadata.rootWorkflowId ?? params.metadata.workflowId ?? params.metadata.runId ?? params.metadata.executionId,
5976
6384
  reasonerId: params.metadata.reasonerId ?? params.targetName
5977
6385
  };
6386
+ const costTracker = params.costTracker ?? new CostTracker();
5978
6387
  const { input: unwrappedInput, triggerContext } = unwrapEnvelope(params.input);
5979
6388
  let resolvedInput = unwrappedInput;
5980
6389
  if (triggerContext) {
@@ -5986,7 +6395,8 @@ var Agent = class {
5986
6395
  metadata: executionMetadata,
5987
6396
  req,
5988
6397
  res,
5989
- agent: this
6398
+ agent: this,
6399
+ costTracker
5990
6400
  });
5991
6401
  const { controller, release } = this.cancelRegistry.register(
5992
6402
  executionMetadata.executionId,
@@ -6032,7 +6442,8 @@ var Agent = class {
6032
6442
  workflow: this.getWorkflowReporter(executionMetadata),
6033
6443
  did: this.getDidInterface(executionMetadata, resolvedInput, params.targetName),
6034
6444
  signal: controller.signal,
6035
- trigger: triggerContext
6445
+ trigger: triggerContext,
6446
+ costTracker
6036
6447
  });
6037
6448
  const result = await reasoner.handler(ctx);
6038
6449
  this.executionLogger.system("reasoner.completed", "Reasoner execution completed", {
@@ -6051,7 +6462,7 @@ var Agent = class {
6051
6462
  rootWorkflowId: executionMetadata.rootWorkflowId
6052
6463
  });
6053
6464
  if (params.respond && params.res) {
6054
- params.res.json(result);
6465
+ params.res.json(attachUsageToSyncResult(result, costTracker));
6055
6466
  return;
6056
6467
  }
6057
6468
  return result;
@@ -7711,6 +8122,7 @@ function sleep3(ms) {
7711
8122
 
7712
8123
  // src/harness/index.ts
7713
8124
  init_types();
8125
+ init_modelVariant();
7714
8126
  init_factory();
7715
8127
  init_runner();
7716
8128
  async function simulateTrigger(handler, options) {
@@ -7762,6 +8174,6 @@ function loadFixture(source) {
7762
8174
  );
7763
8175
  }
7764
8176
 
7765
- export { ACTIVE_STATUSES, AIClient, Agent, AgentRouter, ApprovalClient, ApprovalResult, Audio, CANONICAL_STATUSES, DIDAuthenticator, DidClient, DidInterface, DidManager, ExecutionContext, ExecutionLogger, ExecutionStatus, File, HEADER_CALLER_DID, HEADER_DID_NONCE, HEADER_DID_SIGNATURE, HEADER_DID_TIMESTAMP, HarnessRunner, Image, JWE_ALG, JWE_ENC, KEY_AGREEMENT_TYPE, MediaProviderError, MediaRouter, MemoryClient, MemoryClientBase, MemoryEventClient, MemoryInterface, MultimodalResponse, OpenRouterMediaProvider, PauseClock, PauseManager, PayloadEncryptionError, RateLimitError, RealtimeSession, ReasonerContext, SUPPORTED_PROVIDERS, SUPPORTED_SESSION_TRANSPORTS, SessionTransportError, SkillContext, StatelessRateLimiter, TERMINAL_STATUSES, Text, Video, WorkflowReporter, applyTriggerTransform, audioFromBase64, audioFromBuffer, audioFromFile, audioFromUrl, buildProvider, buildSessionDefinition, buildToolConfig, capabilitiesToTools, capabilityToMetadataTool, capabilityToTool, createExecutionLogger, createHarnessResult, createMetrics, createMultimodalResponse, createRawResult, decrypt, decryptToString, encryptForDid, encryptToJwk, eventTrigger, executeToolCallLoop, extractKeyAgreementJwk, fileFromBase64, fileFromBuffer, fileFromPath, fileFromUrl, generateX25519KeyPair, getCurrentContext, getCurrentSkillContext, imageFromBase64, imageFromBuffer, imageFromFile, imageFromUrl, installApprovalWebhookRoute, isActive, isExecutionLogBatchPayload, isTerminal, isTriggerEnvelope, loadFixture, normalizeExecutionLogEntry, normalizeSessionTransportValue, normalizeStatus, scheduleTrigger, serializeExecutionLogEntry, simulateSchedule, simulateTrigger, text, triggerToPayload, unwrapEnvelope, validateSessionTransport, videoFromBase64, videoFromBuffer, videoFromFile, videoFromUrl };
8177
+ export { ACTIVE_STATUSES, AIClient, Agent, AgentRouter, ApprovalClient, ApprovalResult, Audio, CANONICAL_STATUSES, CostTracker, DIDAuthenticator, DidClient, DidInterface, DidManager, ExecutionContext, ExecutionLogger, ExecutionStatus, File, HEADER_CALLER_DID, HEADER_DID_NONCE, HEADER_DID_SIGNATURE, HEADER_DID_TIMESTAMP, HarnessRunner, Image, JWE_ALG, JWE_ENC, KEY_AGREEMENT_TYPE, MODEL_VARIANT_SEP, MediaProviderError, MediaRouter, MemoryClient, MemoryClientBase, MemoryEventClient, MemoryInterface, MultimodalResponse, OpenRouterMediaProvider, PauseClock, PauseManager, PayloadEncryptionError, RateLimitError, RealtimeSession, ReasonerContext, SUPPORTED_PROVIDERS, SUPPORTED_SESSION_TRANSPORTS, SessionTransportError, SkillContext, StatelessRateLimiter, TERMINAL_STATUSES, Text, USAGE_ENVELOPE_KEY, Video, WorkflowReporter, applyTriggerTransform, attachUsageToSyncResult, audioFromBase64, audioFromBuffer, audioFromFile, audioFromUrl, buildProvider, buildSessionDefinition, buildToolConfig, capabilitiesToTools, capabilityToMetadataTool, capabilityToTool, createExecutionLogger, createHarnessResult, createMetrics, createMultimodalResponse, createRawResult, decrypt, decryptToString, deriveProvider, encryptForDid, encryptToJwk, eventTrigger, executeToolCallLoop, extractKeyAgreementJwk, fileFromBase64, fileFromBuffer, fileFromPath, fileFromUrl, generateX25519KeyPair, getCurrentContext, getCurrentSkillContext, imageFromBase64, imageFromBuffer, imageFromFile, imageFromUrl, installApprovalWebhookRoute, isActive, isExecutionLogBatchPayload, isTerminal, isTriggerEnvelope, loadFixture, normalizeExecutionLogEntry, normalizeSessionTransportValue, normalizeStatus, resolveModelAndVariant, scheduleTrigger, serializeExecutionLogEntry, simulateSchedule, simulateTrigger, splitModelVariant, text, triggerToPayload, unwrapEnvelope, usageSummaryOrNull, validateSessionTransport, videoFromBase64, videoFromBuffer, videoFromFile, videoFromUrl };
7766
8178
  //# sourceMappingURL=index.js.map
7767
8179
  //# sourceMappingURL=index.js.map