opencode-gitlab-duo-agentic 0.2.5 → 0.2.6

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.
Files changed (2) hide show
  1. package/dist/index.js +248 -27
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1089,7 +1089,7 @@ var WorkflowSession = class {
1089
1089
  // ---------------------------------------------------------------------------
1090
1090
  // Messaging
1091
1091
  // ---------------------------------------------------------------------------
1092
- sendStartRequest(goal) {
1092
+ sendStartRequest(goal, additionalContext = []) {
1093
1093
  if (!this.#socket || !this.#workflowId) throw new Error("Not connected");
1094
1094
  const mcpTools = this.#toolsConfig?.mcpTools ?? [];
1095
1095
  const preapprovedTools = mcpTools.map((t) => t.name);
@@ -1104,7 +1104,7 @@ var WorkflowSession = class {
1104
1104
  }),
1105
1105
  clientCapabilities: ["shell_command"],
1106
1106
  mcpTools,
1107
- additional_context: [],
1107
+ additional_context: additionalContext,
1108
1108
  preapproved_tools: preapprovedTools,
1109
1109
  ...this.#toolsConfig?.flowConfig ? {
1110
1110
  flowConfig: this.#toolsConfig.flowConfig,
@@ -1265,16 +1265,28 @@ function extractToolResults(prompt) {
1265
1265
  if (!Array.isArray(content)) continue;
1266
1266
  for (const part of content) {
1267
1267
  const p = part;
1268
- if (p.type !== "tool-result") continue;
1269
- const toolCallId = String(p.toolCallId ?? "");
1270
- const toolName = String(p.toolName ?? "");
1271
- if (!toolCallId) continue;
1272
- const { output, error } = parseToolResultOutput(p);
1273
- results.push({ toolCallId, toolName, output, error });
1268
+ if (p.type === "tool-result") {
1269
+ const toolCallId = String(p.toolCallId ?? "");
1270
+ const toolName = String(p.toolName ?? "");
1271
+ if (!toolCallId) continue;
1272
+ const { output, error } = parseToolResultOutput(p);
1273
+ const finalError = error ?? asString2(p.error) ?? asString2(p.errorText);
1274
+ results.push({ toolCallId, toolName, output, error: finalError });
1275
+ }
1276
+ if (p.type === "tool-error") {
1277
+ const toolCallId = String(p.toolCallId ?? "");
1278
+ const toolName = String(p.toolName ?? "");
1279
+ const errorValue = p.error ?? p.errorText ?? p.message;
1280
+ const error = asString2(errorValue) ?? String(errorValue ?? "");
1281
+ results.push({ toolCallId, toolName, output: "", error });
1282
+ }
1274
1283
  }
1275
1284
  }
1276
1285
  return results;
1277
1286
  }
1287
+ function asString2(value) {
1288
+ return typeof value === "string" ? value : void 0;
1289
+ }
1278
1290
  function parseToolResultOutput(part) {
1279
1291
  const outputField = part.output;
1280
1292
  const resultField = part.result;
@@ -1296,10 +1308,67 @@ function parseToolResultOutput(part) {
1296
1308
  return { output: typeof outputField === "string" ? outputField : JSON.stringify(outputField) };
1297
1309
  }
1298
1310
  if (resultField !== void 0) {
1299
- return { output: typeof resultField === "string" ? resultField : JSON.stringify(resultField) };
1311
+ const output = typeof resultField === "string" ? resultField : JSON.stringify(resultField);
1312
+ const error = isPlainObject(resultField) ? asString2(resultField.error) : void 0;
1313
+ return { output, error };
1300
1314
  }
1301
1315
  return { output: "" };
1302
1316
  }
1317
+ function extractSystemPrompt(prompt) {
1318
+ if (!Array.isArray(prompt)) return null;
1319
+ const parts = [];
1320
+ for (const message of prompt) {
1321
+ const msg = message;
1322
+ if (msg.role === "system" && typeof msg.content === "string" && msg.content.trim()) {
1323
+ parts.push(msg.content);
1324
+ }
1325
+ }
1326
+ return parts.length > 0 ? parts.join("\n") : null;
1327
+ }
1328
+ function sanitizeSystemPrompt(prompt) {
1329
+ let result = prompt;
1330
+ result = result.replace(/^You are [Oo]pen[Cc]ode[,.].*$/gm, "");
1331
+ result = result.replace(/^Your name is opencode\s*$/gm, "");
1332
+ result = result.replace(
1333
+ /If the user asks for help or wants to give feedback[\s\S]*?https:\/\/github\.com\/anomalyco\/opencode\s*/g,
1334
+ ""
1335
+ );
1336
+ result = result.replace(
1337
+ /When the user directly asks about OpenCode[\s\S]*?https:\/\/opencode\.ai\/docs\s*/g,
1338
+ ""
1339
+ );
1340
+ result = result.replace(/https:\/\/github\.com\/anomalyco\/opencode\S*/g, "");
1341
+ result = result.replace(/https:\/\/opencode\.ai\S*/g, "");
1342
+ result = result.replace(/\bOpenCode\b/g, "GitLab Duo");
1343
+ result = result.replace(/\bopencode\b/g, "GitLab Duo");
1344
+ result = result.replace(/The exact model ID is GitLab Duo\//g, "The exact model ID is ");
1345
+ result = result.replace(/\n{3,}/g, "\n\n");
1346
+ return result.trim();
1347
+ }
1348
+ function extractAgentReminders(prompt) {
1349
+ if (!Array.isArray(prompt)) return [];
1350
+ let textParts = [];
1351
+ for (let i = prompt.length - 1; i >= 0; i--) {
1352
+ const message = prompt[i];
1353
+ if (message?.role !== "user" || !Array.isArray(message.content)) continue;
1354
+ textParts = message.content.filter((p) => p.type === "text");
1355
+ if (textParts.length > 0) break;
1356
+ }
1357
+ if (textParts.length === 0) return [];
1358
+ const reminders = [];
1359
+ for (const part of textParts) {
1360
+ if (!part.text) continue;
1361
+ const text2 = String(part.text);
1362
+ if (part.synthetic) {
1363
+ const trimmed = text2.trim();
1364
+ if (trimmed.length > 0) reminders.push(trimmed);
1365
+ continue;
1366
+ }
1367
+ const matches = text2.match(/<system-reminder>[\s\S]*?<\/system-reminder>/g);
1368
+ if (matches) reminders.push(...matches);
1369
+ }
1370
+ return reminders;
1371
+ }
1303
1372
  function isPlainObject(value) {
1304
1373
  return typeof value === "object" && value !== null && !Array.isArray(value);
1305
1374
  }
@@ -1308,14 +1377,14 @@ function isPlainObject(value) {
1308
1377
  function mapDuoToolRequest(toolName, args) {
1309
1378
  switch (toolName) {
1310
1379
  case "list_dir": {
1311
- const directory = asString2(args.directory) ?? ".";
1380
+ const directory = asString3(args.directory) ?? ".";
1312
1381
  return {
1313
1382
  toolName: "bash",
1314
1383
  args: { command: `ls -la ${shellQuote(directory)}`, description: "List directory contents", workdir: "." }
1315
1384
  };
1316
1385
  }
1317
1386
  case "read_file": {
1318
- const filePath = asString2(args.file_path) ?? asString2(args.filepath) ?? asString2(args.filePath) ?? asString2(args.path);
1387
+ const filePath = asString3(args.file_path) ?? asString3(args.filepath) ?? asString3(args.filePath) ?? asString3(args.path);
1319
1388
  if (!filePath) return { toolName, args };
1320
1389
  const mapped = { filePath };
1321
1390
  if (typeof args.offset === "number") mapped.offset = args.offset;
@@ -1328,27 +1397,27 @@ function mapDuoToolRequest(toolName, args) {
1328
1397
  return filePaths.map((fp) => ({ toolName: "read", args: { filePath: fp } }));
1329
1398
  }
1330
1399
  case "create_file_with_contents": {
1331
- const filePath = asString2(args.file_path);
1332
- const content = asString2(args.contents);
1400
+ const filePath = asString3(args.file_path);
1401
+ const content = asString3(args.contents);
1333
1402
  if (!filePath || content === void 0) return { toolName, args };
1334
1403
  return { toolName: "write", args: { filePath, content } };
1335
1404
  }
1336
1405
  case "edit_file": {
1337
- const filePath = asString2(args.file_path);
1338
- const oldString = asString2(args.old_str);
1339
- const newString = asString2(args.new_str);
1406
+ const filePath = asString3(args.file_path);
1407
+ const oldString = asString3(args.old_str);
1408
+ const newString = asString3(args.new_str);
1340
1409
  if (!filePath || oldString === void 0 || newString === void 0) return { toolName, args };
1341
1410
  return { toolName: "edit", args: { filePath, oldString, newString } };
1342
1411
  }
1343
1412
  case "find_files": {
1344
- const pattern = asString2(args.name_pattern);
1413
+ const pattern = asString3(args.name_pattern);
1345
1414
  if (!pattern) return { toolName, args };
1346
1415
  return { toolName: "glob", args: { pattern } };
1347
1416
  }
1348
1417
  case "grep": {
1349
- const pattern = asString2(args.pattern);
1418
+ const pattern = asString3(args.pattern);
1350
1419
  if (!pattern) return { toolName, args };
1351
- const searchDir = asString2(args.search_directory);
1420
+ const searchDir = asString3(args.search_directory);
1352
1421
  const caseInsensitive = Boolean(args.case_insensitive);
1353
1422
  const normalizedPattern = caseInsensitive && !pattern.startsWith("(?i)") ? `(?i)${pattern}` : pattern;
1354
1423
  const mapped = { pattern: normalizedPattern };
@@ -1356,32 +1425,32 @@ function mapDuoToolRequest(toolName, args) {
1356
1425
  return { toolName: "grep", args: mapped };
1357
1426
  }
1358
1427
  case "mkdir": {
1359
- const directory = asString2(args.directory_path);
1428
+ const directory = asString3(args.directory_path);
1360
1429
  if (!directory) return { toolName, args };
1361
1430
  return { toolName: "bash", args: { command: `mkdir -p ${shellQuote(directory)}`, description: "Create directory", workdir: "." } };
1362
1431
  }
1363
1432
  case "shell_command": {
1364
- const command = asString2(args.command);
1433
+ const command = asString3(args.command);
1365
1434
  if (!command) return { toolName, args };
1366
1435
  return { toolName: "bash", args: { command, description: "Run shell command", workdir: "." } };
1367
1436
  }
1368
1437
  case "run_command": {
1369
- const program = asString2(args.program);
1438
+ const program = asString3(args.program);
1370
1439
  if (program) {
1371
1440
  const parts = [shellQuote(program)];
1372
1441
  if (Array.isArray(args.flags)) parts.push(...args.flags.map((f) => shellQuote(String(f))));
1373
1442
  if (Array.isArray(args.arguments)) parts.push(...args.arguments.map((a) => shellQuote(String(a))));
1374
1443
  return { toolName: "bash", args: { command: parts.join(" "), description: "Run command", workdir: "." } };
1375
1444
  }
1376
- const command = asString2(args.command);
1445
+ const command = asString3(args.command);
1377
1446
  if (!command) return { toolName, args };
1378
1447
  return { toolName: "bash", args: { command, description: "Run command", workdir: "." } };
1379
1448
  }
1380
1449
  case "run_git_command": {
1381
- const command = asString2(args.command);
1450
+ const command = asString3(args.command);
1382
1451
  if (!command) return { toolName, args };
1383
1452
  const rawArgs = args.args;
1384
- const extraArgs = Array.isArray(rawArgs) ? rawArgs.map((v) => shellQuote(String(v))).join(" ") : asString2(rawArgs);
1453
+ const extraArgs = Array.isArray(rawArgs) ? rawArgs.map((v) => shellQuote(String(v))).join(" ") : asString3(rawArgs);
1385
1454
  const gitCmd = extraArgs ? `git ${shellQuote(command)} ${extraArgs}` : `git ${shellQuote(command)}`;
1386
1455
  return { toolName: "bash", args: { command: gitCmd, description: "Run git command", workdir: "." } };
1387
1456
  }
@@ -1389,7 +1458,7 @@ function mapDuoToolRequest(toolName, args) {
1389
1458
  return { toolName, args };
1390
1459
  }
1391
1460
  }
1392
- function asString2(value) {
1461
+ function asString3(value) {
1393
1462
  return typeof value === "string" ? value : void 0;
1394
1463
  }
1395
1464
  function asStringArray2(value) {
@@ -1421,6 +1490,85 @@ function readProviderBlock(options) {
1421
1490
  return void 0;
1422
1491
  }
1423
1492
 
1493
+ // src/provider/system-context.ts
1494
+ import os2 from "os";
1495
+ function buildSystemContext() {
1496
+ const platform = os2.platform();
1497
+ const arch = os2.arch();
1498
+ return [
1499
+ {
1500
+ category: "os_information",
1501
+ content: `<os><platform>${platform}</platform><architecture>${arch}</architecture></os>`,
1502
+ id: "os_information",
1503
+ metadata: JSON.stringify({
1504
+ title: "Operating System",
1505
+ enabled: true,
1506
+ subType: "os"
1507
+ })
1508
+ },
1509
+ {
1510
+ category: "user_rule",
1511
+ content: SYSTEM_RULES,
1512
+ id: "user_rules",
1513
+ metadata: JSON.stringify({
1514
+ title: "System Rules",
1515
+ enabled: true,
1516
+ subType: "user_rule"
1517
+ })
1518
+ }
1519
+ ];
1520
+ }
1521
+ var SYSTEM_RULES = `<system-reminder>
1522
+ You MUST follow ALL the rules in this block strictly.
1523
+
1524
+ <tool_orchestration>
1525
+ PARALLEL EXECUTION:
1526
+ - When gathering information, plan all needed searches upfront and execute
1527
+ them together using multiple tool calls in the same turn where possible.
1528
+ - Read multiple related files together rather than one at a time.
1529
+ - Patterns: grep + find_files together, read_file for multiple files together.
1530
+
1531
+ SEQUENTIAL EXECUTION (only when output depends on previous step):
1532
+ - Read a file BEFORE editing it (always).
1533
+ - Check dependencies BEFORE importing them.
1534
+ - Run tests AFTER making changes.
1535
+
1536
+ READ BEFORE WRITE:
1537
+ - Always read existing files before modifying them to understand context.
1538
+ - Check for existing patterns (naming, imports, error handling) and match them.
1539
+ - Verify the exact content to replace when using edit_file.
1540
+
1541
+ ERROR HANDLING:
1542
+ - If a tool fails, analyze the error before retrying.
1543
+ - If a shell command fails, check the error output and adapt.
1544
+ - Do not repeat the same failing operation without changes.
1545
+ </tool_orchestration>
1546
+
1547
+ <development_workflow>
1548
+ For software development tasks, follow this workflow:
1549
+
1550
+ 1. UNDERSTAND: Read relevant files, explore the codebase structure
1551
+ 2. PLAN: Break down the task into clear steps
1552
+ 3. IMPLEMENT: Make changes methodically, one step at a time
1553
+ 4. VERIFY: Run tests, type-checking, or build to validate changes
1554
+ 5. COMPLETE: Summarize what was accomplished
1555
+
1556
+ CODE QUALITY:
1557
+ - Match existing code style and patterns in the project
1558
+ - Write immediately executable code (no TODOs or placeholders)
1559
+ - Prefer editing existing files over creating new ones
1560
+ - Use the project's established error handling patterns
1561
+ </development_workflow>
1562
+
1563
+ <communication>
1564
+ - Be concise and direct. Responses appear in a chat panel.
1565
+ - Focus on practical solutions over theoretical discussion.
1566
+ - When unable to complete a request, explain the limitation briefly and
1567
+ provide alternatives.
1568
+ - Use active language: "Analyzing...", "Searching..." instead of "Let me..."
1569
+ </communication>
1570
+ </system-reminder>`;
1571
+
1424
1572
  // src/provider/duo-workflow-model.ts
1425
1573
  var sessions = /* @__PURE__ */ new Map();
1426
1574
  var UNKNOWN_USAGE = {
@@ -1442,6 +1590,8 @@ var DuoWorkflowModel = class {
1442
1590
  #sentToolCallIds = /* @__PURE__ */ new Set();
1443
1591
  #lastSentGoal = null;
1444
1592
  #stateSessionId;
1593
+ #agentMode;
1594
+ #agentModeReminder;
1445
1595
  constructor(modelId, client, cwd) {
1446
1596
  this.modelId = modelId;
1447
1597
  this.#client = client;
@@ -1481,6 +1631,8 @@ var DuoWorkflowModel = class {
1481
1631
  this.#multiCallGroups.clear();
1482
1632
  this.#sentToolCallIds.clear();
1483
1633
  this.#lastSentGoal = null;
1634
+ this.#agentMode = void 0;
1635
+ this.#agentModeReminder = void 0;
1484
1636
  this.#stateSessionId = sessionID;
1485
1637
  }
1486
1638
  const model = this;
@@ -1491,6 +1643,15 @@ var DuoWorkflowModel = class {
1491
1643
  const onAbort = () => session.abort();
1492
1644
  options.abortSignal?.addEventListener("abort", onAbort, { once: true });
1493
1645
  try {
1646
+ if (!session.hasStarted) {
1647
+ model.#sentToolCallIds.clear();
1648
+ for (const r of toolResults) {
1649
+ if (!model.#pendingToolRequests.has(r.toolCallId)) {
1650
+ model.#sentToolCallIds.add(r.toolCallId);
1651
+ }
1652
+ }
1653
+ model.#lastSentGoal = null;
1654
+ }
1494
1655
  const freshResults = toolResults.filter(
1495
1656
  (r) => !model.#sentToolCallIds.has(r.toolCallId)
1496
1657
  );
@@ -1530,7 +1691,41 @@ var DuoWorkflowModel = class {
1530
1691
  if (!sentToolResults && isNewGoal) {
1531
1692
  await session.ensureConnected(goal);
1532
1693
  if (!session.hasStarted) {
1533
- session.sendStartRequest(goal);
1694
+ const extraContext = [];
1695
+ extraContext.push(...buildSystemContext());
1696
+ const systemPrompt = extractSystemPrompt(options.prompt);
1697
+ if (systemPrompt) {
1698
+ extraContext.push({
1699
+ category: "agent_context",
1700
+ content: sanitizeSystemPrompt(systemPrompt),
1701
+ id: "agent_system_prompt",
1702
+ metadata: JSON.stringify({
1703
+ title: "Agent System Prompt",
1704
+ enabled: true,
1705
+ subType: "system_prompt"
1706
+ })
1707
+ });
1708
+ }
1709
+ const agentReminders = extractAgentReminders(options.prompt);
1710
+ const modeReminder = detectLatestModeReminder(agentReminders);
1711
+ if (modeReminder) {
1712
+ model.#agentMode = modeReminder.mode;
1713
+ model.#agentModeReminder = modeReminder.reminder;
1714
+ }
1715
+ const remindersForContext = buildReminderContext(agentReminders, model.#agentModeReminder);
1716
+ if (remindersForContext.length > 0) {
1717
+ extraContext.push({
1718
+ category: "agent_context",
1719
+ content: sanitizeSystemPrompt(remindersForContext.join("\n\n")),
1720
+ id: "agent_reminders",
1721
+ metadata: JSON.stringify({
1722
+ title: "Agent Reminders",
1723
+ enabled: true,
1724
+ subType: "agent_reminders"
1725
+ })
1726
+ });
1727
+ }
1728
+ session.sendStartRequest(goal, extraContext);
1534
1729
  }
1535
1730
  model.#lastSentGoal = goal;
1536
1731
  }
@@ -1638,6 +1833,32 @@ var DuoWorkflowModel = class {
1638
1833
  function sessionKey(instanceUrl, modelId, sessionID) {
1639
1834
  return `${instanceUrl}::${modelId}::${sessionID}`;
1640
1835
  }
1836
+ function detectLatestModeReminder(reminders) {
1837
+ let latest;
1838
+ for (const reminder of reminders) {
1839
+ const classification = classifyModeReminder(reminder);
1840
+ if (classification === "other") continue;
1841
+ latest = { mode: classification, reminder };
1842
+ }
1843
+ return latest;
1844
+ }
1845
+ function buildReminderContext(reminders, modeReminder) {
1846
+ const nonModeReminders = reminders.filter(
1847
+ (r) => classifyModeReminder(r) === "other"
1848
+ );
1849
+ if (!modeReminder) return nonModeReminders;
1850
+ return [...nonModeReminders, modeReminder];
1851
+ }
1852
+ function classifyModeReminder(reminder) {
1853
+ const text2 = reminder.toLowerCase();
1854
+ if (text2.includes("operational mode has changed from build to plan")) return "plan";
1855
+ if (text2.includes("operational mode has changed from plan to build")) return "build";
1856
+ if (text2.includes("you are no longer in read-only mode")) return "build";
1857
+ if (text2.includes("you are now in read-only mode")) return "plan";
1858
+ if (text2.includes("you are in read-only mode")) return "plan";
1859
+ if (text2.includes("you are permitted to make file changes")) return "build";
1860
+ return "other";
1861
+ }
1641
1862
 
1642
1863
  // src/provider/index.ts
1643
1864
  function createFallbackProvider(input = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-gitlab-duo-agentic",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "OpenCode plugin and provider for GitLab Duo Agentic workflows",
5
5
  "license": "MIT",
6
6
  "type": "module",