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