@braedonsaunders/appkit-ai 1.0.1
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/LICENSE +661 -0
- package/README.md +30 -0
- package/agent.d.ts +33 -0
- package/agent.d.ts.map +1 -0
- package/agent.js +53 -0
- package/agent.js.map +1 -0
- package/analysis.d.ts +72 -0
- package/analysis.d.ts.map +1 -0
- package/analysis.js +114 -0
- package/analysis.js.map +1 -0
- package/builder.d.ts +7 -0
- package/builder.d.ts.map +1 -0
- package/builder.js +28 -0
- package/builder.js.map +1 -0
- package/client.d.ts +91 -0
- package/client.d.ts.map +1 -0
- package/client.js +314 -0
- package/client.js.map +1 -0
- package/context.d.ts +18 -0
- package/context.d.ts.map +1 -0
- package/context.js +84 -0
- package/context.js.map +1 -0
- package/digest.d.ts +13 -0
- package/digest.d.ts.map +1 -0
- package/digest.js +23 -0
- package/digest.js.map +1 -0
- package/doc-chat.d.ts +36 -0
- package/doc-chat.d.ts.map +1 -0
- package/doc-chat.js +106 -0
- package/doc-chat.js.map +1 -0
- package/extract.d.ts +10 -0
- package/extract.d.ts.map +1 -0
- package/extract.js +31 -0
- package/extract.js.map +1 -0
- package/index.d.ts +14 -0
- package/index.d.ts.map +1 -0
- package/index.js +14 -0
- package/index.js.map +1 -0
- package/models.d.ts +11 -0
- package/models.d.ts.map +1 -0
- package/models.js +104 -0
- package/models.js.map +1 -0
- package/package.json +77 -0
- package/prompts.d.ts +11 -0
- package/prompts.d.ts.map +1 -0
- package/prompts.js +22 -0
- package/prompts.js.map +1 -0
- package/react.d.ts +47 -0
- package/react.d.ts.map +1 -0
- package/react.js +137 -0
- package/react.js.map +1 -0
- package/vision.d.ts +66 -0
- package/vision.d.ts.map +1 -0
- package/vision.js +137 -0
- package/vision.js.map +1 -0
- package/writing.d.ts +15 -0
- package/writing.d.ts.map +1 -0
- package/writing.js +46 -0
- package/writing.js.map +1 -0
package/client.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// Config-driven AI client. Provider + API key + models (+ base URL) are passed
|
|
2
|
+
// in per call (resolved from per-tenant settings), NOT read from the environment.
|
|
3
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
4
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
5
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
6
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
7
|
+
import { resolvePublicHost, secureFetch, validateOutboundRequestConfiguration, } from '@braedonsaunders/appkit-sync/egress';
|
|
8
|
+
import { generateText } from 'ai';
|
|
9
|
+
const MAX_AI_REQUEST_BYTES = 16 * 1024 * 1024;
|
|
10
|
+
const MAX_AI_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
11
|
+
/**
|
|
12
|
+
* How long one model call may take before the transport gives up on it.
|
|
13
|
+
*
|
|
14
|
+
* This is not a budget — the caller's own loop governs how much work an agent
|
|
15
|
+
* may do — it is only the point past which a socket is assumed dead. Two
|
|
16
|
+
* minutes turned out to be inside the normal range for a reasoning model
|
|
17
|
+
* answering with tool calls: measured runs on several hosted models spent
|
|
18
|
+
* longer than that on a single step, and each one died on this timeout having
|
|
19
|
+
* produced nothing, billed nothing, and lost every step that came before it.
|
|
20
|
+
*
|
|
21
|
+
* Ten minutes is comfortably past the slowest observed step while still
|
|
22
|
+
* catching a connection that has genuinely gone away.
|
|
23
|
+
*/
|
|
24
|
+
const AI_REQUEST_TIMEOUT_MS = 600_000;
|
|
25
|
+
/**
|
|
26
|
+
* Provider catalogue — single source of truth for the settings UI, the model
|
|
27
|
+
* factory and config validation. Add a provider here and it lights up everywhere.
|
|
28
|
+
*/
|
|
29
|
+
export const AI_PROVIDER_SPECS = [
|
|
30
|
+
{
|
|
31
|
+
value: 'anthropic',
|
|
32
|
+
label: 'Anthropic — Claude',
|
|
33
|
+
kind: 'anthropic',
|
|
34
|
+
baseUrl: null,
|
|
35
|
+
requiresBaseUrl: false,
|
|
36
|
+
fast: 'claude-haiku-4-5-20251001',
|
|
37
|
+
smart: 'claude-sonnet-4-6',
|
|
38
|
+
keyHint: 'sk-ant-…',
|
|
39
|
+
visionToolResults: true,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
value: 'openai',
|
|
43
|
+
label: 'OpenAI — GPT',
|
|
44
|
+
kind: 'openai',
|
|
45
|
+
baseUrl: null,
|
|
46
|
+
requiresBaseUrl: false,
|
|
47
|
+
fast: 'gpt-4o-mini',
|
|
48
|
+
smart: 'gpt-4o',
|
|
49
|
+
keyHint: 'sk-…',
|
|
50
|
+
visionToolResults: false,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
value: 'google',
|
|
54
|
+
label: 'Google — Gemini',
|
|
55
|
+
kind: 'google',
|
|
56
|
+
baseUrl: null,
|
|
57
|
+
requiresBaseUrl: false,
|
|
58
|
+
fast: 'gemini-2.5-flash',
|
|
59
|
+
smart: 'gemini-2.5-pro',
|
|
60
|
+
keyHint: 'AIza…',
|
|
61
|
+
visionToolResults: false,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
value: 'openrouter',
|
|
65
|
+
label: 'OpenRouter — any model, one key',
|
|
66
|
+
kind: 'openai-compatible',
|
|
67
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
68
|
+
requiresBaseUrl: false,
|
|
69
|
+
fast: 'anthropic/claude-3.5-haiku',
|
|
70
|
+
smart: 'anthropic/claude-3.5-sonnet',
|
|
71
|
+
keyHint: 'sk-or-…',
|
|
72
|
+
modelHint: 'Use vendor/model slugs, e.g. anthropic/claude-3.5-sonnet or openai/gpt-4o.',
|
|
73
|
+
// OpenRouter proxies many vendors over an OpenAI-compatible surface; image
|
|
74
|
+
// tool-results aren't reliably supported, so keep vision tools off here.
|
|
75
|
+
visionToolResults: false,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
value: 'groq',
|
|
79
|
+
label: 'Groq — fast inference',
|
|
80
|
+
kind: 'openai-compatible',
|
|
81
|
+
baseUrl: 'https://api.groq.com/openai/v1',
|
|
82
|
+
requiresBaseUrl: false,
|
|
83
|
+
fast: 'llama-3.1-8b-instant',
|
|
84
|
+
smart: 'llama-3.3-70b-versatile',
|
|
85
|
+
keyHint: 'gsk_…',
|
|
86
|
+
visionToolResults: false,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
value: 'xai',
|
|
90
|
+
label: 'xAI — Grok',
|
|
91
|
+
kind: 'openai-compatible',
|
|
92
|
+
baseUrl: 'https://api.x.ai/v1',
|
|
93
|
+
requiresBaseUrl: false,
|
|
94
|
+
fast: 'grok-2-1212',
|
|
95
|
+
smart: 'grok-2-vision-1212',
|
|
96
|
+
keyHint: 'xai-…',
|
|
97
|
+
visionToolResults: false,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
value: 'deepseek',
|
|
101
|
+
label: 'DeepSeek',
|
|
102
|
+
kind: 'openai-compatible',
|
|
103
|
+
baseUrl: 'https://api.deepseek.com',
|
|
104
|
+
requiresBaseUrl: false,
|
|
105
|
+
fast: 'deepseek-chat',
|
|
106
|
+
smart: 'deepseek-chat',
|
|
107
|
+
keyHint: 'sk-…',
|
|
108
|
+
visionToolResults: false,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
value: 'mistral',
|
|
112
|
+
label: 'Mistral',
|
|
113
|
+
kind: 'openai-compatible',
|
|
114
|
+
baseUrl: 'https://api.mistral.ai/v1',
|
|
115
|
+
requiresBaseUrl: false,
|
|
116
|
+
fast: 'mistral-small-latest',
|
|
117
|
+
smart: 'mistral-large-latest',
|
|
118
|
+
keyHint: 'Your Mistral API key',
|
|
119
|
+
visionToolResults: false,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
value: 'custom',
|
|
123
|
+
label: 'Custom (OpenAI-compatible)',
|
|
124
|
+
kind: 'openai-compatible',
|
|
125
|
+
baseUrl: null,
|
|
126
|
+
requiresBaseUrl: true,
|
|
127
|
+
fast: '',
|
|
128
|
+
smart: '',
|
|
129
|
+
keyHint: 'Your provider API key',
|
|
130
|
+
modelHint: 'Use a public HTTPS OpenAI-compatible endpoint (for example Together, Fireworks, Perplexity, or hosted vLLM) and set explicit model ids.',
|
|
131
|
+
visionToolResults: false,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
const SPEC_BY_VALUE = Object.fromEntries(AI_PROVIDER_SPECS.map((s) => [s.value, s]));
|
|
135
|
+
export function isAiProvider(value) {
|
|
136
|
+
return typeof value === 'string' && Object.hasOwn(SPEC_BY_VALUE, value);
|
|
137
|
+
}
|
|
138
|
+
export function providerSpec(provider) {
|
|
139
|
+
return SPEC_BY_VALUE[provider];
|
|
140
|
+
}
|
|
141
|
+
function withoutTrailingSlashes(value) {
|
|
142
|
+
let end = value.length;
|
|
143
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47)
|
|
144
|
+
end -= 1;
|
|
145
|
+
return value.slice(0, end);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Validate and canonicalize a persisted AI endpoint override. Runtime requests
|
|
149
|
+
* repeat the public-DNS check immediately before opening each socket.
|
|
150
|
+
*/
|
|
151
|
+
export async function validateAiBaseUrl(provider, rawBaseUrl) {
|
|
152
|
+
const spec = providerSpec(provider);
|
|
153
|
+
const raw = rawBaseUrl?.trim() ?? '';
|
|
154
|
+
if (spec.kind !== 'openai-compatible') {
|
|
155
|
+
if (raw)
|
|
156
|
+
throw new Error(`${spec.label} does not support a custom base URL.`);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
if (!raw) {
|
|
160
|
+
if (spec.requiresBaseUrl)
|
|
161
|
+
throw new Error('A public HTTPS base URL is required.');
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
let submitted;
|
|
165
|
+
try {
|
|
166
|
+
submitted = new URL(raw);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
throw new Error('The AI base URL is not valid.');
|
|
170
|
+
}
|
|
171
|
+
if (submitted.search || submitted.hash) {
|
|
172
|
+
throw new Error('The AI base URL must not include a query string or fragment.');
|
|
173
|
+
}
|
|
174
|
+
const validated = validateOutboundRequestConfiguration(submitted).url;
|
|
175
|
+
await resolvePublicHost(validated.hostname);
|
|
176
|
+
const path = withoutTrailingSlashes(validated.pathname);
|
|
177
|
+
validated.pathname = path || '/';
|
|
178
|
+
const canonical = withoutTrailingSlashes(validated.href);
|
|
179
|
+
return canonical || validated.origin;
|
|
180
|
+
}
|
|
181
|
+
async function readBoundedRequestBody(request) {
|
|
182
|
+
if (!request.body)
|
|
183
|
+
return undefined;
|
|
184
|
+
const declaredLength = Number(request.headers.get('content-length'));
|
|
185
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_AI_REQUEST_BYTES) {
|
|
186
|
+
throw new Error(`AI request body exceeded ${MAX_AI_REQUEST_BYTES} bytes.`);
|
|
187
|
+
}
|
|
188
|
+
const reader = request.body.getReader();
|
|
189
|
+
const chunks = [];
|
|
190
|
+
let total = 0;
|
|
191
|
+
try {
|
|
192
|
+
for (;;) {
|
|
193
|
+
if (request.signal.aborted)
|
|
194
|
+
throw request.signal.reason;
|
|
195
|
+
const { done, value } = await reader.read();
|
|
196
|
+
if (done)
|
|
197
|
+
break;
|
|
198
|
+
total += value.byteLength;
|
|
199
|
+
if (total > MAX_AI_REQUEST_BYTES) {
|
|
200
|
+
await reader.cancel();
|
|
201
|
+
throw new Error(`AI request body exceeded ${MAX_AI_REQUEST_BYTES} bytes.`);
|
|
202
|
+
}
|
|
203
|
+
chunks.push(value);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
reader.releaseLock();
|
|
208
|
+
}
|
|
209
|
+
const body = new Uint8Array(total);
|
|
210
|
+
let offset = 0;
|
|
211
|
+
for (const chunk of chunks) {
|
|
212
|
+
body.set(chunk, offset);
|
|
213
|
+
offset += chunk.byteLength;
|
|
214
|
+
}
|
|
215
|
+
return body;
|
|
216
|
+
}
|
|
217
|
+
/** Socket-pinned transport for tenant-configurable OpenAI-compatible endpoints. */
|
|
218
|
+
export const secureAiFetch = async (input, init) => {
|
|
219
|
+
const request = new Request(input, init);
|
|
220
|
+
const method = request.method.toUpperCase();
|
|
221
|
+
if (!['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
222
|
+
throw new Error(`AI request method ${method} is not supported.`);
|
|
223
|
+
}
|
|
224
|
+
const body = await readBoundedRequestBody(request);
|
|
225
|
+
return secureFetch(request.url, {
|
|
226
|
+
method: method,
|
|
227
|
+
headers: request.headers,
|
|
228
|
+
body,
|
|
229
|
+
timeoutMs: AI_REQUEST_TIMEOUT_MS,
|
|
230
|
+
maxRequestBytes: MAX_AI_REQUEST_BYTES,
|
|
231
|
+
maxResponseBytes: MAX_AI_RESPONSE_BYTES,
|
|
232
|
+
maxRedirects: 2,
|
|
233
|
+
signal: request.signal,
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
export function defaultModel(provider, tier) {
|
|
237
|
+
const spec = SPEC_BY_VALUE[provider] ?? SPEC_BY_VALUE.anthropic;
|
|
238
|
+
return tier === 'smart' ? spec.smart : spec.fast;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Whether the configured provider accepts IMAGE content in a tool result — the
|
|
242
|
+
* capability that lets a tool hand rendered PDF pages back to the model for
|
|
243
|
+
* vision reading. Currently Anthropic only; other providers' tool/function
|
|
244
|
+
* results are text/JSON only, so exposing such a tool to them would break the
|
|
245
|
+
* agent turn. Used to gate the assistant's `view_document_pages` tool.
|
|
246
|
+
*/
|
|
247
|
+
export function providerSupportsImageToolResults(config) {
|
|
248
|
+
if (!config)
|
|
249
|
+
return false;
|
|
250
|
+
return SPEC_BY_VALUE[config.provider]?.visionToolResults ?? false;
|
|
251
|
+
}
|
|
252
|
+
export function isAiConfigured(config) {
|
|
253
|
+
if (!config || !config.apiKey)
|
|
254
|
+
return false;
|
|
255
|
+
const spec = SPEC_BY_VALUE[config.provider];
|
|
256
|
+
if (!spec)
|
|
257
|
+
return false;
|
|
258
|
+
if (spec.requiresBaseUrl && !config.baseUrl?.trim())
|
|
259
|
+
return false;
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
/** Resolve a language model from a tenant's config, or null when not configured. */
|
|
263
|
+
export function getModel(config, tier = 'fast') {
|
|
264
|
+
if (!isAiConfigured(config))
|
|
265
|
+
return null;
|
|
266
|
+
const spec = SPEC_BY_VALUE[config.provider];
|
|
267
|
+
const modelId = (tier === 'smart' ? config.modelSmart : config.modelFast) || defaultModel(config.provider, tier);
|
|
268
|
+
// `custom` has no default model — without an explicit one, AI stays disabled.
|
|
269
|
+
if (!modelId)
|
|
270
|
+
return null;
|
|
271
|
+
switch (spec.kind) {
|
|
272
|
+
case 'anthropic':
|
|
273
|
+
return createAnthropic({ apiKey: config.apiKey })(modelId);
|
|
274
|
+
case 'openai':
|
|
275
|
+
return createOpenAI({ apiKey: config.apiKey })(modelId);
|
|
276
|
+
case 'google':
|
|
277
|
+
return createGoogleGenerativeAI({ apiKey: config.apiKey })(modelId);
|
|
278
|
+
case 'openai-compatible': {
|
|
279
|
+
const baseURL = config.baseUrl?.trim() || spec.baseUrl;
|
|
280
|
+
if (!baseURL)
|
|
281
|
+
return null;
|
|
282
|
+
return createOpenAICompatible({
|
|
283
|
+
name: spec.value,
|
|
284
|
+
apiKey: config.apiKey,
|
|
285
|
+
baseURL,
|
|
286
|
+
fetch: secureAiFetch,
|
|
287
|
+
})(modelId);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
export class AIDisabledError extends Error {
|
|
292
|
+
name = 'AIDisabledError';
|
|
293
|
+
constructor() {
|
|
294
|
+
super('No AI provider is available for this request.');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/** Live test of a config — sends a tiny prompt and reports success/failure. */
|
|
298
|
+
export async function pingModel(config) {
|
|
299
|
+
const model = getModel(config, 'fast');
|
|
300
|
+
if (!model) {
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
message: 'Not configured yet — check the provider, API key, model and base URL.',
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const { text } = await generateText({ model, prompt: 'Reply with the single word: ok' });
|
|
308
|
+
return { ok: true, message: `Connected — the model replied “${text.trim().slice(0, 24)}”.` };
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
return { ok: false, message: e instanceof Error ? e.message.slice(0, 180) : 'Request failed.' };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
//# sourceMappingURL=client.js.map
|
package/client.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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","sourcesContent":["// Config-driven AI client. Provider + API key + models (+ base URL) are passed\n// in per call (resolved from per-tenant settings), NOT read from the environment.\n\nimport { createAnthropic } from '@ai-sdk/anthropic'\nimport { createGoogleGenerativeAI } from '@ai-sdk/google'\nimport { createOpenAI } from '@ai-sdk/openai'\nimport { createOpenAICompatible } from '@ai-sdk/openai-compatible'\nimport {\n resolvePublicHost,\n secureFetch,\n validateOutboundRequestConfiguration,\n} from '@braedonsaunders/appkit-sync/egress'\nimport { generateText, type LanguageModel } from 'ai'\n\nconst MAX_AI_REQUEST_BYTES = 16 * 1024 * 1024\nconst MAX_AI_RESPONSE_BYTES = 16 * 1024 * 1024\n/**\n * How long one model call may take before the transport gives up on it.\n *\n * This is not a budget — the caller's own loop governs how much work an agent\n * may do — it is only the point past which a socket is assumed dead. Two\n * minutes turned out to be inside the normal range for a reasoning model\n * answering with tool calls: measured runs on several hosted models spent\n * longer than that on a single step, and each one died on this timeout having\n * produced nothing, billed nothing, and lost every step that came before it.\n *\n * Ten minutes is comfortably past the slowest observed step while still\n * catching a connection that has genuinely gone away.\n */\nconst AI_REQUEST_TIMEOUT_MS = 600_000\n\nexport type AiProvider =\n | 'anthropic'\n | 'openai'\n | 'google'\n | 'openrouter'\n | 'groq'\n | 'xai'\n | 'deepseek'\n | 'mistral'\n | 'custom'\n\nexport type ModelTier = 'fast' | 'smart'\n\n/**\n * Platform-wide AI policy governing per-tenant overrides (mirrors the email/SMS\n * policy modes). 'disabled' is a global kill switch; 'global_only' forces the\n * platform provider for every tenant; 'tenant_optional' lets each tenant use its\n * own provider and falls back to the platform default.\n */\nexport type AiPolicyMode = 'tenant_optional' | 'global_only' | 'disabled'\n\nexport type AiConfig = {\n provider: AiProvider\n apiKey: string\n modelFast?: string | null\n modelSmart?: string | null\n /**\n * Endpoint for OpenAI-compatible providers. Required for `custom`; for the\n * named compatible providers it is an optional override of the built-in URL.\n */\n baseUrl?: string | null\n /**\n * Organization (tenant) identity for prompt grounding, so generated content\n * uses the real org name instead of a placeholder. Populated by\n * `getTenantAiConfig`; content-generation paths inject it into the system\n * prompt, analysis/vision paths ignore it.\n */\n org?: { name: string } | null\n}\n\n// How a provider's language model is constructed. The OpenAI-compatible kind\n// covers OpenRouter, Groq, xAI, DeepSeek, Mistral and any user `custom` endpoint.\ntype ProviderKind = 'anthropic' | 'openai' | 'google' | 'openai-compatible'\n\nexport type ProviderSpec = {\n value: AiProvider\n label: string\n kind: ProviderKind\n /** Built-in endpoint for a named OpenAI-compatible provider (null otherwise). */\n baseUrl: string | null\n /** True when the tenant MUST supply their own base URL (i.e. `custom`). */\n requiresBaseUrl: boolean\n /** Default fast/smart model ids (placeholders + fallbacks). Empty for `custom`. */\n fast: string\n smart: string\n /** Placeholder shown in the API-key field. */\n keyHint: string\n /** Optional note about the model-id format for this provider. */\n modelHint?: string\n /**\n * True when this provider's API accepts IMAGE content inside a tool result\n * (Anthropic does; OpenAI's and Google's function/tool results are text/JSON\n * only). Gates vision tools that return rendered page images to the model —\n * see `providerSupportsImageToolResults`.\n */\n visionToolResults: boolean\n}\n\n/**\n * Provider catalogue — single source of truth for the settings UI, the model\n * factory and config validation. Add a provider here and it lights up everywhere.\n */\nexport const AI_PROVIDER_SPECS: ProviderSpec[] = [\n {\n value: 'anthropic',\n label: 'Anthropic — Claude',\n kind: 'anthropic',\n baseUrl: null,\n requiresBaseUrl: false,\n fast: 'claude-haiku-4-5-20251001',\n smart: 'claude-sonnet-4-6',\n keyHint: 'sk-ant-…',\n visionToolResults: true,\n },\n {\n value: 'openai',\n label: 'OpenAI — GPT',\n kind: 'openai',\n baseUrl: null,\n requiresBaseUrl: false,\n fast: 'gpt-4o-mini',\n smart: 'gpt-4o',\n keyHint: 'sk-…',\n visionToolResults: false,\n },\n {\n value: 'google',\n label: 'Google — Gemini',\n kind: 'google',\n baseUrl: null,\n requiresBaseUrl: false,\n fast: 'gemini-2.5-flash',\n smart: 'gemini-2.5-pro',\n keyHint: 'AIza…',\n visionToolResults: false,\n },\n {\n value: 'openrouter',\n label: 'OpenRouter — any model, one key',\n kind: 'openai-compatible',\n baseUrl: 'https://openrouter.ai/api/v1',\n requiresBaseUrl: false,\n fast: 'anthropic/claude-3.5-haiku',\n smart: 'anthropic/claude-3.5-sonnet',\n keyHint: 'sk-or-…',\n modelHint: 'Use vendor/model slugs, e.g. anthropic/claude-3.5-sonnet or openai/gpt-4o.',\n // OpenRouter proxies many vendors over an OpenAI-compatible surface; image\n // tool-results aren't reliably supported, so keep vision tools off here.\n visionToolResults: false,\n },\n {\n value: 'groq',\n label: 'Groq — fast inference',\n kind: 'openai-compatible',\n baseUrl: 'https://api.groq.com/openai/v1',\n requiresBaseUrl: false,\n fast: 'llama-3.1-8b-instant',\n smart: 'llama-3.3-70b-versatile',\n keyHint: 'gsk_…',\n visionToolResults: false,\n },\n {\n value: 'xai',\n label: 'xAI — Grok',\n kind: 'openai-compatible',\n baseUrl: 'https://api.x.ai/v1',\n requiresBaseUrl: false,\n fast: 'grok-2-1212',\n smart: 'grok-2-vision-1212',\n keyHint: 'xai-…',\n visionToolResults: false,\n },\n {\n value: 'deepseek',\n label: 'DeepSeek',\n kind: 'openai-compatible',\n baseUrl: 'https://api.deepseek.com',\n requiresBaseUrl: false,\n fast: 'deepseek-chat',\n smart: 'deepseek-chat',\n keyHint: 'sk-…',\n visionToolResults: false,\n },\n {\n value: 'mistral',\n label: 'Mistral',\n kind: 'openai-compatible',\n baseUrl: 'https://api.mistral.ai/v1',\n requiresBaseUrl: false,\n fast: 'mistral-small-latest',\n smart: 'mistral-large-latest',\n keyHint: 'Your Mistral API key',\n visionToolResults: false,\n },\n {\n value: 'custom',\n label: 'Custom (OpenAI-compatible)',\n kind: 'openai-compatible',\n baseUrl: null,\n requiresBaseUrl: true,\n fast: '',\n smart: '',\n keyHint: 'Your provider API key',\n modelHint:\n 'Use a public HTTPS OpenAI-compatible endpoint (for example Together, Fireworks, Perplexity, or hosted vLLM) and set explicit model ids.',\n visionToolResults: false,\n },\n]\n\nconst SPEC_BY_VALUE = Object.fromEntries(AI_PROVIDER_SPECS.map((s) => [s.value, s])) as Record<\n AiProvider,\n ProviderSpec\n>\n\nexport function isAiProvider(value: unknown): value is AiProvider {\n return typeof value === 'string' && Object.hasOwn(SPEC_BY_VALUE, value)\n}\n\nexport function providerSpec(provider: AiProvider): ProviderSpec {\n return SPEC_BY_VALUE[provider]\n}\n\nfunction withoutTrailingSlashes(value: string): string {\n let end = value.length\n while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1\n return value.slice(0, end)\n}\n\n/**\n * Validate and canonicalize a persisted AI endpoint override. Runtime requests\n * repeat the public-DNS check immediately before opening each socket.\n */\nexport async function validateAiBaseUrl(\n provider: AiProvider,\n rawBaseUrl: string | null | undefined,\n): Promise<string | null> {\n const spec = providerSpec(provider)\n const raw = rawBaseUrl?.trim() ?? ''\n if (spec.kind !== 'openai-compatible') {\n if (raw) throw new Error(`${spec.label} does not support a custom base URL.`)\n return null\n }\n if (!raw) {\n if (spec.requiresBaseUrl) throw new Error('A public HTTPS base URL is required.')\n return null\n }\n\n let submitted: URL\n try {\n submitted = new URL(raw)\n } catch {\n throw new Error('The AI base URL is not valid.')\n }\n if (submitted.search || submitted.hash) {\n throw new Error('The AI base URL must not include a query string or fragment.')\n }\n const validated = validateOutboundRequestConfiguration(submitted).url\n await resolvePublicHost(validated.hostname)\n\n const path = withoutTrailingSlashes(validated.pathname)\n validated.pathname = path || '/'\n const canonical = withoutTrailingSlashes(validated.href)\n return canonical || validated.origin\n}\n\nasync function readBoundedRequestBody(request: Request): Promise<Uint8Array | undefined> {\n if (!request.body) return undefined\n const declaredLength = Number(request.headers.get('content-length'))\n if (Number.isFinite(declaredLength) && declaredLength > MAX_AI_REQUEST_BYTES) {\n throw new Error(`AI request body exceeded ${MAX_AI_REQUEST_BYTES} bytes.`)\n }\n\n const reader = request.body.getReader()\n const chunks: Uint8Array[] = []\n let total = 0\n try {\n for (;;) {\n if (request.signal.aborted) throw request.signal.reason\n const { done, value } = await reader.read()\n if (done) break\n total += value.byteLength\n if (total > MAX_AI_REQUEST_BYTES) {\n await reader.cancel()\n throw new Error(`AI request body exceeded ${MAX_AI_REQUEST_BYTES} bytes.`)\n }\n chunks.push(value)\n }\n } finally {\n reader.releaseLock()\n }\n\n const body = new Uint8Array(total)\n let offset = 0\n for (const chunk of chunks) {\n body.set(chunk, offset)\n offset += chunk.byteLength\n }\n return body\n}\n\n/** Socket-pinned transport for tenant-configurable OpenAI-compatible endpoints. */\nexport const secureAiFetch: typeof globalThis.fetch = async (input, init) => {\n const request = new Request(input, init)\n const method = request.method.toUpperCase()\n if (!['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {\n throw new Error(`AI request method ${method} is not supported.`)\n }\n const body = await readBoundedRequestBody(request)\n return secureFetch(request.url, {\n method: method as 'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',\n headers: request.headers,\n body,\n timeoutMs: AI_REQUEST_TIMEOUT_MS,\n maxRequestBytes: MAX_AI_REQUEST_BYTES,\n maxResponseBytes: MAX_AI_RESPONSE_BYTES,\n maxRedirects: 2,\n signal: request.signal,\n })\n}\n\nexport function defaultModel(provider: AiProvider, tier: ModelTier): string {\n const spec = SPEC_BY_VALUE[provider] ?? SPEC_BY_VALUE.anthropic\n return tier === 'smart' ? spec.smart : spec.fast\n}\n\n/**\n * Whether the configured provider accepts IMAGE content in a tool result — the\n * capability that lets a tool hand rendered PDF pages back to the model for\n * vision reading. Currently Anthropic only; other providers' tool/function\n * results are text/JSON only, so exposing such a tool to them would break the\n * agent turn. Used to gate the assistant's `view_document_pages` tool.\n */\nexport function providerSupportsImageToolResults(config: AiConfig | null | undefined): boolean {\n if (!config) return false\n return SPEC_BY_VALUE[config.provider]?.visionToolResults ?? false\n}\n\nexport function isAiConfigured(config: AiConfig | null | undefined): config is AiConfig {\n if (!config || !config.apiKey) return false\n const spec = SPEC_BY_VALUE[config.provider]\n if (!spec) return false\n if (spec.requiresBaseUrl && !config.baseUrl?.trim()) return false\n return true\n}\n\n/** Resolve a language model from a tenant's config, or null when not configured. */\nexport function getModel(\n config: AiConfig | null | undefined,\n tier: ModelTier = 'fast',\n): LanguageModel | null {\n if (!isAiConfigured(config)) return null\n const spec = SPEC_BY_VALUE[config.provider]\n const modelId =\n (tier === 'smart' ? config.modelSmart : config.modelFast) || defaultModel(config.provider, tier)\n // `custom` has no default model — without an explicit one, AI stays disabled.\n if (!modelId) return null\n\n switch (spec.kind) {\n case 'anthropic':\n return createAnthropic({ apiKey: config.apiKey })(modelId)\n case 'openai':\n return createOpenAI({ apiKey: config.apiKey })(modelId)\n case 'google':\n return createGoogleGenerativeAI({ apiKey: config.apiKey })(modelId)\n case 'openai-compatible': {\n const baseURL = config.baseUrl?.trim() || spec.baseUrl\n if (!baseURL) return null\n return createOpenAICompatible({\n name: spec.value,\n apiKey: config.apiKey,\n baseURL,\n fetch: secureAiFetch,\n })(modelId)\n }\n }\n}\n\nexport class AIDisabledError extends Error {\n override readonly name = 'AIDisabledError'\n constructor() {\n super('No AI provider is available for this request.')\n }\n}\n\n/** Live test of a config — sends a tiny prompt and reports success/failure. */\nexport async function pingModel(\n config: AiConfig | null | undefined,\n): Promise<{ ok: boolean; message: string }> {\n const model = getModel(config, 'fast')\n if (!model) {\n return {\n ok: false,\n message: 'Not configured yet — check the provider, API key, model and base URL.',\n }\n }\n try {\n const { text } = await generateText({ model, prompt: 'Reply with the single word: ok' })\n return { ok: true, message: `Connected — the model replied “${text.trim().slice(0, 24)}”.` }\n } catch (e) {\n return { ok: false, message: e instanceof Error ? e.message.slice(0, 180) : 'Request failed.' }\n }\n}\n"]}
|
package/context.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ModelMessage } from 'ai';
|
|
2
|
+
export declare const DEFAULT_VISUAL_CONTEXT_FRAMES = 2;
|
|
3
|
+
export type VisualContextPruneResult = {
|
|
4
|
+
messages: ModelMessage[];
|
|
5
|
+
prunedFrames: number;
|
|
6
|
+
deduplicatedFrames: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Keep only the newest distinct tool-result images. Tool-call/result structure
|
|
10
|
+
* remains intact, and textual metadata stays available, but a long desktop or
|
|
11
|
+
* browser session no longer resends every historical screenshot on every step.
|
|
12
|
+
* User-supplied image messages are deliberately untouched.
|
|
13
|
+
*/
|
|
14
|
+
export declare function pruneVisualToolContext(messages: readonly ModelMessage[], options?: {
|
|
15
|
+
keepRecent?: number;
|
|
16
|
+
omittedText?: string;
|
|
17
|
+
}): VisualContextPruneResult;
|
|
18
|
+
//# sourceMappingURL=context.d.ts.map
|
package/context.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AAEtC,eAAO,MAAM,6BAA6B,IAAI,CAAA;AAE9C,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB,YAAY,EAAE,MAAM,CAAA;IACpB,kBAAkB,EAAE,MAAM,CAAA;CAC3B,CAAA;AAID;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,YAAY,EAAE,EACjC,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GAC1D,wBAAwB,CAoD1B"}
|
package/context.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export const DEFAULT_VISUAL_CONTEXT_FRAMES = 2;
|
|
2
|
+
/**
|
|
3
|
+
* Keep only the newest distinct tool-result images. Tool-call/result structure
|
|
4
|
+
* remains intact, and textual metadata stays available, but a long desktop or
|
|
5
|
+
* browser session no longer resends every historical screenshot on every step.
|
|
6
|
+
* User-supplied image messages are deliberately untouched.
|
|
7
|
+
*/
|
|
8
|
+
export function pruneVisualToolContext(messages, options = {}) {
|
|
9
|
+
const keepRecent = options.keepRecent ?? DEFAULT_VISUAL_CONTEXT_FRAMES;
|
|
10
|
+
if (!Number.isInteger(keepRecent) || keepRecent < 0) {
|
|
11
|
+
throw new Error('keepRecent must be a non-negative integer.');
|
|
12
|
+
}
|
|
13
|
+
const omittedText = options.omittedText ?? '[Earlier visual frame omitted; request a fresh observation if needed.]';
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
let retained = 0;
|
|
16
|
+
let prunedFrames = 0;
|
|
17
|
+
let deduplicatedFrames = 0;
|
|
18
|
+
const copied = [...messages];
|
|
19
|
+
for (let messageIndex = copied.length - 1; messageIndex >= 0; messageIndex -= 1) {
|
|
20
|
+
const message = copied[messageIndex];
|
|
21
|
+
if (!message || message.role !== 'tool' || !Array.isArray(message.content))
|
|
22
|
+
continue;
|
|
23
|
+
let messageChanged = false;
|
|
24
|
+
const content = [...message.content];
|
|
25
|
+
for (let partIndex = content.length - 1; partIndex >= 0; partIndex -= 1) {
|
|
26
|
+
const part = content[partIndex];
|
|
27
|
+
if (!part || part.type !== 'tool-result')
|
|
28
|
+
continue;
|
|
29
|
+
const output = asRecord(part.output);
|
|
30
|
+
if (!output || output.type !== 'content' || !Array.isArray(output.value))
|
|
31
|
+
continue;
|
|
32
|
+
const visual = output.value.filter(isImagePart);
|
|
33
|
+
if (visual.length === 0)
|
|
34
|
+
continue;
|
|
35
|
+
const fingerprints = visual.map(frameFingerprint);
|
|
36
|
+
const duplicate = fingerprints.every((fingerprint) => seen.has(fingerprint));
|
|
37
|
+
const keep = !duplicate && retained < keepRecent;
|
|
38
|
+
if (keep) {
|
|
39
|
+
retained += 1;
|
|
40
|
+
for (const fingerprint of fingerprints)
|
|
41
|
+
seen.add(fingerprint);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (duplicate)
|
|
45
|
+
deduplicatedFrames += visual.length;
|
|
46
|
+
else
|
|
47
|
+
prunedFrames += visual.length;
|
|
48
|
+
const value = output.value.filter((entry) => !isImagePart(entry));
|
|
49
|
+
if (!value.some((entry) => asRecord(entry)?.type === 'text')) {
|
|
50
|
+
value.push({ type: 'text', text: omittedText });
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
value.push({ type: 'text', text: omittedText });
|
|
54
|
+
}
|
|
55
|
+
content[partIndex] = {
|
|
56
|
+
...part,
|
|
57
|
+
output: { ...output, value },
|
|
58
|
+
};
|
|
59
|
+
messageChanged = true;
|
|
60
|
+
}
|
|
61
|
+
if (messageChanged)
|
|
62
|
+
copied[messageIndex] = { ...message, content };
|
|
63
|
+
}
|
|
64
|
+
return { messages: copied, prunedFrames, deduplicatedFrames };
|
|
65
|
+
}
|
|
66
|
+
function asRecord(value) {
|
|
67
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
68
|
+
? value
|
|
69
|
+
: null;
|
|
70
|
+
}
|
|
71
|
+
function isImagePart(value) {
|
|
72
|
+
const record = asRecord(value);
|
|
73
|
+
return Boolean(record
|
|
74
|
+
&& (record.type === 'image-data' || record.type === 'image')
|
|
75
|
+
&& typeof record.mediaType === 'string'
|
|
76
|
+
&& (typeof record.data === 'string' || record.data instanceof Uint8Array));
|
|
77
|
+
}
|
|
78
|
+
function frameFingerprint(frame) {
|
|
79
|
+
const data = frame.data;
|
|
80
|
+
if (typeof data === 'string')
|
|
81
|
+
return `${String(frame.mediaType)}:${data}`;
|
|
82
|
+
return `${String(frame.mediaType)}:${Buffer.from(data).toString('base64')}`;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=context.js.map
|
package/context.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAA;AAU9C;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CACpC,QAAiC,EACjC,UAAyD,EAAE;IAE3D,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,6BAA6B,CAAA;IACtE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAA;IAC/D,CAAC;IACD,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,wEAAwE,CAAA;IACnH,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,IAAI,QAAQ,GAAG,CAAC,CAAA;IAChB,IAAI,YAAY,GAAG,CAAC,CAAA;IACpB,IAAI,kBAAkB,GAAG,CAAC,CAAA;IAC1B,MAAM,MAAM,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAA;IAE5B,KAAK,IAAI,YAAY,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,YAAY,IAAI,CAAC,EAAE,YAAY,IAAI,CAAC,EAAE,CAAC;QAChF,MAAM,OAAO,GAAG,MAAM,CAAC,YAAY,CAAC,CAAA;QACpC,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC;YAAE,SAAQ;QACpF,IAAI,cAAc,GAAG,KAAK,CAAA;QAC1B,MAAM,OAAO,GAAG,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;QACpC,KAAK,IAAI,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,SAAS,IAAI,CAAC,EAAE,SAAS,IAAI,CAAC,EAAE,CAAC;YACxE,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;YAC/B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,aAAa;gBAAE,SAAQ;YAClD,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YACpC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;gBAAE,SAAQ;YAClF,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;YAC/C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAQ;YAEjC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAA;YACjD,MAAM,SAAS,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAA;YAC5E,MAAM,IAAI,GAAG,CAAC,SAAS,IAAI,QAAQ,GAAG,UAAU,CAAA;YAChD,IAAI,IAAI,EAAE,CAAC;gBACT,QAAQ,IAAI,CAAC,CAAA;gBACb,KAAK,MAAM,WAAW,IAAI,YAAY;oBAAE,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;gBAC7D,SAAQ;YACV,CAAC;YAED,IAAI,SAAS;gBAAE,kBAAkB,IAAI,MAAM,CAAC,MAAM,CAAA;;gBAC7C,YAAY,IAAI,MAAM,CAAC,MAAM,CAAA;YAClC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAc,CAAA;YAC9E,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,IAAI,KAAK,MAAM,CAAC,EAAE,CAAC;gBAC7D,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAA;YACjD,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAA;YACjD,CAAC;YACD,OAAO,CAAC,SAAS,CAAC,GAAG;gBACnB,GAAG,IAAI;gBACP,MAAM,EAAE,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE;aACd,CAAA;YAChB,cAAc,GAAG,IAAI,CAAA;QACvB,CAAC;QACD,IAAI,cAAc;YAAE,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,CAAA;IACpE,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,kBAAkB,EAAE,CAAA;AAC/D,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACzE,CAAC,CAAC,KAAgC;QAClC,CAAC,CAAC,IAAI,CAAA;AACV,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC9B,OAAO,OAAO,CACZ,MAAM;WACH,CAAC,MAAM,CAAC,IAAI,KAAK,YAAY,IAAI,MAAM,CAAC,IAAI,KAAK,OAAO,CAAC;WACzD,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ;WACpC,CAAC,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,YAAY,UAAU,CAAC,CAC1E,CAAA;AACH,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAkB;IAC1C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAA;IACvB,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,IAAI,EAAE,CAAA;IACzE,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,IAAkB,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAA;AAC3F,CAAC","sourcesContent":["import type { ModelMessage } from 'ai'\n\nexport const DEFAULT_VISUAL_CONTEXT_FRAMES = 2\n\nexport type VisualContextPruneResult = {\n messages: ModelMessage[]\n prunedFrames: number\n deduplicatedFrames: number\n}\n\ntype ContentPart = Record<string, unknown> & { type: string }\n\n/**\n * Keep only the newest distinct tool-result images. Tool-call/result structure\n * remains intact, and textual metadata stays available, but a long desktop or\n * browser session no longer resends every historical screenshot on every step.\n * User-supplied image messages are deliberately untouched.\n */\nexport function pruneVisualToolContext(\n messages: readonly ModelMessage[],\n options: { keepRecent?: number; omittedText?: string } = {},\n): VisualContextPruneResult {\n const keepRecent = options.keepRecent ?? DEFAULT_VISUAL_CONTEXT_FRAMES\n if (!Number.isInteger(keepRecent) || keepRecent < 0) {\n throw new Error('keepRecent must be a non-negative integer.')\n }\n const omittedText = options.omittedText ?? '[Earlier visual frame omitted; request a fresh observation if needed.]'\n const seen = new Set<string>()\n let retained = 0\n let prunedFrames = 0\n let deduplicatedFrames = 0\n const copied = [...messages]\n\n for (let messageIndex = copied.length - 1; messageIndex >= 0; messageIndex -= 1) {\n const message = copied[messageIndex]\n if (!message || message.role !== 'tool' || !Array.isArray(message.content)) continue\n let messageChanged = false\n const content = [...message.content]\n for (let partIndex = content.length - 1; partIndex >= 0; partIndex -= 1) {\n const part = content[partIndex]\n if (!part || part.type !== 'tool-result') continue\n const output = asRecord(part.output)\n if (!output || output.type !== 'content' || !Array.isArray(output.value)) continue\n const visual = output.value.filter(isImagePart)\n if (visual.length === 0) continue\n\n const fingerprints = visual.map(frameFingerprint)\n const duplicate = fingerprints.every((fingerprint) => seen.has(fingerprint))\n const keep = !duplicate && retained < keepRecent\n if (keep) {\n retained += 1\n for (const fingerprint of fingerprints) seen.add(fingerprint)\n continue\n }\n\n if (duplicate) deduplicatedFrames += visual.length\n else prunedFrames += visual.length\n const value = output.value.filter((entry) => !isImagePart(entry)) as unknown[]\n if (!value.some((entry) => asRecord(entry)?.type === 'text')) {\n value.push({ type: 'text', text: omittedText })\n } else {\n value.push({ type: 'text', text: omittedText })\n }\n content[partIndex] = {\n ...part,\n output: { ...output, value },\n } as typeof part\n messageChanged = true\n }\n if (messageChanged) copied[messageIndex] = { ...message, content }\n }\n\n return { messages: copied, prunedFrames, deduplicatedFrames }\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n ? value as Record<string, unknown>\n : null\n}\n\nfunction isImagePart(value: unknown): value is ContentPart {\n const record = asRecord(value)\n return Boolean(\n record\n && (record.type === 'image-data' || record.type === 'image')\n && typeof record.mediaType === 'string'\n && (typeof record.data === 'string' || record.data instanceof Uint8Array),\n )\n}\n\nfunction frameFingerprint(frame: ContentPart): string {\n const data = frame.data\n if (typeof data === 'string') return `${String(frame.mediaType)}:${data}`\n return `${String(frame.mediaType)}:${Buffer.from(data as Uint8Array).toString('base64')}`\n}\n"]}
|
package/digest.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type AiConfig } from './client.js';
|
|
2
|
+
export type DigestEntry = {
|
|
3
|
+
date: string;
|
|
4
|
+
author?: string | null;
|
|
5
|
+
location?: string | null;
|
|
6
|
+
text: string;
|
|
7
|
+
};
|
|
8
|
+
/** Summarise a batch of activity entries. Null when AI is unconfigured or empty. */
|
|
9
|
+
export declare function generateDigest(config: AiConfig | null | undefined, args: {
|
|
10
|
+
scope?: string;
|
|
11
|
+
entries: DigestEntry[];
|
|
12
|
+
}): Promise<string | null>;
|
|
13
|
+
//# sourceMappingURL=digest.d.ts.map
|
package/digest.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../src/digest.ts"],"names":[],"mappings":"AAGA,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AAGlD,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,oFAAoF;AACpF,wBAAsB,cAAc,CAClC,MAAM,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,EACnC,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,WAAW,EAAE,CAAA;CAAE,GAC/C,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAmBxB"}
|
package/digest.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Period digest — synthesise many activity entries into a short manager recap.
|
|
2
|
+
import { generateText } from 'ai';
|
|
3
|
+
import { getModel } from './client.js';
|
|
4
|
+
import { ENTRY_WRITING_SYSTEM } from './prompts.js';
|
|
5
|
+
/** Summarise a batch of activity entries. Null when AI is unconfigured or empty. */
|
|
6
|
+
export async function generateDigest(config, args) {
|
|
7
|
+
const model = getModel(config, 'smart');
|
|
8
|
+
if (!model || args.entries.length === 0)
|
|
9
|
+
return null;
|
|
10
|
+
const scope = args.scope ?? 'recent';
|
|
11
|
+
const corpus = args.entries
|
|
12
|
+
.slice(0, 200)
|
|
13
|
+
.map((e) => `- [${e.date}${e.location ? ` · ${e.location}` : ''}${e.author ? ` · ${e.author}` : ''}] ${e.text}`)
|
|
14
|
+
.join('\n');
|
|
15
|
+
const { text } = await generateText({
|
|
16
|
+
model,
|
|
17
|
+
system: ENTRY_WRITING_SYSTEM,
|
|
18
|
+
prompt: `Below are ${args.entries.length} activity entries. Write a concise ${scope} digest for a manager: the main activity and themes, anything notable or recurring, and any follow-ups or action items mentioned. Use 4–8 sentences of plain prose with no preamble or bullet list.\n\n---\n${corpus}`,
|
|
19
|
+
temperature: 0.4,
|
|
20
|
+
});
|
|
21
|
+
return text.trim();
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=digest.js.map
|
package/digest.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digest.js","sourceRoot":"","sources":["../src/digest.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AACjC,OAAO,EAAE,QAAQ,EAAiB,MAAM,UAAU,CAAA;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAShD,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,MAAmC,EACnC,IAAgD;IAEhD,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACvC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACpD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,QAAQ,CAAA;IACpC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO;SACxB,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;SACb,GAAG,CACF,CAAC,CAAC,EAAE,EAAE,CACJ,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CACtG;SACA,IAAI,CAAC,IAAI,CAAC,CAAA;IAEb,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,YAAY,CAAC;QAClC,KAAK;QACL,MAAM,EAAE,oBAAoB;QAC5B,MAAM,EAAE,aAAa,IAAI,CAAC,OAAO,CAAC,MAAM,sCAAsC,KAAK,+MAA+M,MAAM,EAAE;QAC1S,WAAW,EAAE,GAAG;KACjB,CAAC,CAAA;IACF,OAAO,IAAI,CAAC,IAAI,EAAE,CAAA;AACpB,CAAC","sourcesContent":["// Period digest — synthesise many activity entries into a short manager recap.\n\nimport { generateText } from 'ai'\nimport { getModel, type AiConfig } from './client'\nimport { ENTRY_WRITING_SYSTEM } from './prompts'\n\nexport type DigestEntry = {\n date: string\n author?: string | null\n location?: string | null\n text: string\n}\n\n/** Summarise a batch of activity entries. Null when AI is unconfigured or empty. */\nexport async function generateDigest(\n config: AiConfig | null | undefined,\n args: { scope?: string; entries: DigestEntry[] },\n): Promise<string | null> {\n const model = getModel(config, 'smart')\n if (!model || args.entries.length === 0) return null\n const scope = args.scope ?? 'recent'\n const corpus = args.entries\n .slice(0, 200)\n .map(\n (e) =>\n `- [${e.date}${e.location ? ` · ${e.location}` : ''}${e.author ? ` · ${e.author}` : ''}] ${e.text}`,\n )\n .join('\\n')\n\n const { text } = await generateText({\n model,\n system: ENTRY_WRITING_SYSTEM,\n prompt: `Below are ${args.entries.length} activity entries. Write a concise ${scope} digest for a manager: the main activity and themes, anything notable or recurring, and any follow-ups or action items mentioned. Use 4–8 sentences of plain prose with no preamble or bullet list.\\n\\n---\\n${corpus}`,\n temperature: 0.4,\n })\n return text.trim()\n}\n"]}
|
package/doc-chat.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type AiConfig } from './client.js';
|
|
2
|
+
export type DocChatMessage = {
|
|
3
|
+
role: 'user' | 'assistant';
|
|
4
|
+
content: string;
|
|
5
|
+
};
|
|
6
|
+
export type DocAgentToolImpl = {
|
|
7
|
+
/** Fresh plain text of the current document. */
|
|
8
|
+
readDocument: () => Promise<string>;
|
|
9
|
+
/** Exact-match text edits; returns per-edit occurrence counts. */
|
|
10
|
+
editDocument: (edits: {
|
|
11
|
+
find: string;
|
|
12
|
+
replace: string;
|
|
13
|
+
}[]) => Promise<{
|
|
14
|
+
find: string;
|
|
15
|
+
count: number;
|
|
16
|
+
}[]>;
|
|
17
|
+
/** Replace the entire document with new HTML content. */
|
|
18
|
+
writeDocument: (html: string) => Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
export type DocAgentResult = {
|
|
21
|
+
text: string;
|
|
22
|
+
/** Human-readable notes about what the agent did (shown in the panel). */
|
|
23
|
+
actions: string[];
|
|
24
|
+
docChanged: boolean;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Run one agent turn (multi-step tool loop) for the document assistant.
|
|
28
|
+
* Throws AIDisabledError when the tenant has no model configured.
|
|
29
|
+
*/
|
|
30
|
+
export declare function runDocAgent(config: AiConfig | null | undefined, args: {
|
|
31
|
+
messages: DocChatMessage[];
|
|
32
|
+
docText?: string;
|
|
33
|
+
tools: DocAgentToolImpl;
|
|
34
|
+
maxSteps?: number;
|
|
35
|
+
}): Promise<DocAgentResult>;
|
|
36
|
+
//# sourceMappingURL=doc-chat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doc-chat.d.ts","sourceRoot":"","sources":["../src/doc-chat.ts"],"names":[],"mappings":"AAQA,OAAO,EAA6B,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AAEnE,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,gDAAgD;IAChD,YAAY,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;IACnC,kEAAkE;IAClE,YAAY,EAAE,CACZ,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,KACvC,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAAA;IAC/C,yDAAyD;IACzD,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/C,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,0EAA0E;IAC1E,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,UAAU,EAAE,OAAO,CAAA;CACpB,CAAA;AAgBD;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,EACnC,IAAI,EAAE;IACJ,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,gBAAgB,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,GACA,OAAO,CAAC,cAAc,CAAC,CAyFzB"}
|