@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.js CHANGED
@@ -35,6 +35,8 @@ __export(index_exports, {
35
35
  AbstractAgent: () => AbstractAgent,
36
36
  AgentFactory: () => AgentFactory,
37
37
  BotResponseError: () => BotResponseError,
38
+ BudgetController: () => BudgetController,
39
+ BudgetExceededError: () => BudgetExceededError,
38
40
  CACHE_TIER_MARKER: () => CACHE_TIER_MARKER,
39
41
  ClaudeAgent: () => ClaudeAgent,
40
42
  DEEPSEEK_PEAK_SCHEDULE: () => DEEPSEEK_PEAK_SCHEDULE,
@@ -52,6 +54,7 @@ __export(index_exports, {
52
54
  GoogleVoiceAgent: () => GoogleVoiceAgent,
53
55
  Gpt5Agent: () => Gpt5Agent,
54
56
  GrokAgent: () => GrokAgent,
57
+ InMemorySpendStore: () => InMemorySpendStore,
55
58
  KimiAgent: () => KimiAgent,
56
59
  LLM_CONSTANTS: () => LLM_CONSTANTS,
57
60
  MESSAGE_ROLE: () => MESSAGE_ROLE,
@@ -78,6 +81,7 @@ __export(index_exports, {
78
81
  VOICE_PROVIDER_API_KEY: () => VOICE_PROVIDER_API_KEY,
79
82
  VoiceAgentFactory: () => VoiceAgentFactory,
80
83
  ZodSchemaConverter: () => ZodSchemaConverter,
84
+ applySpend: () => applySpend,
81
85
  buildGoogleTtsPrompt: () => buildGoogleTtsPrompt,
82
86
  calculateAnthropicCost: () => calculateAnthropicCost,
83
87
  calculateCost: () => calculateCost,
@@ -96,6 +100,7 @@ __export(index_exports, {
96
100
  cleanResponse: () => cleanResponse,
97
101
  createCatalog: () => createCatalog,
98
102
  createVoiceAgent: () => createVoiceAgent,
103
+ evaluateBudget: () => evaluateBudget,
99
104
  extractAnthropicTokenUsage: () => extractAnthropicTokenUsage,
100
105
  extractAnthropicTokenUsageFromResponse: () => extractTokenUsageFromResponse5,
101
106
  extractDeepSeekTokenUsage: () => extractDeepSeekTokenUsage,
@@ -113,6 +118,7 @@ __export(index_exports, {
113
118
  extractOpenAITokenUsageFromResponse: () => extractTokenUsageFromResponse,
114
119
  extractTokenUsage: () => extractTokenUsage,
115
120
  extractUsageAndCalculateCost: () => extractUsageAndCalculateCost,
121
+ firstRefusal: () => firstRefusal,
116
122
  generateGoogleTtsAudio: () => generateGoogleTtsAudio,
117
123
  generateOpenAiTtsAudio: () => generateOpenAiTtsAudio,
118
124
  generateSchemaInstructions: () => generateSchemaInstructions,
@@ -121,10 +127,12 @@ __export(index_exports, {
121
127
  getModelProviderName: () => getModelProviderName,
122
128
  getModelTags: () => getModelTags,
123
129
  getProviderSignatureFields: () => getProviderSignatureFields,
130
+ isBudgetExceededError: () => isBudgetExceededError,
124
131
  isHybridThinkingModel: () => isHybridThinkingModel,
125
132
  isInPeakWindow: () => isInPeakWindow,
126
133
  isPeakBilling: () => isPeakBilling,
127
134
  isWeekendAt: () => isWeekendAt,
135
+ ledgerSpend: () => ledgerSpend,
128
136
  logger: () => logger,
129
137
  mergeThinking: () => mergeThinking,
130
138
  modelHasTag: () => modelHasTag,
@@ -132,6 +140,8 @@ __export(index_exports, {
132
140
  needsPromptBasedSchema: () => needsPromptBasedSchema,
133
141
  parseAndValidateLlmJson: () => parseAndValidateLlmJson,
134
142
  pcmToWav: () => pcmToWav,
143
+ periodEnd: () => periodEnd,
144
+ periodKey: () => periodKey,
135
145
  safeValidateResponse: () => safeValidateResponse,
136
146
  setLlmLogger: () => setLlmLogger,
137
147
  stableHashHex: () => stableHashHex,
@@ -2026,6 +2036,152 @@ function createVoiceAgent(provider, apiKey) {
2026
2036
  return VoiceAgentFactory.createAgent(provider, apiKey);
2027
2037
  }
2028
2038
 
2039
+ // src/budget/index.ts
2040
+ function round6(n) {
2041
+ return parseFloat((Number(n) || 0).toFixed(6));
2042
+ }
2043
+ function periodKey(timestamp, window) {
2044
+ const d = new Date(timestamp);
2045
+ const y = d.getUTCFullYear();
2046
+ const m = String(d.getUTCMonth() + 1).padStart(2, "0");
2047
+ if (window === "month") {
2048
+ return `${y}-${m}`;
2049
+ }
2050
+ const day = String(d.getUTCDate()).padStart(2, "0");
2051
+ return `${y}-${m}-${day}`;
2052
+ }
2053
+ function periodEnd(timestamp, window) {
2054
+ const d = new Date(timestamp);
2055
+ if (window === "month") {
2056
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1);
2057
+ }
2058
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 1);
2059
+ }
2060
+ function normalizeLedger(ledger, period) {
2061
+ if (!ledger || ledger.period !== period) {
2062
+ return { period, totalUSD: 0, buckets: {} };
2063
+ }
2064
+ const buckets = {};
2065
+ for (const [k, v] of Object.entries(ledger.buckets ?? {})) {
2066
+ buckets[k] = round6(v);
2067
+ }
2068
+ return { period, totalUSD: round6(ledger.totalUSD ?? 0), buckets };
2069
+ }
2070
+ function applySpend(ledger, input) {
2071
+ const timestamp = input.timestamp ?? Date.now();
2072
+ const period = periodKey(timestamp, input.window);
2073
+ const current2 = normalizeLedger(ledger, period);
2074
+ const amount = round6(input.amountUSD);
2075
+ if (!(amount > 0)) {
2076
+ return current2;
2077
+ }
2078
+ const buckets = { ...current2.buckets };
2079
+ if (input.bucket) {
2080
+ buckets[input.bucket] = round6((buckets[input.bucket] ?? 0) + amount);
2081
+ }
2082
+ return { period, totalUSD: round6(current2.totalUSD + amount), buckets };
2083
+ }
2084
+ function ledgerSpend(ledger, window, timestamp = Date.now(), bucket) {
2085
+ const current2 = normalizeLedger(ledger, periodKey(timestamp, window));
2086
+ return bucket ? current2.buckets[bucket] ?? 0 : current2.totalUSD;
2087
+ }
2088
+ function evaluateBudget(spentUSD, limit, timestamp = Date.now()) {
2089
+ const spent = round6(spentUSD);
2090
+ const unlimited = !Number.isFinite(limit.limitUSD);
2091
+ const remaining = unlimited ? Number.POSITIVE_INFINITY : Math.max(0, round6(limit.limitUSD - spent));
2092
+ return {
2093
+ allowed: unlimited || spent < limit.limitUSD,
2094
+ window: limit.window,
2095
+ ...limit.bucket ? { bucket: limit.bucket } : {},
2096
+ limitUSD: limit.limitUSD,
2097
+ spentUSD: spent,
2098
+ remainingUSD: remaining,
2099
+ resetsAt: periodEnd(timestamp, limit.window)
2100
+ };
2101
+ }
2102
+ function firstRefusal(verdicts) {
2103
+ return verdicts.find((v) => !v.allowed);
2104
+ }
2105
+ var BudgetExceededError = class _BudgetExceededError extends Error {
2106
+ verdict;
2107
+ subject;
2108
+ constructor(verdict, subject, message) {
2109
+ super(message ?? _BudgetExceededError.describe(verdict));
2110
+ this.name = "BudgetExceededError";
2111
+ this.verdict = verdict;
2112
+ this.subject = subject;
2113
+ }
2114
+ static describe(v) {
2115
+ const when = v.window === "day" ? "daily" : "monthly";
2116
+ return `${when} budget of $${v.limitUSD} exhausted ($${v.spentUSD} spent); resets at ${new Date(v.resetsAt).toISOString()}`;
2117
+ }
2118
+ };
2119
+ function isBudgetExceededError(err) {
2120
+ return err instanceof BudgetExceededError || typeof err === "object" && err !== null && err.name === "BudgetExceededError" && !!err.verdict;
2121
+ }
2122
+ var InMemorySpendStore = class {
2123
+ ledgers = /* @__PURE__ */ new Map();
2124
+ async read(subject) {
2125
+ return { ...this.ledgers.get(subject) ?? {} };
2126
+ }
2127
+ async update(subject, fn) {
2128
+ const next = fn({ ...this.ledgers.get(subject) ?? {} });
2129
+ this.ledgers.set(subject, next);
2130
+ return { ...next };
2131
+ }
2132
+ };
2133
+ var BudgetController = class {
2134
+ constructor(store, options) {
2135
+ this.store = store;
2136
+ this.limits = options.limits;
2137
+ this.bucket = options.bucket;
2138
+ this.clock = options.clock ?? (() => Date.now());
2139
+ }
2140
+ store;
2141
+ limits;
2142
+ bucket;
2143
+ clock;
2144
+ verdicts(ledgers, now) {
2145
+ return this.limits.map((limit) => {
2146
+ const spent = ledgerSpend(ledgers[limit.window], limit.window, now, limit.bucket ?? this.bucket);
2147
+ return evaluateBudget(spent, limit, now);
2148
+ });
2149
+ }
2150
+ async check(subject) {
2151
+ return this.verdicts(await this.store.read(subject), this.clock());
2152
+ }
2153
+ async assertWithinBudget(subject) {
2154
+ const refused = firstRefusal(await this.check(subject));
2155
+ if (refused) {
2156
+ throw new BudgetExceededError(refused, subject);
2157
+ }
2158
+ }
2159
+ /**
2160
+ * Record `amountUSD` against every limited window. Re-checks the limits on the
2161
+ * ledgers as they are at write time and throws `BudgetExceededError` (writing
2162
+ * nothing) if any window is already exhausted.
2163
+ */
2164
+ async record(subject, amountUSD) {
2165
+ const now = this.clock();
2166
+ return this.store.update(subject, (current2) => {
2167
+ const refused = firstRefusal(this.verdicts(current2, now));
2168
+ if (refused) {
2169
+ throw new BudgetExceededError(refused, subject);
2170
+ }
2171
+ const next = { ...current2 };
2172
+ for (const limit of this.limits) {
2173
+ next[limit.window] = applySpend(current2[limit.window], {
2174
+ window: limit.window,
2175
+ amountUSD,
2176
+ bucket: limit.bucket ?? this.bucket,
2177
+ timestamp: now
2178
+ });
2179
+ }
2180
+ return next;
2181
+ });
2182
+ }
2183
+ };
2184
+
2029
2185
  // src/agents/abstract-agent.ts
2030
2186
  var AbstractAgent = class {
2031
2187
  name;
@@ -2176,6 +2332,11 @@ var import_openai3 = __toESM(require("openai"));
2176
2332
  var import_zod2 = require("openai/helpers/zod");
2177
2333
  var Gpt5Agent = class extends AbstractAgent {
2178
2334
  client;
2335
+ // Routing hint for OpenAI's prefix cache (same scheme as the Mistral/Grok agents): one
2336
+ // key per agent+instruction, so an agent's own calls group together instead of every
2337
+ // agent that shares a static prefix hashing to the same route. Keys influence routing
2338
+ // only; they do not guarantee a hit.
2339
+ promptCacheKey;
2179
2340
  // Log message templates
2180
2341
  logTemplates = {
2181
2342
  error: (name, error) => `Error in ${name} agent: ${error}`
@@ -2188,6 +2349,8 @@ var Gpt5Agent = class extends AbstractAgent {
2188
2349
  };
2189
2350
  constructor(name, instruction, model, apiKey, temperature, enableThinking = false, agentLoggingConfig = DEFAULT_LOGGING_CONFIG.agents) {
2190
2351
  super(name, instruction, model, temperature, enableThinking, agentLoggingConfig);
2352
+ this.promptCacheKey = stableHashHex(`${name}
2353
+ ${instruction}`);
2191
2354
  this.client = new import_openai3.default({
2192
2355
  apiKey
2193
2356
  });
@@ -2202,10 +2365,7 @@ var Gpt5Agent = class extends AbstractAgent {
2202
2365
  try {
2203
2366
  this.logAsking(messages);
2204
2367
  this.logMessages(messages);
2205
- const input = [
2206
- `System: ${this.instruction}`,
2207
- ...this.prepareMessages(messages).map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`)
2208
- ].join("\n\n");
2368
+ const input = this.prepareMessages(messages).map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`).join("\n\n");
2209
2369
  const schemaToSend = zodSchema;
2210
2370
  let response;
2211
2371
  try {
@@ -2214,6 +2374,7 @@ var Gpt5Agent = class extends AbstractAgent {
2214
2374
  instructions: this.instruction,
2215
2375
  input,
2216
2376
  max_output_tokens: this.maxOutputTokens,
2377
+ prompt_cache_key: this.promptCacheKey,
2217
2378
  text: {
2218
2379
  format: (0, import_zod2.zodTextFormat)(schemaToSend, "response_schema")
2219
2380
  }
@@ -2292,15 +2453,13 @@ var Gpt5Agent = class extends AbstractAgent {
2292
2453
  try {
2293
2454
  this.logAsking(messages);
2294
2455
  this.logMessages(messages);
2295
- const input = [
2296
- `System: ${this.instruction}`,
2297
- ...this.prepareMessages(messages).map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`)
2298
- ].join("\n\n");
2456
+ const input = this.prepareMessages(messages).map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`).join("\n\n");
2299
2457
  const response = await this.client.responses.create({
2300
2458
  model: this.model,
2301
2459
  instructions: this.instruction,
2302
2460
  input,
2303
- max_output_tokens: this.maxOutputTokens
2461
+ max_output_tokens: this.maxOutputTokens,
2462
+ prompt_cache_key: this.promptCacheKey
2304
2463
  });
2305
2464
  const content = response.output_text;
2306
2465
  if (!content) {
@@ -2356,13 +2515,24 @@ var Gpt5Agent = class extends AbstractAgent {
2356
2515
  var import_sdk = require("@anthropic-ai/sdk");
2357
2516
  var ClaudeAgent = class extends AbstractAgent {
2358
2517
  client;
2518
+ /**
2519
+ * TTL for every breakpoint this agent places (system tiers and the message anchor).
2520
+ * Anthropic bills a 5m write at 1.25x input, a 1h write at 2x, reads at 0.1x, and a read
2521
+ * refreshes the timer on either TTL. Default '1h' because the main consumer runs at human
2522
+ * pace: consecutive calls for one agent measured 12-78 minutes apart, so 5m entries
2523
+ * expired before they were ever read (0-16% hit rate over 30 days, hits only on gaps
2524
+ * under five minutes). Set '5m' for continuous traffic where every call lands inside the
2525
+ * window; there the cheaper write wins. One knob for all breakpoints on purpose: Anthropic
2526
+ * requires 1h entries to precede 5m ones, and a single TTL keeps that trivially true.
2527
+ */
2528
+ cacheTtl = "1h";
2359
2529
  // System-prompt breakpoints, one per cache tier (see CACHE_TIER_MARKER):
2360
- // block 1 shared static rules, byte-identical across all bots and games with the
2361
- // same rule set, so one org-level entry serves everyone and ANY bot's call
2362
- // refreshes its TTL;
2363
- // block 2 per-bot identity + game state + summaries, byte-stable from the start of
2364
- // a game day through the end of its night (deaths/role knowledge/summaries
2365
- // only change in startNewDay), so every call within a day reads it.
2530
+ // block 1 - shared static rules, byte-identical across all bots and games with the
2531
+ // same rule set. Caches are scoped per model, so one entry serves every bot
2532
+ // ON THAT MODEL (not the whole lobby), and any of their calls refreshes it;
2533
+ // block 2 - per-bot identity + game state + summaries, byte-stable between the game's
2534
+ // state writes (a lynch, the night resolution, the summary rewrite, the new
2535
+ // day), so every call inside one of those windows reads it.
2366
2536
  // GM prompts have no marker → single block, same behavior as before. Haiku 4.5 needs a
2367
2537
  // 4096-token cacheable prefix, so tiers below that silently no-op on Haiku — expected.
2368
2538
  // A getter, not a field: `maxOutputTokens` can be raised after construction, and a field
@@ -2370,7 +2540,7 @@ var ClaudeAgent = class extends AbstractAgent {
2370
2540
  get defaultParams() {
2371
2541
  return {
2372
2542
  max_tokens: this.maxOutputTokens,
2373
- system: this.instructionParts.map((part) => ({ type: "text", text: part, cache_control: { type: "ephemeral" } })),
2543
+ system: this.instructionParts.map((part) => ({ type: "text", text: part, cache_control: { type: "ephemeral", ttl: this.cacheTtl } })),
2374
2544
  model: this.model
2375
2545
  };
2376
2546
  }
@@ -2475,14 +2645,14 @@ var ClaudeAgent = class extends AbstractAgent {
2475
2645
  const anchor = messages[messages.length - 2];
2476
2646
  if (typeof anchor.content === "string") {
2477
2647
  if (anchor.content.length > 0) {
2478
- anchor.content = [{ type: "text", text: anchor.content, cache_control: { type: "ephemeral" } }];
2648
+ anchor.content = [{ type: "text", text: anchor.content, cache_control: { type: "ephemeral", ttl: this.cacheTtl } }];
2479
2649
  }
2480
2650
  return;
2481
2651
  }
2482
2652
  for (let i = anchor.content.length - 1; i >= 0; i--) {
2483
2653
  const block = anchor.content[i];
2484
2654
  if (block.type === "text" && block.text.length > 0) {
2485
- block.cache_control = { type: "ephemeral" };
2655
+ block.cache_control = { type: "ephemeral", ttl: this.cacheTtl };
2486
2656
  return;
2487
2657
  }
2488
2658
  }
@@ -4741,6 +4911,8 @@ var AgentFactory = class {
4741
4911
  AbstractAgent,
4742
4912
  AgentFactory,
4743
4913
  BotResponseError,
4914
+ BudgetController,
4915
+ BudgetExceededError,
4744
4916
  CACHE_TIER_MARKER,
4745
4917
  ClaudeAgent,
4746
4918
  DEEPSEEK_PEAK_SCHEDULE,
@@ -4758,6 +4930,7 @@ var AgentFactory = class {
4758
4930
  GoogleVoiceAgent,
4759
4931
  Gpt5Agent,
4760
4932
  GrokAgent,
4933
+ InMemorySpendStore,
4761
4934
  KimiAgent,
4762
4935
  LLM_CONSTANTS,
4763
4936
  MESSAGE_ROLE,
@@ -4784,6 +4957,7 @@ var AgentFactory = class {
4784
4957
  VOICE_PROVIDER_API_KEY,
4785
4958
  VoiceAgentFactory,
4786
4959
  ZodSchemaConverter,
4960
+ applySpend,
4787
4961
  buildGoogleTtsPrompt,
4788
4962
  calculateAnthropicCost,
4789
4963
  calculateCost,
@@ -4802,6 +4976,7 @@ var AgentFactory = class {
4802
4976
  cleanResponse,
4803
4977
  createCatalog,
4804
4978
  createVoiceAgent,
4979
+ evaluateBudget,
4805
4980
  extractAnthropicTokenUsage,
4806
4981
  extractAnthropicTokenUsageFromResponse,
4807
4982
  extractDeepSeekTokenUsage,
@@ -4819,6 +4994,7 @@ var AgentFactory = class {
4819
4994
  extractOpenAITokenUsageFromResponse,
4820
4995
  extractTokenUsage,
4821
4996
  extractUsageAndCalculateCost,
4997
+ firstRefusal,
4822
4998
  generateGoogleTtsAudio,
4823
4999
  generateOpenAiTtsAudio,
4824
5000
  generateSchemaInstructions,
@@ -4827,10 +5003,12 @@ var AgentFactory = class {
4827
5003
  getModelProviderName,
4828
5004
  getModelTags,
4829
5005
  getProviderSignatureFields,
5006
+ isBudgetExceededError,
4830
5007
  isHybridThinkingModel,
4831
5008
  isInPeakWindow,
4832
5009
  isPeakBilling,
4833
5010
  isWeekendAt,
5011
+ ledgerSpend,
4834
5012
  logger,
4835
5013
  mergeThinking,
4836
5014
  modelHasTag,
@@ -4838,6 +5016,8 @@ var AgentFactory = class {
4838
5016
  needsPromptBasedSchema,
4839
5017
  parseAndValidateLlmJson,
4840
5018
  pcmToWav,
5019
+ periodEnd,
5020
+ periodKey,
4841
5021
  safeValidateResponse,
4842
5022
  setLlmLogger,
4843
5023
  stableHashHex,