dsh-llm-fallbacks 0.6.13 → 0.7.0
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 +14 -10
- package/README.zh-CN.md +14 -10
- package/dist/chain-walk.d.ts +211 -0
- package/dist/chains.d.ts +28 -6
- package/dist/client/index.js +86 -194
- package/dist/client/locales.d.ts +2 -12
- package/dist/commands.d.ts +3 -10
- package/dist/config.d.ts +4 -16
- package/dist/cooldown.d.ts +1 -15
- package/dist/events.d.ts +8 -8
- package/dist/gateway.d.ts +5 -3
- package/dist/index.d.ts +19 -40
- package/dist/index.js +685 -875
- package/dist/role-resolution.d.ts +12 -51
- package/dist/roles.d.ts +3 -4
- package/dist/state.d.ts +18 -102
- package/dist/subagent-policy.d.ts +22 -8
- package/dist/subagents-seam.d.ts +70 -11
- package/dist/tui-settings.d.ts +4 -2
- package/dist/virtual-adapter.d.ts +42 -46
- package/docs/DETAIL.md +1 -0
- package/docs/DETAIL.zh.md +1 -0
- package/docs/preview.svg +47 -0
- package/docs/preview.zh.svg +47 -0
- package/locale/en.json +2 -1
- package/locale/zh.json +2 -1
- package/package.json +29 -25
- package/dist/authorized-route.d.ts +0 -68
- package/dist/automatch.d.ts +0 -98
- package/dist/override.d.ts +0 -39
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Dispatch-time role resolution (plan fallbacks-role-automatch Task 2; the
|
|
3
3
|
* dispatch-role stage added by plan dsh-148-agent-team-role-delivery Task 1).
|
|
4
4
|
*
|
|
5
|
-
* Precedence (first hit wins) across
|
|
5
|
+
* Precedence (first hit wins) across three ordered stages:
|
|
6
6
|
* 1. explicit — `session.header.agentPreset` trimmed matches a declared role
|
|
7
7
|
* id → that role's DECLARED RAW id (via the `roleIds` trimmed-id map, the
|
|
8
8
|
* same canonicalization the rules path uses); the reserved `'inherit'`
|
|
@@ -16,51 +16,30 @@
|
|
|
16
16
|
* not an error;
|
|
17
17
|
* 3. rules — the existing `resolveRole` passthrough, unchanged (a declared
|
|
18
18
|
* role wins; no-rule-match / an explicit rule targeting `'inherit'`
|
|
19
|
-
* resolve to `'inherit'`)
|
|
20
|
-
* 4. auto-match — only when stage 3 resolved to `'inherit'` AND
|
|
21
|
-
* `opts.automatchEnabled`: call `opts.automatch` with the agent. The hook
|
|
22
|
-
* (Task 3 supplies the bounded LLM caller) MUST return a declared raw id
|
|
23
|
-
* or `null`; the resolver defensively validates the return against
|
|
24
|
-
* `roleIds` — an unknown id warns and resolves to `'inherit'`, never an
|
|
25
|
-
* undeclared role. When `automatch` is undefined or `automatchEnabled`
|
|
26
|
-
* is false the hook is skipped.
|
|
19
|
+
* resolve to `'inherit'`).
|
|
27
20
|
*
|
|
28
21
|
* `'inherit'` is the single "no specific role" outcome — reached by
|
|
29
|
-
* no-rule-match OR an explicit rule targeting `'inherit'
|
|
30
|
-
* auto-match eligible when enabled. An operator who wants a class to never
|
|
31
|
-
* auto-match sets `roleAutoMatch: false` (→ `automatchEnabled: false`).
|
|
22
|
+
* no-rule-match OR an explicit rule targeting `'inherit'`.
|
|
32
23
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
24
|
+
* There is deliberately NO auto-match stage: the only host point it ever had
|
|
25
|
+
* was the deleted `agent/request` inject, and running an LLM call inside the
|
|
26
|
+
* walk's `stream()` would be a nested request per failure (design
|
|
27
|
+
* dsh-022-rc1-upgrade §2.2). Resolution is therefore pure and synchronous —
|
|
28
|
+
* the walk must throw its pre-attempt errors before the first chunk.
|
|
35
29
|
*
|
|
36
30
|
* @module dsh-llm-fallbacks/role-resolution
|
|
37
31
|
*/
|
|
38
32
|
import { type FallbacksRoleRule } from './config.ts';
|
|
39
33
|
import { type AgentLike } from './roles.ts';
|
|
40
|
-
import type { Selector } from './selectors.ts';
|
|
41
34
|
export type { FallbacksRoleRule } from './config.ts';
|
|
42
|
-
/** Options for {@link
|
|
43
|
-
export interface
|
|
35
|
+
/** Options for {@link resolveDeclaredRole}. */
|
|
36
|
+
export interface DeclaredRoleOptions {
|
|
44
37
|
/**
|
|
45
38
|
* Dispatch-declared role: the DECLARED RAW id the seam resolved from the
|
|
46
39
|
* Assignment/teammate prompt (`**Execute as**: <id>`). Validated against the
|
|
47
40
|
* live `roleIds` map; absent, `inherit`, or no-longer-declared → skipped.
|
|
48
41
|
*/
|
|
49
42
|
dispatchRole?: string;
|
|
50
|
-
/**
|
|
51
|
-
* Whether the auto-match stage may run (the `roleAutoMatch` config switch).
|
|
52
|
-
* When `false`, stage 4 never runs and the outcome is identical to the
|
|
53
|
-
* explicit/dispatch/rules stages alone.
|
|
54
|
-
*/
|
|
55
|
-
automatchEnabled: boolean;
|
|
56
|
-
/**
|
|
57
|
-
* Auto-match hook — Task 3 supplies the bounded LLM caller; the resolver
|
|
58
|
-
* itself contains no LLM. Contract: given the agent being resolved, return
|
|
59
|
-
* a declared raw role id or `null`. The resolver defensively validates the
|
|
60
|
-
* return against `roleIds` — an unknown id warns and resolves to
|
|
61
|
-
* `'inherit'`, never an undeclared role. Skipped when undefined.
|
|
62
|
-
*/
|
|
63
|
-
automatch?: (agent: AgentLike) => Promise<string | null>;
|
|
64
43
|
/** Warning sink — the decision path injects the plugin logger. */
|
|
65
44
|
warn: (message: string) => void;
|
|
66
45
|
/**
|
|
@@ -76,11 +55,7 @@ export interface ResolveRoleAtDispatchOptions {
|
|
|
76
55
|
};
|
|
77
56
|
}
|
|
78
57
|
/**
|
|
79
|
-
*
|
|
80
|
-
* (explicit → dispatch → rules → auto-match hook). Never throws on its own
|
|
81
|
-
* inputs; the hook's own rejection is deliberately NOT caught here (the
|
|
82
|
-
* Task-3/4 wiring bounds the LLM call so it never throws out of the request
|
|
83
|
-
* path).
|
|
58
|
+
* The dispatch role: explicit → dispatch-declared → rules.
|
|
84
59
|
*
|
|
85
60
|
* `roleIds` is the canonical trimmed-id map (`trimmed id → declared raw id`,
|
|
86
61
|
* built in `src/index.ts` apply()): every stage canonicalizes its reference
|
|
@@ -88,18 +63,4 @@ export interface ResolveRoleAtDispatchOptions {
|
|
|
88
63
|
* `chains.ts` / `commands.ts` find the stored `roles.list[]` entry exactly
|
|
89
64
|
* (qc2 F-001 — no silent role inertness on padded YAML ids).
|
|
90
65
|
*/
|
|
91
|
-
export declare function
|
|
92
|
-
/**
|
|
93
|
-
* The dispatch-time chain head (plan fallbacks-role-automatch Task 4): the
|
|
94
|
-
* FIRST candidate of a `resolveChainViews` pass whose `wildcard` flag is
|
|
95
|
-
* false — an exact `provider/model` entry. `provider/*` entries are NOT
|
|
96
|
-
* dispatch injection targets (no failing model to anchor them; a guessed
|
|
97
|
-
* injection risks a hard failure whose code may sit outside `triggerCodes`),
|
|
98
|
-
* so a wildcard-only chain yields `undefined` → no injection (today's
|
|
99
|
-
* behavior). No cooldown/failed filtering and no existence probe run here —
|
|
100
|
-
* a fresh subagent's first request has no failure-scoped state (see the Task
|
|
101
|
-
* 4 decision) and exact heads are never probed. Pairs with
|
|
102
|
-
* {@link resolveChainViews}'s parallel `wildcard` provenance (`src/chains.ts`);
|
|
103
|
-
* the caller resolves the role's concatenated chain first.
|
|
104
|
-
*/
|
|
105
|
-
export declare function firstExactCandidate(all: readonly Selector[], wildcard: readonly boolean[]): Selector | undefined;
|
|
66
|
+
export declare function resolveDeclaredRole(agent: AgentLike, rules: FallbacksRoleRule[], roleIds: ReadonlyMap<string, string>, opts: DeclaredRoleOptions): string;
|
package/dist/roles.d.ts
CHANGED
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* This module is the RULES stage. Dispatch-time role resolution
|
|
6
6
|
* (plan fallbacks-role-automatch; plan dsh-148 added the dispatch stage) is
|
|
7
|
-
* the
|
|
7
|
+
* the three-stage resolver in `role-resolution.ts` — explicit
|
|
8
8
|
* (`session.header.agentPreset`) → dispatch-declared role (the seam record
|
|
9
|
-
* resolved from `**Execute as**`) → this rules stage
|
|
10
|
-
*
|
|
11
|
-
* carrier for that explicit stage.
|
|
9
|
+
* resolved from `**Execute as**`) → this rules stage; `AgentLike` carries the
|
|
10
|
+
* additive `session.header.agentPreset?` carrier for that explicit stage.
|
|
12
11
|
*
|
|
13
12
|
* Precedence (first hit wins):
|
|
14
13
|
* 1. the first `rules` entry whose specified provider/model patterns match
|
package/dist/state.d.ts
CHANGED
|
@@ -1,69 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-agent fallback state
|
|
2
|
+
* Per-agent fallback state (direction C, plan dsh-022-rc1-upgrade).
|
|
3
3
|
*
|
|
4
4
|
* One `AgentFallbackState` per `agent.id`, held in a `FallbackStateStore`
|
|
5
|
-
* created at plugin apply, removed on `agent/disposed`,
|
|
6
|
-
*
|
|
7
|
-
* 无残留状态).
|
|
5
|
+
* created at plugin apply, removed on `agent/disposed`, and cleared wholesale
|
|
6
|
+
* on plugin dispose (spec §6: 无残留状态).
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* (turn, step), records `appliedTurnStep`, and clears `pendingSwitch`
|
|
18
|
-
* (anti-replay guard).
|
|
19
|
-
* 3. **Discard**: chain exhaustion / safety valve → the caller delegates
|
|
20
|
-
* (`next()`) and the pending state dies with the agent.
|
|
21
|
-
*
|
|
22
|
-
* **stepFailures** resets whenever (turn, step) advances (`syncStep`);
|
|
23
|
-
* **cooldown** (`CooldownStore`) is a `provider/model → expiry epoch ms` map
|
|
24
|
-
* with lazy expiry (`revertPolicy: 'never'` = `Infinity` TTL). The time-boxed
|
|
25
|
-
* cooldown deliberately survives idle cleanup so `cooldown-expiry` revert
|
|
26
|
-
* (US-4 / T4 integration) works across turns.
|
|
8
|
+
* The store is the CROSS-CALL memory the chain walk reads: **cooldown**
|
|
9
|
+
* (`CooldownStore` — a `provider/model → expiry epoch ms` map with lazy expiry;
|
|
10
|
+
* `revertPolicy: 'never'` = `Infinity` TTL) plus the **recovery** circuit
|
|
11
|
+
* (`RecoveryStore`). The time-boxed cooldown deliberately survives idle
|
|
12
|
+
* cleanup so `cooldown-expiry` revert (US-4 / T4 integration) works across
|
|
13
|
+
* turns. Per-request bookkeeping (the failed-candidate set, the switch cap) is
|
|
14
|
+
* walk-local: a walk lives inside one `stream()` call, so nothing durable is
|
|
15
|
+
* kept for it here.
|
|
27
16
|
*
|
|
28
17
|
* @module dsh-llm-fallbacks/state
|
|
29
18
|
*/
|
|
30
19
|
import type { FallbackSwitchReason } from './events.ts';
|
|
31
|
-
import { CooldownStore
|
|
20
|
+
import { CooldownStore } from './cooldown.ts';
|
|
32
21
|
import { RecoveryStore } from './recovery.ts';
|
|
33
22
|
import type { RecoveryPolicy } from './config.ts';
|
|
34
|
-
/**
|
|
35
|
-
* What a switch's failure was actually about.
|
|
36
|
-
*
|
|
37
|
-
* `'route'` (the default) is every failure the plugin has ever walked: the
|
|
38
|
-
* provider/model itself is unhealthy (auth, quota, rate limit, ...), so the
|
|
39
|
-
* from-route is cooled down and counted as a route failure.
|
|
40
|
-
* `'request'` is a failure of the REQUEST on a healthy route — a
|
|
41
|
-
* `CONTEXT_WINDOW_EXCEEDED` rejection is the request being too big, not the
|
|
42
|
-
* model being down. `commit()` keeps the step-scoped bookkeeping for those
|
|
43
|
-
* but writes no suppression and no recovery failure (see `src/index.ts`
|
|
44
|
-
* `REQUEST_SCOPED_CODES`).
|
|
45
|
-
*/
|
|
46
|
-
export type SwitchScope = 'route' | 'request';
|
|
47
|
-
/** Decision awaiting application at the next `agent/request` boundary (spec §5.1). */
|
|
48
|
-
export interface PendingSwitch {
|
|
49
|
-
from: {
|
|
50
|
-
provider: string;
|
|
51
|
-
model: string;
|
|
52
|
-
};
|
|
53
|
-
to: {
|
|
54
|
-
provider: string;
|
|
55
|
-
model: string;
|
|
56
|
-
};
|
|
57
|
-
role: string;
|
|
58
|
-
reason: FallbackSwitchReason;
|
|
59
|
-
/**
|
|
60
|
-
* What the failure was about — absent means `'route'`, the semantics every
|
|
61
|
-
* switch had before request-scoped codes existed (same optional-with-a-
|
|
62
|
-
* documented-default idiom as `FallbacksRole.fallback`). Only the
|
|
63
|
-
* `agent/request-error` listener ever sets `'request'`.
|
|
64
|
-
*/
|
|
65
|
-
scope?: SwitchScope;
|
|
66
|
-
}
|
|
67
23
|
/**
|
|
68
24
|
* One plugin-originated subagent switch the host allowlist blocked (plan
|
|
69
25
|
* dsh-012-subagent-routing T3; spec D1). The intersection of the resolved
|
|
@@ -85,8 +41,9 @@ export interface BlockedSwitchAttempt {
|
|
|
85
41
|
/**
|
|
86
42
|
* How the effective subagent chain head was chosen (plan dsh-012-subagent-routing
|
|
87
43
|
* T5 / T2 M3). Lives on a per-agent map (mirrors {@link BlockedSwitchAttempt}),
|
|
88
|
-
* not on {@link AgentFallbackState}
|
|
89
|
-
* (
|
|
44
|
+
* not on {@link AgentFallbackState}. The write point is the dispatch seam
|
|
45
|
+
* (direction C): `authorized` = the spawn request carried an explicit route,
|
|
46
|
+
* `injected` = the seam mounted the child on the virtual chain row.
|
|
90
47
|
*/
|
|
91
48
|
export type ChainHeadSource = 'authorized' | 'injected';
|
|
92
49
|
/**
|
|
@@ -104,34 +61,16 @@ export interface EffectiveChainHead {
|
|
|
104
61
|
/** `authorized` = host spawn preserved; `injected` = plugin role-inject. */
|
|
105
62
|
source: ChainHeadSource;
|
|
106
63
|
}
|
|
107
|
-
/**
|
|
108
|
-
export interface StepFailures {
|
|
109
|
-
turn: number;
|
|
110
|
-
step: number;
|
|
111
|
-
/** `${provider}/${model}` keys failed in this step (double suppression with cooldown). */
|
|
112
|
-
failed: StepFailureSet;
|
|
113
|
-
/** Switches committed in this step; the caller judges it against `maxSwitchesPerStep`. */
|
|
114
|
-
switchCount: number;
|
|
115
|
-
}
|
|
116
|
-
/** One agent's whole fallback runtime state (spec §5.1). */
|
|
64
|
+
/** One agent's fallback runtime state. */
|
|
117
65
|
export interface AgentFallbackState {
|
|
118
|
-
/** Decision produced but not yet applied at an `agent/request` boundary. */
|
|
119
|
-
pendingSwitch?: PendingSwitch;
|
|
120
|
-
/** The (turn, step) the last pending switch was applied to — anti-replay. */
|
|
121
|
-
appliedTurnStep?: {
|
|
122
|
-
turn: number;
|
|
123
|
-
step: number;
|
|
124
|
-
};
|
|
125
|
-
/** Current (turn, step)'s failure bookkeeping. */
|
|
126
|
-
stepFailures: StepFailures;
|
|
127
66
|
/** `provider/model → expiry epoch ms`; lazily expired on read. */
|
|
128
67
|
cooldown: CooldownStore;
|
|
129
68
|
/** Half-open recovery bookkeeping (plan fallbacks-half-open-recovery P2). */
|
|
130
69
|
recovery: RecoveryStore;
|
|
131
70
|
}
|
|
132
71
|
/**
|
|
133
|
-
* The `Map<agent.id, AgentFallbackState>` store plus the state
|
|
134
|
-
*
|
|
72
|
+
* The `Map<agent.id, AgentFallbackState>` store plus the state operations the
|
|
73
|
+
* walk and the diagnostics surfaces read.
|
|
135
74
|
*/
|
|
136
75
|
export declare class FallbackStateStore {
|
|
137
76
|
private readonly states;
|
|
@@ -146,29 +85,6 @@ export declare class FallbackStateStore {
|
|
|
146
85
|
delete(agentId: string): void;
|
|
147
86
|
/** Remove every agent's state (plugin dispose — no residual state). */
|
|
148
87
|
clear(): void;
|
|
149
|
-
/** Reset step-scoped bookkeeping when (turn, step) advances (spec §5.1). */
|
|
150
|
-
syncStep(state: AgentFallbackState, turn: number, step: number): void;
|
|
151
|
-
/** Record the current model as failed in this step (spec §5.1). */
|
|
152
|
-
recordFailure(state: AgentFallbackState, key: string): void;
|
|
153
|
-
/** Bump the step's switch budget (spec §5.1). */
|
|
154
|
-
recordSwitch(state: AgentFallbackState): void;
|
|
155
|
-
/**
|
|
156
|
-
* Produce a pending switch. A fresh decision always supersedes a previous
|
|
157
|
-
* one and must be applicable at the current (turn, step), so the applied
|
|
158
|
-
* marker is cleared here (see module doc — chains).
|
|
159
|
-
*/
|
|
160
|
-
writePending(state: AgentFallbackState, pending: PendingSwitch): void;
|
|
161
|
-
/**
|
|
162
|
-
* Apply the pending switch at (turn, step) when one exists and has not
|
|
163
|
-
* already been applied there; records the applied marker and clears the
|
|
164
|
-
* pending switch (spec §5.1 lifecycle step 2).
|
|
165
|
-
*/
|
|
166
|
-
applyPending(state: AgentFallbackState, turn: number, step: number): PendingSwitch | undefined;
|
|
167
|
-
/**
|
|
168
|
-
* Defensive `agent/status` idle cleanup: drop per-step state. The time-boxed
|
|
169
|
-
* cooldown survives so `cooldown-expiry` revert works across turns (US-4).
|
|
170
|
-
*/
|
|
171
|
-
clearStepState(state: AgentFallbackState): void;
|
|
172
88
|
/** Suppress `key` until `untilEpochMs` (`Infinity` for `revertPolicy: 'never'`). */
|
|
173
89
|
suppress(state: AgentFallbackState, key: string, untilEpochMs: number): void;
|
|
174
90
|
/**
|
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
* STRUCTURALLY — the upstream reader is not published from
|
|
10
10
|
* `@deepseek-ai/dsh-tool-subagent` and throws on malformed payloads,
|
|
11
11
|
* which is incompatible with the fail-closed no-throw requirement;
|
|
12
|
-
* 2. the host settings
|
|
13
|
-
*
|
|
12
|
+
* 2. the host's live subagent model-selection settings value (the
|
|
13
|
+
* `subagent-model-selection-settings` settings namespace), typed via a
|
|
14
|
+
* type-only peer import that is erased at emit. rc.1 made the settings
|
|
15
|
+
* service's own snapshot reader private, so the wiring reads the settings
|
|
16
|
+
* form value and narrows it structurally here.
|
|
14
17
|
*
|
|
15
18
|
* The session event wins over settings (ADR read order). Anything present but
|
|
16
19
|
* unreadable yields `'unprovable'` (fail-closed): callers must not emit a
|
|
@@ -80,12 +83,22 @@ export type PolicySettings = {
|
|
|
80
83
|
/** Assignability guard: fails typecheck if the peer's settings shape drifts from the reader's snapshot contract. */
|
|
81
84
|
type AssertSettingsShape<T extends PolicySettings> = T;
|
|
82
85
|
/**
|
|
83
|
-
* The host settings
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
86
|
+
* The host settings value's shape, re-exported from the published peer
|
|
87
|
+
* subpath (type-only). Wiring passes the settings form's namespace value in as
|
|
88
|
+
* plain data; if upstream drifts, this alias fails typecheck instead of
|
|
89
|
+
* drifting silently at runtime (plan ADR).
|
|
87
90
|
*/
|
|
88
91
|
export type PeerSettingsSnapshot = AssertSettingsShape<SubagentModelSelectionSettings>;
|
|
92
|
+
/**
|
|
93
|
+
* Narrow a host settings value into the reader's snapshot contract.
|
|
94
|
+
* Reads STRUCTURALLY like {@link readSessionPolicyEvent} — rc.1 publishes no
|
|
95
|
+
* settings value reader — so a malformed value is reported, never raised; the
|
|
96
|
+
* wiring decides its failure face (warn + fail-closed).
|
|
97
|
+
*
|
|
98
|
+
* @param value - untrusted settings-form value for the policy namespace.
|
|
99
|
+
* @returns a detached snapshot, or undefined when the value is not one.
|
|
100
|
+
*/
|
|
101
|
+
export declare function policySettingsFrom(value: unknown): PolicySettings | undefined;
|
|
89
102
|
/**
|
|
90
103
|
* Structurally read the session's `subagent/model-selection-policy` event.
|
|
91
104
|
* Never throws: a present-but-malformed payload is reported, not raised.
|
|
@@ -116,8 +129,9 @@ export declare function readSessionPolicyEvent(events: readonly {
|
|
|
116
129
|
* malformed route list, yields `'unprovable'`. Never throws.
|
|
117
130
|
*
|
|
118
131
|
* @param event - result of {@link readSessionPolicyEvent}.
|
|
119
|
-
* @param settings - settings
|
|
120
|
-
*
|
|
132
|
+
* @param settings - host settings value narrowed by
|
|
133
|
+
* {@link policySettingsFrom}, or undefined when the host composes no such
|
|
134
|
+
* settings entry.
|
|
121
135
|
*/
|
|
122
136
|
export declare function effectivePolicy(event: SessionPolicyEventRead, settings: PolicySettings | undefined): EffectivePolicy;
|
|
123
137
|
export {};
|
package/dist/subagents-seam.d.ts
CHANGED
|
@@ -58,8 +58,25 @@
|
|
|
58
58
|
* (the runtime mints its own child id and `agent/created` has already run), in
|
|
59
59
|
* which case this module resolves the same question against the live roles
|
|
60
60
|
* source and carries the verdict onto the record keyed off the RESOLVED id,
|
|
61
|
-
* before the child's first pre-step. This module
|
|
62
|
-
*
|
|
61
|
+
* before the child's first pre-step. This module reads no provider persona
|
|
62
|
+
* capability and injects no agent preset.
|
|
63
|
+
*
|
|
64
|
+
* Virtual chain mount (plan dsh-022-rc1-upgrade §1.3; direction C): the SAME
|
|
65
|
+
* resolution point is where a dispatch child is mounted on the virtual
|
|
66
|
+
* `FallbacksChain/Auto` row — `request.agentOptions` is stamped (rc.1 merges
|
|
67
|
+
* it over the parent's route when `manager.startLocal` creates the child) when
|
|
68
|
+
* all five conditions hold: a resolved role, no explicit `agentOptions` route
|
|
69
|
+
* (explicit = authorized, untouched), a LOCAL backend, an active config whose
|
|
70
|
+
* resolved role chain has a dispatchable exact head ON the host allowlist when
|
|
71
|
+
* the subagent policy is `enabled`, and a policy that is not `unprovable`. The
|
|
72
|
+
* record carries `virtualRouteInjected` (sticky) and the effective chain head
|
|
73
|
+
* is labelled in `chainHeads` (`injected` / `authorized`). Injection is
|
|
74
|
+
* creation-time only BY CONSTRUCTION: rc.1's `startActivation` rejects any
|
|
75
|
+
* child id that already exists (`DUPLICATE_CHILD`), so this path never attaches
|
|
76
|
+
* to an existing child and cannot overwrite its durable selection — a
|
|
77
|
+
* caller-reserved `childId` is a pre-chosen identity for a NEW child. A
|
|
78
|
+
* non-injectable dispatch forwards the caller's spec untouched (external
|
|
79
|
+
* backends keep pure inheritance).
|
|
63
80
|
*
|
|
64
81
|
* In-session notice row (plan Task 3): the SAME install point registers the
|
|
65
82
|
* `agent/pre-step` emitter (`./role-notice.ts`) over the record map it owns, so
|
|
@@ -89,7 +106,9 @@
|
|
|
89
106
|
* @module dsh-llm-fallbacks/subagents-seam
|
|
90
107
|
*/
|
|
91
108
|
import type { Context } from '@deepseek-ai/cordis';
|
|
92
|
-
import { type FallbacksRole } from './config.ts';
|
|
109
|
+
import { type FallbacksConfig, type FallbacksRole } from './config.ts';
|
|
110
|
+
import type { EffectiveChainHead } from './state.ts';
|
|
111
|
+
import type { EffectivePolicy } from './subagent-policy.ts';
|
|
93
112
|
/**
|
|
94
113
|
* The cordis service-read waterfall event the seam registers on. Referenced
|
|
95
114
|
* ONCE (registration) so a future cordis rename is a one-line change here.
|
|
@@ -149,6 +168,18 @@ export interface SubagentSeamRecord {
|
|
|
149
168
|
* `true` survives.
|
|
150
169
|
*/
|
|
151
170
|
personaNotApplied?: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* `true` when this dispatch MOUNTED the child on the virtual
|
|
173
|
+
* `FallbacksChain/Auto` row (design §1.3): the plugin stamped the creation
|
|
174
|
+
* options, so the child's chain is plugin-originated and its switches are
|
|
175
|
+
* allowlist-filtered. Read by the chain walk (`ChainWalkDeps.seamRecord`)
|
|
176
|
+
* and never by a host surface.
|
|
177
|
+
*
|
|
178
|
+
* STICKY, like `personaNotApplied`: the mount happened at spawn and a record
|
|
179
|
+
* rewrite (a resume reusing the `childId`) carries no verdict of its own, so
|
|
180
|
+
* the first `true` survives.
|
|
181
|
+
*/
|
|
182
|
+
virtualRouteInjected?: true;
|
|
152
183
|
}
|
|
153
184
|
/** One consumed prompt content block (`@deepseek-ai/dsh-llm` `ContentBlock` text members). */
|
|
154
185
|
export interface SubagentPromptBlockView {
|
|
@@ -169,14 +200,25 @@ export interface SubagentStartRequestView {
|
|
|
169
200
|
readonly id?: string;
|
|
170
201
|
};
|
|
171
202
|
};
|
|
203
|
+
/**
|
|
204
|
+
* rc.1 `SubagentStartRequest.agentOptions` (design §1.1) — the route the
|
|
205
|
+
* caller (or the model, on the tool path) asked the child to run on. Read
|
|
206
|
+
* only: an explicit pair here is the AUTHORIZED route and is never touched.
|
|
207
|
+
*/
|
|
208
|
+
readonly agentOptions?: {
|
|
209
|
+
readonly provider?: string;
|
|
210
|
+
readonly model?: string;
|
|
211
|
+
};
|
|
172
212
|
}
|
|
173
213
|
/**
|
|
174
214
|
* Structural view of the activation spec (rc.1 `SubagentActivationSpec`,
|
|
175
|
-
* consumed fields only): `childId` is the caller-
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
215
|
+
* consumed fields only): `childId` is the caller-RESERVED identity of a NEW
|
|
216
|
+
* local child (rc.1 `startLocal` rejects an id that already exists with
|
|
217
|
+
* `DUPLICATE_CHILD`, so `startActivation` never attaches to a child that
|
|
218
|
+
* outlived an earlier activation) — also the record fallback when the request
|
|
219
|
+
* carries no Assignment header — and `label` is the catalog join's other half
|
|
220
|
+
* (the cataloged child membership carries this exact string). `signal`,
|
|
221
|
+
* `delivery` and every future field travel with the caller's spec object.
|
|
180
222
|
*/
|
|
181
223
|
export interface SubagentActivationSpecView {
|
|
182
224
|
readonly provider: string;
|
|
@@ -199,6 +241,23 @@ export interface SubagentSeamOptions {
|
|
|
199
241
|
* wired) ⇒ no verdict, exactly like a listener that never saw the child.
|
|
200
242
|
*/
|
|
201
243
|
roles?: () => readonly FallbacksRole[];
|
|
244
|
+
/**
|
|
245
|
+
* Live config reader — the SAME `source()` the runtime and the adapter walk
|
|
246
|
+
* read. Absent ⇒ the seam never mounts a child on the virtual route (a
|
|
247
|
+
* partial wiring keeps the native dispatch unchanged).
|
|
248
|
+
*/
|
|
249
|
+
source?: () => FallbacksConfig;
|
|
250
|
+
/**
|
|
251
|
+
* The host subagent model-selection policy for one agent (guarded read,
|
|
252
|
+
* fail-closed). Absent ⇒ treated as `'disabled'` (no policy to fail closed
|
|
253
|
+
* on); an `'unprovable'` answer skips the mount.
|
|
254
|
+
*/
|
|
255
|
+
policyFor?: (agentId: string) => EffectivePolicy;
|
|
256
|
+
/**
|
|
257
|
+
* Per-agent effective chain head, keyed by child session id (the Subagents
|
|
258
|
+
* card's read). Absent ⇒ the seam records no head label.
|
|
259
|
+
*/
|
|
260
|
+
chainHeads?: Map<string, EffectiveChainHead>;
|
|
202
261
|
/** Contained debug sink (at most one line per contained degrade). */
|
|
203
262
|
debug?: (message: string) => void;
|
|
204
263
|
}
|
|
@@ -220,8 +279,8 @@ export interface SubagentSeam {
|
|
|
220
279
|
* deliberately NOT cleared on `agent/disposed` — a continuable child's durable
|
|
221
280
|
* session survives the activation, so clearing the marker would let a
|
|
222
281
|
* re-activation resume append a second notice row to the same log. It is
|
|
223
|
-
* bounded (`NOTICE_EMITTED_LIMIT`) and cleared only
|
|
224
|
-
*
|
|
282
|
+
* bounded (`NOTICE_EMITTED_LIMIT`) and cleared only when the LAST applied
|
|
283
|
+
* fiber of its root disposes (a surviving fiber's children still need it).
|
|
225
284
|
*/
|
|
226
285
|
readonly noticeEmitted: Set<string>;
|
|
227
286
|
/**
|
|
@@ -233,7 +292,7 @@ export interface SubagentSeam {
|
|
|
233
292
|
* message.
|
|
234
293
|
*/
|
|
235
294
|
readonly ownsWrapper: boolean;
|
|
236
|
-
/**
|
|
295
|
+
/** Withdraw this fiber's registrations (its own lifecycle effect does this on teardown). */
|
|
237
296
|
dispose(): void;
|
|
238
297
|
}
|
|
239
298
|
/**
|
package/dist/tui-settings.d.ts
CHANGED
|
@@ -114,12 +114,14 @@ export declare const FALLBACKS_TUI_SECTION_NS = "fallbacks";
|
|
|
114
114
|
*/
|
|
115
115
|
export declare const JSON_FIELD_MAX_DRAFT_BYTES: number;
|
|
116
116
|
/**
|
|
117
|
-
* The `fallbacks` /settings section:
|
|
117
|
+
* The `fallbacks` /settings section: 10 fields covering the web-card
|
|
118
118
|
* capabilities (the default-model choice rides `rootChain`'s last entry and
|
|
119
119
|
* the per-role fallback strategy rides `roles.list` JSON; the config-level
|
|
120
120
|
* `enabled` switch is REMOVED — plan fallbacks-web-ux-alignment T2, the
|
|
121
121
|
* Plugins-page row toggle is the master switch and the runtime gates read
|
|
122
|
-
* content presence
|
|
122
|
+
* content presence; the always-mode retry cap and the dispatch role
|
|
123
|
+
* auto-match switch are REMOVED with their mechanisms — direction C, only a
|
|
124
|
+
* `FallbacksChain/Auto` session walks a chain). Scalar
|
|
123
125
|
* capabilities use native kinds (boolean/number/select); complex structures
|
|
124
126
|
* use `text` fields with custom `format`/`parse` that mirror the gateway's
|
|
125
127
|
* validation. Built fresh per call — the host deep-freezes whatever it
|
|
@@ -24,22 +24,22 @@
|
|
|
24
24
|
* re-narrow the condition to ignore `timeSlots`. Conformance-only chain
|
|
25
25
|
* edits still never churn (conformance gates the delegate, not the row).
|
|
26
26
|
*
|
|
27
|
-
* Adapter behavior (P1/P3): `listModels` advertises exactly the
|
|
28
|
-
* row; `stream()`
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* policy and the token meter — also never throwing.
|
|
27
|
+
* Adapter behavior (P1/P3, direction C): `listModels` advertises exactly the
|
|
28
|
+
* one virtual row; `stream()` WALKS the session's chain (cooldown / failed-set
|
|
29
|
+
* / switch cap / recovery bookkeeping) in `src/chain-walk.ts` — the walk is
|
|
30
|
+
* injected at install and owns the pre-attempt guards, so this module stays
|
|
31
|
+
* the catalog + metadata surface. `resolveModel` proxies the current effective
|
|
32
|
+
* head's metadata when resolvable (modalities/context-window/reasoning follow
|
|
33
|
+
* the head) with a permissive default otherwise — never throws.
|
|
34
|
+
* `providerRetryPolicy` and `imageRequestPricing` mirror the same head for the
|
|
35
|
+
* host-captured retry policy and the token meter — also never throwing.
|
|
37
36
|
*
|
|
38
37
|
* @module dsh-llm-fallbacks/virtual-adapter
|
|
39
38
|
*/
|
|
40
39
|
import type { Context } from '@deepseek-ai/cordis';
|
|
41
40
|
import { LlmAdapter, type GenerateOptions, type LlmImageRequestPricing, type LlmModelInfo, type LlmResolvedModelInfo, type LlmRuntime, type ResolvedRetryPolicy, type StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
42
41
|
import { type FallbacksConfig } from './config.ts';
|
|
42
|
+
import type { PolicyRoute } from './subagent-policy.ts';
|
|
43
43
|
/** Provider route of the virtual adapter (exact string, spec lock). */
|
|
44
44
|
export declare const FALLBACKS_PROVIDER = "FallbacksChain";
|
|
45
45
|
/**
|
|
@@ -49,19 +49,6 @@ export declare const FALLBACKS_PROVIDER = "FallbacksChain";
|
|
|
49
49
|
* {@link pickerDisplayName} (`Auto: <model>[<slot>]`).
|
|
50
50
|
*/
|
|
51
51
|
export declare const FALLBACKS_CHAIN_MODEL = "Auto";
|
|
52
|
-
/**
|
|
53
|
-
* `LlmError` code: the effective chain is empty — the virtual route has no
|
|
54
|
-
* head to delegate to (P1 guard).
|
|
55
|
-
*/
|
|
56
|
-
export declare const EMPTY_EFFECTIVE_CHAIN_CODE = "EMPTY_EFFECTIVE_CHAIN";
|
|
57
|
-
/**
|
|
58
|
-
* `LlmError` code: the effective chain head is not a dispatchable real pair
|
|
59
|
-
* (non-conforming all-day, wildcard selector, malformed selector, or a
|
|
60
|
-
* self-route back to `FallbacksChain/*` — the P1 recursion guard).
|
|
61
|
-
*/
|
|
62
|
-
export declare const UNDISPATCHABLE_HEAD_CODE = "UNDISPATCHABLE_EFFECTIVE_HEAD";
|
|
63
|
-
/** `LlmError` code (defensive): the `llm` runtime disappeared mid-flight. */
|
|
64
|
-
export declare const LLM_UNAVAILABLE_CODE = "LLM_UNAVAILABLE";
|
|
65
52
|
/** One dispatchable exact head: `provider/model`. Wildcards are never seeds (P3). */
|
|
66
53
|
export interface EffectiveHead {
|
|
67
54
|
provider: string;
|
|
@@ -69,16 +56,25 @@ export interface EffectiveHead {
|
|
|
69
56
|
}
|
|
70
57
|
/**
|
|
71
58
|
* The FIRST DISPATCHABLE exact head of a chain — the single definition of
|
|
72
|
-
* "effective head" (F-001) shared by
|
|
73
|
-
*
|
|
74
|
-
* `
|
|
75
|
-
*
|
|
59
|
+
* "effective head" (F-001) shared by this module's metadata paths
|
|
60
|
+
* (`pickerDisplayName` / `resolveModel` / `providerRetryPolicy` /
|
|
61
|
+
* `imageRequestPricing`), the walk's head and served-route resolution
|
|
62
|
+
* (`servedRouteOf` in `src/chain-walk.ts`), and the seam's mount gate
|
|
63
|
+
* (`injectRoute` in `src/subagents-seam.ts`). Walks the SAME chain
|
|
64
|
+
* `resolveEffectiveChain` produces, skipping entries that can never
|
|
76
65
|
* be dispatched: malformed selectors (config-warning path), `provider/*`
|
|
77
66
|
* `provider/*` wildcards (no real pair), and self-routes back to
|
|
78
67
|
* `FallbacksChain/*` (the P1 recursion guard). `undefined` when the chain
|
|
79
68
|
* is empty or no entry is dispatchable.
|
|
69
|
+
*
|
|
70
|
+
* `allowedModels` (spec D1) additionally restricts the result to the host
|
|
71
|
+
* subagent allowlist: a PLUGIN-originated route is only ever attempted on a
|
|
72
|
+
* route the host authorizes, so the seam's mount gate and a plugin-originated
|
|
73
|
+
* walk's start pass the enabled policy's list and an entry it excludes is
|
|
74
|
+
* skipped like any other non-dispatchable entry. An empty intersection is
|
|
75
|
+
* `undefined` — the caller declines the mount / refuses the walk.
|
|
80
76
|
*/
|
|
81
|
-
export declare function firstDispatchableExactHead(chain: readonly string[]): EffectiveHead | undefined;
|
|
77
|
+
export declare function firstDispatchableExactHead(chain: readonly string[], allowedModels?: readonly PolicyRoute[]): EffectiveHead | undefined;
|
|
82
78
|
/**
|
|
83
79
|
* The effective chain head at `now` (P1): the first DISPATCHABLE exact head
|
|
84
80
|
* of the SAME `resolveEffectiveChain` the routing engine uses — thin
|
|
@@ -92,8 +88,9 @@ export declare function firstDispatchableExactHead(chain: readonly string[]): Ef
|
|
|
92
88
|
* for a successful delegate —
|
|
93
89
|
* a legacy multi-model (or empty) all-day chain earns no primary
|
|
94
90
|
* semantics even though the picker row is visible. `resolveModel` then
|
|
95
|
-
* falls back to the permissive default
|
|
96
|
-
*
|
|
91
|
+
* falls back to the permissive default. The delegate side of that gate is the
|
|
92
|
+
* chain walk's ROOT-origin rule (`UNDISPATCHABLE_EFFECTIVE_HEAD`); a subagent
|
|
93
|
+
* walks its role chain instead, so the all-day shape never gates a child.
|
|
97
94
|
*/
|
|
98
95
|
export declare function effectiveHeadOf(config: FallbacksConfig, now: Date): EffectiveHead | undefined;
|
|
99
96
|
/**
|
|
@@ -105,17 +102,14 @@ export declare function effectiveHeadOf(config: FallbacksConfig, now: Date): Eff
|
|
|
105
102
|
*/
|
|
106
103
|
export declare function pickerDisplayName(config: FallbacksConfig, now?: Date, modelDisplayName?: string): string;
|
|
107
104
|
/**
|
|
108
|
-
* The virtual adapter (P1/P3). `stream()`
|
|
109
|
-
*
|
|
110
|
-
* or state writes live here — those stay in the `agent/request` /
|
|
111
|
-
* `agent/request-error` listeners. Exported so the defensive guard paths
|
|
112
|
-
* the registration lifecycle cannot reach (a registered route whose `llm`
|
|
113
|
-
* vanished mid-teardown) stay unit-testable by direct construction.
|
|
105
|
+
* The virtual adapter (P1/P3). `stream()` hands the call to the injected walk
|
|
106
|
+
* (see `src/chain-walk.ts`) — registration metadata stays here.
|
|
114
107
|
*/
|
|
115
108
|
export declare class FallbacksChainAdapter extends LlmAdapter {
|
|
116
109
|
private readonly readConfig;
|
|
117
110
|
private readonly getLlm;
|
|
118
|
-
|
|
111
|
+
private readonly walk;
|
|
112
|
+
constructor(readConfig: () => FallbacksConfig, getLlm: () => LlmRuntime | undefined, walk: (options: GenerateOptions) => AsyncIterable<StreamChunk>);
|
|
119
113
|
/** Minimal honest descriptor (P3) — the provider route IS the display name. */
|
|
120
114
|
providerInfo(provider: string): {
|
|
121
115
|
id: string;
|
|
@@ -157,14 +151,14 @@ export declare class FallbacksChainAdapter extends LlmAdapter {
|
|
|
157
151
|
*/
|
|
158
152
|
imageRequestPricing(_provider: string, _model: string): LlmImageRequestPricing | undefined;
|
|
159
153
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* `
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
154
|
+
* The virtual route's entry point (P1, direction C): the session's chain is
|
|
155
|
+
* WALKED (`src/chain-walk.ts`) — the walk owns the pre-attempt guards
|
|
156
|
+
* (`EMPTY_EFFECTIVE_CHAIN` / `UNDISPATCHABLE_EFFECTIVE_HEAD` /
|
|
157
|
+
* `LLM_UNAVAILABLE`, thrown synchronously before any chunk, so the runtime
|
|
158
|
+
* normalizes them exactly like an adapter throw) because only it can resolve
|
|
159
|
+
* WHICH chain the calling session walks: a subagent's role chain is not the
|
|
160
|
+
* root's all-day chain, so a root-only gate here would refuse a child that
|
|
161
|
+
* has a perfectly walkable role chain.
|
|
168
162
|
*/
|
|
169
163
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
170
164
|
}
|
|
@@ -174,7 +168,9 @@ export declare class FallbacksChainAdapter extends LlmAdapter {
|
|
|
174
168
|
* @param ctx - the plugin context.
|
|
175
169
|
* @param readConfig - live config reader (the same `source()` the runtime
|
|
176
170
|
* reads, so reconcile always sees COMMITTED composed snapshots).
|
|
171
|
+
* @param walk - the chain walk the adapter's `stream()` runs for one virtual
|
|
172
|
+
* call (built by `apply()` over its live state — see `src/chain-walk.ts`).
|
|
177
173
|
* @returns the reconcile thunk — call it from the settings `onChange` hook
|
|
178
174
|
* (child activation runs one reconcile on its own).
|
|
179
175
|
*/
|
|
180
|
-
export declare function installFallbacksAdapter(ctx: Context, readConfig: () => FallbacksConfig): () => void;
|
|
176
|
+
export declare function installFallbacksAdapter(ctx: Context, readConfig: () => FallbacksConfig, walk: (options: GenerateOptions) => AsyncIterable<StreamChunk>): () => void;
|
package/docs/DETAIL.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+

|