@diologue/local-agent 0.2.0 → 0.3.0
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/cli.mjs +313 -42
- package/dist/cli.mjs.map +2 -2
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -167,7 +167,7 @@ import { access as access3, constants as constants2 } from "node:fs/promises";
|
|
|
167
167
|
import { spawn as spawn2 } from "node:child_process";
|
|
168
168
|
import path4 from "node:path";
|
|
169
169
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
170
|
-
var __filename2, __dirname2, REPO_ROOT2, DEFAULT_ENGINE_DIR, ENTRY_REL, DEFAULT_STARTUP_TIMEOUT_MS, LISTENING_REGEX, defaultRuntimeCheck, exists3, defaultClientFactory, LocalEngineLocator, truncate;
|
|
170
|
+
var __filename2, __dirname2, REPO_ROOT2, DEFAULT_ENGINE_DIR, ENTRY_REL, DEFAULT_STARTUP_TIMEOUT_MS, LISTENING_REGEX, defaultRuntimeCheck, exists3, isRecord, parseExistingInlineConfig, mergeInlineConfig, buildEngineEnv, defaultClientFactory, LocalEngineLocator, truncate;
|
|
171
171
|
var init_engine_locator_local = __esm({
|
|
172
172
|
"src/adapters/engine-locator-local.ts"() {
|
|
173
173
|
"use strict";
|
|
@@ -192,6 +192,37 @@ var init_engine_locator_local = __esm({
|
|
|
192
192
|
return false;
|
|
193
193
|
}
|
|
194
194
|
};
|
|
195
|
+
isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
196
|
+
parseExistingInlineConfig = (existing) => {
|
|
197
|
+
if (!existing) return {};
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(existing);
|
|
200
|
+
return isRecord(parsed) ? parsed : {};
|
|
201
|
+
} catch {
|
|
202
|
+
return {};
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
mergeInlineConfig = (existing, injected) => {
|
|
206
|
+
if (!injected) return existing;
|
|
207
|
+
const base = parseExistingInlineConfig(existing);
|
|
208
|
+
const baseProvider = isRecord(base.provider) ? base.provider : {};
|
|
209
|
+
return JSON.stringify({
|
|
210
|
+
...base,
|
|
211
|
+
...injected,
|
|
212
|
+
provider: {
|
|
213
|
+
...baseProvider,
|
|
214
|
+
...injected.provider ?? {}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
buildEngineEnv = (config) => {
|
|
219
|
+
const env = { ...process.env };
|
|
220
|
+
const content = mergeInlineConfig(env.OPENCODE_CONFIG_CONTENT, config);
|
|
221
|
+
if (content) {
|
|
222
|
+
env.OPENCODE_CONFIG_CONTENT = content;
|
|
223
|
+
}
|
|
224
|
+
return env;
|
|
225
|
+
};
|
|
195
226
|
defaultClientFactory = async (baseUrl) => {
|
|
196
227
|
const sdk = await import("@opencode-ai/sdk");
|
|
197
228
|
return sdk.createOpencodeClient({ baseUrl });
|
|
@@ -281,13 +312,12 @@ var init_engine_locator_local = __esm({
|
|
|
281
312
|
const child = spawn2(command, args, {
|
|
282
313
|
cwd,
|
|
283
314
|
stdio: ["ignore", "pipe", "pipe"],
|
|
284
|
-
// Inherit env so the engine sees any LOCAL_AGENT_* /
|
|
285
|
-
// credentials the parent has,
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
env: process.env
|
|
315
|
+
// Inherit env so the engine sees any LOCAL_AGENT_* / provider
|
|
316
|
+
// credentials the parent has, then inject the brokered provider as
|
|
317
|
+
// inline opencode config. Without this, the default local engine path
|
|
318
|
+
// does not know the diologue/diologue-routed model, so the prompt
|
|
319
|
+
// fails before edit tools can create files in the selected repo.
|
|
320
|
+
env: buildEngineEnv(options.config)
|
|
291
321
|
});
|
|
292
322
|
let url;
|
|
293
323
|
try {
|
|
@@ -504,6 +534,9 @@ var createCorsMiddleware = (options) => {
|
|
|
504
534
|
`Content-Type, ${LOCAL_AGENT_TOKEN_HEADER}`
|
|
505
535
|
);
|
|
506
536
|
res.setHeader("Access-Control-Max-Age", "300");
|
|
537
|
+
if (req.header("access-control-request-private-network") === "true") {
|
|
538
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
539
|
+
}
|
|
507
540
|
}
|
|
508
541
|
if (req.method === "OPTIONS") {
|
|
509
542
|
res.status(204).end();
|
|
@@ -641,8 +674,56 @@ var getStatusShort = async (cwd) => {
|
|
|
641
674
|
const out = await runGit(cwd, ["status", "--short", "-uall"]);
|
|
642
675
|
return parseShortStatus(out);
|
|
643
676
|
};
|
|
677
|
+
var runGitAllowingExit = (cwd, args, allowedExitCodes) => new Promise((resolve, reject) => {
|
|
678
|
+
execFile(
|
|
679
|
+
"git",
|
|
680
|
+
args,
|
|
681
|
+
{
|
|
682
|
+
cwd,
|
|
683
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
684
|
+
env: { ...process.env, GIT_PAGER: "cat", PAGER: "cat" }
|
|
685
|
+
},
|
|
686
|
+
(err, stdout, stderr) => {
|
|
687
|
+
if (!err) {
|
|
688
|
+
resolve(stdout);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const e = err;
|
|
692
|
+
const exitCode = e.code ?? -1;
|
|
693
|
+
if (allowedExitCodes.has(exitCode)) {
|
|
694
|
+
resolve(stdout);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
reject(new GitCommandError(args, exitCode, stderr));
|
|
698
|
+
}
|
|
699
|
+
);
|
|
700
|
+
});
|
|
701
|
+
var getUntrackedFiles = async (cwd) => {
|
|
702
|
+
const out = await runGit(cwd, [
|
|
703
|
+
"ls-files",
|
|
704
|
+
"--others",
|
|
705
|
+
"--exclude-standard",
|
|
706
|
+
"-z"
|
|
707
|
+
]);
|
|
708
|
+
return out.split("\0").filter(Boolean);
|
|
709
|
+
};
|
|
710
|
+
var getUntrackedFileDiff = async (cwd, file) => {
|
|
711
|
+
const diff = await runGitAllowingExit(
|
|
712
|
+
cwd,
|
|
713
|
+
["diff", "--no-index", "--", "/dev/null", file],
|
|
714
|
+
/* @__PURE__ */ new Set([1])
|
|
715
|
+
);
|
|
716
|
+
return diff.replace(/^diff --git a\/dev\/null b\/(.+)$/m, "diff --git a/$1 b/$1").replace(/^--- \/dev\/null$/m, "--- /dev/null");
|
|
717
|
+
};
|
|
644
718
|
var getDiff = async (cwd) => {
|
|
645
|
-
|
|
719
|
+
const [trackedDiff, untrackedFiles] = await Promise.all([
|
|
720
|
+
runGit(cwd, ["diff", "HEAD"]),
|
|
721
|
+
getUntrackedFiles(cwd)
|
|
722
|
+
]);
|
|
723
|
+
const untrackedDiffs = await Promise.all(
|
|
724
|
+
untrackedFiles.map((file) => getUntrackedFileDiff(cwd, file))
|
|
725
|
+
);
|
|
726
|
+
return [trackedDiff, ...untrackedDiffs].filter((part) => part.trim().length > 0).join("\n");
|
|
646
727
|
};
|
|
647
728
|
var runGitWithStdin = async (cwd, args, input) => {
|
|
648
729
|
const { execFile: execFile2 } = await import("node:child_process");
|
|
@@ -682,6 +763,20 @@ var canApplyDiff = async (cwd, unified) => {
|
|
|
682
763
|
var applyDiff = async (cwd, unified) => {
|
|
683
764
|
await runGitWithStdin(cwd, ["apply"], unified);
|
|
684
765
|
};
|
|
766
|
+
var canRevertDiff = async (cwd, unified) => {
|
|
767
|
+
try {
|
|
768
|
+
await runGitWithStdin(cwd, ["apply", "--reverse", "--check"], unified);
|
|
769
|
+
return { ok: true };
|
|
770
|
+
} catch (err) {
|
|
771
|
+
if (err instanceof GitCommandError) {
|
|
772
|
+
return { ok: false, stderr: err.stderr };
|
|
773
|
+
}
|
|
774
|
+
throw err;
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
var revertDiff = async (cwd, unified) => {
|
|
778
|
+
await runGitWithStdin(cwd, ["apply", "--reverse"], unified);
|
|
779
|
+
};
|
|
685
780
|
var parseDiffPaths = (unified) => {
|
|
686
781
|
const paths = [];
|
|
687
782
|
for (const line of unified.split("\n")) {
|
|
@@ -761,6 +856,9 @@ var applyPatchSchema = z.object({
|
|
|
761
856
|
unified: z.string().min(1).max(8 * 1024 * 1024),
|
|
762
857
|
baselineHash: z.string().optional()
|
|
763
858
|
});
|
|
859
|
+
var revertPatchSchema = z.object({
|
|
860
|
+
unified: z.string().min(1).max(8 * 1024 * 1024)
|
|
861
|
+
});
|
|
764
862
|
var buildRepoStatus = async (resolvedPath) => {
|
|
765
863
|
const [branch, head, dirty] = await Promise.all([
|
|
766
864
|
getBranch(resolvedPath),
|
|
@@ -910,6 +1008,40 @@ var createRepoRouter = (state) => {
|
|
|
910
1008
|
throw err;
|
|
911
1009
|
}
|
|
912
1010
|
});
|
|
1011
|
+
router.post("/revert", async (req, res) => {
|
|
1012
|
+
const repo = requireSelectedRepo(state, res);
|
|
1013
|
+
if (!repo) return;
|
|
1014
|
+
const parsed = revertPatchSchema.safeParse(req.body);
|
|
1015
|
+
if (!parsed.success) {
|
|
1016
|
+
res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
const check = await canRevertDiff(repo.path, parsed.data.unified);
|
|
1021
|
+
if (!check.ok) {
|
|
1022
|
+
res.status(409).json({
|
|
1023
|
+
error: "diff_does_not_revert",
|
|
1024
|
+
message: "These changes can't be reverted cleanly. The working tree may have changed since the agent wrote them, or they were already reverted.",
|
|
1025
|
+
stderr: check.stderr
|
|
1026
|
+
});
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
await revertDiff(repo.path, parsed.data.unified);
|
|
1030
|
+
const paths = parseDiffPaths(parsed.data.unified);
|
|
1031
|
+
const body = {
|
|
1032
|
+
ok: true,
|
|
1033
|
+
filesChanged: paths.length,
|
|
1034
|
+
paths
|
|
1035
|
+
};
|
|
1036
|
+
res.json(body);
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
if (err instanceof GitCommandError) {
|
|
1039
|
+
sendGitError(res, err);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
throw err;
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
913
1045
|
return router;
|
|
914
1046
|
};
|
|
915
1047
|
|
|
@@ -919,6 +1051,9 @@ import { z as z2 } from "zod";
|
|
|
919
1051
|
|
|
920
1052
|
// src/broker.ts
|
|
921
1053
|
import { randomUUID } from "node:crypto";
|
|
1054
|
+
var logBroker = (message) => {
|
|
1055
|
+
console.error(`[llm-broker] ${message}`);
|
|
1056
|
+
};
|
|
922
1057
|
var LlmBroker = class {
|
|
923
1058
|
pending = /* @__PURE__ */ new Map();
|
|
924
1059
|
emit;
|
|
@@ -948,13 +1083,18 @@ var LlmBroker = class {
|
|
|
948
1083
|
return new Promise((resolve, reject) => {
|
|
949
1084
|
const timer = setTimeout(() => {
|
|
950
1085
|
this.pending.delete(requestId);
|
|
1086
|
+
logBroker(
|
|
1087
|
+
`timeout request=${requestId.slice(0, 8)} pending=${this.pending.size} after=${this.timeoutMs}ms`
|
|
1088
|
+
);
|
|
951
1089
|
reject(
|
|
952
1090
|
new Error(
|
|
953
1091
|
`llm_broker_timeout: no response within ${this.timeoutMs}ms`
|
|
954
1092
|
)
|
|
955
1093
|
);
|
|
956
1094
|
}, this.timeoutMs);
|
|
957
|
-
|
|
1095
|
+
if (this.timeoutMs >= 1e3) {
|
|
1096
|
+
timer.unref?.();
|
|
1097
|
+
}
|
|
958
1098
|
this.pending.set(requestId, { resolve, reject, timer, observer });
|
|
959
1099
|
const effectiveProvider = this.overrideProvider ?? payload.provider;
|
|
960
1100
|
const effectiveModel = this.overrideModel ?? payload.model;
|
|
@@ -971,6 +1111,9 @@ var LlmBroker = class {
|
|
|
971
1111
|
}
|
|
972
1112
|
};
|
|
973
1113
|
try {
|
|
1114
|
+
logBroker(
|
|
1115
|
+
`emit request=${requestId.slice(0, 8)} provider=${effectiveProvider ?? "(default)"} model=${effectiveModel ?? "(default)"} messages=${payload.messages.length} tools=${payload.tools?.length ?? 0}`
|
|
1116
|
+
);
|
|
974
1117
|
this.emit(envelope);
|
|
975
1118
|
} catch (err) {
|
|
976
1119
|
clearTimeout(timer);
|
|
@@ -995,7 +1138,14 @@ var LlmBroker = class {
|
|
|
995
1138
|
* route (e.g. for providers that don't support streaming). */
|
|
996
1139
|
fulfill(requestId, response) {
|
|
997
1140
|
const entry = this.pending.get(requestId);
|
|
998
|
-
if (!entry)
|
|
1141
|
+
if (!entry) {
|
|
1142
|
+
logBroker(`fulfill_miss request=${requestId.slice(0, 8)}`);
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
const toolNames = (response.toolCalls ?? []).map((tc) => tc.function?.name ?? "?").join(",");
|
|
1146
|
+
logBroker(
|
|
1147
|
+
`fulfill request=${requestId.slice(0, 8)} text=${response.text.length} toolCalls=${response.toolCalls?.length ?? 0}${toolNames ? ` tools=[${toolNames}]` : ""} finish=${response.finishReason ?? "(none)"}`
|
|
1148
|
+
);
|
|
999
1149
|
clearTimeout(entry.timer);
|
|
1000
1150
|
this.pending.delete(requestId);
|
|
1001
1151
|
if (entry.observer) {
|
|
@@ -1024,7 +1174,15 @@ var LlmBroker = class {
|
|
|
1024
1174
|
* invoked; a `complete` chunk also resolves the pending Promise. */
|
|
1025
1175
|
pushChunk(requestId, chunk) {
|
|
1026
1176
|
const entry = this.pending.get(requestId);
|
|
1027
|
-
if (!entry)
|
|
1177
|
+
if (!entry) {
|
|
1178
|
+
logBroker(
|
|
1179
|
+
`chunk_miss request=${requestId.slice(0, 8)} type=${chunk.type}`
|
|
1180
|
+
);
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
logBroker(
|
|
1184
|
+
`chunk request=${requestId.slice(0, 8)} type=${chunk.type}${chunk.type === "text_delta" ? ` chars=${chunk.text.length}` : ""}${chunk.type === "tool_call_delta" ? ` index=${chunk.index} name=${chunk.name ?? "(delta)"} argsDelta=${chunk.argumentsDelta?.length ?? 0}` : ""}`
|
|
1185
|
+
);
|
|
1028
1186
|
if (entry.observer) {
|
|
1029
1187
|
try {
|
|
1030
1188
|
entry.observer(chunk);
|
|
@@ -1046,7 +1204,11 @@ var LlmBroker = class {
|
|
|
1046
1204
|
* failure rather than a completion. */
|
|
1047
1205
|
fail(requestId, message) {
|
|
1048
1206
|
const entry = this.pending.get(requestId);
|
|
1049
|
-
if (!entry)
|
|
1207
|
+
if (!entry) {
|
|
1208
|
+
logBroker(`fail_miss request=${requestId.slice(0, 8)}`);
|
|
1209
|
+
return false;
|
|
1210
|
+
}
|
|
1211
|
+
logBroker(`fail request=${requestId.slice(0, 8)} message=${message}`);
|
|
1050
1212
|
clearTimeout(entry.timer);
|
|
1051
1213
|
this.pending.delete(requestId);
|
|
1052
1214
|
entry.reject(new Error(message));
|
|
@@ -1055,6 +1217,9 @@ var LlmBroker = class {
|
|
|
1055
1217
|
/** Reject all in-flight requests. Called when the parent SSE stream
|
|
1056
1218
|
* closes so a stranded request doesn't sit forever. */
|
|
1057
1219
|
close(reason = "broker_closed") {
|
|
1220
|
+
if (this.pending.size > 0) {
|
|
1221
|
+
logBroker(`close reason=${reason} pending=${this.pending.size}`);
|
|
1222
|
+
}
|
|
1058
1223
|
this.closed = true;
|
|
1059
1224
|
for (const [, entry] of this.pending.entries()) {
|
|
1060
1225
|
clearTimeout(entry.timer);
|
|
@@ -1111,6 +1276,9 @@ var writeEvent = (res, event) => {
|
|
|
1111
1276
|
var writeDone = (res) => {
|
|
1112
1277
|
res.write("data: [DONE]\n\n");
|
|
1113
1278
|
};
|
|
1279
|
+
var logAgentRoute = (message) => {
|
|
1280
|
+
console.error(`[agent-route] ${message}`);
|
|
1281
|
+
};
|
|
1114
1282
|
var createAgentRouter = (deps) => {
|
|
1115
1283
|
const router = createRouter2();
|
|
1116
1284
|
router.post("/message", async (req, res) => {
|
|
@@ -1124,6 +1292,9 @@ var createAgentRouter = (deps) => {
|
|
|
1124
1292
|
res.status(409).json({ error: "no_repo_selected" });
|
|
1125
1293
|
return;
|
|
1126
1294
|
}
|
|
1295
|
+
logAgentRoute(
|
|
1296
|
+
`message start session=${parsed.data.sessionId.slice(0, 8)} repo=${repo.path} promptChars=${parsed.data.prompt.length} provider=${parsed.data.preferredProvider ?? "(default)"} model=${parsed.data.preferredModel ?? "(default)"}`
|
|
1297
|
+
);
|
|
1127
1298
|
res.setHeader("Content-Type", "text/event-stream");
|
|
1128
1299
|
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
1129
1300
|
res.setHeader("Connection", "keep-alive");
|
|
@@ -1149,21 +1320,33 @@ var createAgentRouter = (deps) => {
|
|
|
1149
1320
|
prompt: parsed.data.prompt,
|
|
1150
1321
|
history,
|
|
1151
1322
|
signal: controller.signal,
|
|
1152
|
-
broker: (payload) => broker.request(payload),
|
|
1323
|
+
broker: (payload, observer) => broker.request(payload, observer),
|
|
1153
1324
|
preferredProvider: parsed.data.preferredProvider,
|
|
1154
1325
|
preferredModel: parsed.data.preferredModel
|
|
1155
1326
|
})) {
|
|
1156
1327
|
if (controller.signal.aborted) {
|
|
1328
|
+
logAgentRoute(
|
|
1329
|
+
`message aborted session=${parsed.data.sessionId.slice(0, 8)}`
|
|
1330
|
+
);
|
|
1157
1331
|
break;
|
|
1158
1332
|
}
|
|
1333
|
+
logAgentRoute(
|
|
1334
|
+
`stream event session=${parsed.data.sessionId.slice(0, 8)} type=${event.type}`
|
|
1335
|
+
);
|
|
1159
1336
|
writeEvent(res, event);
|
|
1160
1337
|
}
|
|
1338
|
+
logAgentRoute(
|
|
1339
|
+
`message done session=${parsed.data.sessionId.slice(0, 8)}`
|
|
1340
|
+
);
|
|
1161
1341
|
writeDone(res);
|
|
1162
1342
|
} catch (err) {
|
|
1163
1343
|
if (err && typeof err === "object" && "name" in err && err.name === "AbortError") {
|
|
1164
1344
|
writeDone(res);
|
|
1165
1345
|
} else {
|
|
1166
1346
|
const message = err instanceof Error ? err.message : "Adapter stream failed";
|
|
1347
|
+
logAgentRoute(
|
|
1348
|
+
`message error session=${parsed.data.sessionId.slice(0, 8)} error=${message}`
|
|
1349
|
+
);
|
|
1167
1350
|
writeEvent(res, { type: "error", message });
|
|
1168
1351
|
writeDone(res);
|
|
1169
1352
|
}
|
|
@@ -1180,7 +1363,10 @@ var llmResponseBodySchema = z2.object({
|
|
|
1180
1363
|
text: z2.string(),
|
|
1181
1364
|
provider: z2.string().min(1),
|
|
1182
1365
|
model: z2.string().min(1),
|
|
1183
|
-
tokenUsage: z2.object({
|
|
1366
|
+
tokenUsage: z2.object({
|
|
1367
|
+
input: z2.number().nonnegative(),
|
|
1368
|
+
output: z2.number().nonnegative()
|
|
1369
|
+
}).optional(),
|
|
1184
1370
|
error: z2.string().optional(),
|
|
1185
1371
|
toolCalls: z2.array(
|
|
1186
1372
|
z2.object({
|
|
@@ -2285,6 +2471,11 @@ var createEngineLocator = async (name) => {
|
|
|
2285
2471
|
init_engine_locator_npm();
|
|
2286
2472
|
var DIOLOGUE_PROVIDER_ID = "diologue";
|
|
2287
2473
|
var DIOLOGUE_MODEL_ID = "diologue-routed";
|
|
2474
|
+
var END_OF_TURN_GRACE_MS = 600;
|
|
2475
|
+
var EDIT_DIRECTIVE = "You are an autonomous coding agent operating on a real git repository. Carry out the request by editing files directly with your tools (write / edit / patch / bash) \u2014 do not just describe the change, outline steps, or print code for the user to copy. Read only what you need, make the edits on disk, and finish once the change has actually been written.";
|
|
2476
|
+
var logAdapter = (message) => {
|
|
2477
|
+
console.error(`[opencode-adapter] ${message}`);
|
|
2478
|
+
};
|
|
2288
2479
|
var OpenCodeProcessAdapter = class {
|
|
2289
2480
|
constructor(options = {}) {
|
|
2290
2481
|
this.options = options;
|
|
@@ -2335,7 +2526,16 @@ var OpenCodeProcessAdapter = class {
|
|
|
2335
2526
|
}
|
|
2336
2527
|
}
|
|
2337
2528
|
},
|
|
2338
|
-
model: `${DIOLOGUE_PROVIDER_ID}/${DIOLOGUE_MODEL_ID}
|
|
2529
|
+
model: `${DIOLOGUE_PROVIDER_ID}/${DIOLOGUE_MODEL_ID}`,
|
|
2530
|
+
// Auto-approve tool execution. opencode gates edit/bash/webfetch
|
|
2531
|
+
// behind a permission prompt; this helper runs headless with no
|
|
2532
|
+
// approver, so without this the agent's writes are never applied to
|
|
2533
|
+
// the repo and a turn finishes having changed nothing.
|
|
2534
|
+
permission: {
|
|
2535
|
+
edit: "allow",
|
|
2536
|
+
bash: "allow",
|
|
2537
|
+
webfetch: "allow"
|
|
2538
|
+
}
|
|
2339
2539
|
};
|
|
2340
2540
|
}
|
|
2341
2541
|
async ensureServer() {
|
|
@@ -2344,7 +2544,10 @@ var OpenCodeProcessAdapter = class {
|
|
|
2344
2544
|
}
|
|
2345
2545
|
this.serverPromise = (async () => {
|
|
2346
2546
|
const locator = await this.resolveLocator();
|
|
2347
|
-
|
|
2547
|
+
logAdapter(`starting engine helperBase=${this.resolveHelperBaseUrl()}`);
|
|
2548
|
+
const handle = await locator.start({ config: this.buildShimConfig() });
|
|
2549
|
+
logAdapter(`engine ready url=${handle.url}`);
|
|
2550
|
+
return handle;
|
|
2348
2551
|
})();
|
|
2349
2552
|
try {
|
|
2350
2553
|
return await this.serverPromise;
|
|
@@ -2355,7 +2558,13 @@ var OpenCodeProcessAdapter = class {
|
|
|
2355
2558
|
}
|
|
2356
2559
|
async ensureOpencodeSession(handle, ourSessionId, repoPath, title) {
|
|
2357
2560
|
const existing = this.sessionMap.get(ourSessionId);
|
|
2358
|
-
if (existing)
|
|
2561
|
+
if (existing) {
|
|
2562
|
+
logAdapter(
|
|
2563
|
+
`reuse opencodeSession=${existing.slice(0, 8)} session=${ourSessionId.slice(0, 8)} repo=${repoPath}`
|
|
2564
|
+
);
|
|
2565
|
+
return existing;
|
|
2566
|
+
}
|
|
2567
|
+
logAdapter(`create session=${ourSessionId.slice(0, 8)} repo=${repoPath}`);
|
|
2359
2568
|
const created = await handle.client.session.create({
|
|
2360
2569
|
body: { title },
|
|
2361
2570
|
query: { directory: repoPath }
|
|
@@ -2365,6 +2574,9 @@ var OpenCodeProcessAdapter = class {
|
|
|
2365
2574
|
throw new Error("opencode session.create returned no id");
|
|
2366
2575
|
}
|
|
2367
2576
|
this.sessionMap.set(ourSessionId, id);
|
|
2577
|
+
logAdapter(
|
|
2578
|
+
`created opencodeSession=${id.slice(0, 8)} session=${ourSessionId.slice(0, 8)}`
|
|
2579
|
+
);
|
|
2368
2580
|
return id;
|
|
2369
2581
|
}
|
|
2370
2582
|
async *streamMessage(request) {
|
|
@@ -2410,16 +2622,16 @@ var OpenCodeProcessAdapter = class {
|
|
|
2410
2622
|
}
|
|
2411
2623
|
});
|
|
2412
2624
|
const realCallback = callback;
|
|
2413
|
-
streamScopedBroker.request = async (payload) => {
|
|
2414
|
-
return realCallback(payload);
|
|
2625
|
+
streamScopedBroker.request = async (payload, observer) => {
|
|
2626
|
+
return realCallback(payload, observer);
|
|
2415
2627
|
};
|
|
2416
2628
|
activeBrokerHandle = bindActiveBroker(streamScopedBroker);
|
|
2417
|
-
|
|
2418
|
-
`
|
|
2629
|
+
logAdapter(
|
|
2630
|
+
`bound active broker token="${activeBrokerHandle.token.slice(0, 12)}\u2026" session=${request.sessionId.slice(0, 8)}`
|
|
2419
2631
|
);
|
|
2420
2632
|
} else {
|
|
2421
|
-
|
|
2422
|
-
`
|
|
2633
|
+
logAdapter(
|
|
2634
|
+
`WARNING request.broker missing session=${request.sessionId.slice(0, 8)}; shim will 401`
|
|
2423
2635
|
);
|
|
2424
2636
|
}
|
|
2425
2637
|
const eventController = new AbortController();
|
|
@@ -2436,6 +2648,9 @@ var OpenCodeProcessAdapter = class {
|
|
|
2436
2648
|
signal: eventController.signal
|
|
2437
2649
|
});
|
|
2438
2650
|
eventStream = res.stream;
|
|
2651
|
+
logAdapter(
|
|
2652
|
+
`subscribed events session=${request.sessionId.slice(0, 8)} opencodeSession=${openCodeSessionId.slice(0, 8)}`
|
|
2653
|
+
);
|
|
2439
2654
|
} catch (err) {
|
|
2440
2655
|
yield {
|
|
2441
2656
|
type: "error",
|
|
@@ -2446,7 +2661,11 @@ var OpenCodeProcessAdapter = class {
|
|
|
2446
2661
|
return;
|
|
2447
2662
|
}
|
|
2448
2663
|
const promptBody = {
|
|
2449
|
-
parts: [
|
|
2664
|
+
parts: [
|
|
2665
|
+
{ type: "text", text: `${EDIT_DIRECTIVE}
|
|
2666
|
+
|
|
2667
|
+
${request.prompt}` }
|
|
2668
|
+
]
|
|
2450
2669
|
};
|
|
2451
2670
|
if (streamScopedBroker && activeBrokerHandle) {
|
|
2452
2671
|
promptBody.model = {
|
|
@@ -2454,22 +2673,63 @@ var OpenCodeProcessAdapter = class {
|
|
|
2454
2673
|
modelID: DIOLOGUE_MODEL_ID
|
|
2455
2674
|
};
|
|
2456
2675
|
}
|
|
2676
|
+
logAdapter(
|
|
2677
|
+
`prompt start session=${request.sessionId.slice(0, 8)} opencodeSession=${openCodeSessionId.slice(0, 8)} promptChars=${request.prompt.length} provider=${request.preferredProvider ?? "(default)"} model=${request.preferredModel ?? "(default)"}`
|
|
2678
|
+
);
|
|
2457
2679
|
const promptPromise = handle.client.session.prompt({
|
|
2458
2680
|
path: { id: openCodeSessionId },
|
|
2459
2681
|
query: { directory: request.repoPath },
|
|
2460
2682
|
body: promptBody
|
|
2683
|
+
}).then((value) => {
|
|
2684
|
+
logAdapter(
|
|
2685
|
+
`prompt accepted session=${request.sessionId.slice(0, 8)} opencodeSession=${openCodeSessionId.slice(0, 8)}`
|
|
2686
|
+
);
|
|
2687
|
+
return value;
|
|
2461
2688
|
}).catch((err) => {
|
|
2462
|
-
|
|
2689
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2690
|
+
logAdapter(
|
|
2691
|
+
`prompt failed session=${request.sessionId.slice(0, 8)} opencodeSession=${openCodeSessionId.slice(0, 8)} error=${error.message}`
|
|
2692
|
+
);
|
|
2693
|
+
return error;
|
|
2463
2694
|
});
|
|
2695
|
+
let stopConsuming = false;
|
|
2696
|
+
const stopSignal = new Promise((resolve) => {
|
|
2697
|
+
void promptPromise.finally(() => {
|
|
2698
|
+
setTimeout(() => {
|
|
2699
|
+
stopConsuming = true;
|
|
2700
|
+
resolve("stop");
|
|
2701
|
+
}, END_OF_TURN_GRACE_MS);
|
|
2702
|
+
});
|
|
2703
|
+
});
|
|
2704
|
+
let emittedDone = false;
|
|
2705
|
+
const iterator = eventStream[Symbol.asyncIterator]();
|
|
2464
2706
|
try {
|
|
2465
|
-
|
|
2707
|
+
while (!stopConsuming) {
|
|
2466
2708
|
if (request.signal?.aborted) break;
|
|
2709
|
+
const nextP = iterator.next();
|
|
2710
|
+
const next = await Promise.race([
|
|
2711
|
+
nextP,
|
|
2712
|
+
stopSignal.then(() => null)
|
|
2713
|
+
]);
|
|
2714
|
+
if (next === null) {
|
|
2715
|
+
void nextP.catch(() => void 0);
|
|
2716
|
+
break;
|
|
2717
|
+
}
|
|
2718
|
+
if (next.done) break;
|
|
2719
|
+
const rawItem = next.value;
|
|
2720
|
+
const item = "payload" in rawItem && rawItem.payload ? rawItem.payload : rawItem;
|
|
2467
2721
|
const mapped = mapEvent(item, mapState);
|
|
2722
|
+
if (mapped.events.length > 0 || mapped.done) {
|
|
2723
|
+
logAdapter(
|
|
2724
|
+
`event ${item.type} mapped=${mapped.events.map((event) => event.type).join(",") || "(none)"} done=${mapped.done} session=${request.sessionId.slice(0, 8)}`
|
|
2725
|
+
);
|
|
2726
|
+
}
|
|
2468
2727
|
for (const event of mapped.events) {
|
|
2469
2728
|
if (event.type === "diff_proposed" && mapped.patchToFetch) {
|
|
2470
2729
|
sawAnyPatch = true;
|
|
2471
2730
|
lastPatchHash = mapped.patchToFetch.hash;
|
|
2472
2731
|
}
|
|
2732
|
+
if (event.type === "done") emittedDone = true;
|
|
2473
2733
|
yield event;
|
|
2474
2734
|
}
|
|
2475
2735
|
if (mapped.done) {
|
|
@@ -2478,31 +2738,42 @@ var OpenCodeProcessAdapter = class {
|
|
|
2478
2738
|
}
|
|
2479
2739
|
} finally {
|
|
2480
2740
|
eventController.abort();
|
|
2741
|
+
try {
|
|
2742
|
+
await iterator.return?.();
|
|
2743
|
+
} catch {
|
|
2744
|
+
}
|
|
2481
2745
|
request.signal?.removeEventListener("abort", stopOnAbort);
|
|
2482
2746
|
activeBrokerHandle?.release();
|
|
2483
2747
|
streamScopedBroker?.close("turn_ended");
|
|
2484
2748
|
}
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
};
|
|
2498
|
-
}
|
|
2499
|
-
} catch {
|
|
2749
|
+
try {
|
|
2750
|
+
const diffRes = await handle.client.session.diff({
|
|
2751
|
+
path: { id: openCodeSessionId },
|
|
2752
|
+
query: { directory: request.repoPath }
|
|
2753
|
+
});
|
|
2754
|
+
const unified = diffRes.data?.unified ?? "";
|
|
2755
|
+
if (unified) {
|
|
2756
|
+
yield {
|
|
2757
|
+
type: "diff_proposed",
|
|
2758
|
+
unified,
|
|
2759
|
+
files: parseUnifiedDiffFiles(unified)
|
|
2760
|
+
};
|
|
2500
2761
|
}
|
|
2762
|
+
} catch (err) {
|
|
2763
|
+
logAdapter(
|
|
2764
|
+
`diff fetch failed session=${request.sessionId.slice(0, 8)} error=${err instanceof Error ? err.message : String(err)}`
|
|
2765
|
+
);
|
|
2501
2766
|
}
|
|
2502
2767
|
const promptResult = await promptPromise;
|
|
2503
2768
|
if (promptResult instanceof Error) {
|
|
2504
2769
|
yield { type: "error", message: promptResult.message, recoverable: true };
|
|
2770
|
+
} else {
|
|
2771
|
+
logAdapter(`turn complete session=${request.sessionId.slice(0, 8)}`);
|
|
2772
|
+
}
|
|
2773
|
+
if (!emittedDone) {
|
|
2774
|
+
yield { type: "done" };
|
|
2505
2775
|
}
|
|
2776
|
+
void sawAnyPatch;
|
|
2506
2777
|
void lastPatchHash;
|
|
2507
2778
|
}
|
|
2508
2779
|
async close() {
|