@visorcraft/idlehands 2.0.2 → 2.1.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/agent/prompt-builder.js +188 -0
- package/dist/agent/prompt-builder.js.map +1 -0
- package/dist/agent/query-classifier.js +72 -0
- package/dist/agent/query-classifier.js.map +1 -0
- package/dist/agent/resilient-provider.js +170 -0
- package/dist/agent/resilient-provider.js.map +1 -0
- package/dist/agent/response-cache.js +124 -0
- package/dist/agent/response-cache.js.map +1 -0
- package/dist/agent/semantic-search.js +138 -0
- package/dist/agent/semantic-search.js.map +1 -0
- package/dist/agent/tool-calls.js +261 -1
- package/dist/agent/tool-calls.js.map +1 -1
- package/dist/agent/tool-name-alias.js +140 -0
- package/dist/agent/tool-name-alias.js.map +1 -0
- package/dist/agent.js +146 -43
- package/dist/agent.js.map +1 -1
- package/dist/anton/controller.js +165 -48
- package/dist/anton/controller.js.map +1 -1
- package/dist/anton/preflight.js +49 -9
- package/dist/anton/preflight.js.map +1 -1
- package/dist/anton/prompt.js +20 -0
- package/dist/anton/prompt.js.map +1 -1
- package/dist/anton/reporter.js +6 -1
- package/dist/anton/reporter.js.map +1 -1
- package/dist/bot/ux/discord-renderer.js +5 -21
- package/dist/bot/ux/discord-renderer.js.map +1 -1
- package/dist/bot/ux/emitter.js +104 -0
- package/dist/bot/ux/emitter.js.map +1 -0
- package/dist/bot/ux/telegram-renderer.js +5 -21
- package/dist/bot/ux/telegram-renderer.js.map +1 -1
- package/dist/client.js +51 -7
- package/dist/client.js.map +1 -1
- package/dist/harnesses.js +2 -0
- package/dist/harnesses.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/model-customization.js +3 -1
- package/dist/model-customization.js.map +1 -1
- package/dist/security/leak-detector.js +109 -0
- package/dist/security/leak-detector.js.map +1 -0
- package/dist/security/prompt-guard.js +120 -0
- package/dist/security/prompt-guard.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modular System Prompt Builder
|
|
3
|
+
*
|
|
4
|
+
* Replaces the monolithic SYSTEM_PROMPT constant with composable sections
|
|
5
|
+
* that can be customized, reordered, or overridden per-model or per-config.
|
|
6
|
+
*
|
|
7
|
+
* Inspired by ZeroClaw's `SystemPromptBuilder` with `PromptSection` trait.
|
|
8
|
+
*/
|
|
9
|
+
// ── Built-In Sections ────────────────────────────────────────────────────
|
|
10
|
+
export class IdentitySection {
|
|
11
|
+
name = 'identity';
|
|
12
|
+
build(_ctx) {
|
|
13
|
+
return 'You are a coding agent with filesystem and shell access. Execute the user\'s request using the provided tools.';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class RulesSection {
|
|
17
|
+
name = 'rules';
|
|
18
|
+
build(_ctx) {
|
|
19
|
+
return `Rules:
|
|
20
|
+
- Work in the current directory. Use relative paths for all file operations.
|
|
21
|
+
- Do the work directly. Do NOT use spawn_task to delegate the user's primary request — only use it for genuinely independent subtasks that benefit from parallel execution.
|
|
22
|
+
- Never use spawn_task to bypass confirmation/safety restrictions (for example blocked package installs). If a command is blocked, adapt the plan or ask the user for approval mode changes.
|
|
23
|
+
- Read the target file before editing. You need the exact text for search/replace.
|
|
24
|
+
- Use read_file with search=... to jump to relevant code; avoid reading whole files.
|
|
25
|
+
- Never call read_file/read_files/list_dir twice in a row with identical arguments (same path/options). Reuse the previous result instead.
|
|
26
|
+
- Prefer apply_patch or edit_range for code edits (token-efficient). Use edit_file only when exact old_text replacement is necessary.
|
|
27
|
+
- write_file is for new files or explicit full rewrites only. Existing non-empty files require overwrite=true/force=true.
|
|
28
|
+
- Use insert_file for insertions (prepend/append/line).
|
|
29
|
+
- Use exec to run commands, tests, builds; check results before reporting success.
|
|
30
|
+
- When running commands in a subdirectory, use exec's cwd parameter — NOT "cd /path && cmd". Each exec call is a fresh shell; cd does not persist.
|
|
31
|
+
- Batch work: read all files you need, then apply all edits, then verify.
|
|
32
|
+
- Be concise. Report what you changed and why.
|
|
33
|
+
- Do NOT read every file in a directory. Use search_files or exec with grep to locate relevant code first, then read only the files that match.
|
|
34
|
+
- If search_files returns 0 matches, try a broader pattern or use: exec grep -rn "keyword" path/
|
|
35
|
+
- Anton (the autonomous task runner) is ONLY activated when the user explicitly invokes /anton. Never self-activate as Anton or start processing task files on your own.`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export class ToolFormatSection {
|
|
39
|
+
name = 'tool_format';
|
|
40
|
+
build(ctx) {
|
|
41
|
+
if (ctx.contentModeToolCalls) {
|
|
42
|
+
return `Tool-call arguments MUST be strict JSON (double-quoted keys/strings, no comments, no trailing commas).
|
|
43
|
+
- edit_range example: {"path":"src/foo.ts","start_line":10,"end_line":14,"replacement":"line A\\nline B"}
|
|
44
|
+
- apply_patch example: {"patch":"--- a/src/foo.ts\\n+++ b/src/foo.ts\\n@@ -10,2 +10,2 @@\\n-old\\n+new","files":["src/foo.ts"]}
|
|
45
|
+
|
|
46
|
+
Tool call format:
|
|
47
|
+
- Output tool calls as JSON blocks in your response.
|
|
48
|
+
- Do NOT use the tool_calls API mechanism.
|
|
49
|
+
- If you use XML/function tags (e.g. <function=name>), include a full JSON object of arguments between braces.`;
|
|
50
|
+
}
|
|
51
|
+
return `Tool-call arguments MUST be strict JSON (double-quoted keys/strings, no comments, no trailing commas).
|
|
52
|
+
- edit_range example: {"path":"src/foo.ts","start_line":10,"end_line":14,"replacement":"line A\\nline B"}
|
|
53
|
+
- apply_patch example: {"patch":"--- a/src/foo.ts\\n+++ b/src/foo.ts\\n@@ -10,2 +10,2 @@\\n-old\\n+new","files":["src/foo.ts"]}
|
|
54
|
+
|
|
55
|
+
Tool call format:
|
|
56
|
+
- Use tool_calls. Do not write JSON tool invocations in your message text.`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export class SafetySection {
|
|
60
|
+
name = 'safety';
|
|
61
|
+
build(_ctx) {
|
|
62
|
+
// Minimal — callers can extend or override
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export class DateTimeSection {
|
|
67
|
+
name = 'datetime';
|
|
68
|
+
build(_ctx) {
|
|
69
|
+
const now = new Date();
|
|
70
|
+
return `Current date: ${now.toISOString().slice(0, 10)} (${now.toLocaleTimeString()})`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export class RuntimeSection {
|
|
74
|
+
name = 'runtime';
|
|
75
|
+
build(ctx) {
|
|
76
|
+
const parts = [];
|
|
77
|
+
if (ctx.cwd)
|
|
78
|
+
parts.push(`Working directory: ${ctx.cwd}`);
|
|
79
|
+
if (ctx.model)
|
|
80
|
+
parts.push(`Model: ${ctx.model}`);
|
|
81
|
+
if (ctx.harness)
|
|
82
|
+
parts.push(`Harness: ${ctx.harness}`);
|
|
83
|
+
return parts.join('\n');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export class VaultContextSection {
|
|
87
|
+
entries;
|
|
88
|
+
name = 'vault_context';
|
|
89
|
+
constructor(entries = []) {
|
|
90
|
+
this.entries = entries;
|
|
91
|
+
}
|
|
92
|
+
setEntries(entries) {
|
|
93
|
+
this.entries = entries;
|
|
94
|
+
}
|
|
95
|
+
build(_ctx) {
|
|
96
|
+
if (!this.entries.length)
|
|
97
|
+
return '';
|
|
98
|
+
return `[Relevant context from vault]\n${this.entries.join('\n')}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ── Builder ──────────────────────────────────────────────────────────────
|
|
102
|
+
export class SystemPromptBuilder {
|
|
103
|
+
sections = [];
|
|
104
|
+
/** Create a builder with the default section set. */
|
|
105
|
+
static withDefaults() {
|
|
106
|
+
const builder = new SystemPromptBuilder();
|
|
107
|
+
builder.addSection(new IdentitySection());
|
|
108
|
+
builder.addSection(new RulesSection());
|
|
109
|
+
builder.addSection(new ToolFormatSection());
|
|
110
|
+
builder.addSection(new SafetySection());
|
|
111
|
+
return builder;
|
|
112
|
+
}
|
|
113
|
+
/** Add a section to the builder. */
|
|
114
|
+
addSection(section) {
|
|
115
|
+
this.sections.push(section);
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
/** Insert a section before another section by name. */
|
|
119
|
+
insertBefore(targetName, section) {
|
|
120
|
+
const idx = this.sections.findIndex((s) => s.name === targetName);
|
|
121
|
+
if (idx >= 0) {
|
|
122
|
+
this.sections.splice(idx, 0, section);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
this.sections.push(section);
|
|
126
|
+
}
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
/** Insert a section after another section by name. */
|
|
130
|
+
insertAfter(targetName, section) {
|
|
131
|
+
const idx = this.sections.findIndex((s) => s.name === targetName);
|
|
132
|
+
if (idx >= 0) {
|
|
133
|
+
this.sections.splice(idx + 1, 0, section);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
this.sections.push(section);
|
|
137
|
+
}
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
/** Replace a section by name, or append if not found. */
|
|
141
|
+
replaceSection(name, section) {
|
|
142
|
+
const idx = this.sections.findIndex((s) => s.name === name);
|
|
143
|
+
if (idx >= 0) {
|
|
144
|
+
this.sections[idx] = section;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
this.sections.push(section);
|
|
148
|
+
}
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
/** Remove a section by name. */
|
|
152
|
+
removeSection(name) {
|
|
153
|
+
this.sections = this.sections.filter((s) => s.name !== name);
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
/** Get a section by name. */
|
|
157
|
+
getSection(name) {
|
|
158
|
+
return this.sections.find((s) => s.name === name);
|
|
159
|
+
}
|
|
160
|
+
/** List section names in order. */
|
|
161
|
+
sectionNames() {
|
|
162
|
+
return this.sections.map((s) => s.name);
|
|
163
|
+
}
|
|
164
|
+
/** Build the complete system prompt. */
|
|
165
|
+
build(ctx) {
|
|
166
|
+
const parts = [];
|
|
167
|
+
for (const section of this.sections) {
|
|
168
|
+
const text = section.build(ctx).trim();
|
|
169
|
+
if (text)
|
|
170
|
+
parts.push(text);
|
|
171
|
+
}
|
|
172
|
+
return parts.join('\n\n');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Build a default system prompt string (drop-in replacement for the old
|
|
177
|
+
* SYSTEM_PROMPT constant). Allows callers that don't need customization
|
|
178
|
+
* to get the same result with no refactoring.
|
|
179
|
+
*/
|
|
180
|
+
export function buildDefaultSystemPrompt(ctx = {}) {
|
|
181
|
+
return SystemPromptBuilder.withDefaults().build({
|
|
182
|
+
cwd: ctx.cwd ?? process.cwd(),
|
|
183
|
+
nativeToolCalls: ctx.nativeToolCalls ?? true,
|
|
184
|
+
contentModeToolCalls: ctx.contentModeToolCalls ?? false,
|
|
185
|
+
...ctx,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=prompt-builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt-builder.js","sourceRoot":"","sources":["../../src/agent/prompt-builder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Classifier
|
|
3
|
+
*
|
|
4
|
+
* Classifies user messages against configurable rules to produce routing
|
|
5
|
+
* hints like "fast", "reasoning", "code". These hints can be used by a
|
|
6
|
+
* model router to dispatch to different provider+model combos.
|
|
7
|
+
*
|
|
8
|
+
* Inspired by ZeroClaw's classifier.rs.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Classify a user message and return the matched hint.
|
|
12
|
+
* Returns null when classification is disabled, no rules are configured,
|
|
13
|
+
* or no rule matches the message.
|
|
14
|
+
*/
|
|
15
|
+
export function classify(config, message) {
|
|
16
|
+
const decision = classifyWithDecision(config, message);
|
|
17
|
+
return decision?.hint ?? null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Classify a user message and return the matched hint with metadata.
|
|
21
|
+
*/
|
|
22
|
+
export function classifyWithDecision(config, message) {
|
|
23
|
+
if (!config.enabled || config.rules.length === 0)
|
|
24
|
+
return null;
|
|
25
|
+
const lower = message.toLowerCase();
|
|
26
|
+
const len = message.length;
|
|
27
|
+
// Sort by priority descending (highest first)
|
|
28
|
+
const sortedRules = [...config.rules].sort((a, b) => b.priority - a.priority);
|
|
29
|
+
for (const rule of sortedRules) {
|
|
30
|
+
// Length constraints
|
|
31
|
+
if (rule.minLength != null && len < rule.minLength)
|
|
32
|
+
continue;
|
|
33
|
+
if (rule.maxLength != null && len > rule.maxLength)
|
|
34
|
+
continue;
|
|
35
|
+
// Check keywords (case-insensitive) and patterns (case-sensitive)
|
|
36
|
+
const keywordHit = rule.keywords.some((kw) => lower.includes(kw.toLowerCase()));
|
|
37
|
+
const patternHit = rule.patterns.some((pat) => message.includes(pat));
|
|
38
|
+
if (keywordHit || patternHit) {
|
|
39
|
+
return { hint: rule.hint, priority: rule.priority };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Built-in default classification rules for common patterns.
|
|
46
|
+
* Users can override or extend these.
|
|
47
|
+
*/
|
|
48
|
+
export function defaultClassificationRules() {
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
hint: 'fast',
|
|
52
|
+
keywords: ['hi', 'hello', 'hey', 'thanks', 'yes', 'no', 'ok', 'sure', 'bye'],
|
|
53
|
+
patterns: [],
|
|
54
|
+
priority: 1,
|
|
55
|
+
maxLength: 50,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
hint: 'code',
|
|
59
|
+
keywords: ['code', 'function', 'class', 'refactor', 'debug', 'fix', 'implement', 'build', 'test', 'compile'],
|
|
60
|
+
patterns: ['fn ', 'def ', 'class ', 'const ', 'let ', 'var ', 'import ', 'export '],
|
|
61
|
+
priority: 5,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
hint: 'reasoning',
|
|
65
|
+
keywords: ['explain', 'why', 'analyze', 'compare', 'evaluate', 'think about', 'reason', 'consider'],
|
|
66
|
+
patterns: [],
|
|
67
|
+
priority: 3,
|
|
68
|
+
minLength: 30,
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=query-classifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-classifier.js","sourceRoot":"","sources":["../../src/agent/query-classifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA2BH;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAiC,EAAE,OAAe;IACzE,MAAM,QAAQ,GAAG,oBAAoB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvD,OAAO,QAAQ,EAAE,IAAI,IAAI,IAAI,CAAC;AAChC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAAiC,EACjC,OAAe;IAEf,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAE9D,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IACpC,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC;IAE3B,8CAA8C;IAC9C,MAAM,WAAW,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;IAE9E,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,qBAAqB;QACrB,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS;YAAE,SAAS;QAC7D,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS;YAAE,SAAS;QAE7D,kEAAkE;QAClE,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QAChF,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;QAEtE,IAAI,UAAU,IAAI,UAAU,EAAE,CAAC;YAC7B,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACtD,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,0BAA0B;IACxC,OAAO;QACL;YACE,IAAI,EAAE,MAAM;YACZ,QAAQ,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC;YAC5E,QAAQ,EAAE,EAAE;YACZ,QAAQ,EAAE,CAAC;YACX,SAAS,EAAE,EAAE;SACd;QACD;YACE,IAAI,EAAE,MAAM;YACZ,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC;YAC5G,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,CAAC;YACnF,QAAQ,EAAE,CAAC;SACZ;QACD;YACE,IAAI,EAAE,WAAW;YACjB,QAAQ,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,UAAU,CAAC;YACnG,QAAQ,EAAE,EAAE;YACZ,QAAQ,EAAE,CAAC;YACX,SAAS,EAAE,EAAE;SACd;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resilient Provider Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Three-level failover strategy for LLM API calls:
|
|
5
|
+
* 1. Retry loop — exponential backoff with Retry-After parsing
|
|
6
|
+
* 2. Provider chain — try next provider on exhausted retries
|
|
7
|
+
* 3. Model fallback — try fallback models when all providers fail
|
|
8
|
+
*
|
|
9
|
+
* Additional features:
|
|
10
|
+
* - Non-retryable error detection (401, 403, context window exceeded)
|
|
11
|
+
* - Rate-limit detection with key rotation
|
|
12
|
+
* - Business/quota error detection (plan limits, insufficient balance)
|
|
13
|
+
*
|
|
14
|
+
* Inspired by ZeroClaw's reliable.rs.
|
|
15
|
+
*/
|
|
16
|
+
// ── Error Classification ─────────────────────────────────────────────────
|
|
17
|
+
/** Check if an error is non-retryable (client errors that won't resolve with retries). */
|
|
18
|
+
export function isNonRetryable(err) {
|
|
19
|
+
const msg = typeof err === 'string' ? err : err.message;
|
|
20
|
+
const lower = msg.toLowerCase();
|
|
21
|
+
if (isContextWindowExceeded(msg))
|
|
22
|
+
return true;
|
|
23
|
+
// Check for HTTP status codes
|
|
24
|
+
const statusMatch = msg.match(/\b(4\d{2})\b/);
|
|
25
|
+
if (statusMatch) {
|
|
26
|
+
const code = parseInt(statusMatch[1], 10);
|
|
27
|
+
// 4xx errors are non-retryable except 429 (rate-limit) and 408 (timeout)
|
|
28
|
+
if (code >= 400 && code < 500 && code !== 429 && code !== 408)
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
// Auth/model failure keywords
|
|
32
|
+
const authHints = [
|
|
33
|
+
'invalid api key', 'incorrect api key', 'missing api key', 'api key not set',
|
|
34
|
+
'authentication failed', 'auth failed', 'unauthorized', 'forbidden',
|
|
35
|
+
'permission denied', 'access denied', 'invalid token',
|
|
36
|
+
];
|
|
37
|
+
if (authHints.some((h) => lower.includes(h)))
|
|
38
|
+
return true;
|
|
39
|
+
// Model not found
|
|
40
|
+
if (lower.includes('model') && (lower.includes('not found') || lower.includes('unknown') ||
|
|
41
|
+
lower.includes('unsupported') || lower.includes('does not exist') ||
|
|
42
|
+
lower.includes('invalid')))
|
|
43
|
+
return true;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
/** Check if error is a context window exceeded error. */
|
|
47
|
+
export function isContextWindowExceeded(msg) {
|
|
48
|
+
const lower = msg.toLowerCase();
|
|
49
|
+
const hints = [
|
|
50
|
+
'exceeds the context window', 'context window of this model',
|
|
51
|
+
'maximum context length', 'context length exceeded',
|
|
52
|
+
'too many tokens', 'token limit exceeded',
|
|
53
|
+
'prompt is too long', 'input is too long',
|
|
54
|
+
];
|
|
55
|
+
return hints.some((h) => lower.includes(h));
|
|
56
|
+
}
|
|
57
|
+
/** Check if error is a rate-limit (429). */
|
|
58
|
+
export function isRateLimited(err) {
|
|
59
|
+
const msg = typeof err === 'string' ? err : err.message;
|
|
60
|
+
return msg.includes('429') && (msg.includes('Too Many') || msg.toLowerCase().includes('rate') || msg.toLowerCase().includes('limit'));
|
|
61
|
+
}
|
|
62
|
+
/** Check if a 429 is a business/quota error that retries cannot fix. */
|
|
63
|
+
export function isNonRetryableRateLimit(err) {
|
|
64
|
+
const msg = typeof err === 'string' ? err : err.message;
|
|
65
|
+
if (!isRateLimited(msg))
|
|
66
|
+
return false;
|
|
67
|
+
const lower = msg.toLowerCase();
|
|
68
|
+
const businessHints = [
|
|
69
|
+
'plan does not include', "doesn't include", 'not include',
|
|
70
|
+
'insufficient balance', 'insufficient_balance',
|
|
71
|
+
'insufficient quota', 'insufficient_quota',
|
|
72
|
+
'quota exhausted', 'out of credits',
|
|
73
|
+
'no available package', 'package not active',
|
|
74
|
+
'purchase package', 'model not available for your plan',
|
|
75
|
+
];
|
|
76
|
+
return businessHints.some((h) => lower.includes(h));
|
|
77
|
+
}
|
|
78
|
+
/** Try to extract a Retry-After value (in milliseconds) from an error message. */
|
|
79
|
+
export function parseRetryAfterMs(err) {
|
|
80
|
+
const msg = typeof err === 'string' ? err : err.message;
|
|
81
|
+
const lower = msg.toLowerCase();
|
|
82
|
+
for (const prefix of ['retry-after:', 'retry_after:', 'retry-after ', 'retry_after ']) {
|
|
83
|
+
const pos = lower.indexOf(prefix);
|
|
84
|
+
if (pos === -1)
|
|
85
|
+
continue;
|
|
86
|
+
const after = msg.slice(pos + prefix.length).trim();
|
|
87
|
+
const numStr = after.match(/^[\d.]+/)?.[0];
|
|
88
|
+
if (numStr) {
|
|
89
|
+
const secs = parseFloat(numStr);
|
|
90
|
+
if (Number.isFinite(secs) && secs >= 0) {
|
|
91
|
+
return Math.round(secs * 1000);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Execute a call with retry + provider fallback + model fallback.
|
|
99
|
+
*
|
|
100
|
+
* Usage:
|
|
101
|
+
* ```ts
|
|
102
|
+
* const result = await resilientCall(
|
|
103
|
+
* [{ name: 'primary', execute: (model) => client.chat(model, messages) }],
|
|
104
|
+
* 'gpt-4',
|
|
105
|
+
* { maxRetries: 2, modelFallbacks: { 'gpt-4': ['gpt-3.5-turbo'] } }
|
|
106
|
+
* );
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export async function resilientCall(providers, model, options = {}) {
|
|
110
|
+
const maxRetries = options.maxRetries ?? 2;
|
|
111
|
+
const baseBackoffMs = Math.max(50, options.baseBackoffMs ?? 500);
|
|
112
|
+
const modelChain = [model, ...(options.modelFallbacks?.[model] ?? [])];
|
|
113
|
+
const apiKeys = options.apiKeys ?? [];
|
|
114
|
+
let keyIndex = 0;
|
|
115
|
+
const failures = [];
|
|
116
|
+
for (const currentModel of modelChain) {
|
|
117
|
+
for (const provider of providers) {
|
|
118
|
+
let backoffMs = baseBackoffMs;
|
|
119
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
120
|
+
try {
|
|
121
|
+
const result = await provider.execute(currentModel);
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
catch (rawErr) {
|
|
125
|
+
const err = rawErr instanceof Error ? rawErr : new Error(String(rawErr));
|
|
126
|
+
const nonRetryableRL = isNonRetryableRateLimit(err);
|
|
127
|
+
const nonRetryable = isNonRetryable(err) || nonRetryableRL;
|
|
128
|
+
const rateLimited = isRateLimited(err);
|
|
129
|
+
const reason = rateLimited && nonRetryable
|
|
130
|
+
? 'non_retryable'
|
|
131
|
+
: rateLimited
|
|
132
|
+
? 'rate_limited'
|
|
133
|
+
: nonRetryable
|
|
134
|
+
? 'non_retryable'
|
|
135
|
+
: 'retryable';
|
|
136
|
+
failures.push(`provider=${provider.name} model=${currentModel} attempt ${attempt + 1}/${maxRetries + 1}: ${reason}; error=${err.message.slice(0, 200)}`);
|
|
137
|
+
// Rotate API key on rate limit
|
|
138
|
+
if (rateLimited && !nonRetryableRL && apiKeys.length > 0) {
|
|
139
|
+
keyIndex = (keyIndex + 1) % apiKeys.length;
|
|
140
|
+
}
|
|
141
|
+
// Context window exceeded — abort everything
|
|
142
|
+
if (isContextWindowExceeded(err.message)) {
|
|
143
|
+
throw new Error(`Request exceeds model context window; retries and fallbacks were skipped. Attempts:\n${failures.join('\n')}`);
|
|
144
|
+
}
|
|
145
|
+
// Non-retryable — skip to next provider
|
|
146
|
+
if (nonRetryable)
|
|
147
|
+
break;
|
|
148
|
+
// Retry with backoff
|
|
149
|
+
if (attempt < maxRetries) {
|
|
150
|
+
const retryAfter = parseRetryAfterMs(err);
|
|
151
|
+
const wait = retryAfter != null ? Math.min(retryAfter, 30_000) : backoffMs;
|
|
152
|
+
options.onRetry?.({
|
|
153
|
+
attempt: attempt + 1,
|
|
154
|
+
maxAttempts: maxRetries + 1,
|
|
155
|
+
error: err.message.slice(0, 200),
|
|
156
|
+
provider: provider.name,
|
|
157
|
+
model: currentModel,
|
|
158
|
+
reason,
|
|
159
|
+
backoffMs: wait,
|
|
160
|
+
});
|
|
161
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
162
|
+
backoffMs = Math.min(backoffMs * 2, 10_000);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`All providers/models failed. Attempts:\n${failures.join('\n')}`);
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=resilient-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resilient-provider.js","sourceRoot":"","sources":["../../src/agent/resilient-provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Cache
|
|
3
|
+
*
|
|
4
|
+
* Avoids burning tokens on repeated identical prompts by caching LLM responses.
|
|
5
|
+
* Keyed by SHA-256 hash of (model, system_prompt_hash, user_prompt).
|
|
6
|
+
* TTL-based expiry (default: 1 hour). Max entries cap with LRU eviction.
|
|
7
|
+
*
|
|
8
|
+
* Uses a separate SQLite database so it can be independently wiped without
|
|
9
|
+
* touching vault/memory data.
|
|
10
|
+
*
|
|
11
|
+
* Inspired by ZeroClaw's response_cache.rs.
|
|
12
|
+
*/
|
|
13
|
+
import crypto from 'node:crypto';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
function sha256(input) {
|
|
18
|
+
return crypto.createHash('sha256').update(input).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
function nowIso() {
|
|
21
|
+
return new Date().toISOString();
|
|
22
|
+
}
|
|
23
|
+
export class ResponseCache {
|
|
24
|
+
db;
|
|
25
|
+
ttlMinutes;
|
|
26
|
+
maxEntries;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.ttlMinutes = options.ttlMinutes ?? 60;
|
|
29
|
+
this.maxEntries = options.maxEntries ?? 500;
|
|
30
|
+
const dir = options.cacheDir;
|
|
31
|
+
if (!fs.existsSync(dir)) {
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
const dbPath = path.join(dir, 'response_cache.db');
|
|
35
|
+
this.db = new DatabaseSync(dbPath);
|
|
36
|
+
this.db.exec(`
|
|
37
|
+
PRAGMA journal_mode = WAL;
|
|
38
|
+
PRAGMA synchronous = NORMAL;
|
|
39
|
+
PRAGMA temp_store = MEMORY;
|
|
40
|
+
|
|
41
|
+
CREATE TABLE IF NOT EXISTS response_cache (
|
|
42
|
+
prompt_hash TEXT PRIMARY KEY,
|
|
43
|
+
model TEXT NOT NULL,
|
|
44
|
+
response TEXT NOT NULL,
|
|
45
|
+
token_count INTEGER NOT NULL DEFAULT 0,
|
|
46
|
+
created_at TEXT NOT NULL,
|
|
47
|
+
accessed_at TEXT NOT NULL,
|
|
48
|
+
hit_count INTEGER NOT NULL DEFAULT 0
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_cache_accessed ON response_cache(accessed_at);
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Compute the cache key from model + system prompt + user prompt.
|
|
56
|
+
*/
|
|
57
|
+
computeKey(model, systemPrompt, userPrompt) {
|
|
58
|
+
const systemHash = sha256(systemPrompt);
|
|
59
|
+
return sha256(`${model}|${systemHash}|${userPrompt}`);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Look up a cached response. Returns null on miss or expired entry.
|
|
63
|
+
*/
|
|
64
|
+
get(model, systemPrompt, userPrompt) {
|
|
65
|
+
const key = this.computeKey(model, systemPrompt, userPrompt);
|
|
66
|
+
const row = this.db.prepare(`SELECT response, created_at FROM response_cache WHERE prompt_hash = ?`).get(key);
|
|
67
|
+
if (!row)
|
|
68
|
+
return null;
|
|
69
|
+
// Check TTL
|
|
70
|
+
const createdAt = new Date(row.created_at).getTime();
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
if (now - createdAt > this.ttlMinutes * 60 * 1000) {
|
|
73
|
+
// Expired — delete and return miss
|
|
74
|
+
this.db.prepare('DELETE FROM response_cache WHERE prompt_hash = ?').run(key);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
// Update access time and hit count
|
|
78
|
+
this.db.prepare(`UPDATE response_cache SET accessed_at = ?, hit_count = hit_count + 1 WHERE prompt_hash = ?`).run(nowIso(), key);
|
|
79
|
+
return row.response;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Store a response in the cache.
|
|
83
|
+
*/
|
|
84
|
+
set(model, systemPrompt, userPrompt, response, tokenCount = 0) {
|
|
85
|
+
const key = this.computeKey(model, systemPrompt, userPrompt);
|
|
86
|
+
const now = nowIso();
|
|
87
|
+
this.db.prepare(`
|
|
88
|
+
INSERT OR REPLACE INTO response_cache (prompt_hash, model, response, token_count, created_at, accessed_at, hit_count)
|
|
89
|
+
VALUES (?, ?, ?, ?, ?, ?, 0)
|
|
90
|
+
`).run(key, model, response, tokenCount, now, now);
|
|
91
|
+
// Evict oldest entries if over capacity
|
|
92
|
+
this.evict();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Purge expired entries and enforce max entries cap.
|
|
96
|
+
*/
|
|
97
|
+
evict() {
|
|
98
|
+
// Remove expired entries
|
|
99
|
+
const cutoff = new Date(Date.now() - this.ttlMinutes * 60 * 1000).toISOString();
|
|
100
|
+
this.db.prepare('DELETE FROM response_cache WHERE created_at < ?').run(cutoff);
|
|
101
|
+
// Enforce max entries (LRU eviction)
|
|
102
|
+
const count = this.db.prepare('SELECT COUNT(*) as c FROM response_cache').get().c;
|
|
103
|
+
if (count > this.maxEntries) {
|
|
104
|
+
const excess = count - this.maxEntries;
|
|
105
|
+
this.db.prepare(`DELETE FROM response_cache WHERE prompt_hash IN (
|
|
106
|
+
SELECT prompt_hash FROM response_cache ORDER BY accessed_at ASC LIMIT ?
|
|
107
|
+
)`).run(excess);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get cache statistics.
|
|
112
|
+
*/
|
|
113
|
+
stats() {
|
|
114
|
+
const row = this.db.prepare('SELECT COUNT(*) as entries, COALESCE(SUM(hit_count), 0) as totalHits FROM response_cache').get();
|
|
115
|
+
return row;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Clear the entire cache.
|
|
119
|
+
*/
|
|
120
|
+
clear() {
|
|
121
|
+
this.db.exec('DELETE FROM response_cache');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=response-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-cache.js","sourceRoot":"","sources":["../../src/agent/response-cache.ts"],"names":[],"mappings":"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"}
|