aegis-desktop 0.8.13 → 0.8.15
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/lib/local/entitlement.js +81 -0
- package/lib/local/local.js +54 -2
- package/lib/local/tools.js +113 -0
- package/main.js +22 -5
- package/package.json +1 -1
- package/renderer/app.js +94 -8
- package/renderer/transcript-view.js +70 -5
- package/vendor/aegis.js +118 -6
- package/vendor/agent-gate.mjs +1063 -0
package/vendor/aegis.js
CHANGED
|
@@ -65,6 +65,50 @@ function envIdleMs(name) {
|
|
|
65
65
|
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* DeepSeek's chat template wraps tool calls and turn boundaries in special
|
|
70
|
+
* tokens — fullwidth pipe (U+FF5C) delimiting an identifier that uses ▁
|
|
71
|
+
* (U+2581) as its word separator, e.g. |tool▁calls▁begin|, |tool▁call▁end|,
|
|
72
|
+
* |Assistant|, |begin▁of▁sentence|. These are chat-template internals, never
|
|
73
|
+
* user-facing text; when the provider's streaming tool-call path leaks them
|
|
74
|
+
* into `delta.content` they used to reach the screen verbatim — reported as
|
|
75
|
+
* the pasted "||DSML||" envelope on Windows/macOS. Returns a per-stream
|
|
76
|
+
* stripper closure with a short lookback buffer so a token split across two
|
|
77
|
+
* SSE chunks isn't half-rendered before its closing pipe arrives.
|
|
78
|
+
*/
|
|
79
|
+
const DEEPSEEK_ENVELOPE_RE = /|[A-Za-z][A-Za-z0-9]*(?:▁[A-Za-z0-9]+)*|/g;
|
|
80
|
+
function makeDeepSeekEnvelopeStripper() {
|
|
81
|
+
let buf = '';
|
|
82
|
+
const strip = (chunk) => {
|
|
83
|
+
buf += chunk;
|
|
84
|
+
buf = buf.replace(DEEPSEEK_ENVELOPE_RE, '');
|
|
85
|
+
const openIdx = buf.lastIndexOf('|');
|
|
86
|
+
if (openIdx !== -1) {
|
|
87
|
+
const tail = buf.slice(openIdx);
|
|
88
|
+
// Held back only while the tail still looks like an in-progress token
|
|
89
|
+
// (no closing pipe yet); a bare '|' or genuine prose flushes normally.
|
|
90
|
+
if (tail.length <= 60 && /^|[A-Za-z0-9▁]*$/.test(tail)) {
|
|
91
|
+
const out = buf.slice(0, openIdx);
|
|
92
|
+
buf = tail;
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const out = buf;
|
|
97
|
+
buf = '';
|
|
98
|
+
return out;
|
|
99
|
+
};
|
|
100
|
+
// Unconditional release of whatever is left buffered — called once the
|
|
101
|
+
// stream is fully drained, when a held-back '|…' is known to never close
|
|
102
|
+
// (dropped connection, or it was real prose all along) rather than an
|
|
103
|
+
// in-progress token.
|
|
104
|
+
strip.flush = () => {
|
|
105
|
+
const out = buf;
|
|
106
|
+
buf = '';
|
|
107
|
+
return out;
|
|
108
|
+
};
|
|
109
|
+
return strip;
|
|
110
|
+
}
|
|
111
|
+
|
|
68
112
|
/**
|
|
69
113
|
* UUID v4 that works everywhere: Web Crypto first (browsers, Node ≥ 19),
|
|
70
114
|
* then Node's CJS crypto module (Node < 19), then a Math.random fallback for
|
|
@@ -221,10 +265,26 @@ function createClient(opts = {}) {
|
|
|
221
265
|
}
|
|
222
266
|
throw err;
|
|
223
267
|
} finally {
|
|
268
|
+
// The timeout is time-to-*headers* only, so it is cleared the moment the
|
|
269
|
+
// response resolves: a long generation must never be cut off by it.
|
|
224
270
|
if (timer) clearTimeout(timer);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
271
|
+
// …but the caller's abort bridge is deliberately NOT removed here.
|
|
272
|
+
//
|
|
273
|
+
// `fetch` resolves at the status line, while the RESPONSE BODY is still
|
|
274
|
+
// bound to `controller.signal` and keeps being read long after this
|
|
275
|
+
// function has returned. Unhooking the caller's signal at that point
|
|
276
|
+
// detached Stop/Escape from the stream it was supposed to stop: the
|
|
277
|
+
// engine's own controller really did abort — `engine.cancel()` answered
|
|
278
|
+
// `{ ok: true }`, so the right controller was found and `abort()` was
|
|
279
|
+
// really called — and nothing happened, because the only thing wired to
|
|
280
|
+
// the live body had already been told to stop listening. The turn then
|
|
281
|
+
// streamed to completion while the UI sat on "stopping…", never settled,
|
|
282
|
+
// and never produced the salvaged bubble, because no error ever reached
|
|
283
|
+
// the caller's catch.
|
|
284
|
+
//
|
|
285
|
+
// `once: true` plus one signal per turn bounds the cost of leaving it
|
|
286
|
+
// attached to a single listener per turn — a far smaller price than a
|
|
287
|
+
// cancel button that does nothing.
|
|
228
288
|
}
|
|
229
289
|
}
|
|
230
290
|
|
|
@@ -838,6 +898,15 @@ function createClient(opts = {}) {
|
|
|
838
898
|
// resolve with text only and the caller could never see the calls.
|
|
839
899
|
const toolCallsByIndex = new Map();
|
|
840
900
|
let finishReason = null;
|
|
901
|
+
// `fullText` above stays RAW (it is also the `seen` cursor the snapshot
|
|
902
|
+
// reconciliation below compares against the provider's own un-stripped
|
|
903
|
+
// snapshots); `cleanText` is what the caller actually receives — the same
|
|
904
|
+
// stream, with DeepSeek's chat-template envelope removed — and is what the
|
|
905
|
+
// final `message.content` below is built from, so a non-streaming caller
|
|
906
|
+
// reading the resolved result sees the same clean text a streaming caller
|
|
907
|
+
// already saw via `onStream`.
|
|
908
|
+
let cleanText = '';
|
|
909
|
+
const stripEnvelope = makeDeepSeekEnvelopeStripper();
|
|
841
910
|
|
|
842
911
|
// Idle watchdog: if the server holds the connection open without ever
|
|
843
912
|
// sending another byte (a stuck upstream call, a proxy that swallows the
|
|
@@ -863,6 +932,7 @@ function createClient(opts = {}) {
|
|
|
863
932
|
let keepAlives = 0;
|
|
864
933
|
async function readWithIdleTimeout() {
|
|
865
934
|
let timer;
|
|
935
|
+
let onAbort = null;
|
|
866
936
|
const remaining = Math.max(0, idleMs - (Date.now() - lastPayloadAt));
|
|
867
937
|
const timeout = new Promise((_, reject) => {
|
|
868
938
|
timer = setTimeout(() => {
|
|
@@ -873,10 +943,37 @@ function createClient(opts = {}) {
|
|
|
873
943
|
));
|
|
874
944
|
}, remaining);
|
|
875
945
|
});
|
|
946
|
+
// The caller's abort, raced against the read directly rather than trusted
|
|
947
|
+
// to propagate through the body stream.
|
|
948
|
+
//
|
|
949
|
+
// Whether an abort of the caller's signal reaches a response body that is
|
|
950
|
+
// already being read is a property of `boundedFetch`, not of this loop —
|
|
951
|
+
// and it is not a property this loop can observe. Racing here makes "the
|
|
952
|
+
// caller stopped" a fact the read loop establishes for itself, so Stop
|
|
953
|
+
// and Escape end the turn at the next chunk boundary regardless of how
|
|
954
|
+
// the request's signal was assembled. Without it an aborted turn kept
|
|
955
|
+
// consuming a stream nobody wanted and `chat()` never settled at all.
|
|
956
|
+
const aborted = signal && typeof signal.addEventListener === 'function'
|
|
957
|
+
? new Promise((_, reject) => {
|
|
958
|
+
onAbort = () => {
|
|
959
|
+
const e = new Error('The turn was stopped.');
|
|
960
|
+
e.name = 'AbortError';
|
|
961
|
+
e.code = 'ABORT_ERR';
|
|
962
|
+
reject(e);
|
|
963
|
+
};
|
|
964
|
+
if (signal.aborted) onAbort();
|
|
965
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
966
|
+
})
|
|
967
|
+
: null;
|
|
876
968
|
try {
|
|
877
|
-
return await
|
|
969
|
+
return await (aborted
|
|
970
|
+
? Promise.race([reader.read(), timeout, aborted])
|
|
971
|
+
: Promise.race([reader.read(), timeout]));
|
|
878
972
|
} finally {
|
|
879
973
|
clearTimeout(timer);
|
|
974
|
+
if (signal && onAbort && typeof signal.removeEventListener === 'function') {
|
|
975
|
+
signal.removeEventListener('abort', onAbort);
|
|
976
|
+
}
|
|
880
977
|
}
|
|
881
978
|
}
|
|
882
979
|
|
|
@@ -969,7 +1066,13 @@ function createClient(opts = {}) {
|
|
|
969
1066
|
choice && choice.delta && choice.delta.content,
|
|
970
1067
|
choice && choice.message && choice.message.content,
|
|
971
1068
|
fullText,
|
|
972
|
-
(chunk) =>
|
|
1069
|
+
(chunk) => {
|
|
1070
|
+
const clean = stripEnvelope(chunk);
|
|
1071
|
+
if (clean) {
|
|
1072
|
+
cleanText += clean;
|
|
1073
|
+
onStream({ delta: clean });
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
973
1076
|
);
|
|
974
1077
|
const fragments =
|
|
975
1078
|
(choice && choice.delta && choice.delta.tool_calls) ||
|
|
@@ -999,13 +1102,22 @@ function createClient(opts = {}) {
|
|
|
999
1102
|
throw err;
|
|
1000
1103
|
}
|
|
1001
1104
|
|
|
1105
|
+
// A trailing '|…' that never closed (stream ended right after it, or it
|
|
1106
|
+
// was real prose all along) is real content, not an unfinished envelope
|
|
1107
|
+
// token — release it rather than dropping it silently.
|
|
1108
|
+
const strayTail = stripEnvelope.flush();
|
|
1109
|
+
if (strayTail) {
|
|
1110
|
+
cleanText += strayTail;
|
|
1111
|
+
onStream({ delta: strayTail });
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1002
1114
|
// An id-less fragment stream still yields usable calls, but the caller
|
|
1003
1115
|
// needs *an* id to pair results back, so synthesize one.
|
|
1004
1116
|
const toolCalls = [...toolCallsByIndex.entries()]
|
|
1005
1117
|
.sort((a, b) => a[0] - b[0])
|
|
1006
1118
|
.map(([idx, c]) => ({ ...c, id: c.id || `call_${idx}` }));
|
|
1007
1119
|
|
|
1008
|
-
const message = { content:
|
|
1120
|
+
const message = { content: cleanText };
|
|
1009
1121
|
if (toolCalls.length) message.tool_calls = toolCalls;
|
|
1010
1122
|
const result = {
|
|
1011
1123
|
model: resultModel,
|