opencode-gitlab-duo-agentic 0.1.24 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +412 -10
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -136,8 +136,8 @@ async function resolveRootNamespaceId(client, namespaceId) {
|
|
|
136
136
|
}
|
|
137
137
|
async function readGitConfig(cwd) {
|
|
138
138
|
const gitPath = path.join(cwd, ".git");
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
139
|
+
const stat2 = await fs.stat(gitPath);
|
|
140
|
+
if (stat2.isDirectory()) {
|
|
141
141
|
return fs.readFile(path.join(gitPath, "config"), "utf8");
|
|
142
142
|
}
|
|
143
143
|
const content = await fs.readFile(gitPath, "utf8");
|
|
@@ -343,8 +343,236 @@ function toModelsConfig(available) {
|
|
|
343
343
|
return out;
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
+
// src/workflow/opencode-tools.ts
|
|
347
|
+
var SUPPORTED_TOOLS = /* @__PURE__ */ new Set([
|
|
348
|
+
"bash",
|
|
349
|
+
"read",
|
|
350
|
+
"edit",
|
|
351
|
+
"write",
|
|
352
|
+
"glob",
|
|
353
|
+
"grep"
|
|
354
|
+
]);
|
|
355
|
+
async function fetchOpencodeTools(client, provider, model) {
|
|
356
|
+
try {
|
|
357
|
+
const response = await client.tool.list({ provider, model });
|
|
358
|
+
const tools = response.data ?? [];
|
|
359
|
+
return tools.filter((t) => SUPPORTED_TOOLS.has(t.id)).map((t) => ({
|
|
360
|
+
name: t.id,
|
|
361
|
+
description: t.description,
|
|
362
|
+
inputSchema: typeof t.parameters === "string" ? t.parameters : JSON.stringify(t.parameters)
|
|
363
|
+
}));
|
|
364
|
+
} catch {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function buildFallbackTools() {
|
|
369
|
+
return [
|
|
370
|
+
{
|
|
371
|
+
name: "bash",
|
|
372
|
+
description: "Execute a shell command. Use the `command` parameter for the command to run, `description` for a short summary, optional `timeout` in milliseconds, and optional `workdir` to set the working directory.",
|
|
373
|
+
inputSchema: JSON.stringify({
|
|
374
|
+
type: "object",
|
|
375
|
+
properties: {
|
|
376
|
+
command: { type: "string", description: "The command to execute" },
|
|
377
|
+
description: { type: "string", description: "Clear, concise description of what this command does in 5-10 words" },
|
|
378
|
+
timeout: { type: "number", description: "Optional timeout in milliseconds" },
|
|
379
|
+
workdir: { type: "string", description: "The working directory to run the command in" }
|
|
380
|
+
},
|
|
381
|
+
required: ["command", "description"]
|
|
382
|
+
})
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: "read",
|
|
386
|
+
description: "Read a file or directory from the local filesystem. Returns content with line numbers prefixed. Supports offset and limit for pagination. Can read images and PDFs as attachments.",
|
|
387
|
+
inputSchema: JSON.stringify({
|
|
388
|
+
type: "object",
|
|
389
|
+
properties: {
|
|
390
|
+
filePath: { type: "string", description: "The absolute path to the file or directory to read" },
|
|
391
|
+
offset: { type: "number", description: "The line number to start reading from (1-indexed)" },
|
|
392
|
+
limit: { type: "number", description: "The maximum number of lines to read (defaults to 2000)" }
|
|
393
|
+
},
|
|
394
|
+
required: ["filePath"]
|
|
395
|
+
})
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "edit",
|
|
399
|
+
description: "Performs exact string replacements in files. The oldString must match the file contents exactly. Provide surrounding context to make the match unique.",
|
|
400
|
+
inputSchema: JSON.stringify({
|
|
401
|
+
type: "object",
|
|
402
|
+
properties: {
|
|
403
|
+
filePath: { type: "string", description: "The absolute path to the file to modify" },
|
|
404
|
+
oldString: { type: "string", description: "The text to replace" },
|
|
405
|
+
newString: { type: "string", description: "The text to replace it with" },
|
|
406
|
+
replaceAll: { type: "boolean", description: "Replace all occurrences (default false)" }
|
|
407
|
+
},
|
|
408
|
+
required: ["filePath", "oldString", "newString"]
|
|
409
|
+
})
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: "write",
|
|
413
|
+
description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Parent directories are created automatically.",
|
|
414
|
+
inputSchema: JSON.stringify({
|
|
415
|
+
type: "object",
|
|
416
|
+
properties: {
|
|
417
|
+
filePath: { type: "string", description: "The absolute path to the file to write" },
|
|
418
|
+
content: { type: "string", description: "The content to write to the file" }
|
|
419
|
+
},
|
|
420
|
+
required: ["filePath", "content"]
|
|
421
|
+
})
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: "glob",
|
|
425
|
+
description: "Fast file pattern matching. Supports glob patterns like '**/*.js' or 'src/**/*.ts'. Returns matching file paths sorted by modification time.",
|
|
426
|
+
inputSchema: JSON.stringify({
|
|
427
|
+
type: "object",
|
|
428
|
+
properties: {
|
|
429
|
+
pattern: { type: "string", description: "The glob pattern to match files against" },
|
|
430
|
+
path: { type: "string", description: "The directory to search in" }
|
|
431
|
+
},
|
|
432
|
+
required: ["pattern"]
|
|
433
|
+
})
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: "grep",
|
|
437
|
+
description: "Fast content search using regular expressions. Returns file paths and line numbers with matches. Supports filtering files by pattern with the include parameter.",
|
|
438
|
+
inputSchema: JSON.stringify({
|
|
439
|
+
type: "object",
|
|
440
|
+
properties: {
|
|
441
|
+
pattern: { type: "string", description: "The regex pattern to search for in file contents" },
|
|
442
|
+
path: { type: "string", description: "The directory to search in" },
|
|
443
|
+
include: { type: "string", description: "File pattern to include in the search (e.g. '*.js', '*.{ts,tsx}')" }
|
|
444
|
+
},
|
|
445
|
+
required: ["pattern"]
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/workflow/flow-config.ts
|
|
452
|
+
function buildFlowConfig(systemPrompt) {
|
|
453
|
+
return {
|
|
454
|
+
version: "v1",
|
|
455
|
+
environment: "chat-partial",
|
|
456
|
+
components: [
|
|
457
|
+
{
|
|
458
|
+
name: "opencode_agent",
|
|
459
|
+
type: "AgentComponent",
|
|
460
|
+
// Empty toolset: we rely entirely on MCP tools (OpenCode tools)
|
|
461
|
+
// registered via the mcpTools field in startRequest
|
|
462
|
+
toolset: []
|
|
463
|
+
}
|
|
464
|
+
],
|
|
465
|
+
prompts: [
|
|
466
|
+
{
|
|
467
|
+
name: "opencode_prompt",
|
|
468
|
+
prompt_id: "opencode_prompt",
|
|
469
|
+
model: {
|
|
470
|
+
params: {
|
|
471
|
+
model_class_provider: "anthropic",
|
|
472
|
+
max_tokens: 32768
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
unit_primitives: ["duo_chat"],
|
|
476
|
+
prompt_template: {
|
|
477
|
+
system: systemPrompt,
|
|
478
|
+
user: "{{goal}}",
|
|
479
|
+
placeholder: "history"
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
]
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
var OPENCODE_SYSTEM_PROMPT = `You are OpenCode, an AI coding assistant integrated with GitLab Duo.
|
|
486
|
+
|
|
487
|
+
<core_mission>
|
|
488
|
+
Your primary role is collaborative programming - working alongside the user to accomplish coding objectives using the tools available to you.
|
|
489
|
+
</core_mission>
|
|
490
|
+
|
|
491
|
+
<communication_guidelines>
|
|
492
|
+
- Provide clear and concise responses. Brevity and clarity are critical.
|
|
493
|
+
- Focus on clean, practical solutions that help users make progress.
|
|
494
|
+
- Keep responses brief and to the point. One-word answers are fine when they suffice.
|
|
495
|
+
- Use active, present-tense language: 'Analyzing...', 'Processing...' instead of 'Let me...', 'I will...'
|
|
496
|
+
- When unable to complete requests, explain the limitation concisely and provide alternatives.
|
|
497
|
+
- When users correct you, acknowledge briefly and apply the correction immediately.
|
|
498
|
+
</communication_guidelines>
|
|
499
|
+
|
|
500
|
+
<tool_usage>
|
|
501
|
+
You have access to file system and shell tools. Use them effectively:
|
|
502
|
+
|
|
503
|
+
- Use the 'read' tool to read files before editing them.
|
|
504
|
+
- Use the 'edit' tool for precise string replacements in existing files.
|
|
505
|
+
- Use the 'write' tool to create new files or overwrite existing ones.
|
|
506
|
+
- Use the 'glob' tool to find files by name patterns.
|
|
507
|
+
- Use the 'grep' tool to search file contents with regex.
|
|
508
|
+
- Use the 'bash' tool for shell commands (git, npm, docker, tests, builds, etc.).
|
|
509
|
+
|
|
510
|
+
Tool orchestration:
|
|
511
|
+
- Execute multiple independent tool operations in parallel when possible.
|
|
512
|
+
- Read files before modifying them to understand context.
|
|
513
|
+
- Only use sequential execution when one operation depends on another's output.
|
|
514
|
+
- Plan your information needs upfront and execute searches together.
|
|
515
|
+
</tool_usage>
|
|
516
|
+
|
|
517
|
+
<code_analysis>
|
|
518
|
+
Before writing any code:
|
|
519
|
+
1. Read existing files to understand context and preserve important logic.
|
|
520
|
+
2. Check dependencies exist before importing.
|
|
521
|
+
3. Match existing patterns: import style, naming conventions, component structure, error handling.
|
|
522
|
+
</code_analysis>
|
|
523
|
+
|
|
524
|
+
<code_standards>
|
|
525
|
+
- Write high-quality, general purpose solutions that work for all valid inputs.
|
|
526
|
+
- Make code immediately executable. No placeholders like "TODO: implement this".
|
|
527
|
+
- Match existing patterns in the codebase.
|
|
528
|
+
- Follow the project's established error handling approach.
|
|
529
|
+
- Verify changes work as expected before completing the task.
|
|
530
|
+
</code_standards>
|
|
531
|
+
|
|
532
|
+
<file_guidelines>
|
|
533
|
+
- ALWAYS prefer editing existing files over creating new ones.
|
|
534
|
+
- NEVER create documentation files unless explicitly requested.
|
|
535
|
+
- Use extended thinking for complex tasks rather than creating temporary files.
|
|
536
|
+
</file_guidelines>
|
|
537
|
+
|
|
538
|
+
<git_guidelines>
|
|
539
|
+
When working with git:
|
|
540
|
+
- Only create commits when explicitly requested by the user.
|
|
541
|
+
- NEVER run destructive git commands (push --force, hard reset) unless explicitly requested.
|
|
542
|
+
- NEVER skip hooks unless explicitly requested.
|
|
543
|
+
- Draft concise commit messages that focus on the "why" rather than the "what".
|
|
544
|
+
- Do not push to remote unless explicitly asked.
|
|
545
|
+
</git_guidelines>`;
|
|
546
|
+
|
|
547
|
+
// src/workflow/tools-config-store.ts
|
|
548
|
+
var stored;
|
|
549
|
+
function setToolsConfig(config) {
|
|
550
|
+
stored = config;
|
|
551
|
+
}
|
|
552
|
+
function getToolsConfig() {
|
|
553
|
+
return stored;
|
|
554
|
+
}
|
|
555
|
+
|
|
346
556
|
// src/plugin/hooks.ts
|
|
557
|
+
async function initializeToolsConfig(input) {
|
|
558
|
+
let mcpTools = await fetchOpencodeTools(
|
|
559
|
+
input.client,
|
|
560
|
+
"gitlab",
|
|
561
|
+
"duo-chat-sonnet-4-5"
|
|
562
|
+
);
|
|
563
|
+
if (mcpTools.length === 0) {
|
|
564
|
+
mcpTools = buildFallbackTools();
|
|
565
|
+
}
|
|
566
|
+
const flowConfig = buildFlowConfig(OPENCODE_SYSTEM_PROMPT);
|
|
567
|
+
setToolsConfig({
|
|
568
|
+
mcpTools,
|
|
569
|
+
flowConfig,
|
|
570
|
+
flowConfigSchemaVersion: "v1"
|
|
571
|
+
});
|
|
572
|
+
}
|
|
347
573
|
async function createPluginHooks(input) {
|
|
574
|
+
initializeToolsConfig(input).catch(() => {
|
|
575
|
+
});
|
|
348
576
|
return {
|
|
349
577
|
config: async (config) => applyRuntimeConfig(config, input.directory),
|
|
350
578
|
"chat.message": async ({ sessionID }, { parts }) => {
|
|
@@ -412,7 +640,7 @@ var AsyncQueue = class {
|
|
|
412
640
|
shift() {
|
|
413
641
|
const value = this.#values.shift();
|
|
414
642
|
if (value !== void 0) return Promise.resolve(value);
|
|
415
|
-
return new Promise((
|
|
643
|
+
return new Promise((resolve2) => this.#waiters.push(resolve2));
|
|
416
644
|
}
|
|
417
645
|
};
|
|
418
646
|
|
|
@@ -518,6 +746,9 @@ var WORKFLOW_STATUS = {
|
|
|
518
746
|
function isCheckpointAction(action) {
|
|
519
747
|
return "newCheckpoint" in action && action.newCheckpoint != null;
|
|
520
748
|
}
|
|
749
|
+
function isMcpToolAction(action) {
|
|
750
|
+
return "runMCPTool" in action && action.runMCPTool != null;
|
|
751
|
+
}
|
|
521
752
|
var TURN_COMPLETE_STATUSES = /* @__PURE__ */ new Set([
|
|
522
753
|
WORKFLOW_STATUS.INPUT_REQUIRED,
|
|
523
754
|
WORKFLOW_STATUS.FINISHED,
|
|
@@ -543,7 +774,7 @@ var WorkflowWebSocketClient = class {
|
|
|
543
774
|
async connect(url, headers) {
|
|
544
775
|
const socket = new WebSocket(url, { headers });
|
|
545
776
|
this.#socket = socket;
|
|
546
|
-
await new Promise((
|
|
777
|
+
await new Promise((resolve2, reject) => {
|
|
547
778
|
const timeout = setTimeout(() => {
|
|
548
779
|
cleanup();
|
|
549
780
|
socket.close(1e3);
|
|
@@ -556,7 +787,7 @@ var WorkflowWebSocketClient = class {
|
|
|
556
787
|
};
|
|
557
788
|
const onOpen = () => {
|
|
558
789
|
cleanup();
|
|
559
|
-
|
|
790
|
+
resolve2();
|
|
560
791
|
};
|
|
561
792
|
const onError = (error) => {
|
|
562
793
|
cleanup();
|
|
@@ -621,6 +852,141 @@ function decodeSocketMessage(data) {
|
|
|
621
852
|
return void 0;
|
|
622
853
|
}
|
|
623
854
|
|
|
855
|
+
// src/workflow/tool-executor.ts
|
|
856
|
+
import { readFile, readdir, writeFile, mkdir, stat } from "fs/promises";
|
|
857
|
+
import { exec } from "child_process";
|
|
858
|
+
import { resolve, dirname } from "path";
|
|
859
|
+
var ToolExecutor = class {
|
|
860
|
+
#cwd;
|
|
861
|
+
constructor(cwd) {
|
|
862
|
+
this.#cwd = cwd;
|
|
863
|
+
}
|
|
864
|
+
async execute(name, argsJson) {
|
|
865
|
+
const args = argsJson ? JSON.parse(argsJson) : {};
|
|
866
|
+
switch (name) {
|
|
867
|
+
case "bash":
|
|
868
|
+
return this.#bash(args);
|
|
869
|
+
case "read":
|
|
870
|
+
return this.#read(args);
|
|
871
|
+
case "edit":
|
|
872
|
+
return this.#edit(args);
|
|
873
|
+
case "write":
|
|
874
|
+
return this.#write(args);
|
|
875
|
+
case "glob":
|
|
876
|
+
return this.#glob(args);
|
|
877
|
+
case "grep":
|
|
878
|
+
return this.#grep(args);
|
|
879
|
+
default:
|
|
880
|
+
return { response: "", error: `Unknown tool: ${name}` };
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async #bash(args) {
|
|
884
|
+
const cwd = args.workdir ? resolve(this.#cwd, args.workdir) : this.#cwd;
|
|
885
|
+
const timeout = args.timeout ?? 12e4;
|
|
886
|
+
return new Promise((resolve2) => {
|
|
887
|
+
exec(args.command, { cwd, timeout, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
888
|
+
if (error) {
|
|
889
|
+
const output2 = [stdout, stderr].filter(Boolean).join("\n");
|
|
890
|
+
resolve2({ response: output2, error: error.message });
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
const output = [stdout, stderr].filter(Boolean).join("\n");
|
|
894
|
+
resolve2({ response: output });
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
async #read(args) {
|
|
899
|
+
try {
|
|
900
|
+
const filepath = resolve(this.#cwd, args.filePath);
|
|
901
|
+
const info = await stat(filepath);
|
|
902
|
+
if (info.isDirectory()) {
|
|
903
|
+
const entries = await readdir(filepath, { withFileTypes: true });
|
|
904
|
+
const lines = entries.map((e) => e.isDirectory() ? `${e.name}/` : e.name);
|
|
905
|
+
return { response: lines.join("\n") };
|
|
906
|
+
}
|
|
907
|
+
const content = await readFile(filepath, "utf-8");
|
|
908
|
+
const allLines = content.split("\n");
|
|
909
|
+
const offset = Math.max((args.offset ?? 1) - 1, 0);
|
|
910
|
+
const limit = args.limit ?? 2e3;
|
|
911
|
+
const slice = allLines.slice(offset, offset + limit);
|
|
912
|
+
const numbered = slice.map((line, i) => `${offset + i + 1}: ${line}`);
|
|
913
|
+
return { response: numbered.join("\n") };
|
|
914
|
+
} catch (err) {
|
|
915
|
+
return { response: "", error: err instanceof Error ? err.message : String(err) };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
async #edit(args) {
|
|
919
|
+
try {
|
|
920
|
+
const filepath = resolve(this.#cwd, args.filePath);
|
|
921
|
+
let content = await readFile(filepath, "utf-8");
|
|
922
|
+
if (args.replaceAll) {
|
|
923
|
+
if (!content.includes(args.oldString)) {
|
|
924
|
+
return { response: "", error: "oldString not found in content" };
|
|
925
|
+
}
|
|
926
|
+
content = content.replaceAll(args.oldString, args.newString);
|
|
927
|
+
} else {
|
|
928
|
+
const idx = content.indexOf(args.oldString);
|
|
929
|
+
if (idx === -1) {
|
|
930
|
+
return { response: "", error: "oldString not found in content" };
|
|
931
|
+
}
|
|
932
|
+
const secondIdx = content.indexOf(args.oldString, idx + 1);
|
|
933
|
+
if (secondIdx !== -1) {
|
|
934
|
+
return { response: "", error: "Found multiple matches for oldString. Provide more surrounding lines to identify the correct match." };
|
|
935
|
+
}
|
|
936
|
+
content = content.slice(0, idx) + args.newString + content.slice(idx + args.oldString.length);
|
|
937
|
+
}
|
|
938
|
+
await writeFile(filepath, content, "utf-8");
|
|
939
|
+
return { response: `Successfully edited ${args.filePath}` };
|
|
940
|
+
} catch (err) {
|
|
941
|
+
return { response: "", error: err instanceof Error ? err.message : String(err) };
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
async #write(args) {
|
|
945
|
+
try {
|
|
946
|
+
const filepath = resolve(this.#cwd, args.filePath);
|
|
947
|
+
await mkdir(dirname(filepath), { recursive: true });
|
|
948
|
+
await writeFile(filepath, args.content, "utf-8");
|
|
949
|
+
return { response: `Successfully wrote ${args.filePath}` };
|
|
950
|
+
} catch (err) {
|
|
951
|
+
return { response: "", error: err instanceof Error ? err.message : String(err) };
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async #glob(args) {
|
|
955
|
+
const cwd = args.path ? resolve(this.#cwd, args.path) : this.#cwd;
|
|
956
|
+
return new Promise((resolve2) => {
|
|
957
|
+
const command = process.platform === "win32" ? `dir /s /b "${args.pattern}"` : `find . -path "./${args.pattern}" -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | head -200`;
|
|
958
|
+
exec(command, { cwd, timeout: 3e4 }, (error, stdout) => {
|
|
959
|
+
if (error && !stdout) {
|
|
960
|
+
exec(`ls -1 ${args.pattern} 2>/dev/null | head -200`, { cwd, timeout: 3e4 }, (_err, out) => {
|
|
961
|
+
resolve2({ response: out.trim() || "No matches found" });
|
|
962
|
+
});
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
966
|
+
resolve2({ response: lines.length > 0 ? lines.join("\n") : "No matches found" });
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
async #grep(args) {
|
|
971
|
+
const cwd = args.path ? resolve(this.#cwd, args.path) : this.#cwd;
|
|
972
|
+
return new Promise((resolve2) => {
|
|
973
|
+
const includeFlag = args.include ? `--glob '${args.include}'` : "";
|
|
974
|
+
const rgCommand = `rg --line-number --no-heading ${includeFlag} '${args.pattern.replace(/'/g, "'\\''")}' . 2>/dev/null | head -500`;
|
|
975
|
+
exec(rgCommand, { cwd, timeout: 3e4, maxBuffer: 5 * 1024 * 1024 }, (error, stdout) => {
|
|
976
|
+
if (stdout.trim()) {
|
|
977
|
+
resolve2({ response: stdout.trim() });
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const grepInclude = args.include ? `--include='${args.include}'` : "";
|
|
981
|
+
const grepCommand = `grep -rn ${grepInclude} '${args.pattern.replace(/'/g, "'\\''")}' . 2>/dev/null | head -500`;
|
|
982
|
+
exec(grepCommand, { cwd, timeout: 3e4, maxBuffer: 5 * 1024 * 1024 }, (_err, out) => {
|
|
983
|
+
resolve2({ response: out.trim() || "No matches found" });
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
|
|
624
990
|
// src/workflow/session.ts
|
|
625
991
|
var WorkflowSession = class {
|
|
626
992
|
#client;
|
|
@@ -631,6 +997,8 @@ var WorkflowSession = class {
|
|
|
631
997
|
#projectPath;
|
|
632
998
|
#rootNamespaceId;
|
|
633
999
|
#checkpoint = createCheckpointState();
|
|
1000
|
+
#toolsConfig;
|
|
1001
|
+
#toolExecutor;
|
|
634
1002
|
/** Mutex: serialises concurrent calls to runTurn so only one runs at a time. */
|
|
635
1003
|
#turnLock = Promise.resolve();
|
|
636
1004
|
constructor(client, modelId, cwd) {
|
|
@@ -638,6 +1006,10 @@ var WorkflowSession = class {
|
|
|
638
1006
|
this.#tokenService = new WorkflowTokenService(client);
|
|
639
1007
|
this.#modelId = modelId;
|
|
640
1008
|
this.#cwd = cwd;
|
|
1009
|
+
this.#toolExecutor = new ToolExecutor(cwd);
|
|
1010
|
+
}
|
|
1011
|
+
setToolsConfig(config) {
|
|
1012
|
+
this.#toolsConfig = config;
|
|
641
1013
|
}
|
|
642
1014
|
get workflowId() {
|
|
643
1015
|
return this.#workflowId;
|
|
@@ -649,9 +1021,9 @@ var WorkflowSession = class {
|
|
|
649
1021
|
}
|
|
650
1022
|
async *runTurn(goal, abortSignal) {
|
|
651
1023
|
await this.#turnLock;
|
|
652
|
-
let
|
|
1024
|
+
let resolve2;
|
|
653
1025
|
this.#turnLock = new Promise((r) => {
|
|
654
|
-
|
|
1026
|
+
resolve2 = r;
|
|
655
1027
|
});
|
|
656
1028
|
const queue = new AsyncQueue();
|
|
657
1029
|
const socket = new WorkflowWebSocketClient({
|
|
@@ -679,6 +1051,8 @@ var WorkflowSession = class {
|
|
|
679
1051
|
"x-gitlab-client-type": "node-websocket"
|
|
680
1052
|
});
|
|
681
1053
|
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
1054
|
+
const mcpTools = this.#toolsConfig?.mcpTools ?? [];
|
|
1055
|
+
const preapprovedTools = mcpTools.map((t) => t.name);
|
|
682
1056
|
const sent = socket.send({
|
|
683
1057
|
startRequest: {
|
|
684
1058
|
workflowID: this.#workflowId,
|
|
@@ -689,9 +1063,13 @@ var WorkflowSession = class {
|
|
|
689
1063
|
extended_logging: access?.workflow_metadata?.extended_logging ?? false
|
|
690
1064
|
}),
|
|
691
1065
|
clientCapabilities: [],
|
|
692
|
-
mcpTools
|
|
1066
|
+
mcpTools,
|
|
693
1067
|
additional_context: [],
|
|
694
|
-
preapproved_tools:
|
|
1068
|
+
preapproved_tools: preapprovedTools,
|
|
1069
|
+
...this.#toolsConfig?.flowConfig ? {
|
|
1070
|
+
flowConfig: this.#toolsConfig.flowConfig,
|
|
1071
|
+
flowConfigSchemaVersion: this.#toolsConfig.flowConfigSchemaVersion ?? "v1"
|
|
1072
|
+
} : {}
|
|
695
1073
|
}
|
|
696
1074
|
});
|
|
697
1075
|
if (!sent) throw new Error("failed to send workflow startRequest");
|
|
@@ -716,6 +1094,20 @@ var WorkflowSession = class {
|
|
|
716
1094
|
continue;
|
|
717
1095
|
}
|
|
718
1096
|
if (!event.action.requestID) continue;
|
|
1097
|
+
if (isMcpToolAction(event.action)) {
|
|
1098
|
+
const { name, args } = event.action.runMCPTool;
|
|
1099
|
+
const result = await this.#toolExecutor.execute(name, args);
|
|
1100
|
+
socket.send({
|
|
1101
|
+
actionResponse: {
|
|
1102
|
+
requestID: event.action.requestID,
|
|
1103
|
+
plainTextResponse: {
|
|
1104
|
+
response: result.response,
|
|
1105
|
+
error: result.error ?? ""
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
719
1111
|
socket.send({
|
|
720
1112
|
actionResponse: {
|
|
721
1113
|
requestID: event.action.requestID,
|
|
@@ -729,7 +1121,7 @@ var WorkflowSession = class {
|
|
|
729
1121
|
} finally {
|
|
730
1122
|
abortSignal?.removeEventListener("abort", onAbort);
|
|
731
1123
|
socket.close();
|
|
732
|
-
|
|
1124
|
+
resolve2();
|
|
733
1125
|
}
|
|
734
1126
|
}
|
|
735
1127
|
async #createWorkflow(goal) {
|
|
@@ -826,11 +1218,19 @@ var DuoWorkflowModel = class {
|
|
|
826
1218
|
supportedUrls = {};
|
|
827
1219
|
#client;
|
|
828
1220
|
#cwd;
|
|
1221
|
+
#toolsConfig;
|
|
829
1222
|
constructor(modelId, client, cwd) {
|
|
830
1223
|
this.modelId = modelId;
|
|
831
1224
|
this.#client = client;
|
|
832
1225
|
this.#cwd = cwd ?? process.cwd();
|
|
833
1226
|
}
|
|
1227
|
+
/** Set the tools configuration for new and existing sessions. */
|
|
1228
|
+
setToolsConfig(config) {
|
|
1229
|
+
this.#toolsConfig = config;
|
|
1230
|
+
for (const session of sessions.values()) {
|
|
1231
|
+
session.setToolsConfig(config);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
834
1234
|
async doGenerate(options) {
|
|
835
1235
|
const sessionID = readSessionID(options);
|
|
836
1236
|
if (!sessionID) throw new Error("missing workflow session ID");
|
|
@@ -928,6 +1328,8 @@ var DuoWorkflowModel = class {
|
|
|
928
1328
|
const existing = sessions.get(key);
|
|
929
1329
|
if (existing) return existing;
|
|
930
1330
|
const created = new WorkflowSession(this.#client, this.modelId, this.#cwd);
|
|
1331
|
+
const config = this.#toolsConfig ?? getToolsConfig();
|
|
1332
|
+
if (config) created.setToolsConfig(config);
|
|
931
1333
|
sessions.set(key, created);
|
|
932
1334
|
return created;
|
|
933
1335
|
}
|