opencode-gitlab-duo-agentic 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -24
- package/dist/index.js +55 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# opencode-gitlab-duo-agentic
|
|
2
2
|
|
|
3
|
-
OpenCode plugin
|
|
3
|
+
OpenCode plugin for GitLab Duo Agentic. It registers the provider, discovers models from the GitLab API, and exposes file-reading tools.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Setup
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
1. Export your GitLab token:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
export GITLAB_TOKEN=glpat-...
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. Add the plugin to `opencode.json`:
|
|
8
14
|
|
|
9
15
|
```json
|
|
10
16
|
{
|
|
@@ -13,40 +19,40 @@ Add the plugin to your OpenCode config:
|
|
|
13
19
|
}
|
|
14
20
|
```
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
3. Run `opencode`. The provider, models, and tools are registered automatically.
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
`GITLAB_INSTANCE_URL` defaults to `https://gitlab.com`. Set it only for self-managed GitLab.
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
export GITLAB_TOKEN=glpat-...
|
|
22
|
-
export GITLAB_INSTANCE_URL=https://gitlab.com
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
`GITLAB_INSTANCE_URL` is optional and defaults to `https://gitlab.com`.
|
|
26
|
+
## Provider options
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
Override defaults in `opencode.json` under `provider.gitlab-duo-agentic.options`.
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
| Option | Type | Default | Description |
|
|
31
|
+
|--------|------|---------|-------------|
|
|
32
|
+
| `instanceUrl` | string | `GITLAB_INSTANCE_URL` or `https://gitlab.com` | GitLab instance URL |
|
|
33
|
+
| `apiKey` | string | `GITLAB_TOKEN` | Personal access token |
|
|
34
|
+
| `sendSystemContext` | boolean | `true` | Send system context to Duo |
|
|
35
|
+
| `enableMcp` | boolean | `true` | Enable MCP tools |
|
|
36
|
+
| `systemRules` | string | `""` | Inline system rules |
|
|
37
|
+
| `systemRulesPath` | string | `""` | Path to a system rules file |
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
2. `models.json` file resolution:
|
|
33
|
-
- `options.modelsPath` in provider config
|
|
34
|
-
- `GITLAB_DUO_MODELS_PATH`
|
|
35
|
-
- `models.json` found by walking upward from `process.cwd()`
|
|
36
|
-
3. Fallback default model `duo-agentic`
|
|
39
|
+
## Model discovery
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
Models are discovered in this order:
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
1. Local cache (TTL: 24h)
|
|
44
|
+
2. Live fetch from GitLab GraphQL API
|
|
45
|
+
3. Stale cache (if live fetch fails)
|
|
46
|
+
4. `models.json` on disk
|
|
47
|
+
5. Default `duo-agentic` model
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
Cache is stored in `~/.cache/opencode/` (or `XDG_CACHE_HOME`). Override TTL with `GITLAB_DUO_MODELS_CACHE_TTL` (seconds).
|
|
44
50
|
|
|
45
51
|
## Development
|
|
46
52
|
|
|
47
53
|
```bash
|
|
48
54
|
npm install
|
|
49
|
-
npm run typecheck
|
|
50
55
|
npm run build
|
|
56
|
+
npm run typecheck
|
|
51
57
|
npm run pack:check
|
|
52
58
|
```
|
package/dist/index.js
CHANGED
|
@@ -1225,6 +1225,14 @@ var GitLabDuoAgenticLanguageModel = class {
|
|
|
1225
1225
|
const workflowType = "chat";
|
|
1226
1226
|
const promptText = extractLastUserText(options.prompt);
|
|
1227
1227
|
const toolResults = extractToolResults(options.prompt);
|
|
1228
|
+
console.warn("[duo-debug] doStream called", {
|
|
1229
|
+
promptText: promptText?.slice(0, 80),
|
|
1230
|
+
toolResultCount: toolResults.length,
|
|
1231
|
+
hasStarted: this.#runtime.hasStarted,
|
|
1232
|
+
lastSentPrompt: this.#lastSentPrompt?.slice(0, 80) ?? null,
|
|
1233
|
+
pendingToolRequests: this.#pendingToolRequests.size,
|
|
1234
|
+
sentToolCallIds: this.#sentToolCallIds.size
|
|
1235
|
+
});
|
|
1228
1236
|
this.#runtime.resetMapperState();
|
|
1229
1237
|
if (!this.#runtime.hasStarted) {
|
|
1230
1238
|
this.#sentToolCallIds.clear();
|
|
@@ -1234,14 +1242,18 @@ var GitLabDuoAgenticLanguageModel = class {
|
|
|
1234
1242
|
}
|
|
1235
1243
|
}
|
|
1236
1244
|
this.#lastSentPrompt = null;
|
|
1245
|
+
console.warn("[duo-debug] hasStarted=false => reset sentToolCallIds, lastSentPrompt=null");
|
|
1237
1246
|
}
|
|
1238
1247
|
const freshToolResults = toolResults.filter((r) => !this.#sentToolCallIds.has(r.toolCallId));
|
|
1248
|
+
console.warn("[duo-debug] freshToolResults:", freshToolResults.length);
|
|
1239
1249
|
const modelRef = this.modelId === GITLAB_DUO_DEFAULT_MODEL_ID ? void 0 : this.modelId;
|
|
1240
1250
|
this.#runtime.setSelectedModelIdentifier(modelRef);
|
|
1241
1251
|
await this.#runtime.ensureConnected(promptText || "", workflowType);
|
|
1252
|
+
console.warn("[duo-debug] ensureConnected returned OK");
|
|
1242
1253
|
const mcpTools = this.#options.enableMcp === false ? [] : buildMcpTools(options);
|
|
1243
1254
|
const toolContext = buildToolContext(mcpTools);
|
|
1244
1255
|
const isNewUserMessage = promptText != null && promptText !== this.#lastSentPrompt;
|
|
1256
|
+
console.warn("[duo-debug] isNewUserMessage:", isNewUserMessage, "promptText != null:", promptText != null, "promptText !== lastSentPrompt:", promptText !== this.#lastSentPrompt);
|
|
1245
1257
|
let sentToolResults = false;
|
|
1246
1258
|
if (freshToolResults.length > 0) {
|
|
1247
1259
|
for (const result of freshToolResults) {
|
|
@@ -1288,6 +1300,7 @@ var GitLabDuoAgenticLanguageModel = class {
|
|
|
1288
1300
|
this.#pendingToolRequests.delete(result.toolCallId);
|
|
1289
1301
|
}
|
|
1290
1302
|
}
|
|
1303
|
+
console.warn("[duo-debug] startRequest gate: !sentToolResults:", !sentToolResults, "isNewUserMessage:", isNewUserMessage, "=> will send:", !sentToolResults && isNewUserMessage);
|
|
1291
1304
|
if (!sentToolResults && isNewUserMessage) {
|
|
1292
1305
|
const extraContext = [];
|
|
1293
1306
|
if (toolContext) extraContext.push(toolContext);
|
|
@@ -1349,6 +1362,7 @@ var GitLabDuoAgenticLanguageModel = class {
|
|
|
1349
1362
|
}
|
|
1350
1363
|
});
|
|
1351
1364
|
}
|
|
1365
|
+
console.warn("[duo-debug] >>> calling sendStartRequest", { goal: promptText?.slice(0, 80), hasStarted: this.#runtime.hasStarted, extraContextCount: extraContext.length });
|
|
1352
1366
|
this.#runtime.sendStartRequest(
|
|
1353
1367
|
promptText,
|
|
1354
1368
|
workflowType,
|
|
@@ -1357,11 +1371,15 @@ var GitLabDuoAgenticLanguageModel = class {
|
|
|
1357
1371
|
extraContext
|
|
1358
1372
|
);
|
|
1359
1373
|
this.#lastSentPrompt = promptText;
|
|
1374
|
+
console.warn("[duo-debug] >>> sendStartRequest completed, lastSentPrompt set");
|
|
1360
1375
|
this.#usageEstimator.addInputChars(promptText);
|
|
1361
1376
|
for (const ctx of extraContext) {
|
|
1362
1377
|
if (ctx.content) this.#usageEstimator.addInputChars(ctx.content);
|
|
1363
1378
|
}
|
|
1379
|
+
} else {
|
|
1380
|
+
console.warn("[duo-debug] SKIPPED sendStartRequest (sentToolResults:", sentToolResults, "isNewUserMessage:", isNewUserMessage, ")");
|
|
1364
1381
|
}
|
|
1382
|
+
console.warn("[duo-debug] creating event stream iterator");
|
|
1365
1383
|
const iterator = this.#mapEventsToStream(this.#runtime.getEventStream());
|
|
1366
1384
|
const stream = asyncIteratorToReadableStream(iterator);
|
|
1367
1385
|
return {
|
|
@@ -1374,9 +1392,13 @@ var GitLabDuoAgenticLanguageModel = class {
|
|
|
1374
1392
|
async *#mapEventsToStream(events) {
|
|
1375
1393
|
const state = { textStarted: false };
|
|
1376
1394
|
const estimator = this.#usageEstimator;
|
|
1395
|
+
let eventCount = 0;
|
|
1396
|
+
console.warn("[duo-debug] #mapEventsToStream: starting iteration");
|
|
1377
1397
|
yield { type: "stream-start", warnings: [] };
|
|
1378
1398
|
try {
|
|
1379
1399
|
for await (const event of events) {
|
|
1400
|
+
eventCount++;
|
|
1401
|
+
console.warn("[duo-debug] #mapEventsToStream event #" + eventCount + ":", event.type, event.type === "TEXT_CHUNK" ? "(len=" + event.content.length + ")" : "");
|
|
1380
1402
|
if (event.type === "TEXT_CHUNK") {
|
|
1381
1403
|
if (event.content.length > 0) {
|
|
1382
1404
|
estimator.addOutputChars(event.content);
|
|
@@ -1422,9 +1444,11 @@ var GitLabDuoAgenticLanguageModel = class {
|
|
|
1422
1444
|
}
|
|
1423
1445
|
}
|
|
1424
1446
|
} catch (streamErr) {
|
|
1447
|
+
console.warn("[duo-debug] #mapEventsToStream: caught error after", eventCount, "events:", streamErr instanceof Error ? streamErr.message : String(streamErr));
|
|
1425
1448
|
yield { type: "error", error: streamErr instanceof Error ? streamErr : new Error(String(streamErr)) };
|
|
1426
1449
|
return;
|
|
1427
1450
|
}
|
|
1451
|
+
console.warn("[duo-debug] #mapEventsToStream: loop ended normally after", eventCount, "events => yielding finish:stop");
|
|
1428
1452
|
yield { type: "finish", finishReason: "stop", usage: this.#currentUsage };
|
|
1429
1453
|
}
|
|
1430
1454
|
// ---------------------------------------------------------------------------
|
|
@@ -1871,14 +1895,26 @@ var GitLabAgenticRuntime = class {
|
|
|
1871
1895
|
// Connection lifecycle
|
|
1872
1896
|
// ---------------------------------------------------------------------------
|
|
1873
1897
|
async ensureConnected(goal, workflowType) {
|
|
1898
|
+
console.warn("[duo-debug] ensureConnected called", {
|
|
1899
|
+
hasStream: !!this.#stream,
|
|
1900
|
+
hasWorkflowId: !!this.#workflowId,
|
|
1901
|
+
workflowId: this.#workflowId?.slice(0, 12),
|
|
1902
|
+
hasQueue: !!this.#queue,
|
|
1903
|
+
startRequestSent: this.#startRequestSent
|
|
1904
|
+
});
|
|
1874
1905
|
if (this.#stream && this.#workflowId && this.#queue) {
|
|
1906
|
+
console.warn("[duo-debug] ensureConnected: short-circuit (already connected)");
|
|
1875
1907
|
return;
|
|
1876
1908
|
}
|
|
1877
1909
|
if (!this.#containerParams) {
|
|
1878
1910
|
this.#containerParams = await this.#resolveContainerParams();
|
|
1879
1911
|
}
|
|
1880
1912
|
if (!this.#workflowId) {
|
|
1913
|
+
console.warn("[duo-debug] ensureConnected: creating new workflow");
|
|
1881
1914
|
this.#workflowId = await this.#createWorkflow(goal, workflowType);
|
|
1915
|
+
console.warn("[duo-debug] ensureConnected: workflow created:", this.#workflowId?.slice(0, 12));
|
|
1916
|
+
} else {
|
|
1917
|
+
console.warn("[duo-debug] ensureConnected: reusing existing workflowId:", this.#workflowId?.slice(0, 12));
|
|
1882
1918
|
}
|
|
1883
1919
|
const token = await this.#dependencies.workflowService.getWorkflowToken(
|
|
1884
1920
|
this.#options.instanceUrl,
|
|
@@ -1886,15 +1922,19 @@ var GitLabAgenticRuntime = class {
|
|
|
1886
1922
|
workflowType
|
|
1887
1923
|
);
|
|
1888
1924
|
this.#workflowToken = token;
|
|
1925
|
+
console.warn("[duo-debug] ensureConnected: got workflow token");
|
|
1889
1926
|
const MAX_LOCK_RETRIES = 3;
|
|
1890
1927
|
const LOCK_RETRY_DELAY_MS = 3e3;
|
|
1891
1928
|
for (let attempt = 1; attempt <= MAX_LOCK_RETRIES; attempt++) {
|
|
1892
1929
|
this.#queue = new AsyncQueue();
|
|
1893
1930
|
try {
|
|
1931
|
+
console.warn("[duo-debug] ensureConnected: connecting WebSocket (attempt", attempt, ")");
|
|
1894
1932
|
await this.#connectWebSocket();
|
|
1933
|
+
console.warn("[duo-debug] ensureConnected: WebSocket connected OK");
|
|
1895
1934
|
return;
|
|
1896
1935
|
} catch (err2) {
|
|
1897
1936
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
1937
|
+
console.warn("[duo-debug] ensureConnected: WebSocket error:", msg);
|
|
1898
1938
|
if ((msg.includes("1013") || msg.includes("lock")) && attempt < MAX_LOCK_RETRIES) {
|
|
1899
1939
|
this.#resetStreamState();
|
|
1900
1940
|
await this.#dependencies.clock.sleep(LOCK_RETRY_DELAY_MS);
|
|
@@ -1939,7 +1979,13 @@ var GitLabAgenticRuntime = class {
|
|
|
1939
1979
|
preapproved_tools: preapprovedTools
|
|
1940
1980
|
}
|
|
1941
1981
|
};
|
|
1942
|
-
|
|
1982
|
+
console.warn("[duo-debug] sendStartRequest: writing to stream", {
|
|
1983
|
+
workflowId: this.#workflowId?.slice(0, 12),
|
|
1984
|
+
goal: goal?.slice(0, 80),
|
|
1985
|
+
contextCount: additionalContext.length
|
|
1986
|
+
});
|
|
1987
|
+
const writeResult = this.#stream.write(startRequest);
|
|
1988
|
+
console.warn("[duo-debug] sendStartRequest: write() returned:", writeResult);
|
|
1943
1989
|
this.#startRequestSent = true;
|
|
1944
1990
|
}
|
|
1945
1991
|
sendToolResponse(requestId, response, responseType) {
|
|
@@ -2028,12 +2074,14 @@ var GitLabAgenticRuntime = class {
|
|
|
2028
2074
|
#bindStream(stream, queue) {
|
|
2029
2075
|
const now = () => this.#dependencies.clock.now();
|
|
2030
2076
|
const closeWithError = (message) => {
|
|
2077
|
+
console.warn("[duo-debug] stream closeWithError:", message);
|
|
2031
2078
|
queue.push({ type: "ERROR", message, timestamp: now() });
|
|
2032
2079
|
queue.close();
|
|
2033
2080
|
this.#resetStreamState();
|
|
2034
2081
|
};
|
|
2035
2082
|
const handleAction = async (action) => {
|
|
2036
2083
|
if (action.newCheckpoint) {
|
|
2084
|
+
console.warn("[duo-debug] stream data: newCheckpoint status=", action.newCheckpoint.status, "goal=", action.newCheckpoint.goal?.slice(0, 40));
|
|
2037
2085
|
const duoEvent = {
|
|
2038
2086
|
checkpoint: action.newCheckpoint.checkpoint,
|
|
2039
2087
|
errors: action.newCheckpoint.errors || [],
|
|
@@ -2041,6 +2089,7 @@ var GitLabAgenticRuntime = class {
|
|
|
2041
2089
|
workflowStatus: action.newCheckpoint.status
|
|
2042
2090
|
};
|
|
2043
2091
|
const events = await this.#mapper.mapWorkflowEvent(duoEvent);
|
|
2092
|
+
console.warn("[duo-debug] mapper produced", events.length, "events:", events.map((e) => e.type));
|
|
2044
2093
|
for (const event of events) {
|
|
2045
2094
|
queue.push(event);
|
|
2046
2095
|
}
|
|
@@ -2048,6 +2097,7 @@ var GitLabAgenticRuntime = class {
|
|
|
2048
2097
|
}
|
|
2049
2098
|
const toolRequest = mapWorkflowActionToToolRequest(action);
|
|
2050
2099
|
if (toolRequest) {
|
|
2100
|
+
console.warn("[duo-debug] stream data: toolRequest", toolRequest.toolName);
|
|
2051
2101
|
queue.push({
|
|
2052
2102
|
type: "TOOL_REQUEST",
|
|
2053
2103
|
...toolRequest,
|
|
@@ -2055,6 +2105,7 @@ var GitLabAgenticRuntime = class {
|
|
|
2055
2105
|
});
|
|
2056
2106
|
return;
|
|
2057
2107
|
}
|
|
2108
|
+
console.warn("[duo-debug] stream data: unhandled action", Object.keys(action));
|
|
2058
2109
|
};
|
|
2059
2110
|
stream.on("data", (action) => {
|
|
2060
2111
|
void handleAction(action).catch((error) => {
|
|
@@ -2063,9 +2114,11 @@ var GitLabAgenticRuntime = class {
|
|
|
2063
2114
|
});
|
|
2064
2115
|
});
|
|
2065
2116
|
stream.on("error", (err2) => {
|
|
2117
|
+
console.warn("[duo-debug] stream error:", err2.message);
|
|
2066
2118
|
closeWithError(err2.message);
|
|
2067
2119
|
});
|
|
2068
2120
|
stream.on("end", () => {
|
|
2121
|
+
console.warn("[duo-debug] stream end => closing queue + resetStreamState");
|
|
2069
2122
|
queue.close();
|
|
2070
2123
|
this.#resetStreamState();
|
|
2071
2124
|
});
|
|
@@ -2087,6 +2140,7 @@ var GitLabAgenticRuntime = class {
|
|
|
2087
2140
|
this.#bindStream(stream, this.#queue);
|
|
2088
2141
|
}
|
|
2089
2142
|
#resetStreamState() {
|
|
2143
|
+
console.warn("[duo-debug] #resetStreamState called", { hadStream: !!this.#stream, hadQueue: !!this.#queue, workflowId: this.#workflowId?.slice(0, 12) });
|
|
2090
2144
|
this.#stream = void 0;
|
|
2091
2145
|
this.#queue = void 0;
|
|
2092
2146
|
this.#startRequestSent = false;
|