@diologue/local-agent 0.3.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 +177 -10
- package/dist/cli.mjs.map +2 -2
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1266,7 +1266,10 @@ var agentMessageBodySchema = z2.object({
|
|
|
1266
1266
|
/** Optional user-chosen routing for this turn. When absent the cloud
|
|
1267
1267
|
* picks the default (CODING_AGENT_DEFAULT_PROVIDER/MODEL). */
|
|
1268
1268
|
preferredProvider: z2.string().min(1).max(100).optional(),
|
|
1269
|
-
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()
|
|
1270
1273
|
});
|
|
1271
1274
|
var writeEvent = (res, event) => {
|
|
1272
1275
|
res.write(`data: ${JSON.stringify(event)}
|
|
@@ -1322,7 +1325,8 @@ var createAgentRouter = (deps) => {
|
|
|
1322
1325
|
signal: controller.signal,
|
|
1323
1326
|
broker: (payload, observer) => broker.request(payload, observer),
|
|
1324
1327
|
preferredProvider: parsed.data.preferredProvider,
|
|
1325
|
-
preferredModel: parsed.data.preferredModel
|
|
1328
|
+
preferredModel: parsed.data.preferredModel,
|
|
1329
|
+
permissionMode: parsed.data.permissionMode
|
|
1326
1330
|
})) {
|
|
1327
1331
|
if (controller.signal.aborted) {
|
|
1328
1332
|
logAgentRoute(
|
|
@@ -1493,6 +1497,36 @@ var createLlmChunkRouter = (deps) => {
|
|
|
1493
1497
|
});
|
|
1494
1498
|
return router;
|
|
1495
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
|
+
};
|
|
1496
1530
|
|
|
1497
1531
|
// src/routes/llm-shim.ts
|
|
1498
1532
|
import { Router as createRouter3 } from "express";
|
|
@@ -2359,13 +2393,30 @@ var MockOpenCodeAdapter = class {
|
|
|
2359
2393
|
};
|
|
2360
2394
|
|
|
2361
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
|
+
};
|
|
2362
2404
|
var createMapState = (ourSessionId, openCodeSessionId) => ({
|
|
2363
2405
|
ourSessionId,
|
|
2364
2406
|
openCodeSessionId,
|
|
2365
2407
|
startedTools: /* @__PURE__ */ new Set(),
|
|
2366
2408
|
endedTools: /* @__PURE__ */ new Set(),
|
|
2367
|
-
announcedPatches: /* @__PURE__ */ new Set()
|
|
2409
|
+
announcedPatches: /* @__PURE__ */ new Set(),
|
|
2410
|
+
announcedPermissions: /* @__PURE__ */ new Set()
|
|
2368
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
|
+
};
|
|
2369
2420
|
var EMPTY = { events: [], done: false };
|
|
2370
2421
|
var mapEvent = (item, state) => {
|
|
2371
2422
|
const sid = item.properties?.sessionID;
|
|
@@ -2375,6 +2426,25 @@ var mapEvent = (item, state) => {
|
|
|
2375
2426
|
if (item.type === "session.idle" && sid === state.openCodeSessionId) {
|
|
2376
2427
|
return { events: [{ type: "done" }], done: true };
|
|
2377
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
|
+
}
|
|
2378
2448
|
if (item.type !== "message.part.updated") {
|
|
2379
2449
|
return EMPTY;
|
|
2380
2450
|
}
|
|
@@ -2413,7 +2483,10 @@ var mapEvent = (item, state) => {
|
|
|
2413
2483
|
type: "tool_call_end",
|
|
2414
2484
|
toolCallId: callId,
|
|
2415
2485
|
ok: status === "completed",
|
|
2416
|
-
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
|
+
)
|
|
2417
2490
|
};
|
|
2418
2491
|
return { events: [event], done: false };
|
|
2419
2492
|
}
|
|
@@ -2472,6 +2545,7 @@ init_engine_locator_npm();
|
|
|
2472
2545
|
var DIOLOGUE_PROVIDER_ID = "diologue";
|
|
2473
2546
|
var DIOLOGUE_MODEL_ID = "diologue-routed";
|
|
2474
2547
|
var END_OF_TURN_GRACE_MS = 600;
|
|
2548
|
+
var PERMISSION_DECISION_TIMEOUT_MS = 18e4;
|
|
2475
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.";
|
|
2476
2550
|
var logAdapter = (message) => {
|
|
2477
2551
|
console.error(`[opencode-adapter] ${message}`);
|
|
@@ -2485,6 +2559,9 @@ var OpenCodeProcessAdapter = class {
|
|
|
2485
2559
|
/** Maps our sessionId → opencode session id so successive turns within
|
|
2486
2560
|
* one coding-agent session reuse the same opencode conversation. */
|
|
2487
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();
|
|
2488
2565
|
async resolveLocator() {
|
|
2489
2566
|
if (this.options.engineLocator) {
|
|
2490
2567
|
return this.options.engineLocator;
|
|
@@ -2527,14 +2604,17 @@ var OpenCodeProcessAdapter = class {
|
|
|
2527
2604
|
}
|
|
2528
2605
|
},
|
|
2529
2606
|
model: `${DIOLOGUE_PROVIDER_ID}/${DIOLOGUE_MODEL_ID}`,
|
|
2530
|
-
//
|
|
2531
|
-
//
|
|
2532
|
-
//
|
|
2533
|
-
//
|
|
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().
|
|
2534
2614
|
permission: {
|
|
2535
2615
|
edit: "allow",
|
|
2536
|
-
bash: "
|
|
2537
|
-
webfetch: "
|
|
2616
|
+
bash: "ask",
|
|
2617
|
+
webfetch: "ask"
|
|
2538
2618
|
}
|
|
2539
2619
|
};
|
|
2540
2620
|
}
|
|
@@ -2579,6 +2659,44 @@ var OpenCodeProcessAdapter = class {
|
|
|
2579
2659
|
);
|
|
2580
2660
|
return id;
|
|
2581
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
|
+
}
|
|
2582
2700
|
async *streamMessage(request) {
|
|
2583
2701
|
let handle;
|
|
2584
2702
|
try {
|
|
@@ -2613,6 +2731,14 @@ var OpenCodeProcessAdapter = class {
|
|
|
2613
2731
|
);
|
|
2614
2732
|
let sawAnyPatch = false;
|
|
2615
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);
|
|
2616
2742
|
let activeBrokerHandle = null;
|
|
2617
2743
|
let streamScopedBroker = null;
|
|
2618
2744
|
if (request.broker) {
|
|
@@ -2725,6 +2851,34 @@ ${request.prompt}` }
|
|
|
2725
2851
|
);
|
|
2726
2852
|
}
|
|
2727
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
|
+
}
|
|
2728
2882
|
if (event.type === "diff_proposed" && mapped.patchToFetch) {
|
|
2729
2883
|
sawAnyPatch = true;
|
|
2730
2884
|
lastPatchHash = mapped.patchToFetch.hash;
|
|
@@ -2743,6 +2897,18 @@ ${request.prompt}` }
|
|
|
2743
2897
|
} catch {
|
|
2744
2898
|
}
|
|
2745
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);
|
|
2746
2912
|
activeBrokerHandle?.release();
|
|
2747
2913
|
streamScopedBroker?.close("turn_ended");
|
|
2748
2914
|
}
|
|
@@ -2850,6 +3016,7 @@ var createApp = (options) => {
|
|
|
2850
3016
|
app.use("/agent", createAgentRouter({ state, adapter, brokerRegistry }));
|
|
2851
3017
|
app.use("/agent/llm-response", createLlmResponseRouter({ brokerRegistry }));
|
|
2852
3018
|
app.use("/agent/llm-chunk", createLlmChunkRouter({ brokerRegistry }));
|
|
3019
|
+
app.use("/agent/permission", createPermissionRouter({ adapter }));
|
|
2853
3020
|
app.use("/llm-shim", createLlmShimRouter());
|
|
2854
3021
|
app.use((req, res) => {
|
|
2855
3022
|
res.status(404).json({ error: "not_found", method: req.method, path: req.path });
|