@fishwithcat/dsh-user-approval 0.0.0-stage → 0.2.2-alpha.3

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,290 @@
1
+ /**
2
+ * Service Definition for the approval capability seam, covering requests,
3
+ * cancellation, audit, per-session policy, and remembered per-tool grants.
4
+ * Missing answerers fail closed.
5
+ * @module @deepseek-ai/dsh-user-approval
6
+ */
7
+ import { randomUUID } from 'node:crypto';
8
+ import { Service } from '@deepseek-ai/cordis';
9
+ import z from '@deepseek-ai/schemastery';
10
+ import { z as zod } from 'zod';
11
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
12
+ import { scopeTarget } from '@deepseek-ai/dsh-scope';
13
+ import { SessionSeq } from '@deepseek-ai/dsh-session';
14
+ import { ApprovalRequestId } from "./types.js";
15
+ export { ApprovalRequestId } from "./types.js";
16
+ /** Every {@link ApprovalOutcome}, for runtime normalization of answerer returns. */
17
+ const OUTCOMES = ['allowed-once', 'rejected', 'cancelled', 'unavailable'];
18
+ /** Every {@link ApprovalPolicy}, for option advertisement and runtime validation of untrusted policy strings. */
19
+ export const APPROVAL_POLICIES = ['ask', 'never'];
20
+ /** Model-facing statement for the deterministic `'never'` policy. */
21
+ const NEVER_SENTENCE = 'Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).';
22
+ /** Model-facing statement for an interactive policy that may still fail closed. */
23
+ const ASK_SENTENCE = 'Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.';
24
+ /** Model-facing policy names in switch notices; a bare `never` reads as "approval is never granted". */
25
+ const POLICY_NOTICE_NAMES = {
26
+ ask: 'ask the user for approval',
27
+ never: 'never ask the user for approval',
28
+ };
29
+ const approvalStateDefinition = {
30
+ key: 'approvalState',
31
+ stateVersion: 1,
32
+ stateSchema: zod.object({
33
+ policy: zod.enum(['ask', 'never']).nullable(),
34
+ turnOpen: zod.boolean(),
35
+ }),
36
+ init: () => ({ policy: null, turnOpen: false }),
37
+ apply(state, event) {
38
+ switch (event.type) {
39
+ case 'approval/policy':
40
+ return state.policy === event.data.policy ? state : { ...state, policy: event.data.policy };
41
+ case 'turn/start':
42
+ return state.turnOpen ? state : { ...state, turnOpen: true };
43
+ case 'turn/end':
44
+ return state.turnOpen ? { ...state, turnOpen: false } : state;
45
+ default:
46
+ return state;
47
+ }
48
+ },
49
+ };
50
+ /**
51
+ * Append the sole durable representation of a session policy override. Invalid
52
+ * values throw before the log changes; projected state follows the committed record.
53
+ * @param session - the session the override belongs to.
54
+ * @param policy - the policy in effect until the next switch.
55
+ */
56
+ export function setApprovalPolicy(session, policy) {
57
+ if (!APPROVAL_POLICIES.includes(policy)) {
58
+ throw new TypeError('approval policy must be one of "ask" or "never"');
59
+ }
60
+ session.append('approval/policy', { policy });
61
+ }
62
+ /**
63
+ * Approval service that applies session policy and remembered per-tool grants
64
+ * before answerers and logs every ask/outcome pair to the requesting session.
65
+ * It exposes deterministic policy changes to the model through the
66
+ * runtime-context snapshot and switch notices.
67
+ */
68
+ export class ApprovalService extends Service {
69
+ config;
70
+ static inject = ['sessionProjections'];
71
+ static Config = z.object({
72
+ policy: z.union(['ask', 'never']).default('ask'),
73
+ });
74
+ approvalProjectionHandle;
75
+ constructor(ctx, config) {
76
+ super(ctx, 'approval');
77
+ this.config = config;
78
+ }
79
+ async [Service.init]() {
80
+ this.approvalProjectionHandle = await this.ctx.sessionProjections.register(approvalStateDefinition);
81
+ // Registration can finish after an unload has started. Do not add effects then.
82
+ if (this.ctx.fiber.uid === null)
83
+ return;
84
+ const effective = (agent) => this.effectivePolicy(agent.session);
85
+ // The complete current value travels after retained history, so switching
86
+ // policy does not rewrite the stable system-prompt cache prefix.
87
+ await this.ctx.inject(['systemPrompt'], (scope) => {
88
+ scope.systemPrompt.context({
89
+ name: 'approval:policy',
90
+ order: scope.systemPrompt.getContextOrder('APPROVAL_POLICY'),
91
+ text: (context) => {
92
+ const agent = context.agent;
93
+ // A bare assemble() (tests, diagnostics) has no session to state.
94
+ if (agent === undefined)
95
+ return '';
96
+ const policy = effective(agent);
97
+ return policy === 'never' ? NEVER_SENTENCE : ASK_SENTENCE;
98
+ },
99
+ });
100
+ });
101
+ }
102
+ /**
103
+ * Switch one live agent's policy and queue the transition for its next model
104
+ * step. Session initialization uses {@link setApprovalPolicy} directly
105
+ * because there is no previously visible policy to change.
106
+ * @param agent - the live agent whose policy is changing.
107
+ * @param policy - the new effective policy.
108
+ */
109
+ setPolicy(agent, policy) {
110
+ const previous = this.effectivePolicy(agent.session);
111
+ if (previous === policy)
112
+ return;
113
+ setApprovalPolicy(agent.session, policy);
114
+ agent.inject(createUserMessage({
115
+ content: [{
116
+ type: 'text',
117
+ text: `The user changed the approval policy from "${POLICY_NOTICE_NAMES[previous]}" to "${POLICY_NOTICE_NAMES[policy]}".`,
118
+ }],
119
+ source: { kind: 'user-approval' },
120
+ }));
121
+ }
122
+ /**
123
+ * Ask the composed answerers to decide one readonly same-process request.
124
+ * The service borrows the request, agent, session, and live signal directly.
125
+ * A remembered grant for the tool resolves the ask `allowed-once` before any
126
+ * answerer sees it. The request requires an open turn because the audit pair
127
+ * must be enclosed by the durable log's commit/replay boundary; an idle ask
128
+ * rejects before appending anything. The answerer phase always produces an
129
+ * outcome: an aborted signal yields `'cancelled'`, a missing or throwing answerer yields
130
+ * `'unavailable'` (fail closed), and a rogue non-vocabulary return value is
131
+ * normalized to `'unavailable'`. A failure that prevents either audit append
132
+ * from committing still rejects because returning an unlogged decision would
133
+ * violate the pair. Session contains post-commit observer failures, so an
134
+ * authoritative append cannot reject the request or suppress its matching
135
+ * audit event.
136
+ * @param req - the pending decision (agent, tool identity, reason, signal).
137
+ * @returns the closed outcome; `'allowed-once'` is the only grant.
138
+ * @throws when no turn is open or either audit event fails before the session
139
+ * append commit point.
140
+ */
141
+ async request(req) {
142
+ const session = req.agent.session;
143
+ if (!this.approvalStateOf(session).turnOpen) {
144
+ throw new Error('approval.request() outside an open turn: the approval/asked + approval/decided audit pair '
145
+ + 'must be turn-enclosed (a bare event between turns is crash-tail garbage on reload). '
146
+ + 'Ask from inside the turn that needs the decision.');
147
+ }
148
+ const id = ApprovalRequestId(randomUUID());
149
+ session.append('approval/asked', {
150
+ id,
151
+ toolName: req.toolName,
152
+ ...req.callId !== undefined ? { callId: req.callId } : {},
153
+ ...req.reason !== undefined ? { reason: req.reason } : {},
154
+ });
155
+ const outcome = await this.decide(req, session);
156
+ // A grant recorded while the answerer was deciding (the TUI's Always
157
+ // allow) covers this request too: mark the decision so the transcript and
158
+ // watch viewer can show that the tool will not prompt again.
159
+ const remembered = outcome === 'allowed-once' && this.isGranted(req.agent, req.toolName);
160
+ session.append('approval/decided', { id, outcome, ...remembered ? { remembered: true } : {} });
161
+ return outcome;
162
+ }
163
+ /**
164
+ * Remember one tool's approval for a session and its live descendants. Every
165
+ * later ask for that tool resolves `allowed-once` without consulting an
166
+ * answerer while the effective policy is `ask`; `never` still rejects first.
167
+ * Recording nothing when the grant already exists keeps replay and repeated
168
+ * Always allow choices from growing the log.
169
+ * @param session - the session whose subtree the grant covers.
170
+ * @param toolName - the tool whose future asks the grant covers.
171
+ * @throws when the tool name is empty.
172
+ */
173
+ grant(session, toolName) {
174
+ if (toolName.trim() === '') {
175
+ throw new TypeError('approval grant requires a non-empty tool name');
176
+ }
177
+ if (this.grantedIn(session, toolName))
178
+ return;
179
+ session.append('approval/grant', { toolName });
180
+ }
181
+ /**
182
+ * Whether a remembered grant covers this Agent's ask for the tool. The walk
183
+ * reads the Agent's session, then each live ancestor through `parentSession`;
184
+ * a vanished ancestor ends it, so an orphaned child never inherits a grant it
185
+ * cannot prove.
186
+ * @param agent - the asking Agent.
187
+ * @param toolName - the tool being asked about.
188
+ * @returns true when the Agent or a live ancestor holds the grant.
189
+ */
190
+ isGranted(agent, toolName) {
191
+ let current = agent;
192
+ for (;;) {
193
+ if (this.grantedIn(current.session, toolName))
194
+ return true;
195
+ const parentId = current.session.header.parentSession;
196
+ if (parentId === undefined)
197
+ return false;
198
+ const parent = this.ctx.get('agents')?.get(parentId);
199
+ if (parent === undefined)
200
+ return false;
201
+ current = parent;
202
+ }
203
+ }
204
+ /**
205
+ * Read one session's own grants without walking its lineage.
206
+ * @param session - session whose log supplies the grants.
207
+ * @param toolName - tool whose grant is sought.
208
+ * @returns whether this session recorded a grant for the tool.
209
+ */
210
+ grantedIn(session, toolName) {
211
+ for (let seq = session.seq - 1; seq >= 0; seq -= 1) {
212
+ // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
213
+ const event = session.eventAt(SessionSeq(seq));
214
+ if (event?.type === 'approval/grant' && event.data.toolName === toolName)
215
+ return true;
216
+ }
217
+ return false;
218
+ }
219
+ /**
220
+ * The session's effective policy: its projected override, else the
221
+ * configured default (the schema already defaulted an omitted policy to
222
+ * `'ask'`; the `??` only narrows the optional-input TYPE).
223
+ * @param session - the exact accepted session whose policy applies.
224
+ * @returns the policy every ask for this session resolves under right now.
225
+ */
226
+ effectivePolicy(session) {
227
+ return this.overrideOf(session) ?? this.config.policy ?? 'ask';
228
+ }
229
+ /**
230
+ * Read the session override without applying the configured default.
231
+ * @param session - session whose log supplies the override.
232
+ * @returns the last logged policy, or `undefined` without one.
233
+ */
234
+ overrideOf(session) {
235
+ return this.approvalStateOf(session).policy ?? undefined;
236
+ }
237
+ approvalStateOf(session) {
238
+ return this.approvalProjectionHandle.stateOf(session);
239
+ }
240
+ /**
241
+ * Dispatch the waterfall, contained and raced against the request signal.
242
+ * @param req - the borrowed public request.
243
+ * @param session - the request agent's session used for policy lookup.
244
+ * @returns the normalized closed outcome.
245
+ */
246
+ async decide(req, session) {
247
+ const signal = req.signal;
248
+ if (signal?.aborted)
249
+ return 'cancelled';
250
+ // The 'never' policy is decided HERE, before any dispatch: a listener
251
+ // registered with `prepend: true` after this service mounts would sit
252
+ // ahead of any gate LISTENER, so a listener-shaped gate cannot keep the
253
+ // documented promise that 'never' rejects deterministically regardless
254
+ // of registration order — only the service's own request path can.
255
+ if (this.effectivePolicy(session) === 'never')
256
+ return 'rejected';
257
+ // A remembered grant is part of the session policy: it decides before any
258
+ // answerer so a UI that already answered Always allow never sees the ask.
259
+ if (this.isGranted(req.agent, req.toolName))
260
+ return 'allowed-once';
261
+ // Enter the promise chain BEFORE dispatching: a listener that throws
262
+ // SYNCHRONOUSLY (before its first await) must land in the same rejection
263
+ // path as an async one — `Promise.resolve(call())` would let it escape
264
+ // the containment into the caller.
265
+ const answer = Promise.resolve().then(() => this.ctx.waterfall(scopeTarget(req.agent, req.agent), 'approval/request', req, () => Promise.resolve('unavailable'))).then(
266
+ // Normalize a rogue (non-vocabulary) answerer return to the fail-closed
267
+ // outcome instead of leaking it into callers' closed-union switches.
268
+ outcome => OUTCOMES.includes(outcome) ? outcome : 'unavailable',
269
+ // A throwing answerer must fail the QUESTION closed, not the caller's
270
+ // tool call open — the seam contains its callbacks.
271
+ () => 'unavailable');
272
+ if (signal === undefined)
273
+ return answer;
274
+ return await new Promise((resolve) => {
275
+ const onAbort = () => {
276
+ signal.removeEventListener('abort', onAbort);
277
+ resolve('cancelled');
278
+ };
279
+ signal.addEventListener('abort', onAbort, { once: true });
280
+ void answer.then((outcome) => {
281
+ signal.removeEventListener('abort', onAbort);
282
+ // After an abort won the race this resolve is a settled-promise no-op:
283
+ // the late answer is discarded by construction.
284
+ resolve(outcome);
285
+ });
286
+ });
287
+ }
288
+ }
289
+ export default ApprovalService;
290
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Wire-safe approval identifiers and outcome vocabulary, free of
3
+ * cordis/service imports so browser type chains can
4
+ * consume them without loading this package's Context augmentation.
5
+ * @module @deepseek-ai/dsh-user-approval/types
6
+ */
7
+ import type { Branded } from '@deepseek-ai/dsh-brand';
8
+ import type { Scoped } from '@deepseek-ai/dsh-scope';
9
+ import type { Agent } from '@deepseek-ai/dsh-agent/types';
10
+ import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand';
11
+ /**
12
+ * Pairs one `approval/asked` audit event with its `approval/decided`.
13
+ * Service-issued (one fresh id per {@link ApprovalService.request} call).
14
+ */
15
+ export type ApprovalRequestId = Branded<'ApprovalRequestId'>;
16
+ /**
17
+ * Brand a string as an {@link ApprovalRequestId}.
18
+ * @param id - the raw id string to brand.
19
+ * @returns the same string carrying the brand.
20
+ */
21
+ export declare function ApprovalRequestId(id: string): ApprovalRequestId;
22
+ /**
23
+ * Closed approval outcomes: a one-shot grant, explicit rejection, withdrawn
24
+ * request, or unavailable answerer. Callers fail closed on `unavailable`.
25
+ */
26
+ export type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
27
+ declare module '@deepseek-ai/dsh-session/types' {
28
+ interface SessionEventMap {
29
+ /**
30
+ * The session remembered one tool's approval — log-only state, not a
31
+ * surface event. Later asks for the same tool from this session or a live
32
+ * descendant resolve `allowed-once` without consulting an answerer while
33
+ * the effective policy is `ask`. Absence means no remembered grant.
34
+ */
35
+ 'approval/grant': {
36
+ toolName: string;
37
+ };
38
+ /**
39
+ * An approval question was put to the answerer chain — log-only audit
40
+ * (like `hook/*`; NOT a surface event, carries no `surfaceOp`). `id` pairs
41
+ * it with the `approval/decided` that always follows; `toolName` is the
42
+ * tool the question is about, `callId` the exact tool call when the asker
43
+ * had one, `reason` the asker's human-readable explanation (e.g. a hook's
44
+ * permission-decision reason).
45
+ */
46
+ 'approval/asked': {
47
+ id: ApprovalRequestId;
48
+ toolName: string;
49
+ callId?: ToolCallId;
50
+ reason?: string;
51
+ };
52
+ /**
53
+ * The outcome of a prior `approval/asked` (same `id`) — log-only audit.
54
+ * Exactly one per ask, appended when the outcome is known: a decision, a
55
+ * cancellation, or the fail-closed `'unavailable'`.
56
+ */
57
+ 'approval/decided': {
58
+ id: ApprovalRequestId;
59
+ outcome: ApprovalOutcome;
60
+ /**
61
+ * The `allowed-once` outcome was covered by a remembered grant for the
62
+ * asked-about tool, so the same session would allow it again without a
63
+ * prompt. One-shot grants and non-grants omit this field.
64
+ */
65
+ remembered?: true;
66
+ };
67
+ }
68
+ }
69
+ /** Client-safe payload declared for the approval answerer waterfall. */
70
+ export interface ApprovalRequestEvent {
71
+ /** Agent identity projected to the corresponding Client Context in transit. */
72
+ readonly agent: Agent;
73
+ /** Tool whose operation requires a decision. */
74
+ readonly toolName: string;
75
+ /** Exact tool call being decided, when available. */
76
+ readonly callId?: ToolCallId;
77
+ /** Human-readable reason supplied by the asker. */
78
+ readonly reason?: string;
79
+ /** Localized presentation only; never persisted in approval audit events. */
80
+ readonly displayReason?: {
81
+ readonly en: string;
82
+ readonly [locale: string]: string;
83
+ };
84
+ /** Cancellation lifetime of the pending request. */
85
+ readonly signal?: AbortSignal;
86
+ }
87
+ declare module '@deepseek-ai/cordis' {
88
+ interface Events {
89
+ /**
90
+ * Ask composed answerers for one decision. Return an outcome to claim the
91
+ * request or call `next()` to delegate. Scope-filtered dispatch
92
+ * (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
93
+ * @param req - pending approval request.
94
+ * @mode waterfall
95
+ */
96
+ 'approval/request'(this: Scoped<Agent>, req: ApprovalRequestEvent, next: () => Promise<ApprovalOutcome>): Promise<ApprovalOutcome>;
97
+ }
98
+ }
99
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Wire-safe approval identifiers and outcome vocabulary, free of
3
+ * cordis/service imports so browser type chains can
4
+ * consume them without loading this package's Context augmentation.
5
+ * @module @deepseek-ai/dsh-user-approval/types
6
+ */
7
+ /**
8
+ * Brand a string as an {@link ApprovalRequestId}.
9
+ * @param id - the raw id string to brand.
10
+ * @returns the same string carrying the brand.
11
+ */
12
+ export function ApprovalRequestId(id) {
13
+ return id;
14
+ }
15
+ //# sourceMappingURL=types.js.map
package/locale/de.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Benutzergenehmigungsdienst",
4
+ "description": "Fordern Sie vom Benutzer eine Betriebsberechtigung an und geben Sie das Genehmigungsergebnis zurück."
5
+ }
6
+ }
package/locale/en.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "meta": {
3
+ "title": "user-approval"
4
+ }
5
+ }
package/locale/es.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Servicio de aprobación de usuarios",
4
+ "description": "Solicite autorización de operación al usuario y devuelva el resultado de la aprobación."
5
+ }
6
+ }
package/locale/fr.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Service d'approbation des utilisateurs",
4
+ "description": "Demander l'autorisation d'opération à l'utilisateur et renvoyer le résultat de l'approbation."
5
+ }
6
+ }
package/locale/ja.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "ユーザー承認サービス",
4
+ "description": "操作の許可をユーザーに求め、今回の承認結果を返します。"
5
+ }
6
+ }
package/locale/pt.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Serviço de aprovação de usuário",
4
+ "description": "Solicite autorização de operação ao usuário e retorne o resultado da aprovação."
5
+ }
6
+ }
package/locale/ru.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Служба утверждения пользователей",
4
+ "description": "Запросить авторизацию операции у пользователя и вернуть результат утверждения."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "用户审批服务",
4
+ "description": "向用户请求操作授权,并返回本次审批结果。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@fishwithcat/dsh-user-approval",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "description": "User-approval seam (ctx.approval) for the DeepSeek Harness: one-shot permission decisions dispatched to composed answerers over the approval/request waterfall, fail-closed by default",
4
+ "version": "0.2.2-alpha.3",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/FishwithCat/deepseek-harness-tui",
11
+ "directory": "packages/interaction/user-approval"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./types": {
22
+ "types": "./lib/types/types.d.ts",
23
+ "default": "./lib/types/types.js"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json",
27
+ "./locale/*.json": "./locale/*.json"
28
+ },
29
+ "files": [
30
+ "locale/*.json",
31
+ "lib/index.js",
32
+ "lib/types/**/*.js",
33
+ "lib/types/**/*.d.ts"
34
+ ],
35
+ "license": "MIT",
36
+ "peerDependencies": {
37
+ "@deepseek-ai/dsh-agent": "0.2.2-alpha.3",
38
+ "@deepseek-ai/dsh-brand": "0.2.2-alpha.3",
39
+ "@deepseek-ai/dsh-llm": "0.2.2-alpha.3",
40
+ "@deepseek-ai/dsh-scope": "0.2.2-alpha.3",
41
+ "@deepseek-ai/dsh-session": "0.2.2-alpha.3",
42
+ "@deepseek-ai/dsh-system-prompt": "0.2.2-alpha.3",
43
+ "@deepseek-ai/cordis": "~4.0.6-alpha.2",
44
+ "@deepseek-ai/dsh-session-projection": "0.2.2-alpha.3"
45
+ },
46
+ "dependencies": {
47
+ "@deepseek-ai/schemastery": "~3.18.6-alpha.2",
48
+ "zod": "4.4.3"
49
+ },
50
+ "devDependencies": {
51
+ "@deepseek-ai/dsh-agent": "0.2.2-alpha.3",
52
+ "@deepseek-ai/dsh-brand": "0.2.2-alpha.3",
53
+ "@deepseek-ai/dsh-llm": "0.2.2-alpha.3",
54
+ "@deepseek-ai/dsh-scope": "0.2.2-alpha.3",
55
+ "@deepseek-ai/dsh-session": "0.2.2-alpha.3",
56
+ "@deepseek-ai/dsh-system-prompt": "0.2.2-alpha.3",
57
+ "@deepseek-ai/cordis": "~4.0.6-alpha.2",
58
+ "@deepseek-ai/dsh-session-projection": "0.2.2-alpha.3"
59
+ }
60
+ }