@diologue/local-agent 0.2.0 → 0.5.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 +484 -46
- 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);
|
|
@@ -1101,7 +1266,10 @@ var agentMessageBodySchema = z2.object({
|
|
|
1101
1266
|
/** Optional user-chosen routing for this turn. When absent the cloud
|
|
1102
1267
|
* picks the default (CODING_AGENT_DEFAULT_PROVIDER/MODEL). */
|
|
1103
1268
|
preferredProvider: z2.string().min(1).max(100).optional(),
|
|
1104
|
-
preferredModel: z2.string().min(1).max(200).optional()
|
|
1269
|
+
preferredModel: z2.string().min(1).max(200).optional(),
|
|
1270
|
+
/** Tool-gating policy. Absent → "auto" (run everything) so older clients
|
|
1271
|
+
* keep their current behaviour. */
|
|
1272
|
+
permissionMode: z2.enum(["auto", "confirm"]).optional()
|
|
1105
1273
|
});
|
|
1106
1274
|
var writeEvent = (res, event) => {
|
|
1107
1275
|
res.write(`data: ${JSON.stringify(event)}
|
|
@@ -1111,6 +1279,9 @@ var writeEvent = (res, event) => {
|
|
|
1111
1279
|
var writeDone = (res) => {
|
|
1112
1280
|
res.write("data: [DONE]\n\n");
|
|
1113
1281
|
};
|
|
1282
|
+
var logAgentRoute = (message) => {
|
|
1283
|
+
console.error(`[agent-route] ${message}`);
|
|
1284
|
+
};
|
|
1114
1285
|
var createAgentRouter = (deps) => {
|
|
1115
1286
|
const router = createRouter2();
|
|
1116
1287
|
router.post("/message", async (req, res) => {
|
|
@@ -1124,6 +1295,9 @@ var createAgentRouter = (deps) => {
|
|
|
1124
1295
|
res.status(409).json({ error: "no_repo_selected" });
|
|
1125
1296
|
return;
|
|
1126
1297
|
}
|
|
1298
|
+
logAgentRoute(
|
|
1299
|
+
`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)"}`
|
|
1300
|
+
);
|
|
1127
1301
|
res.setHeader("Content-Type", "text/event-stream");
|
|
1128
1302
|
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
1129
1303
|
res.setHeader("Connection", "keep-alive");
|
|
@@ -1149,21 +1323,34 @@ var createAgentRouter = (deps) => {
|
|
|
1149
1323
|
prompt: parsed.data.prompt,
|
|
1150
1324
|
history,
|
|
1151
1325
|
signal: controller.signal,
|
|
1152
|
-
broker: (payload) => broker.request(payload),
|
|
1326
|
+
broker: (payload, observer) => broker.request(payload, observer),
|
|
1153
1327
|
preferredProvider: parsed.data.preferredProvider,
|
|
1154
|
-
preferredModel: parsed.data.preferredModel
|
|
1328
|
+
preferredModel: parsed.data.preferredModel,
|
|
1329
|
+
permissionMode: parsed.data.permissionMode
|
|
1155
1330
|
})) {
|
|
1156
1331
|
if (controller.signal.aborted) {
|
|
1332
|
+
logAgentRoute(
|
|
1333
|
+
`message aborted session=${parsed.data.sessionId.slice(0, 8)}`
|
|
1334
|
+
);
|
|
1157
1335
|
break;
|
|
1158
1336
|
}
|
|
1337
|
+
logAgentRoute(
|
|
1338
|
+
`stream event session=${parsed.data.sessionId.slice(0, 8)} type=${event.type}`
|
|
1339
|
+
);
|
|
1159
1340
|
writeEvent(res, event);
|
|
1160
1341
|
}
|
|
1342
|
+
logAgentRoute(
|
|
1343
|
+
`message done session=${parsed.data.sessionId.slice(0, 8)}`
|
|
1344
|
+
);
|
|
1161
1345
|
writeDone(res);
|
|
1162
1346
|
} catch (err) {
|
|
1163
1347
|
if (err && typeof err === "object" && "name" in err && err.name === "AbortError") {
|
|
1164
1348
|
writeDone(res);
|
|
1165
1349
|
} else {
|
|
1166
1350
|
const message = err instanceof Error ? err.message : "Adapter stream failed";
|
|
1351
|
+
logAgentRoute(
|
|
1352
|
+
`message error session=${parsed.data.sessionId.slice(0, 8)} error=${message}`
|
|
1353
|
+
);
|
|
1167
1354
|
writeEvent(res, { type: "error", message });
|
|
1168
1355
|
writeDone(res);
|
|
1169
1356
|
}
|
|
@@ -1180,7 +1367,10 @@ var llmResponseBodySchema = z2.object({
|
|
|
1180
1367
|
text: z2.string(),
|
|
1181
1368
|
provider: z2.string().min(1),
|
|
1182
1369
|
model: z2.string().min(1),
|
|
1183
|
-
tokenUsage: z2.object({
|
|
1370
|
+
tokenUsage: z2.object({
|
|
1371
|
+
input: z2.number().nonnegative(),
|
|
1372
|
+
output: z2.number().nonnegative()
|
|
1373
|
+
}).optional(),
|
|
1184
1374
|
error: z2.string().optional(),
|
|
1185
1375
|
toolCalls: z2.array(
|
|
1186
1376
|
z2.object({
|
|
@@ -1307,6 +1497,36 @@ var createLlmChunkRouter = (deps) => {
|
|
|
1307
1497
|
});
|
|
1308
1498
|
return router;
|
|
1309
1499
|
};
|
|
1500
|
+
var permissionDecisionBodySchema = z2.object({
|
|
1501
|
+
sessionId: z2.string().min(1),
|
|
1502
|
+
permissionId: z2.string().min(1),
|
|
1503
|
+
response: z2.enum(["once", "always", "reject"])
|
|
1504
|
+
});
|
|
1505
|
+
var createPermissionRouter = (deps) => {
|
|
1506
|
+
const router = createRouter2();
|
|
1507
|
+
router.post("/", async (req, res) => {
|
|
1508
|
+
const parsed = permissionDecisionBodySchema.safeParse(req.body);
|
|
1509
|
+
if (!parsed.success) {
|
|
1510
|
+
res.status(400).json({ error: "invalid_body", issues: parsed.error.issues });
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
if (!deps.adapter.replyPermission) {
|
|
1514
|
+
res.status(501).json({ error: "permissions_unsupported" });
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
const matched = await deps.adapter.replyPermission(
|
|
1518
|
+
parsed.data.sessionId,
|
|
1519
|
+
parsed.data.permissionId,
|
|
1520
|
+
parsed.data.response
|
|
1521
|
+
);
|
|
1522
|
+
if (!matched) {
|
|
1523
|
+
res.status(404).json({ error: "no_active_permission" });
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
res.json({ ok: true });
|
|
1527
|
+
});
|
|
1528
|
+
return router;
|
|
1529
|
+
};
|
|
1310
1530
|
|
|
1311
1531
|
// src/routes/llm-shim.ts
|
|
1312
1532
|
import { Router as createRouter3 } from "express";
|
|
@@ -2173,13 +2393,30 @@ var MockOpenCodeAdapter = class {
|
|
|
2173
2393
|
};
|
|
2174
2394
|
|
|
2175
2395
|
// src/adapters/opencode-event-mapper.ts
|
|
2396
|
+
var MAX_TOOL_OUTPUT_CHARS = 4e3;
|
|
2397
|
+
var truncateOutput = (raw) => {
|
|
2398
|
+
if (!raw) return void 0;
|
|
2399
|
+
if (raw.length <= MAX_TOOL_OUTPUT_CHARS) return raw;
|
|
2400
|
+
const omitted = raw.length - MAX_TOOL_OUTPUT_CHARS;
|
|
2401
|
+
return `${raw.slice(0, MAX_TOOL_OUTPUT_CHARS)}
|
|
2402
|
+
\u2026 [${omitted} more characters truncated]`;
|
|
2403
|
+
};
|
|
2176
2404
|
var createMapState = (ourSessionId, openCodeSessionId) => ({
|
|
2177
2405
|
ourSessionId,
|
|
2178
2406
|
openCodeSessionId,
|
|
2179
2407
|
startedTools: /* @__PURE__ */ new Set(),
|
|
2180
2408
|
endedTools: /* @__PURE__ */ new Set(),
|
|
2181
|
-
announcedPatches: /* @__PURE__ */ new Set()
|
|
2409
|
+
announcedPatches: /* @__PURE__ */ new Set(),
|
|
2410
|
+
announcedPermissions: /* @__PURE__ */ new Set()
|
|
2182
2411
|
});
|
|
2412
|
+
var extractPermissionCommand = (metadata) => {
|
|
2413
|
+
if (!metadata) return void 0;
|
|
2414
|
+
for (const key of ["command", "cmd", "url", "pattern", "filePath"]) {
|
|
2415
|
+
const v = metadata[key];
|
|
2416
|
+
if (typeof v === "string" && v) return v;
|
|
2417
|
+
}
|
|
2418
|
+
return void 0;
|
|
2419
|
+
};
|
|
2183
2420
|
var EMPTY = { events: [], done: false };
|
|
2184
2421
|
var mapEvent = (item, state) => {
|
|
2185
2422
|
const sid = item.properties?.sessionID;
|
|
@@ -2189,6 +2426,25 @@ var mapEvent = (item, state) => {
|
|
|
2189
2426
|
if (item.type === "session.idle" && sid === state.openCodeSessionId) {
|
|
2190
2427
|
return { events: [{ type: "done" }], done: true };
|
|
2191
2428
|
}
|
|
2429
|
+
if (item.type === "permission.updated") {
|
|
2430
|
+
const permissionId = item.properties?.id;
|
|
2431
|
+
if (!permissionId || state.announcedPermissions.has(permissionId)) {
|
|
2432
|
+
return EMPTY;
|
|
2433
|
+
}
|
|
2434
|
+
state.announcedPermissions.add(permissionId);
|
|
2435
|
+
return {
|
|
2436
|
+
events: [
|
|
2437
|
+
{
|
|
2438
|
+
type: "permission_request",
|
|
2439
|
+
permissionId,
|
|
2440
|
+
tool: item.properties?.type ?? "tool",
|
|
2441
|
+
title: item.properties?.title,
|
|
2442
|
+
command: extractPermissionCommand(item.properties?.metadata)
|
|
2443
|
+
}
|
|
2444
|
+
],
|
|
2445
|
+
done: false
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2192
2448
|
if (item.type !== "message.part.updated") {
|
|
2193
2449
|
return EMPTY;
|
|
2194
2450
|
}
|
|
@@ -2227,7 +2483,10 @@ var mapEvent = (item, state) => {
|
|
|
2227
2483
|
type: "tool_call_end",
|
|
2228
2484
|
toolCallId: callId,
|
|
2229
2485
|
ok: status === "completed",
|
|
2230
|
-
summary: status === "error" ? part.state?.error ?? "Tool failed" : part.state?.title
|
|
2486
|
+
summary: status === "error" ? part.state?.error ?? "Tool failed" : part.state?.title,
|
|
2487
|
+
output: truncateOutput(
|
|
2488
|
+
status === "error" ? part.state?.error : part.state?.output
|
|
2489
|
+
)
|
|
2231
2490
|
};
|
|
2232
2491
|
return { events: [event], done: false };
|
|
2233
2492
|
}
|
|
@@ -2285,6 +2544,12 @@ var createEngineLocator = async (name) => {
|
|
|
2285
2544
|
init_engine_locator_npm();
|
|
2286
2545
|
var DIOLOGUE_PROVIDER_ID = "diologue";
|
|
2287
2546
|
var DIOLOGUE_MODEL_ID = "diologue-routed";
|
|
2547
|
+
var END_OF_TURN_GRACE_MS = 600;
|
|
2548
|
+
var PERMISSION_DECISION_TIMEOUT_MS = 18e4;
|
|
2549
|
+
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.";
|
|
2550
|
+
var logAdapter = (message) => {
|
|
2551
|
+
console.error(`[opencode-adapter] ${message}`);
|
|
2552
|
+
};
|
|
2288
2553
|
var OpenCodeProcessAdapter = class {
|
|
2289
2554
|
constructor(options = {}) {
|
|
2290
2555
|
this.options = options;
|
|
@@ -2294,6 +2559,9 @@ var OpenCodeProcessAdapter = class {
|
|
|
2294
2559
|
/** Maps our sessionId → opencode session id so successive turns within
|
|
2295
2560
|
* one coding-agent session reuse the same opencode conversation. */
|
|
2296
2561
|
sessionMap = /* @__PURE__ */ new Map();
|
|
2562
|
+
/** In-flight turns by our sessionId, so replyPermission() can reach the
|
|
2563
|
+
* engine client to resolve a paused tool call. */
|
|
2564
|
+
activeTurns = /* @__PURE__ */ new Map();
|
|
2297
2565
|
async resolveLocator() {
|
|
2298
2566
|
if (this.options.engineLocator) {
|
|
2299
2567
|
return this.options.engineLocator;
|
|
@@ -2335,7 +2603,19 @@ var OpenCodeProcessAdapter = class {
|
|
|
2335
2603
|
}
|
|
2336
2604
|
}
|
|
2337
2605
|
},
|
|
2338
|
-
model: `${DIOLOGUE_PROVIDER_ID}/${DIOLOGUE_MODEL_ID}
|
|
2606
|
+
model: `${DIOLOGUE_PROVIDER_ID}/${DIOLOGUE_MODEL_ID}`,
|
|
2607
|
+
// Permission policy. The ADAPTER is the policy engine, not this config:
|
|
2608
|
+
// - edit stays "allow" — edits land in the working tree and are
|
|
2609
|
+
// reversible via the chat's Keep/Revert, so we never prompt on them.
|
|
2610
|
+
// - bash + webfetch are "ask" so opencode pauses them. In "auto" mode
|
|
2611
|
+
// the adapter auto-approves instantly (behaves like allow); in
|
|
2612
|
+
// "confirm" mode it forwards an AgentPermissionRequest to the browser
|
|
2613
|
+
// and resumes only once the user replies. See streamMessage().
|
|
2614
|
+
permission: {
|
|
2615
|
+
edit: "allow",
|
|
2616
|
+
bash: "ask",
|
|
2617
|
+
webfetch: "ask"
|
|
2618
|
+
}
|
|
2339
2619
|
};
|
|
2340
2620
|
}
|
|
2341
2621
|
async ensureServer() {
|
|
@@ -2344,7 +2624,10 @@ var OpenCodeProcessAdapter = class {
|
|
|
2344
2624
|
}
|
|
2345
2625
|
this.serverPromise = (async () => {
|
|
2346
2626
|
const locator = await this.resolveLocator();
|
|
2347
|
-
|
|
2627
|
+
logAdapter(`starting engine helperBase=${this.resolveHelperBaseUrl()}`);
|
|
2628
|
+
const handle = await locator.start({ config: this.buildShimConfig() });
|
|
2629
|
+
logAdapter(`engine ready url=${handle.url}`);
|
|
2630
|
+
return handle;
|
|
2348
2631
|
})();
|
|
2349
2632
|
try {
|
|
2350
2633
|
return await this.serverPromise;
|
|
@@ -2355,7 +2638,13 @@ var OpenCodeProcessAdapter = class {
|
|
|
2355
2638
|
}
|
|
2356
2639
|
async ensureOpencodeSession(handle, ourSessionId, repoPath, title) {
|
|
2357
2640
|
const existing = this.sessionMap.get(ourSessionId);
|
|
2358
|
-
if (existing)
|
|
2641
|
+
if (existing) {
|
|
2642
|
+
logAdapter(
|
|
2643
|
+
`reuse opencodeSession=${existing.slice(0, 8)} session=${ourSessionId.slice(0, 8)} repo=${repoPath}`
|
|
2644
|
+
);
|
|
2645
|
+
return existing;
|
|
2646
|
+
}
|
|
2647
|
+
logAdapter(`create session=${ourSessionId.slice(0, 8)} repo=${repoPath}`);
|
|
2359
2648
|
const created = await handle.client.session.create({
|
|
2360
2649
|
body: { title },
|
|
2361
2650
|
query: { directory: repoPath }
|
|
@@ -2365,8 +2654,49 @@ var OpenCodeProcessAdapter = class {
|
|
|
2365
2654
|
throw new Error("opencode session.create returned no id");
|
|
2366
2655
|
}
|
|
2367
2656
|
this.sessionMap.set(ourSessionId, id);
|
|
2657
|
+
logAdapter(
|
|
2658
|
+
`created opencodeSession=${id.slice(0, 8)} session=${ourSessionId.slice(0, 8)}`
|
|
2659
|
+
);
|
|
2368
2660
|
return id;
|
|
2369
2661
|
}
|
|
2662
|
+
/** Send a decision for a paused permission to opencode. Best-effort: a
|
|
2663
|
+
* failure is logged but not thrown — the worst case is the turn hangs
|
|
2664
|
+
* until the grace/abort path tears it down. */
|
|
2665
|
+
async replyToOpencode(client, openCodeSessionId, repoPath, permissionId, response) {
|
|
2666
|
+
try {
|
|
2667
|
+
await client.postSessionIdPermissionsPermissionId({
|
|
2668
|
+
path: { id: openCodeSessionId, permissionID: permissionId },
|
|
2669
|
+
query: { directory: repoPath },
|
|
2670
|
+
body: { response }
|
|
2671
|
+
});
|
|
2672
|
+
logAdapter(
|
|
2673
|
+
`permission reply id=${permissionId.slice(0, 8)} response=${response}`
|
|
2674
|
+
);
|
|
2675
|
+
} catch (err) {
|
|
2676
|
+
logAdapter(
|
|
2677
|
+
`permission reply FAILED id=${permissionId.slice(0, 8)} response=${response} error=${err instanceof Error ? err.message : String(err)}`
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
/** Apply a browser decision to a paused permission. Invoked by the
|
|
2682
|
+
* /agent/permission route on a separate HTTP request from the SSE stream. */
|
|
2683
|
+
async replyPermission(sessionId, permissionId, response) {
|
|
2684
|
+
const turn = this.activeTurns.get(sessionId);
|
|
2685
|
+
if (!turn) return false;
|
|
2686
|
+
const timer = turn.pending.get(permissionId);
|
|
2687
|
+
if (timer) {
|
|
2688
|
+
clearTimeout(timer);
|
|
2689
|
+
turn.pending.delete(permissionId);
|
|
2690
|
+
}
|
|
2691
|
+
await this.replyToOpencode(
|
|
2692
|
+
turn.client,
|
|
2693
|
+
turn.openCodeSessionId,
|
|
2694
|
+
turn.repoPath,
|
|
2695
|
+
permissionId,
|
|
2696
|
+
response
|
|
2697
|
+
);
|
|
2698
|
+
return true;
|
|
2699
|
+
}
|
|
2370
2700
|
async *streamMessage(request) {
|
|
2371
2701
|
let handle;
|
|
2372
2702
|
try {
|
|
@@ -2401,6 +2731,14 @@ var OpenCodeProcessAdapter = class {
|
|
|
2401
2731
|
);
|
|
2402
2732
|
let sawAnyPatch = false;
|
|
2403
2733
|
let lastPatchHash = "";
|
|
2734
|
+
const permissionMode = request.permissionMode ?? "auto";
|
|
2735
|
+
const activeTurn = {
|
|
2736
|
+
client: handle.client,
|
|
2737
|
+
openCodeSessionId,
|
|
2738
|
+
repoPath: request.repoPath,
|
|
2739
|
+
pending: /* @__PURE__ */ new Map()
|
|
2740
|
+
};
|
|
2741
|
+
this.activeTurns.set(request.sessionId, activeTurn);
|
|
2404
2742
|
let activeBrokerHandle = null;
|
|
2405
2743
|
let streamScopedBroker = null;
|
|
2406
2744
|
if (request.broker) {
|
|
@@ -2410,16 +2748,16 @@ var OpenCodeProcessAdapter = class {
|
|
|
2410
2748
|
}
|
|
2411
2749
|
});
|
|
2412
2750
|
const realCallback = callback;
|
|
2413
|
-
streamScopedBroker.request = async (payload) => {
|
|
2414
|
-
return realCallback(payload);
|
|
2751
|
+
streamScopedBroker.request = async (payload, observer) => {
|
|
2752
|
+
return realCallback(payload, observer);
|
|
2415
2753
|
};
|
|
2416
2754
|
activeBrokerHandle = bindActiveBroker(streamScopedBroker);
|
|
2417
|
-
|
|
2418
|
-
`
|
|
2755
|
+
logAdapter(
|
|
2756
|
+
`bound active broker token="${activeBrokerHandle.token.slice(0, 12)}\u2026" session=${request.sessionId.slice(0, 8)}`
|
|
2419
2757
|
);
|
|
2420
2758
|
} else {
|
|
2421
|
-
|
|
2422
|
-
`
|
|
2759
|
+
logAdapter(
|
|
2760
|
+
`WARNING request.broker missing session=${request.sessionId.slice(0, 8)}; shim will 401`
|
|
2423
2761
|
);
|
|
2424
2762
|
}
|
|
2425
2763
|
const eventController = new AbortController();
|
|
@@ -2436,6 +2774,9 @@ var OpenCodeProcessAdapter = class {
|
|
|
2436
2774
|
signal: eventController.signal
|
|
2437
2775
|
});
|
|
2438
2776
|
eventStream = res.stream;
|
|
2777
|
+
logAdapter(
|
|
2778
|
+
`subscribed events session=${request.sessionId.slice(0, 8)} opencodeSession=${openCodeSessionId.slice(0, 8)}`
|
|
2779
|
+
);
|
|
2439
2780
|
} catch (err) {
|
|
2440
2781
|
yield {
|
|
2441
2782
|
type: "error",
|
|
@@ -2446,7 +2787,11 @@ var OpenCodeProcessAdapter = class {
|
|
|
2446
2787
|
return;
|
|
2447
2788
|
}
|
|
2448
2789
|
const promptBody = {
|
|
2449
|
-
parts: [
|
|
2790
|
+
parts: [
|
|
2791
|
+
{ type: "text", text: `${EDIT_DIRECTIVE}
|
|
2792
|
+
|
|
2793
|
+
${request.prompt}` }
|
|
2794
|
+
]
|
|
2450
2795
|
};
|
|
2451
2796
|
if (streamScopedBroker && activeBrokerHandle) {
|
|
2452
2797
|
promptBody.model = {
|
|
@@ -2454,22 +2799,91 @@ var OpenCodeProcessAdapter = class {
|
|
|
2454
2799
|
modelID: DIOLOGUE_MODEL_ID
|
|
2455
2800
|
};
|
|
2456
2801
|
}
|
|
2802
|
+
logAdapter(
|
|
2803
|
+
`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)"}`
|
|
2804
|
+
);
|
|
2457
2805
|
const promptPromise = handle.client.session.prompt({
|
|
2458
2806
|
path: { id: openCodeSessionId },
|
|
2459
2807
|
query: { directory: request.repoPath },
|
|
2460
2808
|
body: promptBody
|
|
2809
|
+
}).then((value) => {
|
|
2810
|
+
logAdapter(
|
|
2811
|
+
`prompt accepted session=${request.sessionId.slice(0, 8)} opencodeSession=${openCodeSessionId.slice(0, 8)}`
|
|
2812
|
+
);
|
|
2813
|
+
return value;
|
|
2461
2814
|
}).catch((err) => {
|
|
2462
|
-
|
|
2815
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2816
|
+
logAdapter(
|
|
2817
|
+
`prompt failed session=${request.sessionId.slice(0, 8)} opencodeSession=${openCodeSessionId.slice(0, 8)} error=${error.message}`
|
|
2818
|
+
);
|
|
2819
|
+
return error;
|
|
2463
2820
|
});
|
|
2821
|
+
let stopConsuming = false;
|
|
2822
|
+
const stopSignal = new Promise((resolve) => {
|
|
2823
|
+
void promptPromise.finally(() => {
|
|
2824
|
+
setTimeout(() => {
|
|
2825
|
+
stopConsuming = true;
|
|
2826
|
+
resolve("stop");
|
|
2827
|
+
}, END_OF_TURN_GRACE_MS);
|
|
2828
|
+
});
|
|
2829
|
+
});
|
|
2830
|
+
let emittedDone = false;
|
|
2831
|
+
const iterator = eventStream[Symbol.asyncIterator]();
|
|
2464
2832
|
try {
|
|
2465
|
-
|
|
2833
|
+
while (!stopConsuming) {
|
|
2466
2834
|
if (request.signal?.aborted) break;
|
|
2835
|
+
const nextP = iterator.next();
|
|
2836
|
+
const next = await Promise.race([
|
|
2837
|
+
nextP,
|
|
2838
|
+
stopSignal.then(() => null)
|
|
2839
|
+
]);
|
|
2840
|
+
if (next === null) {
|
|
2841
|
+
void nextP.catch(() => void 0);
|
|
2842
|
+
break;
|
|
2843
|
+
}
|
|
2844
|
+
if (next.done) break;
|
|
2845
|
+
const rawItem = next.value;
|
|
2846
|
+
const item = "payload" in rawItem && rawItem.payload ? rawItem.payload : rawItem;
|
|
2467
2847
|
const mapped = mapEvent(item, mapState);
|
|
2848
|
+
if (mapped.events.length > 0 || mapped.done) {
|
|
2849
|
+
logAdapter(
|
|
2850
|
+
`event ${item.type} mapped=${mapped.events.map((event) => event.type).join(",") || "(none)"} done=${mapped.done} session=${request.sessionId.slice(0, 8)}`
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2468
2853
|
for (const event of mapped.events) {
|
|
2854
|
+
if (event.type === "permission_request") {
|
|
2855
|
+
if (permissionMode === "confirm") {
|
|
2856
|
+
const timer = setTimeout(() => {
|
|
2857
|
+
activeTurn.pending.delete(event.permissionId);
|
|
2858
|
+
void this.replyToOpencode(
|
|
2859
|
+
handle.client,
|
|
2860
|
+
openCodeSessionId,
|
|
2861
|
+
request.repoPath,
|
|
2862
|
+
event.permissionId,
|
|
2863
|
+
"reject"
|
|
2864
|
+
);
|
|
2865
|
+
logAdapter(
|
|
2866
|
+
`permission auto-rejected (timeout) id=${event.permissionId.slice(0, 8)} session=${request.sessionId.slice(0, 8)}`
|
|
2867
|
+
);
|
|
2868
|
+
}, PERMISSION_DECISION_TIMEOUT_MS);
|
|
2869
|
+
activeTurn.pending.set(event.permissionId, timer);
|
|
2870
|
+
yield event;
|
|
2871
|
+
} else {
|
|
2872
|
+
void this.replyToOpencode(
|
|
2873
|
+
handle.client,
|
|
2874
|
+
openCodeSessionId,
|
|
2875
|
+
request.repoPath,
|
|
2876
|
+
event.permissionId,
|
|
2877
|
+
"once"
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2880
|
+
continue;
|
|
2881
|
+
}
|
|
2469
2882
|
if (event.type === "diff_proposed" && mapped.patchToFetch) {
|
|
2470
2883
|
sawAnyPatch = true;
|
|
2471
2884
|
lastPatchHash = mapped.patchToFetch.hash;
|
|
2472
2885
|
}
|
|
2886
|
+
if (event.type === "done") emittedDone = true;
|
|
2473
2887
|
yield event;
|
|
2474
2888
|
}
|
|
2475
2889
|
if (mapped.done) {
|
|
@@ -2478,31 +2892,54 @@ var OpenCodeProcessAdapter = class {
|
|
|
2478
2892
|
}
|
|
2479
2893
|
} finally {
|
|
2480
2894
|
eventController.abort();
|
|
2895
|
+
try {
|
|
2896
|
+
await iterator.return?.();
|
|
2897
|
+
} catch {
|
|
2898
|
+
}
|
|
2481
2899
|
request.signal?.removeEventListener("abort", stopOnAbort);
|
|
2900
|
+
for (const [permId, timer] of activeTurn.pending) {
|
|
2901
|
+
clearTimeout(timer);
|
|
2902
|
+
void this.replyToOpencode(
|
|
2903
|
+
handle.client,
|
|
2904
|
+
openCodeSessionId,
|
|
2905
|
+
request.repoPath,
|
|
2906
|
+
permId,
|
|
2907
|
+
"reject"
|
|
2908
|
+
);
|
|
2909
|
+
}
|
|
2910
|
+
activeTurn.pending.clear();
|
|
2911
|
+
this.activeTurns.delete(request.sessionId);
|
|
2482
2912
|
activeBrokerHandle?.release();
|
|
2483
2913
|
streamScopedBroker?.close("turn_ended");
|
|
2484
2914
|
}
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
};
|
|
2498
|
-
}
|
|
2499
|
-
} catch {
|
|
2915
|
+
try {
|
|
2916
|
+
const diffRes = await handle.client.session.diff({
|
|
2917
|
+
path: { id: openCodeSessionId },
|
|
2918
|
+
query: { directory: request.repoPath }
|
|
2919
|
+
});
|
|
2920
|
+
const unified = diffRes.data?.unified ?? "";
|
|
2921
|
+
if (unified) {
|
|
2922
|
+
yield {
|
|
2923
|
+
type: "diff_proposed",
|
|
2924
|
+
unified,
|
|
2925
|
+
files: parseUnifiedDiffFiles(unified)
|
|
2926
|
+
};
|
|
2500
2927
|
}
|
|
2928
|
+
} catch (err) {
|
|
2929
|
+
logAdapter(
|
|
2930
|
+
`diff fetch failed session=${request.sessionId.slice(0, 8)} error=${err instanceof Error ? err.message : String(err)}`
|
|
2931
|
+
);
|
|
2501
2932
|
}
|
|
2502
2933
|
const promptResult = await promptPromise;
|
|
2503
2934
|
if (promptResult instanceof Error) {
|
|
2504
2935
|
yield { type: "error", message: promptResult.message, recoverable: true };
|
|
2936
|
+
} else {
|
|
2937
|
+
logAdapter(`turn complete session=${request.sessionId.slice(0, 8)}`);
|
|
2938
|
+
}
|
|
2939
|
+
if (!emittedDone) {
|
|
2940
|
+
yield { type: "done" };
|
|
2505
2941
|
}
|
|
2942
|
+
void sawAnyPatch;
|
|
2506
2943
|
void lastPatchHash;
|
|
2507
2944
|
}
|
|
2508
2945
|
async close() {
|
|
@@ -2579,6 +3016,7 @@ var createApp = (options) => {
|
|
|
2579
3016
|
app.use("/agent", createAgentRouter({ state, adapter, brokerRegistry }));
|
|
2580
3017
|
app.use("/agent/llm-response", createLlmResponseRouter({ brokerRegistry }));
|
|
2581
3018
|
app.use("/agent/llm-chunk", createLlmChunkRouter({ brokerRegistry }));
|
|
3019
|
+
app.use("/agent/permission", createPermissionRouter({ adapter }));
|
|
2582
3020
|
app.use("/llm-shim", createLlmShimRouter());
|
|
2583
3021
|
app.use((req, res) => {
|
|
2584
3022
|
res.status(404).json({ error: "not_found", method: req.method, path: req.path });
|