dsh-loop-engine 0.1.5-rc3 → 0.1.5-rc4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -187
- package/README.zh.md +45 -103
- package/lib/client.js +887 -266
- package/lib/index.js +2212 -914
- package/lib/invariant.js +43 -45
- package/lib/types/agent-preset-ids.d.ts +303 -0
- package/lib/types/client/LoopEngineBadge.d.ts +44 -17
- package/lib/types/client/LoopEngineComposerSelect.d.ts +79 -13
- package/lib/types/client/LoopEngineSection.d.ts +5 -4
- package/lib/types/client/locales.d.ts +133 -7
- package/lib/types/client/reload.d.ts +135 -0
- package/lib/types/client/session-engine.d.ts +474 -0
- package/lib/types/client/store.d.ts +1 -1
- package/lib/types/client/turn-status.d.ts +112 -10
- package/lib/types/client/use-session-engine.d.ts +66 -0
- package/lib/types/commands.d.ts +11 -3
- package/lib/types/driver-core/host-servers.d.ts +106 -0
- package/lib/types/driver-core/hosted-engine-runtime.d.ts +190 -0
- package/lib/types/driver-core/hosted-tool-vocabulary.d.ts +72 -0
- package/lib/types/driver-core/model-handover.d.ts +116 -0
- package/lib/types/driver-core/ownership.d.ts +6 -5
- package/lib/types/driver-core/prompt.d.ts +32 -0
- package/lib/types/driver-core/session-lifetime.d.ts +62 -0
- package/lib/types/driver-core/session-model.d.ts +82 -0
- package/lib/types/engine-claude/agent.d.ts +23 -3
- package/lib/types/engine-claude/loop.d.ts +16 -15
- package/lib/types/engine-codex/agent.d.ts +22 -3
- package/lib/types/engine-codex/appserver/client.d.ts +15 -2
- package/lib/types/engine-codex/loop.d.ts +13 -15
- package/lib/types/engine-codex/model-handover.d.ts +44 -0
- package/lib/types/engine-kimi/acp/client.d.ts +10 -0
- package/lib/types/engine-kimi/agent.d.ts +19 -2
- package/lib/types/engine-kimi/commands.d.ts +18 -14
- package/lib/types/engine-kimi/loop.d.ts +14 -16
- package/lib/types/engine-kimi/model-handover.d.ts +32 -0
- package/lib/types/engine-kimi/process.d.ts +2 -2
- package/lib/types/engine-kimi/types.d.ts +1 -1
- package/lib/types/engine-of-session.d.ts +97 -0
- package/lib/types/engine-pi/agent.d.ts +25 -23
- package/lib/types/engine-pi/loop.d.ts +13 -23
- package/lib/types/engine-pi/model-handover.d.ts +35 -0
- package/lib/types/engine-pi/types.d.ts +2 -2
- package/lib/types/engine-remote.d.ts +192 -0
- package/lib/types/engine-surface.d.ts +36 -0
- package/lib/types/index.d.ts +51 -50
- package/lib/types/invariant.d.ts +8 -5
- package/lib/types/model-selection-reset.d.ts +271 -0
- package/lib/types/patch-manager.d.ts +57 -39
- package/lib/types/preset.d.ts +39 -26
- package/lib/types/provider-route.d.ts +83 -36
- package/lib/types/router-loop.d.ts +406 -0
- package/lib/types/session-engine-store.d.ts +138 -0
- package/lib/types/settings.d.ts +12 -11
- package/package.json +109 -104
package/lib/invariant.js
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
// src/settings.ts
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
|
|
4
|
+
// src/agent-preset-ids.ts
|
|
3
5
|
var LOOP_ENGINE_IDS = ["in-process", "claude-code", "codex", "pi", "kimi"];
|
|
6
|
+
var HOSTED_ENGINE_IDS = LOOP_ENGINE_IDS.filter(
|
|
7
|
+
(id) => id !== "in-process"
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
// src/settings.ts
|
|
4
11
|
var LOOP_ENGINE_SETTINGS_SCHEMA = z.object({
|
|
5
12
|
engine: z.union(LOOP_ENGINE_IDS.map((id) => z.const(id))).default("in-process"),
|
|
6
13
|
showInComposer: z.boolean().default(true)
|
|
7
14
|
});
|
|
8
15
|
|
|
9
16
|
// src/patch-manager.ts
|
|
10
|
-
var MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block
|
|
17
|
+
var MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block --";
|
|
18
|
+
var LEGACY_MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block: ";
|
|
11
19
|
var MANAGED_BLOCK_END = "# -- /dsh-loop-engine managed block --";
|
|
12
20
|
var END_MARKER_LINE = `${MANAGED_BLOCK_END}
|
|
13
21
|
`;
|
|
14
|
-
function renderManagedBlock(
|
|
15
|
-
if (engine === "in-process") return "";
|
|
22
|
+
function renderManagedBlock() {
|
|
16
23
|
return [
|
|
17
|
-
|
|
24
|
+
MANAGED_BLOCK_BEGIN,
|
|
18
25
|
"- id: agent-loop",
|
|
19
26
|
" disabled: true",
|
|
20
27
|
"- id: command-goal",
|
|
@@ -22,18 +29,18 @@ function renderManagedBlock(engine) {
|
|
|
22
29
|
END_MARKER_LINE
|
|
23
30
|
].join("\n");
|
|
24
31
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const engine = BEGIN_MARKER_RE.exec(text)?.[1];
|
|
28
|
-
return LOOP_ENGINE_IDS.includes(engine ?? "") ? engine : void 0;
|
|
32
|
+
function hasManagedBlock(text) {
|
|
33
|
+
return text.includes("# -- dsh-loop-engine managed block");
|
|
29
34
|
}
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
var LEGACY_BEGIN_MARKER_RE = /^# -- dsh-loop-engine managed block: (\S+) --$/m;
|
|
36
|
+
function legacyBlockEngineOf(text) {
|
|
37
|
+
const engine = LEGACY_BEGIN_MARKER_RE.exec(text)?.[1];
|
|
38
|
+
return LOOP_ENGINE_IDS.includes(engine ?? "") ? engine : void 0;
|
|
32
39
|
}
|
|
33
40
|
function managedSpan(text) {
|
|
34
|
-
const begin = text.indexOf(
|
|
41
|
+
const begin = text.indexOf("# -- dsh-loop-engine managed block");
|
|
35
42
|
if (begin === -1) return { head: text, tail: "", present: false, blankBefore: false };
|
|
36
|
-
const afterBegin = begin +
|
|
43
|
+
const afterBegin = begin + LEGACY_MANAGED_BLOCK_BEGIN.length;
|
|
37
44
|
const endAt = text.indexOf(MANAGED_BLOCK_END, afterBegin);
|
|
38
45
|
const spanEnd = endAt === -1 ? text.length : endAt + END_MARKER_LINE.length;
|
|
39
46
|
const before = text.slice(0, begin);
|
|
@@ -49,38 +56,18 @@ function ensureTrailingNewline(text) {
|
|
|
49
56
|
return text.endsWith("\n") ? text : `${text}
|
|
50
57
|
`;
|
|
51
58
|
}
|
|
52
|
-
function hasRootEntry(text) {
|
|
53
|
-
return /^(?:- |\[)/m.test(text);
|
|
54
|
-
}
|
|
55
59
|
function dropSeedPlaceholder(text) {
|
|
56
60
|
return text.replace(/^\[\]\n/m, "");
|
|
57
61
|
}
|
|
58
|
-
function
|
|
59
|
-
const
|
|
60
|
-
return head === "" ? "[]\n" : `${head}
|
|
61
|
-
[]
|
|
62
|
-
`;
|
|
63
|
-
}
|
|
64
|
-
function applyManagedBlock(text, engine) {
|
|
65
|
-
const block = renderManagedBlock(engine);
|
|
62
|
+
function applyManagedBlock(text) {
|
|
63
|
+
const block = renderManagedBlock();
|
|
66
64
|
const span = managedSpan(text);
|
|
67
|
-
let result;
|
|
68
65
|
if (!span.present) {
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const base = ensureTrailingNewline(text);
|
|
73
|
-
result = `${base}
|
|
74
|
-
${block}`;
|
|
75
|
-
}
|
|
76
|
-
} else if (block === "") {
|
|
77
|
-
result = span.tail.startsWith("\n") ? `${span.head}${span.tail.slice(1)}` : `${span.head}${span.tail}`;
|
|
78
|
-
} else {
|
|
79
|
-
result = `${span.head}${span.blankBefore ? "\n" : ""}${block}${span.tail}`;
|
|
66
|
+
if (text === "") return block;
|
|
67
|
+
return dropSeedPlaceholder(`${ensureTrailingNewline(text)}
|
|
68
|
+
${block}`);
|
|
80
69
|
}
|
|
81
|
-
|
|
82
|
-
if (text.trim() === "") return result;
|
|
83
|
-
return hasRootEntry(result) ? result : seedEmptyArray(result);
|
|
70
|
+
return dropSeedPlaceholder(`${span.head}${span.blankBefore ? "\n" : ""}${block}${span.tail}`);
|
|
84
71
|
}
|
|
85
72
|
|
|
86
73
|
// src/invariant.ts
|
|
@@ -91,16 +78,27 @@ var install = (ctx, fail) => {
|
|
|
91
78
|
void ctx;
|
|
92
79
|
const seed = "";
|
|
93
80
|
const commentOnly = "# dsh profile patch layer\n";
|
|
81
|
+
const applied = applyManagedBlock(seed);
|
|
82
|
+
if (applyManagedBlock(applied) !== applied) fail("the managed block is not a fixed point");
|
|
83
|
+
if (!hasManagedBlock(renderManagedBlock())) fail("the rendered block is not recognized by hasManagedBlock");
|
|
84
|
+
if (!applied.includes("- id: agent-loop")) fail("the managed block must disable the base agent-loop row");
|
|
85
|
+
if (legacyBlockEngineOf(applied) !== void 0) fail("the current block must not read as a legacy engine pin");
|
|
94
86
|
for (const engine of LOOP_ENGINE_IDS) {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
87
|
+
const head = "# user patch layer\n";
|
|
88
|
+
const legacy = `${head}
|
|
89
|
+
${LEGACY_MANAGED_BLOCK_BEGIN}${engine} --
|
|
90
|
+
- id: agent-loop
|
|
91
|
+
disabled: true
|
|
92
|
+
${MANAGED_BLOCK_END}
|
|
93
|
+
`;
|
|
94
|
+
if (legacyBlockEngineOf(legacy) !== engine) fail(`a legacy ${engine} block must still read as the ${engine} pin`);
|
|
95
|
+
if (applyManagedBlock(legacy) !== applyManagedBlock(head)) {
|
|
96
|
+
fail(`a legacy ${engine} block must migrate to the current block`);
|
|
102
97
|
}
|
|
103
98
|
}
|
|
99
|
+
if (applyManagedBlock(commentOnly) === commentOnly) {
|
|
100
|
+
fail("a comment-only file must gain the block as its top-level array");
|
|
101
|
+
}
|
|
104
102
|
};
|
|
105
103
|
var apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
106
104
|
export {
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loop engine ids, the agent-preset ids that select them, and the two plain
|
|
3
|
+
* wire shapes both halves of the plugin share — in a module with no imports at
|
|
4
|
+
* all.
|
|
5
|
+
*
|
|
6
|
+
* The mapping is pure identity arithmetic (an engine to a preset id and back),
|
|
7
|
+
* so it belongs to neither half; it lives here for the same reason
|
|
8
|
+
* `./namespace.ts` does — the browser bundle needs it, and the files that
|
|
9
|
+
* otherwise carry it are host-side: `./settings.ts` imports `schemastery` and
|
|
10
|
+
* the `dsh-settings` brand type, `./preset.ts` imports `node:fs/promises`.
|
|
11
|
+
* Importing either from `src/client/**` would pull a host package into the
|
|
12
|
+
* client artifact. Both of them re-export what this module defines, so the
|
|
13
|
+
* existing import paths and names stay valid on the node side.
|
|
14
|
+
*
|
|
15
|
+
* The two shapes are here for that same reason: {@link SessionEngine} is what
|
|
16
|
+
* the browser half renders and {@link LoopEngineSelectResult} is what its switch
|
|
17
|
+
* comes back with, so both halves name the same types without either half's
|
|
18
|
+
* modules crossing the boundary. The refusal codes travel with the second one
|
|
19
|
+
* ({@link LoopEngineRefusalCode}) for the same reason plus one more: the browser
|
|
20
|
+
* half is what turns a refusal into copy, so the codes it may read have to be
|
|
21
|
+
* knowable without importing the host's router.
|
|
22
|
+
*
|
|
23
|
+
* @module dsh-loop-engine/agent-preset-ids
|
|
24
|
+
*/
|
|
25
|
+
/** The installed engine driving new Agent turns. */
|
|
26
|
+
export declare const LOOP_ENGINE_IDS: readonly ["in-process", "claude-code", "codex", "pi", "kimi"];
|
|
27
|
+
/** Installed agent loop engine id. */
|
|
28
|
+
export type LoopEngineId = (typeof LOOP_ENGINE_IDS)[number];
|
|
29
|
+
/** An engine this plugin hosts itself; `in-process` is the harness's own loop. */
|
|
30
|
+
export type HostedEngineId = Exclude<LoopEngineId, 'in-process'>;
|
|
31
|
+
/** Every hosted engine id, in selection order. */
|
|
32
|
+
export declare const HOSTED_ENGINE_IDS: HostedEngineId[];
|
|
33
|
+
/**
|
|
34
|
+
* Whether an engine is one this plugin hosts — an external CLI some other
|
|
35
|
+
* vendor runs, which owns its own model — rather than the harness's own loop.
|
|
36
|
+
*
|
|
37
|
+
* The judgement a surface needs to say "the model is the engine's own business"
|
|
38
|
+
* without hardcoding one engine's name: every hosted engine behaves the same
|
|
39
|
+
* way here, and naming one of them would make a general fact read as a property
|
|
40
|
+
* of that engine. An engine nobody has answered for yet is not hosted: a surface
|
|
41
|
+
* that does not know says nothing rather than claiming the wrong half.
|
|
42
|
+
* @param engine - an engine id, or undefined while one is still being read.
|
|
43
|
+
* @returns whether that engine is hosted by this plugin.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isHostedEngine(engine: LoopEngineId | undefined): engine is HostedEngineId;
|
|
46
|
+
/**
|
|
47
|
+
* The single provider route label EVERY hosted engine logs into its sessions'
|
|
48
|
+
* `request/header`, and the one placeholder route this plugin registers in the
|
|
49
|
+
* llm registry.
|
|
50
|
+
*
|
|
51
|
+
* One label for all four engines, because the browser model catalog is built
|
|
52
|
+
* for the whole Host GENERATION and is not scoped to a session
|
|
53
|
+
* (`packages/api/session-controller/src/catalog.ts` — "Build the browser model
|
|
54
|
+
* catalog without requiring a Session"; it walks `ctx.llm.listProviders()`
|
|
55
|
+
* once). A per-engine label would therefore surface one provider group per
|
|
56
|
+
* engine in every session's menu at once — four identical `default` entries —
|
|
57
|
+
* which is what this constant collapses away.
|
|
58
|
+
*
|
|
59
|
+
* The value is ASCII on purpose: it is a WIRE value. It is written into each
|
|
60
|
+
* session's `request/header`, it travels through selections, and the host
|
|
61
|
+
* compares it (`providerInfo().id` must equal the provider the adapter
|
|
62
|
+
* registered for — `packages/llm/llm/src/index.ts` `prepareRoutes`). The
|
|
63
|
+
* display name ({@link HOSTED_ROUTE_NAME}) is the same token, for the reason
|
|
64
|
+
* that constant documents.
|
|
65
|
+
*/
|
|
66
|
+
export declare const HOSTED_ROUTE_LABEL = "external";
|
|
67
|
+
/**
|
|
68
|
+
* The user-visible name of the one hosted route ({@link HOSTED_ROUTE_LABEL}),
|
|
69
|
+
* shown as the model menu's provider group label.
|
|
70
|
+
*
|
|
71
|
+
* The same token as the wire value, and deliberately not localized: the catalog
|
|
72
|
+
* carries exactly one string per provider group (`ModelCatalog`'s `group.name`,
|
|
73
|
+
* taken from `LlmProviderInfo.name`) and the browser half has no hook to
|
|
74
|
+
* re-translate it, so one fixed name is what every locale sees.
|
|
75
|
+
*/
|
|
76
|
+
export declare const HOSTED_ROUTE_NAME = "external";
|
|
77
|
+
/**
|
|
78
|
+
* The one model id a hosted engine's provider route advertises to the model
|
|
79
|
+
* menu, and the model label that engine logs into its sessions'
|
|
80
|
+
* `request/header` when the deployment pins none.
|
|
81
|
+
*
|
|
82
|
+
* The two are ONE string on purpose, and that is a host-side requirement rather
|
|
83
|
+
* than a style choice: the picker builds its selection out of a catalog entry
|
|
84
|
+
* (`ModelSelect.tsx` `choices` — `model: model.id`) and resolves a session's
|
|
85
|
+
* `(provider, model)` back to that entry by comparing the pair with the logged
|
|
86
|
+
* header (`selectedIndex`), so an entry whose `id` differs from the logged
|
|
87
|
+
* label falls through to the raw `provider/model` string — how a menu ends up
|
|
88
|
+
* showing a model that does not exist. The label is the engine's own word for
|
|
89
|
+
* "whatever it decides"; `name` is the same string because `LlmModelInfo.name`
|
|
90
|
+
* is rendered verbatim and has no localization hook.
|
|
91
|
+
*/
|
|
92
|
+
export declare const HOSTED_DEFAULT_MODEL = "default";
|
|
93
|
+
/** Prefix every plugin-authored preset id carries. */
|
|
94
|
+
export declare const HOSTED_PRESET_PREFIX = "loop-engine-";
|
|
95
|
+
/** The deployment's own preset id, which selects the harness loop. */
|
|
96
|
+
export declare const SOURCE_PRESET_ID = "standard";
|
|
97
|
+
/**
|
|
98
|
+
* The pre-routing single preset id: older versions of this plugin authored one
|
|
99
|
+
* preset for every hosted engine and pinned the profile to one of them, so a
|
|
100
|
+
* session carrying this id DID run a hosted engine — but the id does not say
|
|
101
|
+
* which one. It is not a hosted preset id ({@link engineOfPreset} reads it as
|
|
102
|
+
* `undefined`, so the router keeps such a session on the harness loop), and it
|
|
103
|
+
* is not `standard` either; surfaces that name a session's engine must report it
|
|
104
|
+
* as "a hosted engine, unrecorded" rather than claim the in-process loop.
|
|
105
|
+
*/
|
|
106
|
+
export declare const LEGACY_HOSTED_PRESET_ID = "loop-engine";
|
|
107
|
+
/**
|
|
108
|
+
* The preset id that selects one engine for a session. `in-process` names the
|
|
109
|
+
* deployment's own `standard` preset: the harness loop is not something this
|
|
110
|
+
* plugin authors a preset for.
|
|
111
|
+
* @param engine - the engine a session should run.
|
|
112
|
+
* @returns the preset id to record on the session.
|
|
113
|
+
*/
|
|
114
|
+
export declare function enginePresetId(engine: LoopEngineId): string;
|
|
115
|
+
/**
|
|
116
|
+
* Whether an untyped value is one of {@link LOOP_ENGINE_IDS}.
|
|
117
|
+
*
|
|
118
|
+
* The boundary test both halves need: a value crossing the wire, the sidecar
|
|
119
|
+
* file, or a projection read is only an engine when it is one of these.
|
|
120
|
+
* @param value - the value to classify.
|
|
121
|
+
* @returns whether `value` is an installed engine id.
|
|
122
|
+
*/
|
|
123
|
+
export declare function isLoopEngineId(value: unknown): value is LoopEngineId;
|
|
124
|
+
/**
|
|
125
|
+
* The engine a preset id selects, or `undefined` for a preset this plugin does
|
|
126
|
+
* not own (any deployment-authored preset, including `standard`).
|
|
127
|
+
* @param presetId - the session's preset id, when it has one.
|
|
128
|
+
* @returns the hosted engine it names, or undefined for the harness loop.
|
|
129
|
+
*/
|
|
130
|
+
export declare function engineOfPreset(presetId: string | undefined): HostedEngineId | undefined;
|
|
131
|
+
/**
|
|
132
|
+
* What a session's recorded preset says about the engine that session runs.
|
|
133
|
+
*
|
|
134
|
+
* Not every session has an answer: the pre-routing single preset id names a
|
|
135
|
+
* hosted engine without recording which one, and a deployment that composes no
|
|
136
|
+
* presets (or a transcript whose projection is gone) records nothing at all.
|
|
137
|
+
* Both are reported as themselves — a surface that names a session's engine must
|
|
138
|
+
* never read "unknown" as the in-process loop.
|
|
139
|
+
*/
|
|
140
|
+
export type SessionEngine = {
|
|
141
|
+
readonly kind: 'engine';
|
|
142
|
+
readonly engine: LoopEngineId;
|
|
143
|
+
}
|
|
144
|
+
/** The pre-routing preset: this session ran a hosted engine, but the id does not say which. */
|
|
145
|
+
| {
|
|
146
|
+
readonly kind: 'legacy';
|
|
147
|
+
}
|
|
148
|
+
/** The session records no preset at all (a deployment that composes none, or one created before presets existed). */
|
|
149
|
+
| {
|
|
150
|
+
readonly kind: 'unset';
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* What one session's engine report carries: the engine the session ACTUALLY
|
|
154
|
+
* runs, and — when they differ — the engine its own record names for it.
|
|
155
|
+
*
|
|
156
|
+
* The two are one answer because a surface must never render the second as if it
|
|
157
|
+
* were the first. A session switched onto or off the harness loop has its agent
|
|
158
|
+
* RELEASED (`src/router-loop.ts` `move`), so the session is simply cold
|
|
159
|
+
* afterwards and its record IS what its next build runs — no second fact, no
|
|
160
|
+
* pending. This field therefore survives for the one case in which a live agent
|
|
161
|
+
* still differs from the record: the release did not take (its teardown failed,
|
|
162
|
+
* or another process wrote the record), so the session keeps running its old
|
|
163
|
+
* engine while the record names another. Reporting only the live agent would
|
|
164
|
+
* hide that a switch was accepted at all — the "显示在撒谎" bug this report
|
|
165
|
+
* exists to prevent — so both travel, and {@link pending} says which is which.
|
|
166
|
+
*/
|
|
167
|
+
export interface SessionEngineReport {
|
|
168
|
+
/** The engine driving this session NOW. */
|
|
169
|
+
readonly engine: SessionEngine;
|
|
170
|
+
/**
|
|
171
|
+
* Present only while the session has an engine recorded that its LIVE agent is
|
|
172
|
+
* not running — i.e. a recorded switch whose release did not complete. Absent
|
|
173
|
+
* whenever the record and the live agent agree, and always absent for a session
|
|
174
|
+
* with no live agent (there the record IS what the session runs).
|
|
175
|
+
*/
|
|
176
|
+
readonly pending?: LoopEngineId;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Every way this plugin refuses to move a session to another engine, as a stable
|
|
180
|
+
* code.
|
|
181
|
+
*
|
|
182
|
+
* A code exists because the host's own sentence is not what a surface shows any
|
|
183
|
+
* more: the browser half localizes the refusal from the code (`refusalFace` in
|
|
184
|
+
* `./client/locales.ts`) and keeps {@link LoopEngineRefusal.reason} only as
|
|
185
|
+
* detail, so the codes are part of the wire contract and a value may be added but
|
|
186
|
+
* never quietly repurposed. The list is exactly the refusals the host can produce
|
|
187
|
+
* — one per branch of `RouterLoop.selectEngine`, plus the Remote's own
|
|
188
|
+
* "no router mounted" answer:
|
|
189
|
+
*
|
|
190
|
+
* - `session-closed`: the session has no agent in this process (nothing is open
|
|
191
|
+
* to move);
|
|
192
|
+
* - `turn-running`: a turn is in flight, and it is never interrupted;
|
|
193
|
+
* - `subagent-session`: a delegated child's agent belongs to its delegation;
|
|
194
|
+
* - `not-driven`: an agent is live but this router did not build it (the mount
|
|
195
|
+
* window in which the base loop still owns the factory slot);
|
|
196
|
+
* - `router-unmounted`: no router is mounted at all yet;
|
|
197
|
+
* - `record-failed`: the plugin's own per-session record could not be written,
|
|
198
|
+
* so the choice could not be committed;
|
|
199
|
+
* - `rebuild-failed`: the outgoing machine was retired, the successor could not
|
|
200
|
+
* be built, and the session was left cold on the recorded engine.
|
|
201
|
+
*
|
|
202
|
+
* A malformed REQUEST is deliberately NOT one of these: it travels as a
|
|
203
|
+
* `RemoteError` with `gateway/bad-request`, because it is the caller's fault
|
|
204
|
+
* rather than a state of the session.
|
|
205
|
+
*/
|
|
206
|
+
export declare const LOOP_ENGINE_REFUSAL_CODES: readonly ["session-closed", "turn-running", "subagent-session", "not-driven", "router-unmounted", "record-failed", "rebuild-failed"];
|
|
207
|
+
/** Why one session's engine switch was refused. */
|
|
208
|
+
export type LoopEngineRefusalCode = (typeof LOOP_ENGINE_REFUSAL_CODES)[number];
|
|
209
|
+
/**
|
|
210
|
+
* Whether an untyped value is one of {@link LOOP_ENGINE_REFUSAL_CODES}.
|
|
211
|
+
*
|
|
212
|
+
* The boundary test the browser half needs: a code is only a code when this
|
|
213
|
+
* build knows it, so a value it does not recognize is normalized away and the
|
|
214
|
+
* host's `reason` stays readable instead (`./client/session-engine.ts`
|
|
215
|
+
* `parseSelectResult`).
|
|
216
|
+
* @param value - the value to classify.
|
|
217
|
+
* @returns whether `value` is a known refusal code.
|
|
218
|
+
*/
|
|
219
|
+
export declare function isLoopEngineRefusalCode(value: unknown): value is LoopEngineRefusalCode;
|
|
220
|
+
/**
|
|
221
|
+
* What one attempt to move a session to another engine produced.
|
|
222
|
+
*
|
|
223
|
+
* A refusal is a VALUE rather than a thrown error: every reason below is a
|
|
224
|
+
* predictable state of the session (it is not open, it is mid-turn, it belongs
|
|
225
|
+
* to subagent routing) that the surface must show — localized by
|
|
226
|
+
* {@link LoopEngineRefusal.code}, with the host's own sentence kept as detail —
|
|
227
|
+
* and only a malformed REQUEST is the caller's fault (`gateway/bad-request`).
|
|
228
|
+
*/
|
|
229
|
+
export type LoopEngineSelectResult = {
|
|
230
|
+
readonly ok: true;
|
|
231
|
+
readonly engine: LoopEngineId;
|
|
232
|
+
/**
|
|
233
|
+
* Present when the switch was made to land by RELEASING the session's live
|
|
234
|
+
* agent rather than by moving it: a change with the harness loop on either
|
|
235
|
+
* side of it cannot be applied in place, so the agent is torn down, the
|
|
236
|
+
* session goes cold, and the record's engine is what its next build uses.
|
|
237
|
+
*
|
|
238
|
+
* The page must reload for that next build to happen: releasing publishes
|
|
239
|
+
* `session/disposed`, which the browser half reads as this session being
|
|
240
|
+
* gone — with no way back in that page's lifetime — so a reload (and a
|
|
241
|
+
* re-open of the session) is what replaces the page state with a fresh list
|
|
242
|
+
* and a fresh build. The flag is how the surface knows to do that instead of
|
|
243
|
+
* leaving the user on a session that looks broken.
|
|
244
|
+
*/
|
|
245
|
+
readonly reload?: true;
|
|
246
|
+
}
|
|
247
|
+
/** The switch did not happen; the branch that refused it is `code`. */
|
|
248
|
+
| {
|
|
249
|
+
readonly ok: false;
|
|
250
|
+
/**
|
|
251
|
+
* Which refusal this is, for the surface to localize
|
|
252
|
+
* ({@link LoopEngineRefusalCode}). Every refusal this plugin's host produces
|
|
253
|
+
* carries one — `refuse(code, reason)` in `./router-loop.ts` takes it as a
|
|
254
|
+
* required argument, so the node half cannot forget it.
|
|
255
|
+
*
|
|
256
|
+
* Optional in the SHAPE only, because the browser half is what reads it off
|
|
257
|
+
* the wire: a code this build does not know (a newer host, or a hand-written
|
|
258
|
+
* answer) is normalized to absent so that `reason` — which is always there —
|
|
259
|
+
* is what the user reads, instead of the whole answer failing the boundary
|
|
260
|
+
* check and leaving the surface with nothing to show.
|
|
261
|
+
*/
|
|
262
|
+
readonly code?: LoopEngineRefusalCode;
|
|
263
|
+
/**
|
|
264
|
+
* The host's own complete sentence about this session. It is DETAIL, not the
|
|
265
|
+
* message any more: a surface shows its own localized copy for `code` and
|
|
266
|
+
* keeps this beside it (small print) or falls back to it when `code` is
|
|
267
|
+
* absent.
|
|
268
|
+
*/
|
|
269
|
+
readonly reason: string;
|
|
270
|
+
};
|
|
271
|
+
/**
|
|
272
|
+
* The engine one session's recorded preset says it runs, with the harness's own
|
|
273
|
+
* fallback for a preset this plugin does not own: the deployment's `standard` and
|
|
274
|
+
* anything else a deployment authored keep the session on the harness loop.
|
|
275
|
+
*
|
|
276
|
+
* The two answers that are not an engine stay distinct: {@link LEGACY_HOSTED_PRESET_ID}
|
|
277
|
+
* is a hosted engine whose name was never recorded, and a missing preset is this
|
|
278
|
+
* session recording nothing.
|
|
279
|
+
*
|
|
280
|
+
* This is the ONE preset-id → engine judgement: the router routes on what it
|
|
281
|
+
* returns and the plugin's own Remote reports it, so "what the session runs" and
|
|
282
|
+
* "what the session is shown as running" cannot disagree
|
|
283
|
+
* (`src/engine-of-session.ts` supplies the preset id from the durable log;
|
|
284
|
+
* `src/engine-remote.ts` publishes the answer to the browser half).
|
|
285
|
+
* @param presetId - the session's recorded preset id, when it has one. Reads off
|
|
286
|
+
* an untyped projection, so anything that is not a string is as good as no
|
|
287
|
+
* preset at all.
|
|
288
|
+
* @returns what that id says about the session's engine.
|
|
289
|
+
*/
|
|
290
|
+
export declare function sessionEngineOf(presetId: unknown): SessionEngine;
|
|
291
|
+
/**
|
|
292
|
+
* The hosted engine one {@link SessionEngine} names, or `undefined` for every
|
|
293
|
+
* answer that is not this plugin's own engine preset.
|
|
294
|
+
*
|
|
295
|
+
* This is the router's half of the judgement: the harness loop still owns
|
|
296
|
+
* `in-process`, the pre-routing id and an unrecorded session both run whatever
|
|
297
|
+
* the deployment's own composition gives them (the harness loop).
|
|
298
|
+
* @param session - what a session's preset says about its engine.
|
|
299
|
+
* @returns the hosted engine to build a driver runtime for, or undefined for the
|
|
300
|
+
* harness loop.
|
|
301
|
+
*/
|
|
302
|
+
export declare function hostedEngineOf(session: SessionEngine): HostedEngineId | undefined;
|
|
303
|
+
//# sourceMappingURL=agent-preset-ids.d.ts.map
|
|
@@ -1,34 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Session header engine
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Session header engine chip: a read-only pill naming the engine THIS session
|
|
3
|
+
* runs, read from this plugin's own Remote — which answers from the plugin's own
|
|
4
|
+
* per-session record and, for a session with none, from the session's durable
|
|
5
|
+
* log: the same read the router routes on, so the chip cannot disagree with what
|
|
6
|
+
* the session is actually driven by.
|
|
7
|
+
*
|
|
8
|
+
* It deliberately does NOT read the client session list's `agentPreset`
|
|
9
|
+
* projection: that value is a cache-shaped hint, and it names the preset the
|
|
10
|
+
* session was CREATED with, which is the wrong answer both for a session that
|
|
11
|
+
* switched while it was blank and for one that switched after it started. The
|
|
12
|
+
* harness's own preset label reads that same hint and lags the same way; this
|
|
13
|
+
* chip does not.
|
|
14
|
+
*
|
|
15
|
+
* The settings section's engine is NOT what this chip shows: that value is only
|
|
16
|
+
* the default for sessions created later, and a session created before a default
|
|
17
|
+
* change keeps running the engine it has. The two sessions that name no engine
|
|
18
|
+
* are reported as themselves rather than as the in-process loop: the pre-routing
|
|
19
|
+
* single preset id reads as "legacy hosted engine" (it ran a hosted engine, the
|
|
20
|
+
* id just never recorded which), and a session whose engine is not known — no
|
|
21
|
+
* record, no preset, or no answer from the host — renders nothing, which is also
|
|
22
|
+
* what the chip does until the host's first answer arrives.
|
|
23
|
+
*
|
|
24
|
+
* "What the session runs" is the ACTUAL engine, and for a live session that is
|
|
25
|
+
* the agent in front of it rather than the plugin's record: a switch onto the
|
|
26
|
+
* harness loop makes the host RELEASE the session's agent and reload the page,
|
|
27
|
+
* and if that release did not take, the session keeps running the engine it had
|
|
28
|
+
* while its record names another. This chip never renders the record as its name;
|
|
29
|
+
* it appends the `切到 X · 尚未接管` marker beside the engine that really runs,
|
|
30
|
+
* and its tooltip says what to do about it (`pendingSessionNotice`). See
|
|
31
|
+
* `SessionEngineReport` in `src/agent-preset-ids.ts`.
|
|
6
32
|
*
|
|
7
33
|
* Styling is token-driven inline styles like the settings section (the
|
|
8
34
|
* client-module bundle is esbuild-built without a CSS loader).
|
|
9
35
|
* @module dsh-loop-engine/client/badge
|
|
10
36
|
*/
|
|
11
37
|
import type { JSX } from 'react';
|
|
12
|
-
import type {
|
|
13
|
-
import type {
|
|
14
|
-
import type
|
|
15
|
-
import type { en } from './locales.ts';
|
|
38
|
+
import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
39
|
+
import type { SessionEngineCache, SessionSeat } from './session-engine.ts';
|
|
40
|
+
import { type en } from './locales.ts';
|
|
16
41
|
/** Registration-side business face for the header badge. */
|
|
17
42
|
export interface LoopEngineBadgeInjected {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
snapshot: SnapshotStore<LoopEngineState>;
|
|
21
|
-
};
|
|
43
|
+
/** The plugin's authoritative per-session engine cache. */
|
|
44
|
+
sessionEngines: SessionEngineCache;
|
|
22
45
|
/** Section copy bound to the engine dictionaries. */
|
|
23
46
|
t: (key: keyof typeof en) => string;
|
|
24
47
|
}
|
|
25
|
-
/** Props delivered by the slot outlet (the renderer erases the share boundary). */
|
|
26
|
-
export type LoopEngineBadgeProps = Partial<InjectFace<LoopEngineBadgeInjected>>;
|
|
27
48
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
49
|
+
* Props delivered by the slot outlet (the renderer erases the share boundary).
|
|
50
|
+
* The seat members stay partial here so the registration face matches the slot's
|
|
51
|
+
* own props; {@link BadgeFace} asserts them the way the component reads them.
|
|
52
|
+
*/
|
|
53
|
+
export type LoopEngineBadgeProps = PropsRuntime<'conversation.session.header.actions'> & Partial<SessionSeat> & Partial<InjectFace<LoopEngineBadgeInjected>>;
|
|
54
|
+
/**
|
|
55
|
+
* Render the session header's loop-engine chip for the session on screen.
|
|
30
56
|
* @param props - composed slot props.
|
|
31
|
-
* @returns the chip, or null
|
|
57
|
+
* @returns the chip, or null when the seat carries no session to speak about or
|
|
58
|
+
* the session's engine is not known yet (or not recorded at all).
|
|
32
59
|
*/
|
|
33
60
|
export declare function LoopEngineBadge(props: LoopEngineBadgeProps): JSX.Element | null;
|
|
34
61
|
//# sourceMappingURL=LoopEngineBadge.d.ts.map
|
|
@@ -1,12 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Composer loop-engine picker: a compact dropdown registered at the
|
|
3
3
|
* `conversation.input.right` seat, so it sits immediately left of the model
|
|
4
|
-
* select in the composer's tool row.
|
|
5
|
-
*
|
|
6
|
-
* the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* select in the composer's tool row.
|
|
5
|
+
*
|
|
6
|
+
* It picks the engine of the session it is rendered in — the picker's value is
|
|
7
|
+
* this plugin's own authoritative read of that session's engine, and a pick
|
|
8
|
+
* moves the session over this plugin's own `loopEngine/select` Remote. Any
|
|
9
|
+
* session can be moved, at any point in its life, as long as it is open and no
|
|
10
|
+
* turn is in flight, and the host picks one of two ways to make the pick land:
|
|
11
|
+
*
|
|
12
|
+
* - between two hosted engines it REBUILDS that session's agent IN PLACE, on the
|
|
13
|
+
* session's own live Session object, so the conversation it is rendered beside
|
|
14
|
+
* never closes;
|
|
15
|
+
* - when the harness loop is on either side of the change, the host RELEASES
|
|
16
|
+
* that session's agent and answers `reload: true`: the session goes cold with
|
|
17
|
+
* its record naming the new engine, this page is reloaded (which is what
|
|
18
|
+
* clears the client state a `session/disposed` leaves behind), and the page
|
|
19
|
+
* that comes back opens the same session again — the host then builds it on
|
|
20
|
+
* the engine the record names (`src/client/reload.ts`). This control says so
|
|
21
|
+
* in a notice rather than letting the reload look like a glitch.
|
|
22
|
+
*
|
|
23
|
+
* Either way the host refuses one that is
|
|
24
|
+
* running, and a refusal leaves the session's engine untouched — the label goes
|
|
25
|
+
* back to the engine the session actually runs. The refusal is rendered from its
|
|
26
|
+
* CODE in the user's own language (`refusalFace`), with the host's own sentence
|
|
27
|
+
* kept as detail — never as the message itself, which is how a raw
|
|
28
|
+
* `session "…" is running; …` used to reach the user.
|
|
29
|
+
*
|
|
30
|
+
* A pick commits as soon as the host can apply it, with ONE exception: when the
|
|
31
|
+
* target and the engine this session ACTUALLY runs differ in being the in-process
|
|
32
|
+
* one, the host cannot hand the session over in place, so the switch releases its
|
|
33
|
+
* agent and reloads this page — and a reload costs this page's scroll position and
|
|
34
|
+
* any unsent draft in it. That one pick is staged behind a confirmation
|
|
35
|
+
* (`switchNeedsReload(报告里的实际引擎, 目标引擎)`, resolved before anything is
|
|
36
|
+
* sent) and only then committed; a pick between two hosted engines, which swaps
|
|
37
|
+
* the agent in place and reloads nothing, still commits immediately and opens no
|
|
38
|
+
* dialog at all. That judgement is only ever made about an engine somebody knows:
|
|
39
|
+
* until the host has answered what this session runs there is no engine to judge,
|
|
40
|
+
* and a session with no answer may reload in EITHER direction — so the control is
|
|
41
|
+
* DISABLED for as long as it is reading (`engineSwitchReady`), rather than
|
|
42
|
+
* guessing a direction. This control deliberately asks NOTHING else: whether the session
|
|
43
|
+
* can be moved right now is the host's own judgement — it refuses one that is not
|
|
44
|
+
* open, one that is mid-turn, and a subagent's own session, each with a code and a
|
|
45
|
+
* sentence — so a pick that cannot land costs nothing, and the control never
|
|
46
|
+
* reads an idle hint off a cached session list.
|
|
47
|
+
*
|
|
48
|
+
* So it opens two dialogs, with different jobs: the CONFIRMATION (two buttons,
|
|
49
|
+
* cancel and switch) that a reloading pick must pass before it is sent, and the
|
|
50
|
+
* NOTICE (one button, close) that reports what the host answered — the refusal's
|
|
51
|
+
* localized copy with the host's sentence as detail, or this plugin's own
|
|
52
|
+
* sentence for a reloaded pick.
|
|
53
|
+
*
|
|
54
|
+
* WITH a session, the settings default is never shown — not even while the first
|
|
55
|
+
* answer is still in flight: a session on the pre-routing single preset id reads
|
|
56
|
+
* "legacy hosted engine", one whose engine is not recorded (or not readable)
|
|
57
|
+
* reads "not recorded", and a session whose engine has not been answered yet
|
|
58
|
+
* reads "reading" AND is disabled while it does (see above) — a default or a stale
|
|
59
|
+
* hint would be a claim about a session this control has no facts for, and a pick
|
|
60
|
+
* would have to be judged against an engine nobody knows. Without a session (the
|
|
61
|
+
* seat renders only with one, so this is the defensive branch) the trigger names
|
|
62
|
+
* the default and a pick writes it — immediately, like every other pick here, and
|
|
63
|
+
* with the control usable from the start: a new-session page is waiting for
|
|
64
|
+
* nothing, and that pick reloads nothing. The settings section's own picker is the
|
|
65
|
+
* one that still stages its choice behind a confirmation.
|
|
10
66
|
*
|
|
11
67
|
* Styling is token-driven inline styles like the badge and section (the
|
|
12
68
|
* client-module bundle is esbuild-built without a CSS loader).
|
|
@@ -14,9 +70,10 @@
|
|
|
14
70
|
*/
|
|
15
71
|
import { type JSX } from 'react';
|
|
16
72
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
|
|
17
|
-
import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots';
|
|
73
|
+
import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
18
74
|
import type { LoopEngineStore, LoopEngineState } from './store.ts';
|
|
19
|
-
import type
|
|
75
|
+
import { type SessionEngineCache, type SessionEngineSwitcher, type SessionSeat } from './session-engine.ts';
|
|
76
|
+
import { type en } from './locales.ts';
|
|
20
77
|
/** Injected dependencies of {@link LoopEngineComposerSelect} (slot `inject`). */
|
|
21
78
|
export interface LoopEngineComposerSelectInjected {
|
|
22
79
|
/** The selection store (loaded on mount, refreshed by scope pushes). */
|
|
@@ -25,16 +82,25 @@ export interface LoopEngineComposerSelectInjected {
|
|
|
25
82
|
/** Engine snapshot bound by the UI renderer as useSnapshot. */
|
|
26
83
|
snapshot: SnapshotStore<LoopEngineState>;
|
|
27
84
|
};
|
|
85
|
+
/** The plugin's authoritative per-session engine cache. */
|
|
86
|
+
sessionEngines: SessionEngineCache;
|
|
87
|
+
/** Move one session to another engine. */
|
|
88
|
+
switchEngine: SessionEngineSwitcher;
|
|
28
89
|
/** Composer copy bound to the loop engine dictionaries. */
|
|
29
90
|
t: (key: keyof typeof en) => string;
|
|
30
91
|
}
|
|
31
|
-
/** Props delivered by the slot outlet (the renderer erases the share boundary). */
|
|
32
|
-
export type LoopEngineComposerSelectProps = Partial<InjectFace<LoopEngineComposerSelectInjected>>;
|
|
33
92
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
93
|
+
* Props delivered by the slot outlet (the renderer erases the share boundary).
|
|
94
|
+
* The seat members stay partial here so the registration face matches the slot's
|
|
95
|
+
* own props; {@link ComposerFace} asserts them the way the component reads them.
|
|
96
|
+
*/
|
|
97
|
+
export type LoopEngineComposerSelectProps = PropsRuntime<'conversation.input.right'> & Partial<SessionSeat> & Partial<InjectFace<LoopEngineComposerSelectInjected>>;
|
|
98
|
+
/**
|
|
99
|
+
* Render the composer's loop-engine dropdown for the session on screen. Hides
|
|
100
|
+
* until the settings scope settles, so the picker never flashes a provisional
|
|
101
|
+
* default while a session's own engine is already known.
|
|
36
102
|
* @param props - composed slot props.
|
|
37
|
-
* @returns the picker, or null while the
|
|
103
|
+
* @returns the picker, or null while the picker is unavailable or switched off.
|
|
38
104
|
*/
|
|
39
105
|
export declare function LoopEngineComposerSelect(props: LoopEngineComposerSelectProps): JSX.Element | null;
|
|
40
106
|
//# sourceMappingURL=LoopEngineComposerSelect.d.ts.map
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Loop engine settings section component: one dropdown choosing the
|
|
3
|
-
* loop engine, backed by the duplicated settings scope
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Loop engine settings section component: one dropdown choosing the DEFAULT
|
|
3
|
+
* agent loop engine for new sessions, backed by the duplicated settings scope
|
|
4
|
+
* through the inject face. The commit is confirmed first because it changes
|
|
5
|
+
* what every future session runs; sessions that already exist keep the engine
|
|
6
|
+
* they run, so nothing is interrupted and nothing reloads.
|
|
6
7
|
*
|
|
7
8
|
* Styling is token-driven like the rest of the settings shell (`--dsw-*`
|
|
8
9
|
* aliases), with the picker rendered through the shared `Menu` primitive and
|