dsh-agent-persona 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.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The remote surface, in one place. The host registers exactly these names, the generator turns them
3
+ * into the ./remote and ./typert artifacts, and the browser half mounts what the generator produced —
4
+ * so the three never drift. Regenerate with `npm run generate:remote`.
5
+ */
6
+ export declare const RPC_PACKAGE = "dsh-agent-persona";
7
+ /**
8
+ * The cordis service name the host registers (super(ctx, 'agentPersona')) and the namespace the
9
+ * browser half reads (ctx.get('remote.agentPersona')). It is NOT the package name: filling these two
10
+ * fields with the package name mounts the namespace under a key nothing ever looks up.
11
+ */
12
+ export declare const RPC_SERVICE = "agentPersona";
13
+ export declare const RPC_CHANNEL = "/agent-persona";
14
+ export declare const RPC_ENDPOINTS: readonly ['listPersonas', 'savePersona', 'deletePersona', 'movePersona', 'duplicatePersona', 'reorderPersona', 'listTargets', 'sessionHistory', 'sessionPrompt', 'tunePersona', 'listModels', 'savePrefs'];
15
+ /** The three reads that take no argument. Everything else receives one JSON object. */
16
+ export declare const RPC_NO_INPUT: readonly ['listPersonas', 'listTargets', 'listModels'];
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The remote surface, in one place. The host registers exactly these names, the generator turns them
3
+ * into the ./remote and ./typert artifacts, and the browser half mounts what the generator produced —
4
+ * so the three never drift. Regenerate with `npm run generate:remote`.
5
+ */
6
+ export const RPC_PACKAGE = 'dsh-agent-persona';
7
+ /**
8
+ * The cordis service name the host registers (super(ctx, 'agentPersona')) and the namespace the
9
+ * browser half reads (ctx.get('remote.agentPersona')). It is NOT the package name: filling these two
10
+ * fields with the package name mounts the namespace under a key nothing ever looks up.
11
+ */
12
+ export const RPC_SERVICE = 'agentPersona';
13
+ export const RPC_CHANNEL = '/agent-persona';
14
+ export const RPC_ENDPOINTS = [
15
+ 'listPersonas',
16
+ 'savePersona',
17
+ 'deletePersona',
18
+ 'movePersona',
19
+ 'duplicatePersona',
20
+ 'reorderPersona',
21
+ 'listTargets',
22
+ 'sessionHistory',
23
+ 'sessionPrompt',
24
+ 'tunePersona',
25
+ 'listModels',
26
+ 'savePrefs',
27
+ ];
28
+ /** The three reads that take no argument. Everything else receives one JSON object. */
29
+ export const RPC_NO_INPUT = ['listPersonas', 'listTargets', 'listModels'];
package/lib/index.d.ts ADDED
@@ -0,0 +1,261 @@
1
+ /**
2
+ * agent-persona — host half (v2: many personas, each with its own scope).
3
+ *
4
+ * Model
5
+ * -----
6
+ * A persona is a first-class record: `{ id, name, enabled, text, targets[] }`.
7
+ * Every target says WHERE the persona applies:
8
+ *
9
+ * { kind: 'workspace' | 'sessionId', match: 'exact' | 'prefix' | 'regex' | 'contains', value: string }
10
+ *
11
+ * Resolution (see {@link resolvePersona}) is deliberately simple and explainable:
12
+ *
13
+ * 1. personas are tried in ARRAY ORDER (the settings page shows that order and
14
+ * lets it be reordered) — the first match wins;
15
+ * 2. inside one persona the targets are OR-ed (any target matching is enough);
16
+ * 3. a persona with NO targets applies nowhere (a draft), it is never a catch-all: so it
17
+ * belongs at the bottom of the list;
18
+ * 4. a DISABLED persona is skipped entirely;
19
+ * 5. a matching persona whose text is EMPTY wins and contributes nothing —
20
+ * that is how you carve an exception out of a catch-all ("no persona here");
21
+ * 6. when nothing matches, the section renders `''` and `renderPrompt()` drops
22
+ * it, so the agent keeps the stock DSH system prompt.
23
+ *
24
+ * Placement: ONE global system-prompt section at
25
+ * `getSectionOrder('DEPLOYMENT_PERSONA_PREFIX') + 1` (order 1) — right after the
26
+ * harness identity (-1000) and the persona prefix (0), ahead of every
27
+ * tool-guidance section (500+). Workspace `AGENTS.md` is not a section at all
28
+ * (`dsh-agent-instructions` turns it into a durable USER-role message whose own
29
+ * intro says it does not override system instructions), so a persona here
30
+ * outranks `AGENTS.md` structurally.
31
+ *
32
+ * Storage lives in this plugin's own directory — `$DSH_HOME/dsh-agent-persona/`
33
+ * holds `personas.json` (the data) and `state.json` (a load heartbeat), so a
34
+ * deployment does not scatter loose files across `$DSH_HOME`. The data file is
35
+ * re-read on every assembly when its `mtimeNs:size` stamp changes, so edits apply
36
+ * to the next request.
37
+ *
38
+ * The settings page offers dropdowns instead of asking for raw paths and ids;
39
+ * `listTargets` is what feeds them (workspaces + sessions with titles).
40
+ *
41
+ * Row config (all optional):
42
+ * tuneProvider / tuneModel — pin the model used by the AI-tuning button for
43
+ * this deployment; when unset it uses the model DSH itself is set to use
44
+ * (`agentDefaultModel`), which is also what the settings page preselects.
45
+ * storePath — override the persona store location (default
46
+ * `$DSH_HOME/dsh-agent-persona/personas.json`).
47
+ */
48
+ import Schema from '@deepseek-ai/schemastery';
49
+ export declare const name = "agent-persona";
50
+ /**
51
+ * Configuration. Written as a Schemastery schema so the defaults live here, the shape is checked
52
+ * when the row loads, and a wrong value fails loudly instead of being ignored.
53
+ */
54
+ export declare const Config: Schema<Schemastery.ObjectS<{
55
+ /** Where the personas live. Empty means `$DSH_HOME/dsh-agent-persona/personas.json`. */
56
+ storePath: Schema<string, string>;
57
+ }>, Schemastery.ObjectT<{
58
+ /** Where the personas live. Empty means `$DSH_HOME/dsh-agent-persona/personas.json`. */
59
+ storePath: Schema<string, string>;
60
+ }>>;
61
+ export declare const inject: string[];
62
+ /** Persona injection modes: append after the preset persona, or replace it. */
63
+ export declare const PERSONA_MODES: string[];
64
+ /** Target vocabulary, surfaced to the settings page. */
65
+ export declare const TARGET_KINDS: string[];
66
+ export declare const TARGET_MATCHES: string[];
67
+ /**
68
+ * Move one persona to an absolute position (what a drag-and-drop emits).
69
+ * @param store - the store to adjust in place.
70
+ * @param id - persona to move.
71
+ * @param toIndex - target position, clamped into range.
72
+ * @returns whether the order actually changed.
73
+ */
74
+ export declare const reorderPersonas: (store: any, id: any, toIndex: any) => boolean;
75
+ /**
76
+ * A workspace or a session belongs to exactly one persona. When a save claims a
77
+ * value that another persona holds with the same kind and the same exact value,
78
+ * the claim moves to the incoming persona and the other one loses that row.
79
+ *
80
+ * Only exact values can be reasoned about: prefix / regex / contains rows may
81
+ * still overlap, and the first match in list order wins there.
82
+ * @param store - the store to adjust in place.
83
+ * @param persona - the persona whose claims win.
84
+ * @returns the names of the personas that lost a row.
85
+ */
86
+ export declare const takeOverClaims: (store: any, persona: any) => any[];
87
+ export declare const normalizePrefs: (raw: any) => {
88
+ autosave: boolean;
89
+ showTab: boolean;
90
+ showSidebar: boolean;
91
+ };
92
+ export declare const sanitizePersona: (text: any, warn?: () => void) => string;
93
+ /**
94
+ * Does one target claim this subject?
95
+ * @param target - normalized target record.
96
+ * @param subject - `{ cwd?: string, sessionId?: string }`.
97
+ * @returns true when the target matches; an unusable target (empty value, bad
98
+ * regex, or a subject field the session does not have) never matches.
99
+ */
100
+ export declare const matchTarget: (target: any, subject: any) => any;
101
+ /** The target list that claims this subject, empty when none does. */
102
+ /**
103
+ * How specific one matching rule is. A session-level rule beats a directory-level
104
+ * one, and an exact value beats a pattern, so the most specific claim wins without
105
+ * the user having to order the list by hand.
106
+ * @param target - the matching row.
107
+ * @returns 6 (sessionId/exact) down to 1 (workspace pattern); a target-less
108
+ * persona scores 0, i.e. it only takes what nothing else claimed.
109
+ */
110
+ export declare const targetSpecificity: (target: any) => number;
111
+ export declare const matchingTargets: (persona: any, subject: any) => any;
112
+ /**
113
+ * Pick the persona that owns this assembly.
114
+ * @param personas - the store's persona list, already in priority order.
115
+ * @param subject - `{ cwd?, sessionId? }` for the assembling agent.
116
+ * @returns the winning persona and why, or `reason: 'none'`.
117
+ */
118
+ export declare const resolvePersona: (personas: any, subject: any) => any;
119
+ export declare const subjectOf: (agent: any) => {
120
+ cwd: any;
121
+ sessionId: string;
122
+ };
123
+ /** The persona text one assembly should carry, or `''` for the stock prompt. */
124
+ export declare const personaTextFor: (personas: any, agent: any, warn?: () => void) => string;
125
+ /**
126
+ * Everything the picker needs about a session, out of ONE read of DSH's
127
+ * projection cache: the title, the first prompt and the latest prompt.
128
+ *
129
+ * The projection file is the cheapest source by a wide margin — a session log is
130
+ * 2.6x-5.8x larger on a real machine and is a zstd-compressed append-only file
131
+ * that cannot be seeked, so reading its tail would be the most expensive option
132
+ * of all. The service is tried first (the intended fast path) but its
133
+ * `recordFor(id)` needs an identity witness a listed header cannot reconstruct,
134
+ * so the file below is what actually answers; both are cached per process.
135
+ */
136
+ /**
137
+ * Clip one message for transport without breaking its Markdown. A cut that lands
138
+ * inside an open ``` fence would swallow every line after it (and can push the
139
+ * renderer into its plain-text fallback), so the fence is closed first.
140
+ * @param text - the message body.
141
+ * @param maxChars - how much to keep.
142
+ * @returns the clipped body with a truncation note.
143
+ */
144
+ export declare const clipMarkdown: (text: any, maxChars: any) => any;
145
+ /**
146
+ * The candidate lists the settings page offers as dropdowns. Every source is
147
+ * probed defensively: a deployment whose services differ simply gets an empty
148
+ * list, and the page falls back to typing a value.
149
+ * @param ctx - host context.
150
+ * @param warn - warning sink.
151
+ * @returns workspaces (`{path,title,sessionCount}`) and the newest 200 sessions
152
+ * (`{id,title?,cwd,createdAt}`, newest first).
153
+ */
154
+ export declare const collectTargets: (ctx: any, { personas, warn }?: {
155
+ personas?: any[];
156
+ warn?: () => void;
157
+ }) => Promise<{
158
+ workspaces: any[];
159
+ sessions: any[];
160
+ hidden: {
161
+ subagents: number;
162
+ archived: number;
163
+ blank: number;
164
+ };
165
+ }>;
166
+ /**
167
+ * Apply a persona's injection mode to an assembled prompt.
168
+ *
169
+ * The persona text itself always rides in this plugin's own section, so
170
+ * `replace` only has to drop the deployment's persona line — `append` leaves the
171
+ * assembly untouched. A section already carrying something else (an agent preset
172
+ * or a subagent shadowed it) is never overwritten: another author's persona wins.
173
+ * @param assembly - the `system-prompt/assemble` value.
174
+ * @param options.mode - `append` or `replace`.
175
+ * @param options.deploymentPrefix - the deployment's own persona text, when readable.
176
+ * @returns the assembly to hand on (the input object when nothing changes).
177
+ */
178
+ export declare const applyPersonaMode: (assembly: any, { mode, deploymentPrefix }: {
179
+ deploymentPrefix: any;
180
+ mode: any;
181
+ }) => any;
182
+ /**
183
+ * Session ids the user has archived. The registry is asked first; the workspace
184
+ * store file is the fallback, and an unreadable store simply means "none".
185
+ */
186
+ export declare const collectArchivedSessionIds: (ctx: any) => Set<unknown>;
187
+ /**
188
+ * The last system prompt a session actually sent: read out of its own event log, so it
189
+ * is the assembled text rather than a reconstruction of it.
190
+ * @param ctx - the plugin context.
191
+ * @param id - the session id.
192
+ * @returns the prompt text, or null when the log cannot be read.
193
+ */
194
+ export declare const collectLastSystemPrompt: (ctx: any, id: any) => Promise<any>;
195
+ /**
196
+ * What the persona view shows for one session: which persona applies, and the system
197
+ * prompt that session last sent.
198
+ * @param ctx - the plugin context.
199
+ * @param id - the session id.
200
+ * @param personas - the stored personas.
201
+ * @returns the view payload.
202
+ */
203
+ export declare const sessionPromptFrom: (ctx: any, id: any, personas: any) => Promise<{
204
+ cwd?: undefined;
205
+ id: string;
206
+ available: boolean;
207
+ persona: any;
208
+ prompt: any;
209
+ reason: string;
210
+ matchedBy: any[];
211
+ } | {
212
+ id: string;
213
+ available: boolean;
214
+ cwd: any;
215
+ reason: any;
216
+ matchedBy: any;
217
+ persona: {
218
+ id: any;
219
+ name: any;
220
+ mode: any;
221
+ fallback: boolean;
222
+ enabled: boolean;
223
+ text: any;
224
+ targets: any;
225
+ };
226
+ prompt: any;
227
+ }>;
228
+ /**
229
+ * The options a history call carries, in one place: the runtime used to forward a
230
+ * hand-written subset, so a new option (tail/keep) silently never reached the
231
+ * reader and 跳到最新 quietly read the beginning instead.
232
+ * @param input - the remote call's payload.
233
+ * @returns the reader options.
234
+ */
235
+ export declare const historyOptions: (input: any) => {
236
+ offset: any;
237
+ events: any;
238
+ maxChars: any;
239
+ tail: boolean;
240
+ keep: any;
241
+ };
242
+ /**
243
+ * Read one window of a session's conversation.
244
+ *
245
+ * Storage is addressed through per-session handles and the backend is a
246
+ * zstd-compressed append-only log, so a read decompresses from the start: asking
247
+ * for many small pages costs far more than a few large ones. The contract is
248
+ * therefore one generous window plus `nextOffset`, and the page only fetches
249
+ * another window when the user asks for more. Each message is capped so a single
250
+ * huge tool output cannot blow up the payload or the DOM.
251
+ * @param ctx - host context.
252
+ * @param id - session id.
253
+ * @param options.offset - first event index to read (default 0).
254
+ * @param options.events - how many events to read in this window (default 2000, max 4000).
255
+ * @param options.maxChars - per-message text cap (default 1000).
256
+ * @returns a page: `{ messages, offset, nextOffset, done, eventCount, unavailable }`.
257
+ */
258
+ export declare const collectSessionHistory: (ctx: any, id: any, options?: {}) => Promise<any>;
259
+ /** The Connection RPC channel the browser half calls into (see the plugin standard). */
260
+ /** Mount the prompt section, the store-backed service, and the load heartbeat. */
261
+ export declare function apply(ctx: any, config?: {}): void;