dsh-llm-fallbacks 0.4.4 → 0.5.1
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 +86 -25
- package/README.zh-CN.md +86 -25
- package/dist/client/SubagentRoleBadge.d.ts +40 -15
- package/dist/client/fallbacks-store.d.ts +0 -53
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +77 -150
- package/dist/client/locales.d.ts +2 -0
- package/dist/events.d.ts +2 -2
- package/dist/gateway.d.ts +2 -55
- package/dist/index.d.ts +6 -27
- package/dist/index.js +713 -129
- package/dist/role-notice.d.ts +156 -0
- package/dist/role-projection-key.d.ts +41 -0
- package/dist/role-projection.d.ts +151 -0
- package/dist/role-resolution.d.ts +11 -0
- package/dist/roles.d.ts +4 -1
- package/dist/seeds.d.ts +63 -21
- package/dist/subagents-seam.d.ts +292 -0
- package/dist/time-slots.d.ts +3 -2
- package/dist/virtual-adapter.d.ts +42 -6
- package/package.json +27 -25
- package/scripts/build-client.ts +1 -1
- package/scripts/repair-session-logs.ts +2040 -0
- package/scripts/session-logs/catalog.ts +547 -0
- package/scripts/session-logs/classify.ts +47 -0
- package/scripts/session-logs/publish.ts +583 -0
- package/scripts/session-logs/rules.ts +1227 -0
- package/scripts/repair-fallbacks-switch-logs.ts +0 -342
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-session role notice row (plan role-based-subagent-adoption Task 3).
|
|
3
|
+
*
|
|
4
|
+
* The child of a role-carrying dispatch gets ONE durable transcript row naming
|
|
5
|
+
* the role it was dispatched as, so the role is visible where the work is read
|
|
6
|
+
* — inside the child's own session — without reaching for live plugin state.
|
|
7
|
+
*
|
|
8
|
+
* Channel: an `agent/pre-step` listener that appends EXACTLY ONE message to the
|
|
9
|
+
* decision the loop is about to enter (`decision.messages`), which the loop
|
|
10
|
+
* then commits durably on the step's first attempt
|
|
11
|
+
* (`agent-loop/src/agent.ts:373-378` — `if (firstAttempt) for (const message of
|
|
12
|
+
* decision.messages)` the loop commits each message to the session log as a
|
|
13
|
+
* `'user/message'`). The listener is registered with `{ prepend: true }` (the
|
|
14
|
+
* `modelSwitchNotice` precedent in
|
|
15
|
+
* `core/agent/src/model-selection.ts:108-125`), so it is the OUTERMOST layer:
|
|
16
|
+
* `await next()` yields the FINAL decision every other listener shaped. The
|
|
17
|
+
* contract is EXACTLY ONE row, not its position: a plugin that registers its
|
|
18
|
+
* own `{ prepend: true }` `agent/pre-step` listener AFTER this one sits outside
|
|
19
|
+
* it and may append after the notice, and the loop commits the decision's
|
|
20
|
+
* messages in order — so the row is appended to whatever the inner listeners
|
|
21
|
+
* shaped, not guaranteed to be the last one.
|
|
22
|
+
*
|
|
23
|
+
* NOT `agent.inject()`: that queue is documented as lossy — "It may miss a
|
|
24
|
+
* request whose pre-step already claimed its batch. Cancellation or disposal
|
|
25
|
+
* may discard pending context." (`core/agent/src/runtime-types.ts:231-241`), and
|
|
26
|
+
* with `wakeup: false` an idle child is never woken to receive it. The pre-step
|
|
27
|
+
* decision has neither failure mode.
|
|
28
|
+
*
|
|
29
|
+
* Session-write contract (plan Global Constraints, hard): the row is a
|
|
30
|
+
* `user/message` built with `createUserMessage` and the sanctioned plugin source
|
|
31
|
+
* `{ kind: 'plugin', plugin: 'dsh-llm-fallbacks', form: 'notice', summary: … }`.
|
|
32
|
+
* `plugin` is a member of the FROZEN released source-kind set the V2→V3 edge
|
|
33
|
+
* classifies (`dsh-session-format-v2-to-v3/lib/index.js:14-30`: 15 kinds,
|
|
34
|
+
* includes `plugin`); a bespoke kind would write fine and then refuse at the
|
|
35
|
+
* next frozen edge with `cannot safely transform unclassified message source`
|
|
36
|
+
* (`:125`). NO custom session event type, NO `ignorable` marker — a mount-only
|
|
37
|
+
* plugin cannot publish either (`.mstar/projects/_default/references/
|
|
38
|
+
* subagent-role-badge-visible/explore-role-seam-report.md` §Q3). The guard test
|
|
39
|
+
* in `tests/role-notice.spec.ts` pins that membership against a checked-in
|
|
40
|
+
* mirror of the set.
|
|
41
|
+
*
|
|
42
|
+
* Once per child SESSION: the emitter consumes the seam's per-child record
|
|
43
|
+
* marker (`firstNoticePending`) AND a per-agent `emitted` set (mirroring the
|
|
44
|
+
* existing `dispatchInjected` pattern), both written only AFTER the row is built
|
|
45
|
+
* — so a throwing producer degrades without consuming the child's one notice.
|
|
46
|
+
* Never for a root agent, never for `inherit`/unresolved (those never get a
|
|
47
|
+
* record), never twice across steps or within-step retries.
|
|
48
|
+
*
|
|
49
|
+
* CF-6 — the emitted marker is SESSION-stable and bounded. `Agent.id` IS the
|
|
50
|
+
* child session id, and a continuable child's durable session outlives its
|
|
51
|
+
* activation, so the caller (`src/index.ts`) keeps this marker across
|
|
52
|
+
* `agent/disposed` and clears it only when the plugin itself is disposed; that
|
|
53
|
+
* is what makes the documented "exactly one notice row per child session" hold
|
|
54
|
+
* across a dispose + re-activate resume. Because it is never per-activation
|
|
55
|
+
* cleared it must be bounded: {@link markNoticeEmitted} evicts the oldest entry
|
|
56
|
+
* once {@link NOTICE_EMITTED_LIMIT} sessions are remembered (a child evicted
|
|
57
|
+
* that way can at worst be announced a second time, and the projection fold is
|
|
58
|
+
* last-wins, so no wrong role is ever produced).
|
|
59
|
+
*
|
|
60
|
+
* Degrade-never-crash: any throw in the path is contained with ONE debug line
|
|
61
|
+
* and the decision is returned UNCHANGED — a step is never broken, and no
|
|
62
|
+
* partial row is ever published.
|
|
63
|
+
*
|
|
64
|
+
* @module dsh-llm-fallbacks/role-notice
|
|
65
|
+
*/
|
|
66
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
67
|
+
import { type UserMessage } from '@deepseek-ai/dsh-llm';
|
|
68
|
+
import type { SubagentSeamRecord } from './subagents-seam.ts';
|
|
69
|
+
/**
|
|
70
|
+
* The `plugin` field of every notice this plugin writes. Stable: the durable
|
|
71
|
+
* source's provenance label is read from it (`ui-conversation`
|
|
72
|
+
* `context-provenance.ts` — "the plugin id, or the bare source kind").
|
|
73
|
+
*/
|
|
74
|
+
export declare const ROLE_NOTICE_PLUGIN = "dsh-llm-fallbacks";
|
|
75
|
+
/**
|
|
76
|
+
* The message source KIND the notice rides. `plugin` is the sanctioned kind for
|
|
77
|
+
* a plugin-authored notice and a member of the frozen released set (see the
|
|
78
|
+
* module header); the value is exported so the guard test asserts on the exact
|
|
79
|
+
* literal the emitter uses.
|
|
80
|
+
*/
|
|
81
|
+
export declare const ROLE_NOTICE_SOURCE_KIND = "plugin";
|
|
82
|
+
/** Suffix appended when the role declares a persona that was NOT delivered. */
|
|
83
|
+
export declare const ROLE_NOTICE_PERSONA_SKIPPED_SUFFIX = " (persona not applied)";
|
|
84
|
+
/**
|
|
85
|
+
* How many child sessions whose notice was already emitted stay remembered
|
|
86
|
+
* (CF-6). The marker is session-stable — kept across `agent/disposed` so a
|
|
87
|
+
* re-activated continuable child is not announced twice — so it needs a bound:
|
|
88
|
+
* past this many sessions the oldest entry is evicted (worst case: a later
|
|
89
|
+
* re-activation of that evicted child writes a second identical row; the
|
|
90
|
+
* projection fold is last-wins, so the pill never names a wrong role).
|
|
91
|
+
*/
|
|
92
|
+
export declare const NOTICE_EMITTED_LIMIT = 256;
|
|
93
|
+
/**
|
|
94
|
+
* Mark one child session as announced, evicting the oldest remembered session
|
|
95
|
+
* once {@link NOTICE_EMITTED_LIMIT} is reached (insertion-ordered `Set`
|
|
96
|
+
* semantics: the oldest entry is the first value). A session already marked
|
|
97
|
+
* keeps its position — its marker is only ever written once.
|
|
98
|
+
*/
|
|
99
|
+
export declare function markNoticeEmitted(emitted: Set<string>, childSessionId: string): void;
|
|
100
|
+
/** One notice-message producer: role id + whether the declared persona was skipped. */
|
|
101
|
+
export type RoleNoticeBuilder = (role: string, personaNotApplied: boolean) => UserMessage;
|
|
102
|
+
/**
|
|
103
|
+
* Build the ONE role notice row (pure): `[role: <id>]`, with
|
|
104
|
+
* ` (persona not applied)` appended when the role declares a persona that Task
|
|
105
|
+
* 2's capability gate declined to deliver. The summary keeps `role: <id>` either
|
|
106
|
+
* way — it is the collapsed row's one-line account, and the durable role string
|
|
107
|
+
* must live there as well as in the content.
|
|
108
|
+
*
|
|
109
|
+
* @param role - the DECLARED RAW role id resolved at the dispatch seam.
|
|
110
|
+
* @param personaNotApplied - `true` only when a declared persona was skipped.
|
|
111
|
+
*/
|
|
112
|
+
export declare function buildRoleNotice(role: string, personaNotApplied: boolean): UserMessage;
|
|
113
|
+
/** Options for {@link installRoleNotice}. */
|
|
114
|
+
export interface RoleNoticeOptions {
|
|
115
|
+
/**
|
|
116
|
+
* The dispatch seam's per-child records, keyed by child SESSION id (which IS
|
|
117
|
+
* `Agent.id`). Read at each pre-step — never captured — so a record written by
|
|
118
|
+
* a dispatch that races the child's first step is still seen.
|
|
119
|
+
*/
|
|
120
|
+
records: Map<string, SubagentSeamRecord>;
|
|
121
|
+
/**
|
|
122
|
+
* Per-agent marker of children whose notice was already emitted (mirrors the
|
|
123
|
+
* runtime's `dispatchInjected`): the in-memory half of the once-per-child
|
|
124
|
+
* guarantee, independent of the record's `firstNoticePending` (which a record
|
|
125
|
+
* rewrite preserves deliberately) and of the loop's retry bookkeeping.
|
|
126
|
+
*
|
|
127
|
+
* SESSION-stable and bounded (CF-6): the caller keeps it across
|
|
128
|
+
* `agent/disposed` so a re-activated child is never announced twice, and
|
|
129
|
+
* {@link markNoticeEmitted} bounds it. ROOT-shared across the fibers applied
|
|
130
|
+
* over one root (CF-5), so a second fiber's emitter cannot announce a child
|
|
131
|
+
* the first already announced.
|
|
132
|
+
*/
|
|
133
|
+
emitted: Set<string>;
|
|
134
|
+
/**
|
|
135
|
+
* Message producer. Defaults to {@link buildRoleNotice}; the only reason to
|
|
136
|
+
* replace it is to pin the contained-degrade path (a throwing producer must
|
|
137
|
+
* publish no row and must NOT consume the child's one notice).
|
|
138
|
+
*/
|
|
139
|
+
buildNotice?: RoleNoticeBuilder;
|
|
140
|
+
/** Contained debug sink (at most ONE line per contained degrade). */
|
|
141
|
+
debug?: (message: string) => void;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Register the `agent/pre-step` notice emitter on `ctx`; returns its disposer
|
|
145
|
+
* (the owning fiber's teardown registers it, exactly like the seam's other
|
|
146
|
+
* listeners).
|
|
147
|
+
*
|
|
148
|
+
* Emission order per pre-step (cheapest first, all read-only until the emit):
|
|
149
|
+
* `next()` → reject/abort → empty decision → `origin === 'subagent'` → already
|
|
150
|
+
* emitted → pending record. The empty-decision guard is NOT cosmetic: the loop
|
|
151
|
+
* skips a step whose admitted messages are empty (`phase.step === 0 &&
|
|
152
|
+
* decision.messages.length === 0` → turn completed, no model call), so a notice
|
|
153
|
+
* pushed into an empty decision would turn a no-op step into a model call whose
|
|
154
|
+
* only input is our row.
|
|
155
|
+
*/
|
|
156
|
+
export declare function installRoleNotice(ctx: Context, options: RoleNoticeOptions): () => void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session-projection contract of the subagent role pill (plan
|
|
3
|
+
* role-based-subagent-adoption Task 3b): the ONE key the host fold
|
|
4
|
+
* (`./role-projection.ts`) publishes and the session-header badge reads.
|
|
5
|
+
*
|
|
6
|
+
* PURE module — no `@deepseek-ai/*` import, no host-only dependency — because
|
|
7
|
+
* BOTH halves import it: the host through the seam's install point, the client
|
|
8
|
+
* through `SubagentRoleBadge`. Splitting the key (and the value guard) out of
|
|
9
|
+
* the host module is what keeps the client half free of the fold's host
|
|
10
|
+
* imports, and a single shared literal is what keeps the two halves from
|
|
11
|
+
* drifting into a silent "no pill".
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-llm-fallbacks/role-projection-key
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The projection key the role pill reads (`useProjection(ROLE_PROJECTION_KEY)`).
|
|
17
|
+
* Lower-camel like the host's own session-scoped keys (`modelSelection`,
|
|
18
|
+
* `sessionListMetadata`); the `fallbacks` prefix keeps a third-party key
|
|
19
|
+
* unambiguous inside the shared `SessionProjectionMap`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const ROLE_PROJECTION_KEY = "fallbacksSubagentRole";
|
|
22
|
+
/**
|
|
23
|
+
* The client-visible value of the role projection: the DECLARED RAW role id the
|
|
24
|
+
* child was dispatched as (padding included — it is the id the settings
|
|
25
|
+
* document declares), or `null` while the child's log carries no role notice
|
|
26
|
+
* row (`./role-notice.ts`).
|
|
27
|
+
*
|
|
28
|
+
* A role id, NOT a record: the route is a SEPARATE host projection
|
|
29
|
+
* (`modelSelection`), because the notice row is written once at the child's
|
|
30
|
+
* first step while the route it ends up on can change afterwards.
|
|
31
|
+
*/
|
|
32
|
+
export type RoleProjectionValue = string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Read a projection value defensively (degrade-never-crash): only a non-blank
|
|
35
|
+
* string is a role. `undefined` — the key is not registered on this host, or no
|
|
36
|
+
* baseline/frame has carried it yet — and every foreign value read as "no
|
|
37
|
+
* role", so the pill collapses to nothing instead of naming something wrong.
|
|
38
|
+
*
|
|
39
|
+
* @param value - the raw `useProjection('<key>')` value.
|
|
40
|
+
*/
|
|
41
|
+
export declare function readRoleProjectionValue(value: unknown): string | undefined;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable role visibility for the session-header badge (plan
|
|
3
|
+
* role-based-subagent-adoption Task 3b) — ONE host-side session projection unit
|
|
4
|
+
* folding the role out of the child's OWN durable log.
|
|
5
|
+
*
|
|
6
|
+
* Data source = Task 3's notice row, the plugin's single write primitive: the
|
|
7
|
+
* fold reads the child's committed `user/message` events, keeps the one whose
|
|
8
|
+
* provenance is ours (`source.kind === 'plugin'` + `source.plugin ===
|
|
9
|
+
* ROLE_NOTICE_PLUGIN`), and parses the role out of its **content text**
|
|
10
|
+
* (`[role: <id>]`). No second write path, no new event type, no plugin-owned
|
|
11
|
+
* store: the projection is a pure READ of the log, and the host's projection
|
|
12
|
+
* cache checkpoints every registered unit at session creation, `turn/end` and
|
|
13
|
+
* disposal — so a settled child (served as an unpublished observation whose
|
|
14
|
+
* `projections` ride the follow opening snapshot) and a post-restart view both
|
|
15
|
+
* work with nothing of ours on disk.
|
|
16
|
+
*
|
|
17
|
+
* NEVER `source.summary`: summaries are truncated at
|
|
18
|
+
* `CONTEXT_SUMMARY_MAX_CHARS = 120` (`@deepseek-ai/dsh-llm` `message.ts`,
|
|
19
|
+
* applied by `boundContextSummary`) while a role id has no length bound in the
|
|
20
|
+
* settings schema — a long id would be CUT there and the pill would name a role
|
|
21
|
+
* that does not exist. The content blocks are unbounded, so the content text is
|
|
22
|
+
* the only correct source; the row's provenance is what makes the pre-filter
|
|
23
|
+
* cheap.
|
|
24
|
+
*
|
|
25
|
+
* Mount-only + STRUCTURAL: the registry and the unit contract are consumed as
|
|
26
|
+
* the local structural views below and this module value-imports no host seam,
|
|
27
|
+
* so the host calls INTO us. The declared
|
|
28
|
+
* `@deepseek-ai/dsh-session-projection` peerDependency (Task 3b L2 review M-2)
|
|
29
|
+
* serves the in-process Step-0 spec, which builds the REAL registry — being the
|
|
30
|
+
* real registry is the whole point of that proof — not a runtime import here
|
|
31
|
+
* (the badge reads its seat structurally for the same reason).
|
|
32
|
+
* The runtime shape relied on — `register({ key, stateSchema, init, apply,
|
|
33
|
+
* wire: { viewSchema, view }, stateVersion })` → unregister disposer — is the
|
|
34
|
+
* documented `ProjectionDefinition` contract of
|
|
35
|
+
* `@deepseek-ai/dsh-session-projection@0.1.5-rc.1`
|
|
36
|
+
* (`lib/types/index.d.ts:38-80,150-152`); only `.parse` is ever called on the
|
|
37
|
+
* schemas (`lib/index.js:255,305,433`).
|
|
38
|
+
*
|
|
39
|
+
* @module dsh-llm-fallbacks/role-projection
|
|
40
|
+
*/
|
|
41
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
42
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
43
|
+
import { type RoleProjectionValue } from './role-projection-key.ts';
|
|
44
|
+
/**
|
|
45
|
+
* The `.parse` face the registry uses on a definition's `stateSchema` /
|
|
46
|
+
* `wire.viewSchema` (a structural stand-in for the peer's `ZodType`, so this
|
|
47
|
+
* module needs no zod import and no new peer).
|
|
48
|
+
*/
|
|
49
|
+
interface ProjectionValueSchema<T> {
|
|
50
|
+
parse(value: unknown): T;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* One registered projection unit, as this plugin declares it (the consumed
|
|
54
|
+
* subset of the host's `ProjectionDefinition`): `init` and `apply` are PURE and
|
|
55
|
+
* SYNCHRONOUS, `state` is plain JSON (the persisted-checkpoint precondition),
|
|
56
|
+
* and `wire` publishes the client-visible value.
|
|
57
|
+
*/
|
|
58
|
+
export interface RoleProjectionUnit {
|
|
59
|
+
readonly key: string;
|
|
60
|
+
readonly stateSchema: ProjectionValueSchema<RoleProjectionState | null>;
|
|
61
|
+
init(header: unknown, inheritedEventCount: number): RoleProjectionState | null;
|
|
62
|
+
apply(state: RoleProjectionState | null, event: SessionEvent): RoleProjectionState | null;
|
|
63
|
+
readonly stateVersion: number;
|
|
64
|
+
readonly wire: {
|
|
65
|
+
readonly viewSchema: ProjectionValueSchema<RoleProjectionValue>;
|
|
66
|
+
view(state: RoleProjectionState | null): RoleProjectionValue;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The host fold state: the fork boundary the fold was initialized with, plus the
|
|
71
|
+
* resolved role once an OWN notice row has landed (`role` absent until then).
|
|
72
|
+
* An OBJECT, not a bare string, because the registry's publication rule is
|
|
73
|
+
* reference identity (`Object.is`) — the fold must be able to prove it left the
|
|
74
|
+
* state untouched, and that proof is only meaningful for a value the fold could
|
|
75
|
+
* otherwise re-allocate.
|
|
76
|
+
*
|
|
77
|
+
* `inheritedEventCount` is part of the state, not a closure constant: the
|
|
78
|
+
* registry hands `init` the exact fork-inherited prefix length and then folds
|
|
79
|
+
* the WHOLE log into `apply`, so the boundary has to survive into every `apply`
|
|
80
|
+
* call (and into a persisted checkpoint row) for the fold to skip an ancestor's
|
|
81
|
+
* events.
|
|
82
|
+
*/
|
|
83
|
+
export interface RoleProjectionState {
|
|
84
|
+
/**
|
|
85
|
+
* The exact fork-inherited prefix length `init` was handed: events with
|
|
86
|
+
* `seq < inheritedEventCount` belong to the ANCESTOR the child was seeded
|
|
87
|
+
* from, never to the child.
|
|
88
|
+
*/
|
|
89
|
+
readonly inheritedEventCount: number;
|
|
90
|
+
/** The DECLARED RAW role id, verbatim (padding included); absent until then. */
|
|
91
|
+
readonly role?: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The registry face this plugin uses (`ctx.sessionProjections`): the
|
|
95
|
+
* client-visible `register` overload plus the read faces the in-process proof
|
|
96
|
+
* reads. Structural on purpose — see the module header.
|
|
97
|
+
*/
|
|
98
|
+
export interface RoleProjectionRegistryView {
|
|
99
|
+
register(unit: RoleProjectionUnit): () => void;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The projection state/version. Bump `stateVersion` whenever the fold changes —
|
|
103
|
+
* v2 added the fork boundary (`inheritedEventCount`), so a v1 checkpoint row is
|
|
104
|
+
* NOT re-usable and must be refolded from the log.
|
|
105
|
+
*/
|
|
106
|
+
export declare const ROLE_PROJECTION_STATE_VERSION = 2;
|
|
107
|
+
/**
|
|
108
|
+
* The ONE role projection unit (plan Task 3b).
|
|
109
|
+
*
|
|
110
|
+
* `init` records the fork boundary and nothing else (a child's own log carries
|
|
111
|
+
* no role yet), `apply` skips every event BELOW that boundary — the seeded
|
|
112
|
+
* prefix is the ANCESTOR's log, and the plan pins "child whose notice never
|
|
113
|
+
* landed: no pill, never a wrong pill" — and otherwise folds last-wins over our
|
|
114
|
+
* own notice rows, returning the SAME state reference for every event it does
|
|
115
|
+
* not use (the registry's `Object.is` rule: an unchanged reference produces zero
|
|
116
|
+
* downstream work and no client publication) — including a repeated notice with
|
|
117
|
+
* the same role, which is what makes the fold idempotent under a replay.
|
|
118
|
+
*
|
|
119
|
+
* The boundary carries into the state because the registry calls `apply` with
|
|
120
|
+
* the WHOLE log (including the fork prefix) on every rung: `buildCell`,
|
|
121
|
+
* `drive`'s late-registration fold and `restore` all fold from `init` forward.
|
|
122
|
+
* The host's own sibling unit in the same tree does exactly this
|
|
123
|
+
* (`@deepseek-ai/dsh-subagent` `subagentCatalogProjectionDefinition`:
|
|
124
|
+
* `init: (_header, inheritedEventCount) => ({ inheritedEventCount })` and
|
|
125
|
+
* `apply: … || event.seq < state.inheritedEventCount → return state`).
|
|
126
|
+
*/
|
|
127
|
+
export declare const roleProjectionUnit: RoleProjectionUnit;
|
|
128
|
+
/** Options for {@link installRoleProjection}. */
|
|
129
|
+
export interface RoleProjectionOptions {
|
|
130
|
+
/** Contained debug sink (at most ONE line per contained degrade). */
|
|
131
|
+
debug?: (message: string) => void;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Register {@link roleProjectionUnit} on `ctx.sessionProjections` through the
|
|
135
|
+
* same conditional-inject idiom the plugin already uses for `settings` / `llm` /
|
|
136
|
+
* `typert`: the child activates only when the registry is composed, so a
|
|
137
|
+
* composition without it keeps the key absent and the badge renders nothing
|
|
138
|
+
* (capability absence — never a dispatch failure, never a crash).
|
|
139
|
+
*
|
|
140
|
+
* Registration is an effect on the child fiber: a second apply over the same
|
|
141
|
+
* root registers the SAME key at the SAME `stateVersion`, which the registry
|
|
142
|
+
* counts (one unit, the key survives until the last holder unloads) — so this
|
|
143
|
+
* needs no dedupe of its own, and a reshaped registry that refuses the
|
|
144
|
+
* registration degrades with one contained debug line.
|
|
145
|
+
*
|
|
146
|
+
* @param ctx - the plugin fiber's context.
|
|
147
|
+
* @param options - contained debug sink.
|
|
148
|
+
* @returns the disposer that withdraws the child (and with it the key).
|
|
149
|
+
*/
|
|
150
|
+
export declare function installRoleProjection(ctx: Context, options?: RoleProjectionOptions): () => void;
|
|
151
|
+
export {};
|
|
@@ -51,6 +51,17 @@ export interface ResolveRoleAtDispatchOptions {
|
|
|
51
51
|
automatch?: (agent: AgentLike) => Promise<string | null>;
|
|
52
52
|
/** Warning sink — the decision path injects the plugin logger. */
|
|
53
53
|
warn: (message: string) => void;
|
|
54
|
+
/**
|
|
55
|
+
* The route the agent's request was actually SERVED by, when that differs
|
|
56
|
+
* from the pair recorded on the agent (the virtual `FallbacksChain/Auto`
|
|
57
|
+
* row). Rule matching accepts either, so a rule keyed on the real chain head
|
|
58
|
+
* keeps matching a child whose recorded route is the virtual pair (QC1 I-1).
|
|
59
|
+
* `undefined` for every real route.
|
|
60
|
+
*/
|
|
61
|
+
servedRoute?: {
|
|
62
|
+
provider: string;
|
|
63
|
+
model: string;
|
|
64
|
+
};
|
|
54
65
|
}
|
|
55
66
|
/**
|
|
56
67
|
* Resolve the dispatch-time role for an agent across three ordered stages
|
package/dist/roles.d.ts
CHANGED
|
@@ -82,4 +82,7 @@ export interface AgentLike {
|
|
|
82
82
|
* decision path injects the cordis `logger.warn` so every plugin warning
|
|
83
83
|
* flows through the plugin log namespace (qc2 F-002 / qc3 S-3).
|
|
84
84
|
*/
|
|
85
|
-
export declare function resolveRole(agent: AgentLike, rules: FallbacksRoleRule[], roleIds: ReadonlyMap<string, string>, warn?: (message: string) => void
|
|
85
|
+
export declare function resolveRole(agent: AgentLike, rules: FallbacksRoleRule[], roleIds: ReadonlyMap<string, string>, warn?: (message: string) => void, servedRoute?: {
|
|
86
|
+
provider: string;
|
|
87
|
+
model: string;
|
|
88
|
+
}): string;
|
package/dist/seeds.d.ts
CHANGED
|
@@ -11,20 +11,34 @@
|
|
|
11
11
|
* State model (spec §9.2) — two stores, strictly separated:
|
|
12
12
|
* 1. operator config rows (persisted; a seeded role is a plain
|
|
13
13
|
* `roles.list` row `{ id, persona }`), and
|
|
14
|
-
* 2. an in-memory per-apply seed registry
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* 2. an in-memory per-apply seed registry keyed by PRODUCER LABEL
|
|
15
|
+
* (`bundled` | set name | `external`), each slice holding that
|
|
16
|
+
* producer's current id → `{ persona, set }` declarations. Declare =
|
|
17
|
+
* replacement PER PRODUCER: a declare replaces only its own slice, so
|
|
18
|
+
* the plugin's bundled presets stay `bundled` even when a companion
|
|
19
|
+
* merge-preserves them into its own batch (plan
|
|
20
|
+
* seeds-source-and-persona-width, Decisions #1).
|
|
17
21
|
* `seeded` / `personaOverridden` / `source` are DERIVED at read time, never
|
|
18
22
|
* stored — a config round-trip cannot orphan an override (AC-3), and
|
|
19
23
|
* provenance rides the in-memory registry only, so it can never churn a
|
|
20
24
|
* settings write.
|
|
21
25
|
*
|
|
26
|
+
* Resolution precedence (Decisions #2): `bundled` wins for any id the
|
|
27
|
+
* plugin's own preset self-declare currently declares; otherwise the most
|
|
28
|
+
* recent non-bundled producer declaring the id wins (its set name, or
|
|
29
|
+
* `external` when unnamed); no live declaration ⇒ `user`. Unnamed producers
|
|
30
|
+
* share the `external` slice (Decisions #3); `{ set }` is the remedy.
|
|
31
|
+
*
|
|
22
32
|
* Materialization (spec §9.2): append `{ id, persona }` (two keys only,
|
|
23
33
|
* R4) / attach row untouched / at-default tracking / override preserved +
|
|
24
|
-
* `'persona-source'` conflict / omitted id drops from the
|
|
25
|
-
* the row stays (R2).
|
|
26
|
-
*
|
|
27
|
-
*
|
|
34
|
+
* `'persona-source'` conflict / omitted id drops from the declaring
|
|
35
|
+
* producer's slice while the row stays (R2). Tracking is driven by the
|
|
36
|
+
* id's RESOLVED effective default — `prior` = resolved before the
|
|
37
|
+
* declare, `incoming` = resolved after the candidate slice commit — so a
|
|
38
|
+
* producer that is not the resolution winner for an id it declares never
|
|
39
|
+
* rewrites the row (F-001). No delta → no settings write (idempotent,
|
|
40
|
+
* AC-1). Compute → write → commit registry: a failed write throws and
|
|
41
|
+
* leaves the registry unchanged (retry-safe).
|
|
28
42
|
*
|
|
29
43
|
* @module dsh-llm-fallbacks/seeds
|
|
30
44
|
*/
|
|
@@ -151,13 +165,22 @@ export interface SeedsIo {
|
|
|
151
165
|
*/
|
|
152
166
|
export declare class FallbacksSeedManager {
|
|
153
167
|
private readonly logger;
|
|
154
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* Per-producer declaration registry: producer label (`bundled` | set
|
|
170
|
+
* name | `external`) → that producer's current slice. A declare replaces
|
|
171
|
+
* only its own slice; an empty batch drops the slice.
|
|
172
|
+
*/
|
|
155
173
|
private registry;
|
|
174
|
+
/** Monotonic declare counter — "most recent producer" for resolution precedence. */
|
|
175
|
+
private declareSeq;
|
|
156
176
|
constructor(logger: FallbacksConfigLogger);
|
|
157
177
|
/**
|
|
158
|
-
* Declare seeds with replacement semantics — the batch is
|
|
159
|
-
*
|
|
160
|
-
* drop out of
|
|
178
|
+
* Declare seeds with replacement semantics PER PRODUCER — the batch is
|
|
179
|
+
* the producer's FULL current declaration set; ids omitted from the
|
|
180
|
+
* batch drop out of that producer's slice while their rows remain (R2).
|
|
181
|
+
* Other producers' slices are untouched, so a companion merge-preserving
|
|
182
|
+
* the bundled presets into its own batch never re-labels them (Decisions
|
|
183
|
+
* #1).
|
|
161
184
|
*
|
|
162
185
|
* `options` labels the whole batch's provenance (spec §2), resolved once
|
|
163
186
|
* per call: an invalid set name warns once and degrades to the unnamed
|
|
@@ -165,27 +188,46 @@ export declare class FallbacksSeedManager {
|
|
|
165
188
|
*
|
|
166
189
|
* Per-id validation AS DECLARED (spec §9.3): non-string / pattern miss /
|
|
167
190
|
* reserved `inherit` / duplicate-in-batch → skip + warn; valid siblings
|
|
168
|
-
* still apply (AC-5). Materializes per spec §9.2
|
|
169
|
-
*
|
|
170
|
-
* (
|
|
171
|
-
*
|
|
191
|
+
* still apply (AC-5). Materializes per spec §9.2 against the id's
|
|
192
|
+
* RESOLVED effective default before/after the candidate slice commit
|
|
193
|
+
* (at-default tracking follows the resolution winner — a producer that
|
|
194
|
+
* is not the winner for an id it declares never rewrites the row),
|
|
195
|
+
* writes only when the computed `{ list, rules }` differs from the
|
|
196
|
+
* current composed roles (idempotent, AC-1), and commits the slice only
|
|
197
|
+
* after a successful write (compute → write → commit; retry-safe). An
|
|
198
|
+
* empty batch drops the producer's slice.
|
|
172
199
|
*/
|
|
173
200
|
declare(seeds: readonly SeedDeclaration[], io: SeedsIo, options?: SeedsInternalDeclareOptions): Promise<SeedDeclareOutcome>;
|
|
201
|
+
/**
|
|
202
|
+
* Resolve one id's live declaration across producer slices (spec §2,
|
|
203
|
+
* Decisions #2): the bundled preset self-declare wins for any id it
|
|
204
|
+
* currently declares; otherwise the most recent non-bundled producer
|
|
205
|
+
* declaring the id wins (its set name, or `external` when unnamed); no
|
|
206
|
+
* live declaration ⇒ `undefined` (the row reads `user`). Single place —
|
|
207
|
+
* `effectiveRoles`, `wireStatus`, and `revert` can never disagree. The
|
|
208
|
+
* optional `registry` argument resolves against a CANDIDATE registry
|
|
209
|
+
* (the declaring slice already replaced) — the same precedence, used by
|
|
210
|
+
* `declare` to compute the post-commit effective default for tracking.
|
|
211
|
+
*/
|
|
212
|
+
private resolveDeclared;
|
|
174
213
|
/**
|
|
175
214
|
* Readback (b) — sync, derived: every config row annotated with
|
|
176
215
|
* `seeded` / `personaOverridden` / `seedPersona` (trimmed row-id
|
|
177
216
|
* membership in the live declaration set; persona inequality) plus the
|
|
178
|
-
* row's provenance `source` (spec §2): the
|
|
179
|
-
* `user` when no live declaration covers the row.
|
|
180
|
-
* override-shaped is stored, so a config round-trip cannot
|
|
217
|
+
* row's provenance `source` (spec §2): the winning producer's label per
|
|
218
|
+
* `resolveDeclared`, or `user` when no live declaration covers the row.
|
|
219
|
+
* Nothing override-shaped is stored, so a config round-trip cannot
|
|
220
|
+
* orphan state.
|
|
181
221
|
*/
|
|
182
222
|
effectiveRoles(io: SeedsIo): EffectiveRolesReadback;
|
|
183
223
|
/** Card badge state (spec §9.4): seeded rows, with the override flag and the provenance label. */
|
|
184
224
|
wireStatus(io: SeedsIo): SeedsWireStatus[];
|
|
185
225
|
/**
|
|
186
|
-
* Revert one id to the CURRENT declared seed default (AC-3)
|
|
187
|
-
*
|
|
188
|
-
*
|
|
226
|
+
* Revert one id to the CURRENT declared seed default (AC-3) — resolved
|
|
227
|
+
* through the same precedence as the readbacks, so a preserved bundled id
|
|
228
|
+
* reverts to the bundled persona, never a companion's copy. Writes
|
|
229
|
+
* persona only — the row is otherwise copied verbatim (R4). Ids with no
|
|
230
|
+
* live declaration (`not-seeded`) or with a deleted row (`row-absent`)
|
|
189
231
|
* return a non-reverted outcome without throwing; a failed settings
|
|
190
232
|
* write propagates loudly (spec §9.1).
|
|
191
233
|
*/
|