opencode-gitlab-duo-agentic 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +2368 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2368 @@
|
|
|
1
|
+
// src/plugin/config.ts
|
|
2
|
+
import path2 from "path";
|
|
3
|
+
import fs2 from "fs";
|
|
4
|
+
|
|
5
|
+
// src/plugin/models.ts
|
|
6
|
+
import path from "path";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
|
|
9
|
+
// src/shared/model_entry.ts
|
|
10
|
+
function buildModelEntry(name) {
|
|
11
|
+
return {
|
|
12
|
+
name,
|
|
13
|
+
release_date: "",
|
|
14
|
+
attachment: false,
|
|
15
|
+
reasoning: false,
|
|
16
|
+
temperature: true,
|
|
17
|
+
tool_call: true,
|
|
18
|
+
limit: { context: 0, output: 0 },
|
|
19
|
+
modalities: { input: ["text"], output: ["text"] },
|
|
20
|
+
options: {}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/shared/constants.ts
|
|
25
|
+
var GITLAB_DUO_PROVIDER_ID = "gitlab-duo-agentic";
|
|
26
|
+
var GITLAB_DUO_DEFAULT_MODEL_ID = "duo-agentic";
|
|
27
|
+
var GITLAB_DUO_DEFAULT_MODEL_NAME = "Duo Agentic";
|
|
28
|
+
var GITLAB_DUO_PLUGIN_PACKAGE_NAME = "opencode-gitlab-duo-agentic";
|
|
29
|
+
var GITLAB_DUO_PROVIDER_NPM_ENTRY = GITLAB_DUO_PLUGIN_PACKAGE_NAME;
|
|
30
|
+
|
|
31
|
+
// src/plugin/models.ts
|
|
32
|
+
async function loadGitLabModels(options = {}) {
|
|
33
|
+
const modelsJsonPath = resolveModelsJsonPath(options.modelsPath);
|
|
34
|
+
if (modelsJsonPath) {
|
|
35
|
+
try {
|
|
36
|
+
const raw = await fs.promises.readFile(modelsJsonPath, "utf8");
|
|
37
|
+
const data = JSON.parse(raw);
|
|
38
|
+
if (data.models && Object.keys(data.models).length > 0) {
|
|
39
|
+
console.log(
|
|
40
|
+
`[gitlab-duo] Loaded ${Object.keys(data.models).length} model(s) from ${modelsJsonPath}`
|
|
41
|
+
);
|
|
42
|
+
return data.models;
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.warn(
|
|
46
|
+
`[gitlab-duo] Failed to read models.json at ${modelsJsonPath}:`,
|
|
47
|
+
error instanceof Error ? error.message : error
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
[GITLAB_DUO_DEFAULT_MODEL_ID]: buildModelEntry(GITLAB_DUO_DEFAULT_MODEL_NAME)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function resolveModelsJsonPath(overridePath) {
|
|
56
|
+
const override = typeof overridePath === "string" && overridePath.trim() ? overridePath.trim() : process.env.GITLAB_DUO_MODELS_PATH;
|
|
57
|
+
if (override) {
|
|
58
|
+
const resolved = path.isAbsolute(override) ? override : path.resolve(process.cwd(), override);
|
|
59
|
+
if (fs.existsSync(resolved)) return resolved;
|
|
60
|
+
console.warn(`[gitlab-duo] models.json not found at override path ${resolved}`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
let current = process.cwd();
|
|
64
|
+
while (true) {
|
|
65
|
+
const candidate = path.join(current, "models.json");
|
|
66
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
67
|
+
const parent = path.dirname(current);
|
|
68
|
+
if (parent === current) break;
|
|
69
|
+
current = parent;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/plugin/config.ts
|
|
75
|
+
async function configHook(input) {
|
|
76
|
+
input.provider ??= {};
|
|
77
|
+
const existing = input.provider[GITLAB_DUO_PROVIDER_ID];
|
|
78
|
+
const existingOptions = existing?.options ?? {};
|
|
79
|
+
const providerNpm = typeof existing?.npm === "string" && existing.npm.trim() ? existing.npm : GITLAB_DUO_PROVIDER_NPM_ENTRY;
|
|
80
|
+
const apiKey = typeof existingOptions.apiKey === "string" ? existingOptions.apiKey : process.env.GITLAB_TOKEN || "";
|
|
81
|
+
const instanceUrl = typeof existingOptions.instanceUrl === "string" ? existingOptions.instanceUrl : process.env.GITLAB_INSTANCE_URL || "https://gitlab.com";
|
|
82
|
+
const systemRules = typeof existingOptions.systemRules === "string" ? existingOptions.systemRules : "";
|
|
83
|
+
const systemRulesPath = typeof existingOptions.systemRulesPath === "string" ? existingOptions.systemRulesPath : "";
|
|
84
|
+
const modelsPath = typeof existingOptions.modelsPath === "string" ? existingOptions.modelsPath : void 0;
|
|
85
|
+
const mergedSystemRules = await mergeSystemRules(systemRules, systemRulesPath);
|
|
86
|
+
const sendSystemContext = typeof existingOptions.sendSystemContext === "boolean" ? existingOptions.sendSystemContext : true;
|
|
87
|
+
const enableMcp = typeof existingOptions.enableMcp === "boolean" ? existingOptions.enableMcp : true;
|
|
88
|
+
if (!apiKey) {
|
|
89
|
+
console.warn(
|
|
90
|
+
"[gitlab-duo] GITLAB_TOKEN is empty for the OpenCode process. Ensure it is exported in the same shell."
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
input.provider[GITLAB_DUO_PROVIDER_ID] = {
|
|
94
|
+
name: existing?.name ?? "GitLab Duo Agentic",
|
|
95
|
+
npm: providerNpm,
|
|
96
|
+
options: {
|
|
97
|
+
...existingOptions,
|
|
98
|
+
instanceUrl,
|
|
99
|
+
apiKey,
|
|
100
|
+
sendSystemContext,
|
|
101
|
+
enableMcp,
|
|
102
|
+
systemRules: mergedSystemRules || void 0
|
|
103
|
+
},
|
|
104
|
+
models: await loadGitLabModels({ modelsPath })
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function mergeSystemRules(rules, rulesPath) {
|
|
108
|
+
const baseRules = rules.trim();
|
|
109
|
+
if (!rulesPath) return baseRules;
|
|
110
|
+
const resolvedPath = path2.isAbsolute(rulesPath) ? rulesPath : path2.resolve(process.cwd(), rulesPath);
|
|
111
|
+
try {
|
|
112
|
+
const fileRules = (await fs2.promises.readFile(resolvedPath, "utf8")).trim();
|
|
113
|
+
if (!fileRules) return baseRules;
|
|
114
|
+
return baseRules ? `${baseRules}
|
|
115
|
+
|
|
116
|
+
${fileRules}` : fileRules;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.warn(`[gitlab-duo] Failed to read systemRulesPath at ${resolvedPath}:`, error);
|
|
119
|
+
return baseRules;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/plugin/tools.ts
|
|
124
|
+
import { tool } from "@opencode-ai/plugin";
|
|
125
|
+
import path3 from "path";
|
|
126
|
+
import fs3 from "fs";
|
|
127
|
+
function createReadTools() {
|
|
128
|
+
return {
|
|
129
|
+
read_file: tool({
|
|
130
|
+
description: "Read the contents of a file. Paths are relative to the repository root.",
|
|
131
|
+
args: {
|
|
132
|
+
file_path: tool.schema.string().describe("The file path to read.")
|
|
133
|
+
},
|
|
134
|
+
async execute(args, ctx) {
|
|
135
|
+
const { resolvedPath, displayPath } = resolveReadPath(args.file_path, ctx);
|
|
136
|
+
await ctx.ask({
|
|
137
|
+
permission: "read",
|
|
138
|
+
patterns: [resolvedPath],
|
|
139
|
+
always: ["*"],
|
|
140
|
+
metadata: {}
|
|
141
|
+
});
|
|
142
|
+
try {
|
|
143
|
+
return await fs3.promises.readFile(resolvedPath, "utf8");
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throw new Error(formatReadError(displayPath, error));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}),
|
|
149
|
+
read_files: tool({
|
|
150
|
+
description: "Read the contents of multiple files. Paths are relative to the repository root.",
|
|
151
|
+
args: {
|
|
152
|
+
file_paths: tool.schema.array(tool.schema.string()).describe("The file paths to read.")
|
|
153
|
+
},
|
|
154
|
+
async execute(args, ctx) {
|
|
155
|
+
const targets = (args.file_paths ?? []).map((filePath) => ({
|
|
156
|
+
inputPath: filePath,
|
|
157
|
+
...resolveReadPath(filePath, ctx)
|
|
158
|
+
}));
|
|
159
|
+
await ctx.ask({
|
|
160
|
+
permission: "read",
|
|
161
|
+
patterns: targets.map((target) => target.resolvedPath),
|
|
162
|
+
always: ["*"],
|
|
163
|
+
metadata: {}
|
|
164
|
+
});
|
|
165
|
+
const results = await Promise.all(
|
|
166
|
+
targets.map(async (target) => {
|
|
167
|
+
try {
|
|
168
|
+
const content = await fs3.promises.readFile(target.resolvedPath, "utf8");
|
|
169
|
+
return [target.inputPath, { content }];
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return [target.inputPath, { error: formatReadError(target.displayPath, error) }];
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
const output = {};
|
|
176
|
+
for (const [pathKey, result] of results) {
|
|
177
|
+
output[pathKey] = result;
|
|
178
|
+
}
|
|
179
|
+
return JSON.stringify(output);
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function resolveReadPath(filePath, ctx) {
|
|
185
|
+
const displayPath = filePath;
|
|
186
|
+
const resolvedPath = path3.isAbsolute(filePath) ? filePath : path3.resolve(ctx.worktree, filePath);
|
|
187
|
+
const worktreePath = path3.resolve(ctx.worktree);
|
|
188
|
+
if (resolvedPath !== worktreePath && !resolvedPath.startsWith(worktreePath + path3.sep)) {
|
|
189
|
+
throw new Error(`File is outside the repository: "${displayPath}"`);
|
|
190
|
+
}
|
|
191
|
+
return { resolvedPath, displayPath };
|
|
192
|
+
}
|
|
193
|
+
function formatReadError(filePath, error) {
|
|
194
|
+
const fsError = error;
|
|
195
|
+
if (fsError?.code === "ENOENT") return `File not found: "${filePath}"`;
|
|
196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
+
return `Error reading file: ${message}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/plugin/gitlab-duo-agentic.ts
|
|
201
|
+
var GitLabDuoAgenticPlugin = async () => {
|
|
202
|
+
return {
|
|
203
|
+
config: configHook,
|
|
204
|
+
tool: createReadTools()
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// src/provider/core/stream_adapter.ts
|
|
209
|
+
import { createRequire } from "module";
|
|
210
|
+
function resolveReadableStream() {
|
|
211
|
+
if (typeof ReadableStream !== "undefined") {
|
|
212
|
+
return ReadableStream;
|
|
213
|
+
}
|
|
214
|
+
const require2 = createRequire(import.meta.url);
|
|
215
|
+
const web = require2("node:stream/web");
|
|
216
|
+
return web.ReadableStream;
|
|
217
|
+
}
|
|
218
|
+
function asyncIteratorToReadableStream(iter) {
|
|
219
|
+
const iterator = iter[Symbol.asyncIterator]();
|
|
220
|
+
const Readable = resolveReadableStream();
|
|
221
|
+
return new Readable({
|
|
222
|
+
async pull(controller) {
|
|
223
|
+
try {
|
|
224
|
+
const { value, done } = await iterator.next();
|
|
225
|
+
if (done) {
|
|
226
|
+
controller.close();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
controller.enqueue(value);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
controller.error(error);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
async cancel() {
|
|
235
|
+
if (iterator.return) {
|
|
236
|
+
await iterator.return();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/provider/core/prompt_utils.ts
|
|
243
|
+
function asString(value) {
|
|
244
|
+
return typeof value === "string" ? value : void 0;
|
|
245
|
+
}
|
|
246
|
+
function isPlainObject(value) {
|
|
247
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
248
|
+
}
|
|
249
|
+
function asStringArray(value) {
|
|
250
|
+
if (!Array.isArray(value)) return [];
|
|
251
|
+
return value.filter((item) => typeof item === "string");
|
|
252
|
+
}
|
|
253
|
+
function extractLastUserText(prompt) {
|
|
254
|
+
const parts = getLastUserTextParts(prompt);
|
|
255
|
+
if (parts.length === 0) return null;
|
|
256
|
+
const texts = parts.filter((part) => !part.synthetic && !part.ignored).map((part) => stripSystemReminder(part.text ?? "")).filter((text) => text.trim().length > 0);
|
|
257
|
+
if (texts.length === 0) return null;
|
|
258
|
+
return texts.join("").trim();
|
|
259
|
+
}
|
|
260
|
+
function getLastUserTextParts(prompt) {
|
|
261
|
+
if (!Array.isArray(prompt)) return [];
|
|
262
|
+
for (let i = prompt.length - 1; i >= 0; i -= 1) {
|
|
263
|
+
const message = prompt[i];
|
|
264
|
+
if (message?.role !== "user" || !Array.isArray(message.content)) continue;
|
|
265
|
+
const textParts = message.content.filter((part) => part.type === "text");
|
|
266
|
+
if (textParts.length > 0) return textParts;
|
|
267
|
+
}
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
function stripSystemReminder(text) {
|
|
271
|
+
return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
|
|
272
|
+
}
|
|
273
|
+
function extractAgentReminders(prompt) {
|
|
274
|
+
const parts = getLastUserTextParts(prompt);
|
|
275
|
+
if (parts.length === 0) return [];
|
|
276
|
+
const reminders = [];
|
|
277
|
+
for (const part of parts) {
|
|
278
|
+
if (!part.text) continue;
|
|
279
|
+
if (part.synthetic) {
|
|
280
|
+
const text = part.text.trim();
|
|
281
|
+
if (text.length > 0) {
|
|
282
|
+
reminders.push(text);
|
|
283
|
+
}
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const matches = part.text.match(/<system-reminder>[\s\S]*?<\/system-reminder>/g);
|
|
287
|
+
if (matches) {
|
|
288
|
+
reminders.push(...matches);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return normalizeModeReminders(reminders);
|
|
292
|
+
}
|
|
293
|
+
function normalizeModeReminders(reminders) {
|
|
294
|
+
const mode = detectLatestMode(reminders);
|
|
295
|
+
if (!mode) return reminders;
|
|
296
|
+
return reminders.filter((reminder) => {
|
|
297
|
+
const classification = classifyModeReminder(reminder);
|
|
298
|
+
if (classification === "other") return true;
|
|
299
|
+
return classification === mode;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function detectLatestMode(reminders) {
|
|
303
|
+
let mode = null;
|
|
304
|
+
for (const reminder of reminders) {
|
|
305
|
+
const explicit = /operational mode has changed from\s+([a-z_]+)\s+to\s+([a-z_]+)/i.exec(reminder);
|
|
306
|
+
if (explicit) {
|
|
307
|
+
const normalized = normalizeMode(explicit[2]);
|
|
308
|
+
if (normalized) mode = normalized;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const classification = classifyModeReminder(reminder);
|
|
312
|
+
if (classification !== "other") {
|
|
313
|
+
mode = classification;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return mode;
|
|
317
|
+
}
|
|
318
|
+
function normalizeMode(value) {
|
|
319
|
+
const normalized = value.trim().toLowerCase();
|
|
320
|
+
if (normalized === "plan") return "plan";
|
|
321
|
+
if (normalized === "build") return "build";
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
function classifyModeReminder(reminder) {
|
|
325
|
+
const text = reminder.toLowerCase();
|
|
326
|
+
if (text.includes("operational mode has changed from build to plan")) return "plan";
|
|
327
|
+
if (text.includes("operational mode has changed from plan to build")) return "build";
|
|
328
|
+
if (text.includes("you are no longer in read-only mode")) return "build";
|
|
329
|
+
if (text.includes("you are in read-only mode")) return "plan";
|
|
330
|
+
if (text.includes("your operational mode has changed from plan to build")) return "build";
|
|
331
|
+
if (text.includes("your operational mode has changed from build to plan")) return "plan";
|
|
332
|
+
if (text.includes("you are permitted to make file changes")) return "build";
|
|
333
|
+
return "other";
|
|
334
|
+
}
|
|
335
|
+
function extractSystemPrompt(prompt) {
|
|
336
|
+
if (!Array.isArray(prompt)) return null;
|
|
337
|
+
const parts = [];
|
|
338
|
+
for (const message of prompt) {
|
|
339
|
+
const msg = message;
|
|
340
|
+
if (msg.role === "system" && typeof msg.content === "string" && msg.content.trim()) {
|
|
341
|
+
parts.push(msg.content);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
345
|
+
}
|
|
346
|
+
function sanitizeSystemPrompt(prompt) {
|
|
347
|
+
let result = prompt;
|
|
348
|
+
result = result.replace(/^You are [Oo]pen[Cc]ode[,.].*$/gm, "");
|
|
349
|
+
result = result.replace(/^Your name is opencode\s*$/gm, "");
|
|
350
|
+
result = result.replace(
|
|
351
|
+
/If the user asks for help or wants to give feedback[\s\S]*?https:\/\/github\.com\/anomalyco\/opencode\s*/g,
|
|
352
|
+
""
|
|
353
|
+
);
|
|
354
|
+
result = result.replace(
|
|
355
|
+
/When the user directly asks about OpenCode[\s\S]*?https:\/\/opencode\.ai\/docs\s*/g,
|
|
356
|
+
""
|
|
357
|
+
);
|
|
358
|
+
result = result.replace(/https:\/\/github\.com\/anomalyco\/opencode\S*/g, "");
|
|
359
|
+
result = result.replace(/https:\/\/opencode\.ai\S*/g, "");
|
|
360
|
+
result = result.replace(/\bOpenCode\b/g, "GitLab Duo");
|
|
361
|
+
result = result.replace(/\bopencode\b/g, "GitLab Duo");
|
|
362
|
+
result = result.replace(/The exact model ID is GitLab Duo\//g, "The exact model ID is ");
|
|
363
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
364
|
+
return result.trim();
|
|
365
|
+
}
|
|
366
|
+
function extractToolResults(prompt) {
|
|
367
|
+
if (!Array.isArray(prompt)) return [];
|
|
368
|
+
const results = [];
|
|
369
|
+
for (const message of prompt) {
|
|
370
|
+
const content = message.content;
|
|
371
|
+
if (!Array.isArray(content)) continue;
|
|
372
|
+
for (const part of content) {
|
|
373
|
+
if (part.type === "tool-result") {
|
|
374
|
+
const toolCallId = String(part.toolCallId ?? "");
|
|
375
|
+
const toolName = String(part.toolName ?? "");
|
|
376
|
+
const outputField = part.output;
|
|
377
|
+
const resultField = part.result;
|
|
378
|
+
let output = "";
|
|
379
|
+
let error;
|
|
380
|
+
if (isPlainObject(outputField) && "type" in outputField) {
|
|
381
|
+
const outputType = String(outputField.type);
|
|
382
|
+
const outputValue = outputField.value;
|
|
383
|
+
if (outputType === "text" || outputType === "json") {
|
|
384
|
+
output = typeof outputValue === "string" ? outputValue : JSON.stringify(outputValue ?? "");
|
|
385
|
+
} else if (outputType === "error-text" || outputType === "error-json") {
|
|
386
|
+
error = typeof outputValue === "string" ? outputValue : JSON.stringify(outputValue ?? "");
|
|
387
|
+
} else if (outputType === "content" && Array.isArray(outputValue)) {
|
|
388
|
+
output = outputValue.filter((v) => v.type === "text").map((v) => String(v.text ?? "")).join("\n");
|
|
389
|
+
}
|
|
390
|
+
} else if (outputField !== void 0) {
|
|
391
|
+
output = String(outputField);
|
|
392
|
+
} else if (resultField !== void 0) {
|
|
393
|
+
output = typeof resultField === "string" ? resultField : JSON.stringify(resultField);
|
|
394
|
+
if (isPlainObject(resultField)) error = asString(resultField.error);
|
|
395
|
+
}
|
|
396
|
+
if (!error) {
|
|
397
|
+
error = asString(part.error) ?? asString(part.errorText);
|
|
398
|
+
}
|
|
399
|
+
results.push({ toolCallId, toolName, output, error });
|
|
400
|
+
}
|
|
401
|
+
if (part.type === "tool-error") {
|
|
402
|
+
const toolCallId = String(part.toolCallId ?? "");
|
|
403
|
+
const toolName = String(part.toolName ?? "");
|
|
404
|
+
const errorValue = part.error ?? part.errorText ?? part.message;
|
|
405
|
+
const error = asString(errorValue) ?? String(errorValue ?? "");
|
|
406
|
+
results.push({ toolCallId, toolName, output: "", error });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return results;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/provider/core/shell_quote.ts
|
|
414
|
+
function shellQuote(value) {
|
|
415
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/provider/application/tool_mapping.ts
|
|
419
|
+
function mapDuoToolRequest(toolName, args) {
|
|
420
|
+
switch (toolName) {
|
|
421
|
+
case "list_dir": {
|
|
422
|
+
const directory = asString(args.directory) ?? ".";
|
|
423
|
+
return {
|
|
424
|
+
toolName: "bash",
|
|
425
|
+
args: {
|
|
426
|
+
command: `ls -la ${shellQuote(directory)}`,
|
|
427
|
+
description: "List directory contents",
|
|
428
|
+
workdir: "."
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
case "read_file": {
|
|
433
|
+
const filePath = asString(args.file_path) ?? asString(args.filepath) ?? asString(args.filePath) ?? asString(args.path);
|
|
434
|
+
if (!filePath) return { toolName, args };
|
|
435
|
+
const mappedArgs = { filePath };
|
|
436
|
+
if (typeof args.offset === "number") mappedArgs.offset = args.offset;
|
|
437
|
+
if (typeof args.limit === "number") mappedArgs.limit = args.limit;
|
|
438
|
+
return { toolName: "read", args: mappedArgs };
|
|
439
|
+
}
|
|
440
|
+
case "read_files": {
|
|
441
|
+
const filePaths = asStringArray(args.file_paths);
|
|
442
|
+
if (filePaths.length === 0) return { toolName, args };
|
|
443
|
+
return filePaths.map((fp) => ({ toolName: "read", args: { filePath: fp } }));
|
|
444
|
+
}
|
|
445
|
+
case "create_file_with_contents": {
|
|
446
|
+
const filePath = asString(args.file_path);
|
|
447
|
+
const content = asString(args.contents);
|
|
448
|
+
if (!filePath || content === void 0) return { toolName, args };
|
|
449
|
+
return { toolName: "write", args: { filePath, content } };
|
|
450
|
+
}
|
|
451
|
+
case "edit_file": {
|
|
452
|
+
const filePath = asString(args.file_path);
|
|
453
|
+
const oldString = asString(args.old_str);
|
|
454
|
+
const newString = asString(args.new_str);
|
|
455
|
+
if (!filePath || oldString === void 0 || newString === void 0) return { toolName, args };
|
|
456
|
+
return { toolName: "edit", args: { filePath, oldString, newString } };
|
|
457
|
+
}
|
|
458
|
+
case "find_files": {
|
|
459
|
+
const pattern = asString(args.name_pattern);
|
|
460
|
+
if (!pattern) return { toolName, args };
|
|
461
|
+
return { toolName: "glob", args: { pattern } };
|
|
462
|
+
}
|
|
463
|
+
case "grep": {
|
|
464
|
+
const pattern = asString(args.pattern);
|
|
465
|
+
if (!pattern) return { toolName, args };
|
|
466
|
+
const searchDirectory = asString(args.search_directory);
|
|
467
|
+
const caseInsensitive = Boolean(args.case_insensitive);
|
|
468
|
+
const normalizedPattern = caseInsensitive && !pattern.startsWith("(?i)") ? `(?i)${pattern}` : pattern;
|
|
469
|
+
const mappedArgs = { pattern: normalizedPattern };
|
|
470
|
+
if (searchDirectory) mappedArgs.path = searchDirectory;
|
|
471
|
+
return { toolName: "grep", args: mappedArgs };
|
|
472
|
+
}
|
|
473
|
+
case "mkdir": {
|
|
474
|
+
const directory = asString(args.directory_path);
|
|
475
|
+
if (!directory) return { toolName, args };
|
|
476
|
+
return {
|
|
477
|
+
toolName: "bash",
|
|
478
|
+
args: {
|
|
479
|
+
command: `mkdir -p ${shellQuote(directory)}`,
|
|
480
|
+
description: "Create directory",
|
|
481
|
+
workdir: "."
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
case "shell_command": {
|
|
486
|
+
const command = asString(args.command);
|
|
487
|
+
if (!command) return { toolName, args };
|
|
488
|
+
return {
|
|
489
|
+
toolName: "bash",
|
|
490
|
+
args: { command, description: "Run shell command", workdir: "." }
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
case "run_command": {
|
|
494
|
+
const program = asString(args.program);
|
|
495
|
+
if (program) {
|
|
496
|
+
const parts = [shellQuote(program)];
|
|
497
|
+
const flags = args.flags;
|
|
498
|
+
if (Array.isArray(flags)) parts.push(...flags.map((f) => shellQuote(String(f))));
|
|
499
|
+
const cmdArgs = args.arguments;
|
|
500
|
+
if (Array.isArray(cmdArgs)) parts.push(...cmdArgs.map((a) => shellQuote(String(a))));
|
|
501
|
+
return {
|
|
502
|
+
toolName: "bash",
|
|
503
|
+
args: { command: parts.join(" "), description: "Run command", workdir: "." }
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
const command = asString(args.command);
|
|
507
|
+
if (!command) return { toolName, args };
|
|
508
|
+
return {
|
|
509
|
+
toolName: "bash",
|
|
510
|
+
args: { command, description: "Run command", workdir: "." }
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
case "run_git_command": {
|
|
514
|
+
const command = asString(args.command);
|
|
515
|
+
if (!command) return { toolName, args };
|
|
516
|
+
const rawArgs = args.args;
|
|
517
|
+
const extraArgs = Array.isArray(rawArgs) ? rawArgs.map((value) => shellQuote(String(value))).join(" ") : asString(rawArgs);
|
|
518
|
+
const gitCommand = extraArgs ? `git ${shellQuote(command)} ${extraArgs}` : `git ${shellQuote(command)}`;
|
|
519
|
+
return {
|
|
520
|
+
toolName: "bash",
|
|
521
|
+
args: { command: gitCommand, description: "Run git command", workdir: "." }
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
default:
|
|
525
|
+
return { toolName, args };
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
var DUO_MCP_TOOLS = [
|
|
529
|
+
{
|
|
530
|
+
name: "list_dir",
|
|
531
|
+
description: "List directory contents relative to the repository root.",
|
|
532
|
+
schema: {
|
|
533
|
+
type: "object",
|
|
534
|
+
properties: {
|
|
535
|
+
directory: { type: "string", description: "Directory path relative to repo root." }
|
|
536
|
+
},
|
|
537
|
+
required: ["directory"]
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "read_file",
|
|
542
|
+
description: "Read the contents of a file.",
|
|
543
|
+
schema: {
|
|
544
|
+
type: "object",
|
|
545
|
+
properties: {
|
|
546
|
+
file_path: { type: "string", description: "The file path to read." }
|
|
547
|
+
},
|
|
548
|
+
required: ["file_path"]
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
name: "read_files",
|
|
553
|
+
description: "Read multiple files.",
|
|
554
|
+
schema: {
|
|
555
|
+
type: "object",
|
|
556
|
+
properties: {
|
|
557
|
+
file_paths: {
|
|
558
|
+
type: "array",
|
|
559
|
+
items: { type: "string" },
|
|
560
|
+
description: "List of file paths to read."
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
required: ["file_paths"]
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: "create_file_with_contents",
|
|
568
|
+
description: "Create a file and write contents.",
|
|
569
|
+
schema: {
|
|
570
|
+
type: "object",
|
|
571
|
+
properties: {
|
|
572
|
+
file_path: { type: "string", description: "The file path to write." },
|
|
573
|
+
contents: { type: "string", description: "Contents to write." }
|
|
574
|
+
},
|
|
575
|
+
required: ["file_path", "contents"]
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: "find_files",
|
|
580
|
+
description: "Find files by name pattern.",
|
|
581
|
+
schema: {
|
|
582
|
+
type: "object",
|
|
583
|
+
properties: {
|
|
584
|
+
name_pattern: { type: "string", description: "Pattern to search for." }
|
|
585
|
+
},
|
|
586
|
+
required: ["name_pattern"]
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
name: "mkdir",
|
|
591
|
+
description: "Create a directory.",
|
|
592
|
+
schema: {
|
|
593
|
+
type: "object",
|
|
594
|
+
properties: {
|
|
595
|
+
directory_path: { type: "string", description: "Directory to create." }
|
|
596
|
+
},
|
|
597
|
+
required: ["directory_path"]
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
name: "edit_file",
|
|
602
|
+
description: "Edit a file by replacing a string.",
|
|
603
|
+
schema: {
|
|
604
|
+
type: "object",
|
|
605
|
+
properties: {
|
|
606
|
+
file_path: { type: "string", description: "Path of the file to edit." },
|
|
607
|
+
old_str: { type: "string", description: "String to replace." },
|
|
608
|
+
new_str: { type: "string", description: "Replacement string." }
|
|
609
|
+
},
|
|
610
|
+
required: ["file_path", "old_str", "new_str"]
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
name: "grep",
|
|
615
|
+
description: "Search for a pattern in files.",
|
|
616
|
+
schema: {
|
|
617
|
+
type: "object",
|
|
618
|
+
properties: {
|
|
619
|
+
pattern: { type: "string", description: "Search pattern." },
|
|
620
|
+
search_directory: { type: "string", description: "Directory to search." },
|
|
621
|
+
case_insensitive: { type: "boolean", description: "Case insensitive search." }
|
|
622
|
+
},
|
|
623
|
+
required: ["pattern"]
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
name: "shell_command",
|
|
628
|
+
description: "Execute a shell command.",
|
|
629
|
+
schema: {
|
|
630
|
+
type: "object",
|
|
631
|
+
properties: {
|
|
632
|
+
command: { type: "string", description: "Command to execute." }
|
|
633
|
+
},
|
|
634
|
+
required: ["command"]
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
name: "run_git_command",
|
|
639
|
+
description: "Run a git command in the repo.",
|
|
640
|
+
schema: {
|
|
641
|
+
type: "object",
|
|
642
|
+
properties: {
|
|
643
|
+
repository_url: { type: "string", description: "Git remote URL." },
|
|
644
|
+
command: { type: "string", description: "Git command (status, log, diff, ...)." },
|
|
645
|
+
args: { type: "string", description: "Arguments for the git command." }
|
|
646
|
+
},
|
|
647
|
+
required: ["repository_url", "command"]
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
];
|
|
651
|
+
var BUILTIN_TOOL_NAMES = new Set(DUO_MCP_TOOLS.map((t) => t.name));
|
|
652
|
+
var OPENCODE_BUILTIN_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
653
|
+
"bash",
|
|
654
|
+
"edit",
|
|
655
|
+
"write",
|
|
656
|
+
"read",
|
|
657
|
+
"grep",
|
|
658
|
+
"glob",
|
|
659
|
+
"patch",
|
|
660
|
+
"skill",
|
|
661
|
+
"todowrite",
|
|
662
|
+
"todoread",
|
|
663
|
+
"webfetch",
|
|
664
|
+
"websearch",
|
|
665
|
+
"question",
|
|
666
|
+
"lsp",
|
|
667
|
+
"read_file",
|
|
668
|
+
"read_files"
|
|
669
|
+
]);
|
|
670
|
+
function buildMcpTools(options) {
|
|
671
|
+
const tools = DUO_MCP_TOOLS.map((t) => ({
|
|
672
|
+
name: t.name,
|
|
673
|
+
description: t.description,
|
|
674
|
+
schema: t.schema,
|
|
675
|
+
isApproved: false
|
|
676
|
+
}));
|
|
677
|
+
if (options.tools) {
|
|
678
|
+
for (const t of options.tools) {
|
|
679
|
+
if (t.type !== "function") continue;
|
|
680
|
+
if (BUILTIN_TOOL_NAMES.has(t.name)) continue;
|
|
681
|
+
if (OPENCODE_BUILTIN_TOOL_NAMES.has(t.name)) continue;
|
|
682
|
+
tools.push({
|
|
683
|
+
name: t.name,
|
|
684
|
+
description: t.description,
|
|
685
|
+
schema: t.inputSchema,
|
|
686
|
+
isApproved: false
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return tools;
|
|
691
|
+
}
|
|
692
|
+
function buildToolContext(tools) {
|
|
693
|
+
if (tools.length === 0) return null;
|
|
694
|
+
const content = `<tools>
|
|
695
|
+
${tools.map((t) => {
|
|
696
|
+
const desc = t.description?.trim();
|
|
697
|
+
return desc ? `- ${t.name}: ${desc}` : `- ${t.name}`;
|
|
698
|
+
}).join("\n")}
|
|
699
|
+
</tools>
|
|
700
|
+
<rules>
|
|
701
|
+
- MUST use the tool-call simulation formats when requesting tools.
|
|
702
|
+
</rules>`;
|
|
703
|
+
return {
|
|
704
|
+
category: "tool_information",
|
|
705
|
+
content,
|
|
706
|
+
id: "available_tools",
|
|
707
|
+
metadata: {
|
|
708
|
+
title: "Available Tools",
|
|
709
|
+
enabled: true,
|
|
710
|
+
subType: "tools",
|
|
711
|
+
icon: "tool",
|
|
712
|
+
secondaryText: `${tools.length} tools`,
|
|
713
|
+
subTypeLabel: "Tooling"
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// src/provider/core/token_usage.ts
|
|
719
|
+
var DEFAULT_CHARS_PER_TOKEN = 4;
|
|
720
|
+
var TokenUsageEstimator = class {
|
|
721
|
+
#inputChars = 0;
|
|
722
|
+
#outputChars = 0;
|
|
723
|
+
#charsPerToken;
|
|
724
|
+
constructor(charsPerToken = DEFAULT_CHARS_PER_TOKEN) {
|
|
725
|
+
this.#charsPerToken = charsPerToken;
|
|
726
|
+
}
|
|
727
|
+
/** Record characters sent to DWS (prompt, system context, tool results). */
|
|
728
|
+
addInputChars(text) {
|
|
729
|
+
this.#inputChars += text.length;
|
|
730
|
+
}
|
|
731
|
+
/** Record characters received from DWS (text chunks, tool call args). */
|
|
732
|
+
addOutputChars(text) {
|
|
733
|
+
this.#outputChars += text.length;
|
|
734
|
+
}
|
|
735
|
+
get inputTokens() {
|
|
736
|
+
return Math.ceil(this.#inputChars / this.#charsPerToken);
|
|
737
|
+
}
|
|
738
|
+
get outputTokens() {
|
|
739
|
+
return Math.ceil(this.#outputChars / this.#charsPerToken);
|
|
740
|
+
}
|
|
741
|
+
get totalTokens() {
|
|
742
|
+
return this.inputTokens + this.outputTokens;
|
|
743
|
+
}
|
|
744
|
+
/** Reset counters for a new turn. */
|
|
745
|
+
reset() {
|
|
746
|
+
this.#inputChars = 0;
|
|
747
|
+
this.#outputChars = 0;
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
// src/provider/application/model.ts
|
|
752
|
+
var GitLabDuoAgenticLanguageModel = class {
|
|
753
|
+
specificationVersion = "v2";
|
|
754
|
+
provider = GITLAB_DUO_PROVIDER_ID;
|
|
755
|
+
modelId;
|
|
756
|
+
supportedUrls = {};
|
|
757
|
+
#options;
|
|
758
|
+
#runtime;
|
|
759
|
+
#pendingToolRequests = /* @__PURE__ */ new Map();
|
|
760
|
+
#multiCallGroups = /* @__PURE__ */ new Map();
|
|
761
|
+
#sentToolCallIds = /* @__PURE__ */ new Set();
|
|
762
|
+
#lastSentPrompt = null;
|
|
763
|
+
#agentMode;
|
|
764
|
+
#agentModeReminder;
|
|
765
|
+
#usageEstimator = new TokenUsageEstimator();
|
|
766
|
+
constructor(modelId, options, runtime) {
|
|
767
|
+
this.modelId = modelId;
|
|
768
|
+
this.#options = options;
|
|
769
|
+
this.#runtime = runtime;
|
|
770
|
+
}
|
|
771
|
+
// ---------------------------------------------------------------------------
|
|
772
|
+
// LanguageModelV2 interface
|
|
773
|
+
// ---------------------------------------------------------------------------
|
|
774
|
+
async doGenerate(options) {
|
|
775
|
+
let text = "";
|
|
776
|
+
const stream = await this.doStream(options);
|
|
777
|
+
for await (const part of stream.stream) {
|
|
778
|
+
if (part.type === "text-delta") text += part.delta;
|
|
779
|
+
}
|
|
780
|
+
const content = [{ type: "text", text }];
|
|
781
|
+
const finishReason = "stop";
|
|
782
|
+
return {
|
|
783
|
+
content,
|
|
784
|
+
finishReason,
|
|
785
|
+
usage: {
|
|
786
|
+
inputTokens: this.#usageEstimator.inputTokens,
|
|
787
|
+
outputTokens: this.#usageEstimator.outputTokens,
|
|
788
|
+
totalTokens: this.#usageEstimator.totalTokens
|
|
789
|
+
},
|
|
790
|
+
warnings: []
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
async doStream(options) {
|
|
794
|
+
const workflowType = "chat";
|
|
795
|
+
const promptText = extractLastUserText(options.prompt);
|
|
796
|
+
const toolResults = extractToolResults(options.prompt);
|
|
797
|
+
this.#runtime.resetMapperState();
|
|
798
|
+
if (!this.#runtime.hasStarted) {
|
|
799
|
+
this.#sentToolCallIds.clear();
|
|
800
|
+
for (const r of toolResults) {
|
|
801
|
+
if (!this.#pendingToolRequests.has(r.toolCallId)) {
|
|
802
|
+
this.#sentToolCallIds.add(r.toolCallId);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
this.#lastSentPrompt = null;
|
|
806
|
+
}
|
|
807
|
+
const freshToolResults = toolResults.filter((r) => !this.#sentToolCallIds.has(r.toolCallId));
|
|
808
|
+
const modelRef = this.modelId === GITLAB_DUO_DEFAULT_MODEL_ID ? void 0 : this.modelId;
|
|
809
|
+
this.#runtime.setSelectedModelIdentifier(modelRef);
|
|
810
|
+
await this.#runtime.ensureConnected(promptText || "", workflowType);
|
|
811
|
+
const mcpTools = this.#options.enableMcp === false ? [] : buildMcpTools(options);
|
|
812
|
+
const toolContext = buildToolContext(mcpTools);
|
|
813
|
+
const isNewUserMessage = promptText != null && promptText !== this.#lastSentPrompt;
|
|
814
|
+
let sentToolResults = false;
|
|
815
|
+
if (freshToolResults.length > 0) {
|
|
816
|
+
for (const result of freshToolResults) {
|
|
817
|
+
const hashIdx = result.toolCallId.indexOf("#");
|
|
818
|
+
if (hashIdx !== -1) {
|
|
819
|
+
const originalId = result.toolCallId.substring(0, hashIdx);
|
|
820
|
+
const group = this.#multiCallGroups.get(originalId);
|
|
821
|
+
if (!group) {
|
|
822
|
+
this.#sentToolCallIds.add(result.toolCallId);
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
this.#usageEstimator.addInputChars(result.output);
|
|
826
|
+
if (result.error) this.#usageEstimator.addInputChars(result.error);
|
|
827
|
+
group.collected.set(result.toolCallId, result.error ?? result.output);
|
|
828
|
+
this.#sentToolCallIds.add(result.toolCallId);
|
|
829
|
+
this.#pendingToolRequests.delete(result.toolCallId);
|
|
830
|
+
if (group.collected.size === group.subIds.length) {
|
|
831
|
+
const aggregated = group.subIds.map((id) => group.collected.get(id) ?? "").join("\n");
|
|
832
|
+
this.#runtime.sendToolResponse(
|
|
833
|
+
originalId,
|
|
834
|
+
{ output: aggregated },
|
|
835
|
+
group.responseType
|
|
836
|
+
);
|
|
837
|
+
this.#multiCallGroups.delete(originalId);
|
|
838
|
+
this.#pendingToolRequests.delete(originalId);
|
|
839
|
+
sentToolResults = true;
|
|
840
|
+
}
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
const pending = this.#pendingToolRequests.get(result.toolCallId);
|
|
844
|
+
if (!pending) {
|
|
845
|
+
this.#sentToolCallIds.add(result.toolCallId);
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
this.#usageEstimator.addInputChars(result.output);
|
|
849
|
+
if (result.error) this.#usageEstimator.addInputChars(result.error);
|
|
850
|
+
this.#runtime.sendToolResponse(
|
|
851
|
+
result.toolCallId,
|
|
852
|
+
{ output: result.output, error: result.error },
|
|
853
|
+
pending.responseType
|
|
854
|
+
);
|
|
855
|
+
sentToolResults = true;
|
|
856
|
+
this.#sentToolCallIds.add(result.toolCallId);
|
|
857
|
+
this.#pendingToolRequests.delete(result.toolCallId);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (!sentToolResults && isNewUserMessage) {
|
|
861
|
+
const extraContext = [];
|
|
862
|
+
if (toolContext) extraContext.push(toolContext);
|
|
863
|
+
if (!this.#runtime.hasStarted) {
|
|
864
|
+
const systemPrompt = extractSystemPrompt(options.prompt);
|
|
865
|
+
if (systemPrompt) {
|
|
866
|
+
extraContext.push({
|
|
867
|
+
category: "agent_context",
|
|
868
|
+
content: sanitizeSystemPrompt(systemPrompt),
|
|
869
|
+
id: "agent_system_prompt",
|
|
870
|
+
metadata: {
|
|
871
|
+
title: "Agent System Prompt",
|
|
872
|
+
enabled: true,
|
|
873
|
+
subType: "system_prompt",
|
|
874
|
+
icon: "file-text",
|
|
875
|
+
secondaryText: "Full system prompt",
|
|
876
|
+
subTypeLabel: "System Prompt"
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
} else {
|
|
881
|
+
const promptContent = extractSystemPrompt(options.prompt);
|
|
882
|
+
if (promptContent) {
|
|
883
|
+
extraContext.push({
|
|
884
|
+
category: "agent_context",
|
|
885
|
+
content: sanitizeSystemPrompt(promptContent),
|
|
886
|
+
id: "agent_system_prompt",
|
|
887
|
+
metadata: {
|
|
888
|
+
title: "Agent System Prompt",
|
|
889
|
+
enabled: true,
|
|
890
|
+
subType: "system_prompt",
|
|
891
|
+
icon: "file-text",
|
|
892
|
+
secondaryText: "System prompt",
|
|
893
|
+
subTypeLabel: "System Prompt"
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const agentReminders = extractAgentReminders(options.prompt);
|
|
899
|
+
const modeReminder = detectLatestModeReminder(agentReminders);
|
|
900
|
+
if (modeReminder) {
|
|
901
|
+
this.#agentMode = modeReminder.mode;
|
|
902
|
+
this.#agentModeReminder = modeReminder.reminder;
|
|
903
|
+
}
|
|
904
|
+
const remindersForContext = buildReminderContext(agentReminders, this.#agentModeReminder);
|
|
905
|
+
if (remindersForContext.length > 0) {
|
|
906
|
+
const reminderContent = sanitizeSystemPrompt(remindersForContext.join("\n\n"));
|
|
907
|
+
extraContext.push({
|
|
908
|
+
category: "agent_context",
|
|
909
|
+
content: reminderContent,
|
|
910
|
+
id: "agent_reminders",
|
|
911
|
+
metadata: {
|
|
912
|
+
title: "Agent Reminders",
|
|
913
|
+
enabled: true,
|
|
914
|
+
subType: "agent_reminders",
|
|
915
|
+
icon: "file-text",
|
|
916
|
+
secondaryText: "Agent mode instructions",
|
|
917
|
+
subTypeLabel: "Agent Reminders"
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
this.#runtime.sendStartRequest(
|
|
922
|
+
promptText,
|
|
923
|
+
workflowType,
|
|
924
|
+
mcpTools,
|
|
925
|
+
[],
|
|
926
|
+
extraContext
|
|
927
|
+
);
|
|
928
|
+
this.#lastSentPrompt = promptText;
|
|
929
|
+
this.#usageEstimator.addInputChars(promptText);
|
|
930
|
+
for (const ctx of extraContext) {
|
|
931
|
+
if (ctx.content) this.#usageEstimator.addInputChars(ctx.content);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
const iterator = this.#mapEventsToStream(this.#runtime.getEventStream());
|
|
935
|
+
const stream = asyncIteratorToReadableStream(iterator);
|
|
936
|
+
return {
|
|
937
|
+
stream
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
// ---------------------------------------------------------------------------
|
|
941
|
+
// Event → stream mapping (2 paths: TEXT_CHUNK + TOOL_REQUEST)
|
|
942
|
+
// ---------------------------------------------------------------------------
|
|
943
|
+
async *#mapEventsToStream(events) {
|
|
944
|
+
const state = { textStarted: false };
|
|
945
|
+
const estimator = this.#usageEstimator;
|
|
946
|
+
yield { type: "stream-start", warnings: [] };
|
|
947
|
+
try {
|
|
948
|
+
for await (const event of events) {
|
|
949
|
+
if (event.type === "TEXT_CHUNK") {
|
|
950
|
+
if (event.content.length > 0) {
|
|
951
|
+
estimator.addOutputChars(event.content);
|
|
952
|
+
yield* this.#emitTextDelta(state, event.content);
|
|
953
|
+
}
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
if (event.type === "TOOL_COMPLETE") {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
if (event.type === "TOOL_REQUEST") {
|
|
960
|
+
const args = event.args;
|
|
961
|
+
let mapped;
|
|
962
|
+
try {
|
|
963
|
+
mapped = mapDuoToolRequest(event.toolName, args);
|
|
964
|
+
} catch {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const responseType = event.responseType;
|
|
968
|
+
estimator.addOutputChars(JSON.stringify(args));
|
|
969
|
+
if (Array.isArray(mapped)) {
|
|
970
|
+
const subIds = mapped.map((_, i) => `${event.requestId}#${i}`);
|
|
971
|
+
this.#multiCallGroups.set(event.requestId, { subIds, collected: /* @__PURE__ */ new Map(), responseType });
|
|
972
|
+
this.#pendingToolRequests.set(event.requestId, { responseType });
|
|
973
|
+
for (const subId of subIds) {
|
|
974
|
+
this.#pendingToolRequests.set(subId, {});
|
|
975
|
+
}
|
|
976
|
+
yield* this.#emitMultiToolCalls(subIds, mapped);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
this.#pendingToolRequests.set(event.requestId, { responseType });
|
|
980
|
+
yield* this.#emitToolCall(event.requestId, mapped.toolName, mapped.args);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (event.type === "ERROR") {
|
|
984
|
+
const msg = event.message;
|
|
985
|
+
if (msg.includes("1013") || msg.includes("lock")) {
|
|
986
|
+
yield { type: "error", error: new Error("GitLab Duo workflow is locked (another session may still be active). Please try again in a few seconds.") };
|
|
987
|
+
} else {
|
|
988
|
+
yield { type: "error", error: new Error(`GitLab Duo: ${msg}`) };
|
|
989
|
+
}
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
} catch (streamErr) {
|
|
994
|
+
yield { type: "error", error: streamErr instanceof Error ? streamErr : new Error(String(streamErr)) };
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
yield { type: "finish", finishReason: "stop", usage: this.#currentUsage };
|
|
998
|
+
}
|
|
999
|
+
// ---------------------------------------------------------------------------
|
|
1000
|
+
// Stream part helpers
|
|
1001
|
+
// ---------------------------------------------------------------------------
|
|
1002
|
+
get #currentUsage() {
|
|
1003
|
+
return {
|
|
1004
|
+
inputTokens: this.#usageEstimator.inputTokens,
|
|
1005
|
+
outputTokens: this.#usageEstimator.outputTokens,
|
|
1006
|
+
totalTokens: this.#usageEstimator.totalTokens
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
*#emitTextDelta(state, delta) {
|
|
1010
|
+
if (!state.textStarted) {
|
|
1011
|
+
state.textStarted = true;
|
|
1012
|
+
yield { type: "text-start", id: "txt-0" };
|
|
1013
|
+
}
|
|
1014
|
+
yield { type: "text-delta", id: "txt-0", delta };
|
|
1015
|
+
}
|
|
1016
|
+
*#emitToolCall(id, toolName, args) {
|
|
1017
|
+
const inputJson = JSON.stringify(args ?? {});
|
|
1018
|
+
yield { type: "tool-input-start", id, toolName };
|
|
1019
|
+
yield { type: "tool-input-delta", id, delta: inputJson };
|
|
1020
|
+
yield { type: "tool-input-end", id };
|
|
1021
|
+
yield { type: "tool-call", toolCallId: id, toolName, input: inputJson };
|
|
1022
|
+
yield { type: "finish", finishReason: "tool-calls", usage: this.#currentUsage };
|
|
1023
|
+
}
|
|
1024
|
+
*#emitMultiToolCalls(ids, calls) {
|
|
1025
|
+
for (let i = 0; i < calls.length; i++) {
|
|
1026
|
+
const inputJson = JSON.stringify(calls[i].args ?? {});
|
|
1027
|
+
yield { type: "tool-input-start", id: ids[i], toolName: calls[i].toolName };
|
|
1028
|
+
yield { type: "tool-input-delta", id: ids[i], delta: inputJson };
|
|
1029
|
+
yield { type: "tool-input-end", id: ids[i] };
|
|
1030
|
+
yield { type: "tool-call", toolCallId: ids[i], toolName: calls[i].toolName, input: inputJson };
|
|
1031
|
+
}
|
|
1032
|
+
yield { type: "finish", finishReason: "tool-calls", usage: this.#currentUsage };
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
function buildReminderContext(reminders, modeReminder) {
|
|
1036
|
+
const nonModeReminders = reminders.filter((reminder) => classifyModeReminder2(reminder) === "other");
|
|
1037
|
+
if (!modeReminder) {
|
|
1038
|
+
return nonModeReminders;
|
|
1039
|
+
}
|
|
1040
|
+
return [...nonModeReminders, modeReminder];
|
|
1041
|
+
}
|
|
1042
|
+
function detectLatestModeReminder(reminders) {
|
|
1043
|
+
let latest;
|
|
1044
|
+
for (const reminder of reminders) {
|
|
1045
|
+
const classification = classifyModeReminder2(reminder);
|
|
1046
|
+
if (classification === "other") continue;
|
|
1047
|
+
latest = { mode: classification, reminder };
|
|
1048
|
+
}
|
|
1049
|
+
return latest;
|
|
1050
|
+
}
|
|
1051
|
+
function classifyModeReminder2(reminder) {
|
|
1052
|
+
const text = reminder.toLowerCase();
|
|
1053
|
+
if (text.includes("operational mode has changed from build to plan")) return "plan";
|
|
1054
|
+
if (text.includes("operational mode has changed from plan to build")) return "build";
|
|
1055
|
+
if (text.includes("you are no longer in read-only mode")) return "build";
|
|
1056
|
+
if (text.includes("you are now in read-only mode")) return "plan";
|
|
1057
|
+
if (text.includes("you are in read-only mode")) return "plan";
|
|
1058
|
+
if (text.includes("you are permitted to make file changes")) return "build";
|
|
1059
|
+
return "other";
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/provider/application/workflow_event_mapper.ts
|
|
1063
|
+
import crypto from "crypto";
|
|
1064
|
+
|
|
1065
|
+
// src/provider/core/ui_chat_log.ts
|
|
1066
|
+
import { z } from "zod";
|
|
1067
|
+
import { err, ok } from "neverthrow";
|
|
1068
|
+
var ToolInfoArgsSchema = z.record(z.unknown());
|
|
1069
|
+
var ToolResponseSchema = z.object({
|
|
1070
|
+
content: z.string(),
|
|
1071
|
+
additional_kwargs: z.record(z.unknown()),
|
|
1072
|
+
response_metadata: z.record(z.unknown()),
|
|
1073
|
+
type: z.string(),
|
|
1074
|
+
name: z.string(),
|
|
1075
|
+
id: z.string().nullable(),
|
|
1076
|
+
tool_call_id: z.string(),
|
|
1077
|
+
artifact: z.unknown(),
|
|
1078
|
+
status: z.string()
|
|
1079
|
+
});
|
|
1080
|
+
var ToolInfoSchema = z.object({
|
|
1081
|
+
name: z.string(),
|
|
1082
|
+
args: ToolInfoArgsSchema,
|
|
1083
|
+
tool_response: z.union([ToolResponseSchema, z.string()]).optional()
|
|
1084
|
+
});
|
|
1085
|
+
var BaseMessageSchema = z.object({
|
|
1086
|
+
message_sub_type: z.string().nullable(),
|
|
1087
|
+
content: z.string(),
|
|
1088
|
+
timestamp: z.string(),
|
|
1089
|
+
status: z.string().nullable(),
|
|
1090
|
+
correlation_id: z.string().nullable(),
|
|
1091
|
+
additional_context: z.unknown()
|
|
1092
|
+
});
|
|
1093
|
+
var WorkflowMessageSchema = BaseMessageSchema.extend({
|
|
1094
|
+
message_type: z.enum(["user", "agent"]),
|
|
1095
|
+
tool_info: z.null()
|
|
1096
|
+
});
|
|
1097
|
+
var WorkflowRequestSchema = BaseMessageSchema.extend({
|
|
1098
|
+
message_type: z.literal("request"),
|
|
1099
|
+
tool_info: ToolInfoSchema
|
|
1100
|
+
});
|
|
1101
|
+
var WorkflowToolSchema = BaseMessageSchema.extend({
|
|
1102
|
+
message_type: z.literal("tool"),
|
|
1103
|
+
tool_info: z.union([ToolInfoSchema, z.null()])
|
|
1104
|
+
});
|
|
1105
|
+
var ChatLogSchema = z.discriminatedUnion("message_type", [
|
|
1106
|
+
WorkflowMessageSchema,
|
|
1107
|
+
WorkflowRequestSchema,
|
|
1108
|
+
WorkflowToolSchema
|
|
1109
|
+
]);
|
|
1110
|
+
function extractUiChatLog(message) {
|
|
1111
|
+
if (!message.checkpoint) return ok([]);
|
|
1112
|
+
let checkpoint;
|
|
1113
|
+
try {
|
|
1114
|
+
checkpoint = JSON.parse(message.checkpoint);
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
1117
|
+
return err(
|
|
1118
|
+
new Error(`Failed to parse workflow checkpoint. Checkpoint: ${message.checkpoint}. Cause: ${cause}`)
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
if (!checkpoint.channel_values?.ui_chat_log || !Array.isArray(checkpoint.channel_values.ui_chat_log)) {
|
|
1122
|
+
return ok([]);
|
|
1123
|
+
}
|
|
1124
|
+
const validatedMessages = [];
|
|
1125
|
+
for (let i = 0; i < checkpoint.channel_values.ui_chat_log.length; i += 1) {
|
|
1126
|
+
const rawMessage = checkpoint.channel_values.ui_chat_log[i];
|
|
1127
|
+
const parseResult = ChatLogSchema.safeParse(rawMessage);
|
|
1128
|
+
if (!parseResult.success) {
|
|
1129
|
+
return err(
|
|
1130
|
+
new Error(
|
|
1131
|
+
`Failed to validate message at index ${i}: ${parseResult.error.message}. Raw message: ${JSON.stringify(
|
|
1132
|
+
rawMessage
|
|
1133
|
+
)}`
|
|
1134
|
+
)
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
validatedMessages.push(parseResult.data);
|
|
1138
|
+
}
|
|
1139
|
+
return ok(validatedMessages);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// src/provider/application/workflow_event_mapper.ts
|
|
1143
|
+
var WorkflowEventMapper = class {
|
|
1144
|
+
#lastMessageContent = "";
|
|
1145
|
+
#lastMessageId = "";
|
|
1146
|
+
resetStreamState() {
|
|
1147
|
+
this.#lastMessageContent = "";
|
|
1148
|
+
this.#lastMessageId = "";
|
|
1149
|
+
}
|
|
1150
|
+
#parseTimestamp(timestamp) {
|
|
1151
|
+
const parsed = Date.parse(timestamp);
|
|
1152
|
+
return Number.isNaN(parsed) ? Date.now() : parsed;
|
|
1153
|
+
}
|
|
1154
|
+
mapWorkflowEvent(duoEvent) {
|
|
1155
|
+
const events = [];
|
|
1156
|
+
const workflowMessagesResult = extractUiChatLog(duoEvent);
|
|
1157
|
+
if (workflowMessagesResult.isErr()) {
|
|
1158
|
+
return events;
|
|
1159
|
+
}
|
|
1160
|
+
const workflowMessages = workflowMessagesResult.value;
|
|
1161
|
+
if (workflowMessages.length === 0) return events;
|
|
1162
|
+
const latestMessage = workflowMessages[workflowMessages.length - 1];
|
|
1163
|
+
const latestMessageIndex = workflowMessages.length - 1;
|
|
1164
|
+
switch (latestMessage.message_type) {
|
|
1165
|
+
case "user":
|
|
1166
|
+
return events;
|
|
1167
|
+
case "agent": {
|
|
1168
|
+
const currentContent = latestMessage.content;
|
|
1169
|
+
const currentId = `${latestMessageIndex}`;
|
|
1170
|
+
const timestamp = this.#parseTimestamp(latestMessage.timestamp);
|
|
1171
|
+
if (currentId === this.#lastMessageId) {
|
|
1172
|
+
if (!currentContent.startsWith(this.#lastMessageContent)) {
|
|
1173
|
+
events.push({
|
|
1174
|
+
type: "TEXT_CHUNK",
|
|
1175
|
+
messageId: currentId,
|
|
1176
|
+
content: currentContent,
|
|
1177
|
+
timestamp
|
|
1178
|
+
});
|
|
1179
|
+
this.#lastMessageContent = currentContent;
|
|
1180
|
+
}
|
|
1181
|
+
const delta = currentContent.slice(this.#lastMessageContent.length);
|
|
1182
|
+
if (delta.length > 0) {
|
|
1183
|
+
events.push({
|
|
1184
|
+
type: "TEXT_CHUNK",
|
|
1185
|
+
messageId: currentId,
|
|
1186
|
+
content: delta,
|
|
1187
|
+
timestamp
|
|
1188
|
+
});
|
|
1189
|
+
this.#lastMessageContent = currentContent;
|
|
1190
|
+
}
|
|
1191
|
+
} else {
|
|
1192
|
+
events.push({
|
|
1193
|
+
type: "TEXT_CHUNK",
|
|
1194
|
+
messageId: currentId,
|
|
1195
|
+
content: currentContent,
|
|
1196
|
+
timestamp
|
|
1197
|
+
});
|
|
1198
|
+
this.#lastMessageContent = currentContent;
|
|
1199
|
+
this.#lastMessageId = currentId;
|
|
1200
|
+
}
|
|
1201
|
+
break;
|
|
1202
|
+
}
|
|
1203
|
+
case "request": {
|
|
1204
|
+
const requestId = latestMessage.correlation_id || crypto.randomUUID();
|
|
1205
|
+
events.push({
|
|
1206
|
+
type: "TOOL_REQUEST",
|
|
1207
|
+
requestId,
|
|
1208
|
+
toolName: latestMessage.tool_info.name,
|
|
1209
|
+
args: latestMessage.tool_info.args ?? {},
|
|
1210
|
+
timestamp: this.#parseTimestamp(latestMessage.timestamp)
|
|
1211
|
+
});
|
|
1212
|
+
break;
|
|
1213
|
+
}
|
|
1214
|
+
case "tool": {
|
|
1215
|
+
const toolId = `${latestMessageIndex}`;
|
|
1216
|
+
const timestamp = this.#parseTimestamp(latestMessage.timestamp);
|
|
1217
|
+
const toolResponse = latestMessage.tool_info?.tool_response;
|
|
1218
|
+
const output = typeof toolResponse === "string" ? toolResponse : toolResponse?.content ?? latestMessage.content;
|
|
1219
|
+
if (output.startsWith("Action error:")) {
|
|
1220
|
+
events.push({
|
|
1221
|
+
type: "TOOL_COMPLETE",
|
|
1222
|
+
toolId,
|
|
1223
|
+
result: "",
|
|
1224
|
+
error: output,
|
|
1225
|
+
timestamp
|
|
1226
|
+
});
|
|
1227
|
+
} else {
|
|
1228
|
+
events.push({
|
|
1229
|
+
type: "TOOL_COMPLETE",
|
|
1230
|
+
toolId,
|
|
1231
|
+
result: output,
|
|
1232
|
+
timestamp
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1237
|
+
default:
|
|
1238
|
+
break;
|
|
1239
|
+
}
|
|
1240
|
+
return events;
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
// src/provider/core/async_queue.ts
|
|
1245
|
+
var AsyncQueue = class {
|
|
1246
|
+
#items = [];
|
|
1247
|
+
#resolvers = [];
|
|
1248
|
+
#closed = false;
|
|
1249
|
+
push(item) {
|
|
1250
|
+
if (this.#closed) return;
|
|
1251
|
+
const resolver = this.#resolvers.shift();
|
|
1252
|
+
if (resolver) {
|
|
1253
|
+
resolver({ value: item, done: false });
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
this.#items.push(item);
|
|
1257
|
+
}
|
|
1258
|
+
close() {
|
|
1259
|
+
this.#closed = true;
|
|
1260
|
+
while (this.#resolvers.length > 0) {
|
|
1261
|
+
const resolver = this.#resolvers.shift();
|
|
1262
|
+
if (resolver) resolver({ value: void 0, done: true });
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
async *iterate() {
|
|
1266
|
+
while (true) {
|
|
1267
|
+
if (this.#items.length > 0) {
|
|
1268
|
+
yield this.#items.shift();
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
if (this.#closed) return;
|
|
1272
|
+
const next = await new Promise((resolve) => {
|
|
1273
|
+
this.#resolvers.push(resolve);
|
|
1274
|
+
});
|
|
1275
|
+
if (next.done) return;
|
|
1276
|
+
yield next.value;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
// src/provider/application/action_handler.ts
|
|
1282
|
+
function mapWorkflowActionToToolRequest(action) {
|
|
1283
|
+
const requestId = action.requestID;
|
|
1284
|
+
if (!requestId) return null;
|
|
1285
|
+
if (action.runMCPTool) {
|
|
1286
|
+
const rawArgs = action.runMCPTool.args;
|
|
1287
|
+
let parsedArgs;
|
|
1288
|
+
if (typeof rawArgs === "string") {
|
|
1289
|
+
try {
|
|
1290
|
+
parsedArgs = JSON.parse(rawArgs);
|
|
1291
|
+
} catch {
|
|
1292
|
+
parsedArgs = {};
|
|
1293
|
+
}
|
|
1294
|
+
} else {
|
|
1295
|
+
parsedArgs = rawArgs ?? {};
|
|
1296
|
+
}
|
|
1297
|
+
return { requestId, toolName: action.runMCPTool.name, args: parsedArgs };
|
|
1298
|
+
}
|
|
1299
|
+
if (action.runReadFile) {
|
|
1300
|
+
return {
|
|
1301
|
+
requestId,
|
|
1302
|
+
toolName: "read_file",
|
|
1303
|
+
args: {
|
|
1304
|
+
file_path: action.runReadFile.filepath,
|
|
1305
|
+
offset: action.runReadFile.offset,
|
|
1306
|
+
limit: action.runReadFile.limit
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
if (action.runReadFiles) {
|
|
1311
|
+
return {
|
|
1312
|
+
requestId,
|
|
1313
|
+
toolName: "read_files",
|
|
1314
|
+
args: { file_paths: action.runReadFiles.filepaths ?? [] }
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
if (action.runWriteFile) {
|
|
1318
|
+
return {
|
|
1319
|
+
requestId,
|
|
1320
|
+
toolName: "create_file_with_contents",
|
|
1321
|
+
args: {
|
|
1322
|
+
file_path: action.runWriteFile.filepath,
|
|
1323
|
+
contents: action.runWriteFile.contents
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
if (action.runEditFile) {
|
|
1328
|
+
return {
|
|
1329
|
+
requestId,
|
|
1330
|
+
toolName: "edit_file",
|
|
1331
|
+
args: {
|
|
1332
|
+
file_path: action.runEditFile.filepath,
|
|
1333
|
+
old_str: action.runEditFile.oldString,
|
|
1334
|
+
new_str: action.runEditFile.newString
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
if (action.findFiles) {
|
|
1339
|
+
return {
|
|
1340
|
+
requestId,
|
|
1341
|
+
toolName: "find_files",
|
|
1342
|
+
args: { name_pattern: action.findFiles.name_pattern }
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
if (action.listDirectory) {
|
|
1346
|
+
return {
|
|
1347
|
+
requestId,
|
|
1348
|
+
toolName: "list_dir",
|
|
1349
|
+
args: { directory: action.listDirectory.directory }
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
if (action.grep) {
|
|
1353
|
+
const args = { pattern: action.grep.pattern };
|
|
1354
|
+
if (action.grep.search_directory) args.search_directory = action.grep.search_directory;
|
|
1355
|
+
if (action.grep.case_insensitive !== void 0) args.case_insensitive = action.grep.case_insensitive;
|
|
1356
|
+
return { requestId, toolName: "grep", args };
|
|
1357
|
+
}
|
|
1358
|
+
if (action.mkdir) {
|
|
1359
|
+
return {
|
|
1360
|
+
requestId,
|
|
1361
|
+
toolName: "mkdir",
|
|
1362
|
+
args: { directory_path: action.mkdir.directory_path }
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
if (action.runShellCommand) {
|
|
1366
|
+
return {
|
|
1367
|
+
requestId,
|
|
1368
|
+
toolName: "shell_command",
|
|
1369
|
+
args: { command: action.runShellCommand.command }
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
if (action.runCommand) {
|
|
1373
|
+
const parts = [shellQuote(action.runCommand.program)];
|
|
1374
|
+
if (action.runCommand.flags) parts.push(...action.runCommand.flags.map(shellQuote));
|
|
1375
|
+
if (action.runCommand.arguments) parts.push(...action.runCommand.arguments.map(shellQuote));
|
|
1376
|
+
return {
|
|
1377
|
+
requestId,
|
|
1378
|
+
toolName: "shell_command",
|
|
1379
|
+
args: { command: parts.join(" ") }
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
if (action.runGitCommand) {
|
|
1383
|
+
return {
|
|
1384
|
+
requestId,
|
|
1385
|
+
toolName: "run_git_command",
|
|
1386
|
+
args: {
|
|
1387
|
+
repository_url: action.runGitCommand.repository_url ?? "",
|
|
1388
|
+
command: action.runGitCommand.command,
|
|
1389
|
+
args: action.runGitCommand.arguments
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
if (action.runHTTPRequest) {
|
|
1394
|
+
return {
|
|
1395
|
+
requestId,
|
|
1396
|
+
toolName: "gitlab_api_request",
|
|
1397
|
+
args: {
|
|
1398
|
+
method: action.runHTTPRequest.method,
|
|
1399
|
+
path: action.runHTTPRequest.path,
|
|
1400
|
+
body: action.runHTTPRequest.body
|
|
1401
|
+
},
|
|
1402
|
+
responseType: "http"
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/provider/application/runtime.ts
|
|
1409
|
+
var GitLabAgenticRuntime = class {
|
|
1410
|
+
#options;
|
|
1411
|
+
#dependencies;
|
|
1412
|
+
#selectedModelIdentifier;
|
|
1413
|
+
#workflowId;
|
|
1414
|
+
#wsClient;
|
|
1415
|
+
#workflowToken;
|
|
1416
|
+
#queue;
|
|
1417
|
+
#stream;
|
|
1418
|
+
#mapper = new WorkflowEventMapper();
|
|
1419
|
+
#containerParams;
|
|
1420
|
+
#startRequestSent = false;
|
|
1421
|
+
constructor(options, dependencies) {
|
|
1422
|
+
this.#options = options;
|
|
1423
|
+
this.#dependencies = dependencies;
|
|
1424
|
+
}
|
|
1425
|
+
// ---------------------------------------------------------------------------
|
|
1426
|
+
// Public accessors
|
|
1427
|
+
// ---------------------------------------------------------------------------
|
|
1428
|
+
get hasStarted() {
|
|
1429
|
+
return this.#startRequestSent;
|
|
1430
|
+
}
|
|
1431
|
+
setSelectedModelIdentifier(ref) {
|
|
1432
|
+
if (ref === this.#selectedModelIdentifier) return;
|
|
1433
|
+
this.#selectedModelIdentifier = ref;
|
|
1434
|
+
this.#resetStreamState();
|
|
1435
|
+
}
|
|
1436
|
+
resetMapperState() {
|
|
1437
|
+
this.#mapper.resetStreamState();
|
|
1438
|
+
}
|
|
1439
|
+
// ---------------------------------------------------------------------------
|
|
1440
|
+
// Connection lifecycle
|
|
1441
|
+
// ---------------------------------------------------------------------------
|
|
1442
|
+
async ensureConnected(goal, workflowType) {
|
|
1443
|
+
if (this.#stream && this.#workflowId && this.#queue) {
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
if (!this.#containerParams) {
|
|
1447
|
+
this.#containerParams = await this.#resolveContainerParams();
|
|
1448
|
+
}
|
|
1449
|
+
if (!this.#workflowId) {
|
|
1450
|
+
this.#workflowId = await this.#createWorkflow(goal, workflowType);
|
|
1451
|
+
}
|
|
1452
|
+
const token = await this.#dependencies.workflowService.getWorkflowToken(
|
|
1453
|
+
this.#options.instanceUrl,
|
|
1454
|
+
this.#options.apiKey,
|
|
1455
|
+
workflowType
|
|
1456
|
+
);
|
|
1457
|
+
this.#workflowToken = token;
|
|
1458
|
+
const MAX_LOCK_RETRIES = 3;
|
|
1459
|
+
const LOCK_RETRY_DELAY_MS = 3e3;
|
|
1460
|
+
for (let attempt = 1; attempt <= MAX_LOCK_RETRIES; attempt++) {
|
|
1461
|
+
this.#queue = new AsyncQueue();
|
|
1462
|
+
try {
|
|
1463
|
+
await this.#connectWebSocket();
|
|
1464
|
+
return;
|
|
1465
|
+
} catch (err2) {
|
|
1466
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
1467
|
+
if ((msg.includes("1013") || msg.includes("lock")) && attempt < MAX_LOCK_RETRIES) {
|
|
1468
|
+
this.#resetStreamState();
|
|
1469
|
+
await this.#dependencies.clock.sleep(LOCK_RETRY_DELAY_MS);
|
|
1470
|
+
const retryToken = await this.#dependencies.workflowService.getWorkflowToken(
|
|
1471
|
+
this.#options.instanceUrl,
|
|
1472
|
+
this.#options.apiKey,
|
|
1473
|
+
workflowType
|
|
1474
|
+
);
|
|
1475
|
+
this.#workflowToken = retryToken;
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
if (msg.includes("1013") || msg.includes("lock")) {
|
|
1479
|
+
throw new Error("GitLab Duo workflow is locked (another session may still be active). Please try again in a few seconds.");
|
|
1480
|
+
}
|
|
1481
|
+
throw new Error(`GitLab Duo connection failed: ${msg}`);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
// ---------------------------------------------------------------------------
|
|
1486
|
+
// Messaging
|
|
1487
|
+
// ---------------------------------------------------------------------------
|
|
1488
|
+
sendStartRequest(goal, workflowType, mcpTools = [], preapprovedTools = [], extraContext = []) {
|
|
1489
|
+
if (!this.#stream || !this.#workflowId) throw new Error("Workflow client not initialized");
|
|
1490
|
+
const additionalContext = this.#options.sendSystemContext === false ? [] : this.#dependencies.systemContext.getSystemContextItems(this.#options.systemRules);
|
|
1491
|
+
additionalContext.push(...extraContext);
|
|
1492
|
+
const startRequest = {
|
|
1493
|
+
startRequest: {
|
|
1494
|
+
workflowID: this.#workflowId,
|
|
1495
|
+
clientVersion: "1.0",
|
|
1496
|
+
workflowDefinition: workflowType,
|
|
1497
|
+
goal,
|
|
1498
|
+
workflowMetadata: JSON.stringify({
|
|
1499
|
+
project_id: this.#containerParams?.projectId,
|
|
1500
|
+
namespace_id: this.#containerParams?.namespaceId
|
|
1501
|
+
}),
|
|
1502
|
+
additional_context: additionalContext.map((context) => ({
|
|
1503
|
+
...context,
|
|
1504
|
+
metadata: context.metadata ? JSON.stringify(context.metadata) : void 0
|
|
1505
|
+
})),
|
|
1506
|
+
clientCapabilities: ["shell_command"],
|
|
1507
|
+
mcpTools,
|
|
1508
|
+
preapproved_tools: preapprovedTools
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
this.#stream.write(startRequest);
|
|
1512
|
+
this.#startRequestSent = true;
|
|
1513
|
+
}
|
|
1514
|
+
sendToolResponse(requestId, response, responseType) {
|
|
1515
|
+
if (!this.#stream) throw new Error("Workflow client not initialized");
|
|
1516
|
+
if (responseType === "http") {
|
|
1517
|
+
const parsed = parseHttpToolOutput(response.output);
|
|
1518
|
+
const event2 = {
|
|
1519
|
+
actionResponse: {
|
|
1520
|
+
requestID: requestId,
|
|
1521
|
+
httpResponse: {
|
|
1522
|
+
status: parsed.status,
|
|
1523
|
+
headers: parsed.headers,
|
|
1524
|
+
response: parsed.body,
|
|
1525
|
+
error: response.error ?? ""
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
this.#stream.write(event2);
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
const event = {
|
|
1533
|
+
actionResponse: {
|
|
1534
|
+
requestID: requestId,
|
|
1535
|
+
plainTextResponse: {
|
|
1536
|
+
response: response.output,
|
|
1537
|
+
error: response.error ?? ""
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
this.#stream.write(event);
|
|
1542
|
+
}
|
|
1543
|
+
getEventStream() {
|
|
1544
|
+
if (!this.#queue) throw new Error("Workflow stream not initialized");
|
|
1545
|
+
return this.#queue.iterate();
|
|
1546
|
+
}
|
|
1547
|
+
// ---------------------------------------------------------------------------
|
|
1548
|
+
// Private: project / workflow resolution
|
|
1549
|
+
// ---------------------------------------------------------------------------
|
|
1550
|
+
async #resolveContainerParams() {
|
|
1551
|
+
const projectPath = await this.#dependencies.projectLookup.detectProjectPath(
|
|
1552
|
+
process.cwd(),
|
|
1553
|
+
this.#options.instanceUrl
|
|
1554
|
+
);
|
|
1555
|
+
if (!projectPath) {
|
|
1556
|
+
throw new Error(
|
|
1557
|
+
"Unable to detect GitLab project. Ensure you run OpenCode in a Git repository with a GitLab remote."
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
try {
|
|
1561
|
+
const details = await this.#dependencies.projectLookup.fetchProjectDetailsWithFallback(
|
|
1562
|
+
this.#options.instanceUrl,
|
|
1563
|
+
this.#options.apiKey,
|
|
1564
|
+
projectPath
|
|
1565
|
+
);
|
|
1566
|
+
return {
|
|
1567
|
+
projectId: details.projectId,
|
|
1568
|
+
namespaceId: details.namespaceId
|
|
1569
|
+
};
|
|
1570
|
+
} catch {
|
|
1571
|
+
throw new Error(
|
|
1572
|
+
"Failed to fetch GitLab project details. Check that the remote URL is correct and the token has access."
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
async #createWorkflow(goal, workflowType) {
|
|
1577
|
+
try {
|
|
1578
|
+
return await this.#dependencies.workflowService.createWorkflow(
|
|
1579
|
+
this.#options.instanceUrl,
|
|
1580
|
+
this.#options.apiKey,
|
|
1581
|
+
goal,
|
|
1582
|
+
workflowType,
|
|
1583
|
+
this.#containerParams
|
|
1584
|
+
);
|
|
1585
|
+
} catch (error) {
|
|
1586
|
+
if (isWorkflowCreateError(error) && error.status === 400 && error.body.includes("No default namespace found")) {
|
|
1587
|
+
throw new Error(
|
|
1588
|
+
"No default namespace found. Ensure this repository has a GitLab remote so the namespace can be detected."
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
throw error;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
// ---------------------------------------------------------------------------
|
|
1595
|
+
// Private: WebSocket stream binding
|
|
1596
|
+
// ---------------------------------------------------------------------------
|
|
1597
|
+
#bindStream(stream, queue) {
|
|
1598
|
+
const now = () => this.#dependencies.clock.now();
|
|
1599
|
+
const closeWithError = (message) => {
|
|
1600
|
+
queue.push({ type: "ERROR", message, timestamp: now() });
|
|
1601
|
+
queue.close();
|
|
1602
|
+
this.#resetStreamState();
|
|
1603
|
+
};
|
|
1604
|
+
const handleAction = async (action) => {
|
|
1605
|
+
if (action.newCheckpoint) {
|
|
1606
|
+
const duoEvent = {
|
|
1607
|
+
checkpoint: action.newCheckpoint.checkpoint,
|
|
1608
|
+
errors: action.newCheckpoint.errors || [],
|
|
1609
|
+
workflowGoal: action.newCheckpoint.goal,
|
|
1610
|
+
workflowStatus: action.newCheckpoint.status
|
|
1611
|
+
};
|
|
1612
|
+
const events = await this.#mapper.mapWorkflowEvent(duoEvent);
|
|
1613
|
+
for (const event of events) {
|
|
1614
|
+
queue.push(event);
|
|
1615
|
+
}
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
const toolRequest = mapWorkflowActionToToolRequest(action);
|
|
1619
|
+
if (toolRequest) {
|
|
1620
|
+
queue.push({
|
|
1621
|
+
type: "TOOL_REQUEST",
|
|
1622
|
+
...toolRequest,
|
|
1623
|
+
timestamp: now()
|
|
1624
|
+
});
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
stream.on("data", (action) => {
|
|
1629
|
+
void handleAction(action).catch((error) => {
|
|
1630
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1631
|
+
closeWithError(message);
|
|
1632
|
+
});
|
|
1633
|
+
});
|
|
1634
|
+
stream.on("error", (err2) => {
|
|
1635
|
+
closeWithError(err2.message);
|
|
1636
|
+
});
|
|
1637
|
+
stream.on("end", () => {
|
|
1638
|
+
queue.close();
|
|
1639
|
+
this.#resetStreamState();
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
async #connectWebSocket() {
|
|
1643
|
+
if (!this.#queue) return;
|
|
1644
|
+
if (!this.#workflowToken) throw new Error("Workflow token unavailable");
|
|
1645
|
+
this.#wsClient = this.#dependencies.createWorkflowClient({
|
|
1646
|
+
gitlabInstanceUrl: new URL(this.#options.instanceUrl),
|
|
1647
|
+
token: this.#options.apiKey,
|
|
1648
|
+
headers: buildWorkflowHeaders(
|
|
1649
|
+
this.#workflowToken.duo_workflow_service.headers,
|
|
1650
|
+
this.#containerParams
|
|
1651
|
+
),
|
|
1652
|
+
selectedModelIdentifier: this.#selectedModelIdentifier
|
|
1653
|
+
});
|
|
1654
|
+
const stream = await this.#wsClient.executeWorkflow();
|
|
1655
|
+
this.#stream = stream;
|
|
1656
|
+
this.#bindStream(stream, this.#queue);
|
|
1657
|
+
}
|
|
1658
|
+
#resetStreamState() {
|
|
1659
|
+
this.#stream = void 0;
|
|
1660
|
+
this.#queue = void 0;
|
|
1661
|
+
this.#startRequestSent = false;
|
|
1662
|
+
this.#wsClient?.dispose();
|
|
1663
|
+
this.#wsClient = void 0;
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
function buildWorkflowHeaders(headers, containerParams) {
|
|
1667
|
+
const result = normalizeHeaders(headers);
|
|
1668
|
+
if (containerParams?.projectId) {
|
|
1669
|
+
result["x-gitlab-project-id"] = containerParams.projectId;
|
|
1670
|
+
}
|
|
1671
|
+
if (containerParams?.namespaceId) {
|
|
1672
|
+
result["x-gitlab-namespace-id"] = containerParams.namespaceId;
|
|
1673
|
+
}
|
|
1674
|
+
const featureSetting = process.env.GITLAB_AGENT_PLATFORM_FEATURE_SETTING_NAME;
|
|
1675
|
+
if (featureSetting) {
|
|
1676
|
+
result["x-gitlab-agent-platform-feature-setting-name"] = featureSetting;
|
|
1677
|
+
}
|
|
1678
|
+
return result;
|
|
1679
|
+
}
|
|
1680
|
+
function normalizeHeaders(headers) {
|
|
1681
|
+
const normalized = {};
|
|
1682
|
+
for (const [key, value] of Object.entries(headers || {})) {
|
|
1683
|
+
normalized[key.toLowerCase()] = value;
|
|
1684
|
+
}
|
|
1685
|
+
return normalized;
|
|
1686
|
+
}
|
|
1687
|
+
function isWorkflowCreateError(error) {
|
|
1688
|
+
if (!error || typeof error !== "object") return false;
|
|
1689
|
+
const value = error;
|
|
1690
|
+
return typeof value.status === "number" && typeof value.body === "string";
|
|
1691
|
+
}
|
|
1692
|
+
function parseHttpToolOutput(output) {
|
|
1693
|
+
const lines = output.trimEnd().split("\n");
|
|
1694
|
+
const lastLine = lines[lines.length - 1]?.trim() ?? "";
|
|
1695
|
+
const statusCode = parseInt(lastLine, 10);
|
|
1696
|
+
if (!Number.isNaN(statusCode) && statusCode >= 100 && statusCode < 600) {
|
|
1697
|
+
return {
|
|
1698
|
+
status: statusCode,
|
|
1699
|
+
headers: {},
|
|
1700
|
+
body: lines.slice(0, -1).join("\n")
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
return { status: 0, headers: {}, body: output };
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// src/provider/adapters/default_runtime_dependencies.ts
|
|
1707
|
+
import { ProxyAgent } from "proxy-agent";
|
|
1708
|
+
|
|
1709
|
+
// src/provider/adapters/gitlab_utils.ts
|
|
1710
|
+
import fs4 from "fs/promises";
|
|
1711
|
+
import path4 from "path";
|
|
1712
|
+
async function detectProjectPath(cwd, instanceUrl) {
|
|
1713
|
+
let current = cwd;
|
|
1714
|
+
const instance = new URL(instanceUrl);
|
|
1715
|
+
const instanceHost = instance.host;
|
|
1716
|
+
const instanceBasePath = instance.pathname.replace(/\/$/, "");
|
|
1717
|
+
while (true) {
|
|
1718
|
+
try {
|
|
1719
|
+
const config = await readGitConfig(current);
|
|
1720
|
+
const url = extractGitRemoteUrl(config) || "";
|
|
1721
|
+
const remote = parseRemote(url);
|
|
1722
|
+
if (!remote) {
|
|
1723
|
+
return void 0;
|
|
1724
|
+
}
|
|
1725
|
+
if (remote.host !== instanceHost) {
|
|
1726
|
+
throw new Error(
|
|
1727
|
+
`GitLab remote host mismatch. Expected ${instanceHost}, got ${remote.host}.`
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
return normalizeProjectPath(remote.path, instanceBasePath);
|
|
1731
|
+
} catch {
|
|
1732
|
+
const parent = path4.dirname(current);
|
|
1733
|
+
if (parent === current) return void 0;
|
|
1734
|
+
current = parent;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
function extractGitRemoteUrl(config) {
|
|
1739
|
+
const lines = config.split("\n");
|
|
1740
|
+
let inOrigin = false;
|
|
1741
|
+
let originUrl;
|
|
1742
|
+
let firstUrl;
|
|
1743
|
+
for (const line of lines) {
|
|
1744
|
+
const trimmed = line.trim();
|
|
1745
|
+
const sectionMatch = /^\[remote\s+"([^"]+)"\]$/.exec(trimmed);
|
|
1746
|
+
if (sectionMatch) {
|
|
1747
|
+
inOrigin = sectionMatch[1] === "origin";
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
const urlMatch = /^url\s*=\s*(.+)$/.exec(trimmed);
|
|
1751
|
+
if (urlMatch) {
|
|
1752
|
+
const value = urlMatch[1].trim();
|
|
1753
|
+
if (!firstUrl) firstUrl = value;
|
|
1754
|
+
if (inOrigin) originUrl = value;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
return originUrl ?? firstUrl;
|
|
1758
|
+
}
|
|
1759
|
+
function parseRemote(remoteUrl) {
|
|
1760
|
+
if (!remoteUrl) return void 0;
|
|
1761
|
+
if (remoteUrl.startsWith("http")) {
|
|
1762
|
+
try {
|
|
1763
|
+
const url = new URL(remoteUrl);
|
|
1764
|
+
return { host: url.host, path: url.pathname.replace(/^\//, "") };
|
|
1765
|
+
} catch {
|
|
1766
|
+
return void 0;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
if (remoteUrl.startsWith("git@")) {
|
|
1770
|
+
const match = /^git@([^:]+):(.+)$/.exec(remoteUrl);
|
|
1771
|
+
if (!match) return void 0;
|
|
1772
|
+
return { host: match[1], path: match[2] };
|
|
1773
|
+
}
|
|
1774
|
+
if (remoteUrl.startsWith("ssh://")) {
|
|
1775
|
+
try {
|
|
1776
|
+
const url = new URL(remoteUrl);
|
|
1777
|
+
return { host: url.host, path: url.pathname.replace(/^\//, "") };
|
|
1778
|
+
} catch {
|
|
1779
|
+
return void 0;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
return void 0;
|
|
1783
|
+
}
|
|
1784
|
+
function normalizeProjectPath(remotePath, instanceBasePath) {
|
|
1785
|
+
let pathValue = remotePath;
|
|
1786
|
+
if (instanceBasePath && instanceBasePath !== "/") {
|
|
1787
|
+
const base = instanceBasePath.replace(/^\//, "") + "/";
|
|
1788
|
+
if (pathValue.startsWith(base)) {
|
|
1789
|
+
pathValue = pathValue.slice(base.length);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
const cleaned = stripGitSuffix(pathValue);
|
|
1793
|
+
return cleaned.length > 0 ? cleaned : void 0;
|
|
1794
|
+
}
|
|
1795
|
+
function stripGitSuffix(pathname) {
|
|
1796
|
+
return pathname.endsWith(".git") ? pathname.slice(0, -4) : pathname;
|
|
1797
|
+
}
|
|
1798
|
+
function buildApiUrl(instanceUrl, apiPath) {
|
|
1799
|
+
const base = instanceUrl.endsWith("/") ? instanceUrl : `${instanceUrl}/`;
|
|
1800
|
+
return new URL(apiPath.replace(/^\//, ""), base).toString();
|
|
1801
|
+
}
|
|
1802
|
+
function buildAuthHeaders(apiKey) {
|
|
1803
|
+
return { authorization: `Bearer ${apiKey}` };
|
|
1804
|
+
}
|
|
1805
|
+
async function fetchProjectDetails(instanceUrl, apiKey, projectPath) {
|
|
1806
|
+
const url = buildApiUrl(instanceUrl, `api/v4/projects/${encodeURIComponent(projectPath)}`);
|
|
1807
|
+
const response = await fetch(url, {
|
|
1808
|
+
headers: buildAuthHeaders(apiKey)
|
|
1809
|
+
});
|
|
1810
|
+
if (!response.ok) {
|
|
1811
|
+
throw new Error(`Failed to fetch project details: ${response.status}`);
|
|
1812
|
+
}
|
|
1813
|
+
const data = await response.json();
|
|
1814
|
+
return {
|
|
1815
|
+
projectId: data.id ? String(data.id) : void 0,
|
|
1816
|
+
namespaceId: data.namespace?.id ? String(data.namespace.id) : void 0
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
async function fetchProjectDetailsWithFallback(instanceUrl, apiKey, projectPath) {
|
|
1820
|
+
const candidates = getProjectPathCandidates(projectPath);
|
|
1821
|
+
for (const candidate of candidates) {
|
|
1822
|
+
try {
|
|
1823
|
+
return await fetchProjectDetails(instanceUrl, apiKey, candidate);
|
|
1824
|
+
} catch {
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
try {
|
|
1829
|
+
const name = projectPath.split("/").pop() || projectPath;
|
|
1830
|
+
const searchUrl = new URL(buildApiUrl(instanceUrl, "api/v4/projects"));
|
|
1831
|
+
searchUrl.searchParams.set("search", name);
|
|
1832
|
+
searchUrl.searchParams.set("simple", "true");
|
|
1833
|
+
searchUrl.searchParams.set("per_page", "100");
|
|
1834
|
+
searchUrl.searchParams.set("membership", "true");
|
|
1835
|
+
const response = await fetch(searchUrl.toString(), {
|
|
1836
|
+
headers: buildAuthHeaders(apiKey)
|
|
1837
|
+
});
|
|
1838
|
+
if (!response.ok) {
|
|
1839
|
+
throw new Error(`Failed to search projects: ${response.status}`);
|
|
1840
|
+
}
|
|
1841
|
+
const data = await response.json();
|
|
1842
|
+
const match = data.find((project) => project.path_with_namespace === projectPath);
|
|
1843
|
+
if (!match) {
|
|
1844
|
+
throw new Error("Project not found via search");
|
|
1845
|
+
}
|
|
1846
|
+
return {
|
|
1847
|
+
projectId: match.id ? String(match.id) : void 0,
|
|
1848
|
+
namespaceId: match.namespace?.id ? String(match.namespace.id) : void 0
|
|
1849
|
+
};
|
|
1850
|
+
} catch {
|
|
1851
|
+
throw new Error("Project not found via API");
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
function getProjectPathCandidates(projectPath) {
|
|
1855
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1856
|
+
candidates.add(projectPath);
|
|
1857
|
+
const parts = projectPath.split("/");
|
|
1858
|
+
if (parts.length > 2) {
|
|
1859
|
+
const withoutFirst = parts.slice(1).join("/");
|
|
1860
|
+
candidates.add(withoutFirst);
|
|
1861
|
+
}
|
|
1862
|
+
return Array.from(candidates);
|
|
1863
|
+
}
|
|
1864
|
+
async function readGitConfig(cwd) {
|
|
1865
|
+
const gitPath = path4.join(cwd, ".git");
|
|
1866
|
+
const stat = await fs4.stat(gitPath);
|
|
1867
|
+
if (stat.isDirectory()) {
|
|
1868
|
+
return fs4.readFile(path4.join(gitPath, "config"), "utf8");
|
|
1869
|
+
}
|
|
1870
|
+
const file = await fs4.readFile(gitPath, "utf8");
|
|
1871
|
+
const match = /^gitdir:\s*(.+)$/m.exec(file);
|
|
1872
|
+
if (!match) throw new Error("Invalid .git file");
|
|
1873
|
+
const gitdir = match[1].trim();
|
|
1874
|
+
const resolved = path4.isAbsolute(gitdir) ? gitdir : path4.join(cwd, gitdir);
|
|
1875
|
+
return fs4.readFile(path4.join(resolved, "config"), "utf8");
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// src/provider/adapters/workflow_service.ts
|
|
1879
|
+
var WorkflowCreateError = class extends Error {
|
|
1880
|
+
status;
|
|
1881
|
+
body;
|
|
1882
|
+
constructor(status, body) {
|
|
1883
|
+
super(`Failed to create workflow: ${status} ${body}`);
|
|
1884
|
+
this.status = status;
|
|
1885
|
+
this.body = body;
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
async function createWorkflow(instanceUrl, apiKey, goal, workflowDefinition, containerParams) {
|
|
1889
|
+
const url = buildApiUrl(instanceUrl, "/api/v4/ai/duo_workflows/workflows");
|
|
1890
|
+
const response = await fetch(url.toString(), {
|
|
1891
|
+
method: "POST",
|
|
1892
|
+
headers: {
|
|
1893
|
+
"content-type": "application/json",
|
|
1894
|
+
...buildAuthHeaders(apiKey)
|
|
1895
|
+
},
|
|
1896
|
+
body: JSON.stringify({
|
|
1897
|
+
project_id: containerParams?.projectId,
|
|
1898
|
+
namespace_id: containerParams?.namespaceId,
|
|
1899
|
+
goal,
|
|
1900
|
+
workflow_definition: workflowDefinition,
|
|
1901
|
+
environment: "ide",
|
|
1902
|
+
allow_agent_to_request_user: true
|
|
1903
|
+
})
|
|
1904
|
+
});
|
|
1905
|
+
if (!response.ok) {
|
|
1906
|
+
const text = await response.text();
|
|
1907
|
+
throw new WorkflowCreateError(response.status, text);
|
|
1908
|
+
}
|
|
1909
|
+
const data = await response.json();
|
|
1910
|
+
if (!data.id) {
|
|
1911
|
+
throw new Error(`Workflow creation failed: ${data.error || data.message || "unknown"}`);
|
|
1912
|
+
}
|
|
1913
|
+
return data.id.toString();
|
|
1914
|
+
}
|
|
1915
|
+
async function getWorkflowToken(instanceUrl, apiKey, workflowDefinition) {
|
|
1916
|
+
const url = buildApiUrl(instanceUrl, "/api/v4/ai/duo_workflows/direct_access");
|
|
1917
|
+
const response = await fetch(url.toString(), {
|
|
1918
|
+
method: "POST",
|
|
1919
|
+
headers: {
|
|
1920
|
+
"content-type": "application/json",
|
|
1921
|
+
...buildAuthHeaders(apiKey)
|
|
1922
|
+
},
|
|
1923
|
+
body: JSON.stringify({ workflow_definition: workflowDefinition })
|
|
1924
|
+
});
|
|
1925
|
+
if (!response.ok) {
|
|
1926
|
+
const text = await response.text();
|
|
1927
|
+
throw new Error(`Failed to fetch workflow token: ${response.status} ${text}`);
|
|
1928
|
+
}
|
|
1929
|
+
return await response.json();
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
// src/provider/adapters/workflow_client.ts
|
|
1933
|
+
import WebSocket2 from "isomorphic-ws";
|
|
1934
|
+
import { v4 as uuid4 } from "uuid";
|
|
1935
|
+
|
|
1936
|
+
// src/provider/adapters/websocket_stream.ts
|
|
1937
|
+
import WebSocket from "isomorphic-ws";
|
|
1938
|
+
import { EventEmitter } from "events";
|
|
1939
|
+
var KEEPALIVE_PING_INTERVAL_MS = 45 * 1e3;
|
|
1940
|
+
var WebSocketWorkflowStream = class extends EventEmitter {
|
|
1941
|
+
#socket;
|
|
1942
|
+
#keepalivePingIntervalId;
|
|
1943
|
+
constructor(socket) {
|
|
1944
|
+
super();
|
|
1945
|
+
this.#socket = socket;
|
|
1946
|
+
this.#setupEventHandlers();
|
|
1947
|
+
}
|
|
1948
|
+
#setupEventHandlers() {
|
|
1949
|
+
this.#socket.on("message", (event) => {
|
|
1950
|
+
try {
|
|
1951
|
+
const data = event && typeof event === "object" && "data" in event ? event.data : event;
|
|
1952
|
+
let message;
|
|
1953
|
+
if (typeof data === "string") {
|
|
1954
|
+
message = data;
|
|
1955
|
+
} else if (Buffer.isBuffer(data)) {
|
|
1956
|
+
message = data.toString("utf8");
|
|
1957
|
+
} else if (data instanceof ArrayBuffer) {
|
|
1958
|
+
message = Buffer.from(data).toString("utf8");
|
|
1959
|
+
} else if (Array.isArray(data)) {
|
|
1960
|
+
message = Buffer.concat(data).toString("utf8");
|
|
1961
|
+
} else {
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
if (!message || message === "undefined") {
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
const parsed = JSON.parse(message);
|
|
1968
|
+
this.emit("data", parsed);
|
|
1969
|
+
} catch (err2) {
|
|
1970
|
+
this.emit("error", err2 instanceof Error ? err2 : new Error(String(err2)));
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
this.#socket.on("open", () => {
|
|
1974
|
+
this.emit("open");
|
|
1975
|
+
});
|
|
1976
|
+
this.#socket.on("error", (event) => {
|
|
1977
|
+
if (event instanceof Error) {
|
|
1978
|
+
this.emit("error", event);
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
const serialized = safeStringifyErrorEvent(event);
|
|
1982
|
+
this.emit("error", new Error(serialized));
|
|
1983
|
+
});
|
|
1984
|
+
this.#socket.on("close", (code, reason) => {
|
|
1985
|
+
clearInterval(this.#keepalivePingIntervalId);
|
|
1986
|
+
if (code === 1e3) {
|
|
1987
|
+
this.emit("end");
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
const reasonString = reason?.toString("utf8");
|
|
1991
|
+
this.emit("error", new Error(`WebSocket closed abnormally: ${code} ${reasonString || ""}`));
|
|
1992
|
+
});
|
|
1993
|
+
this.#socket.on("pong", () => {
|
|
1994
|
+
});
|
|
1995
|
+
this.#startKeepalivePingInterval();
|
|
1996
|
+
}
|
|
1997
|
+
#startKeepalivePingInterval() {
|
|
1998
|
+
this.#keepalivePingIntervalId = setInterval(() => {
|
|
1999
|
+
if (this.#socket.readyState !== WebSocket.OPEN) return;
|
|
2000
|
+
const timestamp = Date.now().toString();
|
|
2001
|
+
this.#socket.ping(Buffer.from(timestamp), void 0, () => {
|
|
2002
|
+
});
|
|
2003
|
+
}, KEEPALIVE_PING_INTERVAL_MS);
|
|
2004
|
+
}
|
|
2005
|
+
write(data) {
|
|
2006
|
+
if (this.#socket.readyState !== WebSocket.OPEN) {
|
|
2007
|
+
return false;
|
|
2008
|
+
}
|
|
2009
|
+
this.#socket.send(JSON.stringify(data));
|
|
2010
|
+
return true;
|
|
2011
|
+
}
|
|
2012
|
+
end() {
|
|
2013
|
+
this.#socket.close(1e3);
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
function safeStringifyErrorEvent(event) {
|
|
2017
|
+
const payload = {
|
|
2018
|
+
type: event.type,
|
|
2019
|
+
message: event.message,
|
|
2020
|
+
error: event.error ? String(event.error) : void 0,
|
|
2021
|
+
target: {
|
|
2022
|
+
readyState: event.target?.readyState,
|
|
2023
|
+
url: event.target?.url
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
return JSON.stringify(payload);
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// src/provider/adapters/workflow_client.ts
|
|
2030
|
+
var WebSocketWorkflowClient = class {
|
|
2031
|
+
#connectionDetails;
|
|
2032
|
+
#selectedModelIdentifier;
|
|
2033
|
+
#socket = null;
|
|
2034
|
+
#stream = null;
|
|
2035
|
+
#correlationId = uuid4();
|
|
2036
|
+
constructor(connectionDetails) {
|
|
2037
|
+
this.#connectionDetails = connectionDetails;
|
|
2038
|
+
this.#selectedModelIdentifier = connectionDetails.selectedModelIdentifier;
|
|
2039
|
+
}
|
|
2040
|
+
async executeWorkflow() {
|
|
2041
|
+
const url = this.#buildWebSocketUrl();
|
|
2042
|
+
const headers = this.#createConnectionHeaders();
|
|
2043
|
+
const clientOptions = { headers };
|
|
2044
|
+
if (this.#connectionDetails.agent) {
|
|
2045
|
+
Object.assign(clientOptions, { agent: this.#connectionDetails.agent });
|
|
2046
|
+
}
|
|
2047
|
+
this.#socket = new WebSocket2(url, clientOptions);
|
|
2048
|
+
this.#stream = new WebSocketWorkflowStream(this.#socket);
|
|
2049
|
+
await new Promise((resolve, reject) => {
|
|
2050
|
+
const timeoutId = setTimeout(() => {
|
|
2051
|
+
reject(new Error("WebSocket connection timeout"));
|
|
2052
|
+
}, 15e3);
|
|
2053
|
+
const onOpen = () => {
|
|
2054
|
+
clearTimeout(timeoutId);
|
|
2055
|
+
this.#stream?.removeListener("error", onError);
|
|
2056
|
+
resolve();
|
|
2057
|
+
};
|
|
2058
|
+
const onError = (err2) => {
|
|
2059
|
+
clearTimeout(timeoutId);
|
|
2060
|
+
this.#stream?.removeListener("open", onOpen);
|
|
2061
|
+
reject(err2);
|
|
2062
|
+
};
|
|
2063
|
+
this.#stream?.once("open", onOpen);
|
|
2064
|
+
this.#stream?.once("error", onError);
|
|
2065
|
+
});
|
|
2066
|
+
return this.#stream;
|
|
2067
|
+
}
|
|
2068
|
+
dispose() {
|
|
2069
|
+
this.#stream?.end();
|
|
2070
|
+
this.#stream = null;
|
|
2071
|
+
this.#socket = null;
|
|
2072
|
+
}
|
|
2073
|
+
#buildWebSocketUrl() {
|
|
2074
|
+
const baseUrl = new URL(this.#connectionDetails.gitlabInstanceUrl);
|
|
2075
|
+
const basePath = baseUrl.pathname.endsWith("/") ? baseUrl.pathname : `${baseUrl.pathname}/`;
|
|
2076
|
+
const url = new URL(basePath + "api/v4/ai/duo_workflows/ws", baseUrl);
|
|
2077
|
+
url.protocol = baseUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
2078
|
+
if (this.#selectedModelIdentifier) {
|
|
2079
|
+
url.searchParams.set("user_selected_model_identifier", this.#selectedModelIdentifier);
|
|
2080
|
+
}
|
|
2081
|
+
return url.toString();
|
|
2082
|
+
}
|
|
2083
|
+
#createConnectionHeaders() {
|
|
2084
|
+
const headers = { ...this.#connectionDetails.headers };
|
|
2085
|
+
headers["authorization"] = `Bearer ${this.#connectionDetails.token}`;
|
|
2086
|
+
headers["x-request-id"] = this.#correlationId;
|
|
2087
|
+
headers["x-gitlab-language-server-version"] = LANGUAGE_SERVER_VERSION;
|
|
2088
|
+
headers["x-gitlab-client-type"] = "node-websocket";
|
|
2089
|
+
headers["user-agent"] = buildUserAgent();
|
|
2090
|
+
headers["origin"] = this.#connectionDetails.gitlabInstanceUrl.origin;
|
|
2091
|
+
return headers;
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
var LANGUAGE_SERVER_VERSION = "8.62.2";
|
|
2095
|
+
function buildUserAgent() {
|
|
2096
|
+
return `unknown/unknown unknown/unknown gitlab-language-server/${LANGUAGE_SERVER_VERSION}`;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// src/provider/adapters/system_context.ts
|
|
2100
|
+
import os from "os";
|
|
2101
|
+
function getSystemContextItems(systemRules) {
|
|
2102
|
+
const platform = os.platform();
|
|
2103
|
+
const arch = os.arch();
|
|
2104
|
+
const items = [
|
|
2105
|
+
{
|
|
2106
|
+
category: "os_information",
|
|
2107
|
+
content: `<os><platform>${platform}</platform><architecture>${arch}</architecture></os>`,
|
|
2108
|
+
id: "os_information",
|
|
2109
|
+
metadata: {
|
|
2110
|
+
title: "Operating System",
|
|
2111
|
+
enabled: true,
|
|
2112
|
+
subType: "os",
|
|
2113
|
+
icon: "monitor",
|
|
2114
|
+
secondaryText: `${platform} ${arch}`,
|
|
2115
|
+
subTypeLabel: "System Information"
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
];
|
|
2119
|
+
const trimmedRules = systemRules?.trim();
|
|
2120
|
+
const combinedRules = trimmedRules ? `${trimmedRules}
|
|
2121
|
+
|
|
2122
|
+
${SYSTEM_RULES}` : SYSTEM_RULES;
|
|
2123
|
+
if (combinedRules.trim()) {
|
|
2124
|
+
items.push({
|
|
2125
|
+
category: "user_rule",
|
|
2126
|
+
content: combinedRules,
|
|
2127
|
+
id: "user_rules",
|
|
2128
|
+
metadata: {
|
|
2129
|
+
title: "System Rules",
|
|
2130
|
+
enabled: true,
|
|
2131
|
+
subType: "user_rule",
|
|
2132
|
+
icon: "file-text",
|
|
2133
|
+
secondaryText: "User rules",
|
|
2134
|
+
subTypeLabel: "System rules"
|
|
2135
|
+
}
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
return items;
|
|
2139
|
+
}
|
|
2140
|
+
var SYSTEM_RULES = `<system-reminder>
|
|
2141
|
+
You MUST follow ALL the rules in this block strictly.
|
|
2142
|
+
|
|
2143
|
+
<tool_orchestration>
|
|
2144
|
+
PARALLEL EXECUTION:
|
|
2145
|
+
- When gathering information, plan all needed searches upfront and execute
|
|
2146
|
+
them together using multiple tool calls in the same turn where possible.
|
|
2147
|
+
- Read multiple related files together rather than one at a time.
|
|
2148
|
+
- Patterns: grep + find_files together, read_file for multiple files together.
|
|
2149
|
+
- Temporary rule: do NOT use read_files; use read_file only (repeat calls as needed).
|
|
2150
|
+
|
|
2151
|
+
SEQUENTIAL EXECUTION (only when output depends on previous step):
|
|
2152
|
+
- Read a file BEFORE editing it (always).
|
|
2153
|
+
- Check dependencies BEFORE importing them.
|
|
2154
|
+
- Run tests AFTER making changes.
|
|
2155
|
+
|
|
2156
|
+
READ BEFORE WRITE:
|
|
2157
|
+
- Always read existing files before modifying them to understand context.
|
|
2158
|
+
- Check for existing patterns (naming, imports, error handling) and match them.
|
|
2159
|
+
- Verify the exact content to replace when using edit_file.
|
|
2160
|
+
|
|
2161
|
+
ERROR HANDLING:
|
|
2162
|
+
- If a tool fails, analyze the error before retrying.
|
|
2163
|
+
- If a shell command fails, check the error output and adapt.
|
|
2164
|
+
- Do not repeat the same failing operation without changes.
|
|
2165
|
+
</tool_orchestration>
|
|
2166
|
+
|
|
2167
|
+
<development_workflow>
|
|
2168
|
+
For software development tasks, follow this workflow:
|
|
2169
|
+
|
|
2170
|
+
1. UNDERSTAND: Read relevant files, explore the codebase structure
|
|
2171
|
+
2. PLAN: Break down the task into clear steps
|
|
2172
|
+
3. IMPLEMENT: Make changes methodically, one step at a time
|
|
2173
|
+
4. VERIFY: Run tests, type-checking, or build to validate changes
|
|
2174
|
+
5. COMPLETE: Summarize what was accomplished
|
|
2175
|
+
|
|
2176
|
+
CODE QUALITY:
|
|
2177
|
+
- Match existing code style and patterns in the project
|
|
2178
|
+
- Write immediately executable code (no TODOs or placeholders)
|
|
2179
|
+
- Prefer editing existing files over creating new ones
|
|
2180
|
+
- Use the project's established error handling patterns
|
|
2181
|
+
</development_workflow>
|
|
2182
|
+
|
|
2183
|
+
<communication>
|
|
2184
|
+
- Be concise and direct. Responses appear in a chat panel.
|
|
2185
|
+
- Focus on practical solutions over theoretical discussion.
|
|
2186
|
+
- When unable to complete a request, explain the limitation briefly and
|
|
2187
|
+
provide alternatives.
|
|
2188
|
+
- Use active language: "Analyzing...", "Searching..." instead of "Let me..."
|
|
2189
|
+
</communication>
|
|
2190
|
+
</system-reminder>`;
|
|
2191
|
+
|
|
2192
|
+
// src/provider/application/di_container.ts
|
|
2193
|
+
var DIContainer = class {
|
|
2194
|
+
#registrations = /* @__PURE__ */ new Map();
|
|
2195
|
+
#resolving = /* @__PURE__ */ new Set();
|
|
2196
|
+
registerSingleton(token, factory) {
|
|
2197
|
+
this.#registrations.set(token, {
|
|
2198
|
+
scope: "singleton",
|
|
2199
|
+
factory,
|
|
2200
|
+
initialized: false
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
registerTransient(token, factory) {
|
|
2204
|
+
this.#registrations.set(token, {
|
|
2205
|
+
scope: "transient",
|
|
2206
|
+
factory,
|
|
2207
|
+
initialized: false
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
resolve(token) {
|
|
2211
|
+
const registration = this.#registrations.get(token);
|
|
2212
|
+
if (!registration) {
|
|
2213
|
+
throw new Error(`Missing DI registration for token: ${token}`);
|
|
2214
|
+
}
|
|
2215
|
+
if (registration.scope === "singleton") {
|
|
2216
|
+
if (!registration.initialized) {
|
|
2217
|
+
registration.instance = this.#build(token, registration.factory);
|
|
2218
|
+
registration.initialized = true;
|
|
2219
|
+
}
|
|
2220
|
+
return registration.instance;
|
|
2221
|
+
}
|
|
2222
|
+
return this.#build(token, registration.factory);
|
|
2223
|
+
}
|
|
2224
|
+
#build(token, factory) {
|
|
2225
|
+
if (this.#resolving.has(token)) {
|
|
2226
|
+
throw new Error(`Circular DI dependency detected for token: ${token}`);
|
|
2227
|
+
}
|
|
2228
|
+
this.#resolving.add(token);
|
|
2229
|
+
try {
|
|
2230
|
+
return factory(this);
|
|
2231
|
+
} finally {
|
|
2232
|
+
this.#resolving.delete(token);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
};
|
|
2236
|
+
|
|
2237
|
+
// src/provider/adapters/default_runtime_dependencies.ts
|
|
2238
|
+
var RUNTIME_TOKENS = {
|
|
2239
|
+
workflowService: "runtime.workflowService",
|
|
2240
|
+
workflowClientFactory: "runtime.workflowClientFactory",
|
|
2241
|
+
projectLookup: "runtime.projectLookup",
|
|
2242
|
+
systemContext: "runtime.systemContext",
|
|
2243
|
+
clock: "runtime.clock",
|
|
2244
|
+
runtimeDependencies: "runtime.dependencies"
|
|
2245
|
+
};
|
|
2246
|
+
function createRuntimeContainer() {
|
|
2247
|
+
const container = new DIContainer();
|
|
2248
|
+
registerDefaultRuntimeDependencies(container);
|
|
2249
|
+
return container;
|
|
2250
|
+
}
|
|
2251
|
+
function registerDefaultRuntimeDependencies(container) {
|
|
2252
|
+
container.registerSingleton(
|
|
2253
|
+
RUNTIME_TOKENS.workflowService,
|
|
2254
|
+
() => ({
|
|
2255
|
+
createWorkflow,
|
|
2256
|
+
getWorkflowToken
|
|
2257
|
+
})
|
|
2258
|
+
);
|
|
2259
|
+
container.registerSingleton(
|
|
2260
|
+
RUNTIME_TOKENS.workflowClientFactory,
|
|
2261
|
+
() => ((config) => new WebSocketWorkflowClient({
|
|
2262
|
+
...config,
|
|
2263
|
+
...resolveWorkflowClientOptions()
|
|
2264
|
+
}))
|
|
2265
|
+
);
|
|
2266
|
+
container.registerSingleton(
|
|
2267
|
+
RUNTIME_TOKENS.projectLookup,
|
|
2268
|
+
() => ({
|
|
2269
|
+
detectProjectPath,
|
|
2270
|
+
fetchProjectDetailsWithFallback
|
|
2271
|
+
})
|
|
2272
|
+
);
|
|
2273
|
+
container.registerSingleton(
|
|
2274
|
+
RUNTIME_TOKENS.systemContext,
|
|
2275
|
+
() => ({
|
|
2276
|
+
getSystemContextItems
|
|
2277
|
+
})
|
|
2278
|
+
);
|
|
2279
|
+
container.registerSingleton(
|
|
2280
|
+
RUNTIME_TOKENS.clock,
|
|
2281
|
+
() => ({
|
|
2282
|
+
now: () => Date.now(),
|
|
2283
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
2284
|
+
})
|
|
2285
|
+
);
|
|
2286
|
+
container.registerSingleton(
|
|
2287
|
+
RUNTIME_TOKENS.runtimeDependencies,
|
|
2288
|
+
(c) => ({
|
|
2289
|
+
workflowService: c.resolve(
|
|
2290
|
+
RUNTIME_TOKENS.workflowService
|
|
2291
|
+
),
|
|
2292
|
+
createWorkflowClient: c.resolve(
|
|
2293
|
+
RUNTIME_TOKENS.workflowClientFactory
|
|
2294
|
+
),
|
|
2295
|
+
projectLookup: c.resolve(
|
|
2296
|
+
RUNTIME_TOKENS.projectLookup
|
|
2297
|
+
),
|
|
2298
|
+
systemContext: c.resolve(
|
|
2299
|
+
RUNTIME_TOKENS.systemContext
|
|
2300
|
+
),
|
|
2301
|
+
clock: c.resolve(RUNTIME_TOKENS.clock)
|
|
2302
|
+
})
|
|
2303
|
+
);
|
|
2304
|
+
}
|
|
2305
|
+
function resolveWorkflowClientOptions() {
|
|
2306
|
+
if (process.env.HTTPS_PROXY || process.env.HTTP_PROXY) {
|
|
2307
|
+
return { agent: new ProxyAgent() };
|
|
2308
|
+
}
|
|
2309
|
+
return {};
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
// src/provider/interfaces/provider.ts
|
|
2313
|
+
import { createRequire as createRequire2 } from "module";
|
|
2314
|
+
var REQUIRED_MODULES = [
|
|
2315
|
+
"isomorphic-ws",
|
|
2316
|
+
"uuid",
|
|
2317
|
+
"zod",
|
|
2318
|
+
"neverthrow",
|
|
2319
|
+
"proxy-agent"
|
|
2320
|
+
];
|
|
2321
|
+
function assertDependencies() {
|
|
2322
|
+
const require2 = createRequire2(import.meta.url);
|
|
2323
|
+
const missing = [];
|
|
2324
|
+
for (const name of REQUIRED_MODULES) {
|
|
2325
|
+
try {
|
|
2326
|
+
require2.resolve(name);
|
|
2327
|
+
} catch {
|
|
2328
|
+
missing.push(name);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
if (missing.length > 0) {
|
|
2332
|
+
throw new Error(
|
|
2333
|
+
"Missing provider dependencies: " + missing.join(", ") + ". Run `npm install` in the package directory."
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
function assertInstanceUrl(value) {
|
|
2338
|
+
try {
|
|
2339
|
+
new URL(value);
|
|
2340
|
+
} catch {
|
|
2341
|
+
throw new Error(`Invalid instanceUrl: "${value}"`);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
function createGitLabDuoAgentic(options) {
|
|
2345
|
+
assertDependencies();
|
|
2346
|
+
assertInstanceUrl(options.instanceUrl);
|
|
2347
|
+
const container = createRuntimeContainer();
|
|
2348
|
+
const dependencies = container.resolve(
|
|
2349
|
+
RUNTIME_TOKENS.runtimeDependencies
|
|
2350
|
+
);
|
|
2351
|
+
const sharedRuntime = new GitLabAgenticRuntime(options, dependencies);
|
|
2352
|
+
return {
|
|
2353
|
+
languageModel(modelId) {
|
|
2354
|
+
return new GitLabDuoAgenticLanguageModel(modelId, options, sharedRuntime);
|
|
2355
|
+
},
|
|
2356
|
+
textEmbeddingModel() {
|
|
2357
|
+
throw new Error("GitLab Duo Agentic does not support text embedding models");
|
|
2358
|
+
},
|
|
2359
|
+
imageModel() {
|
|
2360
|
+
throw new Error("GitLab Duo Agentic does not support image models");
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
export {
|
|
2365
|
+
GitLabDuoAgenticPlugin,
|
|
2366
|
+
createGitLabDuoAgentic,
|
|
2367
|
+
GitLabDuoAgenticPlugin as default
|
|
2368
|
+
};
|