bare-agent 0.16.2 → 0.18.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 +36 -30
- package/bareagent.context.md +60 -1
- package/index.d.ts +7 -1
- package/index.js +12 -1
- package/package.json +5 -4
- package/src/bareguard-adapter.js +22 -4
- package/src/complexity.d.ts +12 -0
- package/src/complexity.js +33 -4
- package/src/evaluator.d.ts +147 -0
- package/src/evaluator.js +270 -0
- package/src/loop.d.ts +75 -4
- package/src/loop.js +233 -44
- package/src/memory.d.ts +10 -3
- package/src/memory.js +15 -4
- package/src/provider-anthropic.d.ts +13 -0
- package/src/provider-anthropic.js +36 -1
- package/src/provider-gemini.d.ts +70 -0
- package/src/provider-gemini.js +197 -0
- package/src/provider-openai.d.ts +10 -0
- package/src/provider-openai.js +20 -4
- package/src/providers.d.ts +2 -1
- package/src/providers.js +3 -0
- package/src/refine.d.ts +86 -0
- package/src/refine.js +65 -0
- package/src/remember.d.ts +118 -0
- package/src/remember.js +163 -0
- package/src/skills.d.ts +71 -0
- package/src/skills.js +200 -0
- package/src/stash.d.ts +44 -0
- package/src/stash.js +342 -0
- package/types/index.d.ts +57 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type Message = import("../types").Message;
|
|
2
|
+
export type ToolDef = import("../types").ToolDef;
|
|
3
|
+
export type ToolCall = import("../types").ToolCall;
|
|
4
|
+
export type GenerateResult = import("../types").GenerateResult;
|
|
5
|
+
export type GeminiOptions = {
|
|
6
|
+
/**
|
|
7
|
+
* - Google AI Studio (Gemini) API key.
|
|
8
|
+
*/
|
|
9
|
+
apiKey?: string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* - Model ID.
|
|
12
|
+
*/
|
|
13
|
+
model?: string | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* - API base (override for proxies; posts to `${baseUrl}/models/${model}:generateContent`).
|
|
16
|
+
*/
|
|
17
|
+
baseUrl?: string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* - Attach the full upstream response to `err.body` on HTTP errors (off by default; `err.message` still carries the API error).
|
|
20
|
+
*/
|
|
21
|
+
exposeErrorBody?: boolean | undefined;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {object} GeminiOptions
|
|
25
|
+
* @property {string} [apiKey] - Google AI Studio (Gemini) API key.
|
|
26
|
+
* @property {string} [model='gemini-2.5-flash'] - Model ID.
|
|
27
|
+
* @property {string} [baseUrl='https://generativelanguage.googleapis.com/v1beta'] - API base (override for proxies; posts to `${baseUrl}/models/${model}:generateContent`).
|
|
28
|
+
* @property {boolean} [exposeErrorBody=false] - Attach the full upstream response to `err.body` on HTTP errors (off by default; `err.message` still carries the API error).
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Google Gemini provider (native `generateContent` API, NOT the OpenAI-compat endpoint — that endpoint
|
|
32
|
+
* omits the cache token tier, so it can't feed the cost meter; verified by POC). Converts the Loop's
|
|
33
|
+
* OpenAI-format messages to Gemini `contents`, declares tools as `functionDeclarations`, and normalizes
|
|
34
|
+
* `usageMetadata` to the neutral Usage shape. Gemini auto-caches (implicit caching on 2.5 models), so the
|
|
35
|
+
* cache-read tier populates with no opt-in.
|
|
36
|
+
*/
|
|
37
|
+
export class GeminiProvider {
|
|
38
|
+
/** @param {GeminiOptions} [options] */
|
|
39
|
+
constructor(options?: GeminiOptions);
|
|
40
|
+
apiKey: string | undefined;
|
|
41
|
+
model: string;
|
|
42
|
+
baseUrl: string;
|
|
43
|
+
exposeErrorBody: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Generate a response from the Gemini API.
|
|
46
|
+
* @param {Message[]} messages - Conversation messages (OpenAI format, auto-converted).
|
|
47
|
+
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
48
|
+
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens).
|
|
49
|
+
* @returns {Promise<GenerateResult>}
|
|
50
|
+
* @throws {Error} `[GeminiProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
51
|
+
*/
|
|
52
|
+
generate(messages: Message[], tools?: ToolDef[], options?: Record<string, any>): Promise<GenerateResult>;
|
|
53
|
+
/**
|
|
54
|
+
* Normalize Gemini `usageMetadata` to the neutral {@link Usage} shape. Like OpenAI, `promptTokenCount`
|
|
55
|
+
* INCLUDES the cached tokens (`cachedContentTokenCount`), so subtract for the uncached remainder
|
|
56
|
+
* (verified live). Gemini bills "thinking" (`thoughtsTokenCount`) as output, so fold it into output
|
|
57
|
+
* (total = prompt + candidates + thoughts — confirmed against live usageMetadata). Implicit caching
|
|
58
|
+
* has no separate write tier → cacheCreationTokens 0.
|
|
59
|
+
* @param {any} u - raw `data.usageMetadata`
|
|
60
|
+
* @returns {import('../types').Usage}
|
|
61
|
+
*/
|
|
62
|
+
_normalizeUsage(u: any): import("../types").Usage;
|
|
63
|
+
/**
|
|
64
|
+
* @param {string} path
|
|
65
|
+
* @param {Record<string, any>} body
|
|
66
|
+
* @returns {Promise<any>}
|
|
67
|
+
*/
|
|
68
|
+
_request(path: string, body: Record<string, any>): Promise<any>;
|
|
69
|
+
_warnedInsecure: boolean | undefined;
|
|
70
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const { ProviderError } = require('./errors');
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('../types').Message} Message */
|
|
8
|
+
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
9
|
+
/** @typedef {import('../types').ToolCall} ToolCall */
|
|
10
|
+
/** @typedef {import('../types').GenerateResult} GenerateResult */
|
|
11
|
+
|
|
12
|
+
/** @param {string} hostname @returns {boolean} */
|
|
13
|
+
function isLoopbackHost(hostname) {
|
|
14
|
+
const h = hostname.replace(/^\[|\]$/g, ''); // strip IPv6 brackets
|
|
15
|
+
return h === 'localhost' || h === '127.0.0.1' || h === '::1' || h.startsWith('127.');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} GeminiOptions
|
|
20
|
+
* @property {string} [apiKey] - Google AI Studio (Gemini) API key.
|
|
21
|
+
* @property {string} [model='gemini-2.5-flash'] - Model ID.
|
|
22
|
+
* @property {string} [baseUrl='https://generativelanguage.googleapis.com/v1beta'] - API base (override for proxies; posts to `${baseUrl}/models/${model}:generateContent`).
|
|
23
|
+
* @property {boolean} [exposeErrorBody=false] - Attach the full upstream response to `err.body` on HTTP errors (off by default; `err.message` still carries the API error).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Google Gemini provider (native `generateContent` API, NOT the OpenAI-compat endpoint — that endpoint
|
|
28
|
+
* omits the cache token tier, so it can't feed the cost meter; verified by POC). Converts the Loop's
|
|
29
|
+
* OpenAI-format messages to Gemini `contents`, declares tools as `functionDeclarations`, and normalizes
|
|
30
|
+
* `usageMetadata` to the neutral Usage shape. Gemini auto-caches (implicit caching on 2.5 models), so the
|
|
31
|
+
* cache-read tier populates with no opt-in.
|
|
32
|
+
*/
|
|
33
|
+
class GeminiProvider {
|
|
34
|
+
/** @param {GeminiOptions} [options] */
|
|
35
|
+
constructor(options = {}) {
|
|
36
|
+
this.apiKey = options.apiKey?.trim();
|
|
37
|
+
this.model = options.model || 'gemini-2.5-flash';
|
|
38
|
+
this.baseUrl = options.baseUrl || 'https://generativelanguage.googleapis.com/v1beta';
|
|
39
|
+
this.exposeErrorBody = options.exposeErrorBody === true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate a response from the Gemini API.
|
|
44
|
+
* @param {Message[]} messages - Conversation messages (OpenAI format, auto-converted).
|
|
45
|
+
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
46
|
+
* @param {Record<string, any>} [options={}] - Options (temperature, maxTokens).
|
|
47
|
+
* @returns {Promise<GenerateResult>}
|
|
48
|
+
* @throws {Error} `[GeminiProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
49
|
+
*/
|
|
50
|
+
async generate(messages, tools = [], options = {}) {
|
|
51
|
+
const systemParts = [];
|
|
52
|
+
/** @type {any[]} */
|
|
53
|
+
const contents = [];
|
|
54
|
+
// Gemini matches a functionResponse to its call by NAME, but OpenAI tool results carry only the
|
|
55
|
+
// tool_call_id. Track id→name from assistant tool_calls so a following tool message can name itself.
|
|
56
|
+
/** @type {Map<string,string>} */
|
|
57
|
+
const toolNames = new Map();
|
|
58
|
+
|
|
59
|
+
for (const m of messages) {
|
|
60
|
+
if (m.role === 'system') {
|
|
61
|
+
if (typeof m.content === 'string' && m.content) systemParts.push({ text: m.content });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (m.role === 'tool') {
|
|
65
|
+
const id = m.tool_call_id || '';
|
|
66
|
+
const name = toolNames.get(id) || id || 'tool';
|
|
67
|
+
contents.push({ role: 'user', parts: [{ functionResponse: { name, response: { content: m.content } } }] });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
|
|
71
|
+
/** @type {any[]} */
|
|
72
|
+
const parts = [];
|
|
73
|
+
if (m.content) parts.push({ text: m.content });
|
|
74
|
+
for (const tc of m.tool_calls) {
|
|
75
|
+
toolNames.set(tc.id, tc.function.name);
|
|
76
|
+
let args = tc.function.arguments;
|
|
77
|
+
if (typeof args === 'string') { try { args = JSON.parse(args); } catch { args = {}; } }
|
|
78
|
+
parts.push({ functionCall: { name: tc.function.name, args: args || {} } });
|
|
79
|
+
}
|
|
80
|
+
contents.push({ role: 'model', parts });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// plain user / assistant text
|
|
84
|
+
const role = m.role === 'assistant' ? 'model' : 'user';
|
|
85
|
+
contents.push({ role, parts: [{ text: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }] });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @type {Record<string, any>} */
|
|
89
|
+
const body = { contents };
|
|
90
|
+
if (systemParts.length) body.systemInstruction = { parts: systemParts };
|
|
91
|
+
if (tools.length > 0) {
|
|
92
|
+
body.tools = [{
|
|
93
|
+
functionDeclarations: tools.map(t => ({
|
|
94
|
+
name: t.name,
|
|
95
|
+
description: t.description,
|
|
96
|
+
...(t.parameters && { parameters: t.parameters }),
|
|
97
|
+
})),
|
|
98
|
+
}];
|
|
99
|
+
}
|
|
100
|
+
/** @type {Record<string, any>} */
|
|
101
|
+
const genConfig = {};
|
|
102
|
+
if (options.maxTokens) genConfig.maxOutputTokens = options.maxTokens;
|
|
103
|
+
if (options.temperature != null) genConfig.temperature = options.temperature;
|
|
104
|
+
if (Object.keys(genConfig).length) body.generationConfig = genConfig;
|
|
105
|
+
|
|
106
|
+
const data = await this._request(`/models/${this.model}:generateContent`, body);
|
|
107
|
+
|
|
108
|
+
let text = '';
|
|
109
|
+
/** @type {ToolCall[]} */
|
|
110
|
+
const toolCalls = [];
|
|
111
|
+
let fnSeq = 0;
|
|
112
|
+
const parts = data.candidates?.[0]?.content?.parts || [];
|
|
113
|
+
for (const part of parts) {
|
|
114
|
+
if (typeof part.text === 'string') text += part.text;
|
|
115
|
+
if (part.functionCall) {
|
|
116
|
+
// Gemini gives no call id; synthesize a stable one so the Loop can pair the tool result.
|
|
117
|
+
toolCalls.push({ id: `gemini_call_${fnSeq++}`, name: part.functionCall.name, arguments: part.functionCall.args || {} });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
text,
|
|
123
|
+
toolCalls,
|
|
124
|
+
model: data.modelVersion || this.model,
|
|
125
|
+
usage: this._normalizeUsage(data.usageMetadata),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Normalize Gemini `usageMetadata` to the neutral {@link Usage} shape. Like OpenAI, `promptTokenCount`
|
|
131
|
+
* INCLUDES the cached tokens (`cachedContentTokenCount`), so subtract for the uncached remainder
|
|
132
|
+
* (verified live). Gemini bills "thinking" (`thoughtsTokenCount`) as output, so fold it into output
|
|
133
|
+
* (total = prompt + candidates + thoughts — confirmed against live usageMetadata). Implicit caching
|
|
134
|
+
* has no separate write tier → cacheCreationTokens 0.
|
|
135
|
+
* @param {any} u - raw `data.usageMetadata`
|
|
136
|
+
* @returns {import('../types').Usage}
|
|
137
|
+
*/
|
|
138
|
+
_normalizeUsage(u) {
|
|
139
|
+
const cacheRead = u?.cachedContentTokenCount || 0;
|
|
140
|
+
return {
|
|
141
|
+
inputTokens: Math.max(0, (u?.promptTokenCount || 0) - cacheRead),
|
|
142
|
+
outputTokens: (u?.candidatesTokenCount || 0) + (u?.thoughtsTokenCount || 0),
|
|
143
|
+
cacheReadTokens: cacheRead,
|
|
144
|
+
cacheCreationTokens: 0,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {string} path
|
|
150
|
+
* @param {Record<string, any>} body
|
|
151
|
+
* @returns {Promise<any>}
|
|
152
|
+
*/
|
|
153
|
+
_request(path, body) {
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
const url = new URL(this.baseUrl + path);
|
|
156
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
157
|
+
const payload = JSON.stringify(body);
|
|
158
|
+
|
|
159
|
+
// Plaintext key to a remote host exposes it on the wire; loopback is the legitimate http case.
|
|
160
|
+
if (this.apiKey && url.protocol === 'http:' && !isLoopbackHost(url.hostname) && !this._warnedInsecure) {
|
|
161
|
+
this._warnedInsecure = true;
|
|
162
|
+
console.warn(`[GeminiProvider] sending x-goog-api-key over PLAINTEXT http to ${url.hostname} — key exposed on the wire. Use https.`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const req = transport.request(url, {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
headers: {
|
|
168
|
+
'Content-Type': 'application/json',
|
|
169
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
170
|
+
...(this.apiKey && { 'x-goog-api-key': this.apiKey }),
|
|
171
|
+
},
|
|
172
|
+
}, (res) => {
|
|
173
|
+
let chunks = '';
|
|
174
|
+
res.on('data', d => (chunks += d));
|
|
175
|
+
res.on('end', () => {
|
|
176
|
+
try {
|
|
177
|
+
const parsed = JSON.parse(chunks);
|
|
178
|
+
if ((res.statusCode ?? 0) >= 400) {
|
|
179
|
+
return reject(new ProviderError(
|
|
180
|
+
`[GeminiProvider] ${parsed.error?.message || `HTTP ${res.statusCode}`}`,
|
|
181
|
+
/** @type {any} */ ({ status: res.statusCode, body: this.exposeErrorBody ? parsed : undefined })
|
|
182
|
+
));
|
|
183
|
+
}
|
|
184
|
+
resolve(parsed);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
reject(new Error(`[GeminiProvider] Invalid JSON response: ${chunks.slice(0, 200)}`));
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
req.on('error', reject);
|
|
191
|
+
req.write(payload);
|
|
192
|
+
req.end();
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { GeminiProvider };
|
package/src/provider-openai.d.ts
CHANGED
|
@@ -44,6 +44,16 @@ export class OpenAIProvider {
|
|
|
44
44
|
* @throws {Error} `[OpenAIProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
45
45
|
*/
|
|
46
46
|
generate(messages: Message[], tools?: ToolDef[], options?: Record<string, any>): Promise<GenerateResult>;
|
|
47
|
+
/**
|
|
48
|
+
* Normalize OpenAI usage to the neutral {@link Usage} shape. OpenAI auto-caches prompt prefixes
|
|
49
|
+
* (>=1024 tokens) and reports the cached portion in `prompt_tokens_details.cached_tokens` —
|
|
50
|
+
* crucially, `prompt_tokens` INCLUDES those cached tokens, so we subtract them to get the uncached
|
|
51
|
+
* remainder (else the cached tokens are double-counted and priced at the full input rate, a ~2x
|
|
52
|
+
* over-charge on a warm prompt). OpenAI has no separate cache-write tier → cacheCreationTokens 0.
|
|
53
|
+
* @param {any} u - raw `data.usage`
|
|
54
|
+
* @returns {import('../types').Usage}
|
|
55
|
+
*/
|
|
56
|
+
_normalizeUsage(u: any): import("../types").Usage;
|
|
47
57
|
/**
|
|
48
58
|
* @param {string} path
|
|
49
59
|
* @param {Record<string, any>} body
|
package/src/provider-openai.js
CHANGED
|
@@ -73,10 +73,26 @@ class OpenAIProvider {
|
|
|
73
73
|
arguments: JSON.parse(tc.function.arguments),
|
|
74
74
|
})),
|
|
75
75
|
model: data.model || this.model,
|
|
76
|
-
usage:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
usage: this._normalizeUsage(data.usage),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Normalize OpenAI usage to the neutral {@link Usage} shape. OpenAI auto-caches prompt prefixes
|
|
82
|
+
* (>=1024 tokens) and reports the cached portion in `prompt_tokens_details.cached_tokens` —
|
|
83
|
+
* crucially, `prompt_tokens` INCLUDES those cached tokens, so we subtract them to get the uncached
|
|
84
|
+
* remainder (else the cached tokens are double-counted and priced at the full input rate, a ~2x
|
|
85
|
+
* over-charge on a warm prompt). OpenAI has no separate cache-write tier → cacheCreationTokens 0.
|
|
86
|
+
* @param {any} u - raw `data.usage`
|
|
87
|
+
* @returns {import('../types').Usage}
|
|
88
|
+
*/
|
|
89
|
+
_normalizeUsage(u) {
|
|
90
|
+
const cacheRead = u?.prompt_tokens_details?.cached_tokens || 0;
|
|
91
|
+
return {
|
|
92
|
+
inputTokens: Math.max(0, (u?.prompt_tokens || 0) - cacheRead),
|
|
93
|
+
outputTokens: u?.completion_tokens || 0,
|
|
94
|
+
cacheReadTokens: cacheRead,
|
|
95
|
+
cacheCreationTokens: 0,
|
|
80
96
|
};
|
|
81
97
|
}
|
|
82
98
|
|
package/src/providers.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OpenAIProvider } from "./provider-openai";
|
|
2
2
|
import { AnthropicProvider } from "./provider-anthropic";
|
|
3
|
+
import { GeminiProvider } from "./provider-gemini";
|
|
3
4
|
import { OllamaProvider } from "./provider-ollama";
|
|
4
5
|
import { CLIPipeProvider } from "./provider-clipipe";
|
|
5
6
|
import { FallbackProvider } from "./provider-fallback";
|
|
6
|
-
export { OpenAIProvider as OpenAI, AnthropicProvider as Anthropic, OllamaProvider as Ollama, CLIPipeProvider as CLIPipe, FallbackProvider as Fallback, OpenAIProvider, AnthropicProvider, OllamaProvider, CLIPipeProvider, FallbackProvider };
|
|
7
|
+
export { OpenAIProvider as OpenAI, AnthropicProvider as Anthropic, GeminiProvider as Gemini, OllamaProvider as Ollama, CLIPipeProvider as CLIPipe, FallbackProvider as Fallback, OpenAIProvider, AnthropicProvider, GeminiProvider, OllamaProvider, CLIPipeProvider, FallbackProvider };
|
package/src/providers.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { OpenAIProvider } = require('./provider-openai');
|
|
4
4
|
const { AnthropicProvider } = require('./provider-anthropic');
|
|
5
|
+
const { GeminiProvider } = require('./provider-gemini');
|
|
5
6
|
const { OllamaProvider } = require('./provider-ollama');
|
|
6
7
|
const { CLIPipeProvider } = require('./provider-clipipe');
|
|
7
8
|
const { FallbackProvider } = require('./provider-fallback');
|
|
@@ -10,6 +11,7 @@ module.exports = {
|
|
|
10
11
|
// Short names (canonical — used throughout docs and the integration guide)
|
|
11
12
|
OpenAI: OpenAIProvider,
|
|
12
13
|
Anthropic: AnthropicProvider,
|
|
14
|
+
Gemini: GeminiProvider,
|
|
13
15
|
Ollama: OllamaProvider,
|
|
14
16
|
CLIPipe: CLIPipeProvider,
|
|
15
17
|
Fallback: FallbackProvider,
|
|
@@ -17,6 +19,7 @@ module.exports = {
|
|
|
17
19
|
// `const { OpenAIProvider } = require('bare-agent/providers')` also works.
|
|
18
20
|
OpenAIProvider,
|
|
19
21
|
AnthropicProvider,
|
|
22
|
+
GeminiProvider,
|
|
20
23
|
OllamaProvider,
|
|
21
24
|
CLIPipeProvider,
|
|
22
25
|
FallbackProvider,
|
package/src/refine.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export type Verdict = import("./evaluator").Verdict;
|
|
2
|
+
export type RefineOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Build one generation. On iteration 0, `lastResult`/`critique` are null. Fresh-feedback (D6/A1) is the
|
|
5
|
+
* consumer's to realize here: seed a NEW Loop with `{goal + critique}` rather than continuing the failed
|
|
6
|
+
* transcript — anchoring on a wrong answer defeats the independent verifier.
|
|
7
|
+
*/
|
|
8
|
+
attempt: (args: {
|
|
9
|
+
iteration: number;
|
|
10
|
+
lastResult: any;
|
|
11
|
+
critique: string | null;
|
|
12
|
+
contract: string | null;
|
|
13
|
+
}) => any;
|
|
14
|
+
/**
|
|
15
|
+
* Judge a result — typically `evaluator.evaluate(goal, result, { rubric, contract })`.
|
|
16
|
+
*/
|
|
17
|
+
evaluate: (result: any, ctx: {
|
|
18
|
+
iteration: number;
|
|
19
|
+
contract: string | null;
|
|
20
|
+
}) => (Verdict | Promise<Verdict>);
|
|
21
|
+
/**
|
|
22
|
+
* - The shared definition of done (A3/D10), forwarded to BOTH `attempt` and
|
|
23
|
+
* `evaluate` so generator and critic agree on what success means.
|
|
24
|
+
*/
|
|
25
|
+
contract?: string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* - Hard cap. The REAL bound is bareguard maxTurns / budget.
|
|
28
|
+
*/
|
|
29
|
+
maxIterations?: number | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* - Stop on the first satisfied verdict.
|
|
32
|
+
*/
|
|
33
|
+
stopOnPass?: boolean | undefined;
|
|
34
|
+
};
|
|
35
|
+
export type RefineOutcome = {
|
|
36
|
+
/**
|
|
37
|
+
* - The last (best available) result.
|
|
38
|
+
*/
|
|
39
|
+
result: any;
|
|
40
|
+
/**
|
|
41
|
+
* - Its verdict.
|
|
42
|
+
*/
|
|
43
|
+
verdict: Verdict | null;
|
|
44
|
+
/**
|
|
45
|
+
* - How many attempts ran.
|
|
46
|
+
*/
|
|
47
|
+
iterations: number;
|
|
48
|
+
/**
|
|
49
|
+
* - Every attempt + verdict, in order.
|
|
50
|
+
*/
|
|
51
|
+
history: Array<{
|
|
52
|
+
result: any;
|
|
53
|
+
verdict: Verdict;
|
|
54
|
+
}>;
|
|
55
|
+
};
|
|
56
|
+
/** @typedef {import('./evaluator').Verdict} Verdict */
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {object} RefineOptions
|
|
59
|
+
* @property {(args: {iteration: number, lastResult: any, critique: string|null, contract: string|null}) => any} attempt
|
|
60
|
+
* Build one generation. On iteration 0, `lastResult`/`critique` are null. Fresh-feedback (D6/A1) is the
|
|
61
|
+
* consumer's to realize here: seed a NEW Loop with `{goal + critique}` rather than continuing the failed
|
|
62
|
+
* transcript — anchoring on a wrong answer defeats the independent verifier.
|
|
63
|
+
* @property {(result: any, ctx: {iteration: number, contract: string|null}) => (Verdict | Promise<Verdict>)} evaluate
|
|
64
|
+
* Judge a result — typically `evaluator.evaluate(goal, result, { rubric, contract })`.
|
|
65
|
+
* @property {string} [contract] - The shared definition of done (A3/D10), forwarded to BOTH `attempt` and
|
|
66
|
+
* `evaluate` so generator and critic agree on what success means.
|
|
67
|
+
* @property {number} [maxIterations=3] - Hard cap. The REAL bound is bareguard maxTurns / budget.
|
|
68
|
+
* @property {boolean} [stopOnPass=true] - Stop on the first satisfied verdict.
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* @typedef {object} RefineOutcome
|
|
72
|
+
* @property {any} result - The last (best available) result.
|
|
73
|
+
* @property {Verdict|null} verdict - Its verdict.
|
|
74
|
+
* @property {number} iterations - How many attempts ran.
|
|
75
|
+
* @property {Array<{result: any, verdict: Verdict}>} history - Every attempt + verdict, in order.
|
|
76
|
+
*/
|
|
77
|
+
/**
|
|
78
|
+
* Thin, Loop-agnostic generate → evaluate → regenerate loop (a port of Managed Agents "Outcomes":
|
|
79
|
+
* iterate → grade → revise, bounded). Knows nothing about providers or `loop.js`; it drives caller-supplied
|
|
80
|
+
* `attempt`/`evaluate` until a satisfied verdict, a terminal `failed`, or `maxIterations`. A `HaltError`
|
|
81
|
+
* thrown by either callback (e.g. a governance cap) propagates as a clean exit — never caught here.
|
|
82
|
+
*
|
|
83
|
+
* @param {RefineOptions} options
|
|
84
|
+
* @returns {Promise<RefineOutcome>}
|
|
85
|
+
*/
|
|
86
|
+
export function refine(options: RefineOptions): Promise<RefineOutcome>;
|
package/src/refine.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** @typedef {import('./evaluator').Verdict} Verdict */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {object} RefineOptions
|
|
7
|
+
* @property {(args: {iteration: number, lastResult: any, critique: string|null, contract: string|null}) => any} attempt
|
|
8
|
+
* Build one generation. On iteration 0, `lastResult`/`critique` are null. Fresh-feedback (D6/A1) is the
|
|
9
|
+
* consumer's to realize here: seed a NEW Loop with `{goal + critique}` rather than continuing the failed
|
|
10
|
+
* transcript — anchoring on a wrong answer defeats the independent verifier.
|
|
11
|
+
* @property {(result: any, ctx: {iteration: number, contract: string|null}) => (Verdict | Promise<Verdict>)} evaluate
|
|
12
|
+
* Judge a result — typically `evaluator.evaluate(goal, result, { rubric, contract })`.
|
|
13
|
+
* @property {string} [contract] - The shared definition of done (A3/D10), forwarded to BOTH `attempt` and
|
|
14
|
+
* `evaluate` so generator and critic agree on what success means.
|
|
15
|
+
* @property {number} [maxIterations=3] - Hard cap. The REAL bound is bareguard maxTurns / budget.
|
|
16
|
+
* @property {boolean} [stopOnPass=true] - Stop on the first satisfied verdict.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} RefineOutcome
|
|
21
|
+
* @property {any} result - The last (best available) result.
|
|
22
|
+
* @property {Verdict|null} verdict - Its verdict.
|
|
23
|
+
* @property {number} iterations - How many attempts ran.
|
|
24
|
+
* @property {Array<{result: any, verdict: Verdict}>} history - Every attempt + verdict, in order.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Thin, Loop-agnostic generate → evaluate → regenerate loop (a port of Managed Agents "Outcomes":
|
|
29
|
+
* iterate → grade → revise, bounded). Knows nothing about providers or `loop.js`; it drives caller-supplied
|
|
30
|
+
* `attempt`/`evaluate` until a satisfied verdict, a terminal `failed`, or `maxIterations`. A `HaltError`
|
|
31
|
+
* thrown by either callback (e.g. a governance cap) propagates as a clean exit — never caught here.
|
|
32
|
+
*
|
|
33
|
+
* @param {RefineOptions} options
|
|
34
|
+
* @returns {Promise<RefineOutcome>}
|
|
35
|
+
*/
|
|
36
|
+
async function refine(options) {
|
|
37
|
+
const { attempt, evaluate } = options;
|
|
38
|
+
if (typeof attempt !== 'function') throw new Error('[refine] requires an attempt(args) function');
|
|
39
|
+
if (typeof evaluate !== 'function') throw new Error('[refine] requires an evaluate(result, ctx) function');
|
|
40
|
+
const contract = typeof options.contract === 'string' ? options.contract : null;
|
|
41
|
+
const mi = options.maxIterations;
|
|
42
|
+
const maxIterations = typeof mi === 'number' && Number.isInteger(mi) && mi > 0 ? mi : 3;
|
|
43
|
+
const stopOnPass = options.stopOnPass !== false;
|
|
44
|
+
|
|
45
|
+
/** @type {Array<{result: any, verdict: Verdict}>} */
|
|
46
|
+
const history = [];
|
|
47
|
+
let lastResult = null;
|
|
48
|
+
/** @type {Verdict|null} */
|
|
49
|
+
let lastVerdict = null;
|
|
50
|
+
|
|
51
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
52
|
+
const result = await attempt({ iteration, lastResult, critique: lastVerdict ? lastVerdict.critique : null, contract });
|
|
53
|
+
const verdict = await evaluate(result, { iteration, contract });
|
|
54
|
+
history.push({ result, verdict });
|
|
55
|
+
lastResult = result;
|
|
56
|
+
lastVerdict = verdict;
|
|
57
|
+
|
|
58
|
+
if (stopOnPass && verdict.pass) break;
|
|
59
|
+
if (verdict.status === 'failed') break; // terminal — revising a fundamentally-wrong approach burns budget
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { result: lastResult, verdict: lastVerdict, iterations: history.length, history };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { refine };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export type RememberOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* - LLM provider implementing `generate(messages, tools, opts)`.
|
|
4
|
+
*/
|
|
5
|
+
provider: Provider;
|
|
6
|
+
/**
|
|
7
|
+
* - The Store socket (or a `Memory` wrapper). remember writes through
|
|
8
|
+
* `.store(content, metadata)` ONLY — no other verb, no backend assumptions.
|
|
9
|
+
*/
|
|
10
|
+
store: Pick<Store, "store">;
|
|
11
|
+
/**
|
|
12
|
+
* - Optional definition of what counts as durable for this task; appended to the
|
|
13
|
+
* distiller prompt to steer it (the A3 contract idea, reused on the write side).
|
|
14
|
+
*/
|
|
15
|
+
contract?: string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* - Merged into every stored fact's metadata. `kind` is always `'fact'`
|
|
18
|
+
* (NOT overridable — `remember` writes facts; `fact` is litectx's canonical durable kind and harmless to other
|
|
19
|
+
* stores, so the stored label always matches the `facts` counter). Add ANY OTHER metadata here (tags, or litectx
|
|
20
|
+
* `format`/`scope` to differentiate facts). NEVER carries ctx — ctx rides in its own option, never persisted.
|
|
21
|
+
*/
|
|
22
|
+
metadata?: Record<string, any> | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* - The run ctx. If it carries
|
|
25
|
+
* the loop-lent `recordMemoryOp`, each fact write counts against `result.metrics.memory.facts`.
|
|
26
|
+
*/
|
|
27
|
+
ctx?: ({
|
|
28
|
+
recordMemoryOp?: (kind: string) => void;
|
|
29
|
+
} & Record<string, any>) | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* - Budget hook;
|
|
32
|
+
* each distill pass forwards usage so a bareguard gate keeps counting (mirror of Evaluator).
|
|
33
|
+
*/
|
|
34
|
+
onLlmResult?: ((payload: {
|
|
35
|
+
usage: any;
|
|
36
|
+
model: string | null;
|
|
37
|
+
kind: "remember";
|
|
38
|
+
}) => any) | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* - Override the distiller system prompt (defaults to the F5-validated one).
|
|
41
|
+
*/
|
|
42
|
+
prompt?: string | undefined;
|
|
43
|
+
};
|
|
44
|
+
export type RememberOutcome = {
|
|
45
|
+
/**
|
|
46
|
+
* - The facts written, in order — deduplicated within this call by exact string
|
|
47
|
+
* (so `facts.length` is the count stored). Cross-run / semantic dedup is the store's/consumer's job.
|
|
48
|
+
*/
|
|
49
|
+
facts: string[];
|
|
50
|
+
/**
|
|
51
|
+
* - How many non-empty spans were processed.
|
|
52
|
+
*/
|
|
53
|
+
spans: number;
|
|
54
|
+
};
|
|
55
|
+
export type Provider = import("../types").Provider;
|
|
56
|
+
export type Store = import("../types").Store;
|
|
57
|
+
/**
|
|
58
|
+
* @typedef {object} RememberOptions
|
|
59
|
+
* @property {Provider} provider - LLM provider implementing `generate(messages, tools, opts)`.
|
|
60
|
+
* @property {Pick<Store, 'store'>} store - The Store socket (or a `Memory` wrapper). remember writes through
|
|
61
|
+
* `.store(content, metadata)` ONLY — no other verb, no backend assumptions.
|
|
62
|
+
* @property {string} [contract] - Optional definition of what counts as durable for this task; appended to the
|
|
63
|
+
* distiller prompt to steer it (the A3 contract idea, reused on the write side).
|
|
64
|
+
* @property {Record<string, any>} [metadata] - Merged into every stored fact's metadata. `kind` is always `'fact'`
|
|
65
|
+
* (NOT overridable — `remember` writes facts; `fact` is litectx's canonical durable kind and harmless to other
|
|
66
|
+
* stores, so the stored label always matches the `facts` counter). Add ANY OTHER metadata here (tags, or litectx
|
|
67
|
+
* `format`/`scope` to differentiate facts). NEVER carries ctx — ctx rides in its own option, never persisted.
|
|
68
|
+
* @property {{ recordMemoryOp?: (kind: string) => void } & Record<string, any>} [ctx] - The run ctx. If it carries
|
|
69
|
+
* the loop-lent `recordMemoryOp`, each fact write counts against `result.metrics.memory.facts`.
|
|
70
|
+
* @property {(payload: { usage: any, model: string|null, kind: 'remember' }) => any} [onLlmResult] - Budget hook;
|
|
71
|
+
* each distill pass forwards usage so a bareguard gate keeps counting (mirror of Evaluator).
|
|
72
|
+
* @property {string} [prompt] - Override the distiller system prompt (defaults to the F5-validated one).
|
|
73
|
+
*/
|
|
74
|
+
/**
|
|
75
|
+
* @typedef {object} RememberOutcome
|
|
76
|
+
* @property {string[]} facts - The facts written, in order — deduplicated within this call by exact string
|
|
77
|
+
* (so `facts.length` is the count stored). Cross-run / semantic dedup is the store's/consumer's job.
|
|
78
|
+
* @property {number} spans - How many non-empty spans were processed.
|
|
79
|
+
*/
|
|
80
|
+
/**
|
|
81
|
+
* Distill durable facts from harvested spans and persist them through the Store socket.
|
|
82
|
+
*
|
|
83
|
+
* @param {Array<string | { content?: string, text?: string }>} spans - The harvested spans (stash's feedstock).
|
|
84
|
+
* Each is a transcript chunk — a raw string, or an object with `content`/`text`. Empty/blank spans are skipped.
|
|
85
|
+
* @param {RememberOptions} options
|
|
86
|
+
* @returns {Promise<RememberOutcome>}
|
|
87
|
+
*/
|
|
88
|
+
export function remember(spans: Array<string | {
|
|
89
|
+
content?: string;
|
|
90
|
+
text?: string;
|
|
91
|
+
}>, options?: RememberOptions): Promise<RememberOutcome>;
|
|
92
|
+
/**
|
|
93
|
+
* remember — the consolidation pass (eval-assist F5). The "future glue" the PRD parked: it turns the spans
|
|
94
|
+
* `stash` harvested out of the live transcript into durable facts, written back through the GENERIC four-verb
|
|
95
|
+
* `Store` socket (`store`/`search`/`get`/`delete`). Backend-agnostic by construction — works with the
|
|
96
|
+
* JsonFileStore, SQLite, litectx, or a custom store — so it carries NO litectx coupling (the reason the
|
|
97
|
+
* read-litectx's-promotion-count alternative was rejected: it reaches past the socket into one backend).
|
|
98
|
+
*
|
|
99
|
+
* One cheap LLM pass per span distills durable signal (decisions, config, identifiers, stable preferences)
|
|
100
|
+
* and DROPS ephemeral chatter, superseded values, and unanswered questions. The distiller prompt below is the
|
|
101
|
+
* one validated live on the real Anthropic wire in `poc/f5-remember-distill.mjs` (faithfulness + correction +
|
|
102
|
+
* discrimination, all falsifiable). A wrong distiller poisons Memory silently, so that POC must stay green.
|
|
103
|
+
*
|
|
104
|
+
* SECURITY — memory-poisoning surface: facts are model output distilled over UNTRUSTED transcript content and
|
|
105
|
+
* written to durable memory. The grounding prompt refuses a direct "record this fact" injection (validated in the
|
|
106
|
+
* F5 POC + integration test), but treat recalled facts as untrusted CONTEXT, never as authority — and never feed
|
|
107
|
+
* a recalled fact into a privileged action without the same gate you'd apply to any model output.
|
|
108
|
+
*
|
|
109
|
+
* Composes AROUND a Loop (like Evaluator/refine), never inside `loop.js`. Optional, flagged-and-deletable.
|
|
110
|
+
* Budget visibility: each distill pass forwards `usage` to `opts.onLlmResult` (mirror of Evaluator) so a wired
|
|
111
|
+
* bareguard gate keeps counting. A `HaltError` from the provider propagates clean. Metering: each fact write
|
|
112
|
+
* counts against `result.metrics.memory.facts` via the loop-lent `ctx.recordMemoryOp('facts')` hook — the
|
|
113
|
+
* honest producer that field waited for. `facts` is DISJOINT from `stored`: remember writes via `store.store`
|
|
114
|
+
* WITHOUT threading ctx, so it never also trips the generic-write counter.
|
|
115
|
+
*/
|
|
116
|
+
/** @typedef {import('../types').Provider} Provider */
|
|
117
|
+
/** @typedef {import('../types').Store} Store */
|
|
118
|
+
export const DISTILL_PROMPT: string;
|