opencode-gitlab-duo-agentic 0.1.22 → 0.1.24
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 +135 -86
- package/package.json +1 -4
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import crypto from "crypto";
|
|
|
17
17
|
import fs2 from "fs/promises";
|
|
18
18
|
import os from "os";
|
|
19
19
|
import path2 from "path";
|
|
20
|
+
import { z } from "zod";
|
|
20
21
|
|
|
21
22
|
// src/gitlab/client.ts
|
|
22
23
|
var GitLabApiError = class extends Error {
|
|
@@ -190,6 +191,16 @@ function normalizeProjectPath(remotePath, instanceBasePath) {
|
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
// src/gitlab/models.ts
|
|
194
|
+
var CachePayloadSchema = z.object({
|
|
195
|
+
cachedAt: z.string(),
|
|
196
|
+
instanceUrl: z.string(),
|
|
197
|
+
models: z.array(
|
|
198
|
+
z.object({
|
|
199
|
+
id: z.string(),
|
|
200
|
+
name: z.string()
|
|
201
|
+
})
|
|
202
|
+
).min(1)
|
|
203
|
+
});
|
|
193
204
|
var QUERY = `query lsp_aiChatAvailableModels($rootNamespaceId: GroupID!) {
|
|
194
205
|
aiChatAvailableModels(rootNamespaceId: $rootNamespaceId) {
|
|
195
206
|
defaultModel { name ref }
|
|
@@ -251,8 +262,7 @@ function isStale(payload) {
|
|
|
251
262
|
async function readCache(cachePath) {
|
|
252
263
|
try {
|
|
253
264
|
const raw = await fs2.readFile(cachePath, "utf8");
|
|
254
|
-
const parsed = JSON.parse(raw);
|
|
255
|
-
if (!parsed.models?.length) return null;
|
|
265
|
+
const parsed = CachePayloadSchema.parse(JSON.parse(raw));
|
|
256
266
|
return parsed;
|
|
257
267
|
} catch {
|
|
258
268
|
return null;
|
|
@@ -286,13 +296,28 @@ function normalizeInstanceUrl(value) {
|
|
|
286
296
|
}
|
|
287
297
|
}
|
|
288
298
|
|
|
299
|
+
// src/gitlab/resolve-credentials.ts
|
|
300
|
+
function resolveCredentials(options = {}) {
|
|
301
|
+
const instanceUrl = normalizeInstanceUrl(options.instanceUrl ?? envInstanceUrl());
|
|
302
|
+
const token = firstNonEmptyString(options.apiKey, options.token) ?? envToken() ?? "";
|
|
303
|
+
return { instanceUrl, token };
|
|
304
|
+
}
|
|
305
|
+
function envToken() {
|
|
306
|
+
return process.env.GITLAB_TOKEN ?? process.env.GITLAB_OAUTH_TOKEN;
|
|
307
|
+
}
|
|
308
|
+
function firstNonEmptyString(...values) {
|
|
309
|
+
for (const v of values) {
|
|
310
|
+
if (typeof v === "string" && v.trim().length > 0) return v.trim();
|
|
311
|
+
}
|
|
312
|
+
return void 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
289
315
|
// src/plugin/config.ts
|
|
290
316
|
async function applyRuntimeConfig(config, directory) {
|
|
291
317
|
config.provider ??= {};
|
|
292
318
|
const current = config.provider[PROVIDER_ID] ?? {};
|
|
293
319
|
const options = current.options ?? {};
|
|
294
|
-
const instanceUrl =
|
|
295
|
-
const token = (typeof options.apiKey === "string" ? options.apiKey : void 0) ?? process.env.GITLAB_TOKEN ?? process.env.GITLAB_OAUTH_TOKEN ?? "";
|
|
320
|
+
const { instanceUrl, token } = resolveCredentials(options);
|
|
296
321
|
const available = await loadAvailableModels(instanceUrl, token, directory);
|
|
297
322
|
const modelIds = available.map((m) => m.id);
|
|
298
323
|
const models = toModelsConfig(available);
|
|
@@ -336,6 +361,7 @@ async function createPluginHooks(input) {
|
|
|
336
361
|
},
|
|
337
362
|
"chat.params": async (context, output) => {
|
|
338
363
|
if (!isGitLabProvider(context.model)) return;
|
|
364
|
+
if (isUtilityAgent(context.agent)) return;
|
|
339
365
|
output.options = {
|
|
340
366
|
...output.options,
|
|
341
367
|
workflowSessionID: context.sessionID
|
|
@@ -343,6 +369,7 @@ async function createPluginHooks(input) {
|
|
|
343
369
|
},
|
|
344
370
|
"chat.headers": async (context, output) => {
|
|
345
371
|
if (!isGitLabProvider(context.model)) return;
|
|
372
|
+
if (isUtilityAgent(context.agent)) return;
|
|
346
373
|
output.headers = {
|
|
347
374
|
...output.headers,
|
|
348
375
|
"x-opencode-session": context.sessionID
|
|
@@ -350,6 +377,11 @@ async function createPluginHooks(input) {
|
|
|
350
377
|
}
|
|
351
378
|
};
|
|
352
379
|
}
|
|
380
|
+
var UTILITY_AGENTS = /* @__PURE__ */ new Set(["title", "compaction"]);
|
|
381
|
+
function isUtilityAgent(agent) {
|
|
382
|
+
const name = typeof agent === "string" ? agent : agent.name;
|
|
383
|
+
return UTILITY_AGENTS.has(name);
|
|
384
|
+
}
|
|
353
385
|
function isGitLabProvider(model) {
|
|
354
386
|
if (model.api?.npm === "opencode-gitlab-duo-agentic") return true;
|
|
355
387
|
if (model.providerID === "gitlab" && model.api?.npm !== "@gitlab/gitlab-ai-provider") return true;
|
|
@@ -365,6 +397,25 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
365
397
|
// src/workflow/session.ts
|
|
366
398
|
import { randomUUID } from "crypto";
|
|
367
399
|
|
|
400
|
+
// src/utils/async-queue.ts
|
|
401
|
+
var AsyncQueue = class {
|
|
402
|
+
#values = [];
|
|
403
|
+
#waiters = [];
|
|
404
|
+
push(value) {
|
|
405
|
+
const waiter = this.#waiters.shift();
|
|
406
|
+
if (waiter) {
|
|
407
|
+
waiter(value);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
this.#values.push(value);
|
|
411
|
+
}
|
|
412
|
+
shift() {
|
|
413
|
+
const value = this.#values.shift();
|
|
414
|
+
if (value !== void 0) return Promise.resolve(value);
|
|
415
|
+
return new Promise((resolve) => this.#waiters.push(resolve));
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
368
419
|
// src/workflow/checkpoint.ts
|
|
369
420
|
function createCheckpointState() {
|
|
370
421
|
return {
|
|
@@ -464,6 +515,20 @@ var WORKFLOW_STATUS = {
|
|
|
464
515
|
PLAN_APPROVAL_REQUIRED: "PLAN_APPROVAL_REQUIRED",
|
|
465
516
|
TOOL_CALL_APPROVAL_REQUIRED: "TOOL_CALL_APPROVAL_REQUIRED"
|
|
466
517
|
};
|
|
518
|
+
function isCheckpointAction(action) {
|
|
519
|
+
return "newCheckpoint" in action && action.newCheckpoint != null;
|
|
520
|
+
}
|
|
521
|
+
var TURN_COMPLETE_STATUSES = /* @__PURE__ */ new Set([
|
|
522
|
+
WORKFLOW_STATUS.INPUT_REQUIRED,
|
|
523
|
+
WORKFLOW_STATUS.FINISHED,
|
|
524
|
+
WORKFLOW_STATUS.FAILED,
|
|
525
|
+
WORKFLOW_STATUS.STOPPED,
|
|
526
|
+
WORKFLOW_STATUS.PLAN_APPROVAL_REQUIRED,
|
|
527
|
+
WORKFLOW_STATUS.TOOL_CALL_APPROVAL_REQUIRED
|
|
528
|
+
]);
|
|
529
|
+
function isTurnComplete(status) {
|
|
530
|
+
return TURN_COMPLETE_STATUSES.has(status);
|
|
531
|
+
}
|
|
467
532
|
|
|
468
533
|
// src/workflow/websocket-client.ts
|
|
469
534
|
import WebSocket from "isomorphic-ws";
|
|
@@ -561,15 +626,18 @@ var WorkflowSession = class {
|
|
|
561
626
|
#client;
|
|
562
627
|
#tokenService;
|
|
563
628
|
#modelId;
|
|
629
|
+
#cwd;
|
|
564
630
|
#workflowId;
|
|
565
631
|
#projectPath;
|
|
566
632
|
#rootNamespaceId;
|
|
567
633
|
#checkpoint = createCheckpointState();
|
|
568
|
-
|
|
569
|
-
|
|
634
|
+
/** Mutex: serialises concurrent calls to runTurn so only one runs at a time. */
|
|
635
|
+
#turnLock = Promise.resolve();
|
|
636
|
+
constructor(client, modelId, cwd) {
|
|
570
637
|
this.#client = client;
|
|
571
638
|
this.#tokenService = new WorkflowTokenService(client);
|
|
572
639
|
this.#modelId = modelId;
|
|
640
|
+
this.#cwd = cwd;
|
|
573
641
|
}
|
|
574
642
|
get workflowId() {
|
|
575
643
|
return this.#workflowId;
|
|
@@ -580,9 +648,9 @@ var WorkflowSession = class {
|
|
|
580
648
|
this.#tokenService.clear();
|
|
581
649
|
}
|
|
582
650
|
async *runTurn(goal, abortSignal) {
|
|
583
|
-
await this.#
|
|
651
|
+
await this.#turnLock;
|
|
584
652
|
let resolve;
|
|
585
|
-
this.#
|
|
653
|
+
this.#turnLock = new Promise((r) => {
|
|
586
654
|
resolve = r;
|
|
587
655
|
});
|
|
588
656
|
const queue = new AsyncQueue();
|
|
@@ -684,7 +752,7 @@ var WorkflowSession = class {
|
|
|
684
752
|
}
|
|
685
753
|
async #loadProjectContext() {
|
|
686
754
|
if (this.#projectPath !== void 0) return;
|
|
687
|
-
const projectPath = await detectProjectPath(
|
|
755
|
+
const projectPath = await detectProjectPath(this.#cwd, this.#client.instanceUrl);
|
|
688
756
|
this.#projectPath = projectPath;
|
|
689
757
|
if (!projectPath) return;
|
|
690
758
|
try {
|
|
@@ -695,12 +763,6 @@ var WorkflowSession = class {
|
|
|
695
763
|
}
|
|
696
764
|
}
|
|
697
765
|
};
|
|
698
|
-
function isCheckpointAction(action) {
|
|
699
|
-
return Boolean(action.newCheckpoint);
|
|
700
|
-
}
|
|
701
|
-
function isTurnComplete(status) {
|
|
702
|
-
return status === WORKFLOW_STATUS.INPUT_REQUIRED || status === WORKFLOW_STATUS.FINISHED || status === WORKFLOW_STATUS.FAILED || status === WORKFLOW_STATUS.STOPPED || status === WORKFLOW_STATUS.PLAN_APPROVAL_REQUIRED || status === WORKFLOW_STATUS.TOOL_CALL_APPROVAL_REQUIRED;
|
|
703
|
-
}
|
|
704
766
|
function buildWebSocketUrl(instanceUrl, modelId) {
|
|
705
767
|
const base = new URL(instanceUrl.endsWith("/") ? instanceUrl : `${instanceUrl}/`);
|
|
706
768
|
const url = new URL("api/v4/ai/duo_workflows/ws", base);
|
|
@@ -709,35 +771,65 @@ function buildWebSocketUrl(instanceUrl, modelId) {
|
|
|
709
771
|
if (modelId) url.searchParams.set("user_selected_model_identifier", modelId);
|
|
710
772
|
return url.toString();
|
|
711
773
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
774
|
+
|
|
775
|
+
// src/provider/prompt.ts
|
|
776
|
+
var SYSTEM_REMINDER_RE = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
777
|
+
var WRAPPED_USER_RE = /^<system-reminder>\s*The user sent the following message:\s*\n([\s\S]*?)\n\s*Please address this message and continue with your tasks\.\s*<\/system-reminder>$/;
|
|
778
|
+
function extractGoal(prompt) {
|
|
779
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
780
|
+
const message = prompt[i];
|
|
781
|
+
if (message.role !== "user") continue;
|
|
782
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
783
|
+
const text2 = content.filter((part) => part.type === "text").map((part) => stripSystemReminders(part.text)).filter(Boolean).join("\n").trim();
|
|
784
|
+
if (text2) return text2;
|
|
722
785
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
786
|
+
return "";
|
|
787
|
+
}
|
|
788
|
+
function stripSystemReminders(value) {
|
|
789
|
+
return value.replace(SYSTEM_REMINDER_RE, (block) => {
|
|
790
|
+
const wrapped = WRAPPED_USER_RE.exec(block);
|
|
791
|
+
return wrapped?.[1]?.trim() ?? "";
|
|
792
|
+
}).trim();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/provider/session-context.ts
|
|
796
|
+
function readSessionID(options) {
|
|
797
|
+
const providerBlock = readProviderBlock(options);
|
|
798
|
+
if (typeof providerBlock?.workflowSessionID === "string" && providerBlock.workflowSessionID.trim()) {
|
|
799
|
+
return providerBlock.workflowSessionID.trim();
|
|
727
800
|
}
|
|
728
|
-
};
|
|
801
|
+
const headers = options.headers ?? {};
|
|
802
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
803
|
+
if (key.toLowerCase() === "x-opencode-session" && value?.trim()) return value.trim();
|
|
804
|
+
}
|
|
805
|
+
return void 0;
|
|
806
|
+
}
|
|
807
|
+
function readProviderBlock(options) {
|
|
808
|
+
const block = options.providerOptions?.[PROVIDER_ID];
|
|
809
|
+
if (block && typeof block === "object" && !Array.isArray(block)) {
|
|
810
|
+
return block;
|
|
811
|
+
}
|
|
812
|
+
return void 0;
|
|
813
|
+
}
|
|
729
814
|
|
|
730
815
|
// src/provider/duo-workflow-model.ts
|
|
731
816
|
var sessions = /* @__PURE__ */ new Map();
|
|
817
|
+
var UNKNOWN_USAGE = {
|
|
818
|
+
inputTokens: void 0,
|
|
819
|
+
outputTokens: void 0,
|
|
820
|
+
totalTokens: void 0
|
|
821
|
+
};
|
|
732
822
|
var DuoWorkflowModel = class {
|
|
733
823
|
specificationVersion = "v2";
|
|
734
824
|
provider = PROVIDER_ID;
|
|
735
825
|
modelId;
|
|
736
826
|
supportedUrls = {};
|
|
737
827
|
#client;
|
|
738
|
-
|
|
828
|
+
#cwd;
|
|
829
|
+
constructor(modelId, client, cwd) {
|
|
739
830
|
this.modelId = modelId;
|
|
740
831
|
this.#client = client;
|
|
832
|
+
this.#cwd = cwd ?? process.cwd();
|
|
741
833
|
}
|
|
742
834
|
async doGenerate(options) {
|
|
743
835
|
const sessionID = readSessionID(options);
|
|
@@ -757,11 +849,7 @@ var DuoWorkflowModel = class {
|
|
|
757
849
|
}
|
|
758
850
|
],
|
|
759
851
|
finishReason: "stop",
|
|
760
|
-
usage:
|
|
761
|
-
inputTokens: void 0,
|
|
762
|
-
outputTokens: void 0,
|
|
763
|
-
totalTokens: void 0
|
|
764
|
-
},
|
|
852
|
+
usage: UNKNOWN_USAGE,
|
|
765
853
|
warnings: []
|
|
766
854
|
};
|
|
767
855
|
}
|
|
@@ -806,11 +894,7 @@ var DuoWorkflowModel = class {
|
|
|
806
894
|
controller.enqueue({
|
|
807
895
|
type: "finish",
|
|
808
896
|
finishReason: "stop",
|
|
809
|
-
usage:
|
|
810
|
-
inputTokens: void 0,
|
|
811
|
-
outputTokens: void 0,
|
|
812
|
-
totalTokens: void 0
|
|
813
|
-
}
|
|
897
|
+
usage: UNKNOWN_USAGE
|
|
814
898
|
});
|
|
815
899
|
controller.close();
|
|
816
900
|
} catch (error) {
|
|
@@ -821,11 +905,7 @@ var DuoWorkflowModel = class {
|
|
|
821
905
|
controller.enqueue({
|
|
822
906
|
type: "finish",
|
|
823
907
|
finishReason: "error",
|
|
824
|
-
usage:
|
|
825
|
-
inputTokens: void 0,
|
|
826
|
-
outputTokens: void 0,
|
|
827
|
-
totalTokens: void 0
|
|
828
|
-
}
|
|
908
|
+
usage: UNKNOWN_USAGE
|
|
829
909
|
});
|
|
830
910
|
controller.close();
|
|
831
911
|
}
|
|
@@ -839,57 +919,26 @@ var DuoWorkflowModel = class {
|
|
|
839
919
|
}
|
|
840
920
|
};
|
|
841
921
|
}
|
|
922
|
+
/** Remove a cached session, freeing its resources. */
|
|
923
|
+
disposeSession(sessionID) {
|
|
924
|
+
return sessions.delete(sessionKey(this.#client.instanceUrl, this.modelId, sessionID));
|
|
925
|
+
}
|
|
842
926
|
#resolveSession(sessionID) {
|
|
843
|
-
const key =
|
|
927
|
+
const key = sessionKey(this.#client.instanceUrl, this.modelId, sessionID);
|
|
844
928
|
const existing = sessions.get(key);
|
|
845
929
|
if (existing) return existing;
|
|
846
|
-
const created = new WorkflowSession(this.#client, this.modelId);
|
|
930
|
+
const created = new WorkflowSession(this.#client, this.modelId, this.#cwd);
|
|
847
931
|
sessions.set(key, created);
|
|
848
932
|
return created;
|
|
849
933
|
}
|
|
850
934
|
};
|
|
851
|
-
function
|
|
852
|
-
|
|
853
|
-
const message = prompt[i];
|
|
854
|
-
if (message.role !== "user") continue;
|
|
855
|
-
const content = Array.isArray(message.content) ? message.content : [];
|
|
856
|
-
const text2 = content.filter((part) => part.type === "text").map((part) => stripSystemReminders(part.text)).filter(Boolean).join("\n").trim();
|
|
857
|
-
if (text2) return text2;
|
|
858
|
-
}
|
|
859
|
-
return "";
|
|
860
|
-
}
|
|
861
|
-
var SYSTEM_REMINDER_RE = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
862
|
-
var WRAPPED_USER_RE = /^<system-reminder>\s*The user sent the following message:\s*\n([\s\S]*?)\n\s*Please address this message and continue with your tasks\.\s*<\/system-reminder>$/;
|
|
863
|
-
function stripSystemReminders(value) {
|
|
864
|
-
return value.replace(SYSTEM_REMINDER_RE, (block) => {
|
|
865
|
-
const wrapped = WRAPPED_USER_RE.exec(block);
|
|
866
|
-
return wrapped?.[1]?.trim() ?? "";
|
|
867
|
-
}).trim();
|
|
868
|
-
}
|
|
869
|
-
function readSessionID(options) {
|
|
870
|
-
const providerBlock = readProviderBlock(options);
|
|
871
|
-
if (typeof providerBlock?.workflowSessionID === "string" && providerBlock.workflowSessionID.trim()) {
|
|
872
|
-
return providerBlock.workflowSessionID.trim();
|
|
873
|
-
}
|
|
874
|
-
const headers = options.headers ?? {};
|
|
875
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
876
|
-
if (key.toLowerCase() === "x-opencode-session" && value?.trim()) return value.trim();
|
|
877
|
-
}
|
|
878
|
-
return void 0;
|
|
879
|
-
}
|
|
880
|
-
function readProviderBlock(options) {
|
|
881
|
-
const block = options.providerOptions?.[PROVIDER_ID];
|
|
882
|
-
if (block && typeof block === "object" && !Array.isArray(block)) {
|
|
883
|
-
return block;
|
|
884
|
-
}
|
|
885
|
-
return void 0;
|
|
935
|
+
function sessionKey(instanceUrl, modelId, sessionID) {
|
|
936
|
+
return `${instanceUrl}::${modelId}::${sessionID}`;
|
|
886
937
|
}
|
|
887
938
|
|
|
888
939
|
// src/provider/index.ts
|
|
889
940
|
function createFallbackProvider(input = {}) {
|
|
890
|
-
const
|
|
891
|
-
const token = text(input.apiKey) ?? text(input.token) ?? process.env.GITLAB_TOKEN ?? process.env.GITLAB_OAUTH_TOKEN ?? "";
|
|
892
|
-
const client = { instanceUrl, token };
|
|
941
|
+
const client = resolveCredentials(input);
|
|
893
942
|
return {
|
|
894
943
|
languageModel(modelId) {
|
|
895
944
|
return new DuoWorkflowModel(modelId, client);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-gitlab-duo-agentic",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"description": "OpenCode plugin and provider for GitLab Duo Agentic workflows",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -43,9 +43,6 @@
|
|
|
43
43
|
"@ai-sdk/provider": "2.0.1",
|
|
44
44
|
"@opencode-ai/plugin": "^1.2.6",
|
|
45
45
|
"isomorphic-ws": "^5.0.0",
|
|
46
|
-
"neverthrow": "^8.2.0",
|
|
47
|
-
"proxy-agent": "^6.5.0",
|
|
48
|
-
"uuid": "^11.0.5",
|
|
49
46
|
"zod": "^3.25.76"
|
|
50
47
|
},
|
|
51
48
|
"devDependencies": {
|