@fractalsynapse/dsh-chapters 0.0.0-stage → 0.2.21
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 +14 -0
- package/README.md +76 -2
- package/cordis.patch.yml +363 -0
- package/docs/architecture.md +692 -0
- package/docs/autorun-guide.md +248 -0
- package/docs/backlog.md +2022 -0
- package/docs/contract.md +938 -0
- package/docs/development.md +990 -0
- package/docs/host-compaction-seam.md +225 -0
- package/docs/host-protected-head-ask.md +80 -0
- package/docs/knowledge-repo.md +1332 -0
- package/docs/plans-carriage.md +571 -0
- package/docs/project-state.md +642 -0
- package/docs/provider.md +98 -0
- package/docs/superpowers/plans/2026-10-17-chapters-save.md +198 -0
- package/docs/superpowers/plans/2026-11-02-chapter-removal.md +151 -0
- package/docs/superpowers/specs/2026-11-02-chapter-removal-design.md +115 -0
- package/docs/verify.md +561 -0
- package/lib/archive.d.ts +161 -0
- package/lib/archive.js +229 -0
- package/lib/archive.js.map +1 -0
- package/lib/arrival.d.ts +23 -0
- package/lib/arrival.js +125 -0
- package/lib/arrival.js.map +1 -0
- package/lib/artifact-query.d.ts +62 -0
- package/lib/artifact-query.js +187 -0
- package/lib/artifact-query.js.map +1 -0
- package/lib/attach.d.ts +46 -0
- package/lib/attach.js +120 -0
- package/lib/attach.js.map +1 -0
- package/lib/auto-save.d.ts +145 -0
- package/lib/auto-save.js +334 -0
- package/lib/auto-save.js.map +1 -0
- package/lib/aux-message.d.ts +17 -0
- package/lib/aux-message.js +20 -0
- package/lib/aux-message.js.map +1 -0
- package/lib/client.js +1419 -0
- package/lib/client.js.map +1 -0
- package/lib/codegraph.d.ts +76 -0
- package/lib/codegraph.js +190 -0
- package/lib/codegraph.js.map +1 -0
- package/lib/commands.d.ts +52 -0
- package/lib/commands.js +946 -0
- package/lib/commands.js.map +1 -0
- package/lib/compose.d.ts +30 -0
- package/lib/compose.js +185 -0
- package/lib/compose.js.map +1 -0
- package/lib/continue-core.d.ts +245 -0
- package/lib/continue-core.js +405 -0
- package/lib/continue-core.js.map +1 -0
- package/lib/diag.d.ts +55 -0
- package/lib/diag.js +186 -0
- package/lib/diag.js.map +1 -0
- package/lib/engine-core.d.ts +301 -0
- package/lib/engine-core.js +531 -0
- package/lib/engine-core.js.map +1 -0
- package/lib/engine-note.d.ts +35 -0
- package/lib/engine-note.js +32 -0
- package/lib/engine-note.js.map +1 -0
- package/lib/engine.d.ts +329 -0
- package/lib/engine.js +1179 -0
- package/lib/engine.js.map +1 -0
- package/lib/enrich-queue.d.ts +66 -0
- package/lib/enrich-queue.js +126 -0
- package/lib/enrich-queue.js.map +1 -0
- package/lib/enrich-store.d.ts +31 -0
- package/lib/enrich-store.js +103 -0
- package/lib/enrich-store.js.map +1 -0
- package/lib/enrich-wire.d.ts +163 -0
- package/lib/enrich-wire.js +353 -0
- package/lib/enrich-wire.js.map +1 -0
- package/lib/enrich.d.ts +102 -0
- package/lib/enrich.js +267 -0
- package/lib/enrich.js.map +1 -0
- package/lib/gitops.d.ts +107 -0
- package/lib/gitops.js +539 -0
- package/lib/gitops.js.map +1 -0
- package/lib/index.d.ts +241 -0
- package/lib/index.js +1162 -0
- package/lib/index.js.map +1 -0
- package/lib/indexing.d.ts +98 -0
- package/lib/indexing.js +223 -0
- package/lib/indexing.js.map +1 -0
- package/lib/injections.d.ts +90 -0
- package/lib/injections.js +165 -0
- package/lib/injections.js.map +1 -0
- package/lib/last-resort.d.ts +24 -0
- package/lib/last-resort.js +27 -0
- package/lib/last-resort.js.map +1 -0
- package/lib/log-shape.d.ts +38 -0
- package/lib/log-shape.js +93 -0
- package/lib/log-shape.js.map +1 -0
- package/lib/models-command.d.ts +74 -0
- package/lib/models-command.js +100 -0
- package/lib/models-command.js.map +1 -0
- package/lib/notice.d.ts +74 -0
- package/lib/notice.js +92 -0
- package/lib/notice.js.map +1 -0
- package/lib/plans.d.ts +154 -0
- package/lib/plans.js +315 -0
- package/lib/plans.js.map +1 -0
- package/lib/preset.d.ts +186 -0
- package/lib/preset.js +212 -0
- package/lib/preset.js.map +1 -0
- package/lib/prompt-keys.d.ts +7 -0
- package/lib/prompt-keys.js +8 -0
- package/lib/prompt-keys.js.map +1 -0
- package/lib/prompt-loop.d.ts +73 -0
- package/lib/prompt-loop.js +147 -0
- package/lib/prompt-loop.js.map +1 -0
- package/lib/prompt-push.d.ts +47 -0
- package/lib/prompt-push.js +115 -0
- package/lib/prompt-push.js.map +1 -0
- package/lib/prompt-runner.d.ts +48 -0
- package/lib/prompt-runner.js +90 -0
- package/lib/prompt-runner.js.map +1 -0
- package/lib/prompt-state.d.ts +142 -0
- package/lib/prompt-state.js +101 -0
- package/lib/prompt-state.js.map +1 -0
- package/lib/prompt.d.ts +270 -0
- package/lib/prompt.js +408 -0
- package/lib/prompt.js.map +1 -0
- package/lib/provider.d.ts +44 -0
- package/lib/provider.js +66 -0
- package/lib/provider.js.map +1 -0
- package/lib/redact.d.ts +24 -0
- package/lib/redact.js +85 -0
- package/lib/redact.js.map +1 -0
- package/lib/registry.d.ts +187 -0
- package/lib/registry.js +235 -0
- package/lib/registry.js.map +1 -0
- package/lib/removal.d.ts +159 -0
- package/lib/removal.js +556 -0
- package/lib/removal.js.map +1 -0
- package/lib/render.d.ts +97 -0
- package/lib/render.js +530 -0
- package/lib/render.js.map +1 -0
- package/lib/repair-run.d.ts +76 -0
- package/lib/repair-run.js +506 -0
- package/lib/repair-run.js.map +1 -0
- package/lib/repair.d.ts +208 -0
- package/lib/repair.js +357 -0
- package/lib/repair.js.map +1 -0
- package/lib/repo.d.ts +78 -0
- package/lib/repo.js +184 -0
- package/lib/repo.js.map +1 -0
- package/lib/rules-commands.d.ts +46 -0
- package/lib/rules-commands.js +379 -0
- package/lib/rules-commands.js.map +1 -0
- package/lib/rules.d.ts +267 -0
- package/lib/rules.js +478 -0
- package/lib/rules.js.map +1 -0
- package/lib/save-core.d.ts +72 -0
- package/lib/save-core.js +220 -0
- package/lib/save-core.js.map +1 -0
- package/lib/search.d.ts +112 -0
- package/lib/search.js +282 -0
- package/lib/search.js.map +1 -0
- package/lib/session-rules.d.ts +222 -0
- package/lib/session-rules.js +454 -0
- package/lib/session-rules.js.map +1 -0
- package/lib/signature.d.ts +45 -0
- package/lib/signature.js +199 -0
- package/lib/signature.js.map +1 -0
- package/lib/store.d.ts +156 -0
- package/lib/store.js +368 -0
- package/lib/store.js.map +1 -0
- package/lib/sync.d.ts +220 -0
- package/lib/sync.js +810 -0
- package/lib/sync.js.map +1 -0
- package/lib/tools.d.ts +113 -0
- package/lib/tools.js +1090 -0
- package/lib/tools.js.map +1 -0
- package/lib/treedx/client.d.ts +57 -0
- package/lib/treedx/client.js +82 -0
- package/lib/treedx/client.js.map +1 -0
- package/lib/treedx/provider.d.ts +39 -0
- package/lib/treedx/provider.js +479 -0
- package/lib/treedx/provider.js.map +1 -0
- package/lib/treedx/state.d.ts +27 -0
- package/lib/treedx/state.js +65 -0
- package/lib/treedx/state.js.map +1 -0
- package/lib/types.d.ts +210 -0
- package/lib/types.js +31 -0
- package/lib/types.js.map +1 -0
- package/lib/version.d.ts +9 -0
- package/lib/version.js +10 -0
- package/lib/version.js.map +1 -0
- package/lib/vocabulary.d.ts +41 -0
- package/lib/vocabulary.js +118 -0
- package/lib/vocabulary.js.map +1 -0
- package/lib/warnings.d.ts +10 -0
- package/lib/warnings.js +18 -0
- package/lib/warnings.js.map +1 -0
- package/package.json +151 -3
package/lib/preset.d.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `chapters` agent preset row — our implementation of the host's own
|
|
3
|
+
* `@deepseek-ai/dsh-agent-preset` declaration row, mounted from the bundle patch
|
|
4
|
+
* (`cordis.patch.yml`, row id `preset-chapters`), so the preset is a visible Loader entry
|
|
5
|
+
* exactly like `preset-standard` / `preset-ptc` rather than something a plugin does aside.
|
|
6
|
+
*
|
|
7
|
+
* WHY OUR ROW AND NOT THE HOST PACKAGE DIRECTLY
|
|
8
|
+
* --------------------------------------------
|
|
9
|
+
* A preset can be unusable in two ways and both are total: one unresolvable child row makes
|
|
10
|
+
* the WHOLE preset `broken` (`mountPreset` throws on any failed row → `record.broken` →
|
|
11
|
+
* `retain()` refuses with `agent-preset/invalid`), which is the same user-visible failure as
|
|
12
|
+
* a missing preset — every chapters session stops composing while its durable log sits there
|
|
13
|
+
* intact. Measured on the stable release with a composition copy-anchored to the alpha:
|
|
14
|
+
* `@deepseek-ai/dsh-tool-schedule` is not published at 0.2.0-rc.2 at all, and the boot said
|
|
15
|
+
* `preset "chapters" is DECLARED BUT BROKEN — tool-schedule … never started`.
|
|
16
|
+
*
|
|
17
|
+
* So this row does what the host's does, plus drop-what-will-not-load. It also refuses to
|
|
18
|
+
* brick a host predating the mechanism: a `@deepseek-ai/dsh-agent-preset` row is
|
|
19
|
+
* unresolvable before 0.1.7-alpha.1, and ONE unresolvable row aborts a legacy plugin tree
|
|
20
|
+
* (`plugin tree failed to load: … Cannot find package`), while a row naming our own package
|
|
21
|
+
* resolves wherever our package runs. The support floor is 0.2.0, so that is now
|
|
22
|
+
* belt-and-braces rather than the reason — and it is why the row plugin is ours.
|
|
23
|
+
*
|
|
24
|
+
* WHAT IS COPIED FROM THE HOST'S PLUGIN, LINE FOR LINE
|
|
25
|
+
* ---------------------------------------------------
|
|
26
|
+
* - `static inject = ['agentPresets']` — the registry's shape, never a version test.
|
|
27
|
+
* - `static [EntryGroup.key] = true` — "preserve child expressions until their own plugins
|
|
28
|
+
* activate". Without it the Loader interpolates `!!js` against the HOST context, and
|
|
29
|
+
* `disabled: !!js process.platform === 'win32'` stops belonging to the child row that
|
|
30
|
+
* owns it.
|
|
31
|
+
* - `[Service.init]` `yield`s the registry's disposer, so Cordis owns the registration's
|
|
32
|
+
* lifetime. That also disposes of the trap the first draft fell into: calling
|
|
33
|
+
* `ctx.effect(disposer)` from `apply()` fires when the apply FIBER finishes, which
|
|
34
|
+
* un-registered the preset the instant it had been registered (measured on 0.2.0-rc.2:
|
|
35
|
+
* `register()` reported success while the roster came back without `chapters`).
|
|
36
|
+
*
|
|
37
|
+
* @module dsh-chapters/src/preset
|
|
38
|
+
*/
|
|
39
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
40
|
+
import { EntryGroup } from '@deepseek-ai/cordis-plugin-loader';
|
|
41
|
+
import z from '@deepseek-ai/schemastery';
|
|
42
|
+
/** One mounted plugin row of a preset composition (the Loader's own shape). A group row
|
|
43
|
+
* carries its children in `config`; an ordinary row carries a config map. */
|
|
44
|
+
export interface PresetRow {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
config?: Record<string, unknown> | PresetRow[];
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
group?: boolean;
|
|
50
|
+
isolate?: Record<string, boolean>;
|
|
51
|
+
}
|
|
52
|
+
/** The preset definition, in the shape `AgentPresetRegistry.register()` accepts — the host
|
|
53
|
+
* plugin's own Config: id, name?, description?, order, plugins. */
|
|
54
|
+
export interface PresetDefinition {
|
|
55
|
+
id: string;
|
|
56
|
+
name?: string;
|
|
57
|
+
description?: string;
|
|
58
|
+
order?: number;
|
|
59
|
+
plugins: PresetRow[];
|
|
60
|
+
}
|
|
61
|
+
/** The preset identity. `config.id` is DURABLE STATE: every continuation child's log
|
|
62
|
+
* records it as `agent-preset/selected`, so renaming it orphans those sessions. */
|
|
63
|
+
export declare const CHAPTERS_PRESET_ID = "chapters";
|
|
64
|
+
/**
|
|
65
|
+
* Drop rows whose module cannot be imported, keeping the group structure intact.
|
|
66
|
+
*
|
|
67
|
+
* The probe is asked with the row's FULL specifier, so a package that still exists but no
|
|
68
|
+
* longer exports the subpath a row names (`…/tools`, `…/list-agents`) is caught too — those
|
|
69
|
+
* renames are what eat presets between host versions.
|
|
70
|
+
*
|
|
71
|
+
* @param rows the composition, groups included.
|
|
72
|
+
* @param resolvable asked with each row's module specifier. MUST NOT throw.
|
|
73
|
+
* @returns the composition to register, and every drop NAMED — a silent drop is how a
|
|
74
|
+
* preset loses a tool nobody notices.
|
|
75
|
+
*/
|
|
76
|
+
export declare function pruneUnresolvableRows(rows: PresetRow[], resolvable: (specifier: string) => boolean): {
|
|
77
|
+
kept: PresetRow[];
|
|
78
|
+
dropped: string[];
|
|
79
|
+
};
|
|
80
|
+
/** What registering a preset can end up as. Every arm has its own log line and its own
|
|
81
|
+
* remedy, because the silent case is the one that cost a human hours. */
|
|
82
|
+
export type RegisterOutcome = {
|
|
83
|
+
state: 'registered';
|
|
84
|
+
} | {
|
|
85
|
+
state: 'pruned';
|
|
86
|
+
dropped: string[];
|
|
87
|
+
} | {
|
|
88
|
+
state: 'already-declared';
|
|
89
|
+
} | {
|
|
90
|
+
state: 'unsupported';
|
|
91
|
+
} | {
|
|
92
|
+
state: 'failed';
|
|
93
|
+
reason: string;
|
|
94
|
+
};
|
|
95
|
+
/** The narrow surface of `ctx.get('agentPresets')` this row uses. */
|
|
96
|
+
export interface PresetRegistryLike {
|
|
97
|
+
register?: (definition: PresetDefinition) => Promise<() => Promise<void>>;
|
|
98
|
+
list?: () => Promise<unknown>;
|
|
99
|
+
/** `compositionInventory()` in `@deepseek-ai/dsh-agent-preset-registry`: every declared
|
|
100
|
+
* preset with its mounted rows and their Loader states. */
|
|
101
|
+
compositionInventory?: () => Promise<unknown>;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Register a declared preset, dropping rows this host cannot import.
|
|
105
|
+
*
|
|
106
|
+
* Split out of the plugin class so every arm is unit-testable without a host: the caller
|
|
107
|
+
* hands in the registry surface and a resolver.
|
|
108
|
+
*
|
|
109
|
+
* @returns the outcome AND the disposer the registry hands back — the caller must keep it
|
|
110
|
+
* alive for the row's lifetime, which is what yielding it from `[Service.init]`
|
|
111
|
+
* does; nothing else has the right lifetime (see the module header).
|
|
112
|
+
*/
|
|
113
|
+
export declare function registerPrunedPreset(presets: PresetRegistryLike | undefined, definition: PresetDefinition, resolvable: (specifier: string) => boolean): Promise<{
|
|
114
|
+
outcome: RegisterOutcome;
|
|
115
|
+
disposer?: () => Promise<void>;
|
|
116
|
+
}>;
|
|
117
|
+
/** One mounted row of a live preset composition, as the host's inventory reports it.
|
|
118
|
+
* `fiberState` is the Loader's state for that row; an enabled row that is not `active`
|
|
119
|
+
* cannot serve a session, and the roster's `broken` flag reports that only for the whole
|
|
120
|
+
* mount — which is why the witness asks for the rows as well. */
|
|
121
|
+
export interface InventoryRow {
|
|
122
|
+
entryId?: string;
|
|
123
|
+
moduleName?: string;
|
|
124
|
+
enabled?: boolean;
|
|
125
|
+
fiberState?: string;
|
|
126
|
+
}
|
|
127
|
+
/** One preset's entry in a `compositionInventory()` response. */
|
|
128
|
+
export interface InventoryEntry {
|
|
129
|
+
id?: string;
|
|
130
|
+
broken?: string;
|
|
131
|
+
rows?: InventoryRow[];
|
|
132
|
+
}
|
|
133
|
+
/** Find one preset in an inventory response, tolerating a missing or odd shape. */
|
|
134
|
+
export declare function inventoryEntry(raw: unknown, presetId: string): InventoryEntry | undefined;
|
|
135
|
+
/** Enabled rows of one preset that are not active. A row `pending` a host service can still
|
|
136
|
+
* activate later, so this is reported apart from `broken` and never as a refusal. */
|
|
137
|
+
export declare function unusableRows(entry: InventoryEntry | undefined): string[];
|
|
138
|
+
/**
|
|
139
|
+
* Whether a module specifier can be imported from THIS package's resolution family.
|
|
140
|
+
*
|
|
141
|
+
* Two limits are the host's, not ours to paper over: a RELATIVE specifier resolves without
|
|
142
|
+
* touching the filesystem (so a file-URL row would always be reported resolvable — the
|
|
143
|
+
* composition uses package specifiers only, and the drift test pins that), and a name that
|
|
144
|
+
* resolves is not yet a name that ACTIVATES, which is what the roster line reports as
|
|
145
|
+
* `DECLARED BUT BROKEN`. Never throws: an unresolvable specifier is the answer.
|
|
146
|
+
*/
|
|
147
|
+
export declare function resolvableFromThisPackage(specifier: string): boolean;
|
|
148
|
+
/** The narrow slice of the Cordis context this row needs. */
|
|
149
|
+
interface RowCtx {
|
|
150
|
+
agentPresets: PresetRegistryLike;
|
|
151
|
+
logger?: {
|
|
152
|
+
info?: (m: string) => void;
|
|
153
|
+
warn?: (m: string) => void;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The declaration row. `config` IS the preset definition, exactly as the host's
|
|
158
|
+
* `AgentPreset` plugin consumes it, so a profile patch can override `preset-chapters`
|
|
159
|
+
* wholesale or `disabled: true` it — the two things a runtime registration done in
|
|
160
|
+
* `apply()` cannot offer, and the reason this is a row.
|
|
161
|
+
*/
|
|
162
|
+
export default class ChaptersPresetRow {
|
|
163
|
+
static inject: readonly ["agentPresets"];
|
|
164
|
+
/** Preserve child `!!js` expressions until their own plugins activate. */
|
|
165
|
+
static [EntryGroup.key]: boolean;
|
|
166
|
+
static Config: z<Schemastery.ObjectS<{
|
|
167
|
+
id: z<string, string>;
|
|
168
|
+
name: z<string, string>;
|
|
169
|
+
description: z<string, string>;
|
|
170
|
+
order: z<number, number>;
|
|
171
|
+
plugins: z<any[], any[]>;
|
|
172
|
+
}>, Schemastery.ObjectT<{
|
|
173
|
+
id: z<string, string>;
|
|
174
|
+
name: z<string, string>;
|
|
175
|
+
description: z<string, string>;
|
|
176
|
+
order: z<number, number>;
|
|
177
|
+
plugins: z<any[], any[]>;
|
|
178
|
+
}>>;
|
|
179
|
+
ctx: RowCtx;
|
|
180
|
+
config: PresetDefinition;
|
|
181
|
+
constructor(ctx: RowCtx, config: PresetDefinition);
|
|
182
|
+
/** Registers, then yields the disposer so Cordis owns the registration's lifetime — the
|
|
183
|
+
* host's own row plugin does exactly this (`yield await this.ctx.agentPresets.register(…)`). */
|
|
184
|
+
[Service.init](): AsyncGenerator<() => Promise<void>, void>;
|
|
185
|
+
}
|
|
186
|
+
export {};
|
package/lib/preset.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `chapters` agent preset row — our implementation of the host's own
|
|
3
|
+
* `@deepseek-ai/dsh-agent-preset` declaration row, mounted from the bundle patch
|
|
4
|
+
* (`cordis.patch.yml`, row id `preset-chapters`), so the preset is a visible Loader entry
|
|
5
|
+
* exactly like `preset-standard` / `preset-ptc` rather than something a plugin does aside.
|
|
6
|
+
*
|
|
7
|
+
* WHY OUR ROW AND NOT THE HOST PACKAGE DIRECTLY
|
|
8
|
+
* --------------------------------------------
|
|
9
|
+
* A preset can be unusable in two ways and both are total: one unresolvable child row makes
|
|
10
|
+
* the WHOLE preset `broken` (`mountPreset` throws on any failed row → `record.broken` →
|
|
11
|
+
* `retain()` refuses with `agent-preset/invalid`), which is the same user-visible failure as
|
|
12
|
+
* a missing preset — every chapters session stops composing while its durable log sits there
|
|
13
|
+
* intact. Measured on the stable release with a composition copy-anchored to the alpha:
|
|
14
|
+
* `@deepseek-ai/dsh-tool-schedule` is not published at 0.2.0-rc.2 at all, and the boot said
|
|
15
|
+
* `preset "chapters" is DECLARED BUT BROKEN — tool-schedule … never started`.
|
|
16
|
+
*
|
|
17
|
+
* So this row does what the host's does, plus drop-what-will-not-load. It also refuses to
|
|
18
|
+
* brick a host predating the mechanism: a `@deepseek-ai/dsh-agent-preset` row is
|
|
19
|
+
* unresolvable before 0.1.7-alpha.1, and ONE unresolvable row aborts a legacy plugin tree
|
|
20
|
+
* (`plugin tree failed to load: … Cannot find package`), while a row naming our own package
|
|
21
|
+
* resolves wherever our package runs. The support floor is 0.2.0, so that is now
|
|
22
|
+
* belt-and-braces rather than the reason — and it is why the row plugin is ours.
|
|
23
|
+
*
|
|
24
|
+
* WHAT IS COPIED FROM THE HOST'S PLUGIN, LINE FOR LINE
|
|
25
|
+
* ---------------------------------------------------
|
|
26
|
+
* - `static inject = ['agentPresets']` — the registry's shape, never a version test.
|
|
27
|
+
* - `static [EntryGroup.key] = true` — "preserve child expressions until their own plugins
|
|
28
|
+
* activate". Without it the Loader interpolates `!!js` against the HOST context, and
|
|
29
|
+
* `disabled: !!js process.platform === 'win32'` stops belonging to the child row that
|
|
30
|
+
* owns it.
|
|
31
|
+
* - `[Service.init]` `yield`s the registry's disposer, so Cordis owns the registration's
|
|
32
|
+
* lifetime. That also disposes of the trap the first draft fell into: calling
|
|
33
|
+
* `ctx.effect(disposer)` from `apply()` fires when the apply FIBER finishes, which
|
|
34
|
+
* un-registered the preset the instant it had been registered (measured on 0.2.0-rc.2:
|
|
35
|
+
* `register()` reported success while the roster came back without `chapters`).
|
|
36
|
+
*
|
|
37
|
+
* @module dsh-chapters/src/preset
|
|
38
|
+
*/
|
|
39
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
40
|
+
import { EntryGroup } from '@deepseek-ai/cordis-plugin-loader';
|
|
41
|
+
import z from '@deepseek-ai/schemastery';
|
|
42
|
+
/** The preset identity. `config.id` is DURABLE STATE: every continuation child's log
|
|
43
|
+
* records it as `agent-preset/selected`, so renaming it orphans those sessions. */
|
|
44
|
+
export const CHAPTERS_PRESET_ID = 'chapters';
|
|
45
|
+
/** Whether a row's module must be probed at all. `cordis:` rows are pseudo-modules the
|
|
46
|
+
* Loader always provides; our own package is by definition present — its realm row is the
|
|
47
|
+
* whole point of the preset, and pruning it would quietly delete the feature. */
|
|
48
|
+
function needsProbe(row) {
|
|
49
|
+
return !row.name.startsWith('cordis:') && !row.name.startsWith('@fractalsynapse/dsh-chapters');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Drop rows whose module cannot be imported, keeping the group structure intact.
|
|
53
|
+
*
|
|
54
|
+
* The probe is asked with the row's FULL specifier, so a package that still exists but no
|
|
55
|
+
* longer exports the subpath a row names (`…/tools`, `…/list-agents`) is caught too — those
|
|
56
|
+
* renames are what eat presets between host versions.
|
|
57
|
+
*
|
|
58
|
+
* @param rows the composition, groups included.
|
|
59
|
+
* @param resolvable asked with each row's module specifier. MUST NOT throw.
|
|
60
|
+
* @returns the composition to register, and every drop NAMED — a silent drop is how a
|
|
61
|
+
* preset loses a tool nobody notices.
|
|
62
|
+
*/
|
|
63
|
+
export function pruneUnresolvableRows(rows, resolvable) {
|
|
64
|
+
const dropped = [];
|
|
65
|
+
const keep = (row) => {
|
|
66
|
+
if (!needsProbe(row))
|
|
67
|
+
return true;
|
|
68
|
+
if (resolvable(row.name))
|
|
69
|
+
return true;
|
|
70
|
+
dropped.push(`${row.id} (${row.name})`);
|
|
71
|
+
return false;
|
|
72
|
+
};
|
|
73
|
+
const visit = (list) => list.flatMap((row) => {
|
|
74
|
+
if (row.group !== true || !Array.isArray(row.config))
|
|
75
|
+
return keep(row) ? [row] : [];
|
|
76
|
+
const children = row.config.filter(keep);
|
|
77
|
+
if (children.length === 0) {
|
|
78
|
+
dropped.push(`${row.id} (group emptied)`);
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
return [{ ...row, config: children }];
|
|
82
|
+
});
|
|
83
|
+
return { kept: visit(rows), dropped };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Register a declared preset, dropping rows this host cannot import.
|
|
87
|
+
*
|
|
88
|
+
* Split out of the plugin class so every arm is unit-testable without a host: the caller
|
|
89
|
+
* hands in the registry surface and a resolver.
|
|
90
|
+
*
|
|
91
|
+
* @returns the outcome AND the disposer the registry hands back — the caller must keep it
|
|
92
|
+
* alive for the row's lifetime, which is what yielding it from `[Service.init]`
|
|
93
|
+
* does; nothing else has the right lifetime (see the module header).
|
|
94
|
+
*/
|
|
95
|
+
export async function registerPrunedPreset(presets, definition, resolvable) {
|
|
96
|
+
if (typeof presets?.register !== 'function')
|
|
97
|
+
return { outcome: { state: 'unsupported' } };
|
|
98
|
+
const { kept, dropped } = pruneUnresolvableRows(definition.plugins, resolvable);
|
|
99
|
+
const pruned = { ...definition, plugins: kept };
|
|
100
|
+
let disposer;
|
|
101
|
+
try {
|
|
102
|
+
disposer = await presets.register(pruned);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
const reason = String(error?.message ?? error);
|
|
106
|
+
// A duplicate is not our failure: someone declared `chapters` themselves — a profile
|
|
107
|
+
// patch override is a legitimate choice. Their row wins; ours must not throw, because a
|
|
108
|
+
// row that fails to activate takes the preset with it, and the roster line with it.
|
|
109
|
+
if (/duplicate agent preset/i.test(reason))
|
|
110
|
+
return { outcome: { state: 'already-declared' } };
|
|
111
|
+
return { outcome: { state: 'failed', reason } };
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
outcome: dropped.length > 0 ? { state: 'pruned', dropped } : { state: 'registered' },
|
|
115
|
+
disposer,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Find one preset in an inventory response, tolerating a missing or odd shape. */
|
|
119
|
+
export function inventoryEntry(raw, presetId) {
|
|
120
|
+
return Array.isArray(raw) ? raw.find((entry) => entry?.id === presetId) : undefined;
|
|
121
|
+
}
|
|
122
|
+
/** Enabled rows of one preset that are not active. A row `pending` a host service can still
|
|
123
|
+
* activate later, so this is reported apart from `broken` and never as a refusal. */
|
|
124
|
+
export function unusableRows(entry) {
|
|
125
|
+
if (!Array.isArray(entry?.rows))
|
|
126
|
+
return [];
|
|
127
|
+
return entry.rows
|
|
128
|
+
.filter((row) => row.enabled !== false && typeof row.fiberState === 'string' && row.fiberState !== 'active')
|
|
129
|
+
.map((row) => `${row.entryId ?? '?'} (${row.moduleName ?? '?'}) ${row.fiberState}`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Whether a module specifier can be imported from THIS package's resolution family.
|
|
133
|
+
*
|
|
134
|
+
* Two limits are the host's, not ours to paper over: a RELATIVE specifier resolves without
|
|
135
|
+
* touching the filesystem (so a file-URL row would always be reported resolvable — the
|
|
136
|
+
* composition uses package specifiers only, and the drift test pins that), and a name that
|
|
137
|
+
* resolves is not yet a name that ACTIVATES, which is what the roster line reports as
|
|
138
|
+
* `DECLARED BUT BROKEN`. Never throws: an unresolvable specifier is the answer.
|
|
139
|
+
*/
|
|
140
|
+
export function resolvableFromThisPackage(specifier) {
|
|
141
|
+
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('file:'))
|
|
142
|
+
return true;
|
|
143
|
+
try {
|
|
144
|
+
import.meta.resolve(specifier);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The declaration row. `config` IS the preset definition, exactly as the host's
|
|
153
|
+
* `AgentPreset` plugin consumes it, so a profile patch can override `preset-chapters`
|
|
154
|
+
* wholesale or `disabled: true` it — the two things a runtime registration done in
|
|
155
|
+
* `apply()` cannot offer, and the reason this is a row.
|
|
156
|
+
*/
|
|
157
|
+
export default class ChaptersPresetRow {
|
|
158
|
+
static inject = ['agentPresets'];
|
|
159
|
+
/** Preserve child `!!js` expressions until their own plugins activate. */
|
|
160
|
+
static [EntryGroup.key] = true;
|
|
161
|
+
static Config = z.object({
|
|
162
|
+
id: z.string().required(),
|
|
163
|
+
name: z.string(),
|
|
164
|
+
description: z.string(),
|
|
165
|
+
order: z.number(),
|
|
166
|
+
plugins: z.array(z.any()).required(),
|
|
167
|
+
});
|
|
168
|
+
ctx;
|
|
169
|
+
config;
|
|
170
|
+
constructor(ctx, config) {
|
|
171
|
+
this.ctx = ctx;
|
|
172
|
+
this.config = config;
|
|
173
|
+
}
|
|
174
|
+
/** Registers, then yields the disposer so Cordis owns the registration's lifetime — the
|
|
175
|
+
* host's own row plugin does exactly this (`yield await this.ctx.agentPresets.register(…)`). */
|
|
176
|
+
async *[Service.init]() {
|
|
177
|
+
const { outcome, disposer } = await registerPrunedPreset(this.ctx.agentPresets, this.config, resolvableFromThisPackage);
|
|
178
|
+
const log = (message, warn) => {
|
|
179
|
+
// `ctx.logger` alone is invisible to a headless `dsh web` (r19), so the loud arms go
|
|
180
|
+
// to stderr as well — a health line nobody reads is how the silent case survived.
|
|
181
|
+
if (warn) {
|
|
182
|
+
this.ctx.logger?.warn?.(message);
|
|
183
|
+
console.error(message);
|
|
184
|
+
}
|
|
185
|
+
else
|
|
186
|
+
this.ctx.logger?.info?.(message);
|
|
187
|
+
};
|
|
188
|
+
if (outcome.state === 'unsupported') {
|
|
189
|
+
// Pre-0.1.7 hosts have no register(). They are below the support floor, but saying so
|
|
190
|
+
// beats aborting their plugin tree over a row they cannot read.
|
|
191
|
+
log(`dsh-chapters: this host cannot register preset declaration rows (before 0.2.0) — the "${CHAPTERS_PRESET_ID}" preset cannot exist here; the chapters tools still work`, true);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (outcome.state === 'already-declared') {
|
|
195
|
+
log(`dsh-chapters: preset "${this.config.id}" is also declared by another row — that one is kept, this bundle's copy stands down`, false);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (outcome.state === 'failed') {
|
|
199
|
+
log(`dsh-chapters: UNHEALTHY — registering the "${this.config.id}" preset was refused: ${outcome.reason} — every session choosing it will fail to compose`, true);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (outcome.state === 'pruned') {
|
|
203
|
+
log(`dsh-chapters: preset "${this.config.id}" registered — dropped ${outcome.dropped.length} row(s) this host cannot import: ${outcome.dropped.join(', ')} (one unresolvable row would make the whole preset broken, so they are gone rather than mounted)`, false);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
log(`dsh-chapters: preset "${this.config.id}" registered as a declaration row`, false);
|
|
207
|
+
}
|
|
208
|
+
if (disposer !== undefined)
|
|
209
|
+
yield disposer;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=preset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preset.js","sourceRoot":"","sources":["../src/preset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAA;AAC9D,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAuBxC;oFACoF;AACpF,MAAM,CAAC,MAAM,kBAAkB,GAAG,UAAU,CAAA;AAE5C;;kFAEkF;AAClF,SAAS,UAAU,CAAC,GAAc;IAChC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,8BAA8B,CAAC,CAAA;AAChG,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,qBAAqB,CACnC,IAAiB,EACjB,UAA0C;IAE1C,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,MAAM,IAAI,GAAG,CAAC,GAAc,EAAW,EAAE;QACvC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAA;QACjC,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAA;QACrC,OAAO,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,IAAI,GAAG,CAAC,CAAA;QACvC,OAAO,KAAK,CAAA;IACd,CAAC,CAAA;IACD,MAAM,KAAK,GAAG,CAAC,IAAiB,EAAe,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAe,EAAE;QAClF,IAAI,GAAG,CAAC,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QACnF,MAAM,QAAQ,GAAI,GAAG,CAAC,MAAsB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACzD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,kBAAkB,CAAC,CAAA;YACzC,OAAO,EAAE,CAAA;QACX,CAAC;QACD,OAAO,CAAC,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAA;IACvC,CAAC,CAAC,CAAA;IACF,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAA;AACvC,CAAC;AAoBD;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAAuC,EACvC,UAA4B,EAC5B,UAA0C;IAE1C,IAAI,OAAO,OAAO,EAAE,QAAQ,KAAK,UAAU;QAAE,OAAO,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,CAAA;IACzF,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,qBAAqB,CAAC,UAAU,CAAC,OAAO,EAAE,UAAU,CAAC,CAAA;IAC/E,MAAM,MAAM,GAAG,EAAE,GAAG,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;IAC/C,IAAI,QAA6B,CAAA;IACjC,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;IAC3C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,MAAM,CAAE,KAAe,EAAE,OAAO,IAAI,KAAK,CAAC,CAAA;QACzD,qFAAqF;QACrF,wFAAwF;QACxF,oFAAoF;QACpF,IAAI,yBAAyB,CAAC,IAAI,CAAC,MAAM,CAAC;YAAE,OAAO,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,EAAE,CAAA;QAC7F,OAAO,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAA;IACjD,CAAC;IACD,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE;QACpF,QAAQ;KACT,CAAA;AACH,CAAC;AAoBD,mFAAmF;AACnF,MAAM,UAAU,cAAc,CAAC,GAAY,EAAE,QAAgB;IAC3D,OAAO,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,GAAwB,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AAC3G,CAAC;AAED;sFACsF;AACtF,MAAM,UAAU,YAAY,CAAC,KAAiC;IAC5D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC;QAAE,OAAO,EAAE,CAAA;IAC1C,OAAQ,KAAK,CAAC,IAAuB;SAClC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,KAAK,KAAK,IAAI,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ,IAAI,GAAG,CAAC,UAAU,KAAK,QAAQ,CAAC;SAC3G,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,OAAO,IAAI,GAAG,KAAK,GAAG,CAAC,UAAU,IAAI,GAAG,KAAK,GAAG,CAAC,UAAU,EAAE,CAAC,CAAA;AACvF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,yBAAyB,CAAC,SAAiB;IACzD,IAAI,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAA;IACxG,IAAI,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;QAC9B,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAQD;;;;;GAKG;AACH,MAAM,CAAC,OAAO,OAAO,iBAAiB;IACpC,MAAM,CAAC,MAAM,GAAG,CAAC,cAAc,CAAU,CAAA;IAEzC,0EAA0E;IAC1E,MAAM,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,IAAI,CAAA;IAE9B,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;QACvB,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QACzB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;QAChB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;QACvB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;QACjB,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,QAAQ,EAAE;KACrC,CAAC,CAAA;IAEF,GAAG,CAAQ;IACX,MAAM,CAAkB;IAExB,YAAY,GAAW,EAAE,MAAwB;QAC/C,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QACd,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;IACtB,CAAC;IAED;qGACiG;IACjG,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;QACnB,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAA;QACvH,MAAM,GAAG,GAAG,CAAC,OAAe,EAAE,IAAa,EAAQ,EAAE;YACnD,qFAAqF;YACrF,kFAAkF;YAClF,IAAI,IAAI,EAAE,CAAC;gBACT,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,OAAO,CAAC,CAAA;gBAChC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YACxB,CAAC;;gBAAM,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,OAAO,CAAC,CAAA;QACzC,CAAC,CAAA;QACD,IAAI,OAAO,CAAC,KAAK,KAAK,aAAa,EAAE,CAAC;YACpC,sFAAsF;YACtF,gEAAgE;YAChE,GAAG,CAAC,yFAAyF,kBAAkB,2DAA2D,EAAE,IAAI,CAAC,CAAA;YACjL,OAAM;QACR,CAAC;QACD,IAAI,OAAO,CAAC,KAAK,KAAK,kBAAkB,EAAE,CAAC;YACzC,GAAG,CAAC,yBAAyB,IAAI,CAAC,MAAM,CAAC,EAAE,sFAAsF,EAAE,KAAK,CAAC,CAAA;YACzI,OAAM;QACR,CAAC;QACD,IAAI,OAAO,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC/B,GAAG,CAAC,8CAA8C,IAAI,CAAC,MAAM,CAAC,EAAE,yBAAyB,OAAO,CAAC,MAAM,mDAAmD,EAAE,IAAI,CAAC,CAAA;YACjK,OAAM;QACR,CAAC;QACD,IAAI,OAAO,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC/B,GAAG,CAAC,yBAAyB,IAAI,CAAC,MAAM,CAAC,EAAE,0BAA0B,OAAO,CAAC,OAAO,CAAC,MAAM,oCAAoC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,kGAAkG,EAAE,KAAK,CAAC,CAAA;QACrQ,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,yBAAyB,IAAI,CAAC,MAAM,CAAC,EAAE,mCAAmC,EAAE,KAAK,CAAC,CAAA;QACxF,CAAC;QACD,IAAI,QAAQ,KAAK,SAAS;YAAE,MAAM,QAAQ,CAAA;IAC5C,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identifiers shared by BOTH planes of the push transport (#194). One module, one spelling: the
|
|
3
|
+
* server registers the projection under this key and the client reads `useProjection(key)` — two
|
|
4
|
+
* literals in two planes would be exactly the drift #59 forbids. No node imports (the client
|
|
5
|
+
* compiles this), no DOM (the server imports this).
|
|
6
|
+
*/
|
|
7
|
+
export declare const PROMPT_PROJECTION_KEY = "chaptersPrompt";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identifiers shared by BOTH planes of the push transport (#194). One module, one spelling: the
|
|
3
|
+
* server registers the projection under this key and the client reads `useProjection(key)` — two
|
|
4
|
+
* literals in two planes would be exactly the drift #59 forbids. No node imports (the client
|
|
5
|
+
* compiles this), no DOM (the server imports this).
|
|
6
|
+
*/
|
|
7
|
+
export const PROMPT_PROJECTION_KEY = 'chaptersPrompt';
|
|
8
|
+
//# sourceMappingURL=prompt-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt-keys.js","sourceRoot":"","sources":["../src/prompt-keys.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,gBAAgB,CAAA"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type GoalSignal } from './prompt-state.ts';
|
|
2
|
+
import type { PromptSuggestionState } from './prompt-state.ts';
|
|
3
|
+
export type AutorunFlag = 'armed' | 'disarmed';
|
|
4
|
+
export interface PromptLoopDeps {
|
|
5
|
+
/**
|
|
6
|
+
* Stop the goal driver re-injecting its own round while KEEPING the goal durable and unchanged
|
|
7
|
+
* (`GoalService.disarm` — "without changing durable goal phase or revision"). Optional: if the seam is
|
|
8
|
+
* absent the loop falls back to yielding the turn, so a missing host verb degrades safely instead of
|
|
9
|
+
* starting two turns at one boundary.
|
|
10
|
+
*/
|
|
11
|
+
displaceGoal?: (sessionId: string) => boolean;
|
|
12
|
+
/** live-session authoring: the runner supplies the agent handle it was given at the turn boundary. */
|
|
13
|
+
followup: (sessionId: string, text: string) => void;
|
|
14
|
+
/** counts only FIRED rounds, so a suggestion nobody armed never spends the budget. */
|
|
15
|
+
maxRounds: () => number;
|
|
16
|
+
/** The LIVE goal signal for a session, from the goal service. Optional: absent means "no goal read", and
|
|
17
|
+
* the loop then behaves as if there is no goal — never as if a goal is armed. */
|
|
18
|
+
goalSignal?: (sessionId: string) => GoalSignal | null;
|
|
19
|
+
now?: () => string;
|
|
20
|
+
log?: (message: string) => void;
|
|
21
|
+
warn?: (message: string) => void;
|
|
22
|
+
}
|
|
23
|
+
export interface PromptLoop {
|
|
24
|
+
statusOf(sessionId: string): {
|
|
25
|
+
autorun: AutorunFlag;
|
|
26
|
+
fired: number;
|
|
27
|
+
};
|
|
28
|
+
/** The human's `/chapters-prompt autorun on|off`. Arming does not fire anything by itself. */
|
|
29
|
+
/**
|
|
30
|
+
* The human's `/chapters-prompt autorun on|off`. Arming fires nothing by itself — but it DOES displace the
|
|
31
|
+
* goal's continuation, per the spec: prompt autorun "can either introduce or REPLACE the autorun by the
|
|
32
|
+
* goal if it is running". My first pass yielded to an armed goal instead, which made the two features
|
|
33
|
+
* mutually exclusive — with a goal armed, a director's autorun would have quietly done nothing.
|
|
34
|
+
*/
|
|
35
|
+
setArmed(sessionId: string, on: boolean): {
|
|
36
|
+
autorun: AutorunFlag;
|
|
37
|
+
fired: number;
|
|
38
|
+
displaced: boolean;
|
|
39
|
+
};
|
|
40
|
+
/** A human took the wheel: disarm now, keep the offer visible, and say why. */
|
|
41
|
+
disarmForHuman(sessionId: string): AutorunFlag;
|
|
42
|
+
/** Called after the runner persisted a `ready` offer. Fires at most one turn per offer. */
|
|
43
|
+
/**
|
|
44
|
+
* `goal` is the LIVE goal signal from the service, or null when there is no goal. Passing it in rather than
|
|
45
|
+
* reading a cached copy is the point: activation is process-local and changes under us (a human pause, a
|
|
46
|
+
* goal edit), and a loop that starts turns must decide against the current fact.
|
|
47
|
+
*/
|
|
48
|
+
maybeFire(sessionId: string, state: PromptSuggestionState, goal?: GoalSignal | null): boolean;
|
|
49
|
+
/** `agent/created` and mount parity with the host driver: nothing arrives armed. */
|
|
50
|
+
reset(sessionId: string): void;
|
|
51
|
+
/** The live goal signal, exposed so the door can pass it at the moment of firing (never a cached copy). */
|
|
52
|
+
goalSignal(sessionId: string): GoalSignal | null;
|
|
53
|
+
}
|
|
54
|
+
export declare function makePromptLoop(deps: PromptLoopDeps): PromptLoop;
|
|
55
|
+
/**
|
|
56
|
+
* The message the loop puts into the conversation, as kernel-admitted `user/message` data: our producer kind,
|
|
57
|
+
* a published form, and an id so the row survives the host's own admission checks.
|
|
58
|
+
*
|
|
59
|
+
* `form: 'notice'` is a deliberate pick from the kernel's published `ContextForm` union
|
|
60
|
+
* (`instructions|catalog|snapshot|notice|relay|recall`, `dsh-llm/lib/types/message.d.ts:46`), not an invented
|
|
61
|
+
* word: my first version used `prompt-suggestion` and the shared assertion refused it at the authorship
|
|
62
|
+
* boundary — which is exactly what that guard exists for. `snapshot` was unavailable in practice: it is the
|
|
63
|
+
* TOC notice's own kind+form pair, and reusing it would have made the archive screen read suggestions as
|
|
64
|
+
* notices of the archive. Reuses `notice.ts`'s
|
|
65
|
+
* assertion rather than re-deriving the rules — one predicate for one decision (#59's lesson), and the
|
|
66
|
+
* `host-seed-contract` row already drift-gates that transcription against the kernel itself.
|
|
67
|
+
*
|
|
68
|
+
* Deliberately NOT `kind: 'user'`: the host's goal tool decides "a direct human turn exists" by exactly that
|
|
69
|
+
* (`dsh-tool-goal:49-65`), so a user-kinded suggestion would hand this loop the authority to pause and
|
|
70
|
+
* resume goals. And never `kind: 'goal'`: an invariant companion deep-equals every goal-sourced message
|
|
71
|
+
* against the host's own `<goal_round>` template, so impersonation fails loudly by design.
|
|
72
|
+
*/
|
|
73
|
+
export declare function suggestPromptMessage(text: string, id: string): Record<string, unknown>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autorun: the suggestion loop that starts the next real turn (#113).
|
|
3
|
+
*
|
|
4
|
+
* This is the piece that makes the feature a loop rather than a widget, and it is deliberately built out of
|
|
5
|
+
* the host goal driver's own observed semantics:
|
|
6
|
+
* - the flag is **process-local** and starts disarmed, so a restart cannot resurrect a loop
|
|
7
|
+
* (`dsh-goal-round-driver` disarms every agent on mount; a re-arm is an explicit human act);
|
|
8
|
+
* - the next turn is started with the agent's own `followup`, which is how the goal round chains turns —
|
|
9
|
+
* no timers, no private queue, nothing that can drift from the agent's real idle state;
|
|
10
|
+
* - a human message **disarms** it. The host's driver only defers in that case; here the human said
|
|
11
|
+
* "stop", because the maintainer's rule is that autorun runs "until the user stops it".
|
|
12
|
+
*
|
|
13
|
+
* The authored message carries our producer kind and its own form, so it is captured as conversation by
|
|
14
|
+
* `isSessionStateNarration` yet can never be mistaken for a human turn — the host's goal tool decides
|
|
15
|
+
* "direct human turn" by `source.kind === 'user'`, so authoring as `user` would hand this loop authority to
|
|
16
|
+
* pause and resume goals. It also never uses `source.kind: 'goal'`: an invariant companion deep-equals every
|
|
17
|
+
* goal-sourced message against the host's own template, so impersonating it is a loud failure by design.
|
|
18
|
+
*/
|
|
19
|
+
import { assertKernelAcceptedUserMessage } from "./notice.js";
|
|
20
|
+
import { goalDriverOwnsTheTurn } from "./prompt-state.js";
|
|
21
|
+
export function makePromptLoop(deps) {
|
|
22
|
+
const slots = new Map();
|
|
23
|
+
const slot = (id) => {
|
|
24
|
+
let s = slots.get(id);
|
|
25
|
+
if (s === undefined) {
|
|
26
|
+
s = { armed: 'disarmed', fired: 0, firedFor: null };
|
|
27
|
+
slots.set(id, s);
|
|
28
|
+
}
|
|
29
|
+
return s;
|
|
30
|
+
};
|
|
31
|
+
const note = (m) => { deps.log?.(m); };
|
|
32
|
+
return {
|
|
33
|
+
statusOf: (id) => { const s = slot(id); return { autorun: s.armed, fired: s.fired }; },
|
|
34
|
+
setArmed(id, on) {
|
|
35
|
+
const s = slot(id);
|
|
36
|
+
s.armed = on ? 'armed' : 'disarmed';
|
|
37
|
+
// Displace, do not defer. One loop owns the next turn: whichever the human armed last.
|
|
38
|
+
let displaced = false;
|
|
39
|
+
if (on && deps.displaceGoal !== undefined) {
|
|
40
|
+
try {
|
|
41
|
+
displaced = deps.displaceGoal(id) === true;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
deps.warn?.(`prompt: could not disarm the goal driver for ${id}: ${String(error?.message ?? error).slice(0, 120)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Arming mid-offer is allowed to fire for the offer already on screen: the human said "keep going
|
|
48
|
+
// from here", and making them wait for the next turn boundary is the difference between a loop and a
|
|
49
|
+
// poller. Re-arming does NOT reset the budget — that is what stops it being an infinite bypass.
|
|
50
|
+
note(on
|
|
51
|
+
? `prompt: autorun armed for ${id} — ${displaced
|
|
52
|
+
? "goal continuation DISARMED (objective unchanged); the suggestion loop drives the worker"
|
|
53
|
+
: 'the goal keeps its own continuation; this loop yields the turn to it'}`
|
|
54
|
+
: `prompt: autorun disarmed for ${id} (${String(s.fired)} round(s) spent of ${String(deps.maxRounds())})`);
|
|
55
|
+
return { autorun: s.armed, fired: s.fired, displaced };
|
|
56
|
+
},
|
|
57
|
+
disarmForHuman(id) {
|
|
58
|
+
const s = slot(id);
|
|
59
|
+
if (s.armed === 'disarmed')
|
|
60
|
+
return s.armed;
|
|
61
|
+
s.armed = 'disarmed';
|
|
62
|
+
note(`prompt: autorun disarmed for ${id} — a human prompt took the wheel`);
|
|
63
|
+
return s.armed;
|
|
64
|
+
},
|
|
65
|
+
maybeFire(id, state, goal = null) {
|
|
66
|
+
const s = slot(id);
|
|
67
|
+
if (s.armed !== 'armed' || state.status !== 'ready' || state.text === '')
|
|
68
|
+
return false;
|
|
69
|
+
// #113, the authority boundary the host drew: `GOAL:` offers RE-CUT the standing objective, and
|
|
70
|
+
// dsh-tool-goal gates that on a direct human turn (`source.kind === 'user'` in the open turn). Our
|
|
71
|
+
// autorun message is plugin-authored, so running it would either be refused mid-turn or — if we
|
|
72
|
+
// faked the kind — escalate the loop into authority it was never given. Hold it for the keystroke
|
|
73
|
+
// and SAY so, rather than trying.
|
|
74
|
+
if (state.intent === 'goal-edit') {
|
|
75
|
+
deps.log?.(`prompt: NOT auto-run for ${id} — this step re-cuts the goal, which needs your keystroke (Run)`);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
// One turn-starter per boundary. The host driver queues its own round when the goal is active AND
|
|
79
|
+
// armed; two loops following up at one idle edge is a race with the driver's reservation fence. We
|
|
80
|
+
// keep generating (the director's eye stays open) and run nothing until the driver is not owning it.
|
|
81
|
+
if (goalDriverOwnsTheTurn(goal)) {
|
|
82
|
+
deps.log?.(`prompt: yielding the next turn to the goal driver for ${id} (goal active and armed) — offers continue, autorun does not`);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
// A goal edit invalidates the offer made against the previous revision: do not push last cycle's
|
|
86
|
+
// directive into an objective the human just re-cut.
|
|
87
|
+
if (goal !== null && state.goalRevision !== null && goal.revision !== state.goalRevision) {
|
|
88
|
+
deps.log?.(`prompt: dropping a stale offer for ${id} (generated against revision ${String(state.goalRevision)}, goal is now ${String(goal.revision)})`);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (state.turnSeq === null)
|
|
92
|
+
return false;
|
|
93
|
+
if (s.firedFor === state.turnSeq)
|
|
94
|
+
return false; // one turn per offer, never a retry storm
|
|
95
|
+
// #188: the round budget is a governor for open-ended iteration, not a guillotine for a goal.
|
|
96
|
+
// While a goal exists (active OR paused — paused may resume), the pursuit is bounded by the
|
|
97
|
+
// GOAL's lifecycle: max_goal_rounds where the driver runs, complete/blocked/cleared where this
|
|
98
|
+
// loop displaced it. The suggestion budget must never cut the pursuit off before the objective.
|
|
99
|
+
// No goal, or a goal that reached a terminal phase: the budget applies exactly as before.
|
|
100
|
+
const goalBoundsIt = goal !== null && (goal.phase === 'active' || goal.phase === 'paused');
|
|
101
|
+
const cap = deps.maxRounds();
|
|
102
|
+
if (!goalBoundsIt && s.fired >= cap) {
|
|
103
|
+
s.armed = 'disarmed';
|
|
104
|
+
deps.warn?.(`prompt: autorun stopped for ${id} after ${String(cap)} rounds (promptMaxRounds) — re-arm to continue`);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
s.fired += 1;
|
|
108
|
+
s.firedFor = state.turnSeq;
|
|
109
|
+
deps.followup(id, state.text);
|
|
110
|
+
note(`prompt: autorun fired round ${String(s.fired)} of ${String(cap)} for ${id} (turn ${String(state.turnSeq)})`);
|
|
111
|
+
return true;
|
|
112
|
+
},
|
|
113
|
+
goalSignal: (id) => (deps.goalSignal === undefined ? null : deps.goalSignal(id)),
|
|
114
|
+
reset(id) {
|
|
115
|
+
slots.set(id, { armed: 'disarmed', fired: 0, firedFor: null });
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The message the loop puts into the conversation, as kernel-admitted `user/message` data: our producer kind,
|
|
121
|
+
* a published form, and an id so the row survives the host's own admission checks.
|
|
122
|
+
*
|
|
123
|
+
* `form: 'notice'` is a deliberate pick from the kernel's published `ContextForm` union
|
|
124
|
+
* (`instructions|catalog|snapshot|notice|relay|recall`, `dsh-llm/lib/types/message.d.ts:46`), not an invented
|
|
125
|
+
* word: my first version used `prompt-suggestion` and the shared assertion refused it at the authorship
|
|
126
|
+
* boundary — which is exactly what that guard exists for. `snapshot` was unavailable in practice: it is the
|
|
127
|
+
* TOC notice's own kind+form pair, and reusing it would have made the archive screen read suggestions as
|
|
128
|
+
* notices of the archive. Reuses `notice.ts`'s
|
|
129
|
+
* assertion rather than re-deriving the rules — one predicate for one decision (#59's lesson), and the
|
|
130
|
+
* `host-seed-contract` row already drift-gates that transcription against the kernel itself.
|
|
131
|
+
*
|
|
132
|
+
* Deliberately NOT `kind: 'user'`: the host's goal tool decides "a direct human turn exists" by exactly that
|
|
133
|
+
* (`dsh-tool-goal:49-65`), so a user-kinded suggestion would hand this loop the authority to pause and
|
|
134
|
+
* resume goals. And never `kind: 'goal'`: an invariant companion deep-equals every goal-sourced message
|
|
135
|
+
* against the host's own `<goal_round>` template, so impersonation fails loudly by design.
|
|
136
|
+
*/
|
|
137
|
+
export function suggestPromptMessage(text, id) {
|
|
138
|
+
const data = {
|
|
139
|
+
id: `suggested-${id}`,
|
|
140
|
+
role: 'user',
|
|
141
|
+
source: { kind: 'plugin:dsh-chapters', form: 'notice' },
|
|
142
|
+
content: [{ type: 'text', text }],
|
|
143
|
+
};
|
|
144
|
+
assertKernelAcceptedUserMessage({ type: 'user/message', seq: 0, data });
|
|
145
|
+
return data;
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=prompt-loop.js.map
|