dsh-agy-cli 0.0.0-stage → 0.12.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 +31 -0
- package/README.en.md +48 -0
- package/README.md +67 -2
- package/agents/dsh-agy-image-view.md +17 -0
- package/agents/dsh-agy-mcp-executor.md +32 -0
- package/agents/dsh-agy-read-only.md +20 -0
- package/agents/dsh-agy-tool-free.md +16 -0
- package/agents/dsh-agy-workspace-write.md +23 -0
- package/bin/dsh-agy-cli.js +9 -0
- package/bin/win32-x64/agy-launcher.exe +0 -0
- package/cordis.patch.yml +16 -0
- package/dist/client.js +4259 -0
- package/lib/agent-installer.d.ts +66 -0
- package/lib/agent-installer.js +147 -0
- package/lib/agent-presets.d.ts +42 -0
- package/lib/agent-presets.js +130 -0
- package/lib/agents-cli.d.ts +3 -0
- package/lib/agents-cli.js +110 -0
- package/lib/agy/diagnostics.d.ts +29 -0
- package/lib/agy/diagnostics.js +109 -0
- package/lib/agy/errors.d.ts +20 -0
- package/lib/agy/errors.js +77 -0
- package/lib/agy/experimental-transport.d.ts +79 -0
- package/lib/agy/experimental-transport.js +612 -0
- package/lib/agy/keepalive.d.ts +140 -0
- package/lib/agy/keepalive.js +445 -0
- package/lib/agy/limiter.d.ts +32 -0
- package/lib/agy/limiter.js +111 -0
- package/lib/agy/log.d.ts +214 -0
- package/lib/agy/log.js +503 -0
- package/lib/agy/models.d.ts +49 -0
- package/lib/agy/models.js +203 -0
- package/lib/agy/overrides.d.ts +17 -0
- package/lib/agy/overrides.js +73 -0
- package/lib/agy/parser.d.ts +102 -0
- package/lib/agy/parser.js +492 -0
- package/lib/agy/persistent-transport.d.ts +101 -0
- package/lib/agy/persistent-transport.js +753 -0
- package/lib/agy/process.d.ts +144 -0
- package/lib/agy/process.js +452 -0
- package/lib/agy/redact.d.ts +3 -0
- package/lib/agy/redact.js +24 -0
- package/lib/agy/stream-protocol.d.ts +50 -0
- package/lib/agy/stream-protocol.js +156 -0
- package/lib/agy/windows-launcher.d.ts +21 -0
- package/lib/agy/windows-launcher.js +76 -0
- package/lib/diagnostics.d.ts +78 -0
- package/lib/diagnostics.js +162 -0
- package/lib/doctor.d.ts +138 -0
- package/lib/doctor.js +940 -0
- package/lib/dsh/context.d.ts +133 -0
- package/lib/dsh/context.js +352 -0
- package/lib/dsh-home.d.ts +6 -0
- package/lib/dsh-home.js +11 -0
- package/lib/index.d.ts +54 -0
- package/lib/index.js +267 -0
- package/lib/mcp/agy-registration.d.ts +70 -0
- package/lib/mcp/agy-registration.js +245 -0
- package/lib/mcp/bridge.d.ts +73 -0
- package/lib/mcp/bridge.js +295 -0
- package/lib/mcp/endpoint.d.ts +41 -0
- package/lib/mcp/endpoint.js +142 -0
- package/lib/mcp/protocol.d.ts +107 -0
- package/lib/mcp/protocol.js +197 -0
- package/lib/mcp/tool-projection.d.ts +64 -0
- package/lib/mcp/tool-projection.js +168 -0
- package/lib/provider/agy.d.ts +153 -0
- package/lib/provider/agy.js +2159 -0
- package/lib/provider/config.d.ts +226 -0
- package/lib/provider/config.js +294 -0
- package/lib/provider/error-codes.d.ts +22 -0
- package/lib/provider/error-codes.js +29 -0
- package/lib/provider/image-bridge.d.ts +60 -0
- package/lib/provider/image-bridge.js +290 -0
- package/lib/provider/mock.d.ts +15 -0
- package/lib/provider/mock.js +78 -0
- package/lib/provider/prompt-budget.d.ts +45 -0
- package/lib/provider/prompt-budget.js +596 -0
- package/lib/provider/serialize.d.ts +15 -0
- package/lib/provider/serialize.js +82 -0
- package/lib/provider/tool-protocol.d.ts +80 -0
- package/lib/provider/tool-protocol.js +848 -0
- package/lib/provider/tool-schema-file.d.ts +11 -0
- package/lib/provider/tool-schema-file.js +27 -0
- package/lib/session/file-store.d.ts +56 -0
- package/lib/session/file-store.js +186 -0
- package/lib/session/store.d.ts +49 -0
- package/lib/session/store.js +62 -0
- package/lib/web/router.d.ts +19 -0
- package/lib/web/router.js +994 -0
- package/package.json +115 -4
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, mkdtemp, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { dshHome } from '../dsh-home.js';
|
|
6
|
+
import { serializeAgyPrompt, serializeAgyTurnPrompt } from './serialize.js';
|
|
7
|
+
export const IMAGE_MEDIA_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
|
|
8
|
+
/** Placeholder media type for a history image of an unsupported kind. */
|
|
9
|
+
export const FALLBACK_IMAGE_MEDIA_TYPE = 'image/png';
|
|
10
|
+
export const DEFAULT_IMAGE_MAX_BYTES = 8 * 1024 * 1024;
|
|
11
|
+
export const DEFAULT_IMAGE_MAX_COUNT = 4;
|
|
12
|
+
/**
|
|
13
|
+
* Images carried by the turn being answered are staged into a per-request temp
|
|
14
|
+
* directory and counted against `DEFAULT_IMAGE_MAX_COUNT`. Images from earlier
|
|
15
|
+
* turns are not: the request only accepts a handful of new ones, and rewriting
|
|
16
|
+
* every historical attachment on every turn is pure cost. They are materialized
|
|
17
|
+
* once into this stable cache instead, so their placeholder can still be opened
|
|
18
|
+
* on demand by the image-view Agent (or by the user in Explorer) long after the
|
|
19
|
+
* turn that uploaded them is over.
|
|
20
|
+
*/
|
|
21
|
+
export const IMAGE_HISTORY_CACHE_DIR = join(dshHome(), 'agy-cli', 'image-cache');
|
|
22
|
+
const IMAGE_HISTORY_CACHE_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
23
|
+
export class AgyImageBridgeError extends Error {
|
|
24
|
+
code;
|
|
25
|
+
constructor(message, code, options) {
|
|
26
|
+
super(message, options);
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.name = 'AgyImageBridgeError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const EXTENSIONS = {
|
|
32
|
+
'image/png': '.png',
|
|
33
|
+
'image/jpeg': '.jpg',
|
|
34
|
+
'image/webp': '.webp',
|
|
35
|
+
'image/gif': '.gif',
|
|
36
|
+
};
|
|
37
|
+
function imageRefsOf(content, output = []) {
|
|
38
|
+
for (const block of content) {
|
|
39
|
+
if (block.type === 'image')
|
|
40
|
+
output.push(block.attachment);
|
|
41
|
+
}
|
|
42
|
+
return output;
|
|
43
|
+
}
|
|
44
|
+
function imageKey(ref) {
|
|
45
|
+
return String(ref.attachmentId);
|
|
46
|
+
}
|
|
47
|
+
function mediaTypeOf(value) {
|
|
48
|
+
return IMAGE_MEDIA_TYPES.includes(value);
|
|
49
|
+
}
|
|
50
|
+
function placeholderFor(image) {
|
|
51
|
+
const meta = `media_type=${image.mediaType} width=${image.width} height=${image.height}`;
|
|
52
|
+
if (image.path === undefined) {
|
|
53
|
+
return `[IMAGE_ATTACHMENT ${meta} earlier_turn=unavailable]`;
|
|
54
|
+
}
|
|
55
|
+
return image.history
|
|
56
|
+
? `[IMAGE_ATTACHMENT file=${image.path} ${meta} earlier_turn=cached]`
|
|
57
|
+
: `[IMAGE_ATTACHMENT file=${image.path} ${meta}]`;
|
|
58
|
+
}
|
|
59
|
+
function replaceImageBlocks(content, images) {
|
|
60
|
+
return content.map(block => {
|
|
61
|
+
if (block.type === 'image') {
|
|
62
|
+
const image = images.get(imageKey(block.attachment));
|
|
63
|
+
if (image === undefined) {
|
|
64
|
+
throw new AgyImageBridgeError('Image attachment was not staged', 'IMAGE_STAGING_FAILED');
|
|
65
|
+
}
|
|
66
|
+
return { type: 'text', text: placeholderFor(image) };
|
|
67
|
+
}
|
|
68
|
+
return block;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function replaceMessageImages(messages, images) {
|
|
72
|
+
return messages.map(message => ({
|
|
73
|
+
...message,
|
|
74
|
+
content: replaceImageBlocks(message.content, images),
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
function validateStoredImage(requested, stored, totalBytes, maxBytes) {
|
|
78
|
+
const ref = stored.ref;
|
|
79
|
+
if (String(ref.attachmentId) !== String(requested.attachmentId)) {
|
|
80
|
+
throw new AgyImageBridgeError('Attachment store returned a different image reference', 'IMAGE_READ_FAILED');
|
|
81
|
+
}
|
|
82
|
+
if (!mediaTypeOf(ref.mediaType)) {
|
|
83
|
+
throw new AgyImageBridgeError(`Unsupported image media type: ${ref.mediaType}`, 'IMAGE_UNSUPPORTED_MEDIA_TYPE');
|
|
84
|
+
}
|
|
85
|
+
if (stored.data.byteLength !== ref.bytes || ref.bytes !== requested.bytes) {
|
|
86
|
+
throw new AgyImageBridgeError('Image attachment byte length failed verification', 'IMAGE_READ_FAILED');
|
|
87
|
+
}
|
|
88
|
+
const nextTotalBytes = totalBytes + stored.data.byteLength;
|
|
89
|
+
if (nextTotalBytes > maxBytes) {
|
|
90
|
+
throw new AgyImageBridgeError(`Image attachments exceed the ${maxBytes}-byte staging limit`, 'IMAGE_SIZE_LIMIT');
|
|
91
|
+
}
|
|
92
|
+
return { mediaType: ref.mediaType, nextTotalBytes };
|
|
93
|
+
}
|
|
94
|
+
async function stageImages(refs, options) {
|
|
95
|
+
const store = options.attachmentStore;
|
|
96
|
+
if (store === undefined) {
|
|
97
|
+
throw new AgyImageBridgeError('Image input requires an available DSH AttachmentStore; configure text-only mode or install the attachment service', 'IMAGE_ATTACHMENT_UNAVAILABLE');
|
|
98
|
+
}
|
|
99
|
+
const maxBytes = options.maxBytes ?? DEFAULT_IMAGE_MAX_BYTES;
|
|
100
|
+
const maxCount = options.maxCount ?? DEFAULT_IMAGE_MAX_COUNT;
|
|
101
|
+
const uniqueRefs = [...new Map(refs.map(ref => [imageKey(ref), ref])).values()];
|
|
102
|
+
// Only the turn being answered is staged, so this budget is per message, not
|
|
103
|
+
// per conversation: earlier images are reference-only placeholders.
|
|
104
|
+
if (uniqueRefs.length > maxCount) {
|
|
105
|
+
throw new AgyImageBridgeError(`Agy image bridge accepts at most ${maxCount} images per request`, 'IMAGE_SIZE_LIMIT');
|
|
106
|
+
}
|
|
107
|
+
let directory;
|
|
108
|
+
try {
|
|
109
|
+
directory = await mkdtemp(join(tmpdir(), 'dsh-agy-image-'));
|
|
110
|
+
const images = new Map();
|
|
111
|
+
let totalBytes = 0;
|
|
112
|
+
for (const [index, ref] of uniqueRefs.entries()) {
|
|
113
|
+
let stored;
|
|
114
|
+
try {
|
|
115
|
+
stored = await store.readImage(ref, options.signal);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error instanceof AgyImageBridgeError)
|
|
119
|
+
throw error;
|
|
120
|
+
throw new AgyImageBridgeError('AttachmentStore could not read the image', 'IMAGE_READ_FAILED', { cause: error });
|
|
121
|
+
}
|
|
122
|
+
const validation = validateStoredImage(ref, stored, totalBytes, maxBytes);
|
|
123
|
+
totalBytes = validation.nextTotalBytes;
|
|
124
|
+
const path = join(directory, `image-${index}-${randomUUID()}${EXTENSIONS[validation.mediaType]}`);
|
|
125
|
+
await writeFile(path, stored.data, { flag: 'wx', mode: 0o600 });
|
|
126
|
+
images.set(imageKey(ref), {
|
|
127
|
+
path,
|
|
128
|
+
mediaType: validation.mediaType,
|
|
129
|
+
width: stored.ref.width,
|
|
130
|
+
height: stored.ref.height,
|
|
131
|
+
history: false,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
let cleaned = false;
|
|
135
|
+
const cleanup = async () => {
|
|
136
|
+
if (cleaned || directory === undefined)
|
|
137
|
+
return;
|
|
138
|
+
cleaned = true;
|
|
139
|
+
await rm(directory, { recursive: true, force: true });
|
|
140
|
+
};
|
|
141
|
+
return { directory, images, cleanup };
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (directory !== undefined)
|
|
145
|
+
await rm(directory, { recursive: true, force: true }).catch(() => undefined);
|
|
146
|
+
if (error instanceof AgyImageBridgeError)
|
|
147
|
+
throw error;
|
|
148
|
+
throw new AgyImageBridgeError(`Could not stage image attachments: ${error instanceof Error ? error.message : String(error)}`, 'IMAGE_STAGING_FAILED', { cause: error });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function cacheFileName(ref, mediaType) {
|
|
152
|
+
return `${String(ref.attachmentId).replace(/[^A-Za-z0-9._-]/g, '_')}${EXTENSIONS[mediaType]}`;
|
|
153
|
+
}
|
|
154
|
+
let cachePruned = false;
|
|
155
|
+
/** Best-effort age-based pruning; a stale cache file costs disk, never correctness. */
|
|
156
|
+
async function pruneImageHistoryCache() {
|
|
157
|
+
if (cachePruned)
|
|
158
|
+
return;
|
|
159
|
+
cachePruned = true;
|
|
160
|
+
try {
|
|
161
|
+
const entries = await readdir(IMAGE_HISTORY_CACHE_DIR);
|
|
162
|
+
const now = Date.now();
|
|
163
|
+
for (const entry of entries) {
|
|
164
|
+
const path = join(IMAGE_HISTORY_CACHE_DIR, entry);
|
|
165
|
+
const info = await stat(path).catch(() => undefined);
|
|
166
|
+
if (info !== undefined && info.isFile() && now - info.mtimeMs > IMAGE_HISTORY_CACHE_MAX_AGE_MS) {
|
|
167
|
+
await rm(path, { force: true }).catch(() => undefined);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// No cache directory yet, or it is unreadable: the bridge still works, it
|
|
173
|
+
// just has nothing to reuse.
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Materialize earlier turns' images into the stable cache. A failure here is not
|
|
178
|
+
* fatal on purpose: one unreadable attachment must not resurrect the very
|
|
179
|
+
* problem this path exists to fix, so it degrades to a placeholder that says
|
|
180
|
+
* `earlier_turn=unavailable` instead of failing the whole request.
|
|
181
|
+
*/
|
|
182
|
+
async function cacheHistoryImages(refs, options) {
|
|
183
|
+
const images = new Map();
|
|
184
|
+
const store = options.attachmentStore;
|
|
185
|
+
await pruneImageHistoryCache();
|
|
186
|
+
for (const ref of refs) {
|
|
187
|
+
const key = imageKey(ref);
|
|
188
|
+
if (!mediaTypeOf(ref.mediaType)) {
|
|
189
|
+
// An unsupported media type degrades to the same unavailable placeholder
|
|
190
|
+
// as an unreadable attachment: omitting the entry would make
|
|
191
|
+
// replaceImageBlocks throw IMAGE_STAGING_FAILED for every later turn of
|
|
192
|
+
// this conversation ("one unreadable attachment must not be fatal").
|
|
193
|
+
images.set(key, { path: undefined, mediaType: FALLBACK_IMAGE_MEDIA_TYPE, width: ref.width ?? 0, height: ref.height ?? 0, history: true });
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const mediaType = ref.mediaType;
|
|
197
|
+
const width = ref.width ?? 0;
|
|
198
|
+
const height = ref.height ?? 0;
|
|
199
|
+
if (store === undefined) {
|
|
200
|
+
images.set(key, { path: undefined, mediaType, width, height, history: true });
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const path = join(IMAGE_HISTORY_CACHE_DIR, cacheFileName(ref, mediaType));
|
|
204
|
+
try {
|
|
205
|
+
await mkdir(IMAGE_HISTORY_CACHE_DIR, { recursive: true });
|
|
206
|
+
if ((await stat(path).catch(() => undefined)) === undefined) {
|
|
207
|
+
const stored = await store.readImage(ref, options.signal);
|
|
208
|
+
await writeFile(path, stored.data, { flag: 'w', mode: 0o600 });
|
|
209
|
+
}
|
|
210
|
+
images.set(key, { path, mediaType, width, height, history: true });
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
images.set(key, { path: undefined, mediaType, width, height, history: true });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return images;
|
|
217
|
+
}
|
|
218
|
+
function lastAssistantIndex(messages) {
|
|
219
|
+
let index = -1;
|
|
220
|
+
messages.forEach((message, at) => {
|
|
221
|
+
if (message.role === 'assistant')
|
|
222
|
+
index = at;
|
|
223
|
+
});
|
|
224
|
+
return index;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Prepare text prompts for the experimental file bridge.
|
|
228
|
+
*
|
|
229
|
+
* Only the turn being answered is staged: the place budget belongs to one
|
|
230
|
+
* message, and a conversation accumulates images forever, so counting the whole
|
|
231
|
+
* replayed history used to kill every later turn once a fifth image existed.
|
|
232
|
+
* Earlier turns keep a placeholder that still points at a real file under
|
|
233
|
+
* `IMAGE_HISTORY_CACHE_DIR`, so nothing is thrown away — the image-view Agent
|
|
234
|
+
* can open it the moment the model actually needs it, one turn or fifty turns
|
|
235
|
+
* later, and the user can open the same path in Explorer.
|
|
236
|
+
*/
|
|
237
|
+
export async function prepareAgyPrompts(options, bridgeOptions) {
|
|
238
|
+
const anyImage = imageRefsOf(options.messages.flatMap(message => message.content));
|
|
239
|
+
if (anyImage.length === 0 || !bridgeOptions.enabled) {
|
|
240
|
+
return {
|
|
241
|
+
fullPrompt: serializeAgyPrompt(options),
|
|
242
|
+
turnPrompt: undefined,
|
|
243
|
+
imageDirectory: undefined,
|
|
244
|
+
imageCacheDirectory: undefined,
|
|
245
|
+
imagePaths: [],
|
|
246
|
+
bridgeActive: false,
|
|
247
|
+
cleanup: async () => undefined,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
if (!bridgeOptions.agentCanViewFile) {
|
|
251
|
+
throw new AgyImageBridgeError('Experimental image input requires the bundled image-view Agent with view_file', 'IMAGE_AGENT_UNSUPPORTED');
|
|
252
|
+
}
|
|
253
|
+
// Everything after the last assistant message is the turn being answered;
|
|
254
|
+
// `serializeAgyTurnPrompt` slices at exactly the same boundary.
|
|
255
|
+
const turnStart = lastAssistantIndex(options.messages) + 1;
|
|
256
|
+
const currentRefs = [];
|
|
257
|
+
const historyRefs = [];
|
|
258
|
+
options.messages.forEach((message, index) => {
|
|
259
|
+
const bucket = index >= turnStart ? currentRefs : historyRefs;
|
|
260
|
+
for (const ref of imageRefsOf(message.content))
|
|
261
|
+
bucket.push(ref);
|
|
262
|
+
});
|
|
263
|
+
const staged = currentRefs.length === 0
|
|
264
|
+
? { directory: undefined, images: new Map(), cleanup: async () => undefined }
|
|
265
|
+
: await stageImages(currentRefs, bridgeOptions);
|
|
266
|
+
const history = historyRefs.length === 0
|
|
267
|
+
? new Map()
|
|
268
|
+
: await cacheHistoryImages(historyRefs, bridgeOptions);
|
|
269
|
+
const images = new Map([...history, ...staged.images]);
|
|
270
|
+
try {
|
|
271
|
+
const messages = replaceMessageImages(options.messages, images);
|
|
272
|
+
return {
|
|
273
|
+
fullPrompt: serializeAgyPrompt({
|
|
274
|
+
...(options.system === undefined ? {} : { system: options.system }),
|
|
275
|
+
messages,
|
|
276
|
+
}),
|
|
277
|
+
turnPrompt: serializeAgyTurnPrompt({ messages }),
|
|
278
|
+
imageDirectory: staged.directory,
|
|
279
|
+
imageCacheDirectory: historyRefs.length === 0 ? undefined : IMAGE_HISTORY_CACHE_DIR,
|
|
280
|
+
imagePaths: [...images.values()].flatMap(image => (image.path === undefined ? [] : [image.path])),
|
|
281
|
+
bridgeActive: true,
|
|
282
|
+
cleanup: staged.cleanup,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
await staged.cleanup();
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=image-bridge.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { LlmAdapter } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
import { type Config } from './config.js';
|
|
4
|
+
export declare class MockAdapter extends LlmAdapter {
|
|
5
|
+
private readonly model;
|
|
6
|
+
private readonly models;
|
|
7
|
+
private readonly response;
|
|
8
|
+
private readonly delayMs;
|
|
9
|
+
constructor(config?: Config);
|
|
10
|
+
providerInfo(provider: string): LlmProviderInfo;
|
|
11
|
+
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
12
|
+
resolveModel(provider: string, model: string, _signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
|
|
13
|
+
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=mock.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { configuredModels, normalizeModelId } from './config.js';
|
|
3
|
+
const DEFAULT_PROVIDER = 'agy-mock';
|
|
4
|
+
const DEFAULT_MOCK_MODEL = 'agy-mock-model';
|
|
5
|
+
const DEFAULT_RESPONSE = 'AGY mock provider is ready.';
|
|
6
|
+
function abortError() {
|
|
7
|
+
return new LlmError('AGY mock request aborted by caller', 'ABORTED');
|
|
8
|
+
}
|
|
9
|
+
function wait(ms, signal) {
|
|
10
|
+
if (ms === 0)
|
|
11
|
+
return Promise.resolve();
|
|
12
|
+
if (signal?.aborted)
|
|
13
|
+
return Promise.reject(abortError());
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const onAbort = () => {
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
signal?.removeEventListener('abort', onAbort);
|
|
18
|
+
reject(abortError());
|
|
19
|
+
};
|
|
20
|
+
// The happy path must release its listener too: without this, every timed
|
|
21
|
+
// mock stream leaves one 'abort' listener attached until the signal dies.
|
|
22
|
+
const timer = setTimeout(() => {
|
|
23
|
+
signal?.removeEventListener('abort', onAbort);
|
|
24
|
+
resolve();
|
|
25
|
+
}, ms);
|
|
26
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export class MockAdapter extends LlmAdapter {
|
|
30
|
+
model;
|
|
31
|
+
models;
|
|
32
|
+
response;
|
|
33
|
+
delayMs;
|
|
34
|
+
constructor(config = {}) {
|
|
35
|
+
super();
|
|
36
|
+
this.model = config.model !== undefined ? normalizeModelId(config.model) : DEFAULT_MOCK_MODEL;
|
|
37
|
+
this.models = configuredModels({ ...config, model: this.model }).map(m => ({ ...m, id: normalizeModelId(m.id) }));
|
|
38
|
+
this.response = config.response ?? DEFAULT_RESPONSE;
|
|
39
|
+
this.delayMs = config.delayMs ?? 0;
|
|
40
|
+
}
|
|
41
|
+
providerInfo(provider) {
|
|
42
|
+
return { id: provider, name: 'AGY (Mock)' };
|
|
43
|
+
}
|
|
44
|
+
listModels(provider) {
|
|
45
|
+
return Promise.resolve(this.models.map(model => ({
|
|
46
|
+
provider,
|
|
47
|
+
id: model.id,
|
|
48
|
+
name: model.name ?? 'AGY Mock Model',
|
|
49
|
+
description: model.description ?? 'DSH Provider contract probe; does not consume AGY quota.',
|
|
50
|
+
inputModalities: ['text'],
|
|
51
|
+
})));
|
|
52
|
+
}
|
|
53
|
+
resolveModel(provider, model, _signal) {
|
|
54
|
+
const configured = this.models.find(entry => entry.id === model);
|
|
55
|
+
return Promise.resolve({
|
|
56
|
+
provider,
|
|
57
|
+
id: model,
|
|
58
|
+
name: configured?.name ?? model,
|
|
59
|
+
...(configured?.description === undefined ? {} : { description: configured.description }),
|
|
60
|
+
inputModalities: ['text'],
|
|
61
|
+
context: { contextWindow: configured?.contextWindow ?? 128_000 },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async *stream(options) {
|
|
65
|
+
if (options.signal?.aborted)
|
|
66
|
+
throw abortError();
|
|
67
|
+
const text = this.response;
|
|
68
|
+
yield { type: 'block-start', index: 0, blockType: 'text' };
|
|
69
|
+
await wait(this.delayMs, options.signal);
|
|
70
|
+
if (options.signal?.aborted)
|
|
71
|
+
throw abortError();
|
|
72
|
+
yield { type: 'text-delta', index: 0, text };
|
|
73
|
+
yield { type: 'block-end', index: 0, block: { type: 'text', text } };
|
|
74
|
+
yield { type: 'usage', usage: { inputTokens: 0, outputTokens: text.length } };
|
|
75
|
+
yield { type: 'finish', reason: { kind: 'stop' } };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=mock.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export declare const DEFAULT_INPUT_FRAME_LIMIT_BYTES: number;
|
|
2
|
+
export declare const DEFAULT_AGY_PROMPT_CONTENT_LIMIT_BYTES: number;
|
|
3
|
+
export declare const DEFAULT_DSH_TOOL_PROMPT_CONTENT_LIMIT_BYTES: number;
|
|
4
|
+
export declare const DEFAULT_MAX_SINGLE_TOOL_RESULT_BYTES: number;
|
|
5
|
+
export declare const DEFAULT_MAX_HISTORICAL_TOOL_RESULT_BYTES: number;
|
|
6
|
+
export declare class AgyPromptBudgetError extends Error {
|
|
7
|
+
readonly actualBytes: number;
|
|
8
|
+
readonly limitBytes: number;
|
|
9
|
+
readonly code: "AGY_INPUT_TOO_LARGE";
|
|
10
|
+
constructor(actualBytes: number, limitBytes: number);
|
|
11
|
+
}
|
|
12
|
+
export interface AgyPromptBudgetOptions {
|
|
13
|
+
maxFrameBytes: number;
|
|
14
|
+
maxPromptBytes?: number;
|
|
15
|
+
maxSingleToolResultBytes?: number;
|
|
16
|
+
maxHistoricalToolResultBytes?: number;
|
|
17
|
+
/**
|
|
18
|
+
* What to do when a single serialized section alone exceeds the prompt
|
|
19
|
+
* budget. `'reject'` (default) throws AGY_INPUT_TOO_LARGE; `'clamp'` clips
|
|
20
|
+
* that section's head and keeps its tail. The main turn stays on `'reject'`:
|
|
21
|
+
* silently losing the beginning of the user's own message is worse than a
|
|
22
|
+
* clean error. Side-channel requests (a session-title or compaction summary
|
|
23
|
+
* over the whole conversation, which arrive as one oversized section) use
|
|
24
|
+
* `'clamp'` — they have no tool protocol to protect, the newest content is
|
|
25
|
+
* what a summary needs, and failing them outright only stalls the panel.
|
|
26
|
+
*/
|
|
27
|
+
oversizedSectionPolicy?: 'reject' | 'clamp';
|
|
28
|
+
}
|
|
29
|
+
export interface AgyPromptBudgetResult {
|
|
30
|
+
readonly prompt: string;
|
|
31
|
+
readonly promptBytes: number;
|
|
32
|
+
readonly promptLimitBytes: number;
|
|
33
|
+
readonly frameBytes: number;
|
|
34
|
+
readonly frameLimitBytes: number;
|
|
35
|
+
readonly toolResultCount: number;
|
|
36
|
+
readonly largestToolResultBytes: number;
|
|
37
|
+
readonly truncatedToolResultCount: number;
|
|
38
|
+
readonly historyCompacted: boolean;
|
|
39
|
+
readonly omittedMessageCount: number;
|
|
40
|
+
}
|
|
41
|
+
/** Return the immutable prefix before the first dynamic DSH history section. */
|
|
42
|
+
export declare function stableAgyPromptPrefix(prompt: string): string;
|
|
43
|
+
/** Bound historical DSH tool output before the prompt is sent to AGY. */
|
|
44
|
+
export declare function boundAgyPrompt(prompt: string, options: AgyPromptBudgetOptions): AgyPromptBudgetResult;
|
|
45
|
+
//# sourceMappingURL=prompt-budget.d.ts.map
|