dsh-loop-engine 0.1.5-rc2 → 0.1.5-rc4
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/README.md +44 -177
- package/README.zh.md +48 -100
- package/lib/client.js +887 -266
- package/lib/index.js +2212 -914
- package/lib/invariant.js +43 -45
- package/lib/types/agent-preset-ids.d.ts +303 -0
- package/lib/types/client/LoopEngineBadge.d.ts +44 -17
- package/lib/types/client/LoopEngineComposerSelect.d.ts +79 -13
- package/lib/types/client/LoopEngineSection.d.ts +5 -4
- package/lib/types/client/locales.d.ts +133 -7
- package/lib/types/client/reload.d.ts +135 -0
- package/lib/types/client/session-engine.d.ts +474 -0
- package/lib/types/client/store.d.ts +1 -1
- package/lib/types/client/turn-status.d.ts +112 -10
- package/lib/types/client/use-session-engine.d.ts +66 -0
- package/lib/types/commands.d.ts +11 -3
- package/lib/types/driver-core/host-servers.d.ts +106 -0
- package/lib/types/driver-core/hosted-engine-runtime.d.ts +190 -0
- package/lib/types/driver-core/hosted-tool-vocabulary.d.ts +72 -0
- package/lib/types/driver-core/model-handover.d.ts +116 -0
- package/lib/types/driver-core/ownership.d.ts +6 -5
- package/lib/types/driver-core/prompt.d.ts +32 -0
- package/lib/types/driver-core/session-lifetime.d.ts +62 -0
- package/lib/types/driver-core/session-model.d.ts +82 -0
- package/lib/types/engine-claude/agent.d.ts +23 -3
- package/lib/types/engine-claude/loop.d.ts +16 -15
- package/lib/types/engine-codex/agent.d.ts +22 -3
- package/lib/types/engine-codex/appserver/client.d.ts +15 -2
- package/lib/types/engine-codex/loop.d.ts +13 -15
- package/lib/types/engine-codex/model-handover.d.ts +44 -0
- package/lib/types/engine-kimi/acp/client.d.ts +10 -0
- package/lib/types/engine-kimi/agent.d.ts +19 -2
- package/lib/types/engine-kimi/commands.d.ts +18 -14
- package/lib/types/engine-kimi/loop.d.ts +14 -16
- package/lib/types/engine-kimi/model-handover.d.ts +32 -0
- package/lib/types/engine-kimi/process.d.ts +2 -2
- package/lib/types/engine-kimi/types.d.ts +1 -1
- package/lib/types/engine-of-session.d.ts +97 -0
- package/lib/types/engine-pi/agent.d.ts +25 -23
- package/lib/types/engine-pi/loop.d.ts +13 -23
- package/lib/types/engine-pi/model-handover.d.ts +35 -0
- package/lib/types/engine-pi/types.d.ts +2 -2
- package/lib/types/engine-remote.d.ts +192 -0
- package/lib/types/engine-surface.d.ts +36 -0
- package/lib/types/index.d.ts +51 -50
- package/lib/types/invariant.d.ts +8 -5
- package/lib/types/model-selection-reset.d.ts +271 -0
- package/lib/types/patch-manager.d.ts +57 -39
- package/lib/types/preset.d.ts +39 -26
- package/lib/types/provider-route.d.ts +83 -36
- package/lib/types/router-loop.d.ts +406 -0
- package/lib/types/session-engine-store.d.ts +138 -0
- package/lib/types/settings.d.ts +12 -11
- package/package.json +109 -104
package/lib/index.js
CHANGED
|
@@ -1,7 +1,51 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
1
4
|
var __knownSymbol = (name2, symbol) => (symbol = Symbol[name2]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name2);
|
|
2
5
|
var __typeError = (msg) => {
|
|
3
6
|
throw TypeError(msg);
|
|
4
7
|
};
|
|
8
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
10
|
+
var __decoratorStart = (base) => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)];
|
|
11
|
+
var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
|
|
12
|
+
var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn;
|
|
13
|
+
var __decoratorContext = (kind, name2, done, metadata, fns) => ({ kind: __decoratorStrings[kind], name: name2, metadata, addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null)) });
|
|
14
|
+
var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
|
|
15
|
+
var __runInitializers = (array, flags, self, value) => {
|
|
16
|
+
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) flags & 1 ? fns[i].call(self) : value = fns[i].call(self, value);
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
var __decorateElement = (array, flags, name2, decorators, target, extra) => {
|
|
20
|
+
var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
|
|
21
|
+
var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
|
|
22
|
+
var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
|
|
23
|
+
var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : { get [name2]() {
|
|
24
|
+
return __privateGet(this, extra);
|
|
25
|
+
}, set [name2](x) {
|
|
26
|
+
return __privateSet(this, extra, x);
|
|
27
|
+
} }, name2));
|
|
28
|
+
k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name2) : __name(target, name2);
|
|
29
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
30
|
+
ctx = __decoratorContext(k, name2, done = {}, array[3], extraInitializers);
|
|
31
|
+
if (k) {
|
|
32
|
+
ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => name2 in x };
|
|
33
|
+
if (k ^ 3) access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name2];
|
|
34
|
+
if (k > 2) access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name2] = y;
|
|
35
|
+
}
|
|
36
|
+
it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? void 0 : { get: desc.get, set: desc.set } : target, ctx), done._ = 1;
|
|
37
|
+
if (k ^ 4 || it === void 0) __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
|
|
38
|
+
else if (typeof it !== "object" || it === null) __typeError("Object expected");
|
|
39
|
+
else __expectFn(fn = it.get) && (desc.get = fn), __expectFn(fn = it.set) && (desc.set = fn), __expectFn(fn = it.init) && initializers.unshift(fn);
|
|
40
|
+
}
|
|
41
|
+
return k || __decoratorMetadata(array, target), desc && __defProp(target, name2, desc), p ? k ^ 4 ? extra : desc : target;
|
|
42
|
+
};
|
|
43
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
44
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
45
|
+
var __privateIn = (member, obj) => Object(obj) !== obj ? __typeError('Cannot use the "in" operator on this value') : member.has(obj);
|
|
46
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
47
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
48
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
5
49
|
var __using = (stack, value, async) => {
|
|
6
50
|
if (value != null) {
|
|
7
51
|
if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
|
|
@@ -45,12 +89,135 @@ var __callDispose = (stack, error, hasError) => {
|
|
|
45
89
|
};
|
|
46
90
|
|
|
47
91
|
// src/index.ts
|
|
48
|
-
import {
|
|
92
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
49
93
|
import { mkdir as mkdir2, readFile as readFile5, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
50
|
-
import { randomUUID as
|
|
51
|
-
import { dirname as
|
|
94
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
95
|
+
import { dirname as dirname6, join as join14 } from "node:path";
|
|
52
96
|
import z6 from "@deepseek-ai/schemastery";
|
|
53
|
-
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
97
|
+
import { resolveDshHome as resolveDshHome2 } from "@deepseek-ai/dsh-home-paths";
|
|
98
|
+
|
|
99
|
+
// src/agent-preset-ids.ts
|
|
100
|
+
var LOOP_ENGINE_IDS = ["in-process", "claude-code", "codex", "pi", "kimi"];
|
|
101
|
+
var HOSTED_ENGINE_IDS = LOOP_ENGINE_IDS.filter(
|
|
102
|
+
(id) => id !== "in-process"
|
|
103
|
+
);
|
|
104
|
+
var HOSTED_ROUTE_LABEL = "external";
|
|
105
|
+
var HOSTED_ROUTE_NAME = "external";
|
|
106
|
+
var HOSTED_DEFAULT_MODEL = "default";
|
|
107
|
+
var HOSTED_PRESET_PREFIX = "loop-engine-";
|
|
108
|
+
var SOURCE_PRESET_ID = "standard";
|
|
109
|
+
var LEGACY_HOSTED_PRESET_ID = "loop-engine";
|
|
110
|
+
function enginePresetId(engine) {
|
|
111
|
+
return engine === "in-process" ? SOURCE_PRESET_ID : `${HOSTED_PRESET_PREFIX}${engine}`;
|
|
112
|
+
}
|
|
113
|
+
function isLoopEngineId(value) {
|
|
114
|
+
return LOOP_ENGINE_IDS.includes(value);
|
|
115
|
+
}
|
|
116
|
+
function engineOfPreset(presetId) {
|
|
117
|
+
if (presetId === void 0) return void 0;
|
|
118
|
+
const engine = presetId.startsWith(HOSTED_PRESET_PREFIX) ? presetId.slice(HOSTED_PRESET_PREFIX.length) : "";
|
|
119
|
+
return HOSTED_ENGINE_IDS.includes(engine) ? engine : void 0;
|
|
120
|
+
}
|
|
121
|
+
function sessionEngineOf(presetId) {
|
|
122
|
+
if (typeof presetId !== "string") return { kind: "unset" };
|
|
123
|
+
if (presetId === LEGACY_HOSTED_PRESET_ID) return { kind: "legacy" };
|
|
124
|
+
return { kind: "engine", engine: engineOfPreset(presetId) ?? "in-process" };
|
|
125
|
+
}
|
|
126
|
+
function hostedEngineOf(session) {
|
|
127
|
+
if (session.kind !== "engine" || session.engine === "in-process") return void 0;
|
|
128
|
+
return session.engine;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/engine-of-session.ts
|
|
132
|
+
async function engineOfSession(ctx, sessionId, records) {
|
|
133
|
+
var _stack = [];
|
|
134
|
+
try {
|
|
135
|
+
const recorded = records.engineOf(sessionId);
|
|
136
|
+
if (recorded !== void 0) return { kind: "engine", engine: recorded };
|
|
137
|
+
const query = ctx.get("sessionQuery");
|
|
138
|
+
if (query === void 0) return { kind: "unset" };
|
|
139
|
+
const observation = __using(_stack, await query.observeSession(sessionId, { projectionMode: "all" }));
|
|
140
|
+
return sessionEngineOf(observation.projections?.values.agentPreset);
|
|
141
|
+
} catch (_) {
|
|
142
|
+
var _error = _, _hasError = true;
|
|
143
|
+
} finally {
|
|
144
|
+
__callDispose(_stack, _error, _hasError);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function engineReportOfSession(ctx, sessionId, records, liveEngineOf) {
|
|
148
|
+
const live = liveEngineOf(sessionId);
|
|
149
|
+
if (live === void 0) return { engine: await engineOfSession(ctx, sessionId, records) };
|
|
150
|
+
const engine = { kind: "engine", engine: live };
|
|
151
|
+
const recorded = records.engineOf(sessionId);
|
|
152
|
+
return recorded === void 0 || recorded === live ? { engine } : { engine, pending: recorded };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/engine-remote.ts
|
|
156
|
+
import { Remote, RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
157
|
+
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
158
|
+
var LOOP_ENGINE_REMOTE_KEY = "loopEngine";
|
|
159
|
+
var LOOP_ENGINE_REMOTE_METHOD = "engine";
|
|
160
|
+
var LOOP_ENGINE_REMOTE_SELECT_METHOD = "select";
|
|
161
|
+
var _select_dec, _engine_dec, _a, _init;
|
|
162
|
+
var LoopEngineRemote = class extends (_a = TypertRemoteService, _engine_dec = [Remote(LOOP_ENGINE_REMOTE_METHOD)], _select_dec = [Remote(LOOP_ENGINE_REMOTE_SELECT_METHOD)], _a) {
|
|
163
|
+
/**
|
|
164
|
+
* @param ctx - the context the service (and its lifetime) belongs to.
|
|
165
|
+
* @param resolve - the router-less read; normally
|
|
166
|
+
* `engineOfSession(ctx, sessionId, records)`.
|
|
167
|
+
* @param router - where both endpoints find the mounted loop router.
|
|
168
|
+
* @param warn - diagnostic sink for a session that could not be read.
|
|
169
|
+
*/
|
|
170
|
+
constructor(ctx, resolve5, router, warn) {
|
|
171
|
+
super(ctx, LOOP_ENGINE_REMOTE_KEY);
|
|
172
|
+
__runInitializers(_init, 5, this);
|
|
173
|
+
__publicField(this, "resolve");
|
|
174
|
+
__publicField(this, "router");
|
|
175
|
+
__publicField(this, "warn");
|
|
176
|
+
this.resolve = resolve5;
|
|
177
|
+
this.router = router;
|
|
178
|
+
this.warn = warn;
|
|
179
|
+
}
|
|
180
|
+
async engine(request) {
|
|
181
|
+
const sessionId = request?.sessionId;
|
|
182
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
183
|
+
throw new RemoteError("gateway/bad-request", "sessionId must be a non-empty string", {});
|
|
184
|
+
}
|
|
185
|
+
const router = this.router.current;
|
|
186
|
+
try {
|
|
187
|
+
return router === void 0 ? { engine: await this.resolve(SessionId(sessionId)) } : await router.reportEngine(SessionId(sessionId));
|
|
188
|
+
} catch (error) {
|
|
189
|
+
this.warn(`loop-engine: could not read the engine of session "${sessionId}": ${String(error)}`);
|
|
190
|
+
return { engine: { kind: "unset" } };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async select(request) {
|
|
194
|
+
const sessionId = request?.sessionId;
|
|
195
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
196
|
+
throw new RemoteError("gateway/bad-request", "sessionId must be a non-empty string", {});
|
|
197
|
+
}
|
|
198
|
+
const engine = request?.engine;
|
|
199
|
+
if (!isLoopEngineId(engine)) {
|
|
200
|
+
throw new RemoteError(
|
|
201
|
+
"gateway/bad-request",
|
|
202
|
+
`engine must be one of ${LOOP_ENGINE_IDS.join(", ")}`,
|
|
203
|
+
{}
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const router = this.router.current;
|
|
207
|
+
if (router === void 0) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
code: "router-unmounted",
|
|
211
|
+
reason: "this process cannot switch engines yet: the loop router is not mounted"
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return await router.selectEngine(SessionId(sessionId), engine);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
_init = __decoratorStart(_a);
|
|
218
|
+
__decorateElement(_init, 1, "engine", _engine_dec, LoopEngineRemote);
|
|
219
|
+
__decorateElement(_init, 1, "select", _select_dec, LoopEngineRemote);
|
|
220
|
+
__decoratorMetadata(_init, LoopEngineRemote);
|
|
54
221
|
|
|
55
222
|
// src/engine-claude/loop.ts
|
|
56
223
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -60,7 +227,7 @@ import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
|
|
|
60
227
|
import { agentEvents } from "@deepseek-ai/dsh-agent";
|
|
61
228
|
import {
|
|
62
229
|
AssistantStreamAccumulator as AssistantStreamAccumulator2,
|
|
63
|
-
LlmError,
|
|
230
|
+
LlmError as LlmError2,
|
|
64
231
|
createAssistantMessage,
|
|
65
232
|
createUserMessage,
|
|
66
233
|
errorChain
|
|
@@ -243,6 +410,19 @@ function renderToolResult(message) {
|
|
|
243
410
|
const tag = block.isError === true ? "tool-result-error" : "tool-result";
|
|
244
411
|
return frame(tag, body || "(no content)");
|
|
245
412
|
}
|
|
413
|
+
var ENGINE_SLASH_LINE = /^\/[^\s/]+(?:[ \t]+.*)?$/;
|
|
414
|
+
function engineSlashPrompt(messages) {
|
|
415
|
+
const last = messages.at(-1);
|
|
416
|
+
if (last === void 0 || last.role !== "user") return void 0;
|
|
417
|
+
const user = last;
|
|
418
|
+
if (user.source.kind !== "user") return void 0;
|
|
419
|
+
if (user.content.length !== 1) return void 0;
|
|
420
|
+
const block = user.content[0];
|
|
421
|
+
if (block?.type !== "text") return void 0;
|
|
422
|
+
const text = block.text;
|
|
423
|
+
if (text.includes("\n")) return void 0;
|
|
424
|
+
return ENGINE_SLASH_LINE.test(text) ? text : void 0;
|
|
425
|
+
}
|
|
246
426
|
function serializeHistory(messages) {
|
|
247
427
|
const sections = [];
|
|
248
428
|
for (const message of messages) {
|
|
@@ -444,6 +624,139 @@ var DriverInbox = class {
|
|
|
444
624
|
}
|
|
445
625
|
};
|
|
446
626
|
|
|
627
|
+
// src/provider-route.ts
|
|
628
|
+
import { LlmAdapter, LlmError } from "@deepseek-ai/dsh-llm";
|
|
629
|
+
function hostedRouteLabelOf(_engine) {
|
|
630
|
+
return HOSTED_ROUTE_LABEL;
|
|
631
|
+
}
|
|
632
|
+
var HOSTED_PROVIDER_LABELS = [HOSTED_ROUTE_LABEL, "claude-code", "codex", "pi", "kimi"];
|
|
633
|
+
function isHostedProviderRoute(provider) {
|
|
634
|
+
return HOSTED_PROVIDER_LABELS.includes(provider);
|
|
635
|
+
}
|
|
636
|
+
var HostedEngineRouteAdapter = class extends LlmAdapter {
|
|
637
|
+
/**
|
|
638
|
+
* Name the route's one entry.
|
|
639
|
+
*
|
|
640
|
+
* `id` must equal the provider the adapter was registered for (the registry
|
|
641
|
+
* rejects a mismatch — `packages/llm/llm/src/index.ts` `prepareRoutes`), so it
|
|
642
|
+
* is echoed back; `name` is the one localized string the catalog carries.
|
|
643
|
+
* @param provider - the route this adapter is registered for.
|
|
644
|
+
* @returns the route's display metadata.
|
|
645
|
+
*/
|
|
646
|
+
providerInfo(provider) {
|
|
647
|
+
return { id: provider, name: HOSTED_ROUTE_NAME };
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Advertise the route's single entry.
|
|
651
|
+
*
|
|
652
|
+
* The id is the model label the engine logs, so the picker resolves a hosted
|
|
653
|
+
* session's selection to this entry (and highlights its row) rather than
|
|
654
|
+
* falling through to the raw composite string; the name is the same word,
|
|
655
|
+
* rendered as-is.
|
|
656
|
+
* @returns the one entry this route advertises.
|
|
657
|
+
*/
|
|
658
|
+
listModels(_provider) {
|
|
659
|
+
return Promise.resolve([{ provider: HOSTED_ROUTE_LABEL, id: HOSTED_DEFAULT_MODEL, name: HOSTED_DEFAULT_MODEL }]);
|
|
660
|
+
}
|
|
661
|
+
stream(_options) {
|
|
662
|
+
throw new LlmError(
|
|
663
|
+
`provider "${HOSTED_ROUTE_LABEL}" is a hosted loop engine route, not a model endpoint`,
|
|
664
|
+
"HOSTED_ENGINE_ROUTE"
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// src/driver-core/session-model.ts
|
|
670
|
+
function currentSelection(session, projections) {
|
|
671
|
+
const pending = projections?.stateOf(session, "modelSelection")?.pending ?? null;
|
|
672
|
+
if (pending !== null) return pending;
|
|
673
|
+
const header = session.requestHeader()?.config;
|
|
674
|
+
return header === void 0 ? void 0 : { provider: header.provider, model: header.model };
|
|
675
|
+
}
|
|
676
|
+
function sessionModelOverride(selection) {
|
|
677
|
+
if (selection === void 0) return void 0;
|
|
678
|
+
if (isHostedProviderRoute(selection.provider)) return void 0;
|
|
679
|
+
return { provider: selection.provider, model: selection.model };
|
|
680
|
+
}
|
|
681
|
+
function sessionModelOverrideOf(ctx, session) {
|
|
682
|
+
return sessionModelOverride(
|
|
683
|
+
currentSelection(session, ctx.get("sessionProjections"))
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/driver-core/model-handover.ts
|
|
688
|
+
var warned = /* @__PURE__ */ new WeakMap();
|
|
689
|
+
function warnOnce(ctx, key, message) {
|
|
690
|
+
let seen = warned.get(ctx);
|
|
691
|
+
if (seen === void 0) {
|
|
692
|
+
seen = /* @__PURE__ */ new Set();
|
|
693
|
+
warned.set(ctx, seen);
|
|
694
|
+
}
|
|
695
|
+
if (seen.has(key)) return;
|
|
696
|
+
seen.add(key);
|
|
697
|
+
ctx.logger.warn(message);
|
|
698
|
+
}
|
|
699
|
+
function settingsAddress(ctx, provider) {
|
|
700
|
+
const llm = ctx.get("llm");
|
|
701
|
+
return llm?.listConfigurableProviders?.().find((entry) => entry.provider === provider);
|
|
702
|
+
}
|
|
703
|
+
function readProviderProfile(ctx, address) {
|
|
704
|
+
const settings = ctx.get("settings");
|
|
705
|
+
let node = settings?.get?.(address.settingsNs);
|
|
706
|
+
for (const segment of address.settingsPath) {
|
|
707
|
+
if (node === null || typeof node !== "object") return void 0;
|
|
708
|
+
node = node[segment];
|
|
709
|
+
}
|
|
710
|
+
if (node === null || typeof node !== "object") return void 0;
|
|
711
|
+
const record = node;
|
|
712
|
+
const { baseURL, api, apiKeyEnv } = record;
|
|
713
|
+
if (typeof baseURL !== "string" || baseURL.length === 0) return void 0;
|
|
714
|
+
if (typeof api !== "string" || api.length === 0) return void 0;
|
|
715
|
+
return {
|
|
716
|
+
baseURL,
|
|
717
|
+
api,
|
|
718
|
+
apiKeyEnv: typeof apiKeyEnv === "string" && apiKeyEnv.length > 0 ? apiKeyEnv : void 0
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
async function readApiKey(ctx, apiKeyEnv) {
|
|
722
|
+
if (apiKeyEnv === void 0) return void 0;
|
|
723
|
+
const credentials = ctx.get("credentials");
|
|
724
|
+
const stored = credentials === void 0 ? void 0 : (await credentials.resolve(apiKeyEnv))?.value;
|
|
725
|
+
if (stored !== void 0 && stored.length > 0) return stored;
|
|
726
|
+
const ambient = process.env[apiKeyEnv];
|
|
727
|
+
return ambient !== void 0 && ambient.length > 0 ? ambient : void 0;
|
|
728
|
+
}
|
|
729
|
+
async function resolveModelHandover(ctx, override) {
|
|
730
|
+
if (override === void 0) return void 0;
|
|
731
|
+
const address = settingsAddress(ctx, override.provider);
|
|
732
|
+
if (address === void 0) {
|
|
733
|
+
return refuse(ctx, override, `no llm registry route maps it to a settings namespace`);
|
|
734
|
+
}
|
|
735
|
+
const profile = readProviderProfile(ctx, address);
|
|
736
|
+
if (profile === void 0) {
|
|
737
|
+
return refuse(ctx, override, `the "${address.settingsNs}" settings section names no baseURL and wire protocol for it`);
|
|
738
|
+
}
|
|
739
|
+
const apiKey = await readApiKey(ctx, profile.apiKeyEnv);
|
|
740
|
+
if (apiKey === void 0) {
|
|
741
|
+
return refuse(ctx, override, "no credential resolves for its apiKeyEnv");
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
provider: override.provider,
|
|
745
|
+
model: override.model,
|
|
746
|
+
baseURL: profile.baseURL,
|
|
747
|
+
api: profile.api,
|
|
748
|
+
apiKey
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
function refuse(ctx, override, reason) {
|
|
752
|
+
warnOnce(
|
|
753
|
+
ctx,
|
|
754
|
+
`${override.provider}/${override.model}:${reason}`,
|
|
755
|
+
`loop-engine: the session selected the dsh model "${override.provider}/${override.model}", but its endpoint could not be resolved (${reason}); the engine keeps its own provider configuration`
|
|
756
|
+
);
|
|
757
|
+
return void 0;
|
|
758
|
+
}
|
|
759
|
+
|
|
447
760
|
// src/driver-core/assistant-stream.ts
|
|
448
761
|
import {
|
|
449
762
|
AssistantStreamAccumulator,
|
|
@@ -549,6 +862,95 @@ var DriverAssistantStream = class {
|
|
|
549
862
|
}
|
|
550
863
|
};
|
|
551
864
|
|
|
865
|
+
// src/driver-core/hosted-tool-vocabulary.ts
|
|
866
|
+
var TODO_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
|
|
867
|
+
function recordOf(argumentsJson) {
|
|
868
|
+
let parsed;
|
|
869
|
+
try {
|
|
870
|
+
parsed = JSON.parse(argumentsJson);
|
|
871
|
+
} catch {
|
|
872
|
+
return void 0;
|
|
873
|
+
}
|
|
874
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
|
|
875
|
+
}
|
|
876
|
+
function stringField(value) {
|
|
877
|
+
return typeof value === "string" ? value : void 0;
|
|
878
|
+
}
|
|
879
|
+
var CLAUDE_NAMES = {
|
|
880
|
+
Write: "write",
|
|
881
|
+
Edit: "edit",
|
|
882
|
+
Read: "read",
|
|
883
|
+
Bash: "bash",
|
|
884
|
+
TodoWrite: "todo_write"
|
|
885
|
+
};
|
|
886
|
+
var KIMI_NAMES = {
|
|
887
|
+
Bash: "bash",
|
|
888
|
+
Read: "read",
|
|
889
|
+
Write: "write",
|
|
890
|
+
Edit: "edit"
|
|
891
|
+
};
|
|
892
|
+
var CODEX_NAMES = {
|
|
893
|
+
command_execution: "bash"
|
|
894
|
+
};
|
|
895
|
+
function withoutPath(args) {
|
|
896
|
+
const rest = { ...args };
|
|
897
|
+
delete rest.path;
|
|
898
|
+
return rest;
|
|
899
|
+
}
|
|
900
|
+
function piReshape(engineName, args) {
|
|
901
|
+
const path = stringField(args.path);
|
|
902
|
+
if (path === void 0) return void 0;
|
|
903
|
+
if (engineName === "write") {
|
|
904
|
+
const content = stringField(args.content);
|
|
905
|
+
return content === void 0 ? void 0 : { file_path: path, ...withoutPath(args), content };
|
|
906
|
+
}
|
|
907
|
+
if (engineName === "read") return { file_path: path, ...withoutPath(args) };
|
|
908
|
+
if (engineName !== "edit") return void 0;
|
|
909
|
+
const edits = args.edits;
|
|
910
|
+
if (!Array.isArray(edits) || edits.length !== 1) return void 0;
|
|
911
|
+
const edit = edits[0];
|
|
912
|
+
if (typeof edit !== "object" || edit === null) return void 0;
|
|
913
|
+
const oldText = stringField(edit.oldText);
|
|
914
|
+
const newText = stringField(edit.newText);
|
|
915
|
+
return oldText === void 0 || newText === void 0 ? void 0 : { file_path: path, old_string: oldText, new_string: newText };
|
|
916
|
+
}
|
|
917
|
+
function claudePlan(engineName, args) {
|
|
918
|
+
if (engineName !== "TodoWrite") return void 0;
|
|
919
|
+
const todos = args.todos;
|
|
920
|
+
if (!Array.isArray(todos)) return void 0;
|
|
921
|
+
const entries = [];
|
|
922
|
+
for (const entry of todos) {
|
|
923
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
924
|
+
const content = stringField(entry.content);
|
|
925
|
+
const status = entry.status;
|
|
926
|
+
if (content === void 0 || typeof status !== "string" || !TODO_STATUSES.has(status)) continue;
|
|
927
|
+
entries.push({ content, status });
|
|
928
|
+
}
|
|
929
|
+
return entries;
|
|
930
|
+
}
|
|
931
|
+
var VOCABULARIES = {
|
|
932
|
+
"claude-code": { names: CLAUDE_NAMES, plan: claudePlan },
|
|
933
|
+
codex: { names: CODEX_NAMES },
|
|
934
|
+
pi: { names: {}, reshape: piReshape },
|
|
935
|
+
kimi: { names: KIMI_NAMES }
|
|
936
|
+
};
|
|
937
|
+
function normalizeHostedToolCall(engine, name2, argumentsJson) {
|
|
938
|
+
const vocabulary = VOCABULARIES[engine];
|
|
939
|
+
const dshName = vocabulary.names[name2] ?? name2;
|
|
940
|
+
const args = recordOf(argumentsJson);
|
|
941
|
+
const reshaped = args === void 0 ? void 0 : vocabulary.reshape?.(name2, args);
|
|
942
|
+
return {
|
|
943
|
+
name: dshName,
|
|
944
|
+
arguments: reshaped === void 0 ? argumentsJson : JSON.stringify(reshaped)
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
function planTodosOfHostedTool(engine, name2, argumentsJson) {
|
|
948
|
+
const plan = VOCABULARIES[engine].plan;
|
|
949
|
+
if (plan === void 0) return void 0;
|
|
950
|
+
const args = recordOf(argumentsJson);
|
|
951
|
+
return args === void 0 ? void 0 : plan(name2, args);
|
|
952
|
+
}
|
|
953
|
+
|
|
552
954
|
// src/driver-core/permission-knobs.ts
|
|
553
955
|
var SANDBOX_MODES = ["read-only", "workspace-write", "danger-full-access"];
|
|
554
956
|
var APPROVAL_POLICIES = ["ask", "never"];
|
|
@@ -798,8 +1200,7 @@ function invokedSkillNames(messages) {
|
|
|
798
1200
|
}
|
|
799
1201
|
|
|
800
1202
|
// src/engine-claude/agent.ts
|
|
801
|
-
var PROVIDER =
|
|
802
|
-
var NATIVE_MODEL_LABEL = "claude-code-native";
|
|
1203
|
+
var PROVIDER = HOSTED_ROUTE_LABEL;
|
|
803
1204
|
function failureCode(subtype) {
|
|
804
1205
|
switch (subtype) {
|
|
805
1206
|
case "error_during_execution":
|
|
@@ -812,12 +1213,13 @@ function failureCode(subtype) {
|
|
|
812
1213
|
}
|
|
813
1214
|
}
|
|
814
1215
|
var ClaudeCodeAgent = class {
|
|
815
|
-
constructor(loopCtx, id, options, session, config) {
|
|
1216
|
+
constructor(loopCtx, id, options, session, config, spawn4) {
|
|
816
1217
|
this.loopCtx = loopCtx;
|
|
817
1218
|
this.id = id;
|
|
818
1219
|
this.options = options;
|
|
819
1220
|
this.session = session;
|
|
820
1221
|
this.config = config;
|
|
1222
|
+
this.spawn = spawn4;
|
|
821
1223
|
this.dispatch = agentEvents(loopCtx, this);
|
|
822
1224
|
this.inbox = new DriverInbox(session, {
|
|
823
1225
|
inserted: (message) => {
|
|
@@ -840,6 +1242,7 @@ var ClaudeCodeAgent = class {
|
|
|
840
1242
|
options;
|
|
841
1243
|
session;
|
|
842
1244
|
config;
|
|
1245
|
+
spawn;
|
|
843
1246
|
inbox;
|
|
844
1247
|
phase;
|
|
845
1248
|
activityDone = Promise.resolve();
|
|
@@ -1126,7 +1529,7 @@ var ClaudeCodeAgent = class {
|
|
|
1126
1529
|
}
|
|
1127
1530
|
turnEnds = {
|
|
1128
1531
|
kind: "error",
|
|
1129
|
-
error: error instanceof
|
|
1532
|
+
error: error instanceof LlmError2 ? error.failure : { message: errorChain(error), code: "UNKNOWN" }
|
|
1130
1533
|
};
|
|
1131
1534
|
this.throwError(error);
|
|
1132
1535
|
} finally {
|
|
@@ -1162,7 +1565,7 @@ var ClaudeCodeAgent = class {
|
|
|
1162
1565
|
}
|
|
1163
1566
|
/** Model label recorded in the request header for one lifecycle. */
|
|
1164
1567
|
modelLabel() {
|
|
1165
|
-
return this.config.model ??
|
|
1568
|
+
return this.config.model ?? HOSTED_DEFAULT_MODEL;
|
|
1166
1569
|
}
|
|
1167
1570
|
/** Append the request header snapshot once per loop instance. */
|
|
1168
1571
|
assertRequestHeader() {
|
|
@@ -1190,7 +1593,7 @@ var ClaudeCodeAgent = class {
|
|
|
1190
1593
|
throw new Error(`agent "${this.id}": no working directory \u2014 start the session with cwd metadata`);
|
|
1191
1594
|
}
|
|
1192
1595
|
const history = this.session.deriveMessages();
|
|
1193
|
-
const prompt = serializeHistory(history);
|
|
1596
|
+
const prompt = engineSlashPrompt(history) ?? serializeHistory(history);
|
|
1194
1597
|
if (prompt.length === 0) {
|
|
1195
1598
|
throw new Error(`agent "${this.id}": cannot derive a prompt from an empty session log`);
|
|
1196
1599
|
}
|
|
@@ -1206,14 +1609,21 @@ var ClaudeCodeAgent = class {
|
|
|
1206
1609
|
const diagnostics = [];
|
|
1207
1610
|
let live;
|
|
1208
1611
|
try {
|
|
1612
|
+
const override = sessionModelOverrideOf(this.loopCtx, this.session);
|
|
1613
|
+
const handover = await resolveModelHandover(this.loopCtx, override);
|
|
1614
|
+
const model = handover?.model ?? override?.model ?? this.config.model;
|
|
1209
1615
|
const options = claudeQueryOptions({
|
|
1210
1616
|
cwd,
|
|
1211
1617
|
...this.queryPermission(),
|
|
1212
|
-
env: this.config.env
|
|
1618
|
+
env: handover === void 0 ? this.config.env : {
|
|
1619
|
+
...this.config.env,
|
|
1620
|
+
ANTHROPIC_BASE_URL: handover.baseURL,
|
|
1621
|
+
ANTHROPIC_AUTH_TOKEN: handover.apiKey
|
|
1622
|
+
},
|
|
1213
1623
|
disposeGraceMs: this.config.disposeGraceMs,
|
|
1214
|
-
...
|
|
1624
|
+
...model === void 0 ? {} : { model },
|
|
1215
1625
|
...this.config.maxTurns === void 0 ? {} : { maxTurns: this.config.maxTurns },
|
|
1216
|
-
spawn:
|
|
1626
|
+
spawn: this.spawn,
|
|
1217
1627
|
onUnattended: (line) => {
|
|
1218
1628
|
diagnostics.push(line);
|
|
1219
1629
|
}
|
|
@@ -1297,13 +1707,16 @@ var ClaudeCodeAgent = class {
|
|
|
1297
1707
|
}
|
|
1298
1708
|
}
|
|
1299
1709
|
for (const call of mapped.toolCalls) {
|
|
1710
|
+
const normalized = normalizeHostedToolCall("claude-code", call.name, call.arguments);
|
|
1300
1711
|
this.session.append("tool/call", {
|
|
1301
1712
|
turn: phase.turn,
|
|
1302
1713
|
step: phase.step,
|
|
1303
1714
|
callId: call.callId,
|
|
1304
|
-
name:
|
|
1305
|
-
arguments:
|
|
1715
|
+
name: normalized.name,
|
|
1716
|
+
arguments: normalized.arguments
|
|
1306
1717
|
});
|
|
1718
|
+
const todos = planTodosOfHostedTool("claude-code", call.name, call.arguments);
|
|
1719
|
+
if (todos !== void 0) this.session.append("todo/write", { todos: [...todos] });
|
|
1307
1720
|
}
|
|
1308
1721
|
break;
|
|
1309
1722
|
}
|
|
@@ -1325,7 +1738,7 @@ var ClaudeCodeAgent = class {
|
|
|
1325
1738
|
step: phase.step,
|
|
1326
1739
|
message: createAssistantMessage({
|
|
1327
1740
|
content: trailing,
|
|
1328
|
-
source: { provider: PROVIDER, model:
|
|
1741
|
+
source: { provider: PROVIDER, model: HOSTED_DEFAULT_MODEL }
|
|
1329
1742
|
}),
|
|
1330
1743
|
...pendingUsage === void 0 ? {} : { usage: pendingUsage },
|
|
1331
1744
|
stream: attempt?.stream ?? []
|
|
@@ -1352,7 +1765,7 @@ var ClaudeCodeAgent = class {
|
|
|
1352
1765
|
finished = true;
|
|
1353
1766
|
} else {
|
|
1354
1767
|
const summary = message.errors[0] ?? `claude code query failed (${message.subtype})`;
|
|
1355
|
-
throw new
|
|
1768
|
+
throw new LlmError2(summary, failureCode(message.subtype));
|
|
1356
1769
|
}
|
|
1357
1770
|
break;
|
|
1358
1771
|
}
|
|
@@ -1361,7 +1774,7 @@ var ClaudeCodeAgent = class {
|
|
|
1361
1774
|
}
|
|
1362
1775
|
}
|
|
1363
1776
|
if (!finished) {
|
|
1364
|
-
throw new
|
|
1777
|
+
throw new LlmError2(
|
|
1365
1778
|
`agent "${this.id}": claude-code query ended without a result message`,
|
|
1366
1779
|
"CLAUDE_CODE_NO_RESULT"
|
|
1367
1780
|
);
|
|
@@ -1376,8 +1789,7 @@ var ClaudeCodeAgent = class {
|
|
|
1376
1789
|
}
|
|
1377
1790
|
};
|
|
1378
1791
|
|
|
1379
|
-
// src/driver-core/hosted-
|
|
1380
|
-
import { Service } from "@deepseek-ai/cordis";
|
|
1792
|
+
// src/driver-core/hosted-engine-runtime.ts
|
|
1381
1793
|
import { emitAgentEvent } from "@deepseek-ai/dsh-agent";
|
|
1382
1794
|
import { interruptedTurnClosers, SessionLogOffset, SessionPreparation } from "@deepseek-ai/dsh-session";
|
|
1383
1795
|
|
|
@@ -1469,35 +1881,83 @@ async function raceAbortCall(operation, signal, id, releaseAbandoned) {
|
|
|
1469
1881
|
}
|
|
1470
1882
|
}
|
|
1471
1883
|
|
|
1472
|
-
// src/driver-core/
|
|
1473
|
-
var
|
|
1474
|
-
/**
|
|
1884
|
+
// src/driver-core/session-lifetime.ts
|
|
1885
|
+
var SessionLifetime = class {
|
|
1886
|
+
/** The live session these resources belong to. */
|
|
1887
|
+
session;
|
|
1888
|
+
/** The write handle this lifetime owns, or `undefined` when no persistence backend is mounted. */
|
|
1889
|
+
handle;
|
|
1890
|
+
/** The store's detacher; `undefined` until the entry is bound by the transaction that entered the session. */
|
|
1891
|
+
detach;
|
|
1892
|
+
/**
|
|
1893
|
+
* @param session - the live session whose entry and write handle these are.
|
|
1894
|
+
* @param handle - the write handle the transaction acquired, if a backend is mounted.
|
|
1895
|
+
*/
|
|
1896
|
+
constructor(session, handle) {
|
|
1897
|
+
this.session = session;
|
|
1898
|
+
this.handle = handle;
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Whether the session's store entry is already held: true for a lifetime a
|
|
1902
|
+
* retiring machine handed over, false for one whose transaction still has to
|
|
1903
|
+
* enter the session it prepared.
|
|
1904
|
+
*/
|
|
1905
|
+
get entered() {
|
|
1906
|
+
return this.detach !== void 0;
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Bind the store entry this lifetime owns. Called once, by the transaction
|
|
1910
|
+
* that enters the session; a joined session's entry is already bound.
|
|
1911
|
+
* @param detach - the detacher the session's own `enter` returned.
|
|
1912
|
+
*/
|
|
1913
|
+
bind(detach) {
|
|
1914
|
+
this.detach = detach;
|
|
1915
|
+
}
|
|
1916
|
+
/** Drain and close the write handle — the durability barrier that precedes leaving the store. */
|
|
1917
|
+
async closeHandle() {
|
|
1918
|
+
await this.handle?.close();
|
|
1919
|
+
}
|
|
1920
|
+
/** Remove the session from the store, publishing its paired disposal exactly once. */
|
|
1921
|
+
leaveStore() {
|
|
1922
|
+
this.detach?.();
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1925
|
+
|
|
1926
|
+
// src/driver-core/hosted-engine-runtime.ts
|
|
1927
|
+
var HostedEngineRuntime = class {
|
|
1928
|
+
/** Validated configuration owned by this engine instance. */
|
|
1475
1929
|
config;
|
|
1476
|
-
|
|
1930
|
+
/** Effect-label prefix; also identifies the engine in diagnostics. */
|
|
1931
|
+
label;
|
|
1477
1932
|
/** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */
|
|
1478
1933
|
runtime;
|
|
1479
|
-
|
|
1480
|
-
|
|
1934
|
+
ownership;
|
|
1935
|
+
/**
|
|
1936
|
+
* @param ctx - the owning context; its fiber's unload tears every live agent down.
|
|
1937
|
+
* @param label - effect-label prefix, e.g. `agentLoopKimi`.
|
|
1938
|
+
* @param config - already-resolved engine configuration.
|
|
1939
|
+
*/
|
|
1481
1940
|
constructor(ctx, label, config) {
|
|
1482
|
-
super(ctx, label);
|
|
1483
1941
|
this.label = label;
|
|
1484
1942
|
this.config = config;
|
|
1485
1943
|
this.ownership = new FactoryOwnership(ctx.fiber);
|
|
1486
1944
|
this.runtime = { ctx };
|
|
1945
|
+
ctx.reflect.provide(label, this);
|
|
1487
1946
|
ctx.effect(() => () => this.ownership.dispose(), `${label}.transactions()`);
|
|
1488
|
-
ctx.effect(() => ctx.agents.setFactory(this), `${label}.setFactory()`);
|
|
1489
|
-
ctx.systemPrompt.variable("provider", (context) => context.agent?.options.provider);
|
|
1490
|
-
ctx.systemPrompt.variable("model", (context) => context.agent?.options.model);
|
|
1491
|
-
ctx.systemPrompt.variable("cwd", (context) => context.agent?.session.header.cwd);
|
|
1492
1947
|
}
|
|
1493
1948
|
/**
|
|
1494
1949
|
* Construct the driver, scope, and one memoized reverse teardown for a new
|
|
1495
1950
|
* agent. The teardown is registered with the factory and the owner fiber
|
|
1496
1951
|
* BEFORE publication, so a mid-setup unload rolls everything back; `signal`
|
|
1497
1952
|
* fuses caller cancellation with lifecycle teardown for setup awaits.
|
|
1953
|
+
*
|
|
1954
|
+
* `lifetime` carries the session's store entry and write handle rather than
|
|
1955
|
+
* this body owning them: a fresh transaction binds them when it publishes,
|
|
1956
|
+
* while a swap publishes a session whose entry another machine already bound
|
|
1957
|
+
* and whose write handle another machine already opened.
|
|
1498
1958
|
*/
|
|
1499
|
-
/* jscpd:ignore-start -- ownership/transaction machinery mirrors the default agent-loop factory
|
|
1500
|
-
prepare(ownerCtx, id, options, session, callerSignal,
|
|
1959
|
+
/* jscpd:ignore-start -- ownership/transaction machinery mirrors the default agent-loop factory: the harness exports the loop as a plugin, not as reusable transaction helpers, so the body is replicated rather than delegated to. */
|
|
1960
|
+
prepare(ownerCtx, id, options, session, callerSignal, lifetime, parentAgent) {
|
|
1501
1961
|
ownerCtx.fiber.assertActive();
|
|
1502
1962
|
if (!this.ownership.isActive()) throw new Error("agent loop is not active");
|
|
1503
1963
|
if (callerSignal?.aborted) {
|
|
@@ -1514,9 +1974,9 @@ var HostedLoopFactory = class extends Service {
|
|
|
1514
1974
|
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
1515
1975
|
this.ownership.signal.addEventListener("abort", onFactoryTeardown, { once: true });
|
|
1516
1976
|
let machine;
|
|
1517
|
-
let detachSession;
|
|
1518
1977
|
let detachAgent;
|
|
1519
1978
|
let disposing;
|
|
1979
|
+
let handedOver = false;
|
|
1520
1980
|
const machineReady = Promise.withResolvers();
|
|
1521
1981
|
const dispose = (ownerTriggered = false) => disposing ??= (async () => {
|
|
1522
1982
|
abort.abort(new Error(`agent "${id}" lifecycle disposed`));
|
|
@@ -1531,14 +1991,17 @@ var HostedLoopFactory = class extends Service {
|
|
|
1531
1991
|
}
|
|
1532
1992
|
} finally {
|
|
1533
1993
|
try {
|
|
1534
|
-
await
|
|
1994
|
+
if (!handedOver) await lifetime.closeHandle();
|
|
1535
1995
|
} finally {
|
|
1536
1996
|
try {
|
|
1537
1997
|
detachAgent?.();
|
|
1538
|
-
detachSession?.();
|
|
1539
1998
|
} finally {
|
|
1540
|
-
|
|
1541
|
-
|
|
1999
|
+
try {
|
|
2000
|
+
if (!handedOver) lifetime.leaveStore();
|
|
2001
|
+
} finally {
|
|
2002
|
+
untrack();
|
|
2003
|
+
if (!ownerTriggered) await unfollowOwner();
|
|
2004
|
+
}
|
|
1542
2005
|
}
|
|
1543
2006
|
}
|
|
1544
2007
|
}
|
|
@@ -1561,6 +2024,10 @@ var HostedLoopFactory = class extends Service {
|
|
|
1561
2024
|
if (!abort.signal.aborted) return;
|
|
1562
2025
|
throw abort.signal.reason instanceof Error ? abort.signal.reason : new Error(String(abort.signal.reason));
|
|
1563
2026
|
};
|
|
2027
|
+
const retire = async () => {
|
|
2028
|
+
handedOver = true;
|
|
2029
|
+
await dispose();
|
|
2030
|
+
};
|
|
1564
2031
|
try {
|
|
1565
2032
|
const agent = machine = this.buildAgent(loopCtx, id, options, session);
|
|
1566
2033
|
machineReady.resolve();
|
|
@@ -1570,15 +2037,16 @@ var HostedLoopFactory = class extends Service {
|
|
|
1570
2037
|
signal: abort.signal,
|
|
1571
2038
|
publish: (source) => {
|
|
1572
2039
|
assertLive();
|
|
1573
|
-
|
|
2040
|
+
const joining = lifetime.entered;
|
|
2041
|
+
if (!joining) lifetime.bind(agent.ctx.sessions.enter(session));
|
|
1574
2042
|
detachAgent = loopCtx.agents.enter(agent, parentAgent);
|
|
1575
|
-
agent.ctx.sessions.announce(session);
|
|
2043
|
+
if (!joining) agent.ctx.sessions.announce(session);
|
|
1576
2044
|
assertLive();
|
|
1577
2045
|
loopCtx.agents.announce(agent);
|
|
1578
2046
|
assertLive();
|
|
1579
2047
|
emitAgentEvent(loopCtx, agent, "agent/session-start", { source });
|
|
1580
2048
|
assertLive();
|
|
1581
|
-
return { agent, dispose };
|
|
2049
|
+
return { agent, dispose, retire, lifetime };
|
|
1582
2050
|
},
|
|
1583
2051
|
dispose
|
|
1584
2052
|
};
|
|
@@ -1589,33 +2057,24 @@ var HostedLoopFactory = class extends Service {
|
|
|
1589
2057
|
}
|
|
1590
2058
|
}
|
|
1591
2059
|
/** Prepare one Agent around an acquired Session, run setup, and publish it. */
|
|
1592
|
-
async setupAndPublish(ownerCtx, id,
|
|
1593
|
-
|
|
2060
|
+
async setupAndPublish(ownerCtx, id, session, agentOptions, setup, signal, source, lifetime, parentAgent, stored) {
|
|
2061
|
+
let prepared;
|
|
1594
2062
|
try {
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
} catch (error) {
|
|
1611
|
-
await prepared.dispose().catch(() => {
|
|
1612
|
-
});
|
|
1613
|
-
throw error;
|
|
1614
|
-
}
|
|
1615
|
-
} catch (_) {
|
|
1616
|
-
var _error = _, _hasError = true;
|
|
1617
|
-
} finally {
|
|
1618
|
-
__callDispose(_stack, _error, _hasError);
|
|
2063
|
+
prepared = this.prepare(ownerCtx, id, agentOptions, session, signal, lifetime, parentAgent);
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
await stored?.handle.close().catch(() => {
|
|
2066
|
+
});
|
|
2067
|
+
throw error;
|
|
2068
|
+
}
|
|
2069
|
+
try {
|
|
2070
|
+
const setupCommit = await raceAbort(setup?.(prepared.agent.ctx, prepared.agent), prepared.signal, id);
|
|
2071
|
+
setupCommit?.commit();
|
|
2072
|
+
await this.appendUnstoredSuffix(stored, session);
|
|
2073
|
+
return prepared.publish(source);
|
|
2074
|
+
} catch (error) {
|
|
2075
|
+
await prepared.dispose().catch(() => {
|
|
2076
|
+
});
|
|
2077
|
+
throw error;
|
|
1619
2078
|
}
|
|
1620
2079
|
}
|
|
1621
2080
|
/**
|
|
@@ -1644,21 +2103,21 @@ var HostedLoopFactory = class extends Service {
|
|
|
1644
2103
|
});
|
|
1645
2104
|
}
|
|
1646
2105
|
);
|
|
1647
|
-
|
|
2106
|
+
return await this.setupAndPublish(
|
|
2107
|
+
ownerCtx,
|
|
2108
|
+
options.sessionId,
|
|
2109
|
+
preparation.session,
|
|
2110
|
+
options.agentOptions ?? {},
|
|
2111
|
+
options.setup,
|
|
2112
|
+
options.signal,
|
|
2113
|
+
"startup",
|
|
2114
|
+
new SessionLifetime(preparation.session, stored?.handle),
|
|
2115
|
+
options.parentAgent,
|
|
2116
|
+
stored
|
|
2117
|
+
);
|
|
2118
|
+
} finally {
|
|
1648
2119
|
preparation[Symbol.dispose]();
|
|
1649
|
-
throw error;
|
|
1650
2120
|
}
|
|
1651
|
-
return this.setupAndPublish(
|
|
1652
|
-
ownerCtx,
|
|
1653
|
-
options.sessionId,
|
|
1654
|
-
preparation,
|
|
1655
|
-
options.agentOptions ?? {},
|
|
1656
|
-
options.setup,
|
|
1657
|
-
options.signal,
|
|
1658
|
-
"startup",
|
|
1659
|
-
stored,
|
|
1660
|
-
options.parentAgent
|
|
1661
|
-
);
|
|
1662
2121
|
})();
|
|
1663
2122
|
this.ownership.trackWrapper(published);
|
|
1664
2123
|
return published;
|
|
@@ -1710,6 +2169,37 @@ var HostedLoopFactory = class extends Service {
|
|
|
1710
2169
|
}
|
|
1711
2170
|
return this.resumeWith(ownerCtx, persistence, options);
|
|
1712
2171
|
}
|
|
2172
|
+
/**
|
|
2173
|
+
* Build this engine's agent onto a LIVE session another engine's agent left,
|
|
2174
|
+
* taking the session's lifetime over.
|
|
2175
|
+
*
|
|
2176
|
+
* The counterpart of {@link HostedAgentHandle.retire}: together they are an
|
|
2177
|
+
* in-place engine swap, and the harness's own factory contract has neither.
|
|
2178
|
+
* Nothing is read from persistence and nothing is created in the store — the
|
|
2179
|
+
* successor drives the Session object that is already live and already
|
|
2180
|
+
* entered, so a browser half attached to that session sees no lifecycle edge
|
|
2181
|
+
* at all. The lifetime carries the still-open write handle, so the session
|
|
2182
|
+
* keeps being stored through the same channel it was already using.
|
|
2183
|
+
* @param ownerCtx - caller context that structurally owns the lifecycle.
|
|
2184
|
+
* @param options - the live session's lifetime, loop options, setup, and parent.
|
|
2185
|
+
* @returns the published handle, which owns the session from here on.
|
|
2186
|
+
*/
|
|
2187
|
+
async swap(ownerCtx, options) {
|
|
2188
|
+
const session = options.lifetime.session;
|
|
2189
|
+
const published = (async () => await this.setupAndPublish(
|
|
2190
|
+
ownerCtx,
|
|
2191
|
+
session.id,
|
|
2192
|
+
session,
|
|
2193
|
+
options.agentOptions ?? {},
|
|
2194
|
+
options.setup,
|
|
2195
|
+
void 0,
|
|
2196
|
+
"resume",
|
|
2197
|
+
options.lifetime,
|
|
2198
|
+
options.parentAgent
|
|
2199
|
+
))();
|
|
2200
|
+
this.ownership.trackWrapper(published);
|
|
2201
|
+
return published;
|
|
2202
|
+
}
|
|
1713
2203
|
/** Resume through an explicit persistence service. */
|
|
1714
2204
|
resumeWith(ownerCtx, persistence, options) {
|
|
1715
2205
|
const id = options.resumeSessionId;
|
|
@@ -1759,13 +2249,14 @@ var HostedLoopFactory = class extends Service {
|
|
|
1759
2249
|
return await this.setupAndPublish(
|
|
1760
2250
|
ownerCtx,
|
|
1761
2251
|
id,
|
|
1762
|
-
preparation,
|
|
2252
|
+
preparation.session,
|
|
1763
2253
|
options.agentOptions ?? {},
|
|
1764
2254
|
options.setup,
|
|
1765
2255
|
options.signal,
|
|
1766
2256
|
"resume",
|
|
1767
|
-
owned,
|
|
1768
|
-
options.parentAgent
|
|
2257
|
+
new SessionLifetime(preparation.session, owned?.handle),
|
|
2258
|
+
options.parentAgent,
|
|
2259
|
+
owned
|
|
1769
2260
|
);
|
|
1770
2261
|
} finally {
|
|
1771
2262
|
preparation?.[Symbol.dispose]();
|
|
@@ -1811,15 +2302,21 @@ function resolveConfig(config) {
|
|
|
1811
2302
|
maxTurns: config.maxTurns
|
|
1812
2303
|
};
|
|
1813
2304
|
}
|
|
1814
|
-
var
|
|
1815
|
-
|
|
1816
|
-
|
|
2305
|
+
var CLAUDE_CODE_ENGINE_LABEL = "agentLoopClaudeCode";
|
|
2306
|
+
var ClaudeCodeLoop = class extends HostedEngineRuntime {
|
|
2307
|
+
/** Process-spawn capability handed to every agent, sandboxed by the subprocess seam. */
|
|
2308
|
+
spawn;
|
|
1817
2309
|
constructor(ctx, config) {
|
|
1818
|
-
super(ctx,
|
|
2310
|
+
super(ctx, CLAUDE_CODE_ENGINE_LABEL, resolveConfig(config));
|
|
2311
|
+
const subprocess = ctx.get("subprocess");
|
|
2312
|
+
if (subprocess === void 0) {
|
|
2313
|
+
throw new Error("loop-engine: the claude-code engine needs the dsh subprocess service on this context");
|
|
2314
|
+
}
|
|
2315
|
+
this.spawn = (spec) => subprocess.spawn(spec);
|
|
1819
2316
|
}
|
|
1820
2317
|
/** Construct the Claude Code driver for one prepared session. */
|
|
1821
2318
|
buildAgent(loopCtx, id, options, session) {
|
|
1822
|
-
return new ClaudeCodeAgent(loopCtx, id, options, session, this.config);
|
|
2319
|
+
return new ClaudeCodeAgent(loopCtx, id, options, session, this.config, this.spawn);
|
|
1823
2320
|
}
|
|
1824
2321
|
};
|
|
1825
2322
|
|
|
@@ -1828,10 +2325,38 @@ import z2 from "@deepseek-ai/schemastery";
|
|
|
1828
2325
|
|
|
1829
2326
|
// src/engine-codex/agent.ts
|
|
1830
2327
|
import { agentEvents as agentEvents2 } from "@deepseek-ai/dsh-agent";
|
|
1831
|
-
import { LlmError as
|
|
2328
|
+
import { LlmError as LlmError3, createAssistantMessage as createAssistantMessage2, createUserMessage as createUserMessage2, errorChain as errorChain2 } from "@deepseek-ai/dsh-llm";
|
|
1832
2329
|
import { createScope as createScope2 } from "@deepseek-ai/dsh-scope";
|
|
1833
2330
|
import { canonicalHeader as canonicalHeader2 } from "@deepseek-ai/dsh-session";
|
|
1834
2331
|
|
|
2332
|
+
// src/engine-codex/model-handover.ts
|
|
2333
|
+
var CODEX_DSH_PROVIDER = "dsh";
|
|
2334
|
+
var CODEX_DSH_API_KEY_ENV = "DSH_LOOP_ENGINE_API_KEY";
|
|
2335
|
+
var CODEX_WIRE_APIS = {
|
|
2336
|
+
"openai-responses": "responses",
|
|
2337
|
+
"openai-completions": "chat"
|
|
2338
|
+
};
|
|
2339
|
+
function tomlString(value) {
|
|
2340
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
2341
|
+
}
|
|
2342
|
+
function codexModelConfig(handover) {
|
|
2343
|
+
const wire = CODEX_WIRE_APIS[handover.api];
|
|
2344
|
+
const profile = [
|
|
2345
|
+
`base_url=${tomlString(handover.baseURL)}`,
|
|
2346
|
+
...wire === void 0 ? [] : [`wire_api=${tomlString(wire)}`],
|
|
2347
|
+
`env_key=${tomlString(CODEX_DSH_API_KEY_ENV)}`
|
|
2348
|
+
].join(",");
|
|
2349
|
+
return {
|
|
2350
|
+
argv: [
|
|
2351
|
+
"-c",
|
|
2352
|
+
`model_provider=${tomlString(CODEX_DSH_PROVIDER)}`,
|
|
2353
|
+
"-c",
|
|
2354
|
+
`model_providers.${CODEX_DSH_PROVIDER}={${profile}}`
|
|
2355
|
+
],
|
|
2356
|
+
env: { [CODEX_DSH_API_KEY_ENV]: handover.apiKey }
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
|
|
1835
2360
|
// src/engine-codex/permission.ts
|
|
1836
2361
|
var DEFAULT_CODEX_PERMISSION = {
|
|
1837
2362
|
sandboxMode: "read-only",
|
|
@@ -1987,10 +2512,24 @@ var AppServerClient = class _AppServerClient {
|
|
|
1987
2512
|
this.pending.clear();
|
|
1988
2513
|
});
|
|
1989
2514
|
}
|
|
1990
|
-
/**
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
2515
|
+
/**
|
|
2516
|
+
* Spawn the pinned app-server dependency and initialize the client.
|
|
2517
|
+
*
|
|
2518
|
+
* `argv` appends the driver's own overrides to the bare `app-server`
|
|
2519
|
+
* subcommand (codex's `-c key=value` configuration), and `env` layers the
|
|
2520
|
+
* driver's explicit entries over the ambient environment — the child still
|
|
2521
|
+
* needs `PATH` and, under a user's own codex setup, the ambient auth facts its
|
|
2522
|
+
* configuration reads, so this is an overlay, not a replacement. Both default
|
|
2523
|
+
* to nothing, which is the bare `codex app-server` the driver used before a
|
|
2524
|
+
* dsh endpoint was ever handed over.
|
|
2525
|
+
* @param argv - extra arguments after the `app-server` subcommand.
|
|
2526
|
+
* @param env - explicit environment entries layered over the ambient one.
|
|
2527
|
+
* @returns the initialized client.
|
|
2528
|
+
*/
|
|
2529
|
+
static async create(argv = [], env = {}) {
|
|
2530
|
+
const proc = spawn(process.execPath, [codexCliEntrypoint(), "app-server", ...argv], {
|
|
2531
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2532
|
+
env: { ...process.env, ...env }
|
|
1994
2533
|
});
|
|
1995
2534
|
const client = new _AppServerClient(proc);
|
|
1996
2535
|
await client.initialize();
|
|
@@ -2302,8 +2841,7 @@ function mapMcpToolCall(item) {
|
|
|
2302
2841
|
}
|
|
2303
2842
|
|
|
2304
2843
|
// src/engine-codex/agent.ts
|
|
2305
|
-
var PROVIDER2 =
|
|
2306
|
-
var NATIVE_MODEL_LABEL2 = "codex-native";
|
|
2844
|
+
var PROVIDER2 = HOSTED_ROUTE_LABEL;
|
|
2307
2845
|
function nonEmptyText(parts) {
|
|
2308
2846
|
return parts !== void 0 && parts.some((part) => part.length > 0) ? parts.join("\n") : void 0;
|
|
2309
2847
|
}
|
|
@@ -2377,10 +2915,30 @@ var CodexAgent = class {
|
|
|
2377
2915
|
}
|
|
2378
2916
|
/** Lazily created app-server client, reused across steps and released on scope teardown. */
|
|
2379
2917
|
appServer;
|
|
2380
|
-
/**
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2918
|
+
/**
|
|
2919
|
+
* The spawn configuration (`argv` + `env`) the cached client was built from.
|
|
2920
|
+
* Codex's endpoint lives in the child's own command line, so a handover change
|
|
2921
|
+
* must respawn the child rather than reuse one configured for another endpoint.
|
|
2922
|
+
*/
|
|
2923
|
+
appServerConfig;
|
|
2924
|
+
/**
|
|
2925
|
+
* Return the cached app-server client, spawning one on first use, after a dead
|
|
2926
|
+
* process, or when the resolved dsh endpoint changed since the last spawn.
|
|
2927
|
+
*
|
|
2928
|
+
* A handover is codex's own `-c` configuration for a dsh provider, so a
|
|
2929
|
+
* different endpoint (different provider, base URL, protocol, or credential)
|
|
2930
|
+
* needs a different child — the config is fixed when the process starts.
|
|
2931
|
+
* @param handover - the session's resolved dsh endpoint, or undefined to leave codex to its own configuration.
|
|
2932
|
+
*/
|
|
2933
|
+
async appServerClient(handover) {
|
|
2934
|
+
const modeled = handover === void 0 ? { argv: [], env: {} } : codexModelConfig(handover);
|
|
2935
|
+
const config = JSON.stringify(modeled);
|
|
2936
|
+
if (this.appServer !== void 0 && !this.appServer.closed && this.appServerConfig === config) {
|
|
2937
|
+
return this.appServer;
|
|
2938
|
+
}
|
|
2939
|
+
this.appServer?.dispose();
|
|
2940
|
+
this.appServer = await AppServerClient.create(modeled.argv, { ...this.config.env, ...modeled.env });
|
|
2941
|
+
this.appServerConfig = config;
|
|
2384
2942
|
this.appServer.onRequest((method, params) => this.answerRequest(method, params));
|
|
2385
2943
|
return this.appServer;
|
|
2386
2944
|
}
|
|
@@ -2694,7 +3252,7 @@ var CodexAgent = class {
|
|
|
2694
3252
|
}
|
|
2695
3253
|
turnEnds = {
|
|
2696
3254
|
kind: "error",
|
|
2697
|
-
error: error instanceof
|
|
3255
|
+
error: error instanceof LlmError3 ? error.failure : { message: errorChain2(error), code: "UNKNOWN" }
|
|
2698
3256
|
};
|
|
2699
3257
|
this.throwError(error);
|
|
2700
3258
|
} finally {
|
|
@@ -2712,7 +3270,7 @@ var CodexAgent = class {
|
|
|
2712
3270
|
}
|
|
2713
3271
|
/** Model label recorded in the request header for one lifecycle. */
|
|
2714
3272
|
modelLabel() {
|
|
2715
|
-
return this.config.model ??
|
|
3273
|
+
return this.config.model ?? HOSTED_DEFAULT_MODEL;
|
|
2716
3274
|
}
|
|
2717
3275
|
/** Append the request header snapshot once per loop instance. */
|
|
2718
3276
|
assertRequestHeader() {
|
|
@@ -2755,12 +3313,15 @@ var CodexAgent = class {
|
|
|
2755
3313
|
let live;
|
|
2756
3314
|
try {
|
|
2757
3315
|
const permission = this.queryPermission();
|
|
2758
|
-
const
|
|
3316
|
+
const override = sessionModelOverrideOf(this.loopCtx, this.session);
|
|
3317
|
+
const handover = await resolveModelHandover(this.loopCtx, override);
|
|
3318
|
+
const client = await this.appServerClient(handover);
|
|
3319
|
+
const model = override?.model ?? this.config.model;
|
|
2759
3320
|
const threadParams = {
|
|
2760
3321
|
cwd,
|
|
2761
3322
|
sandbox: permission.sandboxMode,
|
|
2762
3323
|
approvalPolicy: permission.approvalPolicy,
|
|
2763
|
-
...
|
|
3324
|
+
...model === void 0 ? {} : { model }
|
|
2764
3325
|
};
|
|
2765
3326
|
const thread = await AppServerThread.create(client, threadParams);
|
|
2766
3327
|
const input = [{ type: "text", text: prompt }];
|
|
@@ -2768,7 +3329,7 @@ var CodexAgent = class {
|
|
|
2768
3329
|
signal: controller.signal,
|
|
2769
3330
|
params: {
|
|
2770
3331
|
approvalPolicy: permission.approvalPolicy,
|
|
2771
|
-
...
|
|
3332
|
+
...model === void 0 ? {} : { model }
|
|
2772
3333
|
}
|
|
2773
3334
|
});
|
|
2774
3335
|
let finished = false;
|
|
@@ -2899,12 +3460,13 @@ var CodexAgent = class {
|
|
|
2899
3460
|
const activity = mapCommandExecution(item);
|
|
2900
3461
|
foldToolCall(activity.call);
|
|
2901
3462
|
flushHeld();
|
|
3463
|
+
const normalizedCall = normalizeHostedToolCall("codex", activity.call.name, activity.call.arguments);
|
|
2902
3464
|
this.session.append("tool/call", {
|
|
2903
3465
|
turn: phase.turn,
|
|
2904
3466
|
step: phase.step,
|
|
2905
3467
|
callId: activity.call.callId,
|
|
2906
|
-
name:
|
|
2907
|
-
arguments:
|
|
3468
|
+
name: normalizedCall.name,
|
|
3469
|
+
arguments: normalizedCall.arguments
|
|
2908
3470
|
});
|
|
2909
3471
|
this.session.append("tool/result", { turn: phase.turn, step: phase.step, message: activity.result }, { surfaceOp: "append" });
|
|
2910
3472
|
this.stepSettledTools += 1;
|
|
@@ -2913,12 +3475,13 @@ var CodexAgent = class {
|
|
|
2913
3475
|
const activity = mapFileChange(item);
|
|
2914
3476
|
foldToolCall(activity.call);
|
|
2915
3477
|
flushHeld();
|
|
3478
|
+
const normalizedCall = normalizeHostedToolCall("codex", activity.call.name, activity.call.arguments);
|
|
2916
3479
|
this.session.append("tool/call", {
|
|
2917
3480
|
turn: phase.turn,
|
|
2918
3481
|
step: phase.step,
|
|
2919
3482
|
callId: activity.call.callId,
|
|
2920
|
-
name:
|
|
2921
|
-
arguments:
|
|
3483
|
+
name: normalizedCall.name,
|
|
3484
|
+
arguments: normalizedCall.arguments
|
|
2922
3485
|
});
|
|
2923
3486
|
this.session.append("tool/result", { turn: phase.turn, step: phase.step, message: activity.result }, { surfaceOp: "append" });
|
|
2924
3487
|
this.stepSettledTools += 1;
|
|
@@ -2927,12 +3490,13 @@ var CodexAgent = class {
|
|
|
2927
3490
|
const activity = mapMcpToolCall(item);
|
|
2928
3491
|
foldToolCall(activity.call);
|
|
2929
3492
|
flushHeld();
|
|
3493
|
+
const normalizedCall = normalizeHostedToolCall("codex", activity.call.name, activity.call.arguments);
|
|
2930
3494
|
this.session.append("tool/call", {
|
|
2931
3495
|
turn: phase.turn,
|
|
2932
3496
|
step: phase.step,
|
|
2933
3497
|
callId: activity.call.callId,
|
|
2934
|
-
name:
|
|
2935
|
-
arguments:
|
|
3498
|
+
name: normalizedCall.name,
|
|
3499
|
+
arguments: normalizedCall.arguments
|
|
2936
3500
|
});
|
|
2937
3501
|
this.session.append("tool/result", { turn: phase.turn, step: phase.step, message: activity.result }, { surfaceOp: "append" });
|
|
2938
3502
|
this.stepSettledTools += 1;
|
|
@@ -2947,7 +3511,7 @@ var CodexAgent = class {
|
|
|
2947
3511
|
}
|
|
2948
3512
|
case "error":
|
|
2949
3513
|
flushHeld();
|
|
2950
|
-
throw new
|
|
3514
|
+
throw new LlmError3(event.error.message, "CODEX_ERROR");
|
|
2951
3515
|
/* v8 ignore next -- AppServerEvent is a closed union; no unknown kinds */
|
|
2952
3516
|
default:
|
|
2953
3517
|
break;
|
|
@@ -2955,7 +3519,7 @@ var CodexAgent = class {
|
|
|
2955
3519
|
}
|
|
2956
3520
|
flushHeld();
|
|
2957
3521
|
if (!finished) {
|
|
2958
|
-
throw new
|
|
3522
|
+
throw new LlmError3(
|
|
2959
3523
|
`agent "${this.id}": codex query ended without a completed turn`,
|
|
2960
3524
|
"CODEX_NO_RESULT"
|
|
2961
3525
|
);
|
|
@@ -2995,11 +3559,10 @@ function resolveConfig2(config) {
|
|
|
2995
3559
|
model: config.model
|
|
2996
3560
|
};
|
|
2997
3561
|
}
|
|
2998
|
-
var
|
|
2999
|
-
|
|
3000
|
-
static inject = ["agents", "sessions", "systemPrompt"];
|
|
3562
|
+
var CODEX_ENGINE_LABEL = "agentLoopCodex";
|
|
3563
|
+
var CodexLoop = class extends HostedEngineRuntime {
|
|
3001
3564
|
constructor(ctx, config) {
|
|
3002
|
-
super(ctx,
|
|
3565
|
+
super(ctx, CODEX_ENGINE_LABEL, resolveConfig2(config));
|
|
3003
3566
|
}
|
|
3004
3567
|
/** Construct the Codex driver for one prepared session. */
|
|
3005
3568
|
buildAgent(loopCtx, id, options, session) {
|
|
@@ -3009,16 +3572,68 @@ var CodexLoop = class extends HostedLoopFactory {
|
|
|
3009
3572
|
|
|
3010
3573
|
// src/engine-pi/loop.ts
|
|
3011
3574
|
import { readFileSync } from "node:fs";
|
|
3012
|
-
import { dirname as dirname2, join as
|
|
3575
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
3013
3576
|
import { fileURLToPath } from "node:url";
|
|
3014
3577
|
import z3 from "@deepseek-ai/schemastery";
|
|
3015
3578
|
|
|
3016
3579
|
// src/engine-pi/agent.ts
|
|
3017
3580
|
import { agentEvents as agentEvents3 } from "@deepseek-ai/dsh-agent";
|
|
3018
|
-
import { ToolCallId as ToolCallId5, LlmError as
|
|
3581
|
+
import { ToolCallId as ToolCallId5, LlmError as LlmError4, createAssistantMessage as createAssistantMessage3, createUserMessage as createUserMessage3, errorChain as errorChain3 } from "@deepseek-ai/dsh-llm";
|
|
3019
3582
|
import { createScope as createScope3 } from "@deepseek-ai/dsh-scope";
|
|
3020
3583
|
import { canonicalHeader as canonicalHeader3 } from "@deepseek-ai/dsh-session";
|
|
3021
3584
|
|
|
3585
|
+
// src/engine-pi/model-handover.ts
|
|
3586
|
+
import { createHash } from "node:crypto";
|
|
3587
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3588
|
+
import { tmpdir } from "node:os";
|
|
3589
|
+
import { join as join2 } from "node:path";
|
|
3590
|
+
var AGENT_ROOT = join2(tmpdir(), "dsh-loop-engine-pi-agent");
|
|
3591
|
+
var materialized = /* @__PURE__ */ new Set();
|
|
3592
|
+
process.once("exit", () => {
|
|
3593
|
+
for (const dir of materialized) {
|
|
3594
|
+
try {
|
|
3595
|
+
rmSync(dir, { recursive: true, force: true });
|
|
3596
|
+
} catch {
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
materialized.clear();
|
|
3600
|
+
});
|
|
3601
|
+
function piAgentDir(handover) {
|
|
3602
|
+
const dir = join2(
|
|
3603
|
+
AGENT_ROOT,
|
|
3604
|
+
createHash("sha256").update(JSON.stringify({
|
|
3605
|
+
provider: handover.provider,
|
|
3606
|
+
baseURL: handover.baseURL,
|
|
3607
|
+
api: handover.api,
|
|
3608
|
+
apiKey: handover.apiKey,
|
|
3609
|
+
model: handover.model
|
|
3610
|
+
})).digest("hex").slice(0, 32)
|
|
3611
|
+
);
|
|
3612
|
+
if (!materialized.has(dir)) {
|
|
3613
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
3614
|
+
writeFileSync(
|
|
3615
|
+
join2(dir, "models.json"),
|
|
3616
|
+
`${JSON.stringify(modelsDocument(handover), null, 2)}
|
|
3617
|
+
`,
|
|
3618
|
+
{ mode: 384 }
|
|
3619
|
+
);
|
|
3620
|
+
materialized.add(dir);
|
|
3621
|
+
}
|
|
3622
|
+
return dir;
|
|
3623
|
+
}
|
|
3624
|
+
function modelsDocument(handover) {
|
|
3625
|
+
return {
|
|
3626
|
+
providers: {
|
|
3627
|
+
[handover.provider]: {
|
|
3628
|
+
baseUrl: handover.baseURL,
|
|
3629
|
+
api: handover.api,
|
|
3630
|
+
apiKey: handover.apiKey,
|
|
3631
|
+
models: [{ id: handover.model, name: handover.model }]
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
};
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3022
3637
|
// src/engine-pi/permission.ts
|
|
3023
3638
|
var DEFAULT_PI_PERMISSION = {
|
|
3024
3639
|
sandboxMode: "read-only",
|
|
@@ -3273,11 +3888,10 @@ function mapToolResult(ev) {
|
|
|
3273
3888
|
}
|
|
3274
3889
|
|
|
3275
3890
|
// src/engine-pi/agent.ts
|
|
3276
|
-
var PROVIDER3 =
|
|
3277
|
-
var NATIVE_MODEL_LABEL3 = "pi-native";
|
|
3891
|
+
var PROVIDER3 = HOSTED_ROUTE_LABEL;
|
|
3278
3892
|
var TOOLS_FLAG = "--tools";
|
|
3279
3893
|
var PiAgent = class {
|
|
3280
|
-
constructor(loopCtx, id, options, session, config, spawn4, bin
|
|
3894
|
+
constructor(loopCtx, id, options, session, config, spawn4, bin) {
|
|
3281
3895
|
this.loopCtx = loopCtx;
|
|
3282
3896
|
this.id = id;
|
|
3283
3897
|
this.options = options;
|
|
@@ -3285,7 +3899,6 @@ var PiAgent = class {
|
|
|
3285
3899
|
this.config = config;
|
|
3286
3900
|
this.spawn = spawn4;
|
|
3287
3901
|
this.bin = bin;
|
|
3288
|
-
this.catalog = catalog;
|
|
3289
3902
|
this.dispatch = agentEvents3(loopCtx, this);
|
|
3290
3903
|
this.inbox = new DriverInbox(session, {
|
|
3291
3904
|
inserted: (message) => {
|
|
@@ -3314,7 +3927,6 @@ var PiAgent = class {
|
|
|
3314
3927
|
config;
|
|
3315
3928
|
spawn;
|
|
3316
3929
|
bin;
|
|
3317
|
-
catalog;
|
|
3318
3930
|
inbox;
|
|
3319
3931
|
phase;
|
|
3320
3932
|
activityDone = Promise.resolve();
|
|
@@ -3357,8 +3969,8 @@ var PiAgent = class {
|
|
|
3357
3969
|
* never reuses one: the step teardown disposes it and the next step respawns
|
|
3358
3970
|
* a fresh child.
|
|
3359
3971
|
*/
|
|
3360
|
-
rpcClient(cwd) {
|
|
3361
|
-
const client = PiRpcClient.create(this.spawnSpec(cwd), this.spawn);
|
|
3972
|
+
rpcClient(cwd, handover) {
|
|
3973
|
+
const client = PiRpcClient.create(this.spawnSpec(cwd, handover), this.spawn);
|
|
3362
3974
|
this.rpc = client;
|
|
3363
3975
|
return client;
|
|
3364
3976
|
}
|
|
@@ -3608,7 +4220,7 @@ var PiAgent = class {
|
|
|
3608
4220
|
}
|
|
3609
4221
|
turnEnds = {
|
|
3610
4222
|
kind: "error",
|
|
3611
|
-
error: error instanceof
|
|
4223
|
+
error: error instanceof LlmError4 ? error.failure : { message: errorChain3(error), code: "UNKNOWN" }
|
|
3612
4224
|
};
|
|
3613
4225
|
this.throwError(error);
|
|
3614
4226
|
} finally {
|
|
@@ -3626,7 +4238,7 @@ var PiAgent = class {
|
|
|
3626
4238
|
}
|
|
3627
4239
|
/** Model label recorded in the request header for one lifecycle. */
|
|
3628
4240
|
modelLabel() {
|
|
3629
|
-
return this.config.model ??
|
|
4241
|
+
return this.config.model ?? HOSTED_DEFAULT_MODEL;
|
|
3630
4242
|
}
|
|
3631
4243
|
/** Append the request header snapshot once per loop instance. */
|
|
3632
4244
|
assertRequestHeader() {
|
|
@@ -3642,46 +4254,29 @@ var PiAgent = class {
|
|
|
3642
4254
|
this.requestHeaderLogged = true;
|
|
3643
4255
|
}
|
|
3644
4256
|
/**
|
|
3645
|
-
*
|
|
3646
|
-
*
|
|
3647
|
-
*
|
|
3648
|
-
*
|
|
3649
|
-
*
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
/**
|
|
3663
|
-
* Resolve the `--model` for the RPC child. The session-selected model (last
|
|
3664
|
-
* `model/selection` event) is honored only when it is one of pi's discovered
|
|
3665
|
-
* models; an unknown harness model (e.g. another provider's model such as
|
|
3666
|
-
* `anyai-v1`, which pi cannot serve) is dropped so the child falls back to pi's
|
|
3667
|
-
* own default instead of exiting with "Model ... not found". An empty catalog
|
|
3668
|
-
* (probe not concluded) keeps the candidate, matching prior behavior.
|
|
4257
|
+
* Build the `pi --mode rpc` argv/cwd/env for one step's child process.
|
|
4258
|
+
*
|
|
4259
|
+
* The model comes from the session's own selection when it names a real dsh
|
|
4260
|
+
* model, else from the deployment's pinned configuration
|
|
4261
|
+
* ({@link sessionModelOverrideOf}). Pi's `--model` flag is
|
|
4262
|
+
* `"provider/id"`-qualified, so a session pick travels as the composite —
|
|
4263
|
+
* which also carries the provider, leaving the deployment's `--provider` out
|
|
4264
|
+
* of the argv for that step. A step with neither a selection nor a pin sends
|
|
4265
|
+
* no `--model` at all and lets Pi's own configuration decide. Read every step,
|
|
4266
|
+
* so a model picked mid-conversation lands on the next child.
|
|
4267
|
+
*
|
|
4268
|
+
* When dsh also discloses that model's ENDPOINT, the child is pointed at it:
|
|
4269
|
+
* `--provider` names the provider pi's `models.json` declares, `--api-key`
|
|
4270
|
+
* carries the credential, and `PI_CODING_AGENT_DIR` redirects pi's agent
|
|
4271
|
+
* directory to the plugin-owned one holding that `models.json`
|
|
4272
|
+
* ({@link piAgentDir}) — so the child runs on dsh's endpoint instead of its
|
|
4273
|
+
* own `~/.pi` configuration, which is never touched.
|
|
3669
4274
|
*/
|
|
3670
|
-
|
|
3671
|
-
const candidate = this.dynamicModel() ?? this.config.model;
|
|
3672
|
-
if (candidate === void 0) return void 0;
|
|
3673
|
-
if (this.catalog.entries.length > 0) {
|
|
3674
|
-
const known = this.catalog.entries.some(
|
|
3675
|
-
(entry) => entry.model === candidate || `${entry.provider}/${entry.model}` === candidate
|
|
3676
|
-
);
|
|
3677
|
-
if (!known) return void 0;
|
|
3678
|
-
}
|
|
3679
|
-
return candidate;
|
|
3680
|
-
}
|
|
3681
|
-
spawnSpec(cwd) {
|
|
4275
|
+
spawnSpec(cwd, handover) {
|
|
3682
4276
|
const argv = [];
|
|
3683
|
-
const
|
|
3684
|
-
|
|
4277
|
+
const override = sessionModelOverrideOf(this.loopCtx, this.session);
|
|
4278
|
+
const model = override === void 0 ? this.config.model : `${override.provider}/${override.model}`;
|
|
4279
|
+
if (override === void 0 && this.config.provider !== void 0) argv.push("--provider", this.config.provider);
|
|
3685
4280
|
if (model !== void 0 && this.config.thinkingLevel !== void 0) {
|
|
3686
4281
|
argv.push("--model", `${model}:${this.config.thinkingLevel}`);
|
|
3687
4282
|
} else if (model !== void 0) {
|
|
@@ -3689,6 +4284,7 @@ var PiAgent = class {
|
|
|
3689
4284
|
} else if (this.config.thinkingLevel !== void 0) {
|
|
3690
4285
|
argv.push("--model", `:${this.config.thinkingLevel}`);
|
|
3691
4286
|
}
|
|
4287
|
+
if (handover !== void 0) argv.push("--provider", handover.provider, "--api-key", handover.apiKey);
|
|
3692
4288
|
const permission = this.queryPermission();
|
|
3693
4289
|
if (permission.tools.length > 0) argv.push(TOOLS_FLAG, permission.tools.join(","));
|
|
3694
4290
|
return {
|
|
@@ -3700,7 +4296,7 @@ var PiAgent = class {
|
|
|
3700
4296
|
...argv
|
|
3701
4297
|
],
|
|
3702
4298
|
cwd,
|
|
3703
|
-
env: this.config.env
|
|
4299
|
+
env: handover === void 0 ? this.config.env : { ...this.config.env, PI_CODING_AGENT_DIR: piAgentDir(handover) }
|
|
3704
4300
|
};
|
|
3705
4301
|
}
|
|
3706
4302
|
/**
|
|
@@ -3723,12 +4319,16 @@ var PiAgent = class {
|
|
|
3723
4319
|
throw new Error(`agent "${this.id}": no working directory \u2014 start the session with cwd metadata`);
|
|
3724
4320
|
}
|
|
3725
4321
|
const history = this.session.deriveMessages();
|
|
3726
|
-
const prompt = serializeHistory(history);
|
|
4322
|
+
const prompt = engineSlashPrompt(history) ?? serializeHistory(history);
|
|
3727
4323
|
if (prompt.length === 0) {
|
|
3728
4324
|
throw new Error(`agent "${this.id}": cannot derive a prompt from an empty session log`);
|
|
3729
4325
|
}
|
|
3730
4326
|
this.assertRequestHeader();
|
|
3731
4327
|
signal.throwIfAborted();
|
|
4328
|
+
const handover = await resolveModelHandover(
|
|
4329
|
+
this.loopCtx,
|
|
4330
|
+
sessionModelOverrideOf(this.loopCtx, this.session)
|
|
4331
|
+
);
|
|
3732
4332
|
const controller = new AbortController();
|
|
3733
4333
|
const cancel = () => {
|
|
3734
4334
|
if (!controller.signal.aborted) {
|
|
@@ -3752,7 +4352,7 @@ var PiAgent = class {
|
|
|
3752
4352
|
}
|
|
3753
4353
|
return live;
|
|
3754
4354
|
};
|
|
3755
|
-
const client = this.rpcClient(cwd);
|
|
4355
|
+
const client = this.rpcClient(cwd, handover);
|
|
3756
4356
|
signal.throwIfAborted();
|
|
3757
4357
|
await client.newSession();
|
|
3758
4358
|
client.clearEvents();
|
|
@@ -3791,12 +4391,13 @@ var PiAgent = class {
|
|
|
3791
4391
|
}
|
|
3792
4392
|
held = void 0;
|
|
3793
4393
|
for (const call of callsToLog) {
|
|
4394
|
+
const normalized = normalizeHostedToolCall("pi", call.name, call.arguments);
|
|
3794
4395
|
this.session.append("tool/call", {
|
|
3795
4396
|
turn: phase.turn,
|
|
3796
4397
|
step: phase.step,
|
|
3797
4398
|
callId: ToolCallId5(call.callId),
|
|
3798
|
-
name:
|
|
3799
|
-
arguments:
|
|
4399
|
+
name: normalized.name,
|
|
4400
|
+
arguments: normalized.arguments
|
|
3800
4401
|
});
|
|
3801
4402
|
}
|
|
3802
4403
|
callsToLog = [];
|
|
@@ -3954,7 +4555,7 @@ var PiAgent = class {
|
|
|
3954
4555
|
}
|
|
3955
4556
|
flushHeld(lastUsage);
|
|
3956
4557
|
if (!finished) {
|
|
3957
|
-
throw new
|
|
4558
|
+
throw new LlmError4(
|
|
3958
4559
|
`agent "${this.id}": pi query ended without an agent settle`,
|
|
3959
4560
|
"PI_NO_RESULT"
|
|
3960
4561
|
);
|
|
@@ -3983,73 +4584,6 @@ var PiAgent = class {
|
|
|
3983
4584
|
}
|
|
3984
4585
|
};
|
|
3985
4586
|
|
|
3986
|
-
// src/engine-pi/probe.ts
|
|
3987
|
-
function waitForExit(child) {
|
|
3988
|
-
return new Promise((resolve5) => {
|
|
3989
|
-
;
|
|
3990
|
-
child.onExit(
|
|
3991
|
-
(code) => resolve5(code ?? 0)
|
|
3992
|
-
);
|
|
3993
|
-
});
|
|
3994
|
-
}
|
|
3995
|
-
function collectOutput(child) {
|
|
3996
|
-
return new Promise((resolve5) => {
|
|
3997
|
-
if (child.stdout.on === void 0 || child.stderr.on === void 0) {
|
|
3998
|
-
resolve5("");
|
|
3999
|
-
return;
|
|
4000
|
-
}
|
|
4001
|
-
let text = "";
|
|
4002
|
-
let pending = 2;
|
|
4003
|
-
const finish = () => {
|
|
4004
|
-
pending -= 1;
|
|
4005
|
-
if (pending === 0) resolve5(text);
|
|
4006
|
-
};
|
|
4007
|
-
const out = child.stdout;
|
|
4008
|
-
const err = child.stderr;
|
|
4009
|
-
out.setEncoding("utf8");
|
|
4010
|
-
out.on("data", (data) => {
|
|
4011
|
-
text += String(data);
|
|
4012
|
-
});
|
|
4013
|
-
out.on("close", finish);
|
|
4014
|
-
err.setEncoding("utf8");
|
|
4015
|
-
err.on("data", (data) => {
|
|
4016
|
-
text += String(data);
|
|
4017
|
-
});
|
|
4018
|
-
err.on("close", finish);
|
|
4019
|
-
});
|
|
4020
|
-
}
|
|
4021
|
-
function parsePiModelList(output) {
|
|
4022
|
-
const entries = [];
|
|
4023
|
-
for (const line of output.split("\n")) {
|
|
4024
|
-
const trimmed = line.trim();
|
|
4025
|
-
if (trimmed.length === 0) continue;
|
|
4026
|
-
const match = /^(\S+)\s{2,}(\S+)/.exec(trimmed);
|
|
4027
|
-
if (match === null) continue;
|
|
4028
|
-
const provider = match[1];
|
|
4029
|
-
if (provider === "provider") continue;
|
|
4030
|
-
entries.push({ provider, model: match[2] });
|
|
4031
|
-
}
|
|
4032
|
-
return entries;
|
|
4033
|
-
}
|
|
4034
|
-
async function probePiModels(bin, spawn4) {
|
|
4035
|
-
const spec = {
|
|
4036
|
-
argv: [bin, "--mode", "rpc", "--list-models"],
|
|
4037
|
-
cwd: process.cwd(),
|
|
4038
|
-
env: {}
|
|
4039
|
-
};
|
|
4040
|
-
let child;
|
|
4041
|
-
try {
|
|
4042
|
-
child = spawn4(spec);
|
|
4043
|
-
} catch {
|
|
4044
|
-
return [];
|
|
4045
|
-
}
|
|
4046
|
-
const outputPromise = collectOutput(child);
|
|
4047
|
-
const exitCode = await waitForExit(child);
|
|
4048
|
-
if (exitCode !== 0) return [];
|
|
4049
|
-
const output = await outputPromise;
|
|
4050
|
-
return parsePiModelList(output);
|
|
4051
|
-
}
|
|
4052
|
-
|
|
4053
4587
|
// src/engine-pi/loop.ts
|
|
4054
4588
|
var PI_SANDBOX_MODES = [
|
|
4055
4589
|
"read-only",
|
|
@@ -4062,8 +4596,7 @@ var Config3 = z3.object({
|
|
|
4062
4596
|
provider: z3.string(),
|
|
4063
4597
|
model: z3.string(),
|
|
4064
4598
|
thinkingLevel: z3.string(),
|
|
4065
|
-
env: z3.dict(z3.string()).default({})
|
|
4066
|
-
piCatalogHolder: z3.any()
|
|
4599
|
+
env: z3.dict(z3.string()).default({})
|
|
4067
4600
|
});
|
|
4068
4601
|
function resolveConfig3(config) {
|
|
4069
4602
|
return {
|
|
@@ -4077,10 +4610,10 @@ function resolveConfig3(config) {
|
|
|
4077
4610
|
function piCliEntrypoint() {
|
|
4078
4611
|
const mainUrl = import.meta.resolve("@earendil-works/pi-coding-agent");
|
|
4079
4612
|
const root = dirname2(dirname2(fileURLToPath(mainUrl)));
|
|
4080
|
-
const pkg = JSON.parse(readFileSync(
|
|
4613
|
+
const pkg = JSON.parse(readFileSync(join3(root, "package.json"), "utf8"));
|
|
4081
4614
|
const bin = pkg.bin;
|
|
4082
4615
|
const rel = typeof bin === "string" ? bin : bin?.["pi"] ?? Object.values(bin ?? {})[0] ?? "bin/pi.js";
|
|
4083
|
-
return
|
|
4616
|
+
return join3(root, rel);
|
|
4084
4617
|
}
|
|
4085
4618
|
function piSubprocessSpec(spec, graceMs) {
|
|
4086
4619
|
return {
|
|
@@ -4102,34 +4635,25 @@ function fromSubprocess(handle) {
|
|
|
4102
4635
|
stdout,
|
|
4103
4636
|
stderr,
|
|
4104
4637
|
onExit: (handler) => {
|
|
4105
|
-
|
|
4106
|
-
void handle.done.then((outcome) => onExit(outcome.exitCode), handler);
|
|
4638
|
+
void handle.done.then(handler, handler);
|
|
4107
4639
|
},
|
|
4108
4640
|
terminate: () => handle.terminate()
|
|
4109
4641
|
};
|
|
4110
4642
|
}
|
|
4111
|
-
var
|
|
4112
|
-
|
|
4113
|
-
static inject = ["agents", "sessions", "systemPrompt", "subprocess"];
|
|
4643
|
+
var PI_ENGINE_LABEL = "agentLoopPi";
|
|
4644
|
+
var PiLoop = class extends HostedEngineRuntime {
|
|
4114
4645
|
/** Process-tree spawn capability handed to every agent, sandboxed by the subprocess seam. */
|
|
4115
4646
|
spawn;
|
|
4116
4647
|
/** Resolved Pi CLI entrypoint; `argv[0]` of every Pi RPC child. */
|
|
4117
4648
|
bin;
|
|
4118
|
-
/** Discovered Pi model catalog, forwarded to each agent so it can validate the session-selected model against what pi can actually serve. */
|
|
4119
|
-
catalog;
|
|
4120
4649
|
constructor(ctx, config) {
|
|
4121
|
-
super(ctx,
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
const holder = config.piCatalogHolder;
|
|
4126
|
-
if (holder !== void 0) {
|
|
4127
|
-
void probePiModels(this.bin, (spec) => this.spawn(spec)).then((models) => {
|
|
4128
|
-
holder.entries = [...models];
|
|
4129
|
-
}).catch(() => {
|
|
4130
|
-
holder.entries = [];
|
|
4131
|
-
});
|
|
4650
|
+
super(ctx, PI_ENGINE_LABEL, resolveConfig3(config));
|
|
4651
|
+
const subprocess = ctx.get("subprocess");
|
|
4652
|
+
if (subprocess === void 0) {
|
|
4653
|
+
throw new Error("loop-engine: the pi engine needs the dsh subprocess service on this context");
|
|
4132
4654
|
}
|
|
4655
|
+
this.bin = piCliEntrypoint();
|
|
4656
|
+
this.spawn = (spec) => fromSubprocess(subprocess.spawn(piSubprocessSpec(spec, PI_DISPOSE_GRACE_MS)));
|
|
4133
4657
|
}
|
|
4134
4658
|
/** Construct the Pi RPC driver for one prepared session. */
|
|
4135
4659
|
buildAgent(loopCtx, id, options, session) {
|
|
@@ -4140,8 +4664,7 @@ var PiLoop = class extends HostedLoopFactory {
|
|
|
4140
4664
|
session,
|
|
4141
4665
|
this.config,
|
|
4142
4666
|
this.spawn,
|
|
4143
|
-
this.bin
|
|
4144
|
-
this.catalog
|
|
4667
|
+
this.bin
|
|
4145
4668
|
);
|
|
4146
4669
|
}
|
|
4147
4670
|
};
|
|
@@ -4151,22 +4674,38 @@ import z4 from "@deepseek-ai/schemastery";
|
|
|
4151
4674
|
|
|
4152
4675
|
// src/engine-kimi/agent.ts
|
|
4153
4676
|
import { agentEvents as agentEvents4 } from "@deepseek-ai/dsh-agent";
|
|
4154
|
-
import { ToolCallId as ToolCallId7, LlmError as
|
|
4677
|
+
import { ToolCallId as ToolCallId7, LlmError as LlmError5, createAssistantMessage as createAssistantMessage4, createUserMessage as createUserMessage4, errorChain as errorChain4 } from "@deepseek-ai/dsh-llm";
|
|
4155
4678
|
import { createScope as createScope4 } from "@deepseek-ai/dsh-scope";
|
|
4156
4679
|
import { canonicalHeader as canonicalHeader4 } from "@deepseek-ai/dsh-session";
|
|
4157
4680
|
|
|
4681
|
+
// src/engine-kimi/model-handover.ts
|
|
4682
|
+
var KIMI_PROVIDER_TYPES = {
|
|
4683
|
+
"anthropic-messages": "anthropic",
|
|
4684
|
+
"openai-completions": "openai",
|
|
4685
|
+
"openai-responses": "openai"
|
|
4686
|
+
};
|
|
4687
|
+
function kimiModelEnv(handover) {
|
|
4688
|
+
const type = KIMI_PROVIDER_TYPES[handover.api];
|
|
4689
|
+
return {
|
|
4690
|
+
KIMI_MODEL_NAME: handover.model,
|
|
4691
|
+
KIMI_MODEL_API_KEY: handover.apiKey,
|
|
4692
|
+
KIMI_MODEL_BASE_URL: handover.baseURL,
|
|
4693
|
+
...type === void 0 ? {} : { KIMI_MODEL_PROVIDER_TYPE: type }
|
|
4694
|
+
};
|
|
4695
|
+
}
|
|
4696
|
+
|
|
4158
4697
|
// src/engine-kimi/process.ts
|
|
4159
4698
|
import { existsSync } from "node:fs";
|
|
4160
4699
|
import { homedir } from "node:os";
|
|
4161
|
-
import { join as
|
|
4700
|
+
import { join as join4 } from "node:path";
|
|
4162
4701
|
function kimiHomeDir() {
|
|
4163
4702
|
const envHome = process.env.KIMI_CODE_HOME;
|
|
4164
|
-
return envHome !== void 0 && envHome !== "" ? envHome :
|
|
4703
|
+
return envHome !== void 0 && envHome !== "" ? envHome : join4(homedir(), ".kimi-code");
|
|
4165
4704
|
}
|
|
4166
4705
|
function kimiBinResolver(configBin) {
|
|
4167
4706
|
if (configBin !== void 0 && configBin !== "") return configBin;
|
|
4168
4707
|
const executable = process.platform === "win32" ? "kimi.exe" : "kimi";
|
|
4169
|
-
const candidate =
|
|
4708
|
+
const candidate = join4(kimiHomeDir(), "bin", executable);
|
|
4170
4709
|
return existsSync(candidate) ? candidate : "kimi";
|
|
4171
4710
|
}
|
|
4172
4711
|
function kimiAcpArgv(bin) {
|
|
@@ -4320,6 +4859,18 @@ var AcpClient = class _AcpClient {
|
|
|
4320
4859
|
}
|
|
4321
4860
|
return sessionId;
|
|
4322
4861
|
}
|
|
4862
|
+
/**
|
|
4863
|
+
* Select the model for one ACP session. The agent answers with an error frame
|
|
4864
|
+
* when it cannot serve the id, and {@link request} rejects on that frame — so
|
|
4865
|
+
* an engine that refuses the model fails the step loud rather than silently
|
|
4866
|
+
* running its own default.
|
|
4867
|
+
* @param sessionId - the ACP session the model applies to.
|
|
4868
|
+
* @param modelId - the model id to select.
|
|
4869
|
+
* @returns the agent's response to `session/set_model`.
|
|
4870
|
+
*/
|
|
4871
|
+
setModel(sessionId, modelId) {
|
|
4872
|
+
return this.request("session/set_model", { sessionId, modelId });
|
|
4873
|
+
}
|
|
4323
4874
|
/** Prompt the agent in a session and resolve when the turn completes. */
|
|
4324
4875
|
prompt(sessionId, text) {
|
|
4325
4876
|
return this.request("session/prompt", { sessionId, prompt: [{ type: "text", text }] });
|
|
@@ -4476,8 +5027,7 @@ function resolveToolApproval(events) {
|
|
|
4476
5027
|
}
|
|
4477
5028
|
|
|
4478
5029
|
// src/engine-kimi/agent.ts
|
|
4479
|
-
var PROVIDER4 =
|
|
4480
|
-
var NATIVE_MODEL_LABEL4 = "kimi-native";
|
|
5030
|
+
var PROVIDER4 = HOSTED_ROUTE_LABEL;
|
|
4481
5031
|
var KimiAgent = class {
|
|
4482
5032
|
constructor(loopCtx, id, options, session, config, spawn4, bin) {
|
|
4483
5033
|
this.loopCtx = loopCtx;
|
|
@@ -4531,6 +5081,13 @@ var KimiAgent = class {
|
|
|
4531
5081
|
acp;
|
|
4532
5082
|
/** The spawn spec the cached client was built from; a change forces a respawn. */
|
|
4533
5083
|
lastSpec;
|
|
5084
|
+
/**
|
|
5085
|
+
* The environment the last handover produced, memoized by its own content so
|
|
5086
|
+
* an unchanged endpoint reuses ONE object: `specsEqual` compares environments
|
|
5087
|
+
* by reference, and a fresh object per step would otherwise respawn the child
|
|
5088
|
+
* every step.
|
|
5089
|
+
*/
|
|
5090
|
+
handoverEnvCache;
|
|
4534
5091
|
get status() {
|
|
4535
5092
|
return this.phase.kind === "idle" || this.phase.kind === "maintenance" ? "idle" : "running";
|
|
4536
5093
|
}
|
|
@@ -4762,7 +5319,7 @@ var KimiAgent = class {
|
|
|
4762
5319
|
}
|
|
4763
5320
|
turnEnds = {
|
|
4764
5321
|
kind: "error",
|
|
4765
|
-
error: error instanceof
|
|
5322
|
+
error: error instanceof LlmError5 ? error.failure : { message: errorChain4(error), code: "UNKNOWN" }
|
|
4766
5323
|
};
|
|
4767
5324
|
this.throwError(error);
|
|
4768
5325
|
} finally {
|
|
@@ -4780,7 +5337,7 @@ var KimiAgent = class {
|
|
|
4780
5337
|
}
|
|
4781
5338
|
/** Model label recorded in the request header for one lifecycle. */
|
|
4782
5339
|
modelLabel() {
|
|
4783
|
-
return this.config.model ??
|
|
5340
|
+
return this.config.model ?? HOSTED_DEFAULT_MODEL;
|
|
4784
5341
|
}
|
|
4785
5342
|
/** Append the request header snapshot once per loop instance. */
|
|
4786
5343
|
assertRequestHeader() {
|
|
@@ -4801,8 +5358,8 @@ var KimiAgent = class {
|
|
|
4801
5358
|
return a.cwd === b.cwd && a.env === b.env && a.argv.length === b.argv.length && a.argv.every((value, index) => value === b.argv[index]);
|
|
4802
5359
|
}
|
|
4803
5360
|
/** Return the cached ACP client, respawning when the spec or process changed. */
|
|
4804
|
-
async acpClient(cwd) {
|
|
4805
|
-
const spec = this.spawnSpec(cwd);
|
|
5361
|
+
async acpClient(cwd, handover) {
|
|
5362
|
+
const spec = this.spawnSpec(cwd, handover);
|
|
4806
5363
|
if (this.acp !== void 0 && !this.acp.closed && this.specsEqual(this.lastSpec, spec)) return this.acp;
|
|
4807
5364
|
this.acp?.dispose();
|
|
4808
5365
|
const client = AcpClient.create(spec, this.spawn);
|
|
@@ -4819,13 +5376,25 @@ var KimiAgent = class {
|
|
|
4819
5376
|
return client;
|
|
4820
5377
|
}
|
|
4821
5378
|
/** Build the `kimi acp` argv/cwd/env for the persistent child. */
|
|
4822
|
-
spawnSpec(cwd) {
|
|
5379
|
+
spawnSpec(cwd, handover) {
|
|
4823
5380
|
return {
|
|
4824
5381
|
argv: kimiAcpArgv(this.bin),
|
|
4825
5382
|
cwd,
|
|
4826
|
-
env: this.
|
|
5383
|
+
env: this.handoverEnv(handover)
|
|
4827
5384
|
};
|
|
4828
5385
|
}
|
|
5386
|
+
/**
|
|
5387
|
+
* The child environment for one handover, memoized by content so an unchanged
|
|
5388
|
+
* endpoint yields the SAME object across steps (see {@link handoverEnvCache}).
|
|
5389
|
+
*/
|
|
5390
|
+
handoverEnv(handover) {
|
|
5391
|
+
if (handover === void 0) return this.config.env;
|
|
5392
|
+
const key = JSON.stringify(handover);
|
|
5393
|
+
if (this.handoverEnvCache?.key === key) return this.handoverEnvCache.env;
|
|
5394
|
+
const env = { ...this.config.env, ...kimiModelEnv(handover) };
|
|
5395
|
+
this.handoverEnvCache = { key, env };
|
|
5396
|
+
return env;
|
|
5397
|
+
}
|
|
4829
5398
|
/** Run one `kimi acp` step for the current session history and map the streamed updates. */
|
|
4830
5399
|
async step() {
|
|
4831
5400
|
if (this.phase.kind !== "running") throw new Error(`agent "${this.id}": step outside running phase`);
|
|
@@ -4843,17 +5412,22 @@ var KimiAgent = class {
|
|
|
4843
5412
|
throw new Error(`agent "${this.id}": no working directory \u2014 start the session with cwd metadata`);
|
|
4844
5413
|
}
|
|
4845
5414
|
const history = this.session.deriveMessages();
|
|
4846
|
-
const prompt = serializeHistory(history);
|
|
5415
|
+
const prompt = engineSlashPrompt(history) ?? serializeHistory(history);
|
|
4847
5416
|
if (prompt.length === 0) {
|
|
4848
5417
|
throw new Error(`agent "${this.id}": cannot derive a prompt from an empty session log`);
|
|
4849
5418
|
}
|
|
4850
5419
|
this.assertRequestHeader();
|
|
4851
5420
|
signal.throwIfAborted();
|
|
4852
|
-
const
|
|
5421
|
+
const override = sessionModelOverrideOf(this.loopCtx, this.session);
|
|
5422
|
+
const handover = await resolveModelHandover(this.loopCtx, override);
|
|
5423
|
+
const client = await this.acpClient(cwd, handover);
|
|
4853
5424
|
signal.throwIfAborted();
|
|
4854
5425
|
client.onPermission(() => resolveToolApproval(this.session.snapshotEvents()));
|
|
4855
5426
|
const acpSessionId = await client.newSession(cwd);
|
|
4856
5427
|
signal.throwIfAborted();
|
|
5428
|
+
const model = override?.model ?? this.config.model;
|
|
5429
|
+
if (handover === void 0 && model !== void 0) await client.setModel(acpSessionId, model);
|
|
5430
|
+
signal.throwIfAborted();
|
|
4857
5431
|
try {
|
|
4858
5432
|
client.onUpdate((update) => this.applyUpdate(phase, update));
|
|
4859
5433
|
const cancel = () => {
|
|
@@ -4871,7 +5445,7 @@ var KimiAgent = class {
|
|
|
4871
5445
|
this.pendingCalls.clear();
|
|
4872
5446
|
this.flushSegment(phase);
|
|
4873
5447
|
if (!this.producedOutput) {
|
|
4874
|
-
throw new
|
|
5448
|
+
throw new LlmError5(
|
|
4875
5449
|
`agent "${this.id}": kimi query produced no assistant output`,
|
|
4876
5450
|
"KIMI_NO_RESULT"
|
|
4877
5451
|
);
|
|
@@ -5028,7 +5602,8 @@ var KimiAgent = class {
|
|
|
5028
5602
|
this.segmentCalls = [];
|
|
5029
5603
|
this.flushAssistant(phase, calls);
|
|
5030
5604
|
for (const call of calls) {
|
|
5031
|
-
|
|
5605
|
+
const normalized = normalizeHostedToolCall("kimi", call.name, call.arguments);
|
|
5606
|
+
this.session.append("tool/call", { turn: phase.turn, step: phase.step, callId: ToolCallId7(call.callId), name: normalized.name, arguments: normalized.arguments });
|
|
5032
5607
|
}
|
|
5033
5608
|
}
|
|
5034
5609
|
/**
|
|
@@ -5093,14 +5668,17 @@ function resolveConfig4(config) {
|
|
|
5093
5668
|
bin: kimiBinResolver(config.bin)
|
|
5094
5669
|
};
|
|
5095
5670
|
}
|
|
5096
|
-
var
|
|
5097
|
-
|
|
5098
|
-
static inject = ["agents", "sessions", "systemPrompt", "subprocess"];
|
|
5671
|
+
var KIMI_ENGINE_LABEL = "agentLoopKimi";
|
|
5672
|
+
var KimiLoop = class extends HostedEngineRuntime {
|
|
5099
5673
|
/** One-shot spawn capability handed to every agent, sandboxed by the subprocess seam. */
|
|
5100
5674
|
spawn;
|
|
5101
5675
|
constructor(ctx, config) {
|
|
5102
|
-
super(ctx,
|
|
5103
|
-
|
|
5676
|
+
super(ctx, KIMI_ENGINE_LABEL, resolveConfig4(config));
|
|
5677
|
+
const subprocess = ctx.get("subprocess");
|
|
5678
|
+
if (subprocess === void 0) {
|
|
5679
|
+
throw new Error("loop-engine: the kimi engine needs the dsh subprocess service on this context");
|
|
5680
|
+
}
|
|
5681
|
+
this.spawn = (spec) => fromSubprocess2(subprocess.spawn(kimiSubprocessSpec(spec, KIMI_DISPOSE_GRACE_MS)));
|
|
5104
5682
|
}
|
|
5105
5683
|
/** Construct the Kimi ACP driver for one prepared session. */
|
|
5106
5684
|
buildAgent(loopCtx, id, options, session) {
|
|
@@ -5108,22 +5686,511 @@ var KimiLoop = class extends HostedLoopFactory {
|
|
|
5108
5686
|
}
|
|
5109
5687
|
};
|
|
5110
5688
|
|
|
5111
|
-
// src/
|
|
5112
|
-
import
|
|
5113
|
-
import { join as join7, resolve as resolve3 } from "node:path";
|
|
5689
|
+
// src/settings.ts
|
|
5690
|
+
import z5 from "@deepseek-ai/schemastery";
|
|
5114
5691
|
|
|
5115
|
-
// src/
|
|
5116
|
-
|
|
5117
|
-
import { dirname as dirname3, join as join6 } from "node:path";
|
|
5692
|
+
// src/namespace.ts
|
|
5693
|
+
var LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
|
|
5118
5694
|
|
|
5119
|
-
// src/
|
|
5120
|
-
|
|
5121
|
-
|
|
5695
|
+
// src/settings.ts
|
|
5696
|
+
var LOOP_ENGINE_SETTINGS_SCHEMA = z5.object({
|
|
5697
|
+
engine: z5.union(LOOP_ENGINE_IDS.map((id) => z5.const(id))).default("in-process"),
|
|
5698
|
+
showInComposer: z5.boolean().default(true)
|
|
5699
|
+
});
|
|
5700
|
+
function loopEngineSettingsNamespace() {
|
|
5701
|
+
return LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL;
|
|
5702
|
+
}
|
|
5122
5703
|
|
|
5123
|
-
// src/
|
|
5124
|
-
|
|
5704
|
+
// src/patch-manager.ts
|
|
5705
|
+
var MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block --";
|
|
5706
|
+
var LEGACY_MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block: ";
|
|
5707
|
+
var MANAGED_BLOCK_END = "# -- /dsh-loop-engine managed block --";
|
|
5708
|
+
var END_MARKER_LINE = `${MANAGED_BLOCK_END}
|
|
5709
|
+
`;
|
|
5710
|
+
function renderManagedBlock() {
|
|
5711
|
+
return [
|
|
5712
|
+
MANAGED_BLOCK_BEGIN,
|
|
5713
|
+
"- id: agent-loop",
|
|
5714
|
+
" disabled: true",
|
|
5715
|
+
"- id: command-goal",
|
|
5716
|
+
" disabled: true",
|
|
5717
|
+
END_MARKER_LINE
|
|
5718
|
+
].join("\n");
|
|
5719
|
+
}
|
|
5720
|
+
function hasLegacyManagedBlock(text) {
|
|
5721
|
+
return text.includes(LEGACY_MANAGED_BLOCK_BEGIN);
|
|
5722
|
+
}
|
|
5723
|
+
function hasManagedBlock(text) {
|
|
5724
|
+
return text.includes("# -- dsh-loop-engine managed block");
|
|
5725
|
+
}
|
|
5726
|
+
var LEGACY_BEGIN_MARKER_RE = /^# -- dsh-loop-engine managed block: (\S+) --$/m;
|
|
5727
|
+
function legacyBlockEngineOf(text) {
|
|
5728
|
+
const engine = LEGACY_BEGIN_MARKER_RE.exec(text)?.[1];
|
|
5729
|
+
return LOOP_ENGINE_IDS.includes(engine ?? "") ? engine : void 0;
|
|
5730
|
+
}
|
|
5731
|
+
function managedSpan(text) {
|
|
5732
|
+
const begin = text.indexOf("# -- dsh-loop-engine managed block");
|
|
5733
|
+
if (begin === -1) return { head: text, tail: "", present: false, blankBefore: false };
|
|
5734
|
+
const afterBegin = begin + LEGACY_MANAGED_BLOCK_BEGIN.length;
|
|
5735
|
+
const endAt = text.indexOf(MANAGED_BLOCK_END, afterBegin);
|
|
5736
|
+
const spanEnd = endAt === -1 ? text.length : endAt + END_MARKER_LINE.length;
|
|
5737
|
+
const before = text.slice(0, begin);
|
|
5738
|
+
const blankBefore = before.endsWith("\n\n");
|
|
5739
|
+
return {
|
|
5740
|
+
head: blankBefore ? before.slice(0, -1) : before,
|
|
5741
|
+
tail: text.slice(spanEnd),
|
|
5742
|
+
present: true,
|
|
5743
|
+
blankBefore
|
|
5744
|
+
};
|
|
5745
|
+
}
|
|
5746
|
+
function ensureTrailingNewline(text) {
|
|
5747
|
+
return text.endsWith("\n") ? text : `${text}
|
|
5748
|
+
`;
|
|
5749
|
+
}
|
|
5750
|
+
function dropSeedPlaceholder(text) {
|
|
5751
|
+
return text.replace(/^\[\]\n/m, "");
|
|
5752
|
+
}
|
|
5753
|
+
function applyManagedBlock(text) {
|
|
5754
|
+
const block = renderManagedBlock();
|
|
5755
|
+
const span = managedSpan(text);
|
|
5756
|
+
if (!span.present) {
|
|
5757
|
+
if (text === "") return block;
|
|
5758
|
+
return dropSeedPlaceholder(`${ensureTrailingNewline(text)}
|
|
5759
|
+
${block}`);
|
|
5760
|
+
}
|
|
5761
|
+
return dropSeedPlaceholder(`${span.head}${span.blankBefore ? "\n" : ""}${block}${span.tail}`);
|
|
5762
|
+
}
|
|
5763
|
+
|
|
5764
|
+
// src/preset.ts
|
|
5765
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
5766
|
+
import { randomUUID } from "node:crypto";
|
|
5767
|
+
import { dirname as dirname3, join as join5 } from "node:path";
|
|
5768
|
+
var USER_PRESET_DIR = ".agent-presets";
|
|
5769
|
+
var COMPOSITION_FILE = "agent.cordis.yml";
|
|
5770
|
+
var METADATA_FILE = "preset.yml";
|
|
5771
|
+
var HOSTED_PRESET_IDS = HOSTED_ENGINE_IDS.map((engine) => enginePresetId(engine));
|
|
5772
|
+
var STRIPPED_ROWS = ["skill-filesystem", "tool-skill", "tool-goal", "command-goal", "planning", "compaction"];
|
|
5773
|
+
var MANAGED_HEADER = `# Managed by dsh-loop-engine: the deployment's "${SOURCE_PRESET_ID}" preset minus
|
|
5774
|
+
# the dsh-native command/skill rows a hosted loop engine replaces. Regenerated
|
|
5775
|
+
# from "${SOURCE_PRESET_ID}" on boot \u2014 hand edits are overwritten. The preset id
|
|
5776
|
+
# names the engine this session runs.
|
|
5777
|
+
`;
|
|
5778
|
+
function managedMetadata(engine) {
|
|
5779
|
+
return `name: ${ENGINE_DISPLAY_NAMES[engine]}
|
|
5780
|
+
description: ${ENGINE_DISPLAY_NAMES[engine]}, with the dsh-native commands and skills it replaces removed.
|
|
5781
|
+
`;
|
|
5782
|
+
}
|
|
5783
|
+
var ENGINE_DISPLAY_NAMES = {
|
|
5784
|
+
"claude-code": "Claude Code",
|
|
5785
|
+
codex: "Codex",
|
|
5786
|
+
pi: "Pi",
|
|
5787
|
+
kimi: "Kimi Code"
|
|
5788
|
+
};
|
|
5789
|
+
function isEntryStart(line) {
|
|
5790
|
+
return line.startsWith("- ");
|
|
5791
|
+
}
|
|
5792
|
+
function entryId(line) {
|
|
5793
|
+
return /^- id:\s*(\S+)\s*$/.exec(line)?.[1];
|
|
5794
|
+
}
|
|
5795
|
+
function stripPresetRows(text, ids = STRIPPED_ROWS) {
|
|
5796
|
+
const lines = text.split("\n");
|
|
5797
|
+
const starts = [];
|
|
5798
|
+
for (const [index, line] of lines.entries()) {
|
|
5799
|
+
if (isEntryStart(line)) starts.push(index);
|
|
5800
|
+
}
|
|
5801
|
+
if (starts.length === 0) return text;
|
|
5802
|
+
const drop = new Set(ids);
|
|
5803
|
+
const entries = [];
|
|
5804
|
+
let heading = lines.slice(0, starts[0]);
|
|
5805
|
+
for (const [index, start] of starts.entries()) {
|
|
5806
|
+
const end = index + 1 < starts.length ? starts[index + 1] : lines.length;
|
|
5807
|
+
const span = lines.slice(start, end);
|
|
5808
|
+
let bodyEnd = span.length;
|
|
5809
|
+
while (bodyEnd > 1) {
|
|
5810
|
+
const line = span[bodyEnd - 1];
|
|
5811
|
+
if (line.trim() !== "" && !line.trimStart().startsWith("#")) break;
|
|
5812
|
+
bodyEnd -= 1;
|
|
5813
|
+
}
|
|
5814
|
+
entries.push({ id: entryId(span[0]), heading, body: span.slice(0, bodyEnd) });
|
|
5815
|
+
heading = span.slice(bodyEnd);
|
|
5816
|
+
}
|
|
5817
|
+
const out = [];
|
|
5818
|
+
out.push(...entries[0].heading);
|
|
5819
|
+
let lastKept = -1;
|
|
5820
|
+
for (const [index, entry] of entries.entries()) {
|
|
5821
|
+
if (entry.id !== void 0 && drop.has(entry.id)) continue;
|
|
5822
|
+
if (index > 0) out.push(...entry.heading);
|
|
5823
|
+
out.push(...entry.body);
|
|
5824
|
+
lastKept = index;
|
|
5825
|
+
}
|
|
5826
|
+
if (lastKept === entries.length - 1) out.push(...heading);
|
|
5827
|
+
if (out.length > 0 && out[out.length - 1] !== "") out.push("");
|
|
5828
|
+
return out.join("\n");
|
|
5829
|
+
}
|
|
5830
|
+
async function writeIfDifferent(path, text) {
|
|
5831
|
+
try {
|
|
5832
|
+
if (await readFile(path, "utf8") === text) return false;
|
|
5833
|
+
} catch {
|
|
5834
|
+
}
|
|
5835
|
+
await mkdir(dirname3(path), { recursive: true });
|
|
5836
|
+
const tmp = `${path}.tmp-${randomUUID()}`;
|
|
5837
|
+
await writeFile(tmp, text, "utf8");
|
|
5838
|
+
await rename(tmp, path);
|
|
5839
|
+
return true;
|
|
5840
|
+
}
|
|
5841
|
+
async function ensureEnginePresets(dshHome, source) {
|
|
5842
|
+
const composition = await source.read(SOURCE_PRESET_ID);
|
|
5843
|
+
const stripped = `${MANAGED_HEADER}
|
|
5844
|
+
${stripPresetRows(composition)}`;
|
|
5845
|
+
let changed = false;
|
|
5846
|
+
for (const engine of HOSTED_ENGINE_IDS) {
|
|
5847
|
+
const dir = join5(dshHome, USER_PRESET_DIR, enginePresetId(engine));
|
|
5848
|
+
const compositionChanged = await writeIfDifferent(join5(dir, COMPOSITION_FILE), stripped);
|
|
5849
|
+
const metadataChanged = await writeIfDifferent(join5(dir, METADATA_FILE), managedMetadata(engine));
|
|
5850
|
+
changed = changed || compositionChanged || metadataChanged;
|
|
5851
|
+
}
|
|
5852
|
+
return changed;
|
|
5853
|
+
}
|
|
5854
|
+
|
|
5855
|
+
// src/router-loop.ts
|
|
5856
|
+
import AgentLoop from "@deepseek-ai/dsh-agent-loop";
|
|
5857
|
+
|
|
5858
|
+
// src/model-selection-reset.ts
|
|
5859
|
+
var AGENT_DEFAULT_MODEL_NS = "agent-default-model";
|
|
5860
|
+
var ModelSelectionReset = class {
|
|
5861
|
+
/**
|
|
5862
|
+
* @param ctx - context carrying the host's default-model service, its settings
|
|
5863
|
+
* (for the configured default), and the session-projection registry.
|
|
5864
|
+
* @param warn - diagnostic sink, used at most once per problem.
|
|
5865
|
+
*/
|
|
5866
|
+
constructor(ctx, warn) {
|
|
5867
|
+
this.ctx = ctx;
|
|
5868
|
+
this.warn = warn;
|
|
5869
|
+
}
|
|
5870
|
+
ctx;
|
|
5871
|
+
warn;
|
|
5872
|
+
/** Whether the one warning this deployment is owed has been reported. */
|
|
5873
|
+
warned = false;
|
|
5874
|
+
/** Whether the one report that the deployment default names a hosted route has been made. */
|
|
5875
|
+
reportedHostedDefault = false;
|
|
5876
|
+
/**
|
|
5877
|
+
* Give one session the selection the engine it is being SWITCHED TO owns,
|
|
5878
|
+
* unless its log already names a real model of its own.
|
|
5879
|
+
*
|
|
5880
|
+
* The switch trigger. A session whose log names no selection at all is left
|
|
5881
|
+
* alone: a switch rewrites a seat an engine already put there, and naming a
|
|
5882
|
+
* session's first seat belongs to the build of a session being created
|
|
5883
|
+
* ({@link guardFor}) rather than to a change of engine.
|
|
5884
|
+
* @param session - the live Session the engine switch is being recorded for.
|
|
5885
|
+
* @param engine - the engine the session is being moved to.
|
|
5886
|
+
* @returns whether a `model/selection` event was appended.
|
|
5887
|
+
*/
|
|
5888
|
+
resetFor(session, engine) {
|
|
5889
|
+
if (engine !== "in-process" && this.currentSelection(session) === void 0) return false;
|
|
5890
|
+
return this.appendSeat(session, engine);
|
|
5891
|
+
}
|
|
5892
|
+
/**
|
|
5893
|
+
* Give one session the selection the engine BUILDING it owns, inside the
|
|
5894
|
+
* router's wrapped `setup` — before the caller's own setup installs the
|
|
5895
|
+
* session's selection — so a selection written here is the one the host reads
|
|
5896
|
+
* back for the agent being built.
|
|
5897
|
+
*
|
|
5898
|
+
* The harness loop's own build keeps its narrower precondition: it makes real
|
|
5899
|
+
* model calls, so it is the one build that must not be left selecting a
|
|
5900
|
+
* placeholder route, and a session whose own log already names a real model
|
|
5901
|
+
* needs nothing written for it. A hosted engine is being given its seat here,
|
|
5902
|
+
* which is what a new session on it starts from.
|
|
5903
|
+
* @param session - the session whose agent is being composed.
|
|
5904
|
+
* @param engine - the engine composing it.
|
|
5905
|
+
* @returns whether a `model/selection` event was appended.
|
|
5906
|
+
*/
|
|
5907
|
+
guardFor(session, engine) {
|
|
5908
|
+
if (engine === "in-process" && !this.selectsHostedRoute(session)) return false;
|
|
5909
|
+
return this.appendSeat(session, engine);
|
|
5910
|
+
}
|
|
5911
|
+
/**
|
|
5912
|
+
* Whether the selection this session's next request would use names one of
|
|
5913
|
+
* the hosted engines' provider routes — a label this plugin serves as a
|
|
5914
|
+
* placeholder and cannot answer a real model call on.
|
|
5915
|
+
*
|
|
5916
|
+
* The read is the host's own (`selectionFor`): a pending selection, else the
|
|
5917
|
+
* logged header, else the deployment's default. A session with nothing in its
|
|
5918
|
+
* log is therefore judged on the deployment default — the value the model menu
|
|
5919
|
+
* can write a hosted label into — and that default is read AS IT IS, never as
|
|
5920
|
+
* {@link defaultSelection} would substitute it: the substitution only exists
|
|
5921
|
+
* once it is written into the session, so judging the substitute would skip
|
|
5922
|
+
* the write that makes it real.
|
|
5923
|
+
* @param session - the session whose selection is being judged.
|
|
5924
|
+
* @returns whether that selection names a hosted engine route.
|
|
5925
|
+
*/
|
|
5926
|
+
selectsHostedRoute(session) {
|
|
5927
|
+
const selected = this.currentSelection(session) ?? this.deploymentDefault();
|
|
5928
|
+
return selected !== void 0 && isHostedProviderRoute(selected.provider);
|
|
5929
|
+
}
|
|
5930
|
+
/**
|
|
5931
|
+
* Append the selection one engine's seat carries, by the judgements both
|
|
5932
|
+
* triggers share.
|
|
5933
|
+
*
|
|
5934
|
+
* A real model selection is never overwritten — the provider is the test,
|
|
5935
|
+
* because only a label some hosted engine logs is a seat this module owns — and
|
|
5936
|
+
* a session that already selects the target route is left alone. Provider and
|
|
5937
|
+
* model are the comparison: a reasoning effort the target carries is not a
|
|
5938
|
+
* reason to log a second event for a session that already selects the same
|
|
5939
|
+
* route, and re-appending would only grow the log on every switch.
|
|
5940
|
+
* @param session - the session whose selection is being written.
|
|
5941
|
+
* @param engine - the engine whose seat the session should select.
|
|
5942
|
+
* @returns whether a `model/selection` event was appended.
|
|
5943
|
+
*/
|
|
5944
|
+
appendSeat(session, engine) {
|
|
5945
|
+
const target = this.seatOf(engine);
|
|
5946
|
+
if (target === void 0) return false;
|
|
5947
|
+
const current = this.currentSelection(session);
|
|
5948
|
+
if (current !== void 0) {
|
|
5949
|
+
if (!isHostedProviderRoute(current.provider)) return false;
|
|
5950
|
+
if (current.provider === target.provider && current.model === target.model) return false;
|
|
5951
|
+
}
|
|
5952
|
+
session.append("model/selection", {
|
|
5953
|
+
provider: target.provider,
|
|
5954
|
+
model: target.model,
|
|
5955
|
+
...target.reasoningEffort === void 0 ? {} : { reasoningEffort: target.reasoningEffort }
|
|
5956
|
+
});
|
|
5957
|
+
return true;
|
|
5958
|
+
}
|
|
5959
|
+
/**
|
|
5960
|
+
* The selection one engine's seat carries: a hosted engine's shared
|
|
5961
|
+
* `external/default`, or — for the harness loop — the deployment's default,
|
|
5962
|
+
* with the substitution {@link defaultSelection} documents.
|
|
5963
|
+
* @param engine - the engine a session runs, or is being moved to.
|
|
5964
|
+
* @returns the selection to write, or undefined when this process cannot name
|
|
5965
|
+
* one for that engine.
|
|
5966
|
+
*/
|
|
5967
|
+
seatOf(engine) {
|
|
5968
|
+
if (engine === "in-process") return this.defaultSelection();
|
|
5969
|
+
return { provider: hostedRouteLabelOf(engine), model: HOSTED_DEFAULT_MODEL };
|
|
5970
|
+
}
|
|
5971
|
+
/**
|
|
5972
|
+
* The selection the host will use for this session, by the host's own read —
|
|
5973
|
+
* the shared read in `driver-core/session-model.ts` (`currentSelection`),
|
|
5974
|
+
* which the four hosted drivers also use to resolve the model they hand over.
|
|
5975
|
+
* @param session - the session whose selection is being read.
|
|
5976
|
+
* @returns the current selection, or undefined when the log records none.
|
|
5977
|
+
*/
|
|
5978
|
+
currentSelection(session) {
|
|
5979
|
+
return currentSelection(session, this.projections());
|
|
5980
|
+
}
|
|
5981
|
+
/**
|
|
5982
|
+
* The selection the deployment's own `agentDefaultModel` service answers, or
|
|
5983
|
+
* undefined when this process cannot read one (reported once).
|
|
5984
|
+
*
|
|
5985
|
+
* Read twice for two different questions — as the selection a session with
|
|
5986
|
+
* nothing in its log would use ({@link selectsHostedRoute}), and as the value
|
|
5987
|
+
* this plugin restores ({@link defaultSelection}) — so the substitution below
|
|
5988
|
+
* never hides the fact that the saved default itself is unusable.
|
|
5989
|
+
* @returns the validated default, or undefined when it cannot be named.
|
|
5990
|
+
*/
|
|
5991
|
+
deploymentDefault() {
|
|
5992
|
+
const service = this.ctx.get("agentDefaultModel");
|
|
5993
|
+
if (service === void 0) return this.unavailable("this deployment composes no agentDefaultModel service");
|
|
5994
|
+
let answered;
|
|
5995
|
+
try {
|
|
5996
|
+
answered = service.currentSelection();
|
|
5997
|
+
} catch (error) {
|
|
5998
|
+
return this.unavailable(`agentDefaultModel.currentSelection() failed: ${String(error)}`);
|
|
5999
|
+
}
|
|
6000
|
+
const named = usableSelection(answered);
|
|
6001
|
+
if (named === void 0) {
|
|
6002
|
+
return this.unavailable("agentDefaultModel.currentSelection() named no usable provider/model");
|
|
6003
|
+
}
|
|
6004
|
+
return named;
|
|
6005
|
+
}
|
|
6006
|
+
/**
|
|
6007
|
+
* The selection this plugin restores: the deployment's default, or — when that
|
|
6008
|
+
* default names a hosted engine ROUTE — the model the deployment's own
|
|
6009
|
+
* composition declares instead.
|
|
6010
|
+
*
|
|
6011
|
+
* The route case is not hypothetical: `session.selectModel` saves whatever the
|
|
6012
|
+
* model menu submitted as the deployment default, and the menu carries the
|
|
6013
|
+
* hosted route as an entry, so one pick made while a hosted session was open
|
|
6014
|
+
* puts a label there that no adapter serves a real model on — handing it to a
|
|
6015
|
+
* session the harness loop drives would fail that session's first request
|
|
6016
|
+
* loud. The composed default is a real model the deployment itself asked for,
|
|
6017
|
+
* so nothing is guessed on its behalf.
|
|
6018
|
+
* @returns the restored selection, or undefined to skip the write.
|
|
6019
|
+
*/
|
|
6020
|
+
defaultSelection() {
|
|
6021
|
+
const named = this.deploymentDefault();
|
|
6022
|
+
if (named === void 0) return void 0;
|
|
6023
|
+
if (!isHostedProviderRoute(named.provider)) return named;
|
|
6024
|
+
const configured = this.configuredDefault();
|
|
6025
|
+
if (configured === void 0) {
|
|
6026
|
+
return this.unavailable(`the deployment default model is the hosted engine route "${named.provider}/${named.model}", and this deployment composes no usable default model to replace it with`);
|
|
6027
|
+
}
|
|
6028
|
+
if (!this.reportedHostedDefault) {
|
|
6029
|
+
this.reportedHostedDefault = true;
|
|
6030
|
+
this.warn(
|
|
6031
|
+
`loop-engine: the deployment default model is the hosted engine route "${named.provider}/${named.model}", which no adapter serves; sessions the in-process engine drives are given the deployment's configured default model "${configured.provider}/${configured.model}" instead \u2014 pick a real model to make that the default again`
|
|
6032
|
+
);
|
|
6033
|
+
}
|
|
6034
|
+
return configured;
|
|
6035
|
+
}
|
|
6036
|
+
/**
|
|
6037
|
+
* The model the DEPLOYMENT's own composition declares as its default, read
|
|
6038
|
+
* from the settings descriptor's `base` layer — the value a saved (user-layer)
|
|
6039
|
+
* default overrode.
|
|
6040
|
+
*
|
|
6041
|
+
* This is what replaces a default that names a hosted route, and it is read
|
|
6042
|
+
* live because the composition is what the deployment itself configured:
|
|
6043
|
+
* `packages/bundle/base/cordis.patch.yml` gives the `agent-default-model` row
|
|
6044
|
+
* a real provider and model, and only the user layer can hold a pick made from
|
|
6045
|
+
* the model menu.
|
|
6046
|
+
* @returns the configured default, or undefined when this deployment composes
|
|
6047
|
+
* no default-model section, cannot enumerate settings, configured no usable
|
|
6048
|
+
* provider/model, or configured a hosted route there too.
|
|
6049
|
+
*/
|
|
6050
|
+
configuredDefault() {
|
|
6051
|
+
const settings = this.ctx.get("settings");
|
|
6052
|
+
if (settings?.describe === void 0) return void 0;
|
|
6053
|
+
const base = settings.describe().find((entry) => entry.ns === AGENT_DEFAULT_MODEL_NS)?.base;
|
|
6054
|
+
const named = usableSelection(base);
|
|
6055
|
+
return named === void 0 || isHostedProviderRoute(named.provider) ? void 0 : named;
|
|
6056
|
+
}
|
|
6057
|
+
/** Read the host session-projection registry, structurally. */
|
|
6058
|
+
projections() {
|
|
6059
|
+
return this.ctx.get("sessionProjections");
|
|
6060
|
+
}
|
|
6061
|
+
/**
|
|
6062
|
+
* Report the one thing this read could not answer, once per plugin lifetime.
|
|
6063
|
+
*
|
|
6064
|
+
* Repeated triggers must not repeat it: a deployment without the service would
|
|
6065
|
+
* otherwise log a line per session built for as long as it runs.
|
|
6066
|
+
* @param problem - what this process cannot read, with the reason.
|
|
6067
|
+
* @returns undefined, so callers can `return this.unavailable(…)` to skip.
|
|
6068
|
+
*/
|
|
6069
|
+
unavailable(problem) {
|
|
6070
|
+
if (this.warned) return void 0;
|
|
6071
|
+
this.warned = true;
|
|
6072
|
+
this.warn(`loop-engine: a session on the in-process engine cannot be given a real model selection: ${problem}; it keeps the selection its log records, which no adapter serves a real model on`);
|
|
6073
|
+
return void 0;
|
|
6074
|
+
}
|
|
6075
|
+
};
|
|
6076
|
+
function usableSelection(answered) {
|
|
6077
|
+
const provider = answered?.provider;
|
|
6078
|
+
const model = answered?.model;
|
|
6079
|
+
if (typeof provider !== "string" || provider.length === 0 || typeof model !== "string" || model.length === 0) {
|
|
6080
|
+
return void 0;
|
|
6081
|
+
}
|
|
6082
|
+
const effort = answered?.reasoningEffort;
|
|
6083
|
+
return {
|
|
6084
|
+
provider,
|
|
6085
|
+
model,
|
|
6086
|
+
...typeof effort !== "string" || effort.length === 0 ? {} : { reasoningEffort: effort }
|
|
6087
|
+
};
|
|
6088
|
+
}
|
|
6089
|
+
|
|
6090
|
+
// src/commands.ts
|
|
6091
|
+
import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
5125
6092
|
import { homedir as homedir2 } from "node:os";
|
|
5126
|
-
import { join as
|
|
6093
|
+
import { join as join6 } from "node:path";
|
|
6094
|
+
import { createUserMessage as createUserMessage5 } from "@deepseek-ai/dsh-llm";
|
|
6095
|
+
var COMMAND_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
6096
|
+
function forwardClaudeCodeCommand(name2) {
|
|
6097
|
+
return (invocation) => {
|
|
6098
|
+
invocation.agent.followup(createUserMessage5({
|
|
6099
|
+
content: [{ type: "text", text: `/${name2}${invocation.rawInput}` }],
|
|
6100
|
+
source: { kind: "user" }
|
|
6101
|
+
}));
|
|
6102
|
+
return { kind: "success" };
|
|
6103
|
+
};
|
|
6104
|
+
}
|
|
6105
|
+
function builtin(name2, description) {
|
|
6106
|
+
return { name: name2, description, handler: forwardClaudeCodeCommand(name2) };
|
|
6107
|
+
}
|
|
6108
|
+
var CLAUDE_CODE_COMMANDS = [
|
|
6109
|
+
builtin("help", "Show help about Claude Code commands"),
|
|
6110
|
+
builtin("compact", "Compact the conversation to reduce context usage"),
|
|
6111
|
+
builtin("clear", "Clear the conversation and start fresh"),
|
|
6112
|
+
builtin("review", "Review recent changes (git diff)")
|
|
6113
|
+
];
|
|
6114
|
+
function discoverUserSlashCommands() {
|
|
6115
|
+
let entries;
|
|
6116
|
+
try {
|
|
6117
|
+
entries = readdirSync(userCommandsDir(), { encoding: "utf8" });
|
|
6118
|
+
} catch {
|
|
6119
|
+
return [];
|
|
6120
|
+
}
|
|
6121
|
+
const definitions = [];
|
|
6122
|
+
const seen = new Set(CLAUDE_CODE_COMMANDS.map((command) => command.name));
|
|
6123
|
+
for (const entry of entries.sort()) {
|
|
6124
|
+
if (!entry.endsWith(".md")) continue;
|
|
6125
|
+
const name2 = entry.slice(0, -".md".length);
|
|
6126
|
+
if (!COMMAND_NAME.test(name2) || seen.has(name2)) continue;
|
|
6127
|
+
const path = join6(userCommandsDir(), entry);
|
|
6128
|
+
let raw;
|
|
6129
|
+
try {
|
|
6130
|
+
raw = readFileSync2(path, "utf8");
|
|
6131
|
+
} catch {
|
|
6132
|
+
continue;
|
|
6133
|
+
}
|
|
6134
|
+
const description = commandDescription(raw);
|
|
6135
|
+
if (description === void 0) continue;
|
|
6136
|
+
seen.add(name2);
|
|
6137
|
+
definitions.push({ name: name2, description, handler: forwardClaudeCodeCommand(name2) });
|
|
6138
|
+
}
|
|
6139
|
+
return definitions;
|
|
6140
|
+
}
|
|
6141
|
+
function userCommandsDir() {
|
|
6142
|
+
return join6(homedir2(), ".claude", "commands");
|
|
6143
|
+
}
|
|
6144
|
+
function commandDescription(raw) {
|
|
6145
|
+
const trimmed = raw.trim();
|
|
6146
|
+
if (trimmed.length === 0) return void 0;
|
|
6147
|
+
let body = trimmed;
|
|
6148
|
+
if (trimmed.startsWith("---\n")) {
|
|
6149
|
+
const closing = trimmed.indexOf("\n---");
|
|
6150
|
+
if (closing <= 0) return void 0;
|
|
6151
|
+
for (const line of trimmed.slice(4, closing).split("\n")) {
|
|
6152
|
+
const colon = line.indexOf(":");
|
|
6153
|
+
if (colon < 0 || line.slice(0, colon).trim() !== "description") continue;
|
|
6154
|
+
const value = line.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
|
|
6155
|
+
if (value.length > 0) return value;
|
|
6156
|
+
}
|
|
6157
|
+
body = trimmed.slice(closing + 4);
|
|
6158
|
+
}
|
|
6159
|
+
for (const line of body.split("\n")) {
|
|
6160
|
+
const candidate = line.trim();
|
|
6161
|
+
if (candidate.length === 0 || candidate.startsWith("#")) continue;
|
|
6162
|
+
return candidate.length > 120 ? `${candidate.slice(0, 119)}\u2026` : candidate;
|
|
6163
|
+
}
|
|
6164
|
+
return void 0;
|
|
6165
|
+
}
|
|
6166
|
+
|
|
6167
|
+
// src/engine-kimi/commands.ts
|
|
6168
|
+
import { createUserMessage as createUserMessage6 } from "@deepseek-ai/dsh-llm";
|
|
6169
|
+
function forwardKimiCommand(name2) {
|
|
6170
|
+
return (invocation) => {
|
|
6171
|
+
invocation.agent.followup(createUserMessage6({
|
|
6172
|
+
content: [{ type: "text", text: `/${name2}${invocation.rawInput}` }],
|
|
6173
|
+
source: { kind: "user" }
|
|
6174
|
+
}));
|
|
6175
|
+
return { kind: "success" };
|
|
6176
|
+
};
|
|
6177
|
+
}
|
|
6178
|
+
function builtin2(name2, description) {
|
|
6179
|
+
return { name: name2, description, handler: forwardKimiCommand(name2) };
|
|
6180
|
+
}
|
|
6181
|
+
var KIMI_COMMANDS = [
|
|
6182
|
+
builtin2("compact", "Compact the conversation context"),
|
|
6183
|
+
builtin2("status", "Show current session status"),
|
|
6184
|
+
builtin2("usage", "Show session token usage"),
|
|
6185
|
+
builtin2("mcp", "Show MCP server status"),
|
|
6186
|
+
builtin2("tasks", "List background tasks"),
|
|
6187
|
+
builtin2("help", "Show available ACP commands")
|
|
6188
|
+
];
|
|
6189
|
+
|
|
6190
|
+
// src/skills.ts
|
|
6191
|
+
import { readFile as readFile2, readdir, stat } from "node:fs/promises";
|
|
6192
|
+
import { homedir as homedir3 } from "node:os";
|
|
6193
|
+
import { join as join7, resolve } from "node:path";
|
|
5127
6194
|
var SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
5128
6195
|
var PROVIDER_NAME = "claude-code";
|
|
5129
6196
|
var CLAUDE_CODE_RANK = 150;
|
|
@@ -5190,8 +6257,8 @@ function findClosingFrontmatter(raw, start) {
|
|
|
5190
6257
|
function parseSkillFile(raw) {
|
|
5191
6258
|
const parsed = parseFrontmatter(raw);
|
|
5192
6259
|
if (parsed === void 0) return void 0;
|
|
5193
|
-
const name2 =
|
|
5194
|
-
const description =
|
|
6260
|
+
const name2 = stringField2(parsed.data, "name");
|
|
6261
|
+
const description = stringField2(parsed.data, "description");
|
|
5195
6262
|
if (name2 === void 0 || description === void 0 || !SKILL_NAME.test(name2)) return void 0;
|
|
5196
6263
|
const disableModelInvocation = booleanField(parsed.data, "disable-model-invocation");
|
|
5197
6264
|
const userInvocable = booleanField(parsed.data, "user-invocable");
|
|
@@ -5207,7 +6274,7 @@ function parseSkillFile(raw) {
|
|
|
5207
6274
|
content: parsed.body.trim()
|
|
5208
6275
|
};
|
|
5209
6276
|
}
|
|
5210
|
-
function
|
|
6277
|
+
function stringField2(data, key) {
|
|
5211
6278
|
const value = data[key];
|
|
5212
6279
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
5213
6280
|
}
|
|
@@ -5233,17 +6300,17 @@ var ClaudeCodeSkillProvider = class {
|
|
|
5233
6300
|
const cwd = options.cwd;
|
|
5234
6301
|
if (cwd !== void 0) {
|
|
5235
6302
|
const projectRoot = await findProjectRoot(resolve(cwd));
|
|
5236
|
-
await collectSkillsDir(
|
|
6303
|
+
await collectSkillsDir(join7(projectRoot, ".claude", "skills"), CLAUDE_CODE_RANK, candidates);
|
|
5237
6304
|
await collectClaudeMd(projectRoot, candidates);
|
|
5238
6305
|
}
|
|
5239
|
-
await collectSkillsDir(
|
|
6306
|
+
await collectSkillsDir(join7(homedir3(), ".claude", "skills"), CLAUDE_CODE_USER_RANK, candidates);
|
|
5240
6307
|
if (this.control.signal.aborted) return [];
|
|
5241
6308
|
return candidates;
|
|
5242
6309
|
}
|
|
5243
6310
|
async get(candidate, _options) {
|
|
5244
6311
|
const locator = candidate.locator;
|
|
5245
6312
|
try {
|
|
5246
|
-
const raw = await
|
|
6313
|
+
const raw = await readFile2(locator.path, { encoding: "utf8" });
|
|
5247
6314
|
const parsed = parseSkillFile(raw);
|
|
5248
6315
|
if (parsed === void 0) return void 0;
|
|
5249
6316
|
return {
|
|
@@ -5270,11 +6337,11 @@ async function collectSkillsDir(skillsDir, rank, candidates) {
|
|
|
5270
6337
|
return;
|
|
5271
6338
|
}
|
|
5272
6339
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
5273
|
-
const entryPath =
|
|
6340
|
+
const entryPath = join7(skillsDir, entry.name);
|
|
5274
6341
|
const info = await stat(entryPath).catch(() => void 0);
|
|
5275
6342
|
if (info === void 0) continue;
|
|
5276
6343
|
if (info.isDirectory()) {
|
|
5277
|
-
const path =
|
|
6344
|
+
const path = join7(entryPath, "SKILL.md");
|
|
5278
6345
|
const skill2 = await tryParseSkill(path);
|
|
5279
6346
|
if (skill2 === void 0) continue;
|
|
5280
6347
|
candidates.push(toCandidate(skill2, path, rank, entryPath));
|
|
@@ -5287,7 +6354,7 @@ async function collectSkillsDir(skillsDir, rank, candidates) {
|
|
|
5287
6354
|
}
|
|
5288
6355
|
}
|
|
5289
6356
|
async function collectClaudeMd(projectRoot, candidates) {
|
|
5290
|
-
const claudeMd =
|
|
6357
|
+
const claudeMd = join7(projectRoot, "CLAUDE.md");
|
|
5291
6358
|
try {
|
|
5292
6359
|
const info = await stat(claudeMd);
|
|
5293
6360
|
if (!info.isFile()) return;
|
|
@@ -5314,7 +6381,7 @@ function toCandidate(skill, path, rank, resourceBaseDir) {
|
|
|
5314
6381
|
}
|
|
5315
6382
|
async function tryParseSkill(path) {
|
|
5316
6383
|
try {
|
|
5317
|
-
const raw = await
|
|
6384
|
+
const raw = await readFile2(path, { encoding: "utf8" });
|
|
5318
6385
|
return parseSkillFile(raw);
|
|
5319
6386
|
} catch {
|
|
5320
6387
|
return void 0;
|
|
@@ -5324,7 +6391,7 @@ async function findProjectRoot(cwd) {
|
|
|
5324
6391
|
let current = cwd;
|
|
5325
6392
|
while (true) {
|
|
5326
6393
|
try {
|
|
5327
|
-
await stat(
|
|
6394
|
+
await stat(join7(current, ".git"));
|
|
5328
6395
|
return current;
|
|
5329
6396
|
} catch {
|
|
5330
6397
|
}
|
|
@@ -5334,7 +6401,17 @@ async function findProjectRoot(cwd) {
|
|
|
5334
6401
|
}
|
|
5335
6402
|
}
|
|
5336
6403
|
|
|
6404
|
+
// src/engine-codex/skills.ts
|
|
6405
|
+
import { homedir as homedir4 } from "node:os";
|
|
6406
|
+
import { join as join10 } from "node:path";
|
|
6407
|
+
|
|
6408
|
+
// src/driver-core/agents-md-skill-provider.ts
|
|
6409
|
+
import { readdir as readdir2, readFile as readFile4, stat as stat3 } from "node:fs/promises";
|
|
6410
|
+
import { dirname as dirname4, join as join9 } from "node:path";
|
|
6411
|
+
|
|
5337
6412
|
// src/driver-core/context-files.ts
|
|
6413
|
+
import { readFile as readFile3, stat as stat2 } from "node:fs/promises";
|
|
6414
|
+
import { join as join8, resolve as resolve2 } from "node:path";
|
|
5338
6415
|
async function projectAncestors(cwd) {
|
|
5339
6416
|
const root = await findProjectRoot(resolve2(cwd));
|
|
5340
6417
|
const dirs = [];
|
|
@@ -5355,11 +6432,11 @@ async function collectProjectContextFiles(cwd, policy) {
|
|
|
5355
6432
|
}
|
|
5356
6433
|
async function dirContextFile(dir, policy) {
|
|
5357
6434
|
if (policy.override !== void 0) {
|
|
5358
|
-
const override =
|
|
6435
|
+
const override = join8(dir, policy.override);
|
|
5359
6436
|
if (await pathExists(override)) return override;
|
|
5360
6437
|
}
|
|
5361
6438
|
for (const name2 of policy.primary) {
|
|
5362
|
-
const candidate =
|
|
6439
|
+
const candidate = join8(dir, name2);
|
|
5363
6440
|
if (await pathExists(candidate)) return candidate;
|
|
5364
6441
|
}
|
|
5365
6442
|
return void 0;
|
|
@@ -5374,7 +6451,7 @@ async function pathExists(path) {
|
|
|
5374
6451
|
}
|
|
5375
6452
|
async function readOptionalFile(path) {
|
|
5376
6453
|
try {
|
|
5377
|
-
return await
|
|
6454
|
+
return await readFile3(path, { encoding: "utf8" });
|
|
5378
6455
|
} catch {
|
|
5379
6456
|
return void 0;
|
|
5380
6457
|
}
|
|
@@ -5419,18 +6496,18 @@ var AgentsMdSkillProvider = class {
|
|
|
5419
6496
|
if (this.spec.skills !== void 0) {
|
|
5420
6497
|
const catalog = this.spec.skills;
|
|
5421
6498
|
for (const dir of await projectAncestors(cwd)) {
|
|
5422
|
-
await this.collectSkillsDir(
|
|
6499
|
+
await this.collectSkillsDir(join9(dir, ...catalog.project), catalog.projectRank, candidates);
|
|
5423
6500
|
}
|
|
5424
6501
|
}
|
|
5425
6502
|
}
|
|
5426
6503
|
const userDir = this.spec.userDir();
|
|
5427
6504
|
const userContext = this.spec.userContext;
|
|
5428
6505
|
if (userContext !== void 0) {
|
|
5429
|
-
const userFile =
|
|
6506
|
+
const userFile = join9(userDir, userContext.file);
|
|
5430
6507
|
if (await fileNonEmpty(userFile)) candidates.push(this.agentsCandidate([userFile], userContext.rank));
|
|
5431
6508
|
}
|
|
5432
6509
|
if (this.spec.skills !== void 0) {
|
|
5433
|
-
await this.collectSkillsDir(
|
|
6510
|
+
await this.collectSkillsDir(join9(userDir, this.spec.skills.userDir), this.spec.skills.userRank, candidates);
|
|
5434
6511
|
}
|
|
5435
6512
|
if (this.control.signal.aborted) return [];
|
|
5436
6513
|
return candidates;
|
|
@@ -5449,7 +6526,7 @@ var AgentsMdSkillProvider = class {
|
|
|
5449
6526
|
provider: this.name,
|
|
5450
6527
|
content: parsed.content,
|
|
5451
6528
|
path: locator.path,
|
|
5452
|
-
resourceBase: { kind: "directory", path:
|
|
6529
|
+
resourceBase: { kind: "directory", path: dirname4(locator.path) }
|
|
5453
6530
|
};
|
|
5454
6531
|
}
|
|
5455
6532
|
const content = await readSources(locator.paths);
|
|
@@ -5490,11 +6567,11 @@ var AgentsMdSkillProvider = class {
|
|
|
5490
6567
|
return;
|
|
5491
6568
|
}
|
|
5492
6569
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
5493
|
-
const entryPath =
|
|
6570
|
+
const entryPath = join9(skillsDir, entry.name);
|
|
5494
6571
|
const info = await stat3(entryPath).catch(() => void 0);
|
|
5495
6572
|
if (info === void 0) continue;
|
|
5496
6573
|
if (info.isDirectory()) {
|
|
5497
|
-
const path =
|
|
6574
|
+
const path = join9(entryPath, "SKILL.md");
|
|
5498
6575
|
const parsed2 = await this.tryParse(path);
|
|
5499
6576
|
if (parsed2 === void 0) continue;
|
|
5500
6577
|
candidates.push(this.skillCandidate(parsed2, path, rank, entryPath));
|
|
@@ -5521,18 +6598,74 @@ var AgentsMdSkillProvider = class {
|
|
|
5521
6598
|
resourceBase: { kind: "directory", path: resourceDir }
|
|
5522
6599
|
};
|
|
5523
6600
|
}
|
|
5524
|
-
/** Parse one SKILL.md file, or `undefined` when it is unreadable or invalid. */
|
|
5525
|
-
async tryParse(path) {
|
|
5526
|
-
try {
|
|
5527
|
-
const raw = await
|
|
5528
|
-
return parseSkillFile(raw);
|
|
5529
|
-
} catch {
|
|
5530
|
-
return void 0;
|
|
5531
|
-
}
|
|
6601
|
+
/** Parse one SKILL.md file, or `undefined` when it is unreadable or invalid. */
|
|
6602
|
+
async tryParse(path) {
|
|
6603
|
+
try {
|
|
6604
|
+
const raw = await readFile4(path, { encoding: "utf8" });
|
|
6605
|
+
return parseSkillFile(raw);
|
|
6606
|
+
} catch {
|
|
6607
|
+
return void 0;
|
|
6608
|
+
}
|
|
6609
|
+
}
|
|
6610
|
+
};
|
|
6611
|
+
|
|
6612
|
+
// src/engine-codex/skills.ts
|
|
6613
|
+
var CODEX_PROJECT_RANK = 140;
|
|
6614
|
+
var CODEX_USER_RANK = 160;
|
|
6615
|
+
var CODEX_CONTEXT_POLICY = { primary: ["AGENTS.md"] };
|
|
6616
|
+
var CODEX_SPEC = {
|
|
6617
|
+
name: "codex",
|
|
6618
|
+
agentsMdDescription: "Codex project/user instructions (AGENTS.md)",
|
|
6619
|
+
contextPolicy: CODEX_CONTEXT_POLICY,
|
|
6620
|
+
userDir: () => join10(homedir4(), ".codex"),
|
|
6621
|
+
projectRank: CODEX_PROJECT_RANK,
|
|
6622
|
+
userContext: { file: "AGENTS.md", rank: CODEX_USER_RANK }
|
|
6623
|
+
};
|
|
6624
|
+
var CodexSkillProvider = class extends AgentsMdSkillProvider {
|
|
6625
|
+
constructor(control) {
|
|
6626
|
+
super(CODEX_SPEC, control);
|
|
6627
|
+
}
|
|
6628
|
+
};
|
|
6629
|
+
|
|
6630
|
+
// src/engine-pi/skills.ts
|
|
6631
|
+
import { homedir as homedir5 } from "node:os";
|
|
6632
|
+
import { join as join11, resolve as resolve3 } from "node:path";
|
|
6633
|
+
var PI_AGENTS_PROJECT_RANK = 140;
|
|
6634
|
+
var PI_SKILL_PROJECT_RANK = 150;
|
|
6635
|
+
var PI_AGENTS_USER_RANK = 160;
|
|
6636
|
+
var PI_SKILL_USER_RANK = 170;
|
|
6637
|
+
var PI_CONTEXT_POLICY = {
|
|
6638
|
+
override: "AGENTS.override.md",
|
|
6639
|
+
primary: ["AGENTS.md", "CLAUDE.md"]
|
|
6640
|
+
};
|
|
6641
|
+
function piAgentDir2() {
|
|
6642
|
+
const override = process.env.PI_CODING_AGENT_DIR;
|
|
6643
|
+
if (override !== void 0 && override.length > 0) return resolve3(override);
|
|
6644
|
+
return join11(homedir5(), ".pi", "agent");
|
|
6645
|
+
}
|
|
6646
|
+
var PI_SPEC = {
|
|
6647
|
+
name: "pi",
|
|
6648
|
+
agentsMdDescription: "Pi project/user instructions (AGENTS.md / CLAUDE.md)",
|
|
6649
|
+
contextPolicy: PI_CONTEXT_POLICY,
|
|
6650
|
+
userDir: piAgentDir2,
|
|
6651
|
+
projectRank: PI_AGENTS_PROJECT_RANK,
|
|
6652
|
+
userContext: { file: "AGENTS.md", rank: PI_AGENTS_USER_RANK },
|
|
6653
|
+
skills: {
|
|
6654
|
+
project: [".pi", "skills"],
|
|
6655
|
+
projectRank: PI_SKILL_PROJECT_RANK,
|
|
6656
|
+
userDir: "skills",
|
|
6657
|
+
userRank: PI_SKILL_USER_RANK
|
|
6658
|
+
}
|
|
6659
|
+
};
|
|
6660
|
+
var PiSkillProvider = class extends AgentsMdSkillProvider {
|
|
6661
|
+
constructor(control) {
|
|
6662
|
+
super(PI_SPEC, control);
|
|
5532
6663
|
}
|
|
5533
6664
|
};
|
|
5534
6665
|
|
|
5535
6666
|
// src/engine-kimi/skills.ts
|
|
6667
|
+
import { homedir as homedir6 } from "node:os";
|
|
6668
|
+
import { join as join12, resolve as resolve4 } from "node:path";
|
|
5536
6669
|
var KIMI_AGENTS_PROJECT_RANK = 140;
|
|
5537
6670
|
var KIMI_SKILL_PROJECT_RANK = 150;
|
|
5538
6671
|
var KIMI_SKILL_USER_RANK = 160;
|
|
@@ -5541,8 +6674,8 @@ var KIMI_CONTEXT_POLICY = {
|
|
|
5541
6674
|
};
|
|
5542
6675
|
function kimiAgentDir() {
|
|
5543
6676
|
const override = process.env.KIMI_CODE_HOME;
|
|
5544
|
-
if (override !== void 0 && override.length > 0) return
|
|
5545
|
-
return
|
|
6677
|
+
if (override !== void 0 && override.length > 0) return resolve4(override);
|
|
6678
|
+
return join12(homedir6(), ".kimi-code");
|
|
5546
6679
|
}
|
|
5547
6680
|
var KIMI_SPEC = {
|
|
5548
6681
|
name: "kimi",
|
|
@@ -5563,390 +6696,635 @@ var KimiSkillProvider = class extends AgentsMdSkillProvider {
|
|
|
5563
6696
|
}
|
|
5564
6697
|
};
|
|
5565
6698
|
|
|
5566
|
-
// src/engine-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
import z5 from "@deepseek-ai/schemastery";
|
|
5594
|
-
|
|
5595
|
-
// src/namespace.ts
|
|
5596
|
-
var LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
|
|
5597
|
-
|
|
5598
|
-
// src/settings.ts
|
|
5599
|
-
var LOOP_ENGINE_IDS = ["in-process", "claude-code", "codex", "pi", "kimi"];
|
|
5600
|
-
var LOOP_ENGINE_SETTINGS_SCHEMA = z5.object({
|
|
5601
|
-
engine: z5.union(LOOP_ENGINE_IDS.map((id) => z5.const(id))).default("in-process"),
|
|
5602
|
-
showInComposer: z5.boolean().default(true)
|
|
5603
|
-
});
|
|
5604
|
-
function loopEngineSettingsNamespace() {
|
|
5605
|
-
return LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL;
|
|
5606
|
-
}
|
|
5607
|
-
|
|
5608
|
-
// src/patch-manager.ts
|
|
5609
|
-
var MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block: ";
|
|
5610
|
-
var MANAGED_BLOCK_END = "# -- /dsh-loop-engine managed block --";
|
|
5611
|
-
var END_MARKER_LINE = `${MANAGED_BLOCK_END}
|
|
5612
|
-
`;
|
|
5613
|
-
function renderManagedBlock(engine) {
|
|
5614
|
-
if (engine === "in-process") return "";
|
|
5615
|
-
return [
|
|
5616
|
-
`${MANAGED_BLOCK_BEGIN}${engine} --`,
|
|
5617
|
-
"- id: agent-loop",
|
|
5618
|
-
" disabled: true",
|
|
5619
|
-
"- id: command-goal",
|
|
5620
|
-
" disabled: true",
|
|
5621
|
-
END_MARKER_LINE
|
|
5622
|
-
].join("\n");
|
|
5623
|
-
}
|
|
5624
|
-
function hasManagedBlock(text) {
|
|
5625
|
-
return text.includes(MANAGED_BLOCK_BEGIN);
|
|
5626
|
-
}
|
|
5627
|
-
var BEGIN_MARKER_RE = /^# -- dsh-loop-engine managed block: (\S+) --$/m;
|
|
5628
|
-
function managedBlockEngineOf(text) {
|
|
5629
|
-
const engine = BEGIN_MARKER_RE.exec(text)?.[1];
|
|
5630
|
-
return LOOP_ENGINE_IDS.includes(engine ?? "") ? engine : void 0;
|
|
5631
|
-
}
|
|
5632
|
-
function currentEngineOf(text) {
|
|
5633
|
-
return managedBlockEngineOf(text) ?? "in-process";
|
|
5634
|
-
}
|
|
5635
|
-
function managedSpan(text) {
|
|
5636
|
-
const begin = text.indexOf(MANAGED_BLOCK_BEGIN);
|
|
5637
|
-
if (begin === -1) return { head: text, tail: "", present: false, blankBefore: false };
|
|
5638
|
-
const afterBegin = begin + MANAGED_BLOCK_BEGIN.length;
|
|
5639
|
-
const endAt = text.indexOf(MANAGED_BLOCK_END, afterBegin);
|
|
5640
|
-
const spanEnd = endAt === -1 ? text.length : endAt + END_MARKER_LINE.length;
|
|
5641
|
-
const before = text.slice(0, begin);
|
|
5642
|
-
const blankBefore = before.endsWith("\n\n");
|
|
5643
|
-
return {
|
|
5644
|
-
head: blankBefore ? before.slice(0, -1) : before,
|
|
5645
|
-
tail: text.slice(spanEnd),
|
|
5646
|
-
present: true,
|
|
5647
|
-
blankBefore
|
|
5648
|
-
};
|
|
5649
|
-
}
|
|
5650
|
-
function ensureTrailingNewline(text) {
|
|
5651
|
-
return text.endsWith("\n") ? text : `${text}
|
|
5652
|
-
`;
|
|
5653
|
-
}
|
|
5654
|
-
function hasRootEntry(text) {
|
|
5655
|
-
return /^(?:- |\[)/m.test(text);
|
|
5656
|
-
}
|
|
5657
|
-
function dropSeedPlaceholder(text) {
|
|
5658
|
-
return text.replace(/^\[\]\n/m, "");
|
|
5659
|
-
}
|
|
5660
|
-
function seedEmptyArray(text) {
|
|
5661
|
-
const head = text.replace(/\n+$/, "");
|
|
5662
|
-
return head === "" ? "[]\n" : `${head}
|
|
5663
|
-
[]
|
|
5664
|
-
`;
|
|
5665
|
-
}
|
|
5666
|
-
function applyManagedBlock(text, engine) {
|
|
5667
|
-
const block = renderManagedBlock(engine);
|
|
5668
|
-
const span = managedSpan(text);
|
|
5669
|
-
let result;
|
|
5670
|
-
if (!span.present) {
|
|
5671
|
-
if (block === "") {
|
|
5672
|
-
result = text;
|
|
5673
|
-
} else {
|
|
5674
|
-
const base = ensureTrailingNewline(text);
|
|
5675
|
-
result = `${base}
|
|
5676
|
-
${block}`;
|
|
6699
|
+
// src/engine-surface.ts
|
|
6700
|
+
var SURFACES = {
|
|
6701
|
+
"claude-code": {
|
|
6702
|
+
commands: () => [...CLAUDE_CODE_COMMANDS, ...discoverUserSlashCommands()],
|
|
6703
|
+
skills: (control) => new ClaudeCodeSkillProvider(control)
|
|
6704
|
+
},
|
|
6705
|
+
codex: {
|
|
6706
|
+
skills: (control) => new CodexSkillProvider(control)
|
|
6707
|
+
},
|
|
6708
|
+
pi: {
|
|
6709
|
+
skills: (control) => new PiSkillProvider(control)
|
|
6710
|
+
},
|
|
6711
|
+
kimi: {
|
|
6712
|
+
commands: () => KIMI_COMMANDS,
|
|
6713
|
+
skills: (control) => new KimiSkillProvider(control)
|
|
6714
|
+
}
|
|
6715
|
+
};
|
|
6716
|
+
function registerEngineSurface(agent, engine, warn) {
|
|
6717
|
+
const surface = SURFACES[engine];
|
|
6718
|
+
const commands = agent.ctx.get("commands");
|
|
6719
|
+
if (commands !== void 0 && surface.commands !== void 0) {
|
|
6720
|
+
for (const command of surface.commands()) {
|
|
6721
|
+
try {
|
|
6722
|
+
commands.register(command);
|
|
6723
|
+
} catch (error) {
|
|
6724
|
+
warn(`loop-engine: skip ${engine} command /${command.name}: ${String(error)}`);
|
|
6725
|
+
}
|
|
5677
6726
|
}
|
|
5678
|
-
} else if (block === "") {
|
|
5679
|
-
result = span.tail.startsWith("\n") ? `${span.head}${span.tail.slice(1)}` : `${span.head}${span.tail}`;
|
|
5680
|
-
} else {
|
|
5681
|
-
result = `${span.head}${span.blankBefore ? "\n" : ""}${block}${span.tail}`;
|
|
5682
6727
|
}
|
|
5683
|
-
|
|
5684
|
-
if (
|
|
5685
|
-
|
|
6728
|
+
const skills = agent.ctx.get("skills");
|
|
6729
|
+
if (skills !== void 0 && surface.skills !== void 0) {
|
|
6730
|
+
skills.registerProvider(surface.skills);
|
|
6731
|
+
}
|
|
5686
6732
|
}
|
|
5687
6733
|
|
|
5688
|
-
// src/
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
6734
|
+
// src/router-loop.ts
|
|
6735
|
+
var ROUTER_SERVICES = [...AgentLoop.inject];
|
|
6736
|
+
var RouterLoop = class extends AgentLoop {
|
|
6737
|
+
engines = /* @__PURE__ */ new Map();
|
|
6738
|
+
live = /* @__PURE__ */ new Map();
|
|
6739
|
+
build;
|
|
6740
|
+
records;
|
|
6741
|
+
warn;
|
|
6742
|
+
/**
|
|
6743
|
+
* The model-selection half of routing: the seat a session's engine owns, moved
|
|
6744
|
+
* at a switch ({@link RouterLoop.moveModelSelection}) and written at a build
|
|
6745
|
+
* ({@link RouterLoop.engineOptions}). Owned here because each warning it owes
|
|
6746
|
+
* is owed once per process, not once per session.
|
|
6747
|
+
*/
|
|
6748
|
+
selectionReset;
|
|
6749
|
+
/**
|
|
6750
|
+
* @param ctx - the context the router's effects belong to.
|
|
6751
|
+
* @param build - memoized builder for one hosted engine's runtime.
|
|
6752
|
+
* @param records - the plugin's own per-session engine record, which outranks
|
|
6753
|
+
* every preset-derived answer and is what {@link selectEngine} writes.
|
|
6754
|
+
* @param warn - diagnostic sink for a skipped engine command.
|
|
6755
|
+
*/
|
|
6756
|
+
constructor(ctx, build, records, warn) {
|
|
6757
|
+
super(ctx, { agents: [] });
|
|
6758
|
+
this.build = build;
|
|
6759
|
+
this.records = records;
|
|
6760
|
+
this.warn = warn;
|
|
6761
|
+
this.selectionReset = new ModelSelectionReset(ctx, warn);
|
|
6762
|
+
this.rebuildOnEngineChange();
|
|
6763
|
+
}
|
|
6764
|
+
/** The engine that drives one session's agent. */
|
|
6765
|
+
async engineFor(presetId, parent) {
|
|
6766
|
+
const named = engineOfPreset(presetId);
|
|
6767
|
+
if (named !== void 0) return named;
|
|
6768
|
+
if (parent !== void 0) {
|
|
6769
|
+
const inherited = hostedEngineOf(await engineOfSession(this.ctx, parent.id, this.records));
|
|
6770
|
+
if (inherited !== void 0) return inherited;
|
|
6771
|
+
}
|
|
6772
|
+
return "in-process";
|
|
6773
|
+
}
|
|
6774
|
+
/** The engine's runtime, built on first use and kept for the plugin's lifetime. */
|
|
6775
|
+
runtimeOf(engine) {
|
|
6776
|
+
const existing = this.engines.get(engine);
|
|
6777
|
+
if (existing !== void 0) return existing;
|
|
6778
|
+
const created = this.build(engine);
|
|
6779
|
+
this.engines.set(engine, created);
|
|
6780
|
+
return created;
|
|
5714
6781
|
}
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
6782
|
+
/**
|
|
6783
|
+
* Record one published agent so a later engine change can rebuild it, and
|
|
6784
|
+
* bridge the engine's command/skill surface into its session.
|
|
6785
|
+
* @param engine - the engine that built the handle.
|
|
6786
|
+
* @param handle - the published handle.
|
|
6787
|
+
* @param recipe - how it was built, for a hot swap's successor.
|
|
6788
|
+
* @param handover - a hosted handle's session handover; absent for the
|
|
6789
|
+
* harness loop, whose handle carries neither the session's lifetime nor a
|
|
6790
|
+
* way to retire the machine without releasing it.
|
|
6791
|
+
* @returns the handle the harness tracks.
|
|
6792
|
+
*/
|
|
6793
|
+
adopt(engine, handle, recipe, handover) {
|
|
6794
|
+
if (engine !== "in-process") registerEngineSurface(handle.agent, engine, this.warn);
|
|
6795
|
+
const entry = {
|
|
6796
|
+
engine,
|
|
6797
|
+
agent: handle.agent,
|
|
6798
|
+
dispose: () => handle.dispose(),
|
|
6799
|
+
recipe,
|
|
6800
|
+
...handover === void 0 ? {} : { handover }
|
|
6801
|
+
};
|
|
6802
|
+
this.live.set(handle.agent.id, entry);
|
|
6803
|
+
return {
|
|
6804
|
+
agent: handle.agent,
|
|
6805
|
+
dispose: async () => {
|
|
6806
|
+
this.forget(entry);
|
|
6807
|
+
await handle.dispose();
|
|
6808
|
+
}
|
|
6809
|
+
};
|
|
6810
|
+
}
|
|
6811
|
+
/** Drop one tracked session, if the record still points at that exact agent. */
|
|
6812
|
+
forget(entry) {
|
|
6813
|
+
if (this.live.get(entry.agent.id) === entry) this.live.delete(entry.agent.id);
|
|
6814
|
+
}
|
|
6815
|
+
/**
|
|
6816
|
+
* Create a session's agent on the engine it runs.
|
|
6817
|
+
*
|
|
6818
|
+
* The plugin's own record is consulted FIRST, ahead of the preset the caller
|
|
6819
|
+
* composed with: a session that already has a record must be built on the
|
|
6820
|
+
* engine the plugin reports for it, or the router and the Remote would answer
|
|
6821
|
+
* differently about one session. Only a session with no record — every session
|
|
6822
|
+
* this plugin has never switched, including every brand-new one — falls
|
|
6823
|
+
* through to the preset.
|
|
6824
|
+
* @param ownerCtx - caller context that owns the lifecycle.
|
|
6825
|
+
* @param options - identities, metadata (carrying the preset), and setup.
|
|
6826
|
+
* @returns the published handle.
|
|
6827
|
+
*/
|
|
6828
|
+
async createAgent(ownerCtx, options) {
|
|
6829
|
+
const recipe = recipeOf(ownerCtx, options);
|
|
6830
|
+
const engine = this.records.engineOf(options.sessionId) ?? await this.engineFor(options.meta?.agentPreset, options.parentAgent);
|
|
6831
|
+
if (engine === "in-process") {
|
|
6832
|
+
return this.adopt(engine, await super.createAgent(ownerCtx, this.engineOptions(options, engine)), recipe);
|
|
5727
6833
|
}
|
|
5728
|
-
|
|
5729
|
-
|
|
6834
|
+
const handle = await this.runtimeOf(engine).createAgent(ownerCtx, this.engineOptions(options, engine));
|
|
6835
|
+
return this.adopt(engine, handle, recipe, handoverOf(handle));
|
|
5730
6836
|
}
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
6837
|
+
/**
|
|
6838
|
+
* Resume a persisted session on the engine it runs.
|
|
6839
|
+
*
|
|
6840
|
+
* The read is {@link engineOfSession}: the plugin's own record first, then the
|
|
6841
|
+
* durable `agentPreset` projection, which folds the session header together
|
|
6842
|
+
* with every committed `agent-preset/selected`. The session header alone would
|
|
6843
|
+
* name the preset the session was CREATED with, which is exactly the wrong
|
|
6844
|
+
* answer for a session that switched engine while it was blank — and the
|
|
6845
|
+
* record is the only answer for one that switched after it started, which no
|
|
6846
|
+
* preset can express.
|
|
6847
|
+
*
|
|
6848
|
+
* A resume is not a create: the session's seat is one a previous build already
|
|
6849
|
+
* put in its log (`model-selection-reset.ts`, the switch and build triggers), so
|
|
6850
|
+
* a hosted engine's resume leaves the selection exactly as it finds it. Only
|
|
6851
|
+
* the harness loop's own resume is wrapped, with the narrower trigger that
|
|
6852
|
+
* applies to it — it makes real model calls, so it must not be left selecting a
|
|
6853
|
+
* placeholder route.
|
|
6854
|
+
* @param ownerCtx - caller context that owns load, setup, and the lifecycle.
|
|
6855
|
+
* @param options - persisted identity, loop options, and setup.
|
|
6856
|
+
* @returns the published handle.
|
|
6857
|
+
*/
|
|
6858
|
+
async resume(ownerCtx, options) {
|
|
6859
|
+
const recipe = recipeOf(ownerCtx, options);
|
|
6860
|
+
const engine = hostedEngineOf(
|
|
6861
|
+
await engineOfSession(this.ctx, options.resumeSessionId, this.records)
|
|
6862
|
+
) ?? await this.engineFor(void 0, options.parentAgent);
|
|
6863
|
+
if (engine === "in-process") {
|
|
6864
|
+
return this.adopt(engine, await super.resume(ownerCtx, this.engineOptions(options, engine)), recipe);
|
|
6865
|
+
}
|
|
6866
|
+
const handle = await this.runtimeOf(engine).resume(ownerCtx, options);
|
|
6867
|
+
return this.adopt(engine, handle, recipe, handoverOf(handle));
|
|
5739
6868
|
}
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
6869
|
+
/**
|
|
6870
|
+
* Wrap the caller's setup so the session's ENGINE gets its model seat written
|
|
6871
|
+
* before the host installs the session's selection, and hand a hosted engine
|
|
6872
|
+
* the `agentOptions` that seat names.
|
|
6873
|
+
*
|
|
6874
|
+
* The wrap is what makes the write land at all: the host installs the session's
|
|
6875
|
+
* model selection from INSIDE the caller's own setup
|
|
6876
|
+
* (`ApiSessionAgentController.selectionFor`), and that read takes the log's
|
|
6877
|
+
* pending `model/selection` — so the write has to happen before it, and the
|
|
6878
|
+
* caller's setup has to run unchanged afterwards. Both of the router's engines
|
|
6879
|
+
* are wrapped, each with its own trigger: the harness loop's build keeps the
|
|
6880
|
+
* narrow one (only a session that would otherwise select a placeholder route),
|
|
6881
|
+
* while a hosted engine's build is where a NEW session gets its seat. The
|
|
6882
|
+
* resume path calls this with `in-process` alone (see {@link resume}).
|
|
6883
|
+
*
|
|
6884
|
+
* The `agentOptions` half keeps the two answers consistent: a hosted engine is
|
|
6885
|
+
* handed the very selection written into the session (`external/default`), not
|
|
6886
|
+
* the deployment default that seat replaced. In-process keeps the caller's own
|
|
6887
|
+
* options, which ARE its route.
|
|
6888
|
+
*
|
|
6889
|
+
* The caller's setup is copied, never mutated: the same options object is the
|
|
6890
|
+
* router's rebuild recipe, which replays the caller's own closure.
|
|
6891
|
+
* @param options - the create or resume options the caller supplied.
|
|
6892
|
+
* @param engine - the engine building this session.
|
|
6893
|
+
* @returns a copy whose setup writes the seat first, then the caller's own.
|
|
6894
|
+
*/
|
|
6895
|
+
engineOptions(options, engine) {
|
|
6896
|
+
const setup = options.setup;
|
|
6897
|
+
const guarded = async (agentCtx, agent) => {
|
|
6898
|
+
this.selectionReset.guardFor(agent.session, engine);
|
|
6899
|
+
return await setup?.(agentCtx, agent);
|
|
6900
|
+
};
|
|
6901
|
+
return {
|
|
6902
|
+
...options,
|
|
6903
|
+
setup: guarded,
|
|
6904
|
+
...engine === "in-process" ? {} : { agentOptions: engineRouteOptions(engine) }
|
|
6905
|
+
};
|
|
5748
6906
|
}
|
|
5749
|
-
await mkdir(dirname4(path), { recursive: true });
|
|
5750
|
-
const tmp = `${path}.tmp-${randomUUID()}`;
|
|
5751
|
-
await writeFile(tmp, text, "utf8");
|
|
5752
|
-
await rename(tmp, path);
|
|
5753
|
-
return true;
|
|
5754
|
-
}
|
|
5755
|
-
async function ensureHostedPreset(dshHome, source) {
|
|
5756
|
-
const composition = await source.read(SOURCE_PRESET_ID);
|
|
5757
|
-
const stripped = `${MANAGED_HEADER}
|
|
5758
|
-
${stripPresetRows(composition)}`;
|
|
5759
|
-
const dir = join8(dshHome, USER_PRESET_DIR, HOSTED_PRESET_ID);
|
|
5760
|
-
const compositionChanged = await writeIfDifferent(join8(dir, COMPOSITION_FILE), stripped);
|
|
5761
|
-
const metadataChanged = await writeIfDifferent(join8(dir, METADATA_FILE), MANAGED_METADATA);
|
|
5762
|
-
return compositionChanged || metadataChanged;
|
|
5763
|
-
}
|
|
5764
|
-
|
|
5765
|
-
// src/provider-route.ts
|
|
5766
|
-
import { LlmAdapter, LlmError as LlmError5 } from "@deepseek-ai/dsh-llm";
|
|
5767
|
-
var HOSTED_PROVIDER_ROUTES = {
|
|
5768
|
-
"claude-code": PROVIDER,
|
|
5769
|
-
codex: PROVIDER2,
|
|
5770
|
-
pi: PROVIDER3,
|
|
5771
|
-
kimi: PROVIDER4
|
|
5772
|
-
};
|
|
5773
|
-
var HostedEngineRouteAdapter = class extends LlmAdapter {
|
|
5774
6907
|
/**
|
|
5775
|
-
*
|
|
5776
|
-
*
|
|
6908
|
+
* Report what one session ACTUALLY runs, and — when they differ — the engine
|
|
6909
|
+
* its record names.
|
|
6910
|
+
*
|
|
6911
|
+
* This is the read the plugin's own Remote publishes, and the reason it is the
|
|
6912
|
+
* router's to make is the live bookkeeping below: for a session with an agent
|
|
6913
|
+
* this router built, {@link RouterLoop.live} says which engine built it, and
|
|
6914
|
+
* that beats every record — it is what is running. A session this router does
|
|
6915
|
+
* not drive (cold, or still on the base loop's slot during the mount window)
|
|
6916
|
+
* has no live fact to report, so the record answers, exactly as it does for a
|
|
6917
|
+
* session about to be built; and since every switch this plugin performs
|
|
6918
|
+
* either swaps the agent in place or releases it, the two facts differ only
|
|
6919
|
+
* for a release that did not take.
|
|
6920
|
+
* @param sessionId - the session to report on.
|
|
6921
|
+
* @returns the engine driving it now, plus the engine its record names for its
|
|
6922
|
+
* next build when the two differ.
|
|
5777
6923
|
*/
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
6924
|
+
async reportEngine(sessionId) {
|
|
6925
|
+
return await engineReportOfSession(
|
|
6926
|
+
this.ctx,
|
|
6927
|
+
sessionId,
|
|
6928
|
+
this.records,
|
|
6929
|
+
(id) => this.live.get(id)?.engine
|
|
6930
|
+
);
|
|
6931
|
+
}
|
|
6932
|
+
/**
|
|
6933
|
+
* Move one live session to another engine.
|
|
6934
|
+
*
|
|
6935
|
+
* The one entry point that makes "switch this session's engine" possible after
|
|
6936
|
+
* a session has started, which the harness's own preset channel refuses
|
|
6937
|
+
* (`agent-preset/locked`, `packages/preset/agent-presets/src/index.ts`). The
|
|
6938
|
+
* order below is load-bearing:
|
|
6939
|
+
*
|
|
6940
|
+
* 1. the checks, so a session that cannot be moved is refused with a reason
|
|
6941
|
+
* instead of half-moved;
|
|
6942
|
+
* 2. the record, because it is the answer the router, the Remote, and the
|
|
6943
|
+
* host's next resolve all read — and a record that could not be written
|
|
6944
|
+
* must leave the session where it is;
|
|
6945
|
+
* 3. the session's model seat, which moves with the choice
|
|
6946
|
+
* ({@link moveModelSelection}) — before the move below, because the move is
|
|
6947
|
+
* what detaches (or re-installs) the agent that reads it;
|
|
6948
|
+
* 4. the move itself: an in-place swap between two hosted engines, or — when
|
|
6949
|
+
* the harness loop is on either side — a release of the session's agent
|
|
6950
|
+
* plus a request that the page reload (`reload: true`), which is what
|
|
6951
|
+
* makes the host build the session again on the recorded engine.
|
|
6952
|
+
*
|
|
6953
|
+
* A session already on the requested engine is recorded and left running: the
|
|
6954
|
+
* record is the user's explicit choice, while tearing an untouched agent down
|
|
6955
|
+
* would cost the session's rebuild for no change at all. "Already on" is
|
|
6956
|
+
* judged on the LIVE AGENT's engine, not on the record: the two can differ
|
|
6957
|
+
* while a released agent is still in flight, and then re-selecting the engine
|
|
6958
|
+
* the session is actually running is how a user takes that record back — the
|
|
6959
|
+
* record follows the agent back, and nothing is torn down.
|
|
6960
|
+
* @param sessionId - the session to move.
|
|
6961
|
+
* @param engine - the engine it should run.
|
|
6962
|
+
* @returns the engine now recorded for it, whether the page must reload for it
|
|
6963
|
+
* to be built, or — as `ok: false` — the {@link LoopEngineRefusalCode} of the
|
|
6964
|
+
* branch that refused it plus its own sentence about this session. Every
|
|
6965
|
+
* refusal this method produces carries a code; the browser half localizes
|
|
6966
|
+
* from it and keeps the sentence as detail.
|
|
6967
|
+
*/
|
|
6968
|
+
async selectEngine(sessionId, engine) {
|
|
6969
|
+
const agent = this.ctx.agents.get(sessionId);
|
|
6970
|
+
if (agent === void 0) {
|
|
6971
|
+
return refuse2("session-closed", `session "${sessionId}" is not open; open it first, then switch its engine`);
|
|
6972
|
+
}
|
|
6973
|
+
if (agent.status === "running") {
|
|
6974
|
+
return refuse2("turn-running", `session "${sessionId}" is running; switch its engine after this turn ends`);
|
|
6975
|
+
}
|
|
6976
|
+
if (agent.session.header.origin === "subagent") {
|
|
6977
|
+
return refuse2("subagent-session", `session "${sessionId}" is a subagent session; its agent belongs to subagent routing`);
|
|
6978
|
+
}
|
|
6979
|
+
const entry = this.live.get(sessionId);
|
|
6980
|
+
if (entry === void 0) {
|
|
6981
|
+
return refuse2("not-driven", `session "${sessionId}" is not driven by this plugin's loop router`);
|
|
6982
|
+
}
|
|
6983
|
+
try {
|
|
6984
|
+
this.records.record(sessionId, engine);
|
|
6985
|
+
} catch (error) {
|
|
6986
|
+
return refuse2("record-failed", `could not record the engine of session "${sessionId}": ${String(error)}`);
|
|
6987
|
+
}
|
|
6988
|
+
this.moveModelSelection(entry, engine);
|
|
6989
|
+
if (entry.engine === engine) return { ok: true, engine };
|
|
6990
|
+
return await this.move(entry, engine);
|
|
6991
|
+
}
|
|
6992
|
+
/**
|
|
6993
|
+
* Make a just-recorded engine change take effect on the live session.
|
|
6994
|
+
*
|
|
6995
|
+
* Between two hosted engines the change is an IN-PLACE SWAP
|
|
6996
|
+
* ({@link hotSwap}): the session's `Session` object is kept, its agent is
|
|
6997
|
+
* replaced, and the browser half attached to that session sees no lifecycle
|
|
6998
|
+
* edge at all.
|
|
6999
|
+
*
|
|
7000
|
+
* A change with the harness loop on EITHER side cannot be done in place, and
|
|
7001
|
+
* this is a limit of the harness rather than a choice: `AgentLoop` neither
|
|
7002
|
+
* hands a live session over nor accepts one it did not create. Its factory
|
|
7003
|
+
* holds the session's store entry and write handle in private closure state
|
|
7004
|
+
* (`agent-loop/src/index.ts` `prepare`), its publication enters the session
|
|
7005
|
+
* (`sessions.enter`, which refuses an id already in the store), and its own
|
|
7006
|
+
* agent class is not exported. So the session's agent is RELEASED instead
|
|
7007
|
+
* ({@link release}) and the change lands on the session's next build, which
|
|
7008
|
+
* the returned `reload: true` puts in the user's hands: the session goes cold
|
|
7009
|
+
* with the record already naming its engine, and the page — reloaded, and
|
|
7010
|
+
* reopening that session — is what makes the host resolve it again, on the
|
|
7011
|
+
* engine the record names.
|
|
7012
|
+
*
|
|
7013
|
+
* The page really does have to reload, and that is the one cost of this path:
|
|
7014
|
+
* a release emits `session/disposed`, and the browser half reads that as this
|
|
7015
|
+
* session being gone, with no way back in that page's lifetime
|
|
7016
|
+
* (`session.ts` `handleRemoved` sets a `removed` flag nothing ever clears), so
|
|
7017
|
+
* a page that stayed put would show a session that can no longer be typed
|
|
7018
|
+
* into. The alternative — leaving the live agent alone and letting the record
|
|
7019
|
+
* wait for a process restart — is what this plugin used to do, and it cost the
|
|
7020
|
+
* user a restart for a switch the host can perform itself.
|
|
7021
|
+
* @param entry - the live session's record; the caller verified it is movable.
|
|
7022
|
+
* @param engine - the engine just recorded for it.
|
|
7023
|
+
* @returns the switch outcome, asking for the page reload that builds the
|
|
7024
|
+
* session again when its agent had to be released.
|
|
7025
|
+
*/
|
|
7026
|
+
async move(entry, engine) {
|
|
7027
|
+
const handover = entry.handover;
|
|
7028
|
+
if (handover === void 0 || engine === "in-process") {
|
|
7029
|
+
await this.release(entry);
|
|
7030
|
+
return { ok: true, engine, reload: true };
|
|
7031
|
+
}
|
|
7032
|
+
return await this.hotSwap(entry, handover, engine);
|
|
7033
|
+
}
|
|
7034
|
+
/**
|
|
7035
|
+
* Replace one live session's agent with a machine the incoming engine builds
|
|
7036
|
+
* onto the SAME `Session`.
|
|
7037
|
+
*
|
|
7038
|
+
* The order is load-bearing. The outgoing machine is retired FIRST and
|
|
7039
|
+
* completely: only one agent may be registered per session id (`agents.enter`
|
|
7040
|
+
* refuses a duplicate), and two machines folded over one session's inbox would
|
|
7041
|
+
* splice each other's queued messages, so the outgoing machine must be settled
|
|
7042
|
+
* before any input can reach the successor. The SESSION is never released:
|
|
7043
|
+
* its store entry and write handle travel in the lifetime the outgoing machine
|
|
7044
|
+
* hands over, which is exactly why the page attached to this session stays
|
|
7045
|
+
* usable across the swap.
|
|
7046
|
+
*
|
|
7047
|
+
* A failure after the handover cannot be undone — the outgoing machine is
|
|
7048
|
+
* gone and the successor never published. The lifetime is then released, so
|
|
7049
|
+
* the session goes cold and reopens on the recorded engine the way any session
|
|
7050
|
+
* the host has not loaded does, and the refusal says what happened.
|
|
7051
|
+
* @param entry - the live session's record; the caller verified it is movable.
|
|
7052
|
+
* @param handover - the outgoing machine's session handover.
|
|
7053
|
+
* @param engine - the hosted engine to build the successor on.
|
|
7054
|
+
* @returns the switch outcome.
|
|
7055
|
+
*/
|
|
7056
|
+
async hotSwap(entry, handover, engine) {
|
|
7057
|
+
const sessionId = entry.agent.id;
|
|
7058
|
+
try {
|
|
7059
|
+
await handover.retire();
|
|
7060
|
+
} catch (error) {
|
|
7061
|
+
this.warn(`loop-engine: could not fully retire the ${entry.engine} agent of "${sessionId}" before switching it to ${engine}: ${String(error)}`);
|
|
7062
|
+
}
|
|
7063
|
+
try {
|
|
7064
|
+
const handle = await this.runtimeOf(engine).swap(entry.recipe.ownerCtx, {
|
|
7065
|
+
lifetime: handover.lifetime,
|
|
7066
|
+
agentOptions: entry.recipe.agentOptions,
|
|
7067
|
+
setup: entry.recipe.setup,
|
|
7068
|
+
parentAgent: entry.recipe.parentAgent
|
|
7069
|
+
});
|
|
7070
|
+
this.adopt(engine, handle, entry.recipe, handoverOf(handle));
|
|
7071
|
+
return { ok: true, engine };
|
|
7072
|
+
} catch (error) {
|
|
7073
|
+
this.forget(entry);
|
|
7074
|
+
try {
|
|
7075
|
+
await handover.lifetime.closeHandle();
|
|
7076
|
+
} catch (closeFailure) {
|
|
7077
|
+
this.warn(`loop-engine: could not close the write handle of session "${sessionId}" after a failed switch: ${String(closeFailure)}`);
|
|
7078
|
+
}
|
|
7079
|
+
handover.lifetime.leaveStore();
|
|
7080
|
+
return refuse2("rebuild-failed", `could not rebuild session "${sessionId}" on ${engine}: ${String(error)}`);
|
|
7081
|
+
}
|
|
7082
|
+
}
|
|
7083
|
+
/** Read the host session-projection registry, structurally. */
|
|
7084
|
+
projections() {
|
|
7085
|
+
return this.ctx.get("sessionProjections");
|
|
7086
|
+
}
|
|
7087
|
+
/**
|
|
7088
|
+
* Drop one tracked session's agent so the host's next resolve rebuilds it.
|
|
7089
|
+
*
|
|
7090
|
+
* Two callers, and they both want the engine change to land at the session's
|
|
7091
|
+
* next build rather than in place:
|
|
7092
|
+
*
|
|
7093
|
+
* - {@link move}, the engine picker's path involving the harness loop, which
|
|
7094
|
+
* WAITS for the teardown (the outcome it is about to return says the
|
|
7095
|
+
* session is cold, and that must be true when it says so);
|
|
7096
|
+
* - the harness's own preset switch on a blank session, which fire-and-forgets
|
|
7097
|
+
* it because it runs inside a synchronous event handler. It is the one
|
|
7098
|
+
* engine change this plugin still makes by releasing the session rather
|
|
7099
|
+
* than by moving it: the successor's composition is the NEW preset's, which
|
|
7100
|
+
* only the API layer composes (a preset the harness mounted itself reaches
|
|
7101
|
+
* the plugin as an id, not as a composition callback), so the plugin cannot
|
|
7102
|
+
* build a correctly-composed successor in place.
|
|
7103
|
+
*
|
|
7104
|
+
* The record is forgotten before the teardown runs, so a teardown-driven event
|
|
7105
|
+
* can never find the entry it is releasing and start a second teardown of the
|
|
7106
|
+
* same agent; a failure is reported rather than thrown, and the returned
|
|
7107
|
+
* promise settles either way.
|
|
7108
|
+
* @param entry - the live session record to release.
|
|
7109
|
+
* @returns the teardown, once it settled.
|
|
7110
|
+
*/
|
|
7111
|
+
release(entry) {
|
|
7112
|
+
this.forget(entry);
|
|
7113
|
+
return entry.dispose().catch((error) => {
|
|
7114
|
+
this.warn(`loop-engine: could not release the ${entry.engine} agent of "${entry.agent.id}": ${String(error)}`);
|
|
7115
|
+
});
|
|
7116
|
+
}
|
|
7117
|
+
/**
|
|
7118
|
+
* Follow the harness's own preset switch, for a BLANK session.
|
|
7119
|
+
*
|
|
7120
|
+
* The harness's preset switch on a blank session re-parents the live agent's
|
|
7121
|
+
* scope and records the choice; it does not rebuild the agent, so the engine
|
|
7122
|
+
* would keep running the one the session was created with. Dropping the agent
|
|
7123
|
+
* instead lets the host's own next resolve fall back to a resume
|
|
7124
|
+
* (`ApiSessionAgentController.resolve`), which composes from the recorded
|
|
7125
|
+
* preset and lands on the new engine — the same path a reopened session
|
|
7126
|
+
* takes, with the durable log as the only carry-over.
|
|
7127
|
+
*
|
|
7128
|
+
* LAST USER ACTION WINS: for a session this plugin already holds a record
|
|
7129
|
+
* for, the harness's picker is an engine choice too, so the record follows it
|
|
7130
|
+
* before the agent is released. Without that, the two entry points would
|
|
7131
|
+
* contradict each other — the picker would rebuild the session on the preset's
|
|
7132
|
+
* engine, while the very next resume would read the plugin's record and move
|
|
7133
|
+
* it back. A session with NO record keeps reading its engine off the preset,
|
|
7134
|
+
* exactly as before, and gets none: only an explicit choice is recorded.
|
|
7135
|
+
*
|
|
7136
|
+
* A session that has already run a turn is left alone — its live engine and
|
|
7137
|
+
* its record both stay where they are, so the router and the Remote keep
|
|
7138
|
+
* agreeing about it. (A started session cannot reach here through the
|
|
7139
|
+
* harness's picker anyway: the roster refuses with `agent-preset/locked`.)
|
|
7140
|
+
*
|
|
7141
|
+
* The session's model seat moves with the choice, before the release
|
|
7142
|
+
* ({@link moveModelSelection}) — the picker is an engine change like the
|
|
7143
|
+
* picker's own switcher, and this is the one place the harness can make it.
|
|
7144
|
+
*/
|
|
7145
|
+
rebuildOnEngineChange() {
|
|
7146
|
+
this.ctx.on("agent-preset/selected", (sessionId, preset) => {
|
|
7147
|
+
const entry = this.live.get(sessionId);
|
|
7148
|
+
if (entry === void 0) return;
|
|
7149
|
+
const next = engineOfPreset(preset) ?? "in-process";
|
|
7150
|
+
if (next === entry.engine) return;
|
|
7151
|
+
const boundary = this.projections().stateOf(entry.agent.session, "turnBoundary");
|
|
7152
|
+
if (boundary !== void 0 && (boundary.openTurnStartSeq !== null || boundary.lastTurn > 0)) {
|
|
7153
|
+
this.warn(`loop-engine: session "${sessionId}" has already started; its engine stays ${entry.engine}`);
|
|
7154
|
+
return;
|
|
7155
|
+
}
|
|
7156
|
+
if (this.records.engineOf(sessionId) !== void 0) {
|
|
7157
|
+
try {
|
|
7158
|
+
this.records.record(sessionId, next);
|
|
7159
|
+
} catch (error) {
|
|
7160
|
+
this.warn(`loop-engine: could not record the engine of "${sessionId}": ${String(error)}`);
|
|
7161
|
+
}
|
|
7162
|
+
}
|
|
7163
|
+
this.moveModelSelection(entry, next);
|
|
7164
|
+
void this.release(entry);
|
|
7165
|
+
});
|
|
5782
7166
|
}
|
|
5783
|
-
label;
|
|
5784
|
-
options;
|
|
5785
7167
|
/**
|
|
5786
|
-
*
|
|
7168
|
+
* Hand one session's model seat to the engine it is being switched to.
|
|
7169
|
+
*
|
|
7170
|
+
* A session's selection follows its engine: a hosted engine owns its model
|
|
7171
|
+
* natively and the shared provider label it logs is served only by this
|
|
7172
|
+
* plugin's placeholder route (which fails loud, `HOSTED_ENGINE_ROUTE`, when a
|
|
7173
|
+
* real model call reaches it), while the harness loop DOES make real calls and therefore needs
|
|
7174
|
+
* a real model selected. So a switch moves the seat — the shared
|
|
7175
|
+
* `external/default` for a hosted engine, the deployment default for the
|
|
7176
|
+
* harness loop. A selection that
|
|
7177
|
+
* names a real model of its own is never touched: the model notice the browser
|
|
7178
|
+
* half shows beside a hosted engine says that selection is inert there, not
|
|
7179
|
+
* that it is gone. The write itself, its judgements, and why an engine change
|
|
7180
|
+
* with no logged selection writes nothing are in `model-selection-reset.ts`;
|
|
7181
|
+
* this method is only the router's half.
|
|
5787
7182
|
*
|
|
5788
|
-
* The
|
|
5789
|
-
*
|
|
5790
|
-
*
|
|
5791
|
-
* session
|
|
7183
|
+
* The selection is written at the moment of the switch, not at the moment the
|
|
7184
|
+
* engine changes. Between two hosted engines the move is an in-place swap whose
|
|
7185
|
+
* `setup` installs the selection onto the successor, and for anything involving
|
|
7186
|
+
* the harness loop the switch releases the session's agent and the page reloads
|
|
7187
|
+
* ({@link move}) — either way the write has to be in the log before the move
|
|
7188
|
+
* runs, which is what this ordering buys.
|
|
7189
|
+
* @param entry - the live session being switched.
|
|
7190
|
+
* @param engine - the engine it is being switched to.
|
|
7191
|
+
* @returns nothing; a selection this process cannot name is skipped with one
|
|
7192
|
+
* warning, and the session keeps what its log records.
|
|
5792
7193
|
*/
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
return catalog.map((entry) => ({
|
|
5796
|
-
provider: this.label,
|
|
5797
|
-
id: `${entry.provider}/${entry.model}`,
|
|
5798
|
-
name: entry.model
|
|
5799
|
-
}));
|
|
5800
|
-
}
|
|
5801
|
-
stream(_options) {
|
|
5802
|
-
throw new LlmError5(
|
|
5803
|
-
`provider "${this.label}" is a hosted loop engine route, not a model endpoint`,
|
|
5804
|
-
"HOSTED_ENGINE_ROUTE"
|
|
5805
|
-
);
|
|
7194
|
+
moveModelSelection(entry, engine) {
|
|
7195
|
+
this.selectionReset.resetFor(entry.agent.session, engine);
|
|
5806
7196
|
}
|
|
5807
7197
|
};
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
}));
|
|
5821
|
-
return { kind: "success" };
|
|
7198
|
+
function engineRouteOptions(engine) {
|
|
7199
|
+
return { provider: hostedRouteLabelOf(engine), model: HOSTED_DEFAULT_MODEL };
|
|
7200
|
+
}
|
|
7201
|
+
function refuse2(code, reason) {
|
|
7202
|
+
return { ok: false, code, reason };
|
|
7203
|
+
}
|
|
7204
|
+
function recipeOf(ownerCtx, options) {
|
|
7205
|
+
return {
|
|
7206
|
+
ownerCtx,
|
|
7207
|
+
agentOptions: options.agentOptions ?? {},
|
|
7208
|
+
setup: options.setup,
|
|
7209
|
+
parentAgent: options.parentAgent
|
|
5822
7210
|
};
|
|
5823
7211
|
}
|
|
5824
|
-
function
|
|
5825
|
-
return {
|
|
7212
|
+
function handoverOf(handle) {
|
|
7213
|
+
return { lifetime: handle.lifetime, retire: () => handle.retire() };
|
|
5826
7214
|
}
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
function
|
|
5837
|
-
|
|
7215
|
+
|
|
7216
|
+
// src/session-engine-store.ts
|
|
7217
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7218
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7219
|
+
import { dirname as dirname5, join as join13 } from "node:path";
|
|
7220
|
+
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
7221
|
+
var ENGINE_RECORD_DIR = ".loop-engine";
|
|
7222
|
+
var ENGINE_RECORD_FILE = "engines.json";
|
|
7223
|
+
var ENGINE_RECORD_VERSION = 1;
|
|
7224
|
+
function resolveEngineRecordPath() {
|
|
7225
|
+
return join13(resolveDshHome(), ENGINE_RECORD_DIR, ENGINE_RECORD_FILE);
|
|
7226
|
+
}
|
|
7227
|
+
function isMissing(error) {
|
|
7228
|
+
return error?.code === "ENOENT";
|
|
7229
|
+
}
|
|
7230
|
+
function writeFileAtomicSync(path, text) {
|
|
7231
|
+
mkdirSync2(dirname5(path), { recursive: true });
|
|
7232
|
+
const tmp = `${path}.tmp-${randomUUID2()}`;
|
|
7233
|
+
writeFileSync2(tmp, text, "utf8");
|
|
7234
|
+
renameSync(tmp, path);
|
|
7235
|
+
}
|
|
7236
|
+
function parseEngineRecord(text) {
|
|
7237
|
+
let parsed;
|
|
5838
7238
|
try {
|
|
5839
|
-
|
|
7239
|
+
parsed = JSON.parse(text);
|
|
5840
7240
|
} catch {
|
|
5841
|
-
return
|
|
5842
|
-
}
|
|
5843
|
-
const definitions = [];
|
|
5844
|
-
const seen = new Set(CLAUDE_CODE_COMMANDS.map((command) => command.name));
|
|
5845
|
-
for (const entry of entries.sort()) {
|
|
5846
|
-
if (!entry.endsWith(".md")) continue;
|
|
5847
|
-
const name2 = entry.slice(0, -".md".length);
|
|
5848
|
-
if (!COMMAND_NAME.test(name2) || seen.has(name2)) continue;
|
|
5849
|
-
const path = join9(userCommandsDir(), entry);
|
|
5850
|
-
let raw;
|
|
5851
|
-
try {
|
|
5852
|
-
raw = readFileSync2(path, "utf8");
|
|
5853
|
-
} catch {
|
|
5854
|
-
continue;
|
|
5855
|
-
}
|
|
5856
|
-
const description = commandDescription(raw);
|
|
5857
|
-
if (description === void 0) continue;
|
|
5858
|
-
seen.add(name2);
|
|
5859
|
-
definitions.push({ name: name2, description, handler: forwardClaudeCodeCommand(name2) });
|
|
7241
|
+
return void 0;
|
|
5860
7242
|
}
|
|
5861
|
-
return
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
function commandDescription(raw) {
|
|
5867
|
-
const trimmed = raw.trim();
|
|
5868
|
-
if (trimmed.length === 0) return void 0;
|
|
5869
|
-
let body = trimmed;
|
|
5870
|
-
if (trimmed.startsWith("---\n")) {
|
|
5871
|
-
const closing = trimmed.indexOf("\n---");
|
|
5872
|
-
if (closing <= 0) return void 0;
|
|
5873
|
-
for (const line of trimmed.slice(4, closing).split("\n")) {
|
|
5874
|
-
const colon = line.indexOf(":");
|
|
5875
|
-
if (colon < 0 || line.slice(0, colon).trim() !== "description") continue;
|
|
5876
|
-
const value = line.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
|
|
5877
|
-
if (value.length > 0) return value;
|
|
5878
|
-
}
|
|
5879
|
-
body = trimmed.slice(closing + 4);
|
|
7243
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
7244
|
+
const document = parsed;
|
|
7245
|
+
const engines = document.engines;
|
|
7246
|
+
if (document.version !== ENGINE_RECORD_VERSION || typeof engines !== "object" || engines === null || Array.isArray(engines)) {
|
|
7247
|
+
return void 0;
|
|
5880
7248
|
}
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
if (
|
|
5884
|
-
return candidate.length > 120 ? `${candidate.slice(0, 119)}\u2026` : candidate;
|
|
7249
|
+
const entries = /* @__PURE__ */ new Map();
|
|
7250
|
+
for (const [sessionId, engine] of Object.entries(engines)) {
|
|
7251
|
+
if (isLoopEngineId(engine)) entries.set(sessionId, engine);
|
|
5885
7252
|
}
|
|
5886
|
-
return
|
|
7253
|
+
return entries;
|
|
5887
7254
|
}
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
name: "codex",
|
|
5897
|
-
agentsMdDescription: "Codex project/user instructions (AGENTS.md)",
|
|
5898
|
-
contextPolicy: CODEX_CONTEXT_POLICY,
|
|
5899
|
-
userDir: () => join10(homedir5(), ".codex"),
|
|
5900
|
-
projectRank: CODEX_PROJECT_RANK,
|
|
5901
|
-
userContext: { file: "AGENTS.md", rank: CODEX_USER_RANK }
|
|
5902
|
-
};
|
|
5903
|
-
var CodexSkillProvider = class extends AgentsMdSkillProvider {
|
|
5904
|
-
constructor(control) {
|
|
5905
|
-
super(CODEX_SPEC, control);
|
|
7255
|
+
var SessionEngineStore = class {
|
|
7256
|
+
/**
|
|
7257
|
+
* @param path - the record document's absolute path.
|
|
7258
|
+
* @param warn - diagnostic sink for a record this build cannot read.
|
|
7259
|
+
*/
|
|
7260
|
+
constructor(path, warn) {
|
|
7261
|
+
this.path = path;
|
|
7262
|
+
this.warn = warn;
|
|
5906
7263
|
}
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
override: "AGENTS.override.md",
|
|
5918
|
-
primary: ["AGENTS.md", "CLAUDE.md"]
|
|
5919
|
-
};
|
|
5920
|
-
function piAgentDir() {
|
|
5921
|
-
const override = process.env.PI_CODING_AGENT_DIR;
|
|
5922
|
-
if (override !== void 0 && override.length > 0) return resolve4(override);
|
|
5923
|
-
return join11(homedir6(), ".pi", "agent");
|
|
5924
|
-
}
|
|
5925
|
-
var PI_SPEC = {
|
|
5926
|
-
name: "pi",
|
|
5927
|
-
agentsMdDescription: "Pi project/user instructions (AGENTS.md / CLAUDE.md)",
|
|
5928
|
-
contextPolicy: PI_CONTEXT_POLICY,
|
|
5929
|
-
userDir: piAgentDir,
|
|
5930
|
-
projectRank: PI_AGENTS_PROJECT_RANK,
|
|
5931
|
-
userContext: { file: "AGENTS.md", rank: PI_AGENTS_USER_RANK },
|
|
5932
|
-
skills: {
|
|
5933
|
-
project: [".pi", "skills"],
|
|
5934
|
-
projectRank: PI_SKILL_PROJECT_RANK,
|
|
5935
|
-
userDir: "skills",
|
|
5936
|
-
userRank: PI_SKILL_USER_RANK
|
|
7264
|
+
path;
|
|
7265
|
+
warn;
|
|
7266
|
+
entries;
|
|
7267
|
+
/**
|
|
7268
|
+
* The engine this plugin's record names for one session.
|
|
7269
|
+
* @param sessionId - the session to look up.
|
|
7270
|
+
* @returns the recorded engine, or undefined when there is no usable record.
|
|
7271
|
+
*/
|
|
7272
|
+
engineOf(sessionId) {
|
|
7273
|
+
return this.load().get(String(sessionId));
|
|
5937
7274
|
}
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
7275
|
+
/**
|
|
7276
|
+
* Record one session's engine, replacing any earlier record for it.
|
|
7277
|
+
*
|
|
7278
|
+
* The write commits before the in-memory view moves, so a caller that sees a
|
|
7279
|
+
* rejection knows the store still answers what it answered before.
|
|
7280
|
+
* @param sessionId - the session whose engine is recorded.
|
|
7281
|
+
* @param engine - the engine that session now runs.
|
|
7282
|
+
* @throws when the document could not be written; the previous file is intact.
|
|
7283
|
+
*/
|
|
7284
|
+
record(sessionId, engine) {
|
|
7285
|
+
const next = new Map(this.load());
|
|
7286
|
+
next.set(String(sessionId), engine);
|
|
7287
|
+
writeFileAtomicSync(this.path, formatEngineRecord(next));
|
|
7288
|
+
this.entries = next;
|
|
7289
|
+
}
|
|
7290
|
+
/** The in-memory record, read from disk on first use. */
|
|
7291
|
+
load() {
|
|
7292
|
+
this.entries ??= this.read();
|
|
7293
|
+
return this.entries;
|
|
7294
|
+
}
|
|
7295
|
+
/** Read the document once, degrading to an empty record. */
|
|
7296
|
+
read() {
|
|
7297
|
+
let text;
|
|
7298
|
+
try {
|
|
7299
|
+
text = readFileSync3(this.path, "utf8");
|
|
7300
|
+
} catch (error) {
|
|
7301
|
+
if (isMissing(error)) return /* @__PURE__ */ new Map();
|
|
7302
|
+
return this.degrade(`could not read "${this.path}": ${String(error)}`);
|
|
7303
|
+
}
|
|
7304
|
+
return parseEngineRecord(text) ?? this.degrade(`"${this.path}" is not a version ${ENGINE_RECORD_VERSION} engine record`);
|
|
7305
|
+
}
|
|
7306
|
+
/**
|
|
7307
|
+
* Report one unusable record and answer "no record".
|
|
7308
|
+
*
|
|
7309
|
+
* The load is memoized, so this runs at most once per store — once per process
|
|
7310
|
+
* for a path — rather than once per lookup: a broken record costs one line, and
|
|
7311
|
+
* the degraded view serves every later read.
|
|
7312
|
+
* @param problem - what is wrong with the record, with the path.
|
|
7313
|
+
* @returns the empty record.
|
|
7314
|
+
*/
|
|
7315
|
+
degrade(problem) {
|
|
7316
|
+
this.warn(`loop-engine: ${problem}; sessions without a record fall back to their agent preset`);
|
|
7317
|
+
return /* @__PURE__ */ new Map();
|
|
5942
7318
|
}
|
|
5943
7319
|
};
|
|
7320
|
+
function formatEngineRecord(entries) {
|
|
7321
|
+
return `${JSON.stringify({ version: ENGINE_RECORD_VERSION, engines: Object.fromEntries(entries) }, void 0, 2)}
|
|
7322
|
+
`;
|
|
7323
|
+
}
|
|
5944
7324
|
|
|
5945
7325
|
// src/index.ts
|
|
5946
7326
|
var name = "loop-engine";
|
|
5947
7327
|
var inject = [];
|
|
5948
|
-
var MAX_MOUNT_ATTEMPTS = 40;
|
|
5949
|
-
var MOUNT_RETRY_MS = 50;
|
|
5950
7328
|
var Config5 = z6.object({
|
|
5951
7329
|
profile: z6.string(),
|
|
5952
7330
|
patchFilename: z6.string(),
|
|
@@ -5964,48 +7342,46 @@ var Config5 = z6.object({
|
|
|
5964
7342
|
});
|
|
5965
7343
|
function resolvePatchPath(config) {
|
|
5966
7344
|
if (config.patchPath !== void 0 && config.patchPath !== "") return config.patchPath;
|
|
5967
|
-
return
|
|
5968
|
-
|
|
7345
|
+
return join14(
|
|
7346
|
+
resolveDshHome2(),
|
|
5969
7347
|
"profiles",
|
|
5970
7348
|
config.profile ?? "web",
|
|
5971
7349
|
config.patchFilename ?? "cordis.patch.yml"
|
|
5972
7350
|
);
|
|
5973
7351
|
}
|
|
5974
|
-
function
|
|
7352
|
+
function isMissing2(error) {
|
|
5975
7353
|
return error?.code === "ENOENT";
|
|
5976
7354
|
}
|
|
5977
7355
|
async function readPatchOrUndefined(path) {
|
|
5978
7356
|
try {
|
|
5979
7357
|
return await readFile5(path, "utf8");
|
|
5980
7358
|
} catch (error) {
|
|
5981
|
-
if (
|
|
7359
|
+
if (isMissing2(error)) return void 0;
|
|
5982
7360
|
throw error;
|
|
5983
7361
|
}
|
|
5984
7362
|
}
|
|
5985
7363
|
async function writePatchFile(path, text) {
|
|
5986
|
-
await mkdir2(
|
|
5987
|
-
const tmp = `${path}.tmp-${
|
|
7364
|
+
await mkdir2(dirname6(path), { recursive: true });
|
|
7365
|
+
const tmp = `${path}.tmp-${randomUUID3()}`;
|
|
5988
7366
|
await writeFile2(tmp, text, "utf8");
|
|
5989
7367
|
await rename2(tmp, path);
|
|
5990
7368
|
}
|
|
5991
7369
|
function writePatchFileSync(path, text) {
|
|
5992
|
-
|
|
5993
|
-
const tmp = `${path}.tmp-${randomUUID2()}`;
|
|
5994
|
-
writeFileSync(tmp, text, "utf8");
|
|
5995
|
-
renameSync(tmp, path);
|
|
7370
|
+
writeFileAtomicSync(path, text);
|
|
5996
7371
|
}
|
|
5997
|
-
async function syncManagedBlock(path
|
|
7372
|
+
async function syncManagedBlock(path) {
|
|
5998
7373
|
const current = await readPatchOrUndefined(path);
|
|
5999
|
-
if (current !== void 0 &&
|
|
6000
|
-
|
|
6001
|
-
|
|
7374
|
+
if (current !== void 0 && hasManagedBlock(current) && !hasLegacyManagedBlock(current)) {
|
|
7375
|
+
return false;
|
|
7376
|
+
}
|
|
7377
|
+
await writePatchFile(path, applyManagedBlock(current ?? ""));
|
|
6002
7378
|
return true;
|
|
6003
7379
|
}
|
|
6004
7380
|
function readPatchFileSync(path) {
|
|
6005
7381
|
try {
|
|
6006
|
-
return
|
|
7382
|
+
return readFileSync4(path, "utf8");
|
|
6007
7383
|
} catch (error) {
|
|
6008
|
-
if (
|
|
7384
|
+
if (isMissing2(error)) return "";
|
|
6009
7385
|
throw error;
|
|
6010
7386
|
}
|
|
6011
7387
|
}
|
|
@@ -6045,51 +7421,37 @@ function kimiConfig(config) {
|
|
|
6045
7421
|
function apply(ctx, config) {
|
|
6046
7422
|
const patchPath = resolvePatchPath(config);
|
|
6047
7423
|
const patchText = readPatchFileSync(patchPath);
|
|
6048
|
-
|
|
6049
|
-
|
|
7424
|
+
const legacyEngine = legacyBlockEngineOf(patchText);
|
|
7425
|
+
const needsWrite = hasLegacyManagedBlock(patchText) || !hasManagedBlock(patchText);
|
|
7426
|
+
if (needsWrite) {
|
|
6050
7427
|
try {
|
|
6051
|
-
writePatchFileSync(patchPath, applyManagedBlock(patchText
|
|
6052
|
-
|
|
7428
|
+
writePatchFileSync(patchPath, applyManagedBlock(patchText));
|
|
7429
|
+
} catch (error) {
|
|
6053
7430
|
ctx.logger.error(
|
|
6054
|
-
|
|
7431
|
+
`loop-engine: could not write the managed block to "${patchPath}": ${String(error)}. The base agent loop still owns the factory slot; restart \`dsh web\` after fixing the path.`
|
|
6055
7432
|
);
|
|
6056
|
-
} catch (error) {
|
|
6057
|
-
ctx.logger.error(`loop-engine: could not repair the unrecognized managed block: ${String(error)}`);
|
|
6058
7433
|
}
|
|
6059
7434
|
}
|
|
6060
7435
|
const AGENT_PRESETS_NS = "agent-presets";
|
|
6061
7436
|
const PRESET_DEFAULT_ATTEMPTS = 30;
|
|
6062
7437
|
const PRESET_DEFAULT_RETRY_MS = 100;
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
7438
|
+
const ROUTE_ATTEMPTS = 30;
|
|
7439
|
+
const ROUTE_RETRY_MS = 100;
|
|
7440
|
+
const ROUTER_ATTEMPTS = 40;
|
|
7441
|
+
const ROUTER_RETRY_MS = 50;
|
|
7442
|
+
let routerAttempts = 0;
|
|
7443
|
+
let routerRetry;
|
|
7444
|
+
const CLEAR_ROUTER_RETRY = () => {
|
|
7445
|
+
if (routerRetry !== void 0) {
|
|
7446
|
+
clearTimeout(routerRetry);
|
|
7447
|
+
routerRetry = void 0;
|
|
7448
|
+
}
|
|
7449
|
+
};
|
|
6066
7450
|
let disposed = false;
|
|
6067
7451
|
const retryLater = (run, ms) => setTimeout(() => {
|
|
6068
7452
|
if (!disposed) run();
|
|
6069
7453
|
}, ms);
|
|
6070
|
-
let
|
|
6071
|
-
let skillDisposer;
|
|
6072
|
-
const piCatalogHolder = { entries: [] };
|
|
6073
|
-
let mountAttempts = 0;
|
|
6074
|
-
let mountRetry;
|
|
6075
|
-
const CLEAR_RETRY = () => {
|
|
6076
|
-
if (mountRetry !== void 0) {
|
|
6077
|
-
clearTimeout(mountRetry);
|
|
6078
|
-
mountRetry = void 0;
|
|
6079
|
-
}
|
|
6080
|
-
};
|
|
6081
|
-
let savedPresetDefault;
|
|
6082
|
-
let presetRetry;
|
|
6083
|
-
const CLEAR_PRESET_RETRY = () => {
|
|
6084
|
-
if (presetRetry !== void 0) {
|
|
6085
|
-
clearTimeout(presetRetry);
|
|
6086
|
-
presetRetry = void 0;
|
|
6087
|
-
}
|
|
6088
|
-
};
|
|
6089
|
-
const ROUTE_ATTEMPTS = 30;
|
|
6090
|
-
const ROUTE_RETRY_MS = 100;
|
|
6091
|
-
let routeHandle;
|
|
6092
|
-
let routeEngine;
|
|
7454
|
+
let routeDisposers;
|
|
6093
7455
|
let routeRetry;
|
|
6094
7456
|
const CLEAR_ROUTE_RETRY = () => {
|
|
6095
7457
|
if (routeRetry !== void 0) {
|
|
@@ -6097,38 +7459,65 @@ function apply(ctx, config) {
|
|
|
6097
7459
|
routeRetry = void 0;
|
|
6098
7460
|
}
|
|
6099
7461
|
};
|
|
6100
|
-
const
|
|
6101
|
-
CLEAR_ROUTE_RETRY();
|
|
6102
|
-
const handle = routeHandle;
|
|
6103
|
-
routeHandle = void 0;
|
|
6104
|
-
routeEngine = void 0;
|
|
6105
|
-
handle?.();
|
|
6106
|
-
};
|
|
6107
|
-
const mountProviderRoute = (engine, attempt = 0) => {
|
|
6108
|
-
if (engine === "in-process") return;
|
|
6109
|
-
if (routeEngine === engine && routeHandle !== void 0) return;
|
|
7462
|
+
const mountProviderRoutes = (attempt = 0) => {
|
|
6110
7463
|
CLEAR_ROUTE_RETRY();
|
|
6111
|
-
const label = HOSTED_PROVIDER_ROUTES[engine];
|
|
6112
7464
|
const llm = ctx.get("llm");
|
|
6113
7465
|
if (llm === void 0) {
|
|
6114
7466
|
if (attempt < ROUTE_ATTEMPTS) {
|
|
6115
|
-
routeRetry = retryLater(() =>
|
|
7467
|
+
routeRetry = retryLater(() => mountProviderRoutes(attempt + 1), ROUTE_RETRY_MS);
|
|
6116
7468
|
}
|
|
6117
7469
|
return;
|
|
6118
7470
|
}
|
|
7471
|
+
const registered = [];
|
|
6119
7472
|
try {
|
|
6120
|
-
|
|
6121
|
-
routeHandle = llm.registerAdapter([label], new HostedEngineRouteAdapter(label, options));
|
|
6122
|
-
routeEngine = engine;
|
|
7473
|
+
registered.push(llm.registerAdapter([HOSTED_ROUTE_LABEL], new HostedEngineRouteAdapter()));
|
|
6123
7474
|
} catch (error) {
|
|
6124
7475
|
const duplicateAdapter = error?.code === "DUPLICATE_ADAPTER";
|
|
6125
7476
|
if (error instanceof Error && (duplicateAdapter || error.message.includes("already registered"))) {
|
|
6126
|
-
ctx.logger.warn(`loop-engine: provider route "${
|
|
6127
|
-
|
|
7477
|
+
ctx.logger.warn(`loop-engine: provider route "${HOSTED_ROUTE_LABEL}" is already served by another adapter`);
|
|
7478
|
+
} else {
|
|
7479
|
+
ctx.logger.error(`loop-engine: provider route "${HOSTED_ROUTE_LABEL}" registration failed: ${String(error)}`);
|
|
7480
|
+
}
|
|
7481
|
+
}
|
|
7482
|
+
routeDisposers = registered;
|
|
7483
|
+
};
|
|
7484
|
+
const releaseRoutes = () => {
|
|
7485
|
+
CLEAR_ROUTE_RETRY();
|
|
7486
|
+
const disposers = routeDisposers;
|
|
7487
|
+
routeDisposers = void 0;
|
|
7488
|
+
for (const dispose of disposers ?? []) dispose();
|
|
7489
|
+
};
|
|
7490
|
+
let presetRetry;
|
|
7491
|
+
let authorRetry;
|
|
7492
|
+
const CLEAR_PRESET_RETRY = () => {
|
|
7493
|
+
if (presetRetry !== void 0) {
|
|
7494
|
+
clearTimeout(presetRetry);
|
|
7495
|
+
presetRetry = void 0;
|
|
7496
|
+
}
|
|
7497
|
+
};
|
|
7498
|
+
const CLEAR_AUTHOR_RETRY = () => {
|
|
7499
|
+
if (authorRetry !== void 0) {
|
|
7500
|
+
clearTimeout(authorRetry);
|
|
7501
|
+
authorRetry = void 0;
|
|
7502
|
+
}
|
|
7503
|
+
};
|
|
7504
|
+
const authorEnginePresets = (attempt = 0) => {
|
|
7505
|
+
CLEAR_AUTHOR_RETRY();
|
|
7506
|
+
const presets = ctx.get("agentPresets");
|
|
7507
|
+
if (presets === void 0) {
|
|
7508
|
+
if (attempt < PRESET_DEFAULT_ATTEMPTS) {
|
|
7509
|
+
authorRetry = retryLater(() => authorEnginePresets(attempt + 1), PRESET_DEFAULT_RETRY_MS);
|
|
6128
7510
|
}
|
|
6129
|
-
|
|
7511
|
+
return void 0;
|
|
6130
7512
|
}
|
|
7513
|
+
const settled = authoring ??= ensureEnginePresets(resolveDshHome2(), presets).catch((error) => {
|
|
7514
|
+
ctx.logger.error(`loop-engine: engine preset authoring failed: ${String(error)}`);
|
|
7515
|
+
authoring = void 0;
|
|
7516
|
+
return false;
|
|
7517
|
+
});
|
|
7518
|
+
return { presets, settled };
|
|
6131
7519
|
};
|
|
7520
|
+
let authoring;
|
|
6132
7521
|
const mutatePresetDefault = (op, attempt = 0) => {
|
|
6133
7522
|
const settings = ctx.get("settings");
|
|
6134
7523
|
if (settings === void 0) return;
|
|
@@ -6148,198 +7537,107 @@ function apply(ctx, config) {
|
|
|
6148
7537
|
ctx.logger.error(`loop-engine: preset default switch failed: ${String(error)}`);
|
|
6149
7538
|
});
|
|
6150
7539
|
};
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
if (presets === void 0) return;
|
|
6154
|
-
if (presets.defaultId === HOSTED_PRESET_ID) {
|
|
6155
|
-
const saved = savedPresetDefault;
|
|
6156
|
-
savedPresetDefault = void 0;
|
|
6157
|
-
mutatePresetDefault(saved === void 0 ? { op: "unset", path: ["default"] } : { op: "set", path: ["default"], value: saved });
|
|
6158
|
-
return;
|
|
6159
|
-
}
|
|
6160
|
-
if (attempt < PRESET_DEFAULT_ATTEMPTS) {
|
|
6161
|
-
presetRetry = retryLater(() => restorePresetDefault(attempt + 1), PRESET_DEFAULT_RETRY_MS);
|
|
6162
|
-
}
|
|
6163
|
-
};
|
|
7540
|
+
let savedPresetDefault;
|
|
7541
|
+
let steeredEngine;
|
|
6164
7542
|
const steerPresetDefault = (engine) => {
|
|
6165
|
-
|
|
6166
|
-
if (presets === void 0) return;
|
|
7543
|
+
steeredEngine = engine;
|
|
6167
7544
|
if (engine === "in-process") {
|
|
6168
|
-
|
|
7545
|
+
const saved = savedPresetDefault;
|
|
7546
|
+
savedPresetDefault = void 0;
|
|
7547
|
+
if (saved === void 0) return;
|
|
7548
|
+
mutatePresetDefault({ op: "set", path: ["default"], value: saved });
|
|
6169
7549
|
return;
|
|
6170
7550
|
}
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
savedPresetDefault = current;
|
|
6181
|
-
mutatePresetDefault({ op: "set", path: ["default"], value: HOSTED_PRESET_ID });
|
|
6182
|
-
})();
|
|
6183
|
-
};
|
|
6184
|
-
const cleanupEngineRegistrations = () => {
|
|
6185
|
-
if (commandDisposers !== void 0) {
|
|
6186
|
-
for (const dispose of commandDisposers) dispose();
|
|
6187
|
-
commandDisposers = void 0;
|
|
6188
|
-
}
|
|
6189
|
-
if (skillDisposer !== void 0) {
|
|
6190
|
-
skillDisposer();
|
|
6191
|
-
skillDisposer = void 0;
|
|
6192
|
-
}
|
|
6193
|
-
};
|
|
6194
|
-
const hostFactory = (engine, mount) => {
|
|
6195
|
-
if (engineFiber !== void 0) return;
|
|
6196
|
-
const generation = ++mountGeneration;
|
|
6197
|
-
const fiber = mount();
|
|
6198
|
-
engineFiber = fiber;
|
|
6199
|
-
mountedEngine = engine;
|
|
6200
|
-
void fiber.then(() => void 0, (error) => {
|
|
6201
|
-
if (generation !== mountGeneration) return;
|
|
6202
|
-
cleanupEngineRegistrations();
|
|
6203
|
-
engineFiber = void 0;
|
|
6204
|
-
mountedEngine = void 0;
|
|
6205
|
-
const baseLoopHoldsSlot = ctx.get("agentLoop") !== void 0;
|
|
6206
|
-
if (error instanceof Error && (baseLoopHoldsSlot || error.message.includes("an agent factory is already registered")) && mountAttempts < MAX_MOUNT_ATTEMPTS) {
|
|
6207
|
-
mountAttempts += 1;
|
|
6208
|
-
mountRetry = retryLater(() => {
|
|
6209
|
-
try {
|
|
6210
|
-
mountEngine(engine);
|
|
6211
|
-
} catch (retryError) {
|
|
6212
|
-
ctx.logger.error(`loop-engine: ${engine} mount retry failed: ${String(retryError)}`);
|
|
6213
|
-
}
|
|
6214
|
-
}, MOUNT_RETRY_MS);
|
|
6215
|
-
return;
|
|
6216
|
-
}
|
|
6217
|
-
ctx.logger.error(
|
|
6218
|
-
`loop-engine: ${engine} factory failed to start: ${String(error)} \u2014 restart \`dsh web\` to release the factory slot`
|
|
6219
|
-
);
|
|
7551
|
+
const started = authorEnginePresets();
|
|
7552
|
+
if (started === void 0) return;
|
|
7553
|
+
void started.settled.then((authored) => {
|
|
7554
|
+
if (!authored) return;
|
|
7555
|
+
const current = started.presets.defaultId;
|
|
7556
|
+
const target = enginePresetId(engine);
|
|
7557
|
+
if (current === target) return;
|
|
7558
|
+
if (!current.startsWith("loop-engine-")) savedPresetDefault = current;
|
|
7559
|
+
mutatePresetDefault({ op: "set", path: ["default"], value: target });
|
|
6220
7560
|
});
|
|
6221
7561
|
};
|
|
6222
|
-
const
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
if (commands !== void 0) {
|
|
6226
|
-
const disposers = [];
|
|
6227
|
-
for (const command of [...CLAUDE_CODE_COMMANDS, ...discoverUserSlashCommands()]) {
|
|
6228
|
-
try {
|
|
6229
|
-
disposers.push(commands.register(command));
|
|
6230
|
-
} catch (error) {
|
|
6231
|
-
ctx.logger.warn(`loop-engine: skip claude-code command /${command.name}: ${String(error)}`);
|
|
6232
|
-
}
|
|
6233
|
-
}
|
|
6234
|
-
commandDisposers = disposers;
|
|
6235
|
-
}
|
|
6236
|
-
const skills = ctx.get("skills");
|
|
6237
|
-
if (skills !== void 0) {
|
|
6238
|
-
skillDisposer = skills.registerProvider((control) => new ClaudeCodeSkillProvider(control));
|
|
6239
|
-
}
|
|
6240
|
-
hostFactory("claude-code", () => ctx.plugin(ClaudeCodeLoop, claudeCodeConfig(config)));
|
|
7562
|
+
const seedEngine = legacyEngine ?? "in-process";
|
|
7563
|
+
const pluginWarn = (message) => {
|
|
7564
|
+
ctx.logger.warn(message);
|
|
6241
7565
|
};
|
|
6242
|
-
const
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
7566
|
+
const engineRecords = new SessionEngineStore(resolveEngineRecordPath(), pluginWarn);
|
|
7567
|
+
const routerHolder = { current: void 0 };
|
|
7568
|
+
const buildEngine = (engineCtx, engine) => {
|
|
7569
|
+
switch (engine) {
|
|
7570
|
+
case "claude-code":
|
|
7571
|
+
return new ClaudeCodeLoop(engineCtx, claudeCodeConfig(config));
|
|
7572
|
+
case "codex":
|
|
7573
|
+
return new CodexLoop(engineCtx, codexConfig(config));
|
|
7574
|
+
case "pi":
|
|
7575
|
+
return new PiLoop(engineCtx, piConfig(config));
|
|
7576
|
+
case "kimi":
|
|
7577
|
+
return new KimiLoop(engineCtx, kimiConfig(config));
|
|
6246
7578
|
}
|
|
6247
|
-
hostFactory("codex", () => ctx.plugin(CodexLoop, codexConfig(config)));
|
|
6248
7579
|
};
|
|
6249
|
-
const
|
|
6250
|
-
const
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
ctx.logger.warn(`loop-engine: skip kimi command /${command.name}: ${String(error)}`);
|
|
7580
|
+
const mountRouter = () => {
|
|
7581
|
+
const fiber = ctx.inject(ROUTER_SERVICES, (routerCtx) => {
|
|
7582
|
+
try {
|
|
7583
|
+
const router = new RouterLoop(
|
|
7584
|
+
routerCtx,
|
|
7585
|
+
(engine) => buildEngine(routerCtx, engine),
|
|
7586
|
+
engineRecords,
|
|
7587
|
+
pluginWarn
|
|
7588
|
+
);
|
|
7589
|
+
routerHolder.current = router;
|
|
7590
|
+
} catch (error) {
|
|
7591
|
+
routerHolder.current = void 0;
|
|
7592
|
+
if (ctx.get("agentLoop") !== void 0 && routerAttempts < ROUTER_ATTEMPTS) {
|
|
7593
|
+
routerAttempts += 1;
|
|
7594
|
+
routerRetry = retryLater(() => {
|
|
7595
|
+
void Promise.resolve(fiber.dispose()).then(mountRouter, mountRouter);
|
|
7596
|
+
}, ROUTER_RETRY_MS);
|
|
7597
|
+
return;
|
|
6268
7598
|
}
|
|
7599
|
+
ctx.logger.error(`loop-engine: could not start the loop router: ${String(error)}`);
|
|
6269
7600
|
}
|
|
6270
|
-
|
|
6271
|
-
}
|
|
6272
|
-
const skills = ctx.get("skills");
|
|
6273
|
-
if (skills !== void 0) {
|
|
6274
|
-
skillDisposer = skills.registerProvider((control) => new KimiSkillProvider(control));
|
|
6275
|
-
}
|
|
6276
|
-
hostFactory("kimi", () => ctx.plugin(KimiLoop, kimiConfig(config)));
|
|
6277
|
-
};
|
|
6278
|
-
const mountEngine = (engine) => {
|
|
6279
|
-
mountProviderRoute(engine);
|
|
6280
|
-
if (engine === "claude-code") mountClaude();
|
|
6281
|
-
else if (engine === "codex") mountCodex();
|
|
6282
|
-
else if (engine === "pi") mountPi();
|
|
6283
|
-
else if (engine === "kimi") mountKimi();
|
|
7601
|
+
});
|
|
6284
7602
|
};
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
cleanupEngineRegistrations();
|
|
6293
|
-
mountedEngine = void 0;
|
|
6294
|
-
if (fiber === void 0) return;
|
|
6295
|
-
engineFiber = void 0;
|
|
6296
|
-
void fiber.then(
|
|
6297
|
-
(resolved) => {
|
|
6298
|
-
resolved.dispose().catch((error) => {
|
|
6299
|
-
ctx.logger.error(`loop-engine: ${String(engine)} factory dispose failed: ${String(error)}`);
|
|
6300
|
-
});
|
|
6301
|
-
},
|
|
6302
|
-
/* v8 ignore next -- a fiber that failed already cleared engineFiber in hostFactory's rejection handler, so this rejection arm is unreachable */
|
|
6303
|
-
() => void 0
|
|
7603
|
+
mountRouter();
|
|
7604
|
+
const mountEngineRemote = () => {
|
|
7605
|
+
new LoopEngineRemote(
|
|
7606
|
+
ctx,
|
|
7607
|
+
(sessionId) => engineOfSession(ctx, sessionId, engineRecords),
|
|
7608
|
+
routerHolder,
|
|
7609
|
+
pluginWarn
|
|
6304
7610
|
);
|
|
6305
7611
|
};
|
|
6306
|
-
|
|
6307
|
-
|
|
7612
|
+
mountEngineRemote();
|
|
7613
|
+
mountProviderRoutes();
|
|
7614
|
+
authorEnginePresets();
|
|
6308
7615
|
ctx.effect(() => () => {
|
|
6309
7616
|
disposed = true;
|
|
6310
|
-
|
|
7617
|
+
CLEAR_ROUTE_RETRY();
|
|
6311
7618
|
CLEAR_PRESET_RETRY();
|
|
6312
|
-
|
|
6313
|
-
|
|
7619
|
+
CLEAR_AUTHOR_RETRY();
|
|
7620
|
+
CLEAR_ROUTER_RETRY();
|
|
7621
|
+
releaseRoutes();
|
|
7622
|
+
}, "loop-engine: cleanup");
|
|
6314
7623
|
let source;
|
|
6315
7624
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
6316
|
-
settingsCtx.settings.installSection(
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
settingsCtx.settings.mutate(loopEngineSettingsNamespace(), [{ op: "set", path: ["engine"], value: fileEngine }]).catch((revertError) => {
|
|
6330
|
-
ctx.logger.error(`loop-engine: engine selection revert failed: ${String(revertError)}`);
|
|
6331
|
-
});
|
|
6332
|
-
}, 0);
|
|
6333
|
-
return;
|
|
6334
|
-
}
|
|
6335
|
-
fileEngine = next;
|
|
6336
|
-
if (mountedEngine !== next) {
|
|
6337
|
-
unmountEngine();
|
|
6338
|
-
mountEngine(next);
|
|
7625
|
+
settingsCtx.settings.installSection(
|
|
7626
|
+
ctx,
|
|
7627
|
+
loopEngineSettingsNamespace(),
|
|
7628
|
+
LOOP_ENGINE_SETTINGS_SCHEMA,
|
|
7629
|
+
{ engine: seedEngine, showInComposer: true },
|
|
7630
|
+
{
|
|
7631
|
+
setSource: (current) => {
|
|
7632
|
+
source = current;
|
|
7633
|
+
},
|
|
7634
|
+
onChange: () => {
|
|
7635
|
+
const next = source().engine;
|
|
7636
|
+
if (next === steeredEngine) return;
|
|
7637
|
+
steerPresetDefault(next);
|
|
6339
7638
|
}
|
|
6340
|
-
steerPresetDefault(next);
|
|
6341
7639
|
}
|
|
6342
|
-
|
|
7640
|
+
);
|
|
6343
7641
|
});
|
|
6344
7642
|
}
|
|
6345
7643
|
export {
|