dsh-llm-fallbacks 0.6.4 → 0.6.6
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 +3 -3
- package/README.zh-CN.md +2 -2
- package/dist/index.js +155 -104
- package/dist/role-persona.d.ts +71 -0
- package/dist/subagents-seam.d.ts +83 -70
- package/package.json +26 -25
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|

|
|
8
8
|

|
|
9
9
|

|
|
10
|
-

|
|
11
11
|
[](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
|
|
12
12
|
|
|
13
13
|
Automatic provider/model fallback chains for dsh (DeepSeek Harness): when an agent's LLM requests keep failing — retries exhausted, auth errors, quota exceeded, rate limiting (429) — the plugin switches provider/model along the fallback chain for the current role, and the current step/turn continues on the target model: tasks are not interrupted by model problems.
|
|
@@ -94,7 +94,7 @@ Build the section up in four steps:
|
|
|
94
94
|
|
|
95
95
|
**2. Set the all-day `rootChain`.** Leading entries are the fallback chain, walked first when a request fails; the **last** entry is the Default model.
|
|
96
96
|
|
|
97
|
-
> **Conformance**: the last entry must be exactly one official model — `deepseek-official/deepseek-flash` or `deepseek-official/deepseek-v4-pro` (XOR). The settings card and gateway reject any other tail on save; a legacy non-official tail warns at startup and keeps working as a fallback-only walk, but cannot be saved as-is. The retired `deepseek-v4-flash` id is no longer a legal tail — a saved tail with it now warns, goes inert (slot rows + virtual picker), and blocks save until a legal tail is picked. The official Pro tail is catalog-served as `deepseek-official/deepseek-v4-pro` (dsh 0.2.0-rc.
|
|
97
|
+
> **Conformance**: the last entry must be exactly one official model — `deepseek-official/deepseek-flash` or `deepseek-official/deepseek-v4-pro` (XOR). The settings card and gateway reject any other tail on save; a legacy non-official tail warns at startup and keeps working as a fallback-only walk, but cannot be saved as-is. The retired `deepseek-v4-flash` id is no longer a legal tail — a saved tail with it now warns, goes inert (slot rows + virtual picker), and blocks save until a legal tail is picked. The official Pro tail is catalog-served as `deepseek-official/deepseek-v4-pro` (dsh 0.2.0-rc.2) and selectable in the card; a hand-written legacy `deepseek-official/deepseek-pro` tail — never catalog-served — reads back as non-conforming (no migration; re-pick the tail). The plugin does not probe catalog availability — a chain containing `deepseek-official/deepseek-v4-pro` dispatches to it like any other exact entry, and on the virtual route the `stream()` delegate serves the first dispatchable exact head of the effective chain, so a chain with a working entry before Pro still routes to that earlier entry.
|
|
98
98
|
|
|
99
99
|
**3. Add `timeSlots` (optional).** Rows rotate the effective root chain by wall-clock windows. Preset rows use frozen UTC+8 windows (only their chain is editable; while a preset row exists, `tz` locks to `Asia/Shanghai`); custom rows take `start`/`end` (may wrap midnight) and an optional `days` list. The first row whose window contains the current moment wins; no match → the all-day `rootChain`. Rotation is a routing seed — it applies on the next root request and consumes no cooldown (see [Time slots](#time-slots)).
|
|
100
100
|
|
|
@@ -187,7 +187,7 @@ The durable fix belongs upstream at the migration edges (the frozen V0→V1 edge
|
|
|
187
187
|
- **Chain as root primary from the picker**: whenever the plugin has content configured, the host model picker (web and TUI alike) shows a virtual `FallbacksChain` / `Auto` row — selecting it uses the configured chain as the root primary (a conforming all-day head is required for the delegation to succeed); selecting a real model keeps fallback-only (see [FallbacksChain in the model picker](#fallbackschain-in-the-model-picker)).
|
|
188
188
|
- **Time slots**: optional `fallbacks.timeSlots` rows rotate the effective root chain by wall-clock windows in the config-level `tz` timezone (default `Asia/Shanghai`) — four frozen UTC+8 presets (`liang-peak` / `liang-valley` / `glm-peak` / `glm-valley`, windows are code constants, models-only edits) or custom `start`/`end`/`days` windows. The first matching row wins; the all-day row is always last. A slot change applies on the **next** root request and is logged as a **time-slot switch** — a routing seed, never a failure decision: it consumes no cooldown and does not count against `maxSwitchesPerStep`. Failure walks keep the **fallback switch** copy (see [Time-slot presets](#time-slot-presets)).
|
|
189
189
|
- **Dispatch-time role resolution**: on a subagent's first request its role is resolved in three stages — explicit (`agentPreset` matches a declared role id) → deterministic rules (matching either the recorded pair or the served head — see [FallbacksChain in the model picker](#fallbackschain-in-the-model-picker)) → LLM auto-match from the declared role taxonomy (`fallbacks.roleAutoMatch`, default `true`). The resolved role's chain-head model is injected into the first request and recorded via an explicit `role → model` log line (no durable `fallbacks/switch` event is written — issue #52 stop-write); set `roleAutoMatch: false` to disable the LLM auto-match stage (the explicit `agentPreset` stage still applies — with no explicit role this reproduces the previous rules-only behavior). The settings card always renders an **Enable role auto-match** switch (default `true`) to toggle it — the schema default applies even to legacy configs that never declared the key.
|
|
190
|
-
- **Role persona on the child (chain-independent)**: when a subagent's dispatch declares a role this plugin knows (the `**Execute as**: <id>` field of its Assignment), that role's `persona` is installed on the child as
|
|
190
|
+
- **Role persona on the child (chain-independent)**: when a subagent's dispatch declares a role this plugin knows (the `**Execute as**: <id>` field of its Assignment), that role's `persona` is installed on the child as a scoped system-prompt section at the `deployment:persona-prefix` slot — the same slot the deployment persona uses, so the role persona **replaces** the deployment prefix for that child instead of stacking under it. The role is the child's identity, not only a routing decision, and delivery does not depend on routing: a role with an empty `chain` gets its persona exactly like a chained one (the chain only picks the model). That section lives on the child's own context, so the persona is **not durable**: a role child resumed after a host restart runs without its role persona (the resumed descriptor cannot carry one). A same-named section the child's scope already holds (a persisted descriptor) counts as delivered; any other registration failure is contained — the child runs unchanged and its notice row carries the ` (persona not applied)` suffix. Role resolution and the catalog record still apply, but two accepted boundaries get no persona delivery at all: an external (acp) child creates no local agent, so it gets **no persona, no notice row and no badge**, and a host that never mounts the `systemPrompt` service runs the child with no persona and **no** ` (persona not applied)` suffix (issue `I-000059`).
|
|
191
191
|
- **Role announced inside the subagent's own session**: a child dispatched with a declared role gets exactly one plugin notice row in its own session, naming the role it was dispatched as — `[role: <id>]`, with ` (persona not applied)` when the role declares a persona that was skipped — written once at the child's first non-empty pre-step, never for a root session and never for `inherit`/unresolved dispatches. The row is a durable transcript row, so the role stays readable in the child's session after it settles.
|
|
192
192
|
- **Subagent role badge**: when a subagent is **dispatched with a declared role** — the `**Execute as**: <id>` field of its Assignment, the same declaration that carries the persona, and the badge's ONLY source (policy on or off) — its session shows a compact role badge next to the session's agent-preset (mode) chip in the web session header; hover shows `role → latest request route` (the route of the newest recorded request, not the dispatch-time route). Only the DECLARED header is read, so a child whose role came from another stage (`agentPreset` match, a role rule, or the LLM auto-match) but which declares no Assignment header is still routed as that role yet shows **no** badge — the badge never reports a role inferred after the fact. `inherit`/unresolved sessions render no badge. The badge is durable: it is read from the child's own session record, so it is still shown for a **settled** subagent session and **after a host restart**.
|
|
193
193
|
- **Context-window-aware fallback**: `triggerCodes` accepts any dsh failure code, including `CONTEXT_WINDOW_EXCEEDED` — a request too big for the current model fails over to a candidate with a **larger** context window (candidates that cannot fit either are skipped), and because the route itself is healthy the switch is request-scoped: no cooldown, no half-open probe burned (see [Fallback triggers](#fallback-triggers-triggercodes)).
|
package/README.zh-CN.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|

|
|
8
8
|

|
|
9
9
|

|
|
10
|
-

|
|
11
11
|
[](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
|
|
12
12
|
|
|
13
13
|
dsh(DeepSeek Harness)的自动模型降级插件:当 root agent 或 subagent 的模型请求持续失败(重试耗尽、权限、配额超限、限流 429)时,按角色/模型 fallback 链自动切换 provider/model,当前 step/turn 在目标模型上继续完成——任务不因模型问题中断。
|
|
@@ -187,7 +187,7 @@ pnpm repair:session-logs -- --drop-legacy-events --apply --backup # 此处必
|
|
|
187
187
|
- **选择器里把链当主模型**:只要插件配置了内容,宿主模型选择器(web 与 TUI 一致)即出现虚拟 `FallbacksChain` / `Auto` 行——选中它即以配置的链作为 root 主模型(需要 all-day 链头合规才能成功委托);选真实模型则保持 fallback-only(见 [模型选择器中的 FallbacksChain](#模型选择器中的-fallbackschain))。
|
|
188
188
|
- **峰谷无忧(分时切换)**:可选的 `fallbacks.timeSlots` 行按墙钟窗口(配置级 `tz` 时区,默认 `Asia/Shanghai`)轮换 root 生效链——四个冻结的 UTC+8 预设(`liang-peak` / `liang-valley` / `glm-peak` / `glm-valley`,窗口为代码常量、仅模型链可编辑),或自定义 `start`/`end`/`days` 窗口。第一条命中的行生效;全时段行固定最后。时段切换在**下一个** root 请求生效,日志记为**分时切换**——路由种子而非失败决策:不消耗冷却、不计入 `maxSwitchesPerStep`。失败降级保留**降级切换**文案(见 [分时槽预设(分时切换)](#分时槽预设分时切换))。
|
|
189
189
|
- **派发时角色解析**:在 subagent 的首次请求上,其角色按三个阶段解析——显式(`agentPreset` 匹配已声明角色 id)→ 确定性规则(匹配记录 pair 或实际服务的链头——见 [模型选择器中的 FallbacksChain](#模型选择器中的-fallbackschain))→ LLM 自动匹配(从已声明角色体系中选择,`fallbacks.roleAutoMatch` 默认 `true`)。解析出的角色的链头模型注入首次请求,并以显式 `role → model` 日志行记录(不写 durable `fallbacks/switch` 事件——issue #52 停写);设 `roleAutoMatch: false` 仅关闭 LLM 自动匹配阶段(显式 `agentPreset` 阶段仍生效——无显式角色时即复现原有仅规则行为)。设置卡总是渲染「启用角色自动匹配」开关(默认 `true`)以切换之——即使是从未声明过该键的旧配置,schema 默认值同样生效。
|
|
190
|
-
- **角色人格注入到子代理(与链无关)**:当 subagent 的派发声明了本插件已知的角色(Assignment 的 `**Execute as**: <id>` 字段)时,该角色的 `persona`
|
|
190
|
+
- **角色人格注入到子代理(与链无关)**:当 subagent 的派发声明了本插件已知的角色(Assignment 的 `**Execute as**: <id>` 字段)时,该角色的 `persona` 会以作用域化的 system-prompt section 安装到子代理上,位置是 `deployment:persona-prefix` 槽位——与部署人格同一个槽位,因此角色人格**替换**该子代理的部署前缀,而不是叠加在其下。角色就是子代理的身份,而不只是路由决策;注入与路由无关:`chain` 为空的角色与带链角色的人格注入完全一致(链只负责选模型)。该 section 只存在于子代理自身的上下文中,因此人格**不持久**:宿主重启后恢复的角色子代理将不带其角色人格运行(恢复的 descriptor 无法承载人格)。子代理作用域中已有同名 section(持久化的 descriptor)时视为已投递;其他注册失败都会被兜住——子代理照常运行,其 notice 行会带上 ` (persona not applied)` 后缀。角色解析与 catalog 记录仍然生效,但两类已接受的边界完全没有人格投递:external(acp)子代理不创建本地 agent,因此**没有人格、没有 notice 行、也没有徽标**;宿主若从未挂载 `systemPrompt` 服务,子代理将不带人格运行,且**不会**带上 ` (persona not applied)` 后缀(issue `I-000059`)。
|
|
191
191
|
- **角色在子代理自己的会话中声明**:声明了角色的派发会让子代理自身的会话得到恰好一条插件 notice 行,写明它被派发为哪个角色——`[role: <id>]`,当角色声明了人格但被跳过时追加 ` (persona not applied)`——在子代理首个非空 pre-step 写入一次,root 会话以及 `inherit`/未解析角色的派发永不写入。该行是持久轨迹行,子代理结束后角色仍可在其会话中读到。
|
|
192
192
|
- **子代理角色徽标**:当 subagent 被**声明了角色地派发**——即其 Assignment 中的 `**Execute as**: <id>` 字段,也就是承载人格的同一处声明,并且是徽标的**唯一**来源(策略开或关皆同)——时,其会话在 Web 会话头部的标题旁显示一个紧凑的角色徽标;悬停显示 `role → 最近一次请求的路由`(最近一条已记录请求的路由,而非派发时的路由)。由于只读取被声明的头部字段,通过其他阶段解析出角色(`agentPreset` 匹配、role 规则或 LLM 自动匹配)但**未**在 Assignment 头部声明角色的子代理仍会按该角色路由,却**不**显示徽标——徽标绝不报告事后推断的角色。`inherit`/未解析角色的会话不显示徽标。徽标是持久的:它读取子代理自身的会话记录,因此**已结束的子代理会话**与**宿主重启之后**仍然显示。
|
|
193
193
|
- **上下文窗口感知降级**:`triggerCodes` 接受任意 dsh 失败码,包括 `CONTEXT_WINDOW_EXCEEDED`——请求超出当前模型上下文时,降级到上下文窗口**更大**的候选(装不下的候选被跳过);由于路由本身健康,该切换是请求级的:不冷却、不浪费半开探针(见 [降级触发码](#降级触发码triggercodes))。
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { LlmAdapter, LlmError, boundContextSummary, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { PERSONA_PREFIX_SECTION } from "@deepseek-ai/dsh-system-prompt";
|
|
4
6
|
import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
5
7
|
//#region .build/host/selectors.js
|
|
6
8
|
var SelectorError = class extends Error {
|
|
@@ -828,7 +830,7 @@ function buildRoleNotice(role, personaNotApplied) {
|
|
|
828
830
|
}
|
|
829
831
|
});
|
|
830
832
|
}
|
|
831
|
-
function errorMessage$
|
|
833
|
+
function errorMessage$3(error) {
|
|
832
834
|
return error instanceof Error ? error.message : String(error);
|
|
833
835
|
}
|
|
834
836
|
function installRoleNotice(ctx, options) {
|
|
@@ -855,7 +857,7 @@ function installRoleNotice(ctx, options) {
|
|
|
855
857
|
messages: [...decision.messages, notice]
|
|
856
858
|
};
|
|
857
859
|
} catch (error) {
|
|
858
|
-
debug(`llm-fallbacks: role notice for agent "${agent.id}" skipped (contained, the step is unchanged): ${errorMessage$
|
|
860
|
+
debug(`llm-fallbacks: role notice for agent "${agent.id}" skipped (contained, the step is unchanged): ${errorMessage$3(error)}`);
|
|
859
861
|
return decision;
|
|
860
862
|
}
|
|
861
863
|
}, { prepend: true });
|
|
@@ -942,7 +944,7 @@ const roleProjectionUnit = {
|
|
|
942
944
|
view: (state) => state === null ? null : state.role ?? null
|
|
943
945
|
}
|
|
944
946
|
};
|
|
945
|
-
function errorMessage$
|
|
947
|
+
function errorMessage$2(error) {
|
|
946
948
|
return error instanceof Error ? error.message : String(error);
|
|
947
949
|
}
|
|
948
950
|
function installRoleProjection(ctx, options = {}) {
|
|
@@ -962,12 +964,12 @@ function installRoleProjection(ctx, options = {}) {
|
|
|
962
964
|
}
|
|
963
965
|
return registry.register(roleProjectionUnit);
|
|
964
966
|
} catch (error) {
|
|
965
|
-
debug(`llm-fallbacks: role projection not registered (contained): ${errorMessage$
|
|
967
|
+
debug(`llm-fallbacks: role projection not registered (contained): ${errorMessage$2(error)}`);
|
|
966
968
|
return () => {};
|
|
967
969
|
}
|
|
968
970
|
});
|
|
969
971
|
} catch (error) {
|
|
970
|
-
debug(`llm-fallbacks: role projection install skipped (contained): ${errorMessage$
|
|
972
|
+
debug(`llm-fallbacks: role projection install skipped (contained): ${errorMessage$2(error)}`);
|
|
971
973
|
}
|
|
972
974
|
return () => {
|
|
973
975
|
try {
|
|
@@ -1066,7 +1068,7 @@ function parentSessionIdOf(request) {
|
|
|
1066
1068
|
function correlationKey(parentSessionId, label) {
|
|
1067
1069
|
return `${parentSessionId}\u0000${label}`;
|
|
1068
1070
|
}
|
|
1069
|
-
function errorMessage(error) {
|
|
1071
|
+
function errorMessage$1(error) {
|
|
1070
1072
|
return error instanceof Error ? error.message : String(error);
|
|
1071
1073
|
}
|
|
1072
1074
|
const subagentSeamStores = new WeakMap();
|
|
@@ -1100,20 +1102,20 @@ function installSubagentSeam(ctx, options) {
|
|
|
1100
1102
|
} catch {}
|
|
1101
1103
|
};
|
|
1102
1104
|
const reportDegrade = (error) => {
|
|
1103
|
-
debug(`llm-fallbacks: subagent role seam degraded to the native path (start unaffected): ${errorMessage(error)}`);
|
|
1105
|
+
debug(`llm-fallbacks: subagent role seam degraded to the native path (start unaffected): ${errorMessage$1(error)}`);
|
|
1104
1106
|
};
|
|
1105
|
-
const recordChild = (
|
|
1106
|
-
const existing = records.get(
|
|
1107
|
-
records.set(
|
|
1107
|
+
const recordChild = (childId, role, at, personaMissed) => {
|
|
1108
|
+
const existing = records.get(childId);
|
|
1109
|
+
records.set(childId, {
|
|
1108
1110
|
role,
|
|
1109
1111
|
at,
|
|
1110
1112
|
firstNoticePending: existing?.firstNoticePending ?? true,
|
|
1111
|
-
...personaNotApplied ||
|
|
1113
|
+
...existing?.personaNotApplied === true || personaMissed === true ? { personaNotApplied: true } : {}
|
|
1112
1114
|
});
|
|
1113
1115
|
};
|
|
1114
|
-
const beginCorrelation = (
|
|
1115
|
-
const parentSessionId = parentSessionIdOf(request);
|
|
1116
|
-
const label = typeof
|
|
1116
|
+
const beginCorrelation = (spec, role, at) => {
|
|
1117
|
+
const parentSessionId = parentSessionIdOf(spec?.request);
|
|
1118
|
+
const label = typeof spec?.label === "string" ? spec.label : void 0;
|
|
1117
1119
|
if (parentSessionId === void 0 || label === void 0 || label === "") return void 0;
|
|
1118
1120
|
const key = correlationKey(parentSessionId, label);
|
|
1119
1121
|
if (!pending.has(key) && pending.size >= 64) {
|
|
@@ -1123,8 +1125,7 @@ function installSubagentSeam(ctx, options) {
|
|
|
1123
1125
|
pending.delete(key);
|
|
1124
1126
|
pending.set(key, {
|
|
1125
1127
|
role,
|
|
1126
|
-
at
|
|
1127
|
-
personaNotApplied
|
|
1128
|
+
at
|
|
1128
1129
|
});
|
|
1129
1130
|
return key;
|
|
1130
1131
|
};
|
|
@@ -1168,68 +1169,13 @@ function installSubagentSeam(ctx, options) {
|
|
|
1168
1169
|
}
|
|
1169
1170
|
onValue(result);
|
|
1170
1171
|
};
|
|
1171
|
-
const
|
|
1172
|
-
if (typeof service.getProvider !== "function") return "unavailable";
|
|
1173
|
-
const provider = service.getProvider(providerName);
|
|
1174
|
-
if (provider === void 0) return "unknown";
|
|
1175
|
-
if (surface === "one-shot") return provider.capabilities?.persona === true ? "ok" : "unsupported";
|
|
1176
|
-
return typeof provider.prepareContinuable === "function" ? "ok" : "unsupported";
|
|
1177
|
-
};
|
|
1178
|
-
const withRolePersona = (service, providerName, request, role, surface) => {
|
|
1179
|
-
if (request.persona !== void 0) return {
|
|
1180
|
-
request,
|
|
1181
|
-
personaNotApplied: false
|
|
1182
|
-
};
|
|
1183
|
-
const readRoles = options.roles;
|
|
1184
|
-
if (readRoles === void 0) return {
|
|
1185
|
-
request,
|
|
1186
|
-
personaNotApplied: false
|
|
1187
|
-
};
|
|
1188
|
-
const persona = personaForRole(readRoles(), role);
|
|
1189
|
-
if (persona === void 0) return {
|
|
1190
|
-
request,
|
|
1191
|
-
personaNotApplied: false
|
|
1192
|
-
};
|
|
1193
|
-
const gate = personaGate(service, providerName, surface);
|
|
1194
|
-
if (gate === "unknown") {
|
|
1195
|
-
debug(`llm-fallbacks: no subagent provider '${providerName}' is registered on the ${surface} start — role persona for '${role}' not delivered (the native start fails loud its own way)`);
|
|
1196
|
-
return {
|
|
1197
|
-
request,
|
|
1198
|
-
personaNotApplied: true
|
|
1199
|
-
};
|
|
1200
|
-
}
|
|
1201
|
-
if (gate === "unavailable") {
|
|
1202
|
-
debug(`llm-fallbacks: role persona for '${role}' skipped on the ${surface} start — the '${SUBAGENT_SEAM_SERVICE}' runtime exposes no provider lookup, so the persona capability cannot be verified (the start proceeds unchanged)`);
|
|
1203
|
-
return {
|
|
1204
|
-
request,
|
|
1205
|
-
personaNotApplied: true
|
|
1206
|
-
};
|
|
1207
|
-
}
|
|
1208
|
-
if (gate === "unsupported") {
|
|
1209
|
-
debug(surface === "one-shot" ? `llm-fallbacks: subagent provider '${providerName}' lacks the persona capability on the ${surface} start — role persona for '${role}' skipped (the start proceeds unchanged)` : `llm-fallbacks: subagent provider '${providerName}' does not support continuable children on the ${surface} surface — role persona for '${role}' skipped (the native continuable start fails loud its own way)`);
|
|
1210
|
-
return {
|
|
1211
|
-
request,
|
|
1212
|
-
personaNotApplied: true
|
|
1213
|
-
};
|
|
1214
|
-
}
|
|
1215
|
-
debug(`llm-fallbacks: role persona delivered via the native subagent persona channel for role '${role}' (${surface} start on provider '${providerName}')`);
|
|
1216
|
-
return {
|
|
1217
|
-
request: {
|
|
1218
|
-
...request,
|
|
1219
|
-
persona
|
|
1220
|
-
},
|
|
1221
|
-
personaNotApplied: false
|
|
1222
|
-
};
|
|
1223
|
-
};
|
|
1224
|
-
const mergePersonaAtSeam = (service, providerName, request, role, surface) => {
|
|
1172
|
+
const isLocalBackend = (service, providerName) => {
|
|
1225
1173
|
try {
|
|
1226
|
-
|
|
1174
|
+
if (typeof service.getProvider !== "function") return false;
|
|
1175
|
+
return typeof service.getProvider(providerName)?.prepareContinuable === "function";
|
|
1227
1176
|
} catch (error) {
|
|
1228
1177
|
reportDegrade(error);
|
|
1229
|
-
return
|
|
1230
|
-
request,
|
|
1231
|
-
personaNotApplied: false
|
|
1232
|
-
};
|
|
1178
|
+
return false;
|
|
1233
1179
|
}
|
|
1234
1180
|
};
|
|
1235
1181
|
const wrapService = (value) => {
|
|
@@ -1241,61 +1187,71 @@ function installSubagentSeam(ctx, options) {
|
|
|
1241
1187
|
return value;
|
|
1242
1188
|
}
|
|
1243
1189
|
const service = value;
|
|
1244
|
-
if (typeof service.
|
|
1190
|
+
if (typeof service.startActivation !== "function") {
|
|
1245
1191
|
if (!unreadableServiceLogged) {
|
|
1246
1192
|
unreadableServiceLogged = true;
|
|
1247
|
-
debug(`llm-fallbacks: subagent role seam skipped — '${SUBAGENT_SEAM_SERVICE}' is not
|
|
1193
|
+
debug(`llm-fallbacks: subagent role seam skipped — '${SUBAGENT_SEAM_SERVICE}' is not an activation-capable runtime`);
|
|
1248
1194
|
}
|
|
1249
1195
|
return value;
|
|
1250
1196
|
}
|
|
1251
1197
|
const cached = wrappers.get(value);
|
|
1252
1198
|
if (cached !== void 0) return cached;
|
|
1253
1199
|
const wrapper = Object.create(value);
|
|
1254
|
-
wrapper.
|
|
1255
|
-
const role = resolveRoleAtSeam(request);
|
|
1256
|
-
if (role === void 0) return service.
|
|
1257
|
-
const
|
|
1200
|
+
wrapper.startActivation = (spec) => {
|
|
1201
|
+
const role = resolveRoleAtSeam(spec?.request) ?? (typeof spec?.childId === "string" ? records.get(spec.childId)?.role : void 0);
|
|
1202
|
+
if (role === void 0) return service.startActivation(spec);
|
|
1203
|
+
const at = Date.now();
|
|
1204
|
+
let effectiveSpec = spec;
|
|
1205
|
+
let reservedId;
|
|
1206
|
+
let reservationPersonaMissed;
|
|
1207
|
+
try {
|
|
1208
|
+
if (isLocalBackend(service, spec.provider)) {
|
|
1209
|
+
reservedId = spec.childId ?? randomUUID();
|
|
1210
|
+
if (reservedId !== spec.childId) effectiveSpec = {
|
|
1211
|
+
...spec,
|
|
1212
|
+
childId: reservedId
|
|
1213
|
+
};
|
|
1214
|
+
recordChild(reservedId, role, at);
|
|
1215
|
+
}
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
if (reservedId !== void 0) records.delete(reservedId);
|
|
1218
|
+
effectiveSpec = spec;
|
|
1219
|
+
reservedId = void 0;
|
|
1220
|
+
reportDegrade(error);
|
|
1221
|
+
try {
|
|
1222
|
+
reservationPersonaMissed = personaForRole(options.roles?.() ?? [], role) !== void 0;
|
|
1223
|
+
} catch (personaError) {
|
|
1224
|
+
reportDegrade(personaError);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1258
1227
|
let pendingKey;
|
|
1259
1228
|
try {
|
|
1260
|
-
pendingKey = beginCorrelation(
|
|
1229
|
+
pendingKey = beginCorrelation(spec, role, at);
|
|
1261
1230
|
} catch (error) {
|
|
1262
1231
|
reportDegrade(error);
|
|
1263
1232
|
}
|
|
1264
1233
|
let result;
|
|
1265
1234
|
try {
|
|
1266
|
-
result = service.
|
|
1235
|
+
result = service.startActivation(effectiveSpec);
|
|
1267
1236
|
} catch (error) {
|
|
1268
1237
|
endCorrelation(pendingKey);
|
|
1238
|
+
if (reservedId !== void 0) records.delete(reservedId);
|
|
1269
1239
|
throw error;
|
|
1270
1240
|
}
|
|
1271
1241
|
try {
|
|
1272
|
-
observeStartResult(result, (
|
|
1242
|
+
observeStartResult(result, (childId) => {
|
|
1243
|
+
endCorrelation(pendingKey);
|
|
1244
|
+
recordChild(childId, role, at, reservationPersonaMissed);
|
|
1245
|
+
}, () => {}, () => {
|
|
1273
1246
|
endCorrelation(pendingKey);
|
|
1274
|
-
|
|
1275
|
-
}
|
|
1247
|
+
if (reservedId !== void 0) records.delete(reservedId);
|
|
1248
|
+
});
|
|
1276
1249
|
} catch (error) {
|
|
1277
1250
|
endCorrelation(pendingKey);
|
|
1278
1251
|
reportDegrade(error);
|
|
1279
1252
|
}
|
|
1280
1253
|
return result;
|
|
1281
1254
|
};
|
|
1282
|
-
const startContinuable = service.startContinuable;
|
|
1283
|
-
if (typeof startContinuable === "function") wrapper.startContinuable = (spec) => {
|
|
1284
|
-
const role = resolveRoleAtSeam(spec?.request) ?? (typeof spec?.childId === "string" ? records.get(spec.childId)?.role : void 0);
|
|
1285
|
-
if (role === void 0) return startContinuable.call(service, spec);
|
|
1286
|
-
const outcome = mergePersonaAtSeam(service, spec.provider, spec.request, role, "continuable");
|
|
1287
|
-
const effectiveSpec = outcome.request === spec.request ? spec : {
|
|
1288
|
-
...spec,
|
|
1289
|
-
request: outcome.request
|
|
1290
|
-
};
|
|
1291
|
-
const result = startContinuable.call(service, effectiveSpec);
|
|
1292
|
-
try {
|
|
1293
|
-
observeStartResult(result, (childSessionId) => recordChild(childSessionId, role, Date.now(), outcome.personaNotApplied), () => {}, () => {});
|
|
1294
|
-
} catch (error) {
|
|
1295
|
-
reportDegrade(error);
|
|
1296
|
-
}
|
|
1297
|
-
return result;
|
|
1298
|
-
};
|
|
1299
1255
|
wrappers.set(value, wrapper);
|
|
1300
1256
|
return wrapper;
|
|
1301
1257
|
};
|
|
@@ -1322,7 +1278,7 @@ function installSubagentSeam(ctx, options) {
|
|
|
1322
1278
|
const claim = pending.get(key);
|
|
1323
1279
|
if (claim === void 0) return;
|
|
1324
1280
|
pending.delete(key);
|
|
1325
|
-
recordChild(childSessionId, claim.role, claim.at
|
|
1281
|
+
recordChild(childSessionId, claim.role, claim.at);
|
|
1326
1282
|
} catch (error) {
|
|
1327
1283
|
reportDegrade(error);
|
|
1328
1284
|
}
|
|
@@ -1347,6 +1303,96 @@ function installSubagentSeam(ctx, options) {
|
|
|
1347
1303
|
return seam;
|
|
1348
1304
|
}
|
|
1349
1305
|
//#endregion
|
|
1306
|
+
//#region .build/host/role-persona.js
|
|
1307
|
+
function errorMessage(error) {
|
|
1308
|
+
return error instanceof Error ? error.message : String(error);
|
|
1309
|
+
}
|
|
1310
|
+
const rolePersonaStates = new WeakMap();
|
|
1311
|
+
function rolePersonaStateFor(root) {
|
|
1312
|
+
const existing = rolePersonaStates.get(root);
|
|
1313
|
+
if (existing !== void 0) return existing;
|
|
1314
|
+
const created = new Map();
|
|
1315
|
+
rolePersonaStates.set(root, created);
|
|
1316
|
+
return created;
|
|
1317
|
+
}
|
|
1318
|
+
function installRolePersona(ctx, options) {
|
|
1319
|
+
const root = ctx.root;
|
|
1320
|
+
const registered = rolePersonaStateFor(root);
|
|
1321
|
+
const owner = {};
|
|
1322
|
+
const fibers = new Map();
|
|
1323
|
+
const debug = (message) => {
|
|
1324
|
+
try {
|
|
1325
|
+
options.debug?.(message);
|
|
1326
|
+
} catch {}
|
|
1327
|
+
};
|
|
1328
|
+
const contain = (agent, record, error) => {
|
|
1329
|
+
debug(`llm-fallbacks: role persona for agent "${agent.id}" (role "${record.role}") not applied — contained, the child runs unchanged: ${errorMessage(error)}`);
|
|
1330
|
+
record.personaNotApplied = true;
|
|
1331
|
+
};
|
|
1332
|
+
const release = (id) => {
|
|
1333
|
+
const fiber = fibers.get(id);
|
|
1334
|
+
if (fiber === void 0) return;
|
|
1335
|
+
fibers.delete(id);
|
|
1336
|
+
fiber.dispose().catch((error) => {
|
|
1337
|
+
debug(`llm-fallbacks: role persona section for agent "${id}" not withdrawn (contained): ${errorMessage(error)}`);
|
|
1338
|
+
});
|
|
1339
|
+
};
|
|
1340
|
+
const installPersona = async (agent) => {
|
|
1341
|
+
const record = options.records.get(agent.id);
|
|
1342
|
+
if (record === void 0) return;
|
|
1343
|
+
if (registered.has(agent.id)) return;
|
|
1344
|
+
let persona;
|
|
1345
|
+
try {
|
|
1346
|
+
persona = personaForRole(options.roles(), record.role);
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
debug(`llm-fallbacks: role persona for agent "${agent.id}" not resolved — contained, the child runs unchanged: ${errorMessage(error)}`);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
if (persona === void 0) return;
|
|
1352
|
+
const text = persona;
|
|
1353
|
+
registered.set(agent.id, owner);
|
|
1354
|
+
try {
|
|
1355
|
+
const fiber = agent.ctx.inject(["systemPrompt"], (scope) => {
|
|
1356
|
+
try {
|
|
1357
|
+
scope.systemPrompt.section({
|
|
1358
|
+
name: PERSONA_PREFIX_SECTION,
|
|
1359
|
+
order: scope.systemPrompt.getSectionOrder("DEPLOYMENT_PERSONA_PREFIX"),
|
|
1360
|
+
text
|
|
1361
|
+
});
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
if (error instanceof Error && error.message === `prompt section "${PERSONA_PREFIX_SECTION}" is already registered in this scope`) {
|
|
1364
|
+
debug(`llm-fallbacks: role persona for agent "${agent.id}" not installed — a section named "${PERSONA_PREFIX_SECTION}" already exists in its scope: ${errorMessage(error)}`);
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
contain(agent, record, error);
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
fibers.set(agent.id, fiber);
|
|
1371
|
+
await fiber;
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
release(agent.id);
|
|
1374
|
+
contain(agent, record, error);
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
const disposers = [];
|
|
1378
|
+
disposers.push(ctx.on("agent/created", async ({ agent }) => {
|
|
1379
|
+
await installPersona(agent);
|
|
1380
|
+
}));
|
|
1381
|
+
disposers.push(ctx.on("agent/disposed", ({ agent }) => {
|
|
1382
|
+
release(agent.id);
|
|
1383
|
+
registered.delete(agent.id);
|
|
1384
|
+
}));
|
|
1385
|
+
const agents = ctx.get("agents");
|
|
1386
|
+
if (typeof agents?.list === "function") for (const agent of agents.list()) installPersona(agent);
|
|
1387
|
+
const dispose = () => {
|
|
1388
|
+
for (const off of disposers.splice(0)) off();
|
|
1389
|
+
for (const id of [...fibers.keys()]) release(id);
|
|
1390
|
+
for (const [id, holder] of [...registered]) if (holder === owner) registered.delete(id);
|
|
1391
|
+
};
|
|
1392
|
+
ctx.effect(() => dispose, "llm-fallbacks: role persona listener");
|
|
1393
|
+
return { dispose };
|
|
1394
|
+
}
|
|
1395
|
+
//#endregion
|
|
1350
1396
|
//#region .build/host/cooldown.js
|
|
1351
1397
|
var CooldownStore = class {
|
|
1352
1398
|
entries = new Map();
|
|
@@ -3151,6 +3197,11 @@ function apply(ctx, config = defaultFallbacksConfig) {
|
|
|
3151
3197
|
debug: (message) => logger.debug(message)
|
|
3152
3198
|
});
|
|
3153
3199
|
if (!subagentSeam.ownsWrapper) ctx.logger("llm-fallbacks").debug("subagent role seam wrapper already installed on this context root — this fiber registers the notice emitter and the projection unit only (multi-fiber dedupe)");
|
|
3200
|
+
installRolePersona(ctx, {
|
|
3201
|
+
records: subagentSeam.records,
|
|
3202
|
+
roles: () => source().roles.list,
|
|
3203
|
+
debug: (message) => logger.debug(message)
|
|
3204
|
+
});
|
|
3154
3205
|
try {
|
|
3155
3206
|
new FallbacksConfigGateway(ctx, bridge, seeds, () => {
|
|
3156
3207
|
try {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role-persona delivery over `agent/created` (plan dsh-021-subagent-seam-repair
|
|
3
|
+
* Task 1).
|
|
4
|
+
*
|
|
5
|
+
* rc.1 removed `SubagentStartRequest.persona`, so the resolved dispatch role's
|
|
6
|
+
* persona is delivered as a SCOPED system-prompt section on the CHILD's own
|
|
7
|
+
* context — the shipped `file-reference-local` pattern (no request mutation, no
|
|
8
|
+
* provider capability gate, no Agent Preset). The section carries the SAME name
|
|
9
|
+
* and order the global deployment persona uses (`SystemPrompt.section`: a
|
|
10
|
+
* scoped section shadows a global section with the same name), so the child's
|
|
11
|
+
* persona REPLACES the deployment prefix instead of stacking under it — the
|
|
12
|
+
* semantics the removed request slot had.
|
|
13
|
+
*
|
|
14
|
+
* Timing: `agent/created` is a serial event awaited before the child's first
|
|
15
|
+
* prompt assembly, and this listener AWAITS the injected section fiber: cordis
|
|
16
|
+
* activates a child fiber on a later tick, so awaiting it is what makes the
|
|
17
|
+
* persona observable by the time the dispatch resolves (the shipped
|
|
18
|
+
* `file-reference-local` shape). The seam's `agent/pre-step` notice emitter
|
|
19
|
+
* therefore always observes a child whose persona section is already
|
|
20
|
+
* registered; both surfaces read the SAME ROOT-shared record map, which is what
|
|
21
|
+
* keeps the ` (persona not applied)` suffix truthful.
|
|
22
|
+
*
|
|
23
|
+
* Ownership: the injected fiber belongs to the CHILD's scope, so it does not
|
|
24
|
+
* die with this listener's fiber — each listener holds ITS OWN fibers per agent
|
|
25
|
+
* and releases them on `agent/disposed`, on plugin-fiber teardown, and on
|
|
26
|
+
* explicit `dispose()`. The root-shared claim that dedupes sibling listeners
|
|
27
|
+
* records which listener owns it, so one installer's teardown never releases
|
|
28
|
+
* another's fibers or claims, and a repeated notification never replaces the
|
|
29
|
+
* handle to the fiber that holds the live registration.
|
|
30
|
+
*
|
|
31
|
+
* Containment (the seam's degrade-never-crash invariant, and required here
|
|
32
|
+
* because a throwing serial `agent/created` listener fails agent creation):
|
|
33
|
+
* only an agent whose id hits the shared record map is touched (a miss is a
|
|
34
|
+
* silent no-op), the registry's exact same-name SCOPED section duplicate means
|
|
35
|
+
* a persona section is already on that scope (one attribution-free debug line,
|
|
36
|
+
* no verdict — the registry cannot say who registered it), and every other
|
|
37
|
+
* throw — including a failed fiber activation — is contained to ONE debug line
|
|
38
|
+
* plus the record's sticky `personaNotApplied`.
|
|
39
|
+
*
|
|
40
|
+
* @module dsh-llm-fallbacks/role-persona
|
|
41
|
+
*/
|
|
42
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
43
|
+
import { type SubagentSeamRecord } from './subagents-seam.ts';
|
|
44
|
+
import type { FallbacksRole } from './config.ts';
|
|
45
|
+
/** Options for {@link installRolePersona}. */
|
|
46
|
+
export interface RolePersonaOptions {
|
|
47
|
+
/**
|
|
48
|
+
* The seam's ROOT-shared per-child records — the SAME map the notice
|
|
49
|
+
* emitter reads; the listener writes `personaNotApplied` verdicts back
|
|
50
|
+
* into it (sticky per the existing {@link SubagentSeamRecord} contract).
|
|
51
|
+
*/
|
|
52
|
+
records: Map<string, SubagentSeamRecord>;
|
|
53
|
+
/** Live declared roles (`roles.list`) — the persona source, read per agent/created. */
|
|
54
|
+
roles: () => readonly FallbacksRole[];
|
|
55
|
+
/** Contained debug sink (at most one line per contained degrade). */
|
|
56
|
+
debug?: (message: string) => void;
|
|
57
|
+
}
|
|
58
|
+
/** The installed listener surface (explicit teardown; fiber teardown also stops it). */
|
|
59
|
+
export interface RolePersona {
|
|
60
|
+
dispose(): void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Install the role-persona listener on `ctx`: register the resolved role's
|
|
64
|
+
* persona as a scoped `deployment:persona-prefix` section on every created
|
|
65
|
+
* agent whose id hits the shared record map, plus a `agents.list()` backfill
|
|
66
|
+
* for children that already exist at install time.
|
|
67
|
+
*
|
|
68
|
+
* Returns the installed listener (its `dispose` withdraws this listener's OWN
|
|
69
|
+
* registrations; a fiber teardown stops it as well).
|
|
70
|
+
*/
|
|
71
|
+
export declare function installRolePersona(ctx: Context, options: RolePersonaOptions): RolePersona;
|
package/dist/subagents-seam.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dispatch-seam role resolution + per-child record (plan
|
|
3
|
-
* role-based-subagent-adoption Task 1
|
|
3
|
+
* role-based-subagent-adoption Task 1; rc.1 `startActivation` migration, plan
|
|
4
|
+
* dsh-021-subagent-seam-repair Task 2).
|
|
4
5
|
*
|
|
5
6
|
* The seam is the documented cordis service-read waterfall
|
|
6
7
|
* (`Events['internal/get']`, `@deepseek-ai/cordis`
|
|
7
8
|
* `lib/types/events.d.ts`): a listener wraps the `subagents` service VALUE on
|
|
8
|
-
* read and returns a prototype-delegating wrapper whose `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* read and returns a prototype-delegating wrapper whose `startActivation`
|
|
10
|
+
* resolves the dispatch role from the incoming activation spec BEFORE
|
|
11
|
+
* delegating. The real service object is never mutated; readers of the
|
|
11
12
|
* `subagents` service transparently receive the wrapper.
|
|
12
13
|
*
|
|
13
14
|
* Role source (plan Global Constraints): the child request's prompt carries
|
|
@@ -29,34 +30,36 @@
|
|
|
29
30
|
* ambiguity rule stands unchanged: the DISTINCT DECLARED ids must collapse to
|
|
30
31
|
* exactly one, else the dispatch is a no-op — never a first-occurrence pick.
|
|
31
32
|
*
|
|
32
|
-
* Per-child record
|
|
33
|
-
* `
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
33
|
+
* Per-child record + the timing rule (rc.1 `startActivation`): the record
|
|
34
|
+
* `{ role, at, firstNoticePending }` is keyed by the child id (Task 3 reads it
|
|
35
|
+
* to emit the one role notice row). The child is created INSIDE the native
|
|
36
|
+
* call — `manager.startLocal` mints/uses `spec.childId` and only then resolves
|
|
37
|
+
* the activation — so `agent/created` fires BEFORE any `.then` on this side
|
|
38
|
+
* could key a record off the resolved `SubagentActivation.childId`. The
|
|
39
|
+
* wrapper therefore reserves the child id itself for a LOCAL backend (the
|
|
40
|
+
* provider exposes `prepareContinuable`; a reserved id on any other backend is
|
|
41
|
+
* rejected with `UNSUPPORTED_CAPABILITY`) and writes the record SYNCHRONOUSLY
|
|
42
|
+
* before delegating, deleting it when the native call fails. An EXTERNAL
|
|
43
|
+
* backend allocates its own child id, receives the caller's OWN spec object
|
|
44
|
+
* untouched, and has its record keyed off the resolved activation's `childId`.
|
|
40
45
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* the
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* absent), and every skip path returns the caller's OWN request object, so the
|
|
59
|
-
* native call stays byte-identical.
|
|
46
|
+
* A result carrying no child id — a `jobId` is a registry id, NOT a session id
|
|
47
|
+
* — is never keyed: the child is correlated instead through the parent-owned
|
|
48
|
+
* `subagent/catalog` session event `{ childId, label }` joined on the
|
|
49
|
+
* delegating parent + the activation `label` (the same string the cataloged
|
|
50
|
+
* child membership carries).
|
|
51
|
+
*
|
|
52
|
+
* Persona delivery is NO LONGER this module's job (plan Task 1): rc.1 removed
|
|
53
|
+
* `SubagentStartRequest.persona`, so the resolved role's persona is registered
|
|
54
|
+
* as a scoped `deployment:persona-prefix` section on the CHILD's own context
|
|
55
|
+
* by `./role-persona.ts`, over the SAME ROOT-shared record map this module
|
|
56
|
+
* writes. That listener owns the `personaNotApplied` verdict the notice row
|
|
57
|
+
* reads — EXCEPT when a reservation failure means it can never see the child
|
|
58
|
+
* (the runtime mints its own child id and `agent/created` has already run), in
|
|
59
|
+
* which case this module resolves the same question against the live roles
|
|
60
|
+
* source and carries the verdict onto the record keyed off the RESOLVED id,
|
|
61
|
+
* before the child's first pre-step. This module writes no request field,
|
|
62
|
+
* reads no provider persona capability, and injects no agent preset.
|
|
60
63
|
*
|
|
61
64
|
* In-session notice row (plan Task 3): the SAME install point registers the
|
|
62
65
|
* `agent/pre-step` emitter (`./role-notice.ts`) over the record map it owns, so
|
|
@@ -103,14 +106,14 @@ export interface SubagentSeamRecord {
|
|
|
103
106
|
* The DECLARED RAW role id resolved at the dispatch seam.
|
|
104
107
|
*
|
|
105
108
|
* Authoritative-role precedence (CF-10, documented rather than resolved by
|
|
106
|
-
* re-branding): a
|
|
107
|
-
*
|
|
109
|
+
* re-branding): a resume that reuses a reserved `childId` may resolve a
|
|
110
|
+
* DIFFERENT declared role, and this field is then rewritten while the
|
|
108
111
|
* durable notice row the child's session already carries keeps the FIRST role.
|
|
109
112
|
* The projection deliberately reads the notice row, so the durable role is the
|
|
110
113
|
* child's announced identity and this record is only the LIVE dispatch hint
|
|
111
|
-
* (what the next
|
|
112
|
-
* disagree after a re-dispatch until the child is disposed; no
|
|
113
|
-
* treat the record as the durable role.
|
|
114
|
+
* (what the next activation / the resume fallback would use). The two can
|
|
115
|
+
* therefore disagree after a re-dispatch until the child is disposed; no
|
|
116
|
+
* consumer may treat the record as the durable role.
|
|
114
117
|
*/
|
|
115
118
|
role: string;
|
|
116
119
|
/** When the record was written (epoch ms). */
|
|
@@ -126,20 +129,24 @@ export interface SubagentSeamRecord {
|
|
|
126
129
|
firstNoticePending: boolean;
|
|
127
130
|
/**
|
|
128
131
|
* `true` when the role DECLARES a persona and that persona was NOT delivered
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
132
|
+
* onto the child. Written by the `agent/created` role-persona listener
|
|
133
|
+
* (`./role-persona.ts`) over this same map — except when a reservation
|
|
134
|
+
* failure means that listener can never see the child (the runtime mints its
|
|
135
|
+
* own child id), where the seam itself carries the verdict onto the record
|
|
136
|
+
* keyed off the RESOLVED id. The seam never delivers or reads a persona
|
|
137
|
+
* other than for that one question. Absent when there was nothing to report:
|
|
138
|
+
* the section was registered, a same-name section was already on the child's
|
|
139
|
+
* scope (whoever registered it), the listener never saw the child (an
|
|
140
|
+
* EXTERNAL backend creates no local agent), the role declares none, or no
|
|
141
|
+
* persona source is wired. Task 3 appends ` (persona not applied)` on `true`
|
|
142
|
+
* only.
|
|
134
143
|
*
|
|
135
|
-
* STICKY (Task 3 review Minor 4): a record rewrite
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* child's one notice row is a claim about that dispatch, so the first `true`
|
|
142
|
-
* survives; a later `false` is simply not recorded.
|
|
144
|
+
* STICKY (Task 3 review Minor 4): a record rewrite — a resume reusing the
|
|
145
|
+
* reserved `childId`, or the resolved activation re-keying the same child
|
|
146
|
+
* after `agent/created` already ran — can only ADD this verdict, never clear
|
|
147
|
+
* it. A rewrite carries no verdict of its own, and the child's one notice row
|
|
148
|
+
* is a claim about the dispatch that already reported it missing, so the first
|
|
149
|
+
* `true` survives.
|
|
143
150
|
*/
|
|
144
151
|
personaNotApplied?: boolean;
|
|
145
152
|
}
|
|
@@ -149,29 +156,29 @@ export interface SubagentPromptBlockView {
|
|
|
149
156
|
readonly text?: string;
|
|
150
157
|
}
|
|
151
158
|
/**
|
|
152
|
-
* Structural view of the
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* other field reaches the service unchanged.
|
|
159
|
+
* Structural view of the activation request (consumed fields only): `prompt` is
|
|
160
|
+
* the role-extraction source and `parent` is the catalog-correlation join half.
|
|
161
|
+
* The wrapper forwards the request OBJECT itself, so every other field (cwd,
|
|
162
|
+
* agentOptions, outputSchema, maxDepth, the signal) reaches the service
|
|
163
|
+
* unchanged.
|
|
158
164
|
*/
|
|
159
165
|
export interface SubagentStartRequestView {
|
|
160
166
|
readonly prompt?: readonly SubagentPromptBlockView[];
|
|
161
|
-
readonly label?: string;
|
|
162
167
|
readonly parent?: {
|
|
163
168
|
readonly session?: {
|
|
164
169
|
readonly id?: string;
|
|
165
170
|
};
|
|
166
171
|
};
|
|
167
|
-
readonly persona?: string;
|
|
168
172
|
}
|
|
169
173
|
/**
|
|
170
|
-
* Structural view of the
|
|
171
|
-
* `childId` is the caller-reserved identity a resume
|
|
172
|
-
* the record fallback when the
|
|
174
|
+
* Structural view of the activation spec (rc.1 `SubagentActivationSpec`,
|
|
175
|
+
* consumed fields only): `childId` is the caller-reserved identity a resume
|
|
176
|
+
* already carries — also the record fallback when the request carries no
|
|
177
|
+
* Assignment header — and `label` is the catalog join's other half (the
|
|
178
|
+
* cataloged child membership carries this exact string). `signal`, `delivery`
|
|
179
|
+
* and every future field travel with the caller's spec object.
|
|
173
180
|
*/
|
|
174
|
-
export interface
|
|
181
|
+
export interface SubagentActivationSpecView {
|
|
175
182
|
readonly provider: string;
|
|
176
183
|
readonly label?: string;
|
|
177
184
|
readonly childId?: string;
|
|
@@ -180,14 +187,16 @@ export interface ContinuableStartSpecView {
|
|
|
180
187
|
/** Options for {@link installSubagentSeam}. */
|
|
181
188
|
export interface SubagentSeamOptions {
|
|
182
189
|
/**
|
|
183
|
-
* Live declared-role id map (trimmed id → DECLARED RAW id), read per
|
|
184
|
-
* so a settings change is observed without a re-install.
|
|
190
|
+
* Live declared-role id map (trimmed id → DECLARED RAW id), read per
|
|
191
|
+
* activation so a settings change is observed without a re-install.
|
|
185
192
|
*/
|
|
186
193
|
roleIds: () => ReadonlyMap<string, string>;
|
|
187
194
|
/**
|
|
188
|
-
* Live declared roles (`roles.list`) — the
|
|
189
|
-
*
|
|
190
|
-
* seam only
|
|
195
|
+
* Live declared roles (`roles.list`) — the SAME source `./role-persona.ts`
|
|
196
|
+
* reads. Consulted ONLY when a reservation failure means that listener can
|
|
197
|
+
* never see the child: the seam is then the only place left that can carry
|
|
198
|
+
* the persona verdict to the child's notice row. Absent (no persona source is
|
|
199
|
+
* wired) ⇒ no verdict, exactly like a listener that never saw the child.
|
|
191
200
|
*/
|
|
192
201
|
roles?: () => readonly FallbacksRole[];
|
|
193
202
|
/** Contained debug sink (at most one line per contained degrade). */
|
|
@@ -196,10 +205,12 @@ export interface SubagentSeamOptions {
|
|
|
196
205
|
/** The installed seam (Task 3 surface + explicit teardown). */
|
|
197
206
|
export interface SubagentSeam {
|
|
198
207
|
/**
|
|
199
|
-
* Per-child records keyed by the child
|
|
200
|
-
*
|
|
208
|
+
* Per-child records keyed by the child id — the reserved id this wrapper
|
|
209
|
+
* wrote before a local activation, or the `childId` a resolved activation
|
|
210
|
+
* reported. ROOT-shared (CF-5): every fiber applied over the same root reads
|
|
201
211
|
* the same map. Mutable by design: Task 3 clears `firstNoticePending` after
|
|
202
|
-
* emitting the notice
|
|
212
|
+
* emitting the notice, and the role-persona listener writes
|
|
213
|
+
* `personaNotApplied`.
|
|
203
214
|
*/
|
|
204
215
|
readonly records: Map<string, SubagentSeamRecord>;
|
|
205
216
|
/**
|
|
@@ -253,7 +264,9 @@ export declare const PENDING_CORRELATION_LIMIT = 64;
|
|
|
253
264
|
*/
|
|
254
265
|
export declare function resolveDeclaredRoleFromAssignment(prompt: string, roleIds: ReadonlyMap<string, string>): string | undefined;
|
|
255
266
|
/**
|
|
256
|
-
* PURE, chain-independent persona lookup
|
|
267
|
+
* PURE, chain-independent persona lookup for the `agent/created` role-persona
|
|
268
|
+
* listener (`./role-persona.ts`; rc.1 delivers the persona as a scoped prompt
|
|
269
|
+
* section on the child, never as a request field):
|
|
257
270
|
* the DECLARED `persona` of `roleId`, trimmed, and only when it is still
|
|
258
271
|
* non-empty after the trim — a blank persona is NO persona. `roleId` is the
|
|
259
272
|
* DECLARED RAW id the seam already resolved, so this is an exact id match, not
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"dsh",
|
|
6
6
|
"dsh-plugin"
|
|
7
7
|
],
|
|
8
|
-
"version": "0.6.
|
|
8
|
+
"version": "0.6.6",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"main": "./dist/index.js",
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
@@ -64,30 +64,31 @@
|
|
|
64
64
|
"use-sync-external-store": "^1.6.0"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
68
|
-
"@deepseek-ai/dsh-agent": "^0.2.
|
|
69
|
-
"@deepseek-ai/dsh-api-gateway": "^0.2.
|
|
70
|
-
"@deepseek-ai/dsh-api-remotes": "^0.2.
|
|
71
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.2.
|
|
72
|
-
"@deepseek-ai/dsh-client-connection": "^0.2.
|
|
73
|
-
"@deepseek-ai/dsh-client-locale": "^0.2.
|
|
74
|
-
"@deepseek-ai/dsh-client-store": "^0.2.
|
|
75
|
-
"@deepseek-ai/dsh-client-ui-chat": "^0.2.
|
|
76
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.2.
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.
|
|
78
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.
|
|
79
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.2.
|
|
80
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.2.
|
|
81
|
-
"@deepseek-ai/dsh-commands": "^0.2.
|
|
82
|
-
"@deepseek-ai/dsh-llm": "^0.2.
|
|
83
|
-
"@deepseek-ai/dsh-llm-retry": "^0.2.
|
|
84
|
-
"@deepseek-ai/dsh-session": "^0.2.
|
|
85
|
-
"@deepseek-ai/dsh-session-projection": "^0.2.
|
|
86
|
-
"@deepseek-ai/dsh-settings": "^0.2.
|
|
87
|
-
"@deepseek-ai/dsh-
|
|
88
|
-
"@deepseek-ai/dsh-
|
|
89
|
-
"@deepseek-ai/dsh-typert-
|
|
90
|
-
"@deepseek-ai/
|
|
67
|
+
"@deepseek-ai/cordis": "^4.0.5",
|
|
68
|
+
"@deepseek-ai/dsh-agent": "^0.2.1-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-api-gateway": "^0.2.1-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-api-remotes": "^0.2.1-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.2.1-rc.2",
|
|
72
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.1-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.1-rc.2",
|
|
74
|
+
"@deepseek-ai/dsh-client-store": "^0.2.1-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-client-ui-chat": "^0.2.1-rc.2",
|
|
76
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.2.1-rc.2",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.1-rc.2",
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.1-rc.2",
|
|
79
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.1-rc.2",
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.1-rc.2",
|
|
81
|
+
"@deepseek-ai/dsh-commands": "^0.2.1-rc.2",
|
|
82
|
+
"@deepseek-ai/dsh-llm": "^0.2.1-rc.2",
|
|
83
|
+
"@deepseek-ai/dsh-llm-retry": "^0.2.1-rc.2",
|
|
84
|
+
"@deepseek-ai/dsh-session": "^0.2.1-rc.2",
|
|
85
|
+
"@deepseek-ai/dsh-session-projection": "^0.2.1-rc.2",
|
|
86
|
+
"@deepseek-ai/dsh-settings": "^0.2.1-rc.2",
|
|
87
|
+
"@deepseek-ai/dsh-system-prompt": "^0.2.1-rc.2",
|
|
88
|
+
"@deepseek-ai/dsh-tool-subagent": "^0.2.1-rc.2",
|
|
89
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.2.1-rc.2",
|
|
90
|
+
"@deepseek-ai/dsh-typert-registry": "^0.2.1-rc.2",
|
|
91
|
+
"@deepseek-ai/schemastery": "^3.18.5",
|
|
91
92
|
"react": "^18.2.0"
|
|
92
93
|
},
|
|
93
94
|
"devDependencies": {
|