opencode-gitlab-duo-agentic 0.2.2 → 0.2.5
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 +562 -151
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -391,16 +391,18 @@ function isGitLabProvider(model) {
|
|
|
391
391
|
import { NoSuchModelError } from "@ai-sdk/provider";
|
|
392
392
|
|
|
393
393
|
// src/provider/duo-workflow-model.ts
|
|
394
|
-
import { randomUUID as
|
|
394
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
395
395
|
|
|
396
396
|
// src/workflow/session.ts
|
|
397
|
-
import { randomUUID } from "crypto";
|
|
397
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
398
398
|
|
|
399
399
|
// src/utils/async-queue.ts
|
|
400
400
|
var AsyncQueue = class {
|
|
401
401
|
#values = [];
|
|
402
402
|
#waiters = [];
|
|
403
|
+
#closed = false;
|
|
403
404
|
push(value) {
|
|
405
|
+
if (this.#closed) return;
|
|
404
406
|
const waiter = this.#waiters.shift();
|
|
405
407
|
if (waiter) {
|
|
406
408
|
waiter(value);
|
|
@@ -408,17 +410,28 @@ var AsyncQueue = class {
|
|
|
408
410
|
}
|
|
409
411
|
this.#values.push(value);
|
|
410
412
|
}
|
|
413
|
+
/** Returns null when closed and no buffered values remain. */
|
|
411
414
|
shift() {
|
|
412
415
|
const value = this.#values.shift();
|
|
413
416
|
if (value !== void 0) return Promise.resolve(value);
|
|
417
|
+
if (this.#closed) return Promise.resolve(null);
|
|
414
418
|
return new Promise((resolve2) => this.#waiters.push(resolve2));
|
|
415
419
|
}
|
|
420
|
+
close() {
|
|
421
|
+
this.#closed = true;
|
|
422
|
+
for (const waiter of this.#waiters) {
|
|
423
|
+
waiter(null);
|
|
424
|
+
}
|
|
425
|
+
this.#waiters = [];
|
|
426
|
+
}
|
|
416
427
|
};
|
|
417
428
|
|
|
418
429
|
// src/workflow/checkpoint.ts
|
|
430
|
+
import { randomUUID } from "crypto";
|
|
419
431
|
function createCheckpointState() {
|
|
420
432
|
return {
|
|
421
|
-
uiChatLog: []
|
|
433
|
+
uiChatLog: [],
|
|
434
|
+
processedRequestIndices: /* @__PURE__ */ new Set()
|
|
422
435
|
};
|
|
423
436
|
}
|
|
424
437
|
function extractAgentTextDeltas(checkpoint, state) {
|
|
@@ -443,6 +456,23 @@ function extractAgentTextDeltas(checkpoint, state) {
|
|
|
443
456
|
state.uiChatLog = next;
|
|
444
457
|
return out;
|
|
445
458
|
}
|
|
459
|
+
function extractToolRequests(checkpoint, state) {
|
|
460
|
+
const next = parseCheckpoint(checkpoint);
|
|
461
|
+
const requests = [];
|
|
462
|
+
for (let i = 0; i < next.length; i++) {
|
|
463
|
+
const item = next[i];
|
|
464
|
+
if (item.message_type !== "request") continue;
|
|
465
|
+
if (!item.tool_info) continue;
|
|
466
|
+
if (state.processedRequestIndices.has(i)) continue;
|
|
467
|
+
state.processedRequestIndices.add(i);
|
|
468
|
+
requests.push({
|
|
469
|
+
requestId: item.correlation_id ?? randomUUID(),
|
|
470
|
+
toolName: item.tool_info.name,
|
|
471
|
+
args: item.tool_info.args ?? {}
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
return requests;
|
|
475
|
+
}
|
|
446
476
|
function parseCheckpoint(raw) {
|
|
447
477
|
if (!raw) return [];
|
|
448
478
|
try {
|
|
@@ -624,6 +654,16 @@ function decodeSocketMessage(data) {
|
|
|
624
654
|
import { readFile, readdir, writeFile, mkdir as fsMkdir, stat } from "fs/promises";
|
|
625
655
|
import { exec } from "child_process";
|
|
626
656
|
import { resolve, dirname } from "path";
|
|
657
|
+
function asString(value) {
|
|
658
|
+
return typeof value === "string" ? value : void 0;
|
|
659
|
+
}
|
|
660
|
+
function asNumber(value) {
|
|
661
|
+
return typeof value === "number" ? value : void 0;
|
|
662
|
+
}
|
|
663
|
+
function asStringArray(value) {
|
|
664
|
+
if (!Array.isArray(value)) return [];
|
|
665
|
+
return value.filter((v) => typeof v === "string");
|
|
666
|
+
}
|
|
627
667
|
var ToolExecutor = class {
|
|
628
668
|
#cwd;
|
|
629
669
|
#client;
|
|
@@ -631,9 +671,104 @@ var ToolExecutor = class {
|
|
|
631
671
|
this.#cwd = cwd;
|
|
632
672
|
this.#client = client;
|
|
633
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Execute a tool request using Duo tool names and arg formats.
|
|
676
|
+
* This handles checkpoint-based tool requests where tool_info contains
|
|
677
|
+
* Duo-native names (read_file, edit_file, etc.) and Duo-native arg keys
|
|
678
|
+
* (file_path, old_str, new_str, etc.).
|
|
679
|
+
*/
|
|
680
|
+
async executeDuoTool(toolName, args) {
|
|
681
|
+
try {
|
|
682
|
+
switch (toolName) {
|
|
683
|
+
case "read_file": {
|
|
684
|
+
const filePath = asString(args.file_path) ?? asString(args.filepath) ?? asString(args.filePath) ?? asString(args.path);
|
|
685
|
+
if (!filePath) return { response: "", error: "read_file: missing file_path" };
|
|
686
|
+
return this.#read({ filePath, offset: asNumber(args.offset), limit: asNumber(args.limit) });
|
|
687
|
+
}
|
|
688
|
+
case "read_files": {
|
|
689
|
+
const paths = asStringArray(args.file_paths);
|
|
690
|
+
if (paths.length === 0) return { response: "", error: "read_files: missing file_paths" };
|
|
691
|
+
return this.#readMultiple(paths);
|
|
692
|
+
}
|
|
693
|
+
case "create_file_with_contents": {
|
|
694
|
+
const filePath = asString(args.file_path);
|
|
695
|
+
const content = asString(args.contents);
|
|
696
|
+
if (!filePath || content === void 0) return { response: "", error: "create_file: missing file_path or contents" };
|
|
697
|
+
return this.#write({ filePath, content });
|
|
698
|
+
}
|
|
699
|
+
case "edit_file": {
|
|
700
|
+
const filePath = asString(args.file_path);
|
|
701
|
+
const oldString = asString(args.old_str);
|
|
702
|
+
const newString = asString(args.new_str);
|
|
703
|
+
if (!filePath || oldString === void 0 || newString === void 0) {
|
|
704
|
+
return { response: "", error: "edit_file: missing file_path, old_str or new_str" };
|
|
705
|
+
}
|
|
706
|
+
return this.#edit({ filePath, oldString, newString });
|
|
707
|
+
}
|
|
708
|
+
case "list_dir": {
|
|
709
|
+
const directory = asString(args.directory) ?? ".";
|
|
710
|
+
return this.#read({ filePath: directory });
|
|
711
|
+
}
|
|
712
|
+
case "find_files": {
|
|
713
|
+
const pattern = asString(args.name_pattern);
|
|
714
|
+
if (!pattern) return { response: "", error: "find_files: missing name_pattern" };
|
|
715
|
+
return this.#glob({ pattern });
|
|
716
|
+
}
|
|
717
|
+
case "grep": {
|
|
718
|
+
const pattern = asString(args.pattern);
|
|
719
|
+
if (!pattern) return { response: "", error: "grep: missing pattern" };
|
|
720
|
+
return this.#grep({
|
|
721
|
+
pattern,
|
|
722
|
+
path: asString(args.search_directory),
|
|
723
|
+
caseInsensitive: Boolean(args.case_insensitive)
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
case "mkdir": {
|
|
727
|
+
const dir = asString(args.directory_path);
|
|
728
|
+
if (!dir) return { response: "", error: "mkdir: missing directory_path" };
|
|
729
|
+
return this.#mkdir(dir);
|
|
730
|
+
}
|
|
731
|
+
case "shell_command": {
|
|
732
|
+
const command = asString(args.command);
|
|
733
|
+
if (!command) return { response: "", error: "shell_command: missing command" };
|
|
734
|
+
return this.#bash({ command });
|
|
735
|
+
}
|
|
736
|
+
case "run_command": {
|
|
737
|
+
const program = asString(args.program);
|
|
738
|
+
if (!program) return { response: "", error: "run_command: missing program" };
|
|
739
|
+
const parts = [program];
|
|
740
|
+
const flags = args.flags;
|
|
741
|
+
if (Array.isArray(flags)) parts.push(...flags.map(String));
|
|
742
|
+
const cmdArgs = args.arguments;
|
|
743
|
+
if (Array.isArray(cmdArgs)) parts.push(...cmdArgs.map(String));
|
|
744
|
+
return this.#bash({ command: parts.join(" ") });
|
|
745
|
+
}
|
|
746
|
+
case "run_git_command": {
|
|
747
|
+
const command = asString(args.command);
|
|
748
|
+
if (!command) return { response: "", error: "run_git_command: missing command" };
|
|
749
|
+
const extra = Array.isArray(args.args) ? args.args.map(String).join(" ") : asString(args.args);
|
|
750
|
+
const gitCmd = extra ? `git ${command} ${extra}` : `git ${command}`;
|
|
751
|
+
return this.#bash({ command: gitCmd });
|
|
752
|
+
}
|
|
753
|
+
case "gitlab_api_request": {
|
|
754
|
+
const method = asString(args.method) ?? "GET";
|
|
755
|
+
const path3 = asString(args.path);
|
|
756
|
+
if (!path3) return { response: "", error: "gitlab_api_request: missing path" };
|
|
757
|
+
return this.#httpRequest({ method, path: path3, body: asString(args.body) });
|
|
758
|
+
}
|
|
759
|
+
default: {
|
|
760
|
+
console.error(`[tool-executor] unknown Duo tool: ${toolName}, args: ${JSON.stringify(args).slice(0, 300)}`);
|
|
761
|
+
return { response: "", error: `Unknown Duo tool: ${toolName}` };
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
} catch (err) {
|
|
765
|
+
console.error(`[tool-executor] executeDuoTool error:`, err);
|
|
766
|
+
return { response: "", error: err instanceof Error ? err.message : String(err) };
|
|
767
|
+
}
|
|
768
|
+
}
|
|
634
769
|
/**
|
|
635
770
|
* Route a Duo WorkflowToolAction to the appropriate local handler.
|
|
636
|
-
* This
|
|
771
|
+
* This handles standalone WebSocket actions (non-checkpoint path).
|
|
637
772
|
*/
|
|
638
773
|
async executeAction(action) {
|
|
639
774
|
try {
|
|
@@ -695,8 +830,11 @@ var ToolExecutor = class {
|
|
|
695
830
|
if (action.runHTTPRequest) {
|
|
696
831
|
return this.#httpRequest(action.runHTTPRequest);
|
|
697
832
|
}
|
|
698
|
-
|
|
833
|
+
const keys = Object.keys(action).filter((k) => k !== "requestID");
|
|
834
|
+
console.error(`[tool-executor] unhandled action, keys: ${JSON.stringify(keys)}, raw: ${JSON.stringify(action).slice(0, 500)}`);
|
|
835
|
+
return { response: "", error: `Unknown tool action (keys: ${keys.join(", ")})` };
|
|
699
836
|
} catch (err) {
|
|
837
|
+
console.error(`[tool-executor] execution error:`, err);
|
|
700
838
|
return { response: "", error: err instanceof Error ? err.message : String(err) };
|
|
701
839
|
}
|
|
702
840
|
}
|
|
@@ -894,8 +1032,9 @@ var WorkflowSession = class {
|
|
|
894
1032
|
#checkpoint = createCheckpointState();
|
|
895
1033
|
#toolsConfig;
|
|
896
1034
|
#toolExecutor;
|
|
897
|
-
|
|
898
|
-
#
|
|
1035
|
+
#socket;
|
|
1036
|
+
#queue;
|
|
1037
|
+
#startRequestSent = false;
|
|
899
1038
|
constructor(client, modelId, cwd) {
|
|
900
1039
|
this.#client = client;
|
|
901
1040
|
this.#tokenService = new WorkflowTokenService(client);
|
|
@@ -905,7 +1044,6 @@ var WorkflowSession = class {
|
|
|
905
1044
|
}
|
|
906
1045
|
/**
|
|
907
1046
|
* Opt-in: override the server-side system prompt and/or register MCP tools.
|
|
908
|
-
* When not called, the server uses its default prompt and built-in tools.
|
|
909
1047
|
*/
|
|
910
1048
|
setToolsConfig(config) {
|
|
911
1049
|
this.#toolsConfig = config;
|
|
@@ -913,103 +1051,152 @@ var WorkflowSession = class {
|
|
|
913
1051
|
get workflowId() {
|
|
914
1052
|
return this.#workflowId;
|
|
915
1053
|
}
|
|
1054
|
+
get hasStarted() {
|
|
1055
|
+
return this.#startRequestSent;
|
|
1056
|
+
}
|
|
916
1057
|
reset() {
|
|
917
1058
|
this.#workflowId = void 0;
|
|
918
1059
|
this.#checkpoint = createCheckpointState();
|
|
919
1060
|
this.#tokenService.clear();
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1061
|
+
this.#closeConnection();
|
|
1062
|
+
this.#startRequestSent = false;
|
|
1063
|
+
}
|
|
1064
|
+
// ---------------------------------------------------------------------------
|
|
1065
|
+
// Connection lifecycle (persistent)
|
|
1066
|
+
// ---------------------------------------------------------------------------
|
|
1067
|
+
async ensureConnected(goal) {
|
|
1068
|
+
if (this.#socket && this.#queue) return;
|
|
1069
|
+
if (!this.#workflowId) {
|
|
1070
|
+
this.#workflowId = await this.#createWorkflow(goal);
|
|
1071
|
+
}
|
|
1072
|
+
await this.#tokenService.get(this.#rootNamespaceId);
|
|
1073
|
+
this.#queue = new AsyncQueue();
|
|
1074
|
+
const queue = this.#queue;
|
|
928
1075
|
const socket = new WorkflowWebSocketClient({
|
|
929
|
-
action: (action) =>
|
|
930
|
-
error: (error) => queue.push({ type: "error", error }),
|
|
931
|
-
close: (
|
|
1076
|
+
action: (action) => this.#handleAction(action, queue),
|
|
1077
|
+
error: (error) => queue.push({ type: "error", message: error.message }),
|
|
1078
|
+
close: (_code, _reason) => queue.close()
|
|
932
1079
|
});
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
}
|
|
983
|
-
if (isCheckpointAction(event.action)) {
|
|
984
|
-
const deltas = extractAgentTextDeltas(event.action.newCheckpoint.checkpoint, this.#checkpoint);
|
|
985
|
-
for (const delta of deltas) {
|
|
986
|
-
yield {
|
|
987
|
-
type: "text-delta",
|
|
988
|
-
value: delta
|
|
989
|
-
};
|
|
990
|
-
}
|
|
991
|
-
if (isTurnComplete(event.action.newCheckpoint.status)) {
|
|
992
|
-
socket.close();
|
|
993
|
-
}
|
|
994
|
-
continue;
|
|
1080
|
+
const url = buildWebSocketUrl(this.#client.instanceUrl, this.#modelId);
|
|
1081
|
+
await socket.connect(url, {
|
|
1082
|
+
authorization: `Bearer ${this.#client.token}`,
|
|
1083
|
+
origin: new URL(this.#client.instanceUrl).origin,
|
|
1084
|
+
"x-request-id": randomUUID2(),
|
|
1085
|
+
"x-gitlab-client-type": "node-websocket"
|
|
1086
|
+
});
|
|
1087
|
+
this.#socket = socket;
|
|
1088
|
+
}
|
|
1089
|
+
// ---------------------------------------------------------------------------
|
|
1090
|
+
// Messaging
|
|
1091
|
+
// ---------------------------------------------------------------------------
|
|
1092
|
+
sendStartRequest(goal) {
|
|
1093
|
+
if (!this.#socket || !this.#workflowId) throw new Error("Not connected");
|
|
1094
|
+
const mcpTools = this.#toolsConfig?.mcpTools ?? [];
|
|
1095
|
+
const preapprovedTools = mcpTools.map((t) => t.name);
|
|
1096
|
+
this.#socket.send({
|
|
1097
|
+
startRequest: {
|
|
1098
|
+
workflowID: this.#workflowId,
|
|
1099
|
+
clientVersion: WORKFLOW_CLIENT_VERSION,
|
|
1100
|
+
workflowDefinition: WORKFLOW_DEFINITION,
|
|
1101
|
+
goal,
|
|
1102
|
+
workflowMetadata: JSON.stringify({
|
|
1103
|
+
extended_logging: false
|
|
1104
|
+
}),
|
|
1105
|
+
clientCapabilities: ["shell_command"],
|
|
1106
|
+
mcpTools,
|
|
1107
|
+
additional_context: [],
|
|
1108
|
+
preapproved_tools: preapprovedTools,
|
|
1109
|
+
...this.#toolsConfig?.flowConfig ? {
|
|
1110
|
+
flowConfig: this.#toolsConfig.flowConfig,
|
|
1111
|
+
flowConfigSchemaVersion: this.#toolsConfig.flowConfigSchemaVersion ?? "v1"
|
|
1112
|
+
} : {}
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
this.#startRequestSent = true;
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Send a tool result back to DWS on the existing connection.
|
|
1119
|
+
*/
|
|
1120
|
+
sendToolResult(requestId, output, error) {
|
|
1121
|
+
if (!this.#socket) throw new Error("Not connected");
|
|
1122
|
+
console.error(`[duo-workflow] sendToolResult: requestId=${requestId} output=${output.length} bytes, error=${error ?? "none"}`);
|
|
1123
|
+
this.#socket.send({
|
|
1124
|
+
actionResponse: {
|
|
1125
|
+
requestID: requestId,
|
|
1126
|
+
plainTextResponse: {
|
|
1127
|
+
response: output,
|
|
1128
|
+
error: error ?? ""
|
|
995
1129
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Wait for the next event from the session.
|
|
1135
|
+
* Returns null when the stream is closed (turn complete or connection lost).
|
|
1136
|
+
*/
|
|
1137
|
+
async waitForEvent() {
|
|
1138
|
+
if (!this.#queue) return null;
|
|
1139
|
+
return this.#queue.shift();
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Send an abort signal to DWS and close the connection.
|
|
1143
|
+
*/
|
|
1144
|
+
abort() {
|
|
1145
|
+
this.#socket?.send({ stopWorkflow: { reason: "ABORTED" } });
|
|
1146
|
+
this.#closeConnection();
|
|
1147
|
+
}
|
|
1148
|
+
// ---------------------------------------------------------------------------
|
|
1149
|
+
// Private: action handling
|
|
1150
|
+
// ---------------------------------------------------------------------------
|
|
1151
|
+
#handleAction(action, queue) {
|
|
1152
|
+
if (isCheckpointAction(action)) {
|
|
1153
|
+
const ckpt = action.newCheckpoint.checkpoint;
|
|
1154
|
+
const deltas = extractAgentTextDeltas(ckpt, this.#checkpoint);
|
|
1155
|
+
for (const delta of deltas) {
|
|
1156
|
+
queue.push({ type: "text-delta", value: delta });
|
|
1157
|
+
}
|
|
1158
|
+
const toolRequests = extractToolRequests(ckpt, this.#checkpoint);
|
|
1159
|
+
for (const req of toolRequests) {
|
|
1160
|
+
console.error(`[duo-workflow] checkpoint tool request: ${req.toolName} requestId=${req.requestId}`);
|
|
1161
|
+
queue.push({
|
|
1162
|
+
type: "tool-request",
|
|
1163
|
+
requestId: req.requestId,
|
|
1164
|
+
toolName: req.toolName,
|
|
1165
|
+
args: req.args
|
|
1006
1166
|
});
|
|
1007
1167
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1168
|
+
if (isTurnComplete(action.newCheckpoint.status)) {
|
|
1169
|
+
queue.close();
|
|
1170
|
+
this.#closeConnection();
|
|
1171
|
+
}
|
|
1172
|
+
return;
|
|
1012
1173
|
}
|
|
1174
|
+
if (action.requestID) {
|
|
1175
|
+
console.error(`[duo-workflow] ws tool action: keys=${JSON.stringify(Object.keys(action).filter((k) => k !== "requestID"))}`);
|
|
1176
|
+
this.#executeStandaloneAction(action);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
async #executeStandaloneAction(action) {
|
|
1180
|
+
if (!action.requestID || !this.#socket) return;
|
|
1181
|
+
const result = await this.#toolExecutor.executeAction(action);
|
|
1182
|
+
this.#socket.send({
|
|
1183
|
+
actionResponse: {
|
|
1184
|
+
requestID: action.requestID,
|
|
1185
|
+
plainTextResponse: {
|
|
1186
|
+
response: result.response,
|
|
1187
|
+
error: result.error ?? ""
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
// ---------------------------------------------------------------------------
|
|
1193
|
+
// Private: connection management
|
|
1194
|
+
// ---------------------------------------------------------------------------
|
|
1195
|
+
#closeConnection() {
|
|
1196
|
+
this.#socket?.close();
|
|
1197
|
+
this.#socket = void 0;
|
|
1198
|
+
this.#queue = void 0;
|
|
1199
|
+
this.#startRequestSent = false;
|
|
1013
1200
|
}
|
|
1014
1201
|
async #createWorkflow(goal) {
|
|
1015
1202
|
await this.#loadProjectContext();
|
|
@@ -1018,9 +1205,7 @@ var WorkflowSession = class {
|
|
|
1018
1205
|
workflow_definition: WORKFLOW_DEFINITION,
|
|
1019
1206
|
environment: WORKFLOW_ENVIRONMENT,
|
|
1020
1207
|
allow_agent_to_request_user: true,
|
|
1021
|
-
...this.#projectPath ? {
|
|
1022
|
-
project_id: this.#projectPath
|
|
1023
|
-
} : {}
|
|
1208
|
+
...this.#projectPath ? { project_id: this.#projectPath } : {}
|
|
1024
1209
|
};
|
|
1025
1210
|
const created = await post(this.#client, "ai/duo_workflows/workflows", body);
|
|
1026
1211
|
if (created.id === void 0 || created.id === null) {
|
|
@@ -1071,6 +1256,151 @@ function stripSystemReminders(value) {
|
|
|
1071
1256
|
}).trim();
|
|
1072
1257
|
}
|
|
1073
1258
|
|
|
1259
|
+
// src/provider/prompt-utils.ts
|
|
1260
|
+
function extractToolResults(prompt) {
|
|
1261
|
+
if (!Array.isArray(prompt)) return [];
|
|
1262
|
+
const results = [];
|
|
1263
|
+
for (const message of prompt) {
|
|
1264
|
+
const content = message.content;
|
|
1265
|
+
if (!Array.isArray(content)) continue;
|
|
1266
|
+
for (const part of content) {
|
|
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 });
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return results;
|
|
1277
|
+
}
|
|
1278
|
+
function parseToolResultOutput(part) {
|
|
1279
|
+
const outputField = part.output;
|
|
1280
|
+
const resultField = part.result;
|
|
1281
|
+
if (isPlainObject(outputField) && "type" in outputField) {
|
|
1282
|
+
const outputType = String(outputField.type);
|
|
1283
|
+
const outputValue = outputField.value;
|
|
1284
|
+
if (outputType === "text" || outputType === "json") {
|
|
1285
|
+
return { output: typeof outputValue === "string" ? outputValue : JSON.stringify(outputValue ?? "") };
|
|
1286
|
+
}
|
|
1287
|
+
if (outputType === "error-text" || outputType === "error-json") {
|
|
1288
|
+
return { output: "", error: typeof outputValue === "string" ? outputValue : JSON.stringify(outputValue ?? "") };
|
|
1289
|
+
}
|
|
1290
|
+
if (outputType === "content" && Array.isArray(outputValue)) {
|
|
1291
|
+
const text2 = outputValue.filter((v) => v.type === "text").map((v) => String(v.text ?? "")).join("\n");
|
|
1292
|
+
return { output: text2 };
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
if (outputField !== void 0) {
|
|
1296
|
+
return { output: typeof outputField === "string" ? outputField : JSON.stringify(outputField) };
|
|
1297
|
+
}
|
|
1298
|
+
if (resultField !== void 0) {
|
|
1299
|
+
return { output: typeof resultField === "string" ? resultField : JSON.stringify(resultField) };
|
|
1300
|
+
}
|
|
1301
|
+
return { output: "" };
|
|
1302
|
+
}
|
|
1303
|
+
function isPlainObject(value) {
|
|
1304
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// src/provider/tool-mapping.ts
|
|
1308
|
+
function mapDuoToolRequest(toolName, args) {
|
|
1309
|
+
switch (toolName) {
|
|
1310
|
+
case "list_dir": {
|
|
1311
|
+
const directory = asString2(args.directory) ?? ".";
|
|
1312
|
+
return {
|
|
1313
|
+
toolName: "bash",
|
|
1314
|
+
args: { command: `ls -la ${shellQuote(directory)}`, description: "List directory contents", workdir: "." }
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
case "read_file": {
|
|
1318
|
+
const filePath = asString2(args.file_path) ?? asString2(args.filepath) ?? asString2(args.filePath) ?? asString2(args.path);
|
|
1319
|
+
if (!filePath) return { toolName, args };
|
|
1320
|
+
const mapped = { filePath };
|
|
1321
|
+
if (typeof args.offset === "number") mapped.offset = args.offset;
|
|
1322
|
+
if (typeof args.limit === "number") mapped.limit = args.limit;
|
|
1323
|
+
return { toolName: "read", args: mapped };
|
|
1324
|
+
}
|
|
1325
|
+
case "read_files": {
|
|
1326
|
+
const filePaths = asStringArray2(args.file_paths);
|
|
1327
|
+
if (filePaths.length === 0) return { toolName, args };
|
|
1328
|
+
return filePaths.map((fp) => ({ toolName: "read", args: { filePath: fp } }));
|
|
1329
|
+
}
|
|
1330
|
+
case "create_file_with_contents": {
|
|
1331
|
+
const filePath = asString2(args.file_path);
|
|
1332
|
+
const content = asString2(args.contents);
|
|
1333
|
+
if (!filePath || content === void 0) return { toolName, args };
|
|
1334
|
+
return { toolName: "write", args: { filePath, content } };
|
|
1335
|
+
}
|
|
1336
|
+
case "edit_file": {
|
|
1337
|
+
const filePath = asString2(args.file_path);
|
|
1338
|
+
const oldString = asString2(args.old_str);
|
|
1339
|
+
const newString = asString2(args.new_str);
|
|
1340
|
+
if (!filePath || oldString === void 0 || newString === void 0) return { toolName, args };
|
|
1341
|
+
return { toolName: "edit", args: { filePath, oldString, newString } };
|
|
1342
|
+
}
|
|
1343
|
+
case "find_files": {
|
|
1344
|
+
const pattern = asString2(args.name_pattern);
|
|
1345
|
+
if (!pattern) return { toolName, args };
|
|
1346
|
+
return { toolName: "glob", args: { pattern } };
|
|
1347
|
+
}
|
|
1348
|
+
case "grep": {
|
|
1349
|
+
const pattern = asString2(args.pattern);
|
|
1350
|
+
if (!pattern) return { toolName, args };
|
|
1351
|
+
const searchDir = asString2(args.search_directory);
|
|
1352
|
+
const caseInsensitive = Boolean(args.case_insensitive);
|
|
1353
|
+
const normalizedPattern = caseInsensitive && !pattern.startsWith("(?i)") ? `(?i)${pattern}` : pattern;
|
|
1354
|
+
const mapped = { pattern: normalizedPattern };
|
|
1355
|
+
if (searchDir) mapped.path = searchDir;
|
|
1356
|
+
return { toolName: "grep", args: mapped };
|
|
1357
|
+
}
|
|
1358
|
+
case "mkdir": {
|
|
1359
|
+
const directory = asString2(args.directory_path);
|
|
1360
|
+
if (!directory) return { toolName, args };
|
|
1361
|
+
return { toolName: "bash", args: { command: `mkdir -p ${shellQuote(directory)}`, description: "Create directory", workdir: "." } };
|
|
1362
|
+
}
|
|
1363
|
+
case "shell_command": {
|
|
1364
|
+
const command = asString2(args.command);
|
|
1365
|
+
if (!command) return { toolName, args };
|
|
1366
|
+
return { toolName: "bash", args: { command, description: "Run shell command", workdir: "." } };
|
|
1367
|
+
}
|
|
1368
|
+
case "run_command": {
|
|
1369
|
+
const program = asString2(args.program);
|
|
1370
|
+
if (program) {
|
|
1371
|
+
const parts = [shellQuote(program)];
|
|
1372
|
+
if (Array.isArray(args.flags)) parts.push(...args.flags.map((f) => shellQuote(String(f))));
|
|
1373
|
+
if (Array.isArray(args.arguments)) parts.push(...args.arguments.map((a) => shellQuote(String(a))));
|
|
1374
|
+
return { toolName: "bash", args: { command: parts.join(" "), description: "Run command", workdir: "." } };
|
|
1375
|
+
}
|
|
1376
|
+
const command = asString2(args.command);
|
|
1377
|
+
if (!command) return { toolName, args };
|
|
1378
|
+
return { toolName: "bash", args: { command, description: "Run command", workdir: "." } };
|
|
1379
|
+
}
|
|
1380
|
+
case "run_git_command": {
|
|
1381
|
+
const command = asString2(args.command);
|
|
1382
|
+
if (!command) return { toolName, args };
|
|
1383
|
+
const rawArgs = args.args;
|
|
1384
|
+
const extraArgs = Array.isArray(rawArgs) ? rawArgs.map((v) => shellQuote(String(v))).join(" ") : asString2(rawArgs);
|
|
1385
|
+
const gitCmd = extraArgs ? `git ${shellQuote(command)} ${extraArgs}` : `git ${shellQuote(command)}`;
|
|
1386
|
+
return { toolName: "bash", args: { command: gitCmd, description: "Run git command", workdir: "." } };
|
|
1387
|
+
}
|
|
1388
|
+
default:
|
|
1389
|
+
return { toolName, args };
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
function asString2(value) {
|
|
1393
|
+
return typeof value === "string" ? value : void 0;
|
|
1394
|
+
}
|
|
1395
|
+
function asStringArray2(value) {
|
|
1396
|
+
if (!Array.isArray(value)) return [];
|
|
1397
|
+
return value.filter((v) => typeof v === "string");
|
|
1398
|
+
}
|
|
1399
|
+
function shellQuote(s) {
|
|
1400
|
+
if (/^[a-zA-Z0-9_\-./=:@]+$/.test(s)) return s;
|
|
1401
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1074
1404
|
// src/provider/session-context.ts
|
|
1075
1405
|
function readSessionID(options) {
|
|
1076
1406
|
const providerBlock = readProviderBlock(options);
|
|
@@ -1106,6 +1436,12 @@ var DuoWorkflowModel = class {
|
|
|
1106
1436
|
#client;
|
|
1107
1437
|
#cwd;
|
|
1108
1438
|
#toolsConfig;
|
|
1439
|
+
// Tool tracking state (per model instance, reset on session change)
|
|
1440
|
+
#pendingToolRequests = /* @__PURE__ */ new Map();
|
|
1441
|
+
#multiCallGroups = /* @__PURE__ */ new Map();
|
|
1442
|
+
#sentToolCallIds = /* @__PURE__ */ new Set();
|
|
1443
|
+
#lastSentGoal = null;
|
|
1444
|
+
#stateSessionId;
|
|
1109
1445
|
constructor(modelId, client, cwd) {
|
|
1110
1446
|
this.modelId = modelId;
|
|
1111
1447
|
this.#client = client;
|
|
@@ -1113,8 +1449,6 @@ var DuoWorkflowModel = class {
|
|
|
1113
1449
|
}
|
|
1114
1450
|
/**
|
|
1115
1451
|
* Opt-in: override the server-side system prompt and/or register MCP tools.
|
|
1116
|
-
* When not called, the server uses its default prompt and built-in tools.
|
|
1117
|
-
* Tool execution is always bridged locally regardless of this setting.
|
|
1118
1452
|
*/
|
|
1119
1453
|
setToolsConfig(config) {
|
|
1120
1454
|
this.#toolsConfig = config;
|
|
@@ -1123,22 +1457,13 @@ var DuoWorkflowModel = class {
|
|
|
1123
1457
|
}
|
|
1124
1458
|
}
|
|
1125
1459
|
async doGenerate(options) {
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
const session = this.#resolveSession(sessionID);
|
|
1131
|
-
const chunks = [];
|
|
1132
|
-
for await (const item of session.runTurn(goal, options.abortSignal)) {
|
|
1133
|
-
if (item.type === "text-delta") chunks.push(item.value);
|
|
1460
|
+
let text2 = "";
|
|
1461
|
+
const { stream } = await this.doStream(options);
|
|
1462
|
+
for await (const part of stream) {
|
|
1463
|
+
if (part.type === "text-delta") text2 += part.delta;
|
|
1134
1464
|
}
|
|
1135
1465
|
return {
|
|
1136
|
-
content: [
|
|
1137
|
-
{
|
|
1138
|
-
type: "text",
|
|
1139
|
-
text: chunks.join("")
|
|
1140
|
-
}
|
|
1141
|
-
],
|
|
1466
|
+
content: [{ type: "text", text: text2 }],
|
|
1142
1467
|
finishReason: "stop",
|
|
1143
1468
|
usage: UNKNOWN_USAGE,
|
|
1144
1469
|
warnings: []
|
|
@@ -1148,57 +1473,143 @@ var DuoWorkflowModel = class {
|
|
|
1148
1473
|
const sessionID = readSessionID(options);
|
|
1149
1474
|
if (!sessionID) throw new Error("missing workflow session ID");
|
|
1150
1475
|
const goal = extractGoal(options.prompt);
|
|
1151
|
-
|
|
1476
|
+
const toolResults = extractToolResults(options.prompt);
|
|
1152
1477
|
const session = this.#resolveSession(sessionID);
|
|
1153
|
-
const textId =
|
|
1478
|
+
const textId = randomUUID3();
|
|
1479
|
+
if (sessionID !== this.#stateSessionId) {
|
|
1480
|
+
this.#pendingToolRequests.clear();
|
|
1481
|
+
this.#multiCallGroups.clear();
|
|
1482
|
+
this.#sentToolCallIds.clear();
|
|
1483
|
+
this.#lastSentGoal = null;
|
|
1484
|
+
this.#stateSessionId = sessionID;
|
|
1485
|
+
}
|
|
1486
|
+
const model = this;
|
|
1154
1487
|
return {
|
|
1155
1488
|
stream: new ReadableStream({
|
|
1156
1489
|
start: async (controller) => {
|
|
1157
|
-
controller.enqueue({
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
});
|
|
1161
|
-
let hasText = false;
|
|
1490
|
+
controller.enqueue({ type: "stream-start", warnings: [] });
|
|
1491
|
+
const onAbort = () => session.abort();
|
|
1492
|
+
options.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
1162
1493
|
try {
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1494
|
+
const freshResults = toolResults.filter(
|
|
1495
|
+
(r) => !model.#sentToolCallIds.has(r.toolCallId)
|
|
1496
|
+
);
|
|
1497
|
+
let sentToolResults = false;
|
|
1498
|
+
for (const result of freshResults) {
|
|
1499
|
+
const hashIdx = result.toolCallId.indexOf("#");
|
|
1500
|
+
if (hashIdx !== -1) {
|
|
1501
|
+
const originalId = result.toolCallId.substring(0, hashIdx);
|
|
1502
|
+
const group = model.#multiCallGroups.get(originalId);
|
|
1503
|
+
if (!group) {
|
|
1504
|
+
model.#sentToolCallIds.add(result.toolCallId);
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
group.collected.set(result.toolCallId, result.error ?? result.output);
|
|
1508
|
+
model.#sentToolCallIds.add(result.toolCallId);
|
|
1509
|
+
model.#pendingToolRequests.delete(result.toolCallId);
|
|
1510
|
+
if (group.collected.size === group.subIds.length) {
|
|
1511
|
+
const aggregated = group.subIds.map((id) => group.collected.get(id) ?? "").join("\n");
|
|
1512
|
+
session.sendToolResult(originalId, aggregated);
|
|
1513
|
+
model.#multiCallGroups.delete(originalId);
|
|
1514
|
+
model.#pendingToolRequests.delete(originalId);
|
|
1515
|
+
sentToolResults = true;
|
|
1516
|
+
}
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
const pending = model.#pendingToolRequests.get(result.toolCallId);
|
|
1520
|
+
if (!pending) {
|
|
1521
|
+
model.#sentToolCallIds.add(result.toolCallId);
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
session.sendToolResult(result.toolCallId, result.output, result.error);
|
|
1525
|
+
sentToolResults = true;
|
|
1526
|
+
model.#sentToolCallIds.add(result.toolCallId);
|
|
1527
|
+
model.#pendingToolRequests.delete(result.toolCallId);
|
|
1528
|
+
}
|
|
1529
|
+
const isNewGoal = goal && goal !== model.#lastSentGoal;
|
|
1530
|
+
if (!sentToolResults && isNewGoal) {
|
|
1531
|
+
await session.ensureConnected(goal);
|
|
1532
|
+
if (!session.hasStarted) {
|
|
1533
|
+
session.sendStartRequest(goal);
|
|
1534
|
+
}
|
|
1535
|
+
model.#lastSentGoal = goal;
|
|
1536
|
+
}
|
|
1537
|
+
let hasText = false;
|
|
1538
|
+
while (true) {
|
|
1539
|
+
const event = await session.waitForEvent();
|
|
1540
|
+
if (!event) break;
|
|
1541
|
+
if (event.type === "text-delta") {
|
|
1542
|
+
if (!event.value) continue;
|
|
1543
|
+
if (!hasText) {
|
|
1544
|
+
hasText = true;
|
|
1545
|
+
controller.enqueue({ type: "text-start", id: textId });
|
|
1546
|
+
}
|
|
1547
|
+
controller.enqueue({ type: "text-delta", id: textId, delta: event.value });
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
if (event.type === "tool-request") {
|
|
1551
|
+
let mapped;
|
|
1552
|
+
try {
|
|
1553
|
+
mapped = mapDuoToolRequest(event.toolName, event.args);
|
|
1554
|
+
} catch {
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1557
|
+
if (hasText) {
|
|
1558
|
+
controller.enqueue({ type: "text-end", id: textId });
|
|
1559
|
+
}
|
|
1560
|
+
if (Array.isArray(mapped)) {
|
|
1561
|
+
const subIds = mapped.map((_, i) => `${event.requestId}#${i}`);
|
|
1562
|
+
model.#multiCallGroups.set(event.requestId, {
|
|
1563
|
+
subIds,
|
|
1564
|
+
collected: /* @__PURE__ */ new Map()
|
|
1565
|
+
});
|
|
1566
|
+
model.#pendingToolRequests.set(event.requestId, {});
|
|
1567
|
+
for (const subId of subIds) {
|
|
1568
|
+
model.#pendingToolRequests.set(subId, {});
|
|
1569
|
+
}
|
|
1570
|
+
for (let i = 0; i < mapped.length; i++) {
|
|
1571
|
+
controller.enqueue({
|
|
1572
|
+
type: "tool-call",
|
|
1573
|
+
toolCallId: subIds[i],
|
|
1574
|
+
toolName: mapped[i].toolName,
|
|
1575
|
+
input: JSON.stringify(mapped[i].args)
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
} else {
|
|
1579
|
+
model.#pendingToolRequests.set(event.requestId, {});
|
|
1580
|
+
controller.enqueue({
|
|
1581
|
+
type: "tool-call",
|
|
1582
|
+
toolCallId: event.requestId,
|
|
1583
|
+
toolName: mapped.toolName,
|
|
1584
|
+
input: JSON.stringify(mapped.args)
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1168
1587
|
controller.enqueue({
|
|
1169
|
-
type: "
|
|
1170
|
-
|
|
1588
|
+
type: "finish",
|
|
1589
|
+
finishReason: "tool-calls",
|
|
1590
|
+
usage: UNKNOWN_USAGE
|
|
1171
1591
|
});
|
|
1592
|
+
controller.close();
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
if (event.type === "error") {
|
|
1596
|
+
controller.enqueue({ type: "error", error: new Error(event.message) });
|
|
1597
|
+
controller.enqueue({ type: "finish", finishReason: "error", usage: UNKNOWN_USAGE });
|
|
1598
|
+
controller.close();
|
|
1599
|
+
return;
|
|
1172
1600
|
}
|
|
1173
|
-
controller.enqueue({
|
|
1174
|
-
type: "text-delta",
|
|
1175
|
-
id: textId,
|
|
1176
|
-
delta: item.value
|
|
1177
|
-
});
|
|
1178
1601
|
}
|
|
1179
1602
|
if (hasText) {
|
|
1180
|
-
controller.enqueue({
|
|
1181
|
-
type: "text-end",
|
|
1182
|
-
id: textId
|
|
1183
|
-
});
|
|
1603
|
+
controller.enqueue({ type: "text-end", id: textId });
|
|
1184
1604
|
}
|
|
1185
|
-
controller.enqueue({
|
|
1186
|
-
type: "finish",
|
|
1187
|
-
finishReason: "stop",
|
|
1188
|
-
usage: UNKNOWN_USAGE
|
|
1189
|
-
});
|
|
1605
|
+
controller.enqueue({ type: "finish", finishReason: "stop", usage: UNKNOWN_USAGE });
|
|
1190
1606
|
controller.close();
|
|
1191
1607
|
} catch (error) {
|
|
1192
|
-
controller.enqueue({
|
|
1193
|
-
|
|
1194
|
-
error
|
|
1195
|
-
});
|
|
1196
|
-
controller.enqueue({
|
|
1197
|
-
type: "finish",
|
|
1198
|
-
finishReason: "error",
|
|
1199
|
-
usage: UNKNOWN_USAGE
|
|
1200
|
-
});
|
|
1608
|
+
controller.enqueue({ type: "error", error });
|
|
1609
|
+
controller.enqueue({ type: "finish", finishReason: "error", usage: UNKNOWN_USAGE });
|
|
1201
1610
|
controller.close();
|
|
1611
|
+
} finally {
|
|
1612
|
+
options.abortSignal?.removeEventListener("abort", onAbort);
|
|
1202
1613
|
}
|
|
1203
1614
|
}
|
|
1204
1615
|
}),
|