@hiper2d/ai-agents 0.2.0 → 0.3.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.
package/dist/index.mjs CHANGED
@@ -1875,6 +1875,152 @@ function createVoiceAgent(provider, apiKey) {
1875
1875
  return VoiceAgentFactory.createAgent(provider, apiKey);
1876
1876
  }
1877
1877
 
1878
+ // src/budget/index.ts
1879
+ function round6(n) {
1880
+ return parseFloat((Number(n) || 0).toFixed(6));
1881
+ }
1882
+ function periodKey(timestamp, window) {
1883
+ const d = new Date(timestamp);
1884
+ const y = d.getUTCFullYear();
1885
+ const m = String(d.getUTCMonth() + 1).padStart(2, "0");
1886
+ if (window === "month") {
1887
+ return `${y}-${m}`;
1888
+ }
1889
+ const day = String(d.getUTCDate()).padStart(2, "0");
1890
+ return `${y}-${m}-${day}`;
1891
+ }
1892
+ function periodEnd(timestamp, window) {
1893
+ const d = new Date(timestamp);
1894
+ if (window === "month") {
1895
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1);
1896
+ }
1897
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 1);
1898
+ }
1899
+ function normalizeLedger(ledger, period) {
1900
+ if (!ledger || ledger.period !== period) {
1901
+ return { period, totalUSD: 0, buckets: {} };
1902
+ }
1903
+ const buckets = {};
1904
+ for (const [k, v] of Object.entries(ledger.buckets ?? {})) {
1905
+ buckets[k] = round6(v);
1906
+ }
1907
+ return { period, totalUSD: round6(ledger.totalUSD ?? 0), buckets };
1908
+ }
1909
+ function applySpend(ledger, input) {
1910
+ const timestamp = input.timestamp ?? Date.now();
1911
+ const period = periodKey(timestamp, input.window);
1912
+ const current2 = normalizeLedger(ledger, period);
1913
+ const amount = round6(input.amountUSD);
1914
+ if (!(amount > 0)) {
1915
+ return current2;
1916
+ }
1917
+ const buckets = { ...current2.buckets };
1918
+ if (input.bucket) {
1919
+ buckets[input.bucket] = round6((buckets[input.bucket] ?? 0) + amount);
1920
+ }
1921
+ return { period, totalUSD: round6(current2.totalUSD + amount), buckets };
1922
+ }
1923
+ function ledgerSpend(ledger, window, timestamp = Date.now(), bucket) {
1924
+ const current2 = normalizeLedger(ledger, periodKey(timestamp, window));
1925
+ return bucket ? current2.buckets[bucket] ?? 0 : current2.totalUSD;
1926
+ }
1927
+ function evaluateBudget(spentUSD, limit, timestamp = Date.now()) {
1928
+ const spent = round6(spentUSD);
1929
+ const unlimited = !Number.isFinite(limit.limitUSD);
1930
+ const remaining = unlimited ? Number.POSITIVE_INFINITY : Math.max(0, round6(limit.limitUSD - spent));
1931
+ return {
1932
+ allowed: unlimited || spent < limit.limitUSD,
1933
+ window: limit.window,
1934
+ ...limit.bucket ? { bucket: limit.bucket } : {},
1935
+ limitUSD: limit.limitUSD,
1936
+ spentUSD: spent,
1937
+ remainingUSD: remaining,
1938
+ resetsAt: periodEnd(timestamp, limit.window)
1939
+ };
1940
+ }
1941
+ function firstRefusal(verdicts) {
1942
+ return verdicts.find((v) => !v.allowed);
1943
+ }
1944
+ var BudgetExceededError = class _BudgetExceededError extends Error {
1945
+ verdict;
1946
+ subject;
1947
+ constructor(verdict, subject, message) {
1948
+ super(message ?? _BudgetExceededError.describe(verdict));
1949
+ this.name = "BudgetExceededError";
1950
+ this.verdict = verdict;
1951
+ this.subject = subject;
1952
+ }
1953
+ static describe(v) {
1954
+ const when = v.window === "day" ? "daily" : "monthly";
1955
+ return `${when} budget of $${v.limitUSD} exhausted ($${v.spentUSD} spent); resets at ${new Date(v.resetsAt).toISOString()}`;
1956
+ }
1957
+ };
1958
+ function isBudgetExceededError(err) {
1959
+ return err instanceof BudgetExceededError || typeof err === "object" && err !== null && err.name === "BudgetExceededError" && !!err.verdict;
1960
+ }
1961
+ var InMemorySpendStore = class {
1962
+ ledgers = /* @__PURE__ */ new Map();
1963
+ async read(subject) {
1964
+ return { ...this.ledgers.get(subject) ?? {} };
1965
+ }
1966
+ async update(subject, fn) {
1967
+ const next = fn({ ...this.ledgers.get(subject) ?? {} });
1968
+ this.ledgers.set(subject, next);
1969
+ return { ...next };
1970
+ }
1971
+ };
1972
+ var BudgetController = class {
1973
+ constructor(store, options) {
1974
+ this.store = store;
1975
+ this.limits = options.limits;
1976
+ this.bucket = options.bucket;
1977
+ this.clock = options.clock ?? (() => Date.now());
1978
+ }
1979
+ store;
1980
+ limits;
1981
+ bucket;
1982
+ clock;
1983
+ verdicts(ledgers, now) {
1984
+ return this.limits.map((limit) => {
1985
+ const spent = ledgerSpend(ledgers[limit.window], limit.window, now, limit.bucket ?? this.bucket);
1986
+ return evaluateBudget(spent, limit, now);
1987
+ });
1988
+ }
1989
+ async check(subject) {
1990
+ return this.verdicts(await this.store.read(subject), this.clock());
1991
+ }
1992
+ async assertWithinBudget(subject) {
1993
+ const refused = firstRefusal(await this.check(subject));
1994
+ if (refused) {
1995
+ throw new BudgetExceededError(refused, subject);
1996
+ }
1997
+ }
1998
+ /**
1999
+ * Record `amountUSD` against every limited window. Re-checks the limits on the
2000
+ * ledgers as they are at write time and throws `BudgetExceededError` (writing
2001
+ * nothing) if any window is already exhausted.
2002
+ */
2003
+ async record(subject, amountUSD) {
2004
+ const now = this.clock();
2005
+ return this.store.update(subject, (current2) => {
2006
+ const refused = firstRefusal(this.verdicts(current2, now));
2007
+ if (refused) {
2008
+ throw new BudgetExceededError(refused, subject);
2009
+ }
2010
+ const next = { ...current2 };
2011
+ for (const limit of this.limits) {
2012
+ next[limit.window] = applySpend(current2[limit.window], {
2013
+ window: limit.window,
2014
+ amountUSD,
2015
+ bucket: limit.bucket ?? this.bucket,
2016
+ timestamp: now
2017
+ });
2018
+ }
2019
+ return next;
2020
+ });
2021
+ }
2022
+ };
2023
+
1878
2024
  // src/agents/abstract-agent.ts
1879
2025
  var AbstractAgent = class {
1880
2026
  name;
@@ -2025,6 +2171,11 @@ import OpenAI3 from "openai";
2025
2171
  import { zodTextFormat } from "openai/helpers/zod";
2026
2172
  var Gpt5Agent = class extends AbstractAgent {
2027
2173
  client;
2174
+ // Routing hint for OpenAI's prefix cache (same scheme as the Mistral/Grok agents): one
2175
+ // key per agent+instruction, so an agent's own calls group together instead of every
2176
+ // agent that shares a static prefix hashing to the same route. Keys influence routing
2177
+ // only; they do not guarantee a hit.
2178
+ promptCacheKey;
2028
2179
  // Log message templates
2029
2180
  logTemplates = {
2030
2181
  error: (name, error) => `Error in ${name} agent: ${error}`
@@ -2037,6 +2188,8 @@ var Gpt5Agent = class extends AbstractAgent {
2037
2188
  };
2038
2189
  constructor(name, instruction, model, apiKey, temperature, enableThinking = false, agentLoggingConfig = DEFAULT_LOGGING_CONFIG.agents) {
2039
2190
  super(name, instruction, model, temperature, enableThinking, agentLoggingConfig);
2191
+ this.promptCacheKey = stableHashHex(`${name}
2192
+ ${instruction}`);
2040
2193
  this.client = new OpenAI3({
2041
2194
  apiKey
2042
2195
  });
@@ -2051,10 +2204,7 @@ var Gpt5Agent = class extends AbstractAgent {
2051
2204
  try {
2052
2205
  this.logAsking(messages);
2053
2206
  this.logMessages(messages);
2054
- const input = [
2055
- `System: ${this.instruction}`,
2056
- ...this.prepareMessages(messages).map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`)
2057
- ].join("\n\n");
2207
+ const input = this.prepareMessages(messages).map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`).join("\n\n");
2058
2208
  const schemaToSend = zodSchema;
2059
2209
  let response;
2060
2210
  try {
@@ -2063,6 +2213,7 @@ var Gpt5Agent = class extends AbstractAgent {
2063
2213
  instructions: this.instruction,
2064
2214
  input,
2065
2215
  max_output_tokens: this.maxOutputTokens,
2216
+ prompt_cache_key: this.promptCacheKey,
2066
2217
  text: {
2067
2218
  format: zodTextFormat(schemaToSend, "response_schema")
2068
2219
  }
@@ -2141,15 +2292,13 @@ var Gpt5Agent = class extends AbstractAgent {
2141
2292
  try {
2142
2293
  this.logAsking(messages);
2143
2294
  this.logMessages(messages);
2144
- const input = [
2145
- `System: ${this.instruction}`,
2146
- ...this.prepareMessages(messages).map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`)
2147
- ].join("\n\n");
2295
+ const input = this.prepareMessages(messages).map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`).join("\n\n");
2148
2296
  const response = await this.client.responses.create({
2149
2297
  model: this.model,
2150
2298
  instructions: this.instruction,
2151
2299
  input,
2152
- max_output_tokens: this.maxOutputTokens
2300
+ max_output_tokens: this.maxOutputTokens,
2301
+ prompt_cache_key: this.promptCacheKey
2153
2302
  });
2154
2303
  const content = response.output_text;
2155
2304
  if (!content) {
@@ -2205,13 +2354,24 @@ var Gpt5Agent = class extends AbstractAgent {
2205
2354
  import { Anthropic } from "@anthropic-ai/sdk";
2206
2355
  var ClaudeAgent = class extends AbstractAgent {
2207
2356
  client;
2357
+ /**
2358
+ * TTL for every breakpoint this agent places (system tiers and the message anchor).
2359
+ * Anthropic bills a 5m write at 1.25x input, a 1h write at 2x, reads at 0.1x, and a read
2360
+ * refreshes the timer on either TTL. Default '1h' because the main consumer runs at human
2361
+ * pace: consecutive calls for one agent measured 12-78 minutes apart, so 5m entries
2362
+ * expired before they were ever read (0-16% hit rate over 30 days, hits only on gaps
2363
+ * under five minutes). Set '5m' for continuous traffic where every call lands inside the
2364
+ * window; there the cheaper write wins. One knob for all breakpoints on purpose: Anthropic
2365
+ * requires 1h entries to precede 5m ones, and a single TTL keeps that trivially true.
2366
+ */
2367
+ cacheTtl = "1h";
2208
2368
  // System-prompt breakpoints, one per cache tier (see CACHE_TIER_MARKER):
2209
- // block 1 shared static rules, byte-identical across all bots and games with the
2210
- // same rule set, so one org-level entry serves everyone and ANY bot's call
2211
- // refreshes its TTL;
2212
- // block 2 per-bot identity + game state + summaries, byte-stable from the start of
2213
- // a game day through the end of its night (deaths/role knowledge/summaries
2214
- // only change in startNewDay), so every call within a day reads it.
2369
+ // block 1 - shared static rules, byte-identical across all bots and games with the
2370
+ // same rule set. Caches are scoped per model, so one entry serves every bot
2371
+ // ON THAT MODEL (not the whole lobby), and any of their calls refreshes it;
2372
+ // block 2 - per-bot identity + game state + summaries, byte-stable between the game's
2373
+ // state writes (a lynch, the night resolution, the summary rewrite, the new
2374
+ // day), so every call inside one of those windows reads it.
2215
2375
  // GM prompts have no marker → single block, same behavior as before. Haiku 4.5 needs a
2216
2376
  // 4096-token cacheable prefix, so tiers below that silently no-op on Haiku — expected.
2217
2377
  // A getter, not a field: `maxOutputTokens` can be raised after construction, and a field
@@ -2219,7 +2379,7 @@ var ClaudeAgent = class extends AbstractAgent {
2219
2379
  get defaultParams() {
2220
2380
  return {
2221
2381
  max_tokens: this.maxOutputTokens,
2222
- system: this.instructionParts.map((part) => ({ type: "text", text: part, cache_control: { type: "ephemeral" } })),
2382
+ system: this.instructionParts.map((part) => ({ type: "text", text: part, cache_control: { type: "ephemeral", ttl: this.cacheTtl } })),
2223
2383
  model: this.model
2224
2384
  };
2225
2385
  }
@@ -2324,14 +2484,14 @@ var ClaudeAgent = class extends AbstractAgent {
2324
2484
  const anchor = messages[messages.length - 2];
2325
2485
  if (typeof anchor.content === "string") {
2326
2486
  if (anchor.content.length > 0) {
2327
- anchor.content = [{ type: "text", text: anchor.content, cache_control: { type: "ephemeral" } }];
2487
+ anchor.content = [{ type: "text", text: anchor.content, cache_control: { type: "ephemeral", ttl: this.cacheTtl } }];
2328
2488
  }
2329
2489
  return;
2330
2490
  }
2331
2491
  for (let i = anchor.content.length - 1; i >= 0; i--) {
2332
2492
  const block = anchor.content[i];
2333
2493
  if (block.type === "text" && block.text.length > 0) {
2334
- block.cache_control = { type: "ephemeral" };
2494
+ block.cache_control = { type: "ephemeral", ttl: this.cacheTtl };
2335
2495
  return;
2336
2496
  }
2337
2497
  }
@@ -4589,6 +4749,8 @@ export {
4589
4749
  AbstractAgent,
4590
4750
  AgentFactory,
4591
4751
  BotResponseError,
4752
+ BudgetController,
4753
+ BudgetExceededError,
4592
4754
  CACHE_TIER_MARKER,
4593
4755
  ClaudeAgent,
4594
4756
  DEEPSEEK_PEAK_SCHEDULE,
@@ -4606,6 +4768,7 @@ export {
4606
4768
  GoogleVoiceAgent,
4607
4769
  Gpt5Agent,
4608
4770
  GrokAgent,
4771
+ InMemorySpendStore,
4609
4772
  KimiAgent,
4610
4773
  LLM_CONSTANTS,
4611
4774
  MESSAGE_ROLE,
@@ -4632,6 +4795,7 @@ export {
4632
4795
  VOICE_PROVIDER_API_KEY,
4633
4796
  VoiceAgentFactory,
4634
4797
  ZodSchemaConverter,
4798
+ applySpend,
4635
4799
  buildGoogleTtsPrompt,
4636
4800
  calculateAnthropicCost,
4637
4801
  calculateCost,
@@ -4650,6 +4814,7 @@ export {
4650
4814
  cleanResponse,
4651
4815
  createCatalog,
4652
4816
  createVoiceAgent,
4817
+ evaluateBudget,
4653
4818
  extractAnthropicTokenUsage,
4654
4819
  extractTokenUsageFromResponse5 as extractAnthropicTokenUsageFromResponse,
4655
4820
  extractDeepSeekTokenUsage,
@@ -4667,6 +4832,7 @@ export {
4667
4832
  extractTokenUsageFromResponse as extractOpenAITokenUsageFromResponse,
4668
4833
  extractTokenUsage,
4669
4834
  extractUsageAndCalculateCost,
4835
+ firstRefusal,
4670
4836
  generateGoogleTtsAudio,
4671
4837
  generateOpenAiTtsAudio,
4672
4838
  generateSchemaInstructions,
@@ -4675,10 +4841,12 @@ export {
4675
4841
  getModelProviderName,
4676
4842
  getModelTags,
4677
4843
  getProviderSignatureFields,
4844
+ isBudgetExceededError,
4678
4845
  isHybridThinkingModel,
4679
4846
  isInPeakWindow,
4680
4847
  isPeakBilling,
4681
4848
  isWeekendAt,
4849
+ ledgerSpend,
4682
4850
  logger,
4683
4851
  mergeThinking,
4684
4852
  modelHasTag,
@@ -4686,6 +4854,8 @@ export {
4686
4854
  needsPromptBasedSchema,
4687
4855
  parseAndValidateLlmJson,
4688
4856
  pcmToWav,
4857
+ periodEnd,
4858
+ periodKey,
4689
4859
  safeValidateResponse,
4690
4860
  setLlmLogger,
4691
4861
  stableHashHex,