@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/README.md +22 -0
- package/dist/index.d.mts +154 -1
- package/dist/index.d.ts +154 -1
- package/dist/index.js +198 -18
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +188 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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
|
|
2210
|
-
// same rule set, so one
|
|
2211
|
-
// refreshes
|
|
2212
|
-
// block 2
|
|
2213
|
-
//
|
|
2214
|
-
//
|
|
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,
|