@archships/dim-agent-sdk 0.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/README.md +69 -0
- package/dist/agent-core/Agent.d.ts +29 -0
- package/dist/agent-core/LoopRunner.d.ts +30 -0
- package/dist/agent-core/MessageFactory.d.ts +20 -0
- package/dist/agent-core/ModelTurnCollector.d.ts +22 -0
- package/dist/agent-core/Session.d.ts +48 -0
- package/dist/agent-core/TerminationPolicy.d.ts +14 -0
- package/dist/agent-core/ToolExecutor.d.ts +27 -0
- package/dist/agent-core/agent-types.d.ts +26 -0
- package/dist/agent-core/createModel.d.ts +2 -0
- package/dist/agent-core/errors.d.ts +5 -0
- package/dist/agent-core/index.d.ts +13 -0
- package/dist/agent-core/session-state.d.ts +17 -0
- package/dist/agent-core/tool-call.d.ts +9 -0
- package/dist/context/AutoContextManager.d.ts +21 -0
- package/dist/context/index.d.ts +3 -0
- package/dist/context/types.d.ts +19 -0
- package/dist/contracts/common.d.ts +19 -0
- package/dist/contracts/content-normalize.d.ts +6 -0
- package/dist/contracts/content.d.ts +16 -0
- package/dist/contracts/event.d.ts +30 -0
- package/dist/contracts/index.d.ts +9 -0
- package/dist/contracts/message.d.ts +32 -0
- package/dist/contracts/model.d.ts +73 -0
- package/dist/contracts/state.d.ts +14 -0
- package/dist/contracts/tool-normalize.d.ts +3 -0
- package/dist/contracts/tool.d.ts +40 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +2997 -0
- package/dist/persistence/FileStateStore.d.ts +14 -0
- package/dist/persistence/InMemoryStateStore.d.ts +9 -0
- package/dist/persistence/SnapshotCodec.d.ts +20 -0
- package/dist/persistence/index.d.ts +6 -0
- package/dist/persistence/store.d.ts +7 -0
- package/dist/plugin-host/HookPipeline.d.ts +7 -0
- package/dist/plugin-host/PluginHost.d.ts +36 -0
- package/dist/plugin-host/helpers.d.ts +3 -0
- package/dist/plugin-host/index.d.ts +4 -0
- package/dist/plugin-host/types.d.ts +1 -0
- package/dist/providers/anthropic/adapter.d.ts +13 -0
- package/dist/providers/anthropic/mapper.d.ts +28 -0
- package/dist/providers/gemini/adapter.d.ts +12 -0
- package/dist/providers/gemini/mapper.d.ts +30 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/openai/adapter.d.ts +12 -0
- package/dist/providers/openai/mapper.d.ts +15 -0
- package/dist/providers/openai-responses/adapter.d.ts +12 -0
- package/dist/providers/openai-responses/mapper.d.ts +40 -0
- package/dist/providers/shared/http-error.d.ts +7 -0
- package/dist/providers/shared/provider-state.d.ts +4 -0
- package/dist/providers/shared/reasoning.d.ts +14 -0
- package/dist/providers/shared/tool-call.d.ts +5 -0
- package/dist/providers/shared/usage.d.ts +2 -0
- package/dist/services/ExecGateway.d.ts +14 -0
- package/dist/services/FileSystemGateway.d.ts +35 -0
- package/dist/services/GitGateway.d.ts +17 -0
- package/dist/services/ModelGateway.d.ts +7 -0
- package/dist/services/NetworkGateway.d.ts +6 -0
- package/dist/services/PermissionGateway.d.ts +11 -0
- package/dist/services/activity.d.ts +10 -0
- package/dist/services/index.d.ts +10 -0
- package/dist/services/permissions.d.ts +17 -0
- package/dist/services/types.d.ts +77 -0
- package/dist/tools/BaseTool.d.ts +6 -0
- package/dist/tools/ToolRegistry.d.ts +9 -0
- package/dist/tools/builtins/EditTool.d.ts +6 -0
- package/dist/tools/builtins/ExecTool.d.ts +6 -0
- package/dist/tools/builtins/ReadTool.d.ts +6 -0
- package/dist/tools/builtins/WriteTool.d.ts +6 -0
- package/dist/tools/builtins/index.d.ts +4 -0
- package/dist/tools/builtins/utils.d.ts +6 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/result.d.ts +3 -0
- package/dist/utils/guards.d.ts +2 -0
- package/dist/utils/id.d.ts +1 -0
- package/dist/utils/json.d.ts +3 -0
- package/dist/utils/usage.d.ts +3 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2997 @@
|
|
|
1
|
+
// src/utils/guards.ts
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function isStringArray(value) {
|
|
6
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// src/contracts/content-normalize.ts
|
|
10
|
+
function isTextContent(value) {
|
|
11
|
+
return isRecord(value) && value.type === "text" && typeof value.text === "string" && (value.annotations === undefined || isRecord(value.annotations)) && (value._meta === undefined || isRecord(value._meta));
|
|
12
|
+
}
|
|
13
|
+
function isImageContent(value) {
|
|
14
|
+
return isRecord(value) && value.type === "image" && typeof value.data === "string" && typeof value.mimeType === "string" && (value.annotations === undefined || isRecord(value.annotations)) && (value._meta === undefined || isRecord(value._meta));
|
|
15
|
+
}
|
|
16
|
+
function isContentBlock(value) {
|
|
17
|
+
return isTextContent(value) || isImageContent(value);
|
|
18
|
+
}
|
|
19
|
+
function normalizeContent(input) {
|
|
20
|
+
if (typeof input === "string") {
|
|
21
|
+
return [{ type: "text", text: input }];
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(input)) {
|
|
24
|
+
const blocks = input.filter((item) => item != null);
|
|
25
|
+
if (!blocks.every(isContentBlock))
|
|
26
|
+
throw new TypeError("Content array must contain only text or image blocks");
|
|
27
|
+
return blocks.map((block) => ({ ...block }));
|
|
28
|
+
}
|
|
29
|
+
if (!isContentBlock(input))
|
|
30
|
+
throw new TypeError("Content must be a string, content block, or content block array");
|
|
31
|
+
return [{ ...input }];
|
|
32
|
+
}
|
|
33
|
+
function contentToText(content) {
|
|
34
|
+
return content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
35
|
+
}
|
|
36
|
+
// src/context/AutoContextManager.ts
|
|
37
|
+
import path from "node:path";
|
|
38
|
+
|
|
39
|
+
// src/utils/id.ts
|
|
40
|
+
function createId(prefix) {
|
|
41
|
+
return `${prefix}_${crypto.randomUUID()}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/context/AutoContextManager.ts
|
|
45
|
+
class AutoContextManager {
|
|
46
|
+
services;
|
|
47
|
+
pluginHost;
|
|
48
|
+
maxItems;
|
|
49
|
+
maxChars;
|
|
50
|
+
constructor(options) {
|
|
51
|
+
this.services = options.services;
|
|
52
|
+
this.pluginHost = options.pluginHost;
|
|
53
|
+
this.maxItems = options.maxItems ?? 8;
|
|
54
|
+
this.maxChars = options.maxChars ?? 8000;
|
|
55
|
+
}
|
|
56
|
+
async resolve(options) {
|
|
57
|
+
const items = [];
|
|
58
|
+
if (options.cwd) {
|
|
59
|
+
items.push({
|
|
60
|
+
id: createId("ctx"),
|
|
61
|
+
type: "cwd",
|
|
62
|
+
title: "Working directory",
|
|
63
|
+
content: options.cwd,
|
|
64
|
+
priority: 10,
|
|
65
|
+
source: "agent"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
items.push(...await this.resolveExplicitFiles(options.input, options.cwd));
|
|
69
|
+
items.push(...await this.resolveRecentFiles(options.cwd));
|
|
70
|
+
items.push(...await this.resolveGitState(options.cwd));
|
|
71
|
+
const contributed = await this.pluginHost?.collectContext({
|
|
72
|
+
sessionId: options.sessionId,
|
|
73
|
+
input: options.input,
|
|
74
|
+
messages: options.messages,
|
|
75
|
+
cwd: options.cwd
|
|
76
|
+
});
|
|
77
|
+
if (contributed)
|
|
78
|
+
items.push(...contributed);
|
|
79
|
+
return budgetContext(items, this.maxItems, this.maxChars);
|
|
80
|
+
}
|
|
81
|
+
format(items) {
|
|
82
|
+
if (items.length === 0)
|
|
83
|
+
return [];
|
|
84
|
+
const sections = items.map((item) => {
|
|
85
|
+
return [`[${item.title}]`, item.content].join(`
|
|
86
|
+
`);
|
|
87
|
+
});
|
|
88
|
+
return [`Additional context for this turn:
|
|
89
|
+
|
|
90
|
+
${sections.join(`
|
|
91
|
+
|
|
92
|
+
`)}`];
|
|
93
|
+
}
|
|
94
|
+
async resolveExplicitFiles(input, cwd) {
|
|
95
|
+
const text = inputToText(input);
|
|
96
|
+
const candidates = extractFileCandidates(text);
|
|
97
|
+
const items = [];
|
|
98
|
+
for (const candidate of candidates) {
|
|
99
|
+
if (!cwd)
|
|
100
|
+
break;
|
|
101
|
+
if (!await this.services.fileSystem.exists(candidate, { cwd }))
|
|
102
|
+
continue;
|
|
103
|
+
const content = await safeReadText(this.services, candidate, cwd);
|
|
104
|
+
if (!content)
|
|
105
|
+
continue;
|
|
106
|
+
items.push({
|
|
107
|
+
id: createId("ctx"),
|
|
108
|
+
type: "file",
|
|
109
|
+
title: `File: ${candidate}`,
|
|
110
|
+
content,
|
|
111
|
+
priority: 100,
|
|
112
|
+
source: "explicit-file"
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return items;
|
|
116
|
+
}
|
|
117
|
+
async resolveRecentFiles(cwd) {
|
|
118
|
+
if (!cwd || !this.pluginHost)
|
|
119
|
+
return [];
|
|
120
|
+
const items = [];
|
|
121
|
+
for (const filePath of this.pluginHost.recentFiles(3)) {
|
|
122
|
+
const relative = toRelative(cwd, filePath);
|
|
123
|
+
const content = await safeReadText(this.services, relative, cwd);
|
|
124
|
+
if (!content)
|
|
125
|
+
continue;
|
|
126
|
+
items.push({
|
|
127
|
+
id: createId("ctx"),
|
|
128
|
+
type: "file",
|
|
129
|
+
title: `Recent file: ${relative}`,
|
|
130
|
+
content,
|
|
131
|
+
priority: 70,
|
|
132
|
+
source: "recent-file"
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return items;
|
|
136
|
+
}
|
|
137
|
+
async resolveGitState(cwd) {
|
|
138
|
+
if (!cwd)
|
|
139
|
+
return [];
|
|
140
|
+
const items = [];
|
|
141
|
+
const status = await this.services.git.status({ cwd });
|
|
142
|
+
if (status) {
|
|
143
|
+
items.push({
|
|
144
|
+
id: createId("ctx"),
|
|
145
|
+
type: "git_status",
|
|
146
|
+
title: "Git status",
|
|
147
|
+
content: status,
|
|
148
|
+
priority: 60,
|
|
149
|
+
source: "git"
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const diff = await this.services.git.diff({ cwd });
|
|
153
|
+
if (diff) {
|
|
154
|
+
items.push({
|
|
155
|
+
id: createId("ctx"),
|
|
156
|
+
type: "git_diff",
|
|
157
|
+
title: "Git diff",
|
|
158
|
+
content: trimText(diff, 4000),
|
|
159
|
+
priority: 50,
|
|
160
|
+
source: "git"
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return items;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function inputToText(input) {
|
|
167
|
+
return contentToText(normalizeContent(input));
|
|
168
|
+
}
|
|
169
|
+
function extractFileCandidates(text) {
|
|
170
|
+
const matches = text.match(/(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+\.[A-Za-z0-9_-]+/g) ?? [];
|
|
171
|
+
return [...new Set(matches)];
|
|
172
|
+
}
|
|
173
|
+
function trimText(text, maxChars) {
|
|
174
|
+
return text.length <= maxChars ? text : `${text.slice(0, maxChars)}
|
|
175
|
+
...`;
|
|
176
|
+
}
|
|
177
|
+
async function safeReadText(services, filePath, cwd) {
|
|
178
|
+
try {
|
|
179
|
+
const text = await services.fileSystem.readText(filePath, { cwd });
|
|
180
|
+
return trimText(text, 2000);
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function budgetContext(items, maxItems, maxChars) {
|
|
186
|
+
const sorted = [...items].sort((left, right) => right.priority - left.priority);
|
|
187
|
+
const selected = [];
|
|
188
|
+
let totalChars = 0;
|
|
189
|
+
for (const item of sorted) {
|
|
190
|
+
if (selected.length >= maxItems)
|
|
191
|
+
break;
|
|
192
|
+
if (totalChars + item.content.length > maxChars && selected.length > 0)
|
|
193
|
+
continue;
|
|
194
|
+
selected.push(item);
|
|
195
|
+
totalChars += item.content.length;
|
|
196
|
+
}
|
|
197
|
+
return selected;
|
|
198
|
+
}
|
|
199
|
+
function toRelative(cwd, filePath) {
|
|
200
|
+
if (!path.isAbsolute(filePath))
|
|
201
|
+
return filePath;
|
|
202
|
+
return path.relative(cwd, filePath) || path.basename(filePath);
|
|
203
|
+
}
|
|
204
|
+
// src/plugin-host/helpers.ts
|
|
205
|
+
function isHookControlResult(value) {
|
|
206
|
+
if (!value || typeof value !== "object")
|
|
207
|
+
return false;
|
|
208
|
+
const candidate = value;
|
|
209
|
+
return (candidate.type === "continue" || candidate.type === "replace" || candidate.type === "stop") && "payload" in candidate;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/plugin-host/HookPipeline.ts
|
|
213
|
+
class HookPipeline {
|
|
214
|
+
registrations = [];
|
|
215
|
+
register(pluginId, priority, hooks) {
|
|
216
|
+
if (!hooks)
|
|
217
|
+
return;
|
|
218
|
+
if (hooks["model.event"]?.middleware && hooks["model.event"]?.middleware.length > 0)
|
|
219
|
+
throw new Error(`Plugin ${pluginId} cannot register middleware for model.event`);
|
|
220
|
+
this.registrations.push({ pluginId, priority, hooks });
|
|
221
|
+
this.registrations.sort((left, right) => right.priority - left.priority);
|
|
222
|
+
}
|
|
223
|
+
async runMiddleware(name, payload, context) {
|
|
224
|
+
let current = payload;
|
|
225
|
+
for (const registration of this.registrations) {
|
|
226
|
+
const middlewares = registration.hooks[name]?.middleware ?? [];
|
|
227
|
+
for (const middleware of middlewares) {
|
|
228
|
+
const result = await middleware({
|
|
229
|
+
payload: current,
|
|
230
|
+
context: { ...context, metadata: { ...context.metadata, pluginId: registration.pluginId } }
|
|
231
|
+
});
|
|
232
|
+
if (result === undefined)
|
|
233
|
+
continue;
|
|
234
|
+
if (isHookControlResult(result)) {
|
|
235
|
+
current = result.payload;
|
|
236
|
+
if (result.type === "stop")
|
|
237
|
+
return current;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
current = result;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return current;
|
|
244
|
+
}
|
|
245
|
+
async runObservers(name, payload, context) {
|
|
246
|
+
for (const registration of this.registrations) {
|
|
247
|
+
const observers = registration.hooks[name]?.observers ?? [];
|
|
248
|
+
for (const observer of observers) {
|
|
249
|
+
await observer({
|
|
250
|
+
payload,
|
|
251
|
+
context: { ...context, metadata: { ...context.metadata, pluginId: registration.pluginId } }
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/plugin-host/PluginHost.ts
|
|
259
|
+
class PluginHost {
|
|
260
|
+
pipeline = new HookPipeline;
|
|
261
|
+
contextContributors = [];
|
|
262
|
+
promptContributors = [];
|
|
263
|
+
tools = [];
|
|
264
|
+
services;
|
|
265
|
+
cwd;
|
|
266
|
+
agentId;
|
|
267
|
+
tracker;
|
|
268
|
+
constructor(options) {
|
|
269
|
+
this.agentId = options.agentId;
|
|
270
|
+
this.cwd = options.cwd;
|
|
271
|
+
this.services = options.services;
|
|
272
|
+
this.tracker = options.tracker;
|
|
273
|
+
for (const plugin of options.plugins ?? []) {
|
|
274
|
+
const priority = plugin.manifest.priority ?? 0;
|
|
275
|
+
const scopedServices = options.permissionGateway.scopeServices(this.services, plugin.manifest.id, plugin.manifest.permissions);
|
|
276
|
+
const setup = plugin.setup?.({
|
|
277
|
+
cwd: this.cwd,
|
|
278
|
+
manifest: plugin.manifest,
|
|
279
|
+
services: scopedServices
|
|
280
|
+
});
|
|
281
|
+
this.pipeline.register(plugin.manifest.id, priority, setup?.hooks);
|
|
282
|
+
for (const tool of setup?.tools ?? []) {
|
|
283
|
+
this.tools.push({
|
|
284
|
+
pluginId: plugin.manifest.id,
|
|
285
|
+
tool: wrapTool(plugin.manifest.id, tool, scopedServices)
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
for (const contributor of setup?.contextContributors ?? [])
|
|
289
|
+
this.contextContributors.push({ pluginId: plugin.manifest.id, contributor });
|
|
290
|
+
for (const contributor of setup?.promptContributors ?? [])
|
|
291
|
+
this.promptContributors.push({ pluginId: plugin.manifest.id, contributor });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
pluginTools() {
|
|
295
|
+
return this.tools.map((entry) => entry.tool);
|
|
296
|
+
}
|
|
297
|
+
recentFiles(limit = 5) {
|
|
298
|
+
return this.tracker?.recentFiles(limit) ?? [];
|
|
299
|
+
}
|
|
300
|
+
async runMiddleware(name, payload, context = {}) {
|
|
301
|
+
return this.pipeline.runMiddleware(name, payload, this.createRuntimeContext(context));
|
|
302
|
+
}
|
|
303
|
+
async runObservers(name, payload, context = {}) {
|
|
304
|
+
await this.pipeline.runObservers(name, payload, this.createRuntimeContext(context));
|
|
305
|
+
}
|
|
306
|
+
async collectContext(input) {
|
|
307
|
+
const items = [];
|
|
308
|
+
for (const entry of this.contextContributors) {
|
|
309
|
+
const next = await entry.contributor(input);
|
|
310
|
+
if (next.length > 0)
|
|
311
|
+
items.push(...next);
|
|
312
|
+
}
|
|
313
|
+
return items;
|
|
314
|
+
}
|
|
315
|
+
async collectPromptSegments(input) {
|
|
316
|
+
const segments = [];
|
|
317
|
+
for (const entry of this.promptContributors) {
|
|
318
|
+
const next = await entry.contributor(input);
|
|
319
|
+
if (!next)
|
|
320
|
+
continue;
|
|
321
|
+
if (Array.isArray(next))
|
|
322
|
+
segments.push(...next.filter(Boolean));
|
|
323
|
+
else
|
|
324
|
+
segments.push(next);
|
|
325
|
+
}
|
|
326
|
+
return segments;
|
|
327
|
+
}
|
|
328
|
+
createRuntimeContext(context) {
|
|
329
|
+
return {
|
|
330
|
+
agentId: this.agentId,
|
|
331
|
+
cwd: context.cwd ?? this.cwd,
|
|
332
|
+
sessionId: context.sessionId,
|
|
333
|
+
requestId: context.requestId,
|
|
334
|
+
iteration: context.iteration,
|
|
335
|
+
metadata: context.metadata,
|
|
336
|
+
services: context.services ?? this.services
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function wrapTool(pluginId, tool, services) {
|
|
341
|
+
return {
|
|
342
|
+
definition: tool.definition,
|
|
343
|
+
async execute(args, context) {
|
|
344
|
+
return tool.execute(args, {
|
|
345
|
+
...context,
|
|
346
|
+
metadata: {
|
|
347
|
+
...context.metadata,
|
|
348
|
+
pluginId
|
|
349
|
+
},
|
|
350
|
+
services
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/services/activity.ts
|
|
357
|
+
class ActivityTracker {
|
|
358
|
+
records = [];
|
|
359
|
+
record(record) {
|
|
360
|
+
this.records.push({ ...record, timestamp: record.timestamp });
|
|
361
|
+
if (this.records.length > 200)
|
|
362
|
+
this.records.splice(0, this.records.length - 200);
|
|
363
|
+
}
|
|
364
|
+
recentFiles(limit = 5) {
|
|
365
|
+
const seen = new Set;
|
|
366
|
+
const files = [];
|
|
367
|
+
for (let index = this.records.length - 1;index >= 0; index -= 1) {
|
|
368
|
+
const record = this.records[index];
|
|
369
|
+
if (!record || record.type !== "file")
|
|
370
|
+
continue;
|
|
371
|
+
if (seen.has(record.label))
|
|
372
|
+
continue;
|
|
373
|
+
seen.add(record.label);
|
|
374
|
+
files.push(record.label);
|
|
375
|
+
if (files.length >= limit)
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
return files;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// src/services/ExecGateway.ts
|
|
382
|
+
import { spawn } from "node:child_process";
|
|
383
|
+
|
|
384
|
+
class NodeExecGateway {
|
|
385
|
+
cwd;
|
|
386
|
+
env;
|
|
387
|
+
tracker;
|
|
388
|
+
constructor(options = {}) {
|
|
389
|
+
this.cwd = options.cwd;
|
|
390
|
+
this.env = options.env ?? process.env;
|
|
391
|
+
this.tracker = options.tracker;
|
|
392
|
+
}
|
|
393
|
+
async execute(command, options = {}) {
|
|
394
|
+
const cwd = options.cwd ?? this.cwd;
|
|
395
|
+
const env = {
|
|
396
|
+
...this.env,
|
|
397
|
+
...options.env
|
|
398
|
+
};
|
|
399
|
+
const result = await new Promise((resolve, reject) => {
|
|
400
|
+
const child = spawn(command, {
|
|
401
|
+
cwd,
|
|
402
|
+
env,
|
|
403
|
+
shell: true,
|
|
404
|
+
signal: options.signal
|
|
405
|
+
});
|
|
406
|
+
let stdout = "";
|
|
407
|
+
let stderr = "";
|
|
408
|
+
let settled = false;
|
|
409
|
+
let timedOut = false;
|
|
410
|
+
let timeout;
|
|
411
|
+
const finish = (value, isError) => {
|
|
412
|
+
if (settled)
|
|
413
|
+
return;
|
|
414
|
+
settled = true;
|
|
415
|
+
if (timeout)
|
|
416
|
+
clearTimeout(timeout);
|
|
417
|
+
if (isError)
|
|
418
|
+
reject(value);
|
|
419
|
+
else
|
|
420
|
+
resolve(value);
|
|
421
|
+
};
|
|
422
|
+
if (options.timeoutMs !== undefined) {
|
|
423
|
+
timeout = setTimeout(() => {
|
|
424
|
+
timedOut = true;
|
|
425
|
+
child.kill("SIGTERM");
|
|
426
|
+
}, options.timeoutMs);
|
|
427
|
+
}
|
|
428
|
+
child.stdout.on("data", (chunk) => {
|
|
429
|
+
stdout += String(chunk);
|
|
430
|
+
});
|
|
431
|
+
child.stderr.on("data", (chunk) => {
|
|
432
|
+
stderr += String(chunk);
|
|
433
|
+
});
|
|
434
|
+
child.on("error", (error) => finish(error, true));
|
|
435
|
+
child.on("close", (code, signal) => {
|
|
436
|
+
finish({
|
|
437
|
+
command,
|
|
438
|
+
cwd: cwd ?? null,
|
|
439
|
+
stdout,
|
|
440
|
+
stderr,
|
|
441
|
+
exitCode: code,
|
|
442
|
+
signal: signal ?? null,
|
|
443
|
+
timedOut
|
|
444
|
+
}, false);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
this.tracker?.record({
|
|
448
|
+
type: "command",
|
|
449
|
+
label: command,
|
|
450
|
+
timestamp: Date.now()
|
|
451
|
+
});
|
|
452
|
+
return result;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// src/services/FileSystemGateway.ts
|
|
456
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
457
|
+
import path2 from "node:path";
|
|
458
|
+
|
|
459
|
+
class NodeFileSystemGateway {
|
|
460
|
+
cwd;
|
|
461
|
+
tracker;
|
|
462
|
+
constructor(options = {}) {
|
|
463
|
+
this.cwd = options.cwd;
|
|
464
|
+
this.tracker = options.tracker;
|
|
465
|
+
}
|
|
466
|
+
async readText(targetPath, options = {}) {
|
|
467
|
+
const resolved = resolvePath(this.cwd, targetPath, options.cwd);
|
|
468
|
+
const text = await readFile(resolved, "utf8");
|
|
469
|
+
this.recordFile(resolved);
|
|
470
|
+
return text;
|
|
471
|
+
}
|
|
472
|
+
async writeText(targetPath, content, options = {}) {
|
|
473
|
+
const resolved = resolvePath(this.cwd, targetPath, options.cwd);
|
|
474
|
+
await mkdir(path2.dirname(resolved), { recursive: true });
|
|
475
|
+
await writeFile(resolved, content, "utf8");
|
|
476
|
+
this.recordFile(resolved);
|
|
477
|
+
return {
|
|
478
|
+
path: resolved,
|
|
479
|
+
bytes: Buffer.byteLength(content, "utf8")
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
async editText(targetPath, options) {
|
|
483
|
+
const resolved = resolvePath(this.cwd, targetPath, options.cwd);
|
|
484
|
+
const source = await readFile(resolved, "utf8");
|
|
485
|
+
const matches = source.split(options.oldText).length - 1;
|
|
486
|
+
if (matches === 0)
|
|
487
|
+
throw new Error("oldText was not found in the target file");
|
|
488
|
+
if (!options.replaceAll && matches > 1)
|
|
489
|
+
throw new Error("oldText matched more than once; set replaceAll to true to replace all matches");
|
|
490
|
+
const updated = options.replaceAll ? source.split(options.oldText).join(options.newText) : source.replace(options.oldText, options.newText);
|
|
491
|
+
await writeFile(resolved, updated, "utf8");
|
|
492
|
+
this.recordFile(resolved);
|
|
493
|
+
return {
|
|
494
|
+
path: resolved,
|
|
495
|
+
replacements: options.replaceAll ? matches : 1
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
async exists(targetPath, options = {}) {
|
|
499
|
+
try {
|
|
500
|
+
const resolved = resolvePath(this.cwd, targetPath, options.cwd);
|
|
501
|
+
await stat(resolved);
|
|
502
|
+
return true;
|
|
503
|
+
} catch {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async glob(pattern, options = {}) {
|
|
508
|
+
const root = path2.resolve(options.cwd ?? this.cwd ?? process.cwd());
|
|
509
|
+
const matcher = compileGlob(pattern);
|
|
510
|
+
const limit = options.limit ?? 100;
|
|
511
|
+
const matches = [];
|
|
512
|
+
await walk(root, root, async (absolutePath, relativePath) => {
|
|
513
|
+
if (matches.length >= limit)
|
|
514
|
+
return false;
|
|
515
|
+
if (matcher.test(relativePath))
|
|
516
|
+
matches.push(absolutePath);
|
|
517
|
+
return true;
|
|
518
|
+
});
|
|
519
|
+
return matches;
|
|
520
|
+
}
|
|
521
|
+
async grep(query, options = {}) {
|
|
522
|
+
const pattern = options.pattern ?? "**/*";
|
|
523
|
+
const files = await this.glob(pattern, options);
|
|
524
|
+
const matches = [];
|
|
525
|
+
const limit = options.limit ?? 50;
|
|
526
|
+
for (const filePath of files) {
|
|
527
|
+
if (matches.length >= limit)
|
|
528
|
+
break;
|
|
529
|
+
let text;
|
|
530
|
+
try {
|
|
531
|
+
text = await readFile(filePath, "utf8");
|
|
532
|
+
} catch {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const lines = text.split(/\r?\n/g);
|
|
536
|
+
for (let index = 0;index < lines.length; index += 1) {
|
|
537
|
+
const line = lines[index];
|
|
538
|
+
if (!line || !line.includes(query))
|
|
539
|
+
continue;
|
|
540
|
+
matches.push({
|
|
541
|
+
path: filePath,
|
|
542
|
+
lineNumber: index + 1,
|
|
543
|
+
line
|
|
544
|
+
});
|
|
545
|
+
if (matches.length >= limit)
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return matches;
|
|
550
|
+
}
|
|
551
|
+
recordFile(filePath) {
|
|
552
|
+
this.tracker?.record({
|
|
553
|
+
type: "file",
|
|
554
|
+
label: filePath,
|
|
555
|
+
timestamp: Date.now()
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function resolvePath(defaultCwd, targetPath, cwdOverride) {
|
|
560
|
+
if (!targetPath.trim())
|
|
561
|
+
throw new Error("path is required");
|
|
562
|
+
if (path2.isAbsolute(targetPath))
|
|
563
|
+
return path2.normalize(targetPath);
|
|
564
|
+
return path2.resolve(cwdOverride ?? defaultCwd ?? process.cwd(), targetPath);
|
|
565
|
+
}
|
|
566
|
+
function compileGlob(pattern) {
|
|
567
|
+
const normalized = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
568
|
+
let expression = "^";
|
|
569
|
+
for (let index = 0;index < normalized.length; index += 1) {
|
|
570
|
+
const character = normalized[index];
|
|
571
|
+
const next = normalized[index + 1];
|
|
572
|
+
if (character === "*" && next === "*") {
|
|
573
|
+
expression += ".*";
|
|
574
|
+
index += 1;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (character === "*") {
|
|
578
|
+
expression += "[^/]*";
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
expression += /[.+^${}()|[\]\\]/.test(character) ? `\\${character}` : character;
|
|
582
|
+
}
|
|
583
|
+
expression += "$";
|
|
584
|
+
return new RegExp(expression);
|
|
585
|
+
}
|
|
586
|
+
async function walk(root, current, visit) {
|
|
587
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
588
|
+
for (const entry of entries) {
|
|
589
|
+
if (entry.name === "node_modules" || entry.name === ".git")
|
|
590
|
+
continue;
|
|
591
|
+
const absolutePath = path2.join(current, entry.name);
|
|
592
|
+
const relativePath = path2.relative(root, absolutePath).replace(/\\/g, "/");
|
|
593
|
+
if (entry.isDirectory()) {
|
|
594
|
+
const keepGoing = await visit(absolutePath, relativePath);
|
|
595
|
+
if (keepGoing !== false)
|
|
596
|
+
await walk(root, absolutePath, visit);
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
await visit(absolutePath, relativePath);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// src/services/GitGateway.ts
|
|
603
|
+
class DefaultGitGateway {
|
|
604
|
+
exec;
|
|
605
|
+
cwd;
|
|
606
|
+
constructor(options) {
|
|
607
|
+
this.exec = options.exec;
|
|
608
|
+
this.cwd = options.cwd;
|
|
609
|
+
}
|
|
610
|
+
async status(options = {}) {
|
|
611
|
+
return this.runGitCommand("git status --short", options.cwd);
|
|
612
|
+
}
|
|
613
|
+
async diff(options = {}) {
|
|
614
|
+
return this.runGitCommand('git diff --stat && printf "\\n---\\n" && git diff -- .', options.cwd);
|
|
615
|
+
}
|
|
616
|
+
async runGitCommand(command, cwd) {
|
|
617
|
+
try {
|
|
618
|
+
const result = await this.exec.execute(command, { cwd: cwd ?? this.cwd, timeoutMs: 1e4 });
|
|
619
|
+
if (result.exitCode !== 0)
|
|
620
|
+
return null;
|
|
621
|
+
const text = [result.stdout, result.stderr].filter(Boolean).join(result.stdout && result.stderr ? `
|
|
622
|
+
` : "");
|
|
623
|
+
return text.trim() || null;
|
|
624
|
+
} catch {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
// src/services/ModelGateway.ts
|
|
630
|
+
class DefaultModelGateway {
|
|
631
|
+
model;
|
|
632
|
+
constructor(model) {
|
|
633
|
+
this.model = model;
|
|
634
|
+
}
|
|
635
|
+
stream(request) {
|
|
636
|
+
return this.model.stream(request);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// src/services/NetworkGateway.ts
|
|
640
|
+
class DefaultNetworkGateway {
|
|
641
|
+
fetchImpl;
|
|
642
|
+
constructor(fetchImpl = globalThis.fetch) {
|
|
643
|
+
this.fetchImpl = fetchImpl;
|
|
644
|
+
}
|
|
645
|
+
fetch(input, init) {
|
|
646
|
+
return this.fetchImpl(input, init);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// src/services/permissions.ts
|
|
650
|
+
var fullPermissions = {
|
|
651
|
+
fs: "full",
|
|
652
|
+
git: true,
|
|
653
|
+
network: true,
|
|
654
|
+
process: true,
|
|
655
|
+
model: true,
|
|
656
|
+
mcp: true
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
class PermissionDeniedError extends Error {
|
|
660
|
+
code = "permission_denied";
|
|
661
|
+
constructor(message) {
|
|
662
|
+
super(message);
|
|
663
|
+
this.name = "PermissionDeniedError";
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function normalizePermissions(input) {
|
|
667
|
+
return {
|
|
668
|
+
...fullPermissions,
|
|
669
|
+
...input
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
function canReadFs(permissions) {
|
|
673
|
+
return permissions.fs === true || permissions.fs === "read" || permissions.fs === "full";
|
|
674
|
+
}
|
|
675
|
+
function canWriteFs(permissions) {
|
|
676
|
+
return permissions.fs === true || permissions.fs === "write" || permissions.fs === "full";
|
|
677
|
+
}
|
|
678
|
+
// src/services/PermissionGateway.ts
|
|
679
|
+
class PermissionGateway {
|
|
680
|
+
permissions;
|
|
681
|
+
constructor(options = {}) {
|
|
682
|
+
this.permissions = normalizePermissions(options.permissions);
|
|
683
|
+
}
|
|
684
|
+
get agentPermissions() {
|
|
685
|
+
return { ...this.permissions };
|
|
686
|
+
}
|
|
687
|
+
scopeServices(services, pluginId, requested) {
|
|
688
|
+
const effective = intersectPermissions(this.permissions, requested);
|
|
689
|
+
return {
|
|
690
|
+
fileSystem: {
|
|
691
|
+
readText: (path3, options) => {
|
|
692
|
+
if (!canReadFs(effective))
|
|
693
|
+
throw new PermissionDeniedError(`Plugin ${pluginId} is not allowed to read files`);
|
|
694
|
+
return services.fileSystem.readText(path3, options);
|
|
695
|
+
},
|
|
696
|
+
writeText: (path3, content, options) => {
|
|
697
|
+
if (!canWriteFs(effective))
|
|
698
|
+
throw new PermissionDeniedError(`Plugin ${pluginId} is not allowed to write files`);
|
|
699
|
+
return services.fileSystem.writeText(path3, content, options);
|
|
700
|
+
},
|
|
701
|
+
editText: (path3, options) => {
|
|
702
|
+
if (!canWriteFs(effective))
|
|
703
|
+
throw new PermissionDeniedError(`Plugin ${pluginId} is not allowed to edit files`);
|
|
704
|
+
return services.fileSystem.editText(path3, options);
|
|
705
|
+
},
|
|
706
|
+
exists: (path3, options) => {
|
|
707
|
+
if (!canReadFs(effective))
|
|
708
|
+
throw new PermissionDeniedError(`Plugin ${pluginId} is not allowed to inspect files`);
|
|
709
|
+
return services.fileSystem.exists(path3, options);
|
|
710
|
+
},
|
|
711
|
+
glob: (pattern, options) => {
|
|
712
|
+
if (!canReadFs(effective))
|
|
713
|
+
throw new PermissionDeniedError(`Plugin ${pluginId} is not allowed to glob files`);
|
|
714
|
+
return services.fileSystem.glob(pattern, options);
|
|
715
|
+
},
|
|
716
|
+
grep: (query, options) => {
|
|
717
|
+
if (!canReadFs(effective))
|
|
718
|
+
throw new PermissionDeniedError(`Plugin ${pluginId} is not allowed to grep files`);
|
|
719
|
+
return services.fileSystem.grep(query, options);
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
exec: {
|
|
723
|
+
execute: (command, options) => {
|
|
724
|
+
if (effective.process !== true)
|
|
725
|
+
throw new PermissionDeniedError(`Plugin ${pluginId} is not allowed to execute commands`);
|
|
726
|
+
return services.exec.execute(command, options);
|
|
727
|
+
}
|
|
728
|
+
},
|
|
729
|
+
git: {
|
|
730
|
+
status: (options) => {
|
|
731
|
+
if (effective.git !== true)
|
|
732
|
+
throw new PermissionDeniedError(`Plugin ${pluginId} is not allowed to inspect git state`);
|
|
733
|
+
return services.git.status(options);
|
|
734
|
+
},
|
|
735
|
+
diff: (options) => {
|
|
736
|
+
if (effective.git !== true)
|
|
737
|
+
throw new PermissionDeniedError(`Plugin ${pluginId} is not allowed to inspect git diff`);
|
|
738
|
+
return services.git.diff(options);
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
network: {
|
|
742
|
+
fetch: (input, init) => {
|
|
743
|
+
if (effective.network !== true)
|
|
744
|
+
throw new PermissionDeniedError(`Plugin ${pluginId} is not allowed to access the network`);
|
|
745
|
+
return services.network.fetch(input, init);
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
model: {
|
|
749
|
+
stream: (request) => {
|
|
750
|
+
if (effective.model !== true)
|
|
751
|
+
throw new PermissionDeniedError(`Plugin ${pluginId} is not allowed to call the model gateway`);
|
|
752
|
+
return services.model.stream(request);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function intersectPermissions(agent, requested) {
|
|
759
|
+
const normalized = requested ?? {};
|
|
760
|
+
return {
|
|
761
|
+
fs: intersectFs(agent.fs, normalized.fs ?? false),
|
|
762
|
+
git: agent.git === true && normalized.git === true,
|
|
763
|
+
network: agent.network === true && normalized.network === true,
|
|
764
|
+
process: agent.process === true && normalized.process === true,
|
|
765
|
+
model: agent.model === true && normalized.model === true,
|
|
766
|
+
mcp: agent.mcp === true && normalized.mcp === true
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function intersectFs(left, right) {
|
|
770
|
+
const rank = new Map([
|
|
771
|
+
[false, 0],
|
|
772
|
+
["read", 1],
|
|
773
|
+
["write", 1],
|
|
774
|
+
[true, 2],
|
|
775
|
+
["full", 2]
|
|
776
|
+
]);
|
|
777
|
+
const leftValue = left ?? false;
|
|
778
|
+
const rightValue = right ?? false;
|
|
779
|
+
if (leftValue === "full" || leftValue === true)
|
|
780
|
+
return rightValue;
|
|
781
|
+
if (rightValue === "full" || rightValue === true)
|
|
782
|
+
return leftValue;
|
|
783
|
+
if (leftValue === rightValue)
|
|
784
|
+
return leftValue;
|
|
785
|
+
if (rank.get(leftValue) === 0 || rank.get(rightValue) === 0)
|
|
786
|
+
return false;
|
|
787
|
+
if (leftValue === "read" && rightValue === "write" || leftValue === "write" && rightValue === "read")
|
|
788
|
+
return false;
|
|
789
|
+
return leftValue;
|
|
790
|
+
}
|
|
791
|
+
// src/utils/json.ts
|
|
792
|
+
function isJsonSafeValue(value) {
|
|
793
|
+
if (value === null)
|
|
794
|
+
return true;
|
|
795
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
796
|
+
return true;
|
|
797
|
+
if (Array.isArray(value))
|
|
798
|
+
return value.every(isJsonSafeValue);
|
|
799
|
+
if (isRecord(value))
|
|
800
|
+
return Object.values(value).every(isJsonSafeValue);
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
function assertJsonSafeObject(value, label) {
|
|
804
|
+
if (!isRecord(value) || !isJsonSafeValue(value))
|
|
805
|
+
throw new TypeError(`${label} must be a JSON-safe object`);
|
|
806
|
+
}
|
|
807
|
+
function parseJsonObject(value, label) {
|
|
808
|
+
let parsed;
|
|
809
|
+
try {
|
|
810
|
+
parsed = JSON.parse(value);
|
|
811
|
+
} catch (error) {
|
|
812
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
813
|
+
throw new Error(`${label} must be valid JSON: ${message}`);
|
|
814
|
+
}
|
|
815
|
+
if (!isRecord(parsed))
|
|
816
|
+
throw new Error(`${label} must decode to a JSON object`);
|
|
817
|
+
return parsed;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/contracts/tool-normalize.ts
|
|
821
|
+
function normalizeToolSchema(schema) {
|
|
822
|
+
if (!isRecord(schema) || schema.type !== "object")
|
|
823
|
+
throw new TypeError('Tool input schema must have type "object"');
|
|
824
|
+
const properties = schema.properties;
|
|
825
|
+
if (properties !== undefined && !isRecord(properties))
|
|
826
|
+
throw new TypeError("Tool input schema properties must be an object");
|
|
827
|
+
const required = schema.required;
|
|
828
|
+
if (required !== undefined && !isStringArray(required))
|
|
829
|
+
throw new TypeError("Tool input schema required must be a string array");
|
|
830
|
+
const normalized = {
|
|
831
|
+
...schema,
|
|
832
|
+
type: "object",
|
|
833
|
+
properties: properties ? cloneSchemaRecord(properties) : {}
|
|
834
|
+
};
|
|
835
|
+
return normalized;
|
|
836
|
+
}
|
|
837
|
+
function cloneSchemaRecord(record) {
|
|
838
|
+
return Object.fromEntries(Object.entries(record).map(([key, value]) => {
|
|
839
|
+
if (!isRecord(value))
|
|
840
|
+
throw new TypeError(`Tool schema property ${key} must be an object`);
|
|
841
|
+
return [key, { ...value }];
|
|
842
|
+
}));
|
|
843
|
+
}
|
|
844
|
+
function normalizeToolResult(result) {
|
|
845
|
+
const normalized = {
|
|
846
|
+
content: normalizeContent(result.content),
|
|
847
|
+
isError: result.isError === true
|
|
848
|
+
};
|
|
849
|
+
if (result.structuredContent !== undefined) {
|
|
850
|
+
assertJsonSafeObject(result.structuredContent, "Tool structuredContent");
|
|
851
|
+
normalized.structuredContent = { ...result.structuredContent };
|
|
852
|
+
}
|
|
853
|
+
return normalized;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// src/tools/BaseTool.ts
|
|
857
|
+
class BaseTool {
|
|
858
|
+
definition;
|
|
859
|
+
constructor(definition) {
|
|
860
|
+
this.definition = {
|
|
861
|
+
...definition,
|
|
862
|
+
inputSchema: normalizeToolSchema(definition.inputSchema)
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
// src/tools/result.ts
|
|
867
|
+
function textToolResult(text) {
|
|
868
|
+
return {
|
|
869
|
+
content: normalizeContent(text)
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
// src/tools/ToolRegistry.ts
|
|
873
|
+
class ToolRegistry {
|
|
874
|
+
tools = new Map;
|
|
875
|
+
register(tool) {
|
|
876
|
+
const name = tool.definition.name.trim();
|
|
877
|
+
if (!name)
|
|
878
|
+
throw new Error("Tool name is required");
|
|
879
|
+
if (this.tools.has(name))
|
|
880
|
+
throw new Error(`Tool already registered: ${name}`);
|
|
881
|
+
const normalizedDefinition = {
|
|
882
|
+
...tool.definition,
|
|
883
|
+
name,
|
|
884
|
+
description: tool.definition.description.trim(),
|
|
885
|
+
inputSchema: normalizeToolSchema(tool.definition.inputSchema)
|
|
886
|
+
};
|
|
887
|
+
const normalizedTool = {
|
|
888
|
+
definition: normalizedDefinition,
|
|
889
|
+
execute(args, context) {
|
|
890
|
+
return tool.execute(args, context);
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
this.tools.set(name, normalizedTool);
|
|
894
|
+
return this;
|
|
895
|
+
}
|
|
896
|
+
unregister(name) {
|
|
897
|
+
return this.tools.delete(name);
|
|
898
|
+
}
|
|
899
|
+
get(name) {
|
|
900
|
+
return this.tools.get(name);
|
|
901
|
+
}
|
|
902
|
+
list() {
|
|
903
|
+
return [...this.tools.values()];
|
|
904
|
+
}
|
|
905
|
+
definitions() {
|
|
906
|
+
return this.list().map((tool) => ({
|
|
907
|
+
...tool.definition,
|
|
908
|
+
inputSchema: normalizeToolSchema(tool.definition.inputSchema)
|
|
909
|
+
}));
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
// src/tools/builtins/utils.ts
|
|
913
|
+
function readStringArg(args, key, options = {}) {
|
|
914
|
+
const value = args[key];
|
|
915
|
+
if (typeof value !== "string")
|
|
916
|
+
throw new Error(`${key} must be a string`);
|
|
917
|
+
if (!options.allowEmpty && value.length === 0)
|
|
918
|
+
throw new Error(`${key} must be a non-empty string`);
|
|
919
|
+
return value;
|
|
920
|
+
}
|
|
921
|
+
function readOptionalBooleanArg(args, key) {
|
|
922
|
+
const value = args[key];
|
|
923
|
+
if (value === undefined)
|
|
924
|
+
return;
|
|
925
|
+
if (typeof value !== "boolean")
|
|
926
|
+
throw new Error(`${key} must be a boolean`);
|
|
927
|
+
return value;
|
|
928
|
+
}
|
|
929
|
+
function readOptionalNumberArg(args, key) {
|
|
930
|
+
const value = args[key];
|
|
931
|
+
if (value === undefined)
|
|
932
|
+
return;
|
|
933
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
934
|
+
throw new Error(`${key} must be a finite number`);
|
|
935
|
+
return value;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// src/tools/builtins/EditTool.ts
|
|
939
|
+
class EditTool extends BaseTool {
|
|
940
|
+
constructor() {
|
|
941
|
+
super({
|
|
942
|
+
name: "edit",
|
|
943
|
+
description: "Replace text in a UTF-8 file.",
|
|
944
|
+
inputSchema: {
|
|
945
|
+
type: "object",
|
|
946
|
+
properties: {
|
|
947
|
+
path: { type: "string", description: "File path to edit." },
|
|
948
|
+
oldText: { type: "string", description: "Text to replace." },
|
|
949
|
+
newText: { type: "string", description: "Replacement text." },
|
|
950
|
+
replaceAll: { type: "boolean", description: "Replace all matches when true." }
|
|
951
|
+
},
|
|
952
|
+
required: ["path", "oldText", "newText"]
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
async execute(args, context) {
|
|
957
|
+
const fileSystem = context.services?.fileSystem;
|
|
958
|
+
if (!fileSystem)
|
|
959
|
+
throw new Error("FileSystemGateway is not available in the tool execution context");
|
|
960
|
+
const targetPath = readStringArg(args, "path");
|
|
961
|
+
const oldText = readStringArg(args, "oldText");
|
|
962
|
+
const newText = readStringArg(args, "newText", { allowEmpty: true });
|
|
963
|
+
const replaceAll = readOptionalBooleanArg(args, "replaceAll") ?? false;
|
|
964
|
+
const result = await fileSystem.editText(targetPath, {
|
|
965
|
+
oldText,
|
|
966
|
+
newText,
|
|
967
|
+
replaceAll,
|
|
968
|
+
cwd: context.cwd
|
|
969
|
+
});
|
|
970
|
+
return normalizeToolResult({
|
|
971
|
+
content: [{ type: "text", text: `Edited ${result.path}` }],
|
|
972
|
+
structuredContent: result
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
// src/tools/builtins/ExecTool.ts
|
|
977
|
+
class ExecTool extends BaseTool {
|
|
978
|
+
constructor() {
|
|
979
|
+
super({
|
|
980
|
+
name: "exec",
|
|
981
|
+
description: "Execute a shell command.",
|
|
982
|
+
inputSchema: {
|
|
983
|
+
type: "object",
|
|
984
|
+
properties: {
|
|
985
|
+
command: { type: "string", description: "Shell command to execute." },
|
|
986
|
+
cwd: { type: "string", description: "Optional working directory override." },
|
|
987
|
+
timeoutMs: { type: "number", description: "Optional timeout in milliseconds." }
|
|
988
|
+
},
|
|
989
|
+
required: ["command"]
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
async execute(args, context) {
|
|
994
|
+
const exec = context.services?.exec;
|
|
995
|
+
if (!exec)
|
|
996
|
+
throw new Error("ExecGateway is not available in the tool execution context");
|
|
997
|
+
const command = readStringArg(args, "command");
|
|
998
|
+
const cwd = typeof args.cwd === "string" && args.cwd.length > 0 ? args.cwd : context.cwd;
|
|
999
|
+
const timeoutMs = readOptionalNumberArg(args, "timeoutMs");
|
|
1000
|
+
const result = await exec.execute(command, {
|
|
1001
|
+
cwd,
|
|
1002
|
+
timeoutMs,
|
|
1003
|
+
signal: context.signal
|
|
1004
|
+
});
|
|
1005
|
+
const text = [result.stdout, result.stderr].filter(Boolean).join(result.stdout && result.stderr ? `
|
|
1006
|
+
` : "");
|
|
1007
|
+
return normalizeToolResult({
|
|
1008
|
+
content: [{ type: "text", text: text || `Command exited with code ${result.exitCode ?? -1}` }],
|
|
1009
|
+
structuredContent: { ...result },
|
|
1010
|
+
isError: result.exitCode !== 0
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
// src/tools/builtins/ReadTool.ts
|
|
1015
|
+
class ReadTool extends BaseTool {
|
|
1016
|
+
constructor() {
|
|
1017
|
+
super({
|
|
1018
|
+
name: "read",
|
|
1019
|
+
description: "Read a UTF-8 text file.",
|
|
1020
|
+
inputSchema: {
|
|
1021
|
+
type: "object",
|
|
1022
|
+
properties: {
|
|
1023
|
+
path: { type: "string", description: "File path to read." }
|
|
1024
|
+
},
|
|
1025
|
+
required: ["path"]
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
async execute(args, context) {
|
|
1030
|
+
const fileSystem = context.services?.fileSystem;
|
|
1031
|
+
if (!fileSystem)
|
|
1032
|
+
throw new Error("FileSystemGateway is not available in the tool execution context");
|
|
1033
|
+
const targetPath = readStringArg(args, "path");
|
|
1034
|
+
const text = await fileSystem.readText(targetPath, { cwd: context.cwd });
|
|
1035
|
+
return normalizeToolResult({
|
|
1036
|
+
content: [{ type: "text", text }],
|
|
1037
|
+
structuredContent: {
|
|
1038
|
+
path: targetPath,
|
|
1039
|
+
size: Buffer.byteLength(text, "utf8")
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
// src/tools/builtins/WriteTool.ts
|
|
1045
|
+
class WriteTool extends BaseTool {
|
|
1046
|
+
constructor() {
|
|
1047
|
+
super({
|
|
1048
|
+
name: "write",
|
|
1049
|
+
description: "Write a UTF-8 text file.",
|
|
1050
|
+
inputSchema: {
|
|
1051
|
+
type: "object",
|
|
1052
|
+
properties: {
|
|
1053
|
+
path: { type: "string", description: "File path to write." },
|
|
1054
|
+
content: { type: "string", description: "Text content to write." }
|
|
1055
|
+
},
|
|
1056
|
+
required: ["path", "content"]
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
async execute(args, context) {
|
|
1061
|
+
const fileSystem = context.services?.fileSystem;
|
|
1062
|
+
if (!fileSystem)
|
|
1063
|
+
throw new Error("FileSystemGateway is not available in the tool execution context");
|
|
1064
|
+
const targetPath = readStringArg(args, "path");
|
|
1065
|
+
const content = readStringArg(args, "content", { allowEmpty: true });
|
|
1066
|
+
const result = await fileSystem.writeText(targetPath, content, { cwd: context.cwd });
|
|
1067
|
+
return normalizeToolResult({
|
|
1068
|
+
content: [{ type: "text", text: `Wrote ${result.path}` }],
|
|
1069
|
+
structuredContent: result
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
// src/agent-core/MessageFactory.ts
|
|
1074
|
+
class DefaultMessageFactory {
|
|
1075
|
+
createSystemMessage(input) {
|
|
1076
|
+
return {
|
|
1077
|
+
id: createId("msg"),
|
|
1078
|
+
role: "system",
|
|
1079
|
+
content: normalizeContent(input),
|
|
1080
|
+
createdAt: Date.now()
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
createUserMessage(input) {
|
|
1084
|
+
return {
|
|
1085
|
+
id: createId("msg"),
|
|
1086
|
+
role: "user",
|
|
1087
|
+
content: normalizeContent(input),
|
|
1088
|
+
createdAt: Date.now()
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
createAssistantMessage(input) {
|
|
1092
|
+
return {
|
|
1093
|
+
id: createId("msg"),
|
|
1094
|
+
role: "assistant",
|
|
1095
|
+
content: input.text.length > 0 ? normalizeContent(input.text) : [],
|
|
1096
|
+
createdAt: Date.now(),
|
|
1097
|
+
thinking: input.thinking,
|
|
1098
|
+
toolCalls: input.toolCalls.length > 0 ? input.toolCalls : undefined,
|
|
1099
|
+
stopReason: input.stopReason,
|
|
1100
|
+
metadata: input.metadata
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
createToolMessage(toolCall, result) {
|
|
1104
|
+
return {
|
|
1105
|
+
id: createId("msg"),
|
|
1106
|
+
role: "tool",
|
|
1107
|
+
content: result.content,
|
|
1108
|
+
createdAt: Date.now(),
|
|
1109
|
+
toolCallId: toolCall.id,
|
|
1110
|
+
toolName: toolCall.function.name,
|
|
1111
|
+
structuredContent: result.structuredContent,
|
|
1112
|
+
isError: result.isError
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/persistence/SnapshotCodec.ts
|
|
1118
|
+
class JsonSnapshotCodec {
|
|
1119
|
+
encode(input) {
|
|
1120
|
+
return structuredClone({
|
|
1121
|
+
schemaVersion: 1,
|
|
1122
|
+
sessionId: input.sessionId,
|
|
1123
|
+
model: input.model,
|
|
1124
|
+
cwd: input.cwd,
|
|
1125
|
+
messages: input.messages,
|
|
1126
|
+
usage: input.usage,
|
|
1127
|
+
createdAt: input.createdAt,
|
|
1128
|
+
updatedAt: input.updatedAt,
|
|
1129
|
+
metadata: input.metadata
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
decode(value) {
|
|
1133
|
+
if (!value || typeof value !== "object")
|
|
1134
|
+
throw new TypeError("Session snapshot must be an object");
|
|
1135
|
+
const snapshot = value;
|
|
1136
|
+
if (snapshot.schemaVersion !== 1)
|
|
1137
|
+
throw new TypeError("Unsupported session snapshot schemaVersion");
|
|
1138
|
+
if (typeof snapshot.sessionId !== "string")
|
|
1139
|
+
throw new TypeError("Session snapshot sessionId is required");
|
|
1140
|
+
if (!Array.isArray(snapshot.messages))
|
|
1141
|
+
throw new TypeError("Session snapshot messages must be an array");
|
|
1142
|
+
if (typeof snapshot.createdAt !== "number" || typeof snapshot.updatedAt !== "number")
|
|
1143
|
+
throw new TypeError("Session snapshot timestamps are required");
|
|
1144
|
+
return structuredClone(snapshot);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
var sessionSnapshotCodec = new JsonSnapshotCodec;
|
|
1148
|
+
|
|
1149
|
+
// src/utils/usage.ts
|
|
1150
|
+
function createEmptyUsage() {
|
|
1151
|
+
return {
|
|
1152
|
+
promptTokens: 0,
|
|
1153
|
+
completionTokens: 0,
|
|
1154
|
+
totalTokens: 0
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
function addUsage(left, right) {
|
|
1158
|
+
if (!right)
|
|
1159
|
+
return { ...left };
|
|
1160
|
+
return {
|
|
1161
|
+
promptTokens: left.promptTokens + right.promptTokens,
|
|
1162
|
+
completionTokens: left.completionTokens + right.completionTokens,
|
|
1163
|
+
totalTokens: left.totalTokens + right.totalTokens
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// src/agent-core/tool-call.ts
|
|
1168
|
+
function startToolCall(callId, toolName) {
|
|
1169
|
+
if (!callId.trim())
|
|
1170
|
+
throw new Error("Tool call id is required");
|
|
1171
|
+
if (!toolName.trim())
|
|
1172
|
+
throw new Error("Tool name is required");
|
|
1173
|
+
return {
|
|
1174
|
+
callId,
|
|
1175
|
+
toolName,
|
|
1176
|
+
argsText: ""
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
function appendToolCallArgsDelta(draft, delta) {
|
|
1180
|
+
return {
|
|
1181
|
+
...draft,
|
|
1182
|
+
argsText: `${draft.argsText}${delta}`
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
function finalizeToolCall(draft) {
|
|
1186
|
+
const rawArgumentsText = draft.argsText.trim();
|
|
1187
|
+
const argumentsObject = rawArgumentsText.length === 0 ? {} : parseJsonObject(rawArgumentsText, `Tool call ${draft.callId} arguments`);
|
|
1188
|
+
return {
|
|
1189
|
+
id: draft.callId,
|
|
1190
|
+
type: "function",
|
|
1191
|
+
function: {
|
|
1192
|
+
name: draft.toolName,
|
|
1193
|
+
arguments: argumentsObject
|
|
1194
|
+
},
|
|
1195
|
+
rawArgumentsText
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// src/agent-core/errors.ts
|
|
1200
|
+
class SessionExecutionError extends Error {
|
|
1201
|
+
payload;
|
|
1202
|
+
constructor(payload) {
|
|
1203
|
+
super(payload.message);
|
|
1204
|
+
this.name = "SessionExecutionError";
|
|
1205
|
+
this.payload = payload;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// src/agent-core/ModelTurnCollector.ts
|
|
1210
|
+
class DefaultModelTurnCollector {
|
|
1211
|
+
async* collect(options) {
|
|
1212
|
+
const assistantTextParts = [];
|
|
1213
|
+
const thinkingParts = [];
|
|
1214
|
+
const toolCalls = [];
|
|
1215
|
+
const drafts = new Map;
|
|
1216
|
+
let stopReason = "final";
|
|
1217
|
+
let responseUsage;
|
|
1218
|
+
let assistantMetadata;
|
|
1219
|
+
for await (const event of options.events) {
|
|
1220
|
+
await options.onEvent?.(event);
|
|
1221
|
+
if (event.type === "response_start")
|
|
1222
|
+
continue;
|
|
1223
|
+
if (event.type === "text_delta") {
|
|
1224
|
+
assistantTextParts.push(event.delta);
|
|
1225
|
+
yield { type: "text_delta", sessionId: options.sessionId, delta: event.delta };
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
if (event.type === "thinking_delta") {
|
|
1229
|
+
thinkingParts.push(event.delta);
|
|
1230
|
+
yield { type: "thinking_delta", sessionId: options.sessionId, delta: event.delta };
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
if (event.type === "tool_call_start") {
|
|
1234
|
+
drafts.set(event.callId, startToolCall(event.callId, event.toolName));
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
if (event.type === "tool_call_args_delta") {
|
|
1238
|
+
const draft = drafts.get(event.callId);
|
|
1239
|
+
if (!draft) {
|
|
1240
|
+
const payload2 = {
|
|
1241
|
+
code: "tool_call_state_error",
|
|
1242
|
+
message: `Received tool_call_args_delta before tool_call_start for ${event.callId}`,
|
|
1243
|
+
requestId: options.requestId
|
|
1244
|
+
};
|
|
1245
|
+
yield { type: "error", sessionId: options.sessionId, error: payload2 };
|
|
1246
|
+
throw new SessionExecutionError(payload2);
|
|
1247
|
+
}
|
|
1248
|
+
drafts.set(event.callId, appendToolCallArgsDelta(draft, event.delta));
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
if (event.type === "tool_call_end") {
|
|
1252
|
+
const draft = drafts.get(event.callId);
|
|
1253
|
+
if (!draft) {
|
|
1254
|
+
const payload2 = {
|
|
1255
|
+
code: "tool_call_state_error",
|
|
1256
|
+
message: `Received tool_call_end before tool_call_start for ${event.callId}`,
|
|
1257
|
+
requestId: options.requestId
|
|
1258
|
+
};
|
|
1259
|
+
yield { type: "error", sessionId: options.sessionId, error: payload2 };
|
|
1260
|
+
throw new SessionExecutionError(payload2);
|
|
1261
|
+
}
|
|
1262
|
+
try {
|
|
1263
|
+
toolCalls.push(finalizeToolCall(draft));
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
const payload2 = {
|
|
1266
|
+
code: "tool_call_parse_error",
|
|
1267
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1268
|
+
requestId: options.requestId
|
|
1269
|
+
};
|
|
1270
|
+
yield { type: "error", sessionId: options.sessionId, error: payload2 };
|
|
1271
|
+
throw new SessionExecutionError(payload2);
|
|
1272
|
+
}
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
if (event.type === "response_end") {
|
|
1276
|
+
stopReason = event.stopReason;
|
|
1277
|
+
responseUsage = event.usage;
|
|
1278
|
+
assistantMetadata = event.assistantMetadata;
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
const payload = {
|
|
1282
|
+
...event.error,
|
|
1283
|
+
requestId: options.requestId
|
|
1284
|
+
};
|
|
1285
|
+
yield { type: "error", sessionId: options.sessionId, error: payload };
|
|
1286
|
+
throw new SessionExecutionError(payload);
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
requestId: options.requestId,
|
|
1290
|
+
text: assistantTextParts.join(""),
|
|
1291
|
+
thinking: thinkingParts.join("") || undefined,
|
|
1292
|
+
toolCalls,
|
|
1293
|
+
stopReason,
|
|
1294
|
+
usage: responseUsage,
|
|
1295
|
+
assistantMetadata
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// src/agent-core/session-state.ts
|
|
1301
|
+
function touchRuntimeSessionState(state) {
|
|
1302
|
+
state.updatedAt = Date.now();
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// src/agent-core/LoopRunner.ts
|
|
1306
|
+
class LoopRunner {
|
|
1307
|
+
messageFactory;
|
|
1308
|
+
turnCollector;
|
|
1309
|
+
toolExecutor;
|
|
1310
|
+
terminationPolicy;
|
|
1311
|
+
contextManager;
|
|
1312
|
+
pluginHost;
|
|
1313
|
+
constructor(dependencies) {
|
|
1314
|
+
this.messageFactory = dependencies.messageFactory;
|
|
1315
|
+
this.turnCollector = dependencies.turnCollector;
|
|
1316
|
+
this.toolExecutor = dependencies.toolExecutor;
|
|
1317
|
+
this.terminationPolicy = dependencies.terminationPolicy;
|
|
1318
|
+
this.contextManager = dependencies.contextManager;
|
|
1319
|
+
this.pluginHost = dependencies.pluginHost;
|
|
1320
|
+
}
|
|
1321
|
+
async* run(state, input, options = {}) {
|
|
1322
|
+
const startPayload = await this.pluginHost?.runMiddleware("session.start", {
|
|
1323
|
+
sessionId: state.id,
|
|
1324
|
+
input
|
|
1325
|
+
}, {
|
|
1326
|
+
sessionId: state.id,
|
|
1327
|
+
cwd: state.cwd,
|
|
1328
|
+
metadata: state.metadata
|
|
1329
|
+
});
|
|
1330
|
+
const initialInput = startPayload?.input ?? input;
|
|
1331
|
+
const userMessage = this.messageFactory.createUserMessage(initialInput);
|
|
1332
|
+
state.messages.push(userMessage);
|
|
1333
|
+
touchRuntimeSessionState(state);
|
|
1334
|
+
let lastRequestId;
|
|
1335
|
+
try {
|
|
1336
|
+
for (let iteration = 0;iteration < state.maxIterations; iteration += 1) {
|
|
1337
|
+
const requestId = createId("req");
|
|
1338
|
+
lastRequestId = requestId;
|
|
1339
|
+
const turnStart = await this.pluginHost?.runMiddleware("turn.start", {
|
|
1340
|
+
requestId,
|
|
1341
|
+
iteration,
|
|
1342
|
+
messages: [...state.messages]
|
|
1343
|
+
}, {
|
|
1344
|
+
sessionId: state.id,
|
|
1345
|
+
requestId,
|
|
1346
|
+
iteration,
|
|
1347
|
+
cwd: state.cwd,
|
|
1348
|
+
metadata: state.metadata
|
|
1349
|
+
});
|
|
1350
|
+
const contextItems = await this.contextManager.resolve({
|
|
1351
|
+
sessionId: state.id,
|
|
1352
|
+
input: initialInput,
|
|
1353
|
+
messages: state.messages,
|
|
1354
|
+
cwd: state.cwd
|
|
1355
|
+
});
|
|
1356
|
+
const resolvedContext = await this.pluginHost?.runMiddleware("context.resolve", {
|
|
1357
|
+
contextItems,
|
|
1358
|
+
input: initialInput,
|
|
1359
|
+
messages: [...state.messages]
|
|
1360
|
+
}, {
|
|
1361
|
+
sessionId: state.id,
|
|
1362
|
+
requestId,
|
|
1363
|
+
iteration,
|
|
1364
|
+
cwd: state.cwd,
|
|
1365
|
+
metadata: state.metadata
|
|
1366
|
+
});
|
|
1367
|
+
const effectiveContext = resolvedContext?.contextItems ?? contextItems;
|
|
1368
|
+
const pluginPromptSegments = await this.pluginHost?.collectPromptSegments({
|
|
1369
|
+
sessionId: state.id,
|
|
1370
|
+
input: initialInput,
|
|
1371
|
+
messages: state.messages,
|
|
1372
|
+
contextItems: effectiveContext,
|
|
1373
|
+
cwd: state.cwd
|
|
1374
|
+
}) ?? [];
|
|
1375
|
+
const promptSegments = [
|
|
1376
|
+
...this.contextManager.format(effectiveContext),
|
|
1377
|
+
...pluginPromptSegments
|
|
1378
|
+
];
|
|
1379
|
+
const resolvedPrompt = await this.pluginHost?.runMiddleware("prompt.resolve", {
|
|
1380
|
+
promptSegments,
|
|
1381
|
+
contextItems: effectiveContext,
|
|
1382
|
+
messages: [...state.messages]
|
|
1383
|
+
}, {
|
|
1384
|
+
sessionId: state.id,
|
|
1385
|
+
requestId,
|
|
1386
|
+
iteration,
|
|
1387
|
+
cwd: state.cwd,
|
|
1388
|
+
metadata: state.metadata
|
|
1389
|
+
});
|
|
1390
|
+
const effectivePromptSegments = resolvedPrompt?.promptSegments ?? promptSegments;
|
|
1391
|
+
const toolDefinitions = state.tools?.definitions() ?? [];
|
|
1392
|
+
const resolvedToolset = await this.pluginHost?.runMiddleware("toolset.resolve", {
|
|
1393
|
+
tools: toolDefinitions
|
|
1394
|
+
}, {
|
|
1395
|
+
sessionId: state.id,
|
|
1396
|
+
requestId,
|
|
1397
|
+
iteration,
|
|
1398
|
+
cwd: state.cwd,
|
|
1399
|
+
metadata: state.metadata
|
|
1400
|
+
});
|
|
1401
|
+
const effectiveTools = resolvedToolset?.tools ?? toolDefinitions;
|
|
1402
|
+
const requestMessages = [...turnStart?.messages ?? state.messages];
|
|
1403
|
+
if (effectivePromptSegments.length > 0)
|
|
1404
|
+
requestMessages.unshift(this.messageFactory.createSystemMessage(effectivePromptSegments.join(`
|
|
1405
|
+
|
|
1406
|
+
`)));
|
|
1407
|
+
const request = {
|
|
1408
|
+
requestId,
|
|
1409
|
+
model: state.modelRef,
|
|
1410
|
+
messages: requestMessages,
|
|
1411
|
+
tools: effectiveTools,
|
|
1412
|
+
reasoning: state.reasoning,
|
|
1413
|
+
signal: options.signal
|
|
1414
|
+
};
|
|
1415
|
+
const resolvedRequest = await this.pluginHost?.runMiddleware("model.request", {
|
|
1416
|
+
request
|
|
1417
|
+
}, {
|
|
1418
|
+
sessionId: state.id,
|
|
1419
|
+
requestId,
|
|
1420
|
+
iteration,
|
|
1421
|
+
cwd: state.cwd,
|
|
1422
|
+
metadata: state.metadata
|
|
1423
|
+
});
|
|
1424
|
+
const effectiveRequest = resolvedRequest?.request ?? request;
|
|
1425
|
+
const turn = yield* this.turnCollector.collect({
|
|
1426
|
+
sessionId: state.id,
|
|
1427
|
+
requestId,
|
|
1428
|
+
events: state.model.stream(effectiveRequest),
|
|
1429
|
+
onEvent: async (event) => {
|
|
1430
|
+
await this.pluginHost?.runObservers("model.event", { event }, {
|
|
1431
|
+
sessionId: state.id,
|
|
1432
|
+
requestId,
|
|
1433
|
+
iteration,
|
|
1434
|
+
cwd: state.cwd,
|
|
1435
|
+
metadata: state.metadata
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
const assistantDraft = this.messageFactory.createAssistantMessage({
|
|
1440
|
+
text: turn.text,
|
|
1441
|
+
thinking: turn.thinking,
|
|
1442
|
+
toolCalls: turn.toolCalls,
|
|
1443
|
+
stopReason: turn.stopReason,
|
|
1444
|
+
metadata: turn.assistantMetadata
|
|
1445
|
+
});
|
|
1446
|
+
const assistantPayload = await this.pluginHost?.runMiddleware("assistant.message", {
|
|
1447
|
+
message: assistantDraft
|
|
1448
|
+
}, {
|
|
1449
|
+
sessionId: state.id,
|
|
1450
|
+
requestId,
|
|
1451
|
+
iteration,
|
|
1452
|
+
cwd: state.cwd,
|
|
1453
|
+
metadata: state.metadata
|
|
1454
|
+
});
|
|
1455
|
+
const assistantMessage = assistantPayload?.message ?? assistantDraft;
|
|
1456
|
+
state.messages.push(assistantMessage);
|
|
1457
|
+
state.usage = addUsage(state.usage, turn.usage);
|
|
1458
|
+
touchRuntimeSessionState(state);
|
|
1459
|
+
const baseDecision = this.terminationPolicy.afterAssistantTurn(turn.toolCalls);
|
|
1460
|
+
const decisionPayload = await this.pluginHost?.runMiddleware("loop.decide", {
|
|
1461
|
+
toolCalls: turn.toolCalls,
|
|
1462
|
+
stopReason: turn.stopReason,
|
|
1463
|
+
decision: baseDecision
|
|
1464
|
+
}, {
|
|
1465
|
+
sessionId: state.id,
|
|
1466
|
+
requestId,
|
|
1467
|
+
iteration,
|
|
1468
|
+
cwd: state.cwd,
|
|
1469
|
+
metadata: state.metadata
|
|
1470
|
+
});
|
|
1471
|
+
const decision = decisionPayload?.decision ?? baseDecision;
|
|
1472
|
+
if (decision.type === "done") {
|
|
1473
|
+
yield {
|
|
1474
|
+
type: "done",
|
|
1475
|
+
sessionId: state.id,
|
|
1476
|
+
message: assistantMessage,
|
|
1477
|
+
usage: { ...state.usage }
|
|
1478
|
+
};
|
|
1479
|
+
await this.pluginHost?.runObservers("session.end", { message: assistantMessage }, {
|
|
1480
|
+
sessionId: state.id,
|
|
1481
|
+
requestId,
|
|
1482
|
+
iteration,
|
|
1483
|
+
cwd: state.cwd,
|
|
1484
|
+
metadata: state.metadata
|
|
1485
|
+
});
|
|
1486
|
+
await options.onDone?.(assistantMessage);
|
|
1487
|
+
return assistantMessage;
|
|
1488
|
+
}
|
|
1489
|
+
for (const toolCall of turn.toolCalls) {
|
|
1490
|
+
yield { type: "tool_call", sessionId: state.id, toolCall };
|
|
1491
|
+
const result = await this.toolExecutor.execute(toolCall, {
|
|
1492
|
+
signal: options.signal,
|
|
1493
|
+
cwd: state.cwd
|
|
1494
|
+
});
|
|
1495
|
+
const toolMessage = this.messageFactory.createToolMessage(toolCall, result);
|
|
1496
|
+
state.messages.push(toolMessage);
|
|
1497
|
+
touchRuntimeSessionState(state);
|
|
1498
|
+
yield { type: "tool_result", sessionId: state.id, toolCallId: toolCall.id, result };
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
if (error instanceof SessionExecutionError) {
|
|
1503
|
+
await this.pluginHost?.runObservers("session.error", { error: error.payload }, {
|
|
1504
|
+
sessionId: state.id,
|
|
1505
|
+
requestId: lastRequestId,
|
|
1506
|
+
cwd: state.cwd,
|
|
1507
|
+
metadata: state.metadata
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
throw error;
|
|
1511
|
+
}
|
|
1512
|
+
const payload = this.terminationPolicy.onMaxIterationsExceeded(state.maxIterations, lastRequestId);
|
|
1513
|
+
yield { type: "error", sessionId: state.id, error: payload };
|
|
1514
|
+
await this.pluginHost?.runObservers("session.error", { error: payload }, {
|
|
1515
|
+
sessionId: state.id,
|
|
1516
|
+
requestId: lastRequestId,
|
|
1517
|
+
cwd: state.cwd,
|
|
1518
|
+
metadata: state.metadata
|
|
1519
|
+
});
|
|
1520
|
+
throw new SessionExecutionError(payload);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// src/agent-core/TerminationPolicy.ts
|
|
1525
|
+
class DefaultTerminationPolicy {
|
|
1526
|
+
afterAssistantTurn(toolCalls) {
|
|
1527
|
+
return toolCalls.length === 0 ? { type: "done" } : { type: "continue" };
|
|
1528
|
+
}
|
|
1529
|
+
onMaxIterationsExceeded(maxIterations, requestId) {
|
|
1530
|
+
return {
|
|
1531
|
+
code: "max_iterations_exceeded",
|
|
1532
|
+
message: `Session exceeded maxIterations (${maxIterations})`,
|
|
1533
|
+
requestId
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// src/agent-core/ToolExecutor.ts
|
|
1539
|
+
class DefaultToolExecutor {
|
|
1540
|
+
sessionId;
|
|
1541
|
+
tools;
|
|
1542
|
+
metadata;
|
|
1543
|
+
services;
|
|
1544
|
+
pluginHost;
|
|
1545
|
+
constructor(options) {
|
|
1546
|
+
this.sessionId = options.sessionId;
|
|
1547
|
+
this.tools = options.tools;
|
|
1548
|
+
this.metadata = options.metadata;
|
|
1549
|
+
this.services = options.services;
|
|
1550
|
+
this.pluginHost = options.pluginHost;
|
|
1551
|
+
}
|
|
1552
|
+
async execute(toolCall, options = {}) {
|
|
1553
|
+
const before = await this.pluginHost?.runMiddleware("tool.beforeExecute", {
|
|
1554
|
+
toolCall
|
|
1555
|
+
}, {
|
|
1556
|
+
sessionId: this.sessionId,
|
|
1557
|
+
cwd: options.cwd,
|
|
1558
|
+
services: this.services,
|
|
1559
|
+
metadata: this.metadata
|
|
1560
|
+
});
|
|
1561
|
+
const currentToolCall = before?.toolCall ?? toolCall;
|
|
1562
|
+
const tool = this.tools?.get(currentToolCall.function.name);
|
|
1563
|
+
if (!tool)
|
|
1564
|
+
return normalizeToolResult({ ...textToolResult(`Tool not found: ${currentToolCall.function.name}`), isError: true });
|
|
1565
|
+
let result;
|
|
1566
|
+
try {
|
|
1567
|
+
result = normalizeToolResult(await tool.execute(currentToolCall.function.arguments, {
|
|
1568
|
+
sessionId: this.sessionId,
|
|
1569
|
+
cwd: options.cwd,
|
|
1570
|
+
signal: options.signal,
|
|
1571
|
+
metadata: this.metadata,
|
|
1572
|
+
services: this.services
|
|
1573
|
+
}));
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1576
|
+
result = normalizeToolResult({
|
|
1577
|
+
...textToolResult(message),
|
|
1578
|
+
isError: true
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
const after = await this.pluginHost?.runMiddleware("tool.afterExecute", {
|
|
1582
|
+
toolCall: currentToolCall,
|
|
1583
|
+
result
|
|
1584
|
+
}, {
|
|
1585
|
+
sessionId: this.sessionId,
|
|
1586
|
+
cwd: options.cwd,
|
|
1587
|
+
services: this.services,
|
|
1588
|
+
metadata: this.metadata
|
|
1589
|
+
});
|
|
1590
|
+
return normalizeToolResult(after?.result ?? result);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// src/agent-core/Session.ts
|
|
1595
|
+
class Session {
|
|
1596
|
+
id;
|
|
1597
|
+
createdAt;
|
|
1598
|
+
stateStore;
|
|
1599
|
+
state;
|
|
1600
|
+
pluginHost;
|
|
1601
|
+
contextManager;
|
|
1602
|
+
services;
|
|
1603
|
+
constructor(options) {
|
|
1604
|
+
const now = Date.now();
|
|
1605
|
+
this.id = options.id ?? createId("session");
|
|
1606
|
+
this.createdAt = options.createdAt ?? now;
|
|
1607
|
+
this.stateStore = options.stateStore;
|
|
1608
|
+
this.pluginHost = options.pluginHost;
|
|
1609
|
+
this.contextManager = options.contextManager;
|
|
1610
|
+
this.services = options.services;
|
|
1611
|
+
this.state = {
|
|
1612
|
+
id: this.id,
|
|
1613
|
+
model: options.model,
|
|
1614
|
+
modelRef: options.modelRef ?? options.model.model,
|
|
1615
|
+
tools: options.tools,
|
|
1616
|
+
maxIterations: options.maxIterations ?? 8,
|
|
1617
|
+
cwd: options.cwd,
|
|
1618
|
+
metadata: options.metadata,
|
|
1619
|
+
reasoning: options.reasoning,
|
|
1620
|
+
messages: [...options.messages ?? []],
|
|
1621
|
+
usage: options.usage ? { ...options.usage } : createEmptyUsage(),
|
|
1622
|
+
createdAt: this.createdAt,
|
|
1623
|
+
updatedAt: options.updatedAt ?? now
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
get messages() {
|
|
1627
|
+
return [...this.state.messages];
|
|
1628
|
+
}
|
|
1629
|
+
get usage() {
|
|
1630
|
+
return { ...this.state.usage };
|
|
1631
|
+
}
|
|
1632
|
+
get updatedAt() {
|
|
1633
|
+
return this.state.updatedAt;
|
|
1634
|
+
}
|
|
1635
|
+
getCwd() {
|
|
1636
|
+
return this.state.cwd;
|
|
1637
|
+
}
|
|
1638
|
+
setCwd(cwd) {
|
|
1639
|
+
this.state.cwd = cwd;
|
|
1640
|
+
touchRuntimeSessionState(this.state);
|
|
1641
|
+
}
|
|
1642
|
+
async save() {
|
|
1643
|
+
if (!this.stateStore)
|
|
1644
|
+
return;
|
|
1645
|
+
let snapshot = this.toSnapshot();
|
|
1646
|
+
if (this.pluginHost) {
|
|
1647
|
+
const payload = await this.pluginHost.runMiddleware("snapshot.encode", { snapshot }, {
|
|
1648
|
+
sessionId: this.id,
|
|
1649
|
+
cwd: this.state.cwd,
|
|
1650
|
+
metadata: this.state.metadata,
|
|
1651
|
+
services: this.services
|
|
1652
|
+
});
|
|
1653
|
+
snapshot = payload.snapshot;
|
|
1654
|
+
}
|
|
1655
|
+
await this.stateStore.save(snapshot);
|
|
1656
|
+
}
|
|
1657
|
+
toSnapshot() {
|
|
1658
|
+
return sessionSnapshotCodec.encode({
|
|
1659
|
+
sessionId: this.id,
|
|
1660
|
+
model: this.state.modelRef,
|
|
1661
|
+
cwd: this.state.cwd,
|
|
1662
|
+
messages: this.messages,
|
|
1663
|
+
usage: this.usage,
|
|
1664
|
+
createdAt: this.createdAt,
|
|
1665
|
+
updatedAt: this.updatedAt,
|
|
1666
|
+
metadata: this.state.metadata ? { ...this.state.metadata } : undefined
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
async send(input, options) {
|
|
1670
|
+
const stream = this.stream(input, options);
|
|
1671
|
+
let doneMessage;
|
|
1672
|
+
while (true) {
|
|
1673
|
+
const next = await stream.next();
|
|
1674
|
+
if (next.done) {
|
|
1675
|
+
doneMessage = next.value;
|
|
1676
|
+
break;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
if (!doneMessage)
|
|
1680
|
+
throw new Error("Session completed without a final assistant message");
|
|
1681
|
+
return doneMessage;
|
|
1682
|
+
}
|
|
1683
|
+
async* stream(input, options) {
|
|
1684
|
+
const runner = this.createLoopRunner();
|
|
1685
|
+
return yield* runner.run(this.state, input, {
|
|
1686
|
+
signal: options?.signal,
|
|
1687
|
+
onDone: async () => {
|
|
1688
|
+
await this.save();
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
createLoopRunner() {
|
|
1693
|
+
return new LoopRunner({
|
|
1694
|
+
messageFactory: new DefaultMessageFactory,
|
|
1695
|
+
turnCollector: new DefaultModelTurnCollector,
|
|
1696
|
+
toolExecutor: new DefaultToolExecutor({
|
|
1697
|
+
sessionId: this.id,
|
|
1698
|
+
tools: this.state.tools,
|
|
1699
|
+
metadata: this.state.metadata,
|
|
1700
|
+
services: this.services,
|
|
1701
|
+
pluginHost: this.pluginHost
|
|
1702
|
+
}),
|
|
1703
|
+
terminationPolicy: new DefaultTerminationPolicy,
|
|
1704
|
+
contextManager: this.contextManager,
|
|
1705
|
+
pluginHost: this.pluginHost
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// src/agent-core/Agent.ts
|
|
1711
|
+
class Agent {
|
|
1712
|
+
id;
|
|
1713
|
+
pluginHost;
|
|
1714
|
+
services;
|
|
1715
|
+
tools;
|
|
1716
|
+
permissions;
|
|
1717
|
+
options;
|
|
1718
|
+
messageFactory = new DefaultMessageFactory;
|
|
1719
|
+
contextManager;
|
|
1720
|
+
tracker;
|
|
1721
|
+
permissionGateway;
|
|
1722
|
+
constructor(options) {
|
|
1723
|
+
this.id = createId("agent");
|
|
1724
|
+
this.options = options;
|
|
1725
|
+
this.permissions = options.permissions;
|
|
1726
|
+
this.tracker = new ActivityTracker;
|
|
1727
|
+
this.services = createServices(options, this.tracker);
|
|
1728
|
+
this.permissionGateway = new PermissionGateway({ permissions: options.permissions });
|
|
1729
|
+
this.pluginHost = new PluginHost({
|
|
1730
|
+
agentId: this.id,
|
|
1731
|
+
cwd: options.cwd,
|
|
1732
|
+
plugins: options.plugins,
|
|
1733
|
+
services: this.services,
|
|
1734
|
+
permissionGateway: this.permissionGateway,
|
|
1735
|
+
tracker: this.tracker
|
|
1736
|
+
});
|
|
1737
|
+
this.contextManager = options.contextManager ?? new AutoContextManager({
|
|
1738
|
+
services: this.services,
|
|
1739
|
+
pluginHost: this.pluginHost
|
|
1740
|
+
});
|
|
1741
|
+
this.tools = createToolRegistry({
|
|
1742
|
+
includeBuiltinTools: options.includeBuiltinTools !== false,
|
|
1743
|
+
tools: options.tools,
|
|
1744
|
+
pluginTools: this.pluginHost.pluginTools()
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
async createSession(options = {}) {
|
|
1748
|
+
const messages = options.messages ? [...options.messages] : [];
|
|
1749
|
+
if (options.systemPrompt)
|
|
1750
|
+
messages.unshift(this.messageFactory.createSystemMessage(options.systemPrompt));
|
|
1751
|
+
return new Session({
|
|
1752
|
+
id: options.sessionId ?? createId("session"),
|
|
1753
|
+
model: this.options.model,
|
|
1754
|
+
modelRef: this.options.model.model,
|
|
1755
|
+
tools: this.tools,
|
|
1756
|
+
stateStore: this.options.stateStore,
|
|
1757
|
+
maxIterations: this.options.maxIterations,
|
|
1758
|
+
cwd: options.cwd ?? this.options.cwd,
|
|
1759
|
+
metadata: options.metadata ?? this.options.metadata,
|
|
1760
|
+
reasoning: this.options.reasoning,
|
|
1761
|
+
messages,
|
|
1762
|
+
pluginHost: this.pluginHost,
|
|
1763
|
+
contextManager: this.contextManager,
|
|
1764
|
+
services: this.services
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
async restoreSession(sessionId) {
|
|
1768
|
+
if (!this.options.stateStore)
|
|
1769
|
+
return null;
|
|
1770
|
+
const snapshot = await this.options.stateStore.load(sessionId);
|
|
1771
|
+
if (!snapshot)
|
|
1772
|
+
return null;
|
|
1773
|
+
return this.sessionFromSnapshot(snapshot);
|
|
1774
|
+
}
|
|
1775
|
+
sessionFromSnapshot(snapshot) {
|
|
1776
|
+
return new Session({
|
|
1777
|
+
id: snapshot.sessionId,
|
|
1778
|
+
model: this.options.model,
|
|
1779
|
+
modelRef: snapshot.model ?? this.options.model.model,
|
|
1780
|
+
tools: this.tools,
|
|
1781
|
+
stateStore: this.options.stateStore,
|
|
1782
|
+
maxIterations: this.options.maxIterations,
|
|
1783
|
+
cwd: snapshot.cwd ?? this.options.cwd,
|
|
1784
|
+
metadata: snapshot.metadata ?? this.options.metadata,
|
|
1785
|
+
reasoning: this.options.reasoning,
|
|
1786
|
+
messages: snapshot.messages,
|
|
1787
|
+
usage: snapshot.usage,
|
|
1788
|
+
createdAt: snapshot.createdAt,
|
|
1789
|
+
updatedAt: snapshot.updatedAt,
|
|
1790
|
+
pluginHost: this.pluginHost,
|
|
1791
|
+
contextManager: this.contextManager,
|
|
1792
|
+
services: this.services
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
function createAgent(options) {
|
|
1797
|
+
return new Agent(options);
|
|
1798
|
+
}
|
|
1799
|
+
function createServices(options, tracker) {
|
|
1800
|
+
const exec = new NodeExecGateway({ cwd: options.cwd, tracker });
|
|
1801
|
+
return {
|
|
1802
|
+
fileSystem: new NodeFileSystemGateway({ cwd: options.cwd, tracker }),
|
|
1803
|
+
exec,
|
|
1804
|
+
git: new DefaultGitGateway({ exec, cwd: options.cwd }),
|
|
1805
|
+
network: new DefaultNetworkGateway,
|
|
1806
|
+
model: new DefaultModelGateway(options.model)
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
function createToolRegistry(input) {
|
|
1810
|
+
const registry = input.tools instanceof ToolRegistry ? cloneRegistry(input.tools) : new ToolRegistry;
|
|
1811
|
+
if (input.includeBuiltinTools) {
|
|
1812
|
+
safeRegister(registry, new ReadTool);
|
|
1813
|
+
safeRegister(registry, new WriteTool);
|
|
1814
|
+
safeRegister(registry, new EditTool);
|
|
1815
|
+
safeRegister(registry, new ExecTool);
|
|
1816
|
+
}
|
|
1817
|
+
for (const tool of Array.isArray(input.tools) ? input.tools : [])
|
|
1818
|
+
safeRegister(registry, tool);
|
|
1819
|
+
for (const tool of input.pluginTools)
|
|
1820
|
+
safeRegister(registry, tool);
|
|
1821
|
+
return registry;
|
|
1822
|
+
}
|
|
1823
|
+
function cloneRegistry(source) {
|
|
1824
|
+
const registry = new ToolRegistry;
|
|
1825
|
+
for (const tool of source.list())
|
|
1826
|
+
registry.register(tool);
|
|
1827
|
+
return registry;
|
|
1828
|
+
}
|
|
1829
|
+
function safeRegister(registry, tool) {
|
|
1830
|
+
if (!registry.get(tool.definition.name))
|
|
1831
|
+
registry.register(tool);
|
|
1832
|
+
}
|
|
1833
|
+
// src/persistence/FileStateStore.ts
|
|
1834
|
+
import { mkdir as mkdir2, readFile as readFile2, readdir as readdir2, rm, writeFile as writeFile2 } from "node:fs/promises";
|
|
1835
|
+
import path3 from "node:path";
|
|
1836
|
+
class FileStateStore {
|
|
1837
|
+
directory;
|
|
1838
|
+
constructor(options) {
|
|
1839
|
+
this.directory = path3.resolve(options.directory);
|
|
1840
|
+
}
|
|
1841
|
+
async save(snapshot) {
|
|
1842
|
+
const validated = sessionSnapshotCodec.decode(snapshot);
|
|
1843
|
+
await mkdir2(this.directory, { recursive: true });
|
|
1844
|
+
await writeFile2(this.filePath(validated.sessionId), JSON.stringify(validated, null, 2), "utf8");
|
|
1845
|
+
}
|
|
1846
|
+
async load(sessionId) {
|
|
1847
|
+
try {
|
|
1848
|
+
const raw = await readFile2(this.filePath(sessionId), "utf8");
|
|
1849
|
+
return sessionSnapshotCodec.decode(JSON.parse(raw));
|
|
1850
|
+
} catch (error) {
|
|
1851
|
+
if (error.code === "ENOENT")
|
|
1852
|
+
return null;
|
|
1853
|
+
throw error;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
async delete(sessionId) {
|
|
1857
|
+
await rm(this.filePath(sessionId), { force: true });
|
|
1858
|
+
}
|
|
1859
|
+
async list() {
|
|
1860
|
+
await mkdir2(this.directory, { recursive: true });
|
|
1861
|
+
const entries = (await readdir2(this.directory)).sort();
|
|
1862
|
+
const snapshots = [];
|
|
1863
|
+
for (const entry of entries) {
|
|
1864
|
+
if (!entry.endsWith(".json"))
|
|
1865
|
+
continue;
|
|
1866
|
+
const raw = await readFile2(path3.join(this.directory, entry), "utf8");
|
|
1867
|
+
snapshots.push(sessionSnapshotCodec.decode(JSON.parse(raw)));
|
|
1868
|
+
}
|
|
1869
|
+
return snapshots;
|
|
1870
|
+
}
|
|
1871
|
+
filePath(sessionId) {
|
|
1872
|
+
return path3.join(this.directory, `${encodeURIComponent(sessionId)}.json`);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
// src/persistence/InMemoryStateStore.ts
|
|
1876
|
+
class InMemoryStateStore {
|
|
1877
|
+
snapshots = new Map;
|
|
1878
|
+
async save(snapshot) {
|
|
1879
|
+
const validated = sessionSnapshotCodec.decode(snapshot);
|
|
1880
|
+
this.snapshots.set(validated.sessionId, validated);
|
|
1881
|
+
}
|
|
1882
|
+
async load(sessionId) {
|
|
1883
|
+
const snapshot = this.snapshots.get(sessionId);
|
|
1884
|
+
return snapshot ? sessionSnapshotCodec.decode(snapshot) : null;
|
|
1885
|
+
}
|
|
1886
|
+
async delete(sessionId) {
|
|
1887
|
+
this.snapshots.delete(sessionId);
|
|
1888
|
+
}
|
|
1889
|
+
async list() {
|
|
1890
|
+
return [...this.snapshots.values()].map((snapshot) => sessionSnapshotCodec.decode(snapshot));
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
// src/providers/shared/http-error.ts
|
|
1894
|
+
var MAX_TEXT_CHARS = 4000;
|
|
1895
|
+
var MAX_JSON_DEPTH = 4;
|
|
1896
|
+
var MAX_JSON_ARRAY_ITEMS = 20;
|
|
1897
|
+
var MAX_JSON_OBJECT_KEYS = 40;
|
|
1898
|
+
var TRUNCATED_SUFFIX = "...[truncated]";
|
|
1899
|
+
async function createHttpErrorPayload(input) {
|
|
1900
|
+
const responseBody = await readErrorResponseBody(input.response);
|
|
1901
|
+
return {
|
|
1902
|
+
code: input.code,
|
|
1903
|
+
message: `${input.provider} request failed with status ${input.response.status}`,
|
|
1904
|
+
status: input.response.status,
|
|
1905
|
+
details: {
|
|
1906
|
+
provider: input.provider,
|
|
1907
|
+
endpoint: input.endpoint,
|
|
1908
|
+
statusText: input.response.statusText,
|
|
1909
|
+
...responseBody === undefined ? {} : { responseBody }
|
|
1910
|
+
}
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
async function readErrorResponseBody(response) {
|
|
1914
|
+
let text = "";
|
|
1915
|
+
try {
|
|
1916
|
+
text = await response.text();
|
|
1917
|
+
} catch {
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
const normalized = text.trim();
|
|
1921
|
+
if (!normalized)
|
|
1922
|
+
return;
|
|
1923
|
+
try {
|
|
1924
|
+
return limitJsonValue(JSON.parse(normalized));
|
|
1925
|
+
} catch {
|
|
1926
|
+
return truncateText(normalized);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
function limitJsonValue(value, depth = 0) {
|
|
1930
|
+
if (value === null || typeof value === "boolean" || typeof value === "number")
|
|
1931
|
+
return value;
|
|
1932
|
+
if (typeof value === "string")
|
|
1933
|
+
return truncateText(value);
|
|
1934
|
+
if (Array.isArray(value))
|
|
1935
|
+
return value.slice(0, MAX_JSON_ARRAY_ITEMS).map((item) => limitJsonValue(item, depth + 1));
|
|
1936
|
+
if (typeof value === "object") {
|
|
1937
|
+
if (depth >= MAX_JSON_DEPTH)
|
|
1938
|
+
return truncateText(JSON.stringify(value));
|
|
1939
|
+
const limited = {};
|
|
1940
|
+
for (const [key, child] of Object.entries(value).slice(0, MAX_JSON_OBJECT_KEYS))
|
|
1941
|
+
limited[key] = limitJsonValue(child, depth + 1);
|
|
1942
|
+
return limited;
|
|
1943
|
+
}
|
|
1944
|
+
return truncateText(String(value));
|
|
1945
|
+
}
|
|
1946
|
+
function truncateText(value) {
|
|
1947
|
+
if (value.length <= MAX_TEXT_CHARS)
|
|
1948
|
+
return value;
|
|
1949
|
+
return `${value.slice(0, MAX_TEXT_CHARS)}${TRUNCATED_SUFFIX}`;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// src/providers/shared/provider-state.ts
|
|
1953
|
+
var PROVIDER_STATE_METADATA_KEY = "_dimProviderState";
|
|
1954
|
+
function attachProviderState(metadata, provider, state) {
|
|
1955
|
+
if (!state || Object.keys(state).length === 0)
|
|
1956
|
+
return metadata;
|
|
1957
|
+
const nextMetadata = metadata ? structuredClone(metadata) : {};
|
|
1958
|
+
const currentBag = isRecord2(nextMetadata[PROVIDER_STATE_METADATA_KEY]) ? nextMetadata[PROVIDER_STATE_METADATA_KEY] : {};
|
|
1959
|
+
nextMetadata[PROVIDER_STATE_METADATA_KEY] = {
|
|
1960
|
+
...currentBag,
|
|
1961
|
+
[provider]: structuredClone(state)
|
|
1962
|
+
};
|
|
1963
|
+
return nextMetadata;
|
|
1964
|
+
}
|
|
1965
|
+
function readProviderState(message, provider) {
|
|
1966
|
+
if (message.role !== "assistant" || !isRecord2(message.metadata))
|
|
1967
|
+
return;
|
|
1968
|
+
const bag = message.metadata[PROVIDER_STATE_METADATA_KEY];
|
|
1969
|
+
if (!isRecord2(bag))
|
|
1970
|
+
return;
|
|
1971
|
+
const state = bag[provider];
|
|
1972
|
+
return isRecord2(state) ? structuredClone(state) : undefined;
|
|
1973
|
+
}
|
|
1974
|
+
function isRecord2(value) {
|
|
1975
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// src/providers/shared/reasoning.ts
|
|
1979
|
+
function isRecord3(value) {
|
|
1980
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1981
|
+
}
|
|
1982
|
+
function normalizeReasoningConfig(reasoning) {
|
|
1983
|
+
if (!isRecord3(reasoning))
|
|
1984
|
+
return;
|
|
1985
|
+
if (reasoning.enabled === false)
|
|
1986
|
+
return;
|
|
1987
|
+
return reasoning;
|
|
1988
|
+
}
|
|
1989
|
+
function resolveReasoningBudget(reasoning, fallback = 1024) {
|
|
1990
|
+
const normalized = normalizeReasoningConfig(reasoning);
|
|
1991
|
+
if (!normalized)
|
|
1992
|
+
return;
|
|
1993
|
+
for (const key of ["budgetTokens", "budget_tokens", "thinkingBudget"]) {
|
|
1994
|
+
const value = normalized[key];
|
|
1995
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0)
|
|
1996
|
+
return Math.floor(value);
|
|
1997
|
+
}
|
|
1998
|
+
return fallback;
|
|
1999
|
+
}
|
|
2000
|
+
function buildOpenAIResponsesReasoning(reasoning) {
|
|
2001
|
+
const normalized = normalizeReasoningConfig(reasoning);
|
|
2002
|
+
if (!normalized)
|
|
2003
|
+
return;
|
|
2004
|
+
const next = {};
|
|
2005
|
+
for (const [key, value] of Object.entries(normalized)) {
|
|
2006
|
+
if (key === "enabled" || key === "budgetTokens" || key === "budget_tokens" || key === "thinkingBudget" || key === "includeThoughts")
|
|
2007
|
+
continue;
|
|
2008
|
+
next[key] = value;
|
|
2009
|
+
}
|
|
2010
|
+
if (next.summary === undefined)
|
|
2011
|
+
next.summary = "auto";
|
|
2012
|
+
return next;
|
|
2013
|
+
}
|
|
2014
|
+
function buildAnthropicThinking(reasoning) {
|
|
2015
|
+
const budget = resolveReasoningBudget(reasoning);
|
|
2016
|
+
if (!budget)
|
|
2017
|
+
return { maxTokens: undefined, thinking: undefined };
|
|
2018
|
+
return {
|
|
2019
|
+
maxTokens: Math.max(budget + 1, budget * 2),
|
|
2020
|
+
thinking: {
|
|
2021
|
+
type: "enabled",
|
|
2022
|
+
budget_tokens: budget
|
|
2023
|
+
}
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
function buildGeminiThinkingConfig(reasoning) {
|
|
2027
|
+
const normalized = normalizeReasoningConfig(reasoning);
|
|
2028
|
+
if (!normalized)
|
|
2029
|
+
return;
|
|
2030
|
+
return {
|
|
2031
|
+
thinkingBudget: resolveReasoningBudget(normalized) ?? 1024,
|
|
2032
|
+
includeThoughts: normalized.includeThoughts !== false
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// src/providers/shared/usage.ts
|
|
2037
|
+
function normalizeUsage(raw) {
|
|
2038
|
+
if (!raw || typeof raw !== "object")
|
|
2039
|
+
return;
|
|
2040
|
+
const usage = raw;
|
|
2041
|
+
const promptTokens = numberOrZero(usage.prompt_tokens ?? usage.input_tokens ?? usage.promptTokenCount);
|
|
2042
|
+
const completionTokens = numberOrZero(usage.completion_tokens ?? usage.output_tokens ?? usage.candidatesTokenCount);
|
|
2043
|
+
const totalTokens = numberOrZero(usage.total_tokens ?? usage.totalTokenCount) || promptTokens + completionTokens;
|
|
2044
|
+
if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0)
|
|
2045
|
+
return;
|
|
2046
|
+
return {
|
|
2047
|
+
promptTokens,
|
|
2048
|
+
completionTokens,
|
|
2049
|
+
totalTokens
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
function numberOrZero(value) {
|
|
2053
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// src/providers/anthropic/mapper.ts
|
|
2057
|
+
var ANTHROPIC_STATE_KEY = "anthropic";
|
|
2058
|
+
function mapToolDefinitionsToAnthropic(tools) {
|
|
2059
|
+
if (!tools || tools.length === 0)
|
|
2060
|
+
return;
|
|
2061
|
+
return tools.map((tool) => ({
|
|
2062
|
+
name: tool.name,
|
|
2063
|
+
description: tool.description,
|
|
2064
|
+
input_schema: tool.inputSchema
|
|
2065
|
+
}));
|
|
2066
|
+
}
|
|
2067
|
+
function mapAnthropicStopReason(reason, toolCallCount) {
|
|
2068
|
+
if (toolCallCount > 0 || reason === "tool_use")
|
|
2069
|
+
return "tool_call";
|
|
2070
|
+
if (reason === "max_tokens")
|
|
2071
|
+
return "length";
|
|
2072
|
+
if (reason === "cancelled")
|
|
2073
|
+
return "cancelled";
|
|
2074
|
+
if (reason === "error")
|
|
2075
|
+
return "error";
|
|
2076
|
+
return "final";
|
|
2077
|
+
}
|
|
2078
|
+
function messagesToAnthropic(messages) {
|
|
2079
|
+
const systemChunks = [];
|
|
2080
|
+
const converted = [];
|
|
2081
|
+
for (const message of messages) {
|
|
2082
|
+
if (message.role === "system") {
|
|
2083
|
+
const text = contentToText(message.content);
|
|
2084
|
+
if (text)
|
|
2085
|
+
systemChunks.push(text);
|
|
2086
|
+
continue;
|
|
2087
|
+
}
|
|
2088
|
+
if (message.role === "tool") {
|
|
2089
|
+
converted.push({
|
|
2090
|
+
role: "user",
|
|
2091
|
+
content: [{
|
|
2092
|
+
type: "tool_result",
|
|
2093
|
+
tool_use_id: message.toolCallId,
|
|
2094
|
+
content: contentToText(message.content),
|
|
2095
|
+
is_error: message.isError === true
|
|
2096
|
+
}]
|
|
2097
|
+
});
|
|
2098
|
+
continue;
|
|
2099
|
+
}
|
|
2100
|
+
if (message.role === "assistant") {
|
|
2101
|
+
const blocks = [
|
|
2102
|
+
...restoreAnthropicThinkingBlocks(message),
|
|
2103
|
+
...message.content.map((block) => block.type === "text" ? { type: "text", text: block.text } : { type: "image", source: { type: "base64", media_type: block.mimeType, data: block.data } })
|
|
2104
|
+
];
|
|
2105
|
+
for (const toolCall of message.toolCalls ?? []) {
|
|
2106
|
+
blocks.push({
|
|
2107
|
+
type: "tool_use",
|
|
2108
|
+
id: toolCall.id,
|
|
2109
|
+
name: toolCall.function.name,
|
|
2110
|
+
input: toolCall.function.arguments
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
converted.push({ role: "assistant", content: blocks });
|
|
2114
|
+
continue;
|
|
2115
|
+
}
|
|
2116
|
+
converted.push({
|
|
2117
|
+
role: "user",
|
|
2118
|
+
content: message.content.map((block) => block.type === "text" ? { type: "text", text: block.text } : { type: "image", source: { type: "base64", media_type: block.mimeType, data: block.data } })
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
return {
|
|
2122
|
+
system: systemChunks.length > 0 ? systemChunks.join(`
|
|
2123
|
+
|
|
2124
|
+
`) : undefined,
|
|
2125
|
+
messages: converted
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
function normalizeAnthropicToolCalls(value) {
|
|
2129
|
+
if (!value)
|
|
2130
|
+
return [];
|
|
2131
|
+
const calls = [];
|
|
2132
|
+
for (const block of value) {
|
|
2133
|
+
if (block.type !== "tool_use" || typeof block.id !== "string" || typeof block.name !== "string")
|
|
2134
|
+
continue;
|
|
2135
|
+
calls.push({
|
|
2136
|
+
id: block.id,
|
|
2137
|
+
name: block.name,
|
|
2138
|
+
argsText: JSON.stringify(block.input ?? {})
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
return calls;
|
|
2142
|
+
}
|
|
2143
|
+
function extractAnthropicThinking(value) {
|
|
2144
|
+
if (!value)
|
|
2145
|
+
return [];
|
|
2146
|
+
return value.filter((block) => block.type === "thinking").map((block) => block.thinking).filter((value2) => typeof value2 === "string" && value2.length > 0);
|
|
2147
|
+
}
|
|
2148
|
+
function createAnthropicThinkingState(value) {
|
|
2149
|
+
if (!value)
|
|
2150
|
+
return;
|
|
2151
|
+
const blocks = value.filter((block) => block.type === "thinking" || block.type === "redacted_thinking").map((block) => {
|
|
2152
|
+
if (block.type === "thinking")
|
|
2153
|
+
return { type: "thinking", thinking: block.thinking, signature: block.signature };
|
|
2154
|
+
return { type: "redacted_thinking", data: block.data };
|
|
2155
|
+
}).filter((block) => block.type === "thinking" && typeof block.signature === "string" || block.type === "redacted_thinking" && typeof block.data === "string");
|
|
2156
|
+
if (blocks.length === 0)
|
|
2157
|
+
return;
|
|
2158
|
+
return { blocks };
|
|
2159
|
+
}
|
|
2160
|
+
function restoreAnthropicThinkingBlocks(message) {
|
|
2161
|
+
const state = readProviderState(message, ANTHROPIC_STATE_KEY);
|
|
2162
|
+
const rawBlocks = state?.blocks;
|
|
2163
|
+
if (!Array.isArray(rawBlocks))
|
|
2164
|
+
return [];
|
|
2165
|
+
const blocks = [];
|
|
2166
|
+
for (const rawBlock of rawBlocks) {
|
|
2167
|
+
if (!isRecord4(rawBlock) || typeof rawBlock.type !== "string")
|
|
2168
|
+
continue;
|
|
2169
|
+
if (rawBlock.type === "thinking" && typeof rawBlock.signature === "string") {
|
|
2170
|
+
blocks.push({
|
|
2171
|
+
type: "thinking",
|
|
2172
|
+
thinking: typeof rawBlock.thinking === "string" ? rawBlock.thinking : undefined,
|
|
2173
|
+
signature: rawBlock.signature
|
|
2174
|
+
});
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
if (rawBlock.type === "redacted_thinking" && typeof rawBlock.data === "string") {
|
|
2178
|
+
blocks.push({
|
|
2179
|
+
type: "redacted_thinking",
|
|
2180
|
+
data: rawBlock.data
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
return blocks;
|
|
2185
|
+
}
|
|
2186
|
+
function isRecord4(value) {
|
|
2187
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// src/providers/anthropic/adapter.ts
|
|
2191
|
+
function createAnthropicAdapter(options) {
|
|
2192
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
2193
|
+
const provider = options.provider ?? "anthropic";
|
|
2194
|
+
const defaultModel = { provider, modelId: options.defaultModel };
|
|
2195
|
+
const baseUrl = (options.baseUrl ?? "https://api.anthropic.com").replace(/\/$/, "");
|
|
2196
|
+
const anthropicVersion = options.anthropicVersion ?? "2023-06-01";
|
|
2197
|
+
return {
|
|
2198
|
+
provider,
|
|
2199
|
+
defaultModel,
|
|
2200
|
+
async* stream(request) {
|
|
2201
|
+
const requestId = request.requestId ?? crypto.randomUUID();
|
|
2202
|
+
yield { type: "response_start", requestId, model: request.model };
|
|
2203
|
+
const { system, messages } = messagesToAnthropic(request.messages);
|
|
2204
|
+
const thinkingConfig = buildAnthropicThinking(request.reasoning);
|
|
2205
|
+
const maxTokens = Math.max(request.maxOutputTokens ?? 1024, thinkingConfig.maxTokens ?? 0);
|
|
2206
|
+
let response;
|
|
2207
|
+
try {
|
|
2208
|
+
response = await fetchImpl(`${baseUrl}/v1/messages`, {
|
|
2209
|
+
method: "POST",
|
|
2210
|
+
headers: {
|
|
2211
|
+
"content-type": "application/json",
|
|
2212
|
+
"anthropic-version": anthropicVersion,
|
|
2213
|
+
...options.apiKey ? { "x-api-key": options.apiKey } : {},
|
|
2214
|
+
...options.headers
|
|
2215
|
+
},
|
|
2216
|
+
body: JSON.stringify({
|
|
2217
|
+
model: request.model.modelId,
|
|
2218
|
+
system,
|
|
2219
|
+
messages,
|
|
2220
|
+
tools: mapToolDefinitionsToAnthropic(request.tools),
|
|
2221
|
+
max_tokens: maxTokens,
|
|
2222
|
+
temperature: request.temperature,
|
|
2223
|
+
top_p: request.topP,
|
|
2224
|
+
thinking: thinkingConfig.thinking
|
|
2225
|
+
}),
|
|
2226
|
+
signal: request.signal
|
|
2227
|
+
});
|
|
2228
|
+
} catch (error) {
|
|
2229
|
+
yield { type: "error", requestId, error: toModelErrorPayload(error), terminal: true };
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
if (!response.ok) {
|
|
2233
|
+
yield {
|
|
2234
|
+
type: "error",
|
|
2235
|
+
requestId,
|
|
2236
|
+
error: await createHttpErrorPayload({
|
|
2237
|
+
code: "anthropic_http_error",
|
|
2238
|
+
provider: "Anthropic",
|
|
2239
|
+
endpoint: `${baseUrl}/v1/messages`,
|
|
2240
|
+
response
|
|
2241
|
+
}),
|
|
2242
|
+
terminal: true
|
|
2243
|
+
};
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
const data = await response.json();
|
|
2247
|
+
for (const delta of extractAnthropicThinking(data.content))
|
|
2248
|
+
yield { type: "thinking_delta", requestId, delta };
|
|
2249
|
+
for (const block of data.content ?? []) {
|
|
2250
|
+
if (block.type === "text" && block.text)
|
|
2251
|
+
yield { type: "text_delta", requestId, delta: block.text };
|
|
2252
|
+
}
|
|
2253
|
+
const toolCalls = normalizeAnthropicToolCalls(data.content);
|
|
2254
|
+
for (const toolCall of toolCalls) {
|
|
2255
|
+
yield { type: "tool_call_start", requestId, callId: toolCall.id, toolName: toolCall.name };
|
|
2256
|
+
yield { type: "tool_call_args_delta", requestId, callId: toolCall.id, delta: toolCall.argsText };
|
|
2257
|
+
yield { type: "tool_call_end", requestId, callId: toolCall.id };
|
|
2258
|
+
}
|
|
2259
|
+
yield {
|
|
2260
|
+
type: "response_end",
|
|
2261
|
+
requestId,
|
|
2262
|
+
stopReason: mapAnthropicStopReason(data.stop_reason, toolCalls.length),
|
|
2263
|
+
usage: normalizeUsage(data.usage),
|
|
2264
|
+
assistantMetadata: attachProviderState(undefined, "anthropic", createAnthropicThinkingState(data.content))
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
function toModelErrorPayload(error) {
|
|
2270
|
+
if (error instanceof Error) {
|
|
2271
|
+
return {
|
|
2272
|
+
code: "anthropic_request_error",
|
|
2273
|
+
message: error.message
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
return {
|
|
2277
|
+
code: "anthropic_request_error",
|
|
2278
|
+
message: String(error)
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
// src/providers/gemini/mapper.ts
|
|
2282
|
+
var GEMINI_STATE_KEY = "gemini";
|
|
2283
|
+
function mapToolDefinitionsToGemini(tools) {
|
|
2284
|
+
if (!tools || tools.length === 0)
|
|
2285
|
+
return {};
|
|
2286
|
+
return {
|
|
2287
|
+
tools: [{
|
|
2288
|
+
functionDeclarations: tools.map((tool) => ({
|
|
2289
|
+
name: tool.name,
|
|
2290
|
+
description: tool.description,
|
|
2291
|
+
parametersJsonSchema: tool.inputSchema
|
|
2292
|
+
}))
|
|
2293
|
+
}],
|
|
2294
|
+
toolConfig: {
|
|
2295
|
+
functionCallingConfig: {
|
|
2296
|
+
mode: "AUTO"
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
function mapGeminiStopReason(reason, toolCallCount) {
|
|
2302
|
+
if (toolCallCount > 0 || reason === "STOPFUNCTIONCALL" || reason === "function_call")
|
|
2303
|
+
return "tool_call";
|
|
2304
|
+
if (reason === "MAX_TOKENS")
|
|
2305
|
+
return "length";
|
|
2306
|
+
if (reason === "CANCELLED")
|
|
2307
|
+
return "cancelled";
|
|
2308
|
+
if (reason === "SAFETY" || reason === "RECITATION" || reason === "ERROR")
|
|
2309
|
+
return "error";
|
|
2310
|
+
return "final";
|
|
2311
|
+
}
|
|
2312
|
+
function messagesToGemini(messages) {
|
|
2313
|
+
const systemChunks = [];
|
|
2314
|
+
const contents = [];
|
|
2315
|
+
for (const message of messages) {
|
|
2316
|
+
if (message.role === "system") {
|
|
2317
|
+
const text = contentToText(message.content);
|
|
2318
|
+
if (text)
|
|
2319
|
+
systemChunks.push(text);
|
|
2320
|
+
continue;
|
|
2321
|
+
}
|
|
2322
|
+
if (message.role === "tool") {
|
|
2323
|
+
contents.push({
|
|
2324
|
+
role: "user",
|
|
2325
|
+
parts: [{
|
|
2326
|
+
functionResponse: {
|
|
2327
|
+
name: message.toolName,
|
|
2328
|
+
response: message.structuredContent ?? {
|
|
2329
|
+
content: contentToText(message.content),
|
|
2330
|
+
isError: message.isError === true
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
}]
|
|
2334
|
+
});
|
|
2335
|
+
continue;
|
|
2336
|
+
}
|
|
2337
|
+
const parts = [];
|
|
2338
|
+
if (message.role === "assistant")
|
|
2339
|
+
parts.push(...restoreGeminiThoughtParts(message));
|
|
2340
|
+
for (const block of message.content) {
|
|
2341
|
+
if (block.type === "text")
|
|
2342
|
+
parts.push({ text: block.text });
|
|
2343
|
+
else
|
|
2344
|
+
parts.push({ inlineData: { mimeType: block.mimeType, data: block.data } });
|
|
2345
|
+
}
|
|
2346
|
+
if (message.role === "assistant") {
|
|
2347
|
+
for (const toolCall of message.toolCalls ?? []) {
|
|
2348
|
+
parts.push({
|
|
2349
|
+
functionCall: {
|
|
2350
|
+
name: toolCall.function.name,
|
|
2351
|
+
args: toolCall.function.arguments
|
|
2352
|
+
}
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
contents.push({
|
|
2357
|
+
role: message.role === "assistant" ? "model" : "user",
|
|
2358
|
+
parts
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
return {
|
|
2362
|
+
contents,
|
|
2363
|
+
systemInstruction: systemChunks.length > 0 ? { parts: systemChunks.map((text) => ({ text })) } : undefined
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
function normalizeGeminiToolCalls(value) {
|
|
2367
|
+
if (!value)
|
|
2368
|
+
return [];
|
|
2369
|
+
const calls = [];
|
|
2370
|
+
for (const part of value) {
|
|
2371
|
+
if (!("functionCall" in part) || typeof part.functionCall?.name !== "string")
|
|
2372
|
+
continue;
|
|
2373
|
+
calls.push({
|
|
2374
|
+
id: crypto.randomUUID(),
|
|
2375
|
+
name: String(part.functionCall.name),
|
|
2376
|
+
argsText: JSON.stringify(part.functionCall.args ?? {})
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
return calls;
|
|
2380
|
+
}
|
|
2381
|
+
function extractGeminiThinking(value) {
|
|
2382
|
+
if (!value)
|
|
2383
|
+
return [];
|
|
2384
|
+
const deltas = [];
|
|
2385
|
+
for (const part of value) {
|
|
2386
|
+
if ("text" in part && part.thought === true && typeof part.text === "string" && part.text.length > 0)
|
|
2387
|
+
deltas.push(part.text);
|
|
2388
|
+
}
|
|
2389
|
+
return deltas;
|
|
2390
|
+
}
|
|
2391
|
+
function createGeminiThoughtState(value) {
|
|
2392
|
+
if (!value)
|
|
2393
|
+
return;
|
|
2394
|
+
const parts = value.filter((part) => part.thought === true || typeof part.thoughtSignature === "string").map((part) => {
|
|
2395
|
+
if ("functionCall" in part) {
|
|
2396
|
+
return {
|
|
2397
|
+
functionCall: part.functionCall,
|
|
2398
|
+
thought: part.thought === true,
|
|
2399
|
+
thoughtSignature: part.thoughtSignature
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
return {
|
|
2403
|
+
text: "text" in part ? part.text : undefined,
|
|
2404
|
+
thought: part.thought === true,
|
|
2405
|
+
thoughtSignature: part.thoughtSignature
|
|
2406
|
+
};
|
|
2407
|
+
});
|
|
2408
|
+
if (parts.length === 0)
|
|
2409
|
+
return;
|
|
2410
|
+
return { parts };
|
|
2411
|
+
}
|
|
2412
|
+
function restoreGeminiThoughtParts(message) {
|
|
2413
|
+
const state = readProviderState(message, GEMINI_STATE_KEY);
|
|
2414
|
+
const rawParts = state?.parts;
|
|
2415
|
+
if (!Array.isArray(rawParts))
|
|
2416
|
+
return [];
|
|
2417
|
+
const parts = [];
|
|
2418
|
+
for (const rawPart of rawParts) {
|
|
2419
|
+
if (!isRecord5(rawPart))
|
|
2420
|
+
continue;
|
|
2421
|
+
const thought = rawPart.thought === true;
|
|
2422
|
+
const thoughtSignature = typeof rawPart.thoughtSignature === "string" ? rawPart.thoughtSignature : undefined;
|
|
2423
|
+
if (isRecord5(rawPart.functionCall) && typeof rawPart.functionCall.name === "string") {
|
|
2424
|
+
parts.push({
|
|
2425
|
+
functionCall: {
|
|
2426
|
+
name: rawPart.functionCall.name,
|
|
2427
|
+
args: isRecord5(rawPart.functionCall.args) ? rawPart.functionCall.args : {}
|
|
2428
|
+
},
|
|
2429
|
+
thought,
|
|
2430
|
+
thoughtSignature
|
|
2431
|
+
});
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
if (typeof rawPart.text === "string" || thoughtSignature) {
|
|
2435
|
+
parts.push({
|
|
2436
|
+
text: typeof rawPart.text === "string" ? rawPart.text : undefined,
|
|
2437
|
+
thought,
|
|
2438
|
+
thoughtSignature
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
return parts;
|
|
2443
|
+
}
|
|
2444
|
+
function isRecord5(value) {
|
|
2445
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
// src/providers/gemini/adapter.ts
|
|
2449
|
+
function createGeminiAdapter(options) {
|
|
2450
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
2451
|
+
const provider = options.provider ?? "gemini";
|
|
2452
|
+
const defaultModel = { provider, modelId: options.defaultModel };
|
|
2453
|
+
const baseUrl = (options.baseUrl ?? "https://generativelanguage.googleapis.com").replace(/\/$/, "");
|
|
2454
|
+
return {
|
|
2455
|
+
provider,
|
|
2456
|
+
defaultModel,
|
|
2457
|
+
async* stream(request) {
|
|
2458
|
+
const requestId = request.requestId ?? crypto.randomUUID();
|
|
2459
|
+
yield { type: "response_start", requestId, model: request.model };
|
|
2460
|
+
const { contents, systemInstruction } = messagesToGemini(request.messages);
|
|
2461
|
+
const { tools, toolConfig } = mapToolDefinitionsToGemini(request.tools);
|
|
2462
|
+
const thinkingConfig = buildGeminiThinkingConfig(request.reasoning);
|
|
2463
|
+
const searchParams = new URLSearchParams;
|
|
2464
|
+
if (options.apiKey)
|
|
2465
|
+
searchParams.set("key", options.apiKey);
|
|
2466
|
+
const url = `${baseUrl}/v1beta/models/${encodeURIComponent(request.model.modelId)}:generateContent${searchParams.toString() ? `?${searchParams}` : ""}`;
|
|
2467
|
+
let response;
|
|
2468
|
+
try {
|
|
2469
|
+
response = await fetchImpl(url, {
|
|
2470
|
+
method: "POST",
|
|
2471
|
+
headers: {
|
|
2472
|
+
"content-type": "application/json",
|
|
2473
|
+
...options.headers
|
|
2474
|
+
},
|
|
2475
|
+
body: JSON.stringify({
|
|
2476
|
+
contents,
|
|
2477
|
+
systemInstruction,
|
|
2478
|
+
tools,
|
|
2479
|
+
toolConfig,
|
|
2480
|
+
generationConfig: {
|
|
2481
|
+
temperature: request.temperature,
|
|
2482
|
+
topP: request.topP,
|
|
2483
|
+
maxOutputTokens: request.maxOutputTokens,
|
|
2484
|
+
stopSequences: request.stop,
|
|
2485
|
+
thinkingConfig
|
|
2486
|
+
}
|
|
2487
|
+
}),
|
|
2488
|
+
signal: request.signal
|
|
2489
|
+
});
|
|
2490
|
+
} catch (error) {
|
|
2491
|
+
yield { type: "error", requestId, error: toModelErrorPayload2(error), terminal: true };
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
if (!response.ok) {
|
|
2495
|
+
yield {
|
|
2496
|
+
type: "error",
|
|
2497
|
+
requestId,
|
|
2498
|
+
error: await createHttpErrorPayload({
|
|
2499
|
+
code: "gemini_http_error",
|
|
2500
|
+
provider: "Gemini",
|
|
2501
|
+
endpoint: url,
|
|
2502
|
+
response
|
|
2503
|
+
}),
|
|
2504
|
+
terminal: true
|
|
2505
|
+
};
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
const data = await response.json();
|
|
2509
|
+
const candidate = data.candidates?.[0];
|
|
2510
|
+
for (const delta of extractGeminiThinking(candidate?.content?.parts))
|
|
2511
|
+
yield { type: "thinking_delta", requestId, delta };
|
|
2512
|
+
for (const part of candidate?.content?.parts ?? []) {
|
|
2513
|
+
if ("text" in part && part.thought !== true && typeof part.text === "string" && part.text.length > 0)
|
|
2514
|
+
yield { type: "text_delta", requestId, delta: part.text };
|
|
2515
|
+
}
|
|
2516
|
+
const toolCalls = normalizeGeminiToolCalls(candidate?.content?.parts);
|
|
2517
|
+
for (const toolCall of toolCalls) {
|
|
2518
|
+
yield { type: "tool_call_start", requestId, callId: toolCall.id, toolName: toolCall.name };
|
|
2519
|
+
yield { type: "tool_call_args_delta", requestId, callId: toolCall.id, delta: toolCall.argsText };
|
|
2520
|
+
yield { type: "tool_call_end", requestId, callId: toolCall.id };
|
|
2521
|
+
}
|
|
2522
|
+
yield {
|
|
2523
|
+
type: "response_end",
|
|
2524
|
+
requestId,
|
|
2525
|
+
stopReason: mapGeminiStopReason(candidate?.finishReason, toolCalls.length),
|
|
2526
|
+
usage: normalizeUsage(data.usageMetadata),
|
|
2527
|
+
assistantMetadata: attachProviderState(undefined, "gemini", createGeminiThoughtState(candidate?.content?.parts))
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
function toModelErrorPayload2(error) {
|
|
2533
|
+
if (error instanceof Error) {
|
|
2534
|
+
return {
|
|
2535
|
+
code: "gemini_request_error",
|
|
2536
|
+
message: error.message
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
return {
|
|
2540
|
+
code: "gemini_request_error",
|
|
2541
|
+
message: String(error)
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
// src/providers/openai/mapper.ts
|
|
2545
|
+
function mapToolDefinitionsToOpenAI(tools) {
|
|
2546
|
+
if (!tools || tools.length === 0)
|
|
2547
|
+
return;
|
|
2548
|
+
return tools.map((tool) => ({
|
|
2549
|
+
type: "function",
|
|
2550
|
+
function: {
|
|
2551
|
+
name: tool.name,
|
|
2552
|
+
description: tool.description,
|
|
2553
|
+
parameters: tool.inputSchema
|
|
2554
|
+
}
|
|
2555
|
+
}));
|
|
2556
|
+
}
|
|
2557
|
+
function mapOpenAIStopReason(reason, toolCallCount) {
|
|
2558
|
+
if (toolCallCount > 0 || reason === "tool_calls")
|
|
2559
|
+
return "tool_call";
|
|
2560
|
+
if (reason === "length")
|
|
2561
|
+
return "length";
|
|
2562
|
+
if (reason === "content_filter" || reason === "error")
|
|
2563
|
+
return "error";
|
|
2564
|
+
if (reason === "cancelled")
|
|
2565
|
+
return "cancelled";
|
|
2566
|
+
return "final";
|
|
2567
|
+
}
|
|
2568
|
+
function messageContentToOpenAI(content) {
|
|
2569
|
+
const hasImage = content.some((block) => block.type === "image");
|
|
2570
|
+
if (!hasImage)
|
|
2571
|
+
return contentToText(content);
|
|
2572
|
+
return content.map((block) => mapContentBlockToOpenAIPart(block));
|
|
2573
|
+
}
|
|
2574
|
+
function mapContentBlockToOpenAIPart(block) {
|
|
2575
|
+
if (block.type === "text")
|
|
2576
|
+
return { type: "text", text: block.text };
|
|
2577
|
+
return {
|
|
2578
|
+
type: "image_url",
|
|
2579
|
+
image_url: {
|
|
2580
|
+
url: imageDataUrl(block)
|
|
2581
|
+
}
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
function imageDataUrl(block) {
|
|
2585
|
+
return `data:${block.mimeType};base64,${block.data}`;
|
|
2586
|
+
}
|
|
2587
|
+
function messagesToOpenAI(messages) {
|
|
2588
|
+
return messages.map((message) => {
|
|
2589
|
+
if (message.role === "tool") {
|
|
2590
|
+
return {
|
|
2591
|
+
role: "tool",
|
|
2592
|
+
tool_call_id: message.toolCallId,
|
|
2593
|
+
content: message.content.length > 0 ? contentToText(message.content) : JSON.stringify(message.structuredContent ?? {})
|
|
2594
|
+
};
|
|
2595
|
+
}
|
|
2596
|
+
if (message.role === "assistant") {
|
|
2597
|
+
return {
|
|
2598
|
+
role: "assistant",
|
|
2599
|
+
content: contentToText(message.content),
|
|
2600
|
+
tool_calls: message.toolCalls?.map((toolCall) => ({
|
|
2601
|
+
id: toolCall.id,
|
|
2602
|
+
type: "function",
|
|
2603
|
+
function: {
|
|
2604
|
+
name: toolCall.function.name,
|
|
2605
|
+
arguments: JSON.stringify(toolCall.function.arguments)
|
|
2606
|
+
}
|
|
2607
|
+
}))
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
return {
|
|
2611
|
+
role: message.role,
|
|
2612
|
+
content: messageContentToOpenAI(message.content),
|
|
2613
|
+
...message.role === "user" && message.name ? { name: message.name } : {}
|
|
2614
|
+
};
|
|
2615
|
+
});
|
|
2616
|
+
}
|
|
2617
|
+
function normalizeOpenAIToolCalls(value) {
|
|
2618
|
+
if (!value)
|
|
2619
|
+
return [];
|
|
2620
|
+
const calls = [];
|
|
2621
|
+
for (const call of value) {
|
|
2622
|
+
if (typeof call?.id !== "string" || typeof call.function?.name !== "string")
|
|
2623
|
+
continue;
|
|
2624
|
+
calls.push({
|
|
2625
|
+
id: call.id,
|
|
2626
|
+
name: call.function.name,
|
|
2627
|
+
argsText: typeof call.function.arguments === "string" ? call.function.arguments : "{}"
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
return calls;
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// src/providers/openai/adapter.ts
|
|
2634
|
+
function createOpenAIAdapter(options) {
|
|
2635
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
2636
|
+
const provider = options.provider ?? "openai-compatible";
|
|
2637
|
+
const defaultModel = { provider, modelId: options.defaultModel };
|
|
2638
|
+
const baseUrl = (options.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "");
|
|
2639
|
+
return {
|
|
2640
|
+
provider,
|
|
2641
|
+
defaultModel,
|
|
2642
|
+
async* stream(request) {
|
|
2643
|
+
const requestId = request.requestId ?? crypto.randomUUID();
|
|
2644
|
+
yield { type: "response_start", requestId, model: request.model };
|
|
2645
|
+
let response;
|
|
2646
|
+
try {
|
|
2647
|
+
response = await fetchImpl(`${baseUrl}/chat/completions`, {
|
|
2648
|
+
method: "POST",
|
|
2649
|
+
headers: {
|
|
2650
|
+
"content-type": "application/json",
|
|
2651
|
+
...options.apiKey ? { authorization: `Bearer ${options.apiKey}` } : {},
|
|
2652
|
+
...options.headers
|
|
2653
|
+
},
|
|
2654
|
+
body: JSON.stringify({
|
|
2655
|
+
model: request.model.modelId,
|
|
2656
|
+
messages: messagesToOpenAI(request.messages),
|
|
2657
|
+
tools: mapToolDefinitionsToOpenAI(request.tools),
|
|
2658
|
+
max_tokens: request.maxOutputTokens,
|
|
2659
|
+
temperature: request.temperature,
|
|
2660
|
+
top_p: request.topP,
|
|
2661
|
+
stop: request.stop,
|
|
2662
|
+
stream: false
|
|
2663
|
+
}),
|
|
2664
|
+
signal: request.signal
|
|
2665
|
+
});
|
|
2666
|
+
} catch (error) {
|
|
2667
|
+
yield { type: "error", requestId, error: toModelErrorPayload3(error), terminal: true };
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
if (!response.ok) {
|
|
2671
|
+
yield {
|
|
2672
|
+
type: "error",
|
|
2673
|
+
requestId,
|
|
2674
|
+
error: await createHttpErrorPayload({
|
|
2675
|
+
code: "openai_http_error",
|
|
2676
|
+
provider: "OpenAI-compatible",
|
|
2677
|
+
endpoint: `${baseUrl}/chat/completions`,
|
|
2678
|
+
response
|
|
2679
|
+
}),
|
|
2680
|
+
terminal: true
|
|
2681
|
+
};
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
const data = await response.json();
|
|
2685
|
+
const choice = data.choices?.[0];
|
|
2686
|
+
const text = choice?.message?.content ?? "";
|
|
2687
|
+
if (text)
|
|
2688
|
+
yield { type: "text_delta", requestId, delta: text };
|
|
2689
|
+
const toolCalls = normalizeOpenAIToolCalls(choice?.message?.tool_calls);
|
|
2690
|
+
for (const toolCall of toolCalls) {
|
|
2691
|
+
yield { type: "tool_call_start", requestId, callId: toolCall.id, toolName: toolCall.name };
|
|
2692
|
+
yield { type: "tool_call_args_delta", requestId, callId: toolCall.id, delta: toolCall.argsText };
|
|
2693
|
+
yield { type: "tool_call_end", requestId, callId: toolCall.id };
|
|
2694
|
+
}
|
|
2695
|
+
yield {
|
|
2696
|
+
type: "response_end",
|
|
2697
|
+
requestId,
|
|
2698
|
+
stopReason: mapOpenAIStopReason(choice?.finish_reason, toolCalls.length),
|
|
2699
|
+
usage: normalizeUsage(data.usage)
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
function toModelErrorPayload3(error) {
|
|
2705
|
+
if (error instanceof Error) {
|
|
2706
|
+
return {
|
|
2707
|
+
code: "openai_request_error",
|
|
2708
|
+
message: error.message
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
return {
|
|
2712
|
+
code: "openai_request_error",
|
|
2713
|
+
message: String(error)
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
// src/providers/openai-responses/mapper.ts
|
|
2717
|
+
var OPENAI_RESPONSES_STATE_KEY = "openaiResponses";
|
|
2718
|
+
function mapToolDefinitionsToOpenAIResponses(tools) {
|
|
2719
|
+
if (!tools || tools.length === 0)
|
|
2720
|
+
return;
|
|
2721
|
+
return tools.map((tool) => ({
|
|
2722
|
+
type: "function",
|
|
2723
|
+
name: tool.name,
|
|
2724
|
+
description: tool.description,
|
|
2725
|
+
parameters: tool.inputSchema
|
|
2726
|
+
}));
|
|
2727
|
+
}
|
|
2728
|
+
function mapOpenAIResponsesStopReason(response, toolCallCount) {
|
|
2729
|
+
if (toolCallCount > 0)
|
|
2730
|
+
return "tool_call";
|
|
2731
|
+
if (response?.status === "cancelled")
|
|
2732
|
+
return "cancelled";
|
|
2733
|
+
if (response?.status === "failed")
|
|
2734
|
+
return "error";
|
|
2735
|
+
if (response?.incomplete_details?.reason === "max_output_tokens")
|
|
2736
|
+
return "length";
|
|
2737
|
+
return "final";
|
|
2738
|
+
}
|
|
2739
|
+
function messagesToOpenAIResponses(messages) {
|
|
2740
|
+
const instructions = messages.filter((message) => message.role === "system").map((message) => contentToText(message.content)).filter(Boolean).join(`
|
|
2741
|
+
|
|
2742
|
+
`) || undefined;
|
|
2743
|
+
let previousResponseId;
|
|
2744
|
+
let startIndex = 0;
|
|
2745
|
+
for (let index = messages.length - 1;index >= 0; index -= 1) {
|
|
2746
|
+
const state = readProviderState(messages[index], OPENAI_RESPONSES_STATE_KEY);
|
|
2747
|
+
if (typeof state?.responseId === "string") {
|
|
2748
|
+
previousResponseId = state.responseId;
|
|
2749
|
+
startIndex = index + 1;
|
|
2750
|
+
break;
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
const input = [];
|
|
2754
|
+
for (const message of messages.slice(startIndex)) {
|
|
2755
|
+
if (message.role === "system")
|
|
2756
|
+
continue;
|
|
2757
|
+
if (message.role === "tool") {
|
|
2758
|
+
input.push({
|
|
2759
|
+
type: "function_call_output",
|
|
2760
|
+
call_id: message.toolCallId,
|
|
2761
|
+
output: JSON.stringify(message.structuredContent ?? {
|
|
2762
|
+
content: contentToText(message.content),
|
|
2763
|
+
isError: message.isError === true
|
|
2764
|
+
})
|
|
2765
|
+
});
|
|
2766
|
+
continue;
|
|
2767
|
+
}
|
|
2768
|
+
if (message.role === "assistant") {
|
|
2769
|
+
if (message.content.length > 0) {
|
|
2770
|
+
input.push({
|
|
2771
|
+
role: "assistant",
|
|
2772
|
+
content: messageContentToOpenAIResponses(message.content)
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
for (const toolCall of message.toolCalls ?? []) {
|
|
2776
|
+
input.push({
|
|
2777
|
+
type: "function_call",
|
|
2778
|
+
call_id: toolCall.id,
|
|
2779
|
+
name: toolCall.function.name,
|
|
2780
|
+
arguments: JSON.stringify(toolCall.function.arguments)
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
continue;
|
|
2784
|
+
}
|
|
2785
|
+
input.push({
|
|
2786
|
+
role: "user",
|
|
2787
|
+
content: messageContentToOpenAIResponses(message.content)
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
return {
|
|
2791
|
+
instructions,
|
|
2792
|
+
input,
|
|
2793
|
+
previousResponseId
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
function normalizeOpenAIResponsesToolCalls(output) {
|
|
2797
|
+
if (!output)
|
|
2798
|
+
return [];
|
|
2799
|
+
const calls = [];
|
|
2800
|
+
for (const item of output) {
|
|
2801
|
+
if (item.type !== "function_call" || typeof item.name !== "string")
|
|
2802
|
+
continue;
|
|
2803
|
+
const callId = typeof item.call_id === "string" && item.call_id.length > 0 ? item.call_id : typeof item.id === "string" && item.id.length > 0 ? item.id : crypto.randomUUID();
|
|
2804
|
+
calls.push({
|
|
2805
|
+
id: callId,
|
|
2806
|
+
name: item.name,
|
|
2807
|
+
argsText: typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments ?? {})
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
return calls;
|
|
2811
|
+
}
|
|
2812
|
+
function extractOpenAIResponsesText(output) {
|
|
2813
|
+
if (!output)
|
|
2814
|
+
return [];
|
|
2815
|
+
const deltas = [];
|
|
2816
|
+
for (const item of output) {
|
|
2817
|
+
if (item.type !== "message")
|
|
2818
|
+
continue;
|
|
2819
|
+
for (const part of item.content ?? []) {
|
|
2820
|
+
if ((part.type === "output_text" || part.type === "text") && typeof part.text === "string" && part.text.length > 0)
|
|
2821
|
+
deltas.push(part.text);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
return deltas;
|
|
2825
|
+
}
|
|
2826
|
+
function extractOpenAIResponsesThinking(output) {
|
|
2827
|
+
if (!output)
|
|
2828
|
+
return [];
|
|
2829
|
+
const deltas = [];
|
|
2830
|
+
for (const item of output) {
|
|
2831
|
+
if (item.type !== "reasoning")
|
|
2832
|
+
continue;
|
|
2833
|
+
for (const part of item.summary ?? []) {
|
|
2834
|
+
if (typeof part.text === "string" && part.text.length > 0)
|
|
2835
|
+
deltas.push(part.text);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
return deltas;
|
|
2839
|
+
}
|
|
2840
|
+
function createOpenAIResponsesState(responseId) {
|
|
2841
|
+
if (!responseId)
|
|
2842
|
+
return;
|
|
2843
|
+
return { responseId };
|
|
2844
|
+
}
|
|
2845
|
+
function messageContentToOpenAIResponses(content) {
|
|
2846
|
+
const hasImage = content.some((block) => block.type === "image");
|
|
2847
|
+
if (!hasImage)
|
|
2848
|
+
return contentToText(content);
|
|
2849
|
+
return content.map((block) => {
|
|
2850
|
+
if (block.type === "text")
|
|
2851
|
+
return { type: "input_text", text: block.text };
|
|
2852
|
+
return {
|
|
2853
|
+
type: "input_image",
|
|
2854
|
+
image_url: imageDataUrl2(block)
|
|
2855
|
+
};
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
function imageDataUrl2(block) {
|
|
2859
|
+
return `data:${block.mimeType};base64,${block.data}`;
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
// src/providers/openai-responses/adapter.ts
|
|
2863
|
+
function createOpenAIResponsesAdapter(options) {
|
|
2864
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
2865
|
+
const provider = options.provider ?? "openai-responses";
|
|
2866
|
+
const defaultModel = { provider, modelId: options.defaultModel };
|
|
2867
|
+
const baseUrl = (options.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "");
|
|
2868
|
+
return {
|
|
2869
|
+
provider,
|
|
2870
|
+
defaultModel,
|
|
2871
|
+
async* stream(request) {
|
|
2872
|
+
const requestId = request.requestId ?? crypto.randomUUID();
|
|
2873
|
+
yield { type: "response_start", requestId, model: request.model };
|
|
2874
|
+
const { instructions, input, previousResponseId } = messagesToOpenAIResponses(request.messages);
|
|
2875
|
+
let response;
|
|
2876
|
+
try {
|
|
2877
|
+
response = await fetchImpl(`${baseUrl}/responses`, {
|
|
2878
|
+
method: "POST",
|
|
2879
|
+
headers: {
|
|
2880
|
+
"content-type": "application/json",
|
|
2881
|
+
...options.apiKey ? { authorization: `Bearer ${options.apiKey}` } : {},
|
|
2882
|
+
...options.headers
|
|
2883
|
+
},
|
|
2884
|
+
body: JSON.stringify({
|
|
2885
|
+
model: request.model.modelId,
|
|
2886
|
+
instructions,
|
|
2887
|
+
input,
|
|
2888
|
+
previous_response_id: previousResponseId,
|
|
2889
|
+
tools: mapToolDefinitionsToOpenAIResponses(request.tools),
|
|
2890
|
+
max_output_tokens: request.maxOutputTokens,
|
|
2891
|
+
temperature: request.temperature,
|
|
2892
|
+
top_p: request.topP,
|
|
2893
|
+
reasoning: buildOpenAIResponsesReasoning(request.reasoning),
|
|
2894
|
+
stream: false
|
|
2895
|
+
}),
|
|
2896
|
+
signal: request.signal
|
|
2897
|
+
});
|
|
2898
|
+
} catch (error) {
|
|
2899
|
+
yield { type: "error", requestId, error: toModelErrorPayload4(error), terminal: true };
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
if (!response.ok) {
|
|
2903
|
+
yield {
|
|
2904
|
+
type: "error",
|
|
2905
|
+
requestId,
|
|
2906
|
+
error: await createHttpErrorPayload({
|
|
2907
|
+
code: "openai_responses_http_error",
|
|
2908
|
+
provider: "OpenAI Responses",
|
|
2909
|
+
endpoint: `${baseUrl}/responses`,
|
|
2910
|
+
response
|
|
2911
|
+
}),
|
|
2912
|
+
terminal: true
|
|
2913
|
+
};
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2916
|
+
const data = await response.json();
|
|
2917
|
+
for (const delta of extractOpenAIResponsesThinking(data.output))
|
|
2918
|
+
yield { type: "thinking_delta", requestId, delta };
|
|
2919
|
+
for (const delta of extractOpenAIResponsesText(data.output))
|
|
2920
|
+
yield { type: "text_delta", requestId, delta };
|
|
2921
|
+
const toolCalls = normalizeOpenAIResponsesToolCalls(data.output);
|
|
2922
|
+
for (const toolCall of toolCalls) {
|
|
2923
|
+
yield { type: "tool_call_start", requestId, callId: toolCall.id, toolName: toolCall.name };
|
|
2924
|
+
yield { type: "tool_call_args_delta", requestId, callId: toolCall.id, delta: toolCall.argsText };
|
|
2925
|
+
yield { type: "tool_call_end", requestId, callId: toolCall.id };
|
|
2926
|
+
}
|
|
2927
|
+
yield {
|
|
2928
|
+
type: "response_end",
|
|
2929
|
+
requestId,
|
|
2930
|
+
stopReason: mapOpenAIResponsesStopReason(data, toolCalls.length),
|
|
2931
|
+
usage: normalizeUsage(data.usage),
|
|
2932
|
+
assistantMetadata: attachProviderState(undefined, "openaiResponses", createOpenAIResponsesState(data.id))
|
|
2933
|
+
};
|
|
2934
|
+
}
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
function toModelErrorPayload4(error) {
|
|
2938
|
+
if (error instanceof Error) {
|
|
2939
|
+
return {
|
|
2940
|
+
code: "openai_responses_request_error",
|
|
2941
|
+
message: error.message
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
return {
|
|
2945
|
+
code: "openai_responses_request_error",
|
|
2946
|
+
message: String(error)
|
|
2947
|
+
};
|
|
2948
|
+
}
|
|
2949
|
+
// src/agent-core/createModel.ts
|
|
2950
|
+
function createModel(adapter) {
|
|
2951
|
+
return {
|
|
2952
|
+
adapter,
|
|
2953
|
+
model: adapter.defaultModel,
|
|
2954
|
+
stream(request) {
|
|
2955
|
+
return adapter.stream({
|
|
2956
|
+
...request,
|
|
2957
|
+
model: request.model ?? adapter.defaultModel
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
export {
|
|
2963
|
+
startToolCall,
|
|
2964
|
+
normalizeToolSchema,
|
|
2965
|
+
normalizeToolResult,
|
|
2966
|
+
normalizePermissions,
|
|
2967
|
+
normalizeContent,
|
|
2968
|
+
fullPermissions,
|
|
2969
|
+
finalizeToolCall,
|
|
2970
|
+
createOpenAIResponsesAdapter,
|
|
2971
|
+
createOpenAIAdapter,
|
|
2972
|
+
createModel,
|
|
2973
|
+
createGeminiAdapter,
|
|
2974
|
+
createAnthropicAdapter,
|
|
2975
|
+
createAgent,
|
|
2976
|
+
appendToolCallArgsDelta,
|
|
2977
|
+
WriteTool,
|
|
2978
|
+
ToolRegistry,
|
|
2979
|
+
SessionExecutionError,
|
|
2980
|
+
Session,
|
|
2981
|
+
ReadTool,
|
|
2982
|
+
PermissionGateway,
|
|
2983
|
+
PermissionDeniedError,
|
|
2984
|
+
NodeFileSystemGateway,
|
|
2985
|
+
NodeExecGateway,
|
|
2986
|
+
InMemoryStateStore,
|
|
2987
|
+
FileStateStore,
|
|
2988
|
+
ExecTool,
|
|
2989
|
+
EditTool,
|
|
2990
|
+
DefaultNetworkGateway,
|
|
2991
|
+
DefaultModelGateway,
|
|
2992
|
+
DefaultGitGateway,
|
|
2993
|
+
BaseTool,
|
|
2994
|
+
AutoContextManager,
|
|
2995
|
+
Agent,
|
|
2996
|
+
ActivityTracker
|
|
2997
|
+
};
|