opencode-gitlab-duo-agentic 0.1.15 → 0.1.17

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 +582 -26
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,7 +3,14 @@ var PROVIDER_ID = "gitlab";
3
3
  var DEFAULT_MODEL_ID = "duo-chat-sonnet-4-5";
4
4
  var DEFAULT_INSTANCE_URL = "https://gitlab.com";
5
5
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
6
- var NOT_IMPLEMENTED_MESSAGE = "GitLab Duo Agentic fallback model is configured, but the Duo Workflow runtime is not implemented yet.";
6
+ var WORKFLOW_DEFINITION = "chat";
7
+ var WORKFLOW_CLIENT_VERSION = "1.0";
8
+ var WORKFLOW_ENVIRONMENT = "ide";
9
+ var WORKFLOW_CONNECT_TIMEOUT_MS = 15e3;
10
+ var WORKFLOW_HEARTBEAT_INTERVAL_MS = 6e4;
11
+ var WORKFLOW_KEEPALIVE_INTERVAL_MS = 45e3;
12
+ var WORKFLOW_TOKEN_EXPIRY_BUFFER_MS = 3e4;
13
+ var WORKFLOW_TOOL_ERROR_MESSAGE = "Tool execution is not implemented in this client yet";
7
14
 
8
15
  // src/gitlab/models.ts
9
16
  import crypto from "crypto";
@@ -19,17 +26,38 @@ var GitLabApiError = class extends Error {
19
26
  this.name = "GitLabApiError";
20
27
  }
21
28
  };
22
- async function get(options, path3) {
29
+ async function request(options, path3, init) {
23
30
  const url = `${options.instanceUrl}/api/v4/${path3}`;
31
+ const headers = new Headers(init.headers);
32
+ headers.set("authorization", `Bearer ${options.token}`);
24
33
  const response = await fetch(url, {
25
- headers: { authorization: `Bearer ${options.token}` }
34
+ ...init,
35
+ headers
26
36
  });
37
+ return response;
38
+ }
39
+ async function get(options, path3) {
40
+ const response = await request(options, path3, { method: "GET" });
27
41
  if (!response.ok) {
28
42
  const text2 = await response.text().catch(() => "");
29
43
  throw new GitLabApiError(response.status, `GET ${path3} failed (${response.status}): ${text2}`);
30
44
  }
31
45
  return response.json();
32
46
  }
47
+ async function post(options, path3, body) {
48
+ const response = await request(options, path3, {
49
+ method: "POST",
50
+ headers: {
51
+ "content-type": "application/json"
52
+ },
53
+ body: JSON.stringify(body)
54
+ });
55
+ if (!response.ok) {
56
+ const text2 = await response.text().catch(() => "");
57
+ throw new GitLabApiError(response.status, `POST ${path3} failed (${response.status}): ${text2}`);
58
+ }
59
+ return response.json();
60
+ }
33
61
  async function graphql(options, query, variables) {
34
62
  const url = `${options.instanceUrl}/api/graphql`;
35
63
  const response = await fetch(url, {
@@ -264,12 +292,13 @@ async function applyRuntimeConfig(config, directory) {
264
292
  const current = config.provider[PROVIDER_ID] ?? {};
265
293
  const options = current.options ?? {};
266
294
  const instanceUrl = normalizeInstanceUrl(options.instanceUrl ?? envInstanceUrl());
267
- const token = (typeof options.apiKey === "string" ? options.apiKey : void 0) ?? process.env.GITLAB_TOKEN ?? "";
295
+ const token = (typeof options.apiKey === "string" ? options.apiKey : void 0) ?? process.env.GITLAB_TOKEN ?? process.env.GITLAB_OAUTH_TOKEN ?? "";
268
296
  const available = await loadAvailableModels(instanceUrl, token, directory);
269
297
  const modelIds = available.map((m) => m.id);
270
298
  const models = toModelsConfig(available);
271
299
  config.provider[PROVIDER_ID] = {
272
300
  ...current,
301
+ npm: "opencode-gitlab-duo-agentic",
273
302
  whitelist: modelIds,
274
303
  options: {
275
304
  ...options,
@@ -292,36 +321,563 @@ function toModelsConfig(available) {
292
321
  // src/plugin/hooks.ts
293
322
  async function createPluginHooks(input) {
294
323
  return {
295
- config: async (config) => applyRuntimeConfig(config, input.directory)
324
+ config: async (config) => applyRuntimeConfig(config, input.directory),
325
+ "chat.params": async (context, output) => {
326
+ if (!isGitLabProvider(context.model)) return;
327
+ output.options = {
328
+ ...output.options,
329
+ workflowSessionID: context.sessionID
330
+ };
331
+ },
332
+ "chat.headers": async (context, output) => {
333
+ if (!isGitLabProvider(context.model)) return;
334
+ output.headers = {
335
+ ...output.headers,
336
+ "x-opencode-session": context.sessionID
337
+ };
338
+ }
296
339
  };
297
340
  }
341
+ function isGitLabProvider(model) {
342
+ if (model.api?.npm === "opencode-gitlab-duo-agentic") return true;
343
+ if (model.providerID === "gitlab" && model.api?.npm !== "@gitlab/gitlab-ai-provider") return true;
344
+ return model.providerID.toLowerCase().includes("gitlab-duo");
345
+ }
298
346
 
299
347
  // src/provider/index.ts
300
- import { NoSuchModelError, UnsupportedFunctionalityError } from "@ai-sdk/provider";
301
- function notImplemented() {
302
- return new UnsupportedFunctionalityError({
303
- functionality: "gitlab-duo-workflow-runtime",
304
- message: NOT_IMPLEMENTED_MESSAGE
305
- });
306
- }
307
- function placeholderModel(modelId) {
348
+ import { NoSuchModelError } from "@ai-sdk/provider";
349
+
350
+ // src/provider/duo-workflow-model.ts
351
+ import { randomUUID as randomUUID2 } from "crypto";
352
+
353
+ // src/workflow/session.ts
354
+ import { randomUUID } from "crypto";
355
+
356
+ // src/workflow/checkpoint.ts
357
+ function createCheckpointState() {
308
358
  return {
309
- specificationVersion: "v2",
310
- provider: PROVIDER_ID,
311
- modelId,
312
- supportedUrls: {},
313
- async doGenerate(_options) {
314
- throw notImplemented();
315
- },
316
- async doStream(_options) {
317
- throw notImplemented();
318
- }
359
+ uiChatLog: []
319
360
  };
320
361
  }
321
- function createFallbackProvider() {
362
+ function extractAgentTextDeltas(checkpoint, state) {
363
+ const next = parseCheckpoint(checkpoint);
364
+ const out = [];
365
+ for (let i = 0; i < next.length; i++) {
366
+ const item = next[i];
367
+ if (item.message_type !== "agent") continue;
368
+ const previous = state.uiChatLog[i];
369
+ if (!previous || previous.message_type !== "agent") {
370
+ if (item.content) out.push(item.content);
371
+ continue;
372
+ }
373
+ if (item.content === previous.content) continue;
374
+ if (item.content.startsWith(previous.content)) {
375
+ const delta = item.content.slice(previous.content.length);
376
+ if (delta) out.push(delta);
377
+ continue;
378
+ }
379
+ if (item.content) out.push(item.content);
380
+ }
381
+ state.uiChatLog = next;
382
+ return out;
383
+ }
384
+ function parseCheckpoint(raw) {
385
+ if (!raw) return [];
386
+ try {
387
+ const parsed = JSON.parse(raw);
388
+ const log = parsed.channel_values?.ui_chat_log;
389
+ if (!Array.isArray(log)) return [];
390
+ return log.filter(isUiChatLogEntry);
391
+ } catch {
392
+ return [];
393
+ }
394
+ }
395
+ function isUiChatLogEntry(value) {
396
+ if (!value || typeof value !== "object") return false;
397
+ const item = value;
398
+ if (typeof item.message_type !== "string") return false;
399
+ if (typeof item.content !== "string") return false;
400
+ return item.message_type === "user" || item.message_type === "agent" || item.message_type === "tool" || item.message_type === "request";
401
+ }
402
+
403
+ // src/workflow/token-service.ts
404
+ var WorkflowTokenService = class {
405
+ #client;
406
+ #cache = /* @__PURE__ */ new Map();
407
+ constructor(client) {
408
+ this.#client = client;
409
+ }
410
+ clear() {
411
+ this.#cache.clear();
412
+ }
413
+ async get(rootNamespaceId) {
414
+ const key = rootNamespaceId ?? "";
415
+ const cached = this.#cache.get(key);
416
+ if (cached && cached.expiresAt > Date.now()) return cached.value;
417
+ try {
418
+ const value = await post(
419
+ this.#client,
420
+ "ai/duo_workflows/direct_access",
421
+ rootNamespaceId ? {
422
+ workflow_definition: WORKFLOW_DEFINITION,
423
+ root_namespace_id: rootNamespaceId
424
+ } : {
425
+ workflow_definition: WORKFLOW_DEFINITION
426
+ }
427
+ );
428
+ const expiresAt = readExpiry(value);
429
+ this.#cache.set(key, { value, expiresAt });
430
+ return value;
431
+ } catch {
432
+ return null;
433
+ }
434
+ }
435
+ };
436
+ function readExpiry(value) {
437
+ const workflowExpiry = typeof value.duo_workflow_service?.token_expires_at === "number" ? value.duo_workflow_service.token_expires_at * 1e3 : Number.POSITIVE_INFINITY;
438
+ const railsExpiry = typeof value.gitlab_rails?.token_expires_at === "string" ? Date.parse(value.gitlab_rails.token_expires_at) : Number.POSITIVE_INFINITY;
439
+ const expiry = Math.min(workflowExpiry, railsExpiry);
440
+ if (!Number.isFinite(expiry)) return Date.now() + 5 * 60 * 1e3;
441
+ return Math.max(Date.now() + 1e3, expiry - WORKFLOW_TOKEN_EXPIRY_BUFFER_MS);
442
+ }
443
+
444
+ // src/workflow/types.ts
445
+ var WORKFLOW_STATUS = {
446
+ CREATED: "CREATED",
447
+ RUNNING: "RUNNING",
448
+ FINISHED: "FINISHED",
449
+ FAILED: "FAILED",
450
+ STOPPED: "STOPPED",
451
+ INPUT_REQUIRED: "INPUT_REQUIRED",
452
+ PLAN_APPROVAL_REQUIRED: "PLAN_APPROVAL_REQUIRED",
453
+ TOOL_CALL_APPROVAL_REQUIRED: "TOOL_CALL_APPROVAL_REQUIRED"
454
+ };
455
+
456
+ // src/workflow/websocket-client.ts
457
+ import WebSocket from "isomorphic-ws";
458
+ var WorkflowWebSocketClient = class {
459
+ #socket = null;
460
+ #heartbeat;
461
+ #keepalive;
462
+ #callbacks;
463
+ constructor(callbacks) {
464
+ this.#callbacks = callbacks;
465
+ }
466
+ async connect(url, headers) {
467
+ const socket = new WebSocket(url, { headers });
468
+ this.#socket = socket;
469
+ await new Promise((resolve, reject) => {
470
+ const timeout = setTimeout(() => {
471
+ cleanup();
472
+ socket.close(1e3);
473
+ reject(new Error(`WebSocket connection timeout after ${WORKFLOW_CONNECT_TIMEOUT_MS}ms`));
474
+ }, WORKFLOW_CONNECT_TIMEOUT_MS);
475
+ const cleanup = () => {
476
+ clearTimeout(timeout);
477
+ socket.off("open", onOpen);
478
+ socket.off("error", onError);
479
+ };
480
+ const onOpen = () => {
481
+ cleanup();
482
+ resolve();
483
+ };
484
+ const onError = (error) => {
485
+ cleanup();
486
+ reject(error);
487
+ };
488
+ socket.once("open", onOpen);
489
+ socket.once("error", onError);
490
+ });
491
+ socket.on("message", (data) => {
492
+ try {
493
+ const payload = decodeSocketMessage(data);
494
+ if (!payload) return;
495
+ const parsed = JSON.parse(payload);
496
+ this.#callbacks.action(parsed);
497
+ } catch (error) {
498
+ const next = error instanceof Error ? error : new Error(String(error));
499
+ this.#callbacks.error(next);
500
+ }
501
+ });
502
+ socket.on("error", (error) => {
503
+ this.#callbacks.error(error instanceof Error ? error : new Error(String(error)));
504
+ });
505
+ socket.on("close", (code, reason) => {
506
+ this.#stopIntervals();
507
+ this.#callbacks.close(code, reason?.toString("utf8") ?? "");
508
+ });
509
+ this.#heartbeat = setInterval(() => {
510
+ this.send({ heartbeat: { timestamp: Date.now() } });
511
+ }, WORKFLOW_HEARTBEAT_INTERVAL_MS);
512
+ this.#keepalive = setInterval(() => {
513
+ if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) return;
514
+ this.#socket.ping(Buffer.from(String(Date.now())));
515
+ }, WORKFLOW_KEEPALIVE_INTERVAL_MS);
516
+ }
517
+ send(event) {
518
+ if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) return false;
519
+ this.#socket.send(JSON.stringify(event));
520
+ return true;
521
+ }
522
+ close() {
523
+ this.#stopIntervals();
524
+ if (!this.#socket) return;
525
+ this.#socket.close(1e3);
526
+ this.#socket = null;
527
+ }
528
+ #stopIntervals() {
529
+ if (this.#heartbeat) {
530
+ clearInterval(this.#heartbeat);
531
+ this.#heartbeat = void 0;
532
+ }
533
+ if (this.#keepalive) {
534
+ clearInterval(this.#keepalive);
535
+ this.#keepalive = void 0;
536
+ }
537
+ }
538
+ };
539
+ function decodeSocketMessage(data) {
540
+ if (typeof data === "string") return data;
541
+ if (Buffer.isBuffer(data)) return data.toString("utf8");
542
+ if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
543
+ if (Array.isArray(data)) return Buffer.concat(data).toString("utf8");
544
+ return void 0;
545
+ }
546
+
547
+ // src/workflow/session.ts
548
+ var WorkflowSession = class {
549
+ #client;
550
+ #tokenService;
551
+ #modelId;
552
+ #workflowId;
553
+ #projectPath;
554
+ #rootNamespaceId;
555
+ #checkpoint = createCheckpointState();
556
+ #running = false;
557
+ constructor(client, modelId) {
558
+ this.#client = client;
559
+ this.#tokenService = new WorkflowTokenService(client);
560
+ this.#modelId = modelId;
561
+ }
562
+ get workflowId() {
563
+ return this.#workflowId;
564
+ }
565
+ reset() {
566
+ this.#workflowId = void 0;
567
+ this.#checkpoint = createCheckpointState();
568
+ this.#tokenService.clear();
569
+ }
570
+ async *runTurn(goal, abortSignal) {
571
+ if (this.#running) {
572
+ throw new Error("workflow session is already running");
573
+ }
574
+ this.#running = true;
575
+ const queue = new AsyncQueue();
576
+ const socket = new WorkflowWebSocketClient({
577
+ action: (action) => queue.push({ type: "action", action }),
578
+ error: (error) => queue.push({ type: "error", error }),
579
+ close: (code, reason) => queue.push({ type: "close", code, reason })
580
+ });
581
+ const onAbort = () => {
582
+ socket.send({
583
+ stopWorkflow: {
584
+ reason: "ABORTED"
585
+ }
586
+ });
587
+ socket.close();
588
+ };
589
+ try {
590
+ if (abortSignal?.aborted) throw new Error("aborted");
591
+ if (!this.#workflowId) this.#workflowId = await this.#createWorkflow(goal);
592
+ const access = await this.#tokenService.get(this.#rootNamespaceId);
593
+ const url = buildWebSocketUrl(this.#client.instanceUrl, this.#modelId);
594
+ await socket.connect(url, {
595
+ authorization: `Bearer ${this.#client.token}`,
596
+ origin: new URL(this.#client.instanceUrl).origin,
597
+ "x-request-id": randomUUID(),
598
+ "x-gitlab-client-type": "node-websocket"
599
+ });
600
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
601
+ const sent = socket.send({
602
+ startRequest: {
603
+ workflowID: this.#workflowId,
604
+ clientVersion: WORKFLOW_CLIENT_VERSION,
605
+ workflowDefinition: WORKFLOW_DEFINITION,
606
+ goal,
607
+ workflowMetadata: JSON.stringify({
608
+ extended_logging: access?.workflow_metadata?.extended_logging ?? false
609
+ }),
610
+ clientCapabilities: [],
611
+ mcpTools: [],
612
+ additional_context: [],
613
+ preapproved_tools: []
614
+ }
615
+ });
616
+ if (!sent) throw new Error("failed to send workflow startRequest");
617
+ for (; ; ) {
618
+ const event = await queue.shift();
619
+ if (event.type === "error") throw event.error;
620
+ if (event.type === "close") {
621
+ if (event.code === 1e3 || event.code === 1006) return;
622
+ throw new Error(`workflow websocket closed abnormally (${event.code}): ${event.reason}`);
623
+ }
624
+ if (isCheckpointAction(event.action)) {
625
+ const deltas = extractAgentTextDeltas(event.action.newCheckpoint.checkpoint, this.#checkpoint);
626
+ for (const delta of deltas) {
627
+ yield {
628
+ type: "text-delta",
629
+ value: delta
630
+ };
631
+ }
632
+ if (isTurnComplete(event.action.newCheckpoint.status)) {
633
+ socket.close();
634
+ }
635
+ continue;
636
+ }
637
+ if (!event.action.requestID) continue;
638
+ socket.send({
639
+ actionResponse: {
640
+ requestID: event.action.requestID,
641
+ plainTextResponse: {
642
+ response: "",
643
+ error: WORKFLOW_TOOL_ERROR_MESSAGE
644
+ }
645
+ }
646
+ });
647
+ }
648
+ } finally {
649
+ this.#running = false;
650
+ abortSignal?.removeEventListener("abort", onAbort);
651
+ socket.close();
652
+ }
653
+ }
654
+ async #createWorkflow(goal) {
655
+ await this.#loadProjectContext();
656
+ const body = {
657
+ goal,
658
+ workflow_definition: WORKFLOW_DEFINITION,
659
+ environment: WORKFLOW_ENVIRONMENT,
660
+ allow_agent_to_request_user: true,
661
+ ...this.#projectPath ? {
662
+ project_id: this.#projectPath
663
+ } : {}
664
+ };
665
+ const created = await post(this.#client, "ai/duo_workflows/workflows", body);
666
+ if (created.id === void 0 || created.id === null) {
667
+ const details = [created.message, created.error].filter(Boolean).join("; ");
668
+ throw new Error(`failed to create workflow${details ? `: ${details}` : ""}`);
669
+ }
670
+ return String(created.id);
671
+ }
672
+ async #loadProjectContext() {
673
+ if (this.#projectPath !== void 0) return;
674
+ const projectPath = await detectProjectPath(process.cwd(), this.#client.instanceUrl);
675
+ this.#projectPath = projectPath;
676
+ if (!projectPath) return;
677
+ try {
678
+ const project = await fetchProjectDetails(this.#client, projectPath);
679
+ this.#rootNamespaceId = await resolveRootNamespaceId(this.#client, project.namespaceId);
680
+ } catch {
681
+ this.#rootNamespaceId = void 0;
682
+ }
683
+ }
684
+ };
685
+ function isCheckpointAction(action) {
686
+ return Boolean(action.newCheckpoint);
687
+ }
688
+ function isTurnComplete(status) {
689
+ 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;
690
+ }
691
+ function buildWebSocketUrl(instanceUrl, modelId) {
692
+ const base = new URL(instanceUrl.endsWith("/") ? instanceUrl : `${instanceUrl}/`);
693
+ const url = new URL("api/v4/ai/duo_workflows/ws", base);
694
+ if (base.protocol === "https:") url.protocol = "wss:";
695
+ if (base.protocol === "http:") url.protocol = "ws:";
696
+ if (modelId) url.searchParams.set("user_selected_model_identifier", modelId);
697
+ return url.toString();
698
+ }
699
+ var AsyncQueue = class {
700
+ #values = [];
701
+ #waiters = [];
702
+ push(value) {
703
+ const waiter = this.#waiters.shift();
704
+ if (waiter) {
705
+ waiter(value);
706
+ return;
707
+ }
708
+ this.#values.push(value);
709
+ }
710
+ shift() {
711
+ const value = this.#values.shift();
712
+ if (value !== void 0) return Promise.resolve(value);
713
+ return new Promise((resolve) => this.#waiters.push(resolve));
714
+ }
715
+ };
716
+
717
+ // src/provider/duo-workflow-model.ts
718
+ var sessions = /* @__PURE__ */ new Map();
719
+ var DuoWorkflowModel = class {
720
+ specificationVersion = "v2";
721
+ provider = PROVIDER_ID;
722
+ modelId;
723
+ supportedUrls = {};
724
+ #client;
725
+ constructor(modelId, client) {
726
+ this.modelId = modelId;
727
+ this.#client = client;
728
+ }
729
+ async doGenerate(options) {
730
+ const goal = extractGoal(options.prompt);
731
+ if (!goal) throw new Error("missing user message content");
732
+ const session = this.#resolveSession(options);
733
+ const chunks = [];
734
+ for await (const item of session.runTurn(goal, options.abortSignal)) {
735
+ if (item.type === "text-delta") chunks.push(item.value);
736
+ }
737
+ return {
738
+ content: [
739
+ {
740
+ type: "text",
741
+ text: chunks.join("")
742
+ }
743
+ ],
744
+ finishReason: "stop",
745
+ usage: {
746
+ inputTokens: void 0,
747
+ outputTokens: void 0,
748
+ totalTokens: void 0
749
+ },
750
+ warnings: []
751
+ };
752
+ }
753
+ async doStream(options) {
754
+ const goal = extractGoal(options.prompt);
755
+ if (!goal) throw new Error("missing user message content");
756
+ const session = this.#resolveSession(options);
757
+ const textId = randomUUID2();
758
+ return {
759
+ stream: new ReadableStream({
760
+ start: async (controller) => {
761
+ controller.enqueue({
762
+ type: "stream-start",
763
+ warnings: []
764
+ });
765
+ let hasText = false;
766
+ try {
767
+ for await (const item of session.runTurn(goal, options.abortSignal)) {
768
+ if (item.type !== "text-delta") continue;
769
+ if (!item.value) continue;
770
+ if (!hasText) {
771
+ hasText = true;
772
+ controller.enqueue({
773
+ type: "text-start",
774
+ id: textId
775
+ });
776
+ }
777
+ controller.enqueue({
778
+ type: "text-delta",
779
+ id: textId,
780
+ delta: item.value
781
+ });
782
+ }
783
+ if (hasText) {
784
+ controller.enqueue({
785
+ type: "text-end",
786
+ id: textId
787
+ });
788
+ }
789
+ controller.enqueue({
790
+ type: "finish",
791
+ finishReason: "stop",
792
+ usage: {
793
+ inputTokens: void 0,
794
+ outputTokens: void 0,
795
+ totalTokens: void 0
796
+ }
797
+ });
798
+ controller.close();
799
+ } catch (error) {
800
+ controller.enqueue({
801
+ type: "error",
802
+ error
803
+ });
804
+ controller.enqueue({
805
+ type: "finish",
806
+ finishReason: "error",
807
+ usage: {
808
+ inputTokens: void 0,
809
+ outputTokens: void 0,
810
+ totalTokens: void 0
811
+ }
812
+ });
813
+ controller.close();
814
+ }
815
+ }
816
+ }),
817
+ request: {
818
+ body: {
819
+ goal,
820
+ workflowID: session.workflowId
821
+ }
822
+ }
823
+ };
824
+ }
825
+ #resolveSession(options) {
826
+ const sessionID = readSessionID(options);
827
+ const key = `${this.#client.instanceUrl}::${this.modelId}::${sessionID}`;
828
+ const existing = sessions.get(key);
829
+ if (existing) return existing;
830
+ const created = new WorkflowSession(this.#client, this.modelId);
831
+ sessions.set(key, created);
832
+ return created;
833
+ }
834
+ };
835
+ function extractGoal(prompt) {
836
+ for (let i = prompt.length - 1; i >= 0; i--) {
837
+ const message = prompt[i];
838
+ if (message.role !== "user") continue;
839
+ const content = Array.isArray(message.content) ? message.content : [];
840
+ const text2 = content.filter((part) => part.type === "text").map((part) => part.text).join("\n").trim();
841
+ if (text2) return text2;
842
+ }
843
+ return "";
844
+ }
845
+ function readSessionID(options) {
846
+ const providerBlock = readProviderBlock(options);
847
+ if (typeof providerBlock?.workflowSessionID === "string" && providerBlock.workflowSessionID.trim()) {
848
+ return providerBlock.workflowSessionID.trim();
849
+ }
850
+ const headers = options.headers ?? {};
851
+ for (const [key, value] of Object.entries(headers)) {
852
+ if (key.toLowerCase() === "x-opencode-session" && value?.trim()) return value.trim();
853
+ }
854
+ return "default";
855
+ }
856
+ function readProviderBlock(options) {
857
+ const block = options.providerOptions?.[PROVIDER_ID];
858
+ if (block && typeof block === "object" && !Array.isArray(block)) {
859
+ return block;
860
+ }
861
+ if (!options.providerOptions) return void 0;
862
+ for (const value of Object.values(options.providerOptions)) {
863
+ if (value && typeof value === "object" && !Array.isArray(value)) {
864
+ return value;
865
+ }
866
+ }
867
+ return void 0;
868
+ }
869
+
870
+ // src/provider/index.ts
871
+ function createFallbackProvider(input = {}) {
872
+ const instanceUrl = normalizeInstanceUrl(input.instanceUrl ?? envInstanceUrl());
873
+ const token = text(input.apiKey) ?? text(input.token) ?? process.env.GITLAB_TOKEN ?? process.env.GITLAB_OAUTH_TOKEN ?? "";
874
+ const client = { instanceUrl, token };
322
875
  return {
323
876
  languageModel(modelId) {
324
- return placeholderModel(modelId);
877
+ return new DuoWorkflowModel(modelId, client);
878
+ },
879
+ agenticChat(modelId, _options) {
880
+ return new DuoWorkflowModel(modelId, client);
325
881
  },
326
882
  textEmbeddingModel(modelId) {
327
883
  throw new NoSuchModelError({ modelId, modelType: "textEmbeddingModel" });
@@ -342,7 +898,7 @@ var entry = (input) => {
342
898
  if (isPluginInput(input)) {
343
899
  return createPluginHooks(input);
344
900
  }
345
- return createFallbackProvider();
901
+ return createFallbackProvider(input);
346
902
  };
347
903
  var createGitLabDuoAgentic = entry;
348
904
  var GitLabDuoAgenticPlugin = entry;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-gitlab-duo-agentic",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "OpenCode plugin and provider for GitLab Duo Agentic workflows",
5
5
  "license": "MIT",
6
6
  "type": "module",