dsh-advisor 0.1.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/LICENSE +21 -0
- package/README.i18n.yaml +7 -0
- package/README.md +303 -0
- package/README.zh.md +166 -0
- package/cordis.patch.yml +6 -0
- package/lib/advisor-runtime.d.ts +242 -0
- package/lib/advisor-runtime.js +662 -0
- package/lib/advisor-runtime.js.map +1 -0
- package/lib/client/advisor-card.d.ts +90 -0
- package/lib/client/advisor-store.d.ts +310 -0
- package/lib/client/index.d.ts +39 -0
- package/lib/client/locales.d.ts +40 -0
- package/lib/client.d.ts +1 -0
- package/lib/client.js +840 -0
- package/lib/commands.d.ts +136 -0
- package/lib/commands.js +185 -0
- package/lib/commands.js.map +1 -0
- package/lib/config.d.ts +74 -0
- package/lib/config.js +93 -0
- package/lib/config.js.map +1 -0
- package/lib/delivery.d.ts +129 -0
- package/lib/delivery.js +169 -0
- package/lib/delivery.js.map +1 -0
- package/lib/emission-guard.d.ts +99 -0
- package/lib/emission-guard.js +155 -0
- package/lib/emission-guard.js.map +1 -0
- package/lib/gateway.d.ts +116 -0
- package/lib/gateway.js +214 -0
- package/lib/gateway.js.map +1 -0
- package/lib/index.d.ts +48 -0
- package/lib/index.js +485 -0
- package/lib/index.js.map +1 -0
- package/lib/kinds.d.ts +38 -0
- package/lib/kinds.js +24 -0
- package/lib/kinds.js.map +1 -0
- package/lib/prompts.d.ts +22 -0
- package/lib/prompts.js +38 -0
- package/lib/prompts.js.map +1 -0
- package/lib/settings.d.ts +96 -0
- package/lib/settings.js +141 -0
- package/lib/settings.js.map +1 -0
- package/lib/transcript.d.ts +257 -0
- package/lib/transcript.js +530 -0
- package/lib/transcript.js.map +1 -0
- package/package.json +90 -0
- package/scripts/build-client.mjs +268 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T1 (plan dsh-advisor-settings-n2) — host-side `advisor` settings namespace
|
|
3
|
+
* + live source wiring.
|
|
4
|
+
*
|
|
5
|
+
* The plugin-row config (the `entry` passed to the plugin's `apply`) is the
|
|
6
|
+
* composition BASE of the `advisor` settings namespace: when a dsh settings
|
|
7
|
+
* service is mounted, its user layer is layered on top (schema defaults →
|
|
8
|
+
* base → user layer) and the runtime reads the live resolved value through
|
|
9
|
+
* the bridge's `source` thunk — the same source-thunk pattern as dsh's
|
|
10
|
+
* `agent-default-model`. Without a settings service the conditional
|
|
11
|
+
* `ctx.inject(['settings'], ...)` child never activates and the source is
|
|
12
|
+
* exactly the entry: behavior identical to today.
|
|
13
|
+
*
|
|
14
|
+
* The hard gate is untouched: the source returns the RAW composed config and
|
|
15
|
+
* every consumer passes it through `resolveAdvisorConfig` — the SSOT for the
|
|
16
|
+
* enabled-without-pair disabled-with-reason resolution (no model call).
|
|
17
|
+
*
|
|
18
|
+
* The namespace does NOT join the apiproxy configuration-client boundary on
|
|
19
|
+
* current upstream dsh builds: the host's `exposedNamespaces()` unions only
|
|
20
|
+
* model-provider namespaces plus its own product namespaces (locale /
|
|
21
|
+
* permission / ui-conversation / ui-theme / ui-onboarding / agent-presets) —
|
|
22
|
+
* there is no registration-level opt-in in upstream dsh (verified against the
|
|
23
|
+
* pristine 20da39e snapshot; `SettingsRegisterOptions` has no
|
|
24
|
+
* `exposeToWebClients` key). The advisor namespace is therefore always absent
|
|
25
|
+
* from `settings.describe` on the web configuration boundary — but the web
|
|
26
|
+
* card reaches the config through the TypertRemoteService channel instead (plan
|
|
27
|
+
* dsh-advisor-settings-gateway-n5: `AdvisorConfigGateway` claims
|
|
28
|
+
* `/api/advisor/get` + `/api/advisor/set`, and the client calls
|
|
29
|
+
* `connection.rpc.call('/api', …)`; the in-process `ctx.settings.update`
|
|
30
|
+
* behind `set` carries no exposed-namespace check — the allowlist gate exists
|
|
31
|
+
* only in the apiproxy wire layer). The unexposed-namespace notice is now
|
|
32
|
+
* only the KD-G5 fallback (gateway unreachable). No host patch is applied or
|
|
33
|
+
* required for the plugin to function — the runtime reads the entry config
|
|
34
|
+
* exactly as before.
|
|
35
|
+
*
|
|
36
|
+
* @module dsh-advisor/settings
|
|
37
|
+
*/
|
|
38
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
39
|
+
import type { AdvisorConfig } from './config.js';
|
|
40
|
+
/** The `advisor` settings namespace (registered when a settings service exists). */
|
|
41
|
+
export declare const ADVISOR_SETTINGS_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
|
|
42
|
+
/**
|
|
43
|
+
* The live configuration source for the runtime.
|
|
44
|
+
*
|
|
45
|
+
* `source()` returns the RAW composed config (schema defaults → plugin-row
|
|
46
|
+
* base → settings user layer); consumers pass it through
|
|
47
|
+
* `resolveAdvisorConfig` — the hard gate stays the SSOT. `onChange` registers
|
|
48
|
+
* a callback that re-applies derived state whenever the composed value changes
|
|
49
|
+
* (attach, committed change, or detach back to the entry). The contract is
|
|
50
|
+
* `(cb) => void` per the plan: the listener set is owned by the consumer's
|
|
51
|
+
* plugin closure for its lifetime, and the detach path is handled by the
|
|
52
|
+
* inject child's disposer (the `installSettingsSection` contract), so no
|
|
53
|
+
* per-listener disposer is returned (qc1 S-3 — a discarded disposer would
|
|
54
|
+
* invite misuse).
|
|
55
|
+
*/
|
|
56
|
+
export interface AdvisorSettingsBridge {
|
|
57
|
+
source(): AdvisorConfig;
|
|
58
|
+
onChange(callback: () => void): void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Install the `advisor` settings namespace and wire the live source.
|
|
62
|
+
*
|
|
63
|
+
* Mirrors the dsh `agent-default-model` pattern exactly (the dsh-settings
|
|
64
|
+
* `installSettingsSection` contract): the registration rides a conditional
|
|
65
|
+
* `ctx.inject(['settings'], ...)` child, so with no settings service the
|
|
66
|
+
* source stays the entry config. `setSource` swaps the authoritative thunk
|
|
67
|
+
* (the settings scope's resolved value while attached); `onChange` fires at
|
|
68
|
+
* attach, on committed changes, and at detach. The registration carries NO
|
|
69
|
+
* `exposeToWebClients` option — upstream dsh (pristine 20da39e) has no such
|
|
70
|
+
* registration-level opt-in (its `exposedNamespaces()` unions model-provider
|
|
71
|
+
* plus product namespaces only), so the advisor namespace stays off the
|
|
72
|
+
* apiproxy web configuration boundary on every current dsh build. Web clients
|
|
73
|
+
* reach the config through the TypertRemoteService channel instead (plan
|
|
74
|
+
* dsh-advisor-settings-gateway-n5 — `/api/advisor/get` + `/api/advisor/set`);
|
|
75
|
+
* the in-process settings service is the write target behind the gateway's
|
|
76
|
+
* `set`. (A previous iteration believed the opt-in existed upstream and
|
|
77
|
+
* declared it here; that conclusion was a circular verification against a
|
|
78
|
+
* locally-modified staging tree — the option does not exist in upstream types
|
|
79
|
+
* and has been removed.)
|
|
80
|
+
*
|
|
81
|
+
* qc1 W-5 (multi-fiber dedupe): the host composes several dsh-advisor fibers,
|
|
82
|
+
* and this runs on EVERY instance — but `Settings.register` fails loud on a
|
|
83
|
+
* duplicate namespace (`settings namespace "advisor" is already registered`).
|
|
84
|
+
* The register call runs inside the conditional inject child, so the duplicate
|
|
85
|
+
* error surfaces ASYNCHRONOUSLY there (an outer try/catch around a library
|
|
86
|
+
* `installSettingsSection` call cannot see it) — this child body wraps the
|
|
87
|
+
* register instead. A deduped instance logs (debug) and keeps the
|
|
88
|
+
* entry-source fallback: its `source` thunk is only ever swapped by a
|
|
89
|
+
* SUCCESSFUL registration's setSource hook, so the ALREADY-REGISTERED
|
|
90
|
+
* instance owns the live namespace. The reviewer's settings wiring (the
|
|
91
|
+
* `bridge.onChange` live re-apply in `index.ts`) is the concern — it only
|
|
92
|
+
* re-applies when the reviewer's own bridge attached to the live scope; in
|
|
93
|
+
* practice the reviewer is the first apply, whose inject child registers
|
|
94
|
+
* first.
|
|
95
|
+
*/
|
|
96
|
+
export declare function installAdvisorSettings(ctx: Context, entry: AdvisorConfig): AdvisorSettingsBridge;
|
package/lib/settings.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T1 (plan dsh-advisor-settings-n2) — host-side `advisor` settings namespace
|
|
3
|
+
* + live source wiring.
|
|
4
|
+
*
|
|
5
|
+
* The plugin-row config (the `entry` passed to the plugin's `apply`) is the
|
|
6
|
+
* composition BASE of the `advisor` settings namespace: when a dsh settings
|
|
7
|
+
* service is mounted, its user layer is layered on top (schema defaults →
|
|
8
|
+
* base → user layer) and the runtime reads the live resolved value through
|
|
9
|
+
* the bridge's `source` thunk — the same source-thunk pattern as dsh's
|
|
10
|
+
* `agent-default-model`. Without a settings service the conditional
|
|
11
|
+
* `ctx.inject(['settings'], ...)` child never activates and the source is
|
|
12
|
+
* exactly the entry: behavior identical to today.
|
|
13
|
+
*
|
|
14
|
+
* The hard gate is untouched: the source returns the RAW composed config and
|
|
15
|
+
* every consumer passes it through `resolveAdvisorConfig` — the SSOT for the
|
|
16
|
+
* enabled-without-pair disabled-with-reason resolution (no model call).
|
|
17
|
+
*
|
|
18
|
+
* The namespace does NOT join the apiproxy configuration-client boundary on
|
|
19
|
+
* current upstream dsh builds: the host's `exposedNamespaces()` unions only
|
|
20
|
+
* model-provider namespaces plus its own product namespaces (locale /
|
|
21
|
+
* permission / ui-conversation / ui-theme / ui-onboarding / agent-presets) —
|
|
22
|
+
* there is no registration-level opt-in in upstream dsh (verified against the
|
|
23
|
+
* pristine 20da39e snapshot; `SettingsRegisterOptions` has no
|
|
24
|
+
* `exposeToWebClients` key). The advisor namespace is therefore always absent
|
|
25
|
+
* from `settings.describe` on the web configuration boundary — but the web
|
|
26
|
+
* card reaches the config through the TypertRemoteService channel instead (plan
|
|
27
|
+
* dsh-advisor-settings-gateway-n5: `AdvisorConfigGateway` claims
|
|
28
|
+
* `/api/advisor/get` + `/api/advisor/set`, and the client calls
|
|
29
|
+
* `connection.rpc.call('/api', …)`; the in-process `ctx.settings.update`
|
|
30
|
+
* behind `set` carries no exposed-namespace check — the allowlist gate exists
|
|
31
|
+
* only in the apiproxy wire layer). The unexposed-namespace notice is now
|
|
32
|
+
* only the KD-G5 fallback (gateway unreachable). No host patch is applied or
|
|
33
|
+
* required for the plugin to function — the runtime reads the entry config
|
|
34
|
+
* exactly as before.
|
|
35
|
+
*
|
|
36
|
+
* @module dsh-advisor/settings
|
|
37
|
+
*/
|
|
38
|
+
import { settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
39
|
+
import { Config } from './config.js';
|
|
40
|
+
/** The `advisor` settings namespace (registered when a settings service exists). */
|
|
41
|
+
export const ADVISOR_SETTINGS_NAMESPACE = settingsNamespace('advisor');
|
|
42
|
+
/**
|
|
43
|
+
* Mirror of @deepseek-ai/dsh-settings' `isUnloading` guard (its
|
|
44
|
+
* `installSettingsSection` skips source/listener work while the plugin fiber
|
|
45
|
+
* is unloading or disposed). The library compares `ctx.fiber.state` against
|
|
46
|
+
* `FiberState.DISPOSED` / `FiberState.UNLOADING`; the const enum is erased at
|
|
47
|
+
* runtime, so the vendored numeric values (4 / 5) are mirrored here.
|
|
48
|
+
*/
|
|
49
|
+
function isUnloading(ctx) {
|
|
50
|
+
const state = ctx.fiber?.state;
|
|
51
|
+
return state === 4 || state === 5;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Install the `advisor` settings namespace and wire the live source.
|
|
55
|
+
*
|
|
56
|
+
* Mirrors the dsh `agent-default-model` pattern exactly (the dsh-settings
|
|
57
|
+
* `installSettingsSection` contract): the registration rides a conditional
|
|
58
|
+
* `ctx.inject(['settings'], ...)` child, so with no settings service the
|
|
59
|
+
* source stays the entry config. `setSource` swaps the authoritative thunk
|
|
60
|
+
* (the settings scope's resolved value while attached); `onChange` fires at
|
|
61
|
+
* attach, on committed changes, and at detach. The registration carries NO
|
|
62
|
+
* `exposeToWebClients` option — upstream dsh (pristine 20da39e) has no such
|
|
63
|
+
* registration-level opt-in (its `exposedNamespaces()` unions model-provider
|
|
64
|
+
* plus product namespaces only), so the advisor namespace stays off the
|
|
65
|
+
* apiproxy web configuration boundary on every current dsh build. Web clients
|
|
66
|
+
* reach the config through the TypertRemoteService channel instead (plan
|
|
67
|
+
* dsh-advisor-settings-gateway-n5 — `/api/advisor/get` + `/api/advisor/set`);
|
|
68
|
+
* the in-process settings service is the write target behind the gateway's
|
|
69
|
+
* `set`. (A previous iteration believed the opt-in existed upstream and
|
|
70
|
+
* declared it here; that conclusion was a circular verification against a
|
|
71
|
+
* locally-modified staging tree — the option does not exist in upstream types
|
|
72
|
+
* and has been removed.)
|
|
73
|
+
*
|
|
74
|
+
* qc1 W-5 (multi-fiber dedupe): the host composes several dsh-advisor fibers,
|
|
75
|
+
* and this runs on EVERY instance — but `Settings.register` fails loud on a
|
|
76
|
+
* duplicate namespace (`settings namespace "advisor" is already registered`).
|
|
77
|
+
* The register call runs inside the conditional inject child, so the duplicate
|
|
78
|
+
* error surfaces ASYNCHRONOUSLY there (an outer try/catch around a library
|
|
79
|
+
* `installSettingsSection` call cannot see it) — this child body wraps the
|
|
80
|
+
* register instead. A deduped instance logs (debug) and keeps the
|
|
81
|
+
* entry-source fallback: its `source` thunk is only ever swapped by a
|
|
82
|
+
* SUCCESSFUL registration's setSource hook, so the ALREADY-REGISTERED
|
|
83
|
+
* instance owns the live namespace. The reviewer's settings wiring (the
|
|
84
|
+
* `bridge.onChange` live re-apply in `index.ts`) is the concern — it only
|
|
85
|
+
* re-applies when the reviewer's own bridge attached to the live scope; in
|
|
86
|
+
* practice the reviewer is the first apply, whose inject child registers
|
|
87
|
+
* first.
|
|
88
|
+
*/
|
|
89
|
+
export function installAdvisorSettings(ctx, entry) {
|
|
90
|
+
const listeners = new Set();
|
|
91
|
+
let source = () => entry;
|
|
92
|
+
const notify = () => {
|
|
93
|
+
for (const listener of [...listeners])
|
|
94
|
+
listener();
|
|
95
|
+
};
|
|
96
|
+
ctx.inject(['settings'], (sctx) => {
|
|
97
|
+
let scope;
|
|
98
|
+
try {
|
|
99
|
+
scope = sctx.settings.register(ADVISOR_SETTINGS_NAMESPACE, Config, {
|
|
100
|
+
base: entry,
|
|
101
|
+
// No `exposeToWebClients` here: the option does not exist in upstream
|
|
102
|
+
// dsh (verified against pristine 20da39e — `SettingsRegisterOptions`
|
|
103
|
+
// has no such key, and the host's `exposedNamespaces()` unions only
|
|
104
|
+
// model-provider plus product namespaces). The namespace stays off
|
|
105
|
+
// the apiproxy web configuration boundary; web clients reach the
|
|
106
|
+
// config through the TypertRemoteService channel (advisor/get +
|
|
107
|
+
// advisor/set), and the runtime reads the entry exactly as it would
|
|
108
|
+
// without a settings service.
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (!(error instanceof Error) || !error.message.includes('already registered'))
|
|
113
|
+
throw error;
|
|
114
|
+
ctx.logger('advisor').debug('settings namespace already registered — entry-source fallback (multi-fiber dedupe)');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Mirrors installSettingsSection: the source thunk reads the scope's live
|
|
118
|
+
// resolved value while attached, and the detach disposer falls back to the
|
|
119
|
+
// entry when the settings service goes away (skipped during unload).
|
|
120
|
+
source = () => scope.get();
|
|
121
|
+
sctx.effect(() => () => {
|
|
122
|
+
if (isUnloading(ctx))
|
|
123
|
+
return;
|
|
124
|
+
source = () => entry;
|
|
125
|
+
notify();
|
|
126
|
+
});
|
|
127
|
+
notify();
|
|
128
|
+
scope.watch(() => {
|
|
129
|
+
if (isUnloading(ctx))
|
|
130
|
+
return;
|
|
131
|
+
notify();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
source: () => source(),
|
|
136
|
+
onChange: (callback) => {
|
|
137
|
+
listeners.add(callback);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAE7D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGpC,oFAAoF;AACpF,MAAM,CAAC,MAAM,0BAA0B,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAA;AAEtE;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,GAAY;IAC/B,MAAM,KAAK,GAAI,GAAiD,CAAC,KAAK,EAAE,KAAK,CAAA;IAC7E,OAAO,KAAK,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,CAAA;AACnC,CAAC;AAqBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,UAAU,sBAAsB,CAAC,GAAY,EAAE,KAAoB;IACvE,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAA;IACvC,IAAI,MAAM,GAAG,GAAkB,EAAE,CAAC,KAAK,CAAA;IACvC,MAAM,MAAM,GAAG,GAAS,EAAE;QACxB,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,SAAS,CAAC;YAAE,QAAQ,EAAE,CAAA;IACnD,CAAC,CAAA;IACD,GAAG,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE;QAChC,IAAI,KAA+C,CAAA;QACnD,IAAI,CAAC;YACH,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,0BAA0B,EAAE,MAAM,EAAE;gBACjE,IAAI,EAAE,KAAK;gBACX,sEAAsE;gBACtE,qEAAqE;gBACrE,oEAAoE;gBACpE,mEAAmE;gBACnE,iEAAiE;gBACjE,gEAAgE;gBAChE,oEAAoE;gBACpE,8BAA8B;aAC/B,CAAC,CAAA;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC;gBAAE,MAAM,KAAK,CAAA;YAC3F,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,oFAAoF,CAAC,CAAA;YACjH,OAAM;QACR,CAAC;QACD,0EAA0E;QAC1E,2EAA2E;QAC3E,qEAAqE;QACrE,MAAM,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAA;QAC1B,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE;YACrB,IAAI,WAAW,CAAC,GAAG,CAAC;gBAAE,OAAM;YAC5B,MAAM,GAAG,GAAG,EAAE,CAAC,KAAK,CAAA;YACpB,MAAM,EAAE,CAAA;QACV,CAAC,CAAC,CAAA;QACF,MAAM,EAAE,CAAA;QACR,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE;YACf,IAAI,WAAW,CAAC,GAAG,CAAC;gBAAE,OAAM;YAC5B,MAAM,EAAE,CAAA;QACV,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IACF,OAAO;QACL,MAAM,EAAE,GAAkB,EAAE,CAAC,MAAM,EAAE;QACrC,QAAQ,EAAE,CAAC,QAAoB,EAAQ,EAAE;YACvC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACzB,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session observer + bounded delta renderer (spec §2 S2, §4 mapping table,
|
|
3
|
+
* §6 self-review exclusion, §8.3 KD-3, §8.5 KD-5).
|
|
4
|
+
*
|
|
5
|
+
* `DeltaRenderer` is the pure transcript core: a cursor over the session event
|
|
6
|
+
* log plus a fingerprint of the delivered prefix. `update(events)` extracts
|
|
7
|
+
* the newly appended messages, excludes advisor-source messages (self-review
|
|
8
|
+
* guard), and renders a role-annotated markdown delta bounded by the
|
|
9
|
+
* `maxDeltaMessages` window. A prefix rewrite — a `compact/*` event, a
|
|
10
|
+
* `user/message` with `surfaceOp.op === 'replace'`, or a fingerprint mismatch
|
|
11
|
+
* (defensive fallback) — resets the cursor and replays the full post-rewrite
|
|
12
|
+
* surface on the next update.
|
|
13
|
+
*
|
|
14
|
+
* `SessionTranscriptObserver` is the per-session wiring unit: it owns one
|
|
15
|
+
* `DeltaRenderer` per session id and dispatches `session/event` tuples to
|
|
16
|
+
* them. It has TWO trigger modes (KD-N4-5):
|
|
17
|
+
*
|
|
18
|
+
* 1. **turn/end** (standard turn-driven sessions): renders only on a stepped
|
|
19
|
+
* `turn/end` whose `reason.kind` is reviewable (`completed` | `max-tokens`
|
|
20
|
+
* | `error`; spec §4 — skip `aborted`/`blocked`/`interrupted`, i.e. do not
|
|
21
|
+
* critique user-cut-short turns). Byte-identical behavior — the original
|
|
22
|
+
* single-trigger path.
|
|
23
|
+
* 2. **agent reply complete** (harness/agentic sessions, which never emit
|
|
24
|
+
* `turn/end`): fires on human-input arrival (`user/message` with
|
|
25
|
+
* `source.kind === 'user'`, or `agent/inbox/spliced` whose `inserted`
|
|
26
|
+
* carries a message with `source.kind === 'user'` — inbox-spliced input
|
|
27
|
+
* may commit as that event first and never re-emit as `user/message`).
|
|
28
|
+
* Non-user inbox splices (the advisor's own inject/steer deliveries,
|
|
29
|
+
* workspace/tool/empty splices) never trigger (C-1 self-trigger fix).
|
|
30
|
+
* Before rendering,
|
|
31
|
+
* a read-only predicate checks for an unreviewed non-advisor
|
|
32
|
+
* `assistant/message` since the renderer cursor; on a miss the cursor is
|
|
33
|
+
* untouched (the first user input of a session neither triggers nor
|
|
34
|
+
* advances). Append-type triggers pass `events` minus the trigger itself
|
|
35
|
+
* (the arriving input opens the next round); rewrite-type triggers
|
|
36
|
+
* (compact summary replace) pass the full log to trigger the KD-5 replay.
|
|
37
|
+
* Both modes share the same renderer/cursor, so the cursor advance dedupes
|
|
38
|
+
* (one delta per completed round), and both call `onSteppedTurnEnd` (the
|
|
39
|
+
* immuneTurns countdown — T6) before rendering.
|
|
40
|
+
*
|
|
41
|
+
* **Mode latch**: once a session has produced ANY `turn/end` event
|
|
42
|
+
* (reviewable or not), the new gate sleeps for it — the session is a
|
|
43
|
+
* standard turn-driven one and keeps verbatim behavior, including spec §4
|
|
44
|
+
* skip-aborted (a cut-short first turn's unreviewed increment must not be
|
|
45
|
+
* supplementarily reviewed by the new gate). Harness/agentic sessions never
|
|
46
|
+
* emit `turn/end`, so their latch never arms.
|
|
47
|
+
*
|
|
48
|
+
* It also exposes the T6 delivery hooks: `onSteppedTurnEnd` (one completed
|
|
49
|
+
* stepped primary turn — the immuneTurns countdown) and `onRewrite` (a
|
|
50
|
+
* compact/replace event — the KD-5 latch reset). `index.ts` binds the cordis
|
|
51
|
+
* `session/event` / `session/disposed` / `agent/disposed` listeners into an
|
|
52
|
+
* instance of this class.
|
|
53
|
+
*
|
|
54
|
+
* @module dsh-advisor/transcript
|
|
55
|
+
*/
|
|
56
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
57
|
+
/** One incremental transcript delta handed to the advisor runtime (T4). */
|
|
58
|
+
export interface Delta {
|
|
59
|
+
/** Role-annotated markdown of the new (or replayed) transcript messages. */
|
|
60
|
+
readonly markdown: string;
|
|
61
|
+
/**
|
|
62
|
+
* Whether more transcript content is expected to follow the same logical
|
|
63
|
+
* update. dsh closes a turn with `turn/end` (all its steps are done), so a
|
|
64
|
+
* delta rendered at a stepped `turn/end` is always complete — `false` for
|
|
65
|
+
* the MVP (see the task-3 report for the rationale).
|
|
66
|
+
*/
|
|
67
|
+
readonly willContinue: boolean;
|
|
68
|
+
}
|
|
69
|
+
/** Options for one {@link DeltaRenderer}. */
|
|
70
|
+
export interface DeltaRendererOptions {
|
|
71
|
+
/**
|
|
72
|
+
* Bounded message window (spec §8.3 KD-3): keep the most recent N messages
|
|
73
|
+
* and prepend the truncation marker when a rendered delta would exceed N;
|
|
74
|
+
* `0` = unbounded. Default 60.
|
|
75
|
+
*/
|
|
76
|
+
readonly maxDeltaMessages: number;
|
|
77
|
+
}
|
|
78
|
+
/** Default delta window (KD-3). */
|
|
79
|
+
export declare const DEFAULT_MAX_DELTA_MESSAGES = 60;
|
|
80
|
+
/** Marker line prepended when a rendered delta omits earlier messages (KD-3). */
|
|
81
|
+
export declare const TRUNCATION_MARKER = "\u2026 <earlier messages omitted>";
|
|
82
|
+
/**
|
|
83
|
+
* Cursor + fingerprint bookkeeping over one session's event log.
|
|
84
|
+
*
|
|
85
|
+
* The renderer consumes the session event log (the full `session.events`
|
|
86
|
+
* snapshot on each call — the cordis wiring passes the live log). It keeps:
|
|
87
|
+
* - `cursor` — how many log events are consumed (seqs are contiguous from 0,
|
|
88
|
+
* so this equals the next unprocessed seq);
|
|
89
|
+
* - `surface` / `messages` — the current ordered surface of derived
|
|
90
|
+
* non-advisor messages (seqs + messages), maintained incrementally for
|
|
91
|
+
* appends and rebuilt from a full fold after a reset;
|
|
92
|
+
* - `deliveredFingerprint` — fingerprint of the delivered prefix, detecting a
|
|
93
|
+
* hidden rewrite;
|
|
94
|
+
* - `droppedPrefix` — whether messages before the bounded window were dropped
|
|
95
|
+
* (drives the truncation marker on replay renders).
|
|
96
|
+
*/
|
|
97
|
+
export declare class DeltaRenderer {
|
|
98
|
+
private maxDeltaMessages;
|
|
99
|
+
private cursor;
|
|
100
|
+
private readonly surface;
|
|
101
|
+
private readonly messages;
|
|
102
|
+
private deliveredFingerprint;
|
|
103
|
+
private droppedPrefix;
|
|
104
|
+
constructor(options?: Partial<DeltaRendererOptions>);
|
|
105
|
+
/**
|
|
106
|
+
* Update the bounded message window (live config — settings onChange, plan
|
|
107
|
+
* dsh-advisor-settings-n2 T1). Existing fold state is kept; the new bound
|
|
108
|
+
* applies from the next render on.
|
|
109
|
+
*/
|
|
110
|
+
setMaxDeltaMessages(value: number): void;
|
|
111
|
+
/**
|
|
112
|
+
* Process a session event log snapshot and return the next delta, or
|
|
113
|
+
* `undefined` when no new (renderable) content was appended.
|
|
114
|
+
*
|
|
115
|
+
* `skipLast` treats the final log event as excluded — the arriving trigger
|
|
116
|
+
* of an append-type review (qc3 F1): equivalent to the caller slicing
|
|
117
|
+
* `events.slice(0, events.length - 1)` WITHOUT the O(full-log) shallow copy
|
|
118
|
+
* the reply-complete gate used to pay on every review. The excluded event
|
|
119
|
+
* must be the last log entry (the wiring guarantees it).
|
|
120
|
+
*
|
|
121
|
+
* - Detects a reset: a rewrite event (`compact/*`, replace surface op) in
|
|
122
|
+
* the unconsumed portion, a shorter log than the cursor, or a delivered
|
|
123
|
+
* prefix whose fingerprint changed. On reset the cursor rewinds to 0 and
|
|
124
|
+
* the full post-rewrite surface is replayed (bounded by KD-3).
|
|
125
|
+
* - Otherwise appends the new events, extracts their messages (advisor
|
|
126
|
+
* excluded), and renders only those (incremental delta).
|
|
127
|
+
*/
|
|
128
|
+
update(events: readonly SessionEvent[], skipLast?: boolean): Delta | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* Full reset: rewind the cursor to 0 and drop all fold state. The next
|
|
131
|
+
* `update` replays the whole current surface (bounded). Also the KD-5
|
|
132
|
+
* reset surface for emission-guard / immuneTurns latches (T5/T6 hook into
|
|
133
|
+
* the same session lifecycle).
|
|
134
|
+
*/
|
|
135
|
+
reset(): void;
|
|
136
|
+
/**
|
|
137
|
+
* Seed the cursor to `length` (KD-5 seed-on-enable): skip existing history
|
|
138
|
+
* — the next update renders only events at/after `length` (no full-history
|
|
139
|
+
* replay, matching omp).
|
|
140
|
+
*/
|
|
141
|
+
seedTo(length: number): void;
|
|
142
|
+
/**
|
|
143
|
+
* Read-only predicate for the agentic reply-complete gate (KD-N4-5): does an
|
|
144
|
+
* unreviewed non-advisor `assistant/message` increment exist since the
|
|
145
|
+
* cursor? Scans the unconsumed log tail with the same derivation the
|
|
146
|
+
* renderer uses (an empty-content `assistant/message` derives no message, so
|
|
147
|
+
* it does not count) and excludes advisor-source messages (self-review
|
|
148
|
+
* guard, spec §6). NEVER mutates state — a blind `update()` here would
|
|
149
|
+
* pre-advance the cursor and lose the user prompt from the first
|
|
150
|
+
* standard-session turn/end delta, so the caller only calls `update()` when
|
|
151
|
+
* this returns true.
|
|
152
|
+
*/
|
|
153
|
+
hasUnreviewedAssistant(events: readonly SessionEvent[]): boolean;
|
|
154
|
+
/** Rebuild the fold from a full log (post-reset replay, bounded to `length`). */
|
|
155
|
+
private rebuild;
|
|
156
|
+
/** Incrementally fold new events in `[start, end)` (guaranteed append-only) and return added messages. */
|
|
157
|
+
private append;
|
|
158
|
+
/** Render the newly appended messages (incremental delta). */
|
|
159
|
+
private renderTail;
|
|
160
|
+
/** Render the current surface in full (post-reset replay, bounded). */
|
|
161
|
+
private renderSurface;
|
|
162
|
+
private render;
|
|
163
|
+
}
|
|
164
|
+
/** Callback contract for {@link SessionTranscriptObserver}. */
|
|
165
|
+
export interface SessionObserverOptions {
|
|
166
|
+
/** Bounded delta window (KD-3); forwarded to every per-session renderer. */
|
|
167
|
+
readonly maxDeltaMessages: number;
|
|
168
|
+
/** Invoked once per stepped reviewable turn/end with the rendered delta. */
|
|
169
|
+
readonly onDelta: (sessionId: string, delta: Delta) => void;
|
|
170
|
+
/**
|
|
171
|
+
* Invoked once per stepped reviewable turn/end (the same gate as `onDelta`),
|
|
172
|
+
* before the delta is rendered — the delivery module (T6) counts completed
|
|
173
|
+
* primary turns here to decrement its immuneTurns cooldown (spec §6).
|
|
174
|
+
*/
|
|
175
|
+
readonly onSteppedTurnEnd?: (sessionId: string) => void;
|
|
176
|
+
/**
|
|
177
|
+
* Invoked when a rewrite event is observed (`compact/*` or a non-append
|
|
178
|
+
* surface op) — the delivery module (T6) clears its immuneTurns latch here
|
|
179
|
+
* (KD-5 reset triggers). Fires before the turn gate: a rewrite can arrive
|
|
180
|
+
* outside a turn/end.
|
|
181
|
+
*/
|
|
182
|
+
readonly onRewrite?: (sessionId: string) => void;
|
|
183
|
+
}
|
|
184
|
+
/** True when a `turn/end` event carries a reviewable end reason. */
|
|
185
|
+
export declare function isReviewableTurnEnd(event: SessionEvent): boolean;
|
|
186
|
+
/**
|
|
187
|
+
* True when an event is a human-input arrival — the trigger of the agentic
|
|
188
|
+
* reply-complete gate (KD-N4-5).
|
|
189
|
+
*
|
|
190
|
+
* - `user/message` with `source.kind === 'user'` — the primary signal: a
|
|
191
|
+
* direct human prompt (the queued message claimed for a step).
|
|
192
|
+
* - `agent/inbox/spliced` — the fallback: inbox-spliced input commits as
|
|
193
|
+
* this log-only event first and may never re-emit as `user/message`
|
|
194
|
+
* (the merged `SessionEventMap` entry comes from the dsh-agent peer, per
|
|
195
|
+
* the `compact/*` precedent). Payload-discriminated (C-1 fix): the event
|
|
196
|
+
* only triggers when `inserted` is non-empty and carries at least one
|
|
197
|
+
* message whose `source.kind === 'user'`. Every other inbox mutation is
|
|
198
|
+
* excluded — the advisor's OWN inject/steer deliveries (source.kind
|
|
199
|
+
* `advisor`), workspace-context sync (`workspace-instructions`),
|
|
200
|
+
* tool-result splicing (`tool`), and claim/clear splices (empty
|
|
201
|
+
* `inserted`) must not self-trigger the review gate.
|
|
202
|
+
*
|
|
203
|
+
* Synthetic/injected user-role messages (tool results, advisor notes,
|
|
204
|
+
* workspace context) carry other `source.kind` values and never trigger.
|
|
205
|
+
*/
|
|
206
|
+
export declare function isHumanInputEvent(event: SessionEvent): boolean;
|
|
207
|
+
/**
|
|
208
|
+
* One renderer per session id, driven by `session/event` tuples. Cordis-free,
|
|
209
|
+
* so the wiring logic is unit-testable; `index.ts` binds the cordis listeners
|
|
210
|
+
* into an instance and forwards `session.events` (the live log).
|
|
211
|
+
*/
|
|
212
|
+
export declare class SessionTranscriptObserver {
|
|
213
|
+
private readonly options;
|
|
214
|
+
private readonly renderers;
|
|
215
|
+
/** seedTo lengths issued before a session's renderer existed (KD-5 enable). */
|
|
216
|
+
private readonly pendingSeeds;
|
|
217
|
+
/**
|
|
218
|
+
* Mode latch (KD-N4-5): sessions that have produced ANY `turn/end` event.
|
|
219
|
+
* Once a session emits `turn/end` it is a standard turn-driven session —
|
|
220
|
+
* the new agentic reply-complete gate sleeps for it (verbatim behavior,
|
|
221
|
+
* including spec §4 skip-aborted: a cut-short first turn's unreviewed
|
|
222
|
+
* increment must not be supplementarily reviewed). Agentic/harness
|
|
223
|
+
* sessions never emit `turn/end`, so they never latch and the new gate
|
|
224
|
+
* stays active. Per-session; cleared on dispose.
|
|
225
|
+
*/
|
|
226
|
+
private readonly turnEndSessions;
|
|
227
|
+
/** Bounded delta window (KD-3); forwarded to every per-session renderer. */
|
|
228
|
+
private maxDeltaMessages;
|
|
229
|
+
constructor(options: SessionObserverOptions);
|
|
230
|
+
/**
|
|
231
|
+
* Update the bounded delta window (live config — settings onChange, plan
|
|
232
|
+
* dsh-advisor-settings-n2 T1): the observer default AND every live
|
|
233
|
+
* per-session renderer, so existing sessions pick up the new bound without
|
|
234
|
+
* losing their fold state.
|
|
235
|
+
*/
|
|
236
|
+
setMaxDeltaMessages(value: number): void;
|
|
237
|
+
/**
|
|
238
|
+
* Feed one session event (mirroring the cordis `session/event` listener:
|
|
239
|
+
* `(session, event)` — `events` is the session's live log, `event` the
|
|
240
|
+
* appended event). Renders (and emits via `onDelta`) on either trigger:
|
|
241
|
+
* a stepped `turn/end` with a reviewable reason (standard sessions), or a
|
|
242
|
+
* human-input arrival with an unreviewed assistant increment (agentic
|
|
243
|
+
* sessions — the reply-complete gate, KD-N4-5). The mode latch keeps the
|
|
244
|
+
* second gate dormant for any session that emits `turn/end`.
|
|
245
|
+
*/
|
|
246
|
+
handleEvent(sessionId: string, events: readonly SessionEvent[], event: SessionEvent): void;
|
|
247
|
+
/** Lazy per-session renderer creation, shared by both trigger modes. */
|
|
248
|
+
private rendererFor;
|
|
249
|
+
/** Drop a session's renderer (wiring: `session/disposed` / `agent/disposed`). */
|
|
250
|
+
disposeSession(sessionId: string): void;
|
|
251
|
+
/**
|
|
252
|
+
* KD-5 seed-on-enable: skip existing history for a session's renderer.
|
|
253
|
+
* Issued before the renderer exists (e.g. `/advisor on` before the session
|
|
254
|
+
* produced a stepped turn), the seed is remembered and applied on creation.
|
|
255
|
+
*/
|
|
256
|
+
seedTo(sessionId: string, length: number): void;
|
|
257
|
+
}
|