@amaster.ai/dsh-langfuse 0.1.21 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -1
- package/lib/hooks.js +52 -0
- package/lib/index.js +81 -22
- package/lib/types/hooks.d.ts +36 -0
- package/lib/types/index.d.ts +1 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -89,11 +89,56 @@ Compaction and session-title calls passing through `llm/stream` already receive
|
|
|
89
89
|
|
|
90
90
|
Hook spans appear when a dsh hook bridge emits the paired session records. The current `hook/result` record does not contain stdout; successful script output can appear in the LLM request when the bridge injects it as context, while the hook span reports decision, exit code, duration, and bounded stderr. `SessionStart` runs before the first turn and does not emit these records, so it has no hook span.
|
|
91
91
|
|
|
92
|
+
### Custom hook producers
|
|
93
|
+
|
|
94
|
+
Custom bridges use the same `hook/invoked` and `hook/result` session records. The writer and consumer accept an unrestricted `dialect` source string (`gemini`, `codex`, `company-validator`, etc.); admission to the session log still depends on installed runtime invariants. No dialect registration or separate exporter is needed. The plugin observes records only and never executes hooks or changes their decisions.
|
|
95
|
+
|
|
96
|
+
Import `appendHookEvent`, `HookInvoked`, and `HookResult` from `@amaster.ai/dsh-langfuse`. This typed writer uses the real `session.append` publication path without redeclaring `SessionEventMap`. Its TypeScript adaptation allows custom payloads despite the optional upstream `dsh-hook-protocol` type augmentation; it does not widen that package's runtime contract, runner, or bridge APIs. The writer validates fields, catches append failures, logs `[dsh-langfuse]`, and returns a boolean; telemetry success must never control a hook's decision.
|
|
97
|
+
|
|
98
|
+
**Runtime compatibility boundary (verified with `dsh-hook-protocol@0.2.0-rc.2`).** When its invariant companion is registered and enabled through `dsh-invariants`, it rejects dialects other than `claude-code`/`codex`, invocations outside an open turn, and results after their turn closes or while another turn is open. A type assertion cannot bypass these pre-commit checks. `appendHookEvent` returns `false` for rejected records; they are not committed or published and produce no Langfuse hook observation.
|
|
99
|
+
|
|
100
|
+
| Runtime composition | Claude Code/Codex pairs in the open turn | Custom dialects | Detached invocations / late results |
|
|
101
|
+
| --- | --- | --- | --- |
|
|
102
|
+
| No active hook-protocol invariant | Supported | Supported | Supported with `invocationId` |
|
|
103
|
+
| Active hook-protocol invariant 0.2.0-rc.2 | Supported | Rejected | Rejected |
|
|
104
|
+
|
|
105
|
+
The writer never disables or reconfigures invariants. Applications using the active companion need an upstream contract change before adopting custom or late records. The producer still owns hook execution and decisions even when telemetry is rejected. The following custom example requires a runtime composition that admits those records.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { randomUUID } from 'node:crypto';
|
|
109
|
+
import { appendHookEvent } from '@amaster.ai/dsh-langfuse';
|
|
110
|
+
|
|
111
|
+
// The bridge determines turn/point, executes the command, and owns its outcome.
|
|
112
|
+
const invocationId = randomUUID();
|
|
113
|
+
const identity = { turn, point: 'BeforeAgent', handlerId: 'request-validator', invocationId };
|
|
114
|
+
appendHookEvent(session, 'hook/invoked', {
|
|
115
|
+
...identity,
|
|
116
|
+
dialect: 'company-validator',
|
|
117
|
+
name: 'Validate request',
|
|
118
|
+
});
|
|
119
|
+
// After execution, including exceptions, timeout or cancellation:
|
|
120
|
+
appendHookEvent(session, 'hook/result', {
|
|
121
|
+
...identity,
|
|
122
|
+
decision: 'pass',
|
|
123
|
+
outcome: 'success',
|
|
124
|
+
exitCode: 0,
|
|
125
|
+
durationMs: 37,
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Every custom execution should have a unique `invocationId` and a result emitted in the producer's `finally` path. Both records must share `turn`, `point`, `handlerId`, and `invocationId`. `handlerId` identifies the configured handler; `invocationId` identifies one execution, including concurrent executions of that handler. Optional `name` labels the hook. Unmatched/skipped hooks must not emit an invocation: these records represent actual execution. The producer bounds `stderrSummary` and omits unavailable optional fields rather than writing `undefined` into the JSON session log.
|
|
130
|
+
|
|
131
|
+
`hook/result.outcome` describes process execution independently of `decision`: `success` is normal completion, `failed` is execution failure, `timed-out` is deadline termination, and `cancelled` is cancellation. The producer maps its process semantics to these values; an exit code expressing a policy denial can still be a successful execution. Optional `exitCode` and `signal` preserve process details. Failed/timed-out executions receive ERROR observations, cancellation receives WARNING, and successful executions retain decision-based warnings. Records without an explicit outcome preserve the legacy exit-code interpretation (0/2 are non-error). The plugin records `durationMs` and never infers a timeout or cancellation from a missing exit code.
|
|
132
|
+
|
|
133
|
+
For records admitted by the session, runs with `invocationId` retain correlation after `turn/end` and a subsequent `turn/start`; a late result ends the original hook span without attaching it to the new turn. Invocations arriving outside their originating turn receive a separate root with `detachedHook: true` and the original turn number; their result ends both hook and root so they export without waiting for another turn. These detached hook roots do not consume `langfuse/context` queued for the next turn. Detached producers must finish reporting before the session is disposed. Session disposal/plugin unload closes unfinished runs with `hook result not observed`; this means missing telemetry, not proof of execution failure. Legacy records without `invocationId` still close at turn boundaries.
|
|
134
|
+
|
|
135
|
+
Custom names and stderr follow `captureContent`; credentials and `redactFields` are masked by the existing reporter. New outcomes, correlation IDs, dialect, point, duration and process metadata remain available with content capture disabled. The event writer does not sanitize the session log itself: application producers remain responsible for what they persist. Applications must explicitly replace any private hook events with this standard producer path, reuse the same `invocationId`, and report the actual outcome; installing/upgrading the plugin does not adapt an application bridge or repair historical Trace overview IO (Issue #59).
|
|
136
|
+
|
|
92
137
|
Observability is a no-throw seam: backend errors are logged with a `[dsh-langfuse]` prefix and never escape into the agent loop.
|
|
93
138
|
|
|
94
139
|
## Compatibility
|
|
95
140
|
|
|
96
|
-
Run `pnpm test:sdk` at the repository root for a secrets-free check using the real Langfuse SDK and the existing local OTLP receiver. It verifies export, native tool type, SDK label redaction, ambient-baggage isolation, external trace ownership,
|
|
141
|
+
Run `pnpm test:sdk` at the repository root for a secrets-free check using the real Langfuse SDK and the existing local OTLP receiver. It verifies export, native tool type, SDK label redaction, ambient-baggage isolation, external trace ownership, agent-less tool unload, and custom hook export across turn closure. It is separate from the mocked unit suite and the gateway/real-Langfuse E2E legs.
|
|
97
142
|
|
|
98
143
|
### Trace overview compatibility
|
|
99
144
|
|
package/lib/hooks.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function baseFields(data) {
|
|
2
|
+
if (!data || typeof data !== 'object')
|
|
3
|
+
return false;
|
|
4
|
+
const value = data;
|
|
5
|
+
return (Number.isSafeInteger(value.turn) &&
|
|
6
|
+
value.turn >= 0 &&
|
|
7
|
+
typeof value.point === 'string' &&
|
|
8
|
+
value.point.length > 0 &&
|
|
9
|
+
typeof value.handlerId === 'string' &&
|
|
10
|
+
value.handlerId.length > 0 &&
|
|
11
|
+
(value.invocationId === undefined ||
|
|
12
|
+
(typeof value.invocationId === 'string' && value.invocationId.length > 0)));
|
|
13
|
+
}
|
|
14
|
+
export function isHookInvoked(data) {
|
|
15
|
+
return (baseFields(data) &&
|
|
16
|
+
typeof data.dialect === 'string' &&
|
|
17
|
+
data.dialect.length > 0 &&
|
|
18
|
+
(data.matcher === undefined || typeof data.matcher === 'string') &&
|
|
19
|
+
(data.name === undefined || typeof data.name === 'string'));
|
|
20
|
+
}
|
|
21
|
+
export function isHookResult(data) {
|
|
22
|
+
return (baseFields(data) &&
|
|
23
|
+
typeof data.decision === 'string' &&
|
|
24
|
+
Number.isFinite(data.durationMs) &&
|
|
25
|
+
data.durationMs >= 0 &&
|
|
26
|
+
(data.outcome === undefined ||
|
|
27
|
+
['success', 'failed', 'timed-out', 'cancelled'].includes(data.outcome)) &&
|
|
28
|
+
(data.exitCode === undefined || Number.isSafeInteger(data.exitCode)) &&
|
|
29
|
+
(data.signal === undefined || typeof data.signal === 'string') &&
|
|
30
|
+
(data.stderrSummary === undefined || typeof data.stderrSummary === 'string'));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Append standard telemetry without changing hook decisions.
|
|
34
|
+
* Runtime invariants still apply; rejected custom/late records return false.
|
|
35
|
+
*/
|
|
36
|
+
export function appendHookEvent(session, type, data) {
|
|
37
|
+
try {
|
|
38
|
+
if (!(type === 'hook/invoked'
|
|
39
|
+
? isHookInvoked(data)
|
|
40
|
+
: type === 'hook/result' && isHookResult(data))) {
|
|
41
|
+
throw new TypeError('invalid hook telemetry fields');
|
|
42
|
+
}
|
|
43
|
+
// Widen TypeScript only; session.append still enforces installed runtime invariants.
|
|
44
|
+
const append = session.append;
|
|
45
|
+
append.call(session, type, data);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error('[dsh-langfuse] hook reporting failed:', error);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import Schema from '@deepseek-ai/schemastery';
|
|
32
32
|
import { LangfuseReporter } from './client.js';
|
|
33
|
+
import { isHookInvoked, isHookResult } from './hooks.js';
|
|
33
34
|
import { displayBlocks } from './media.js';
|
|
35
|
+
export { appendHookEvent } from './hooks.js';
|
|
34
36
|
export const name = 'dsh-langfuse';
|
|
35
37
|
/**
|
|
36
38
|
* Waterfall listeners only need the events to exist; no hard service
|
|
@@ -270,11 +272,19 @@ export function apply(ctx, config) {
|
|
|
270
272
|
if (state)
|
|
271
273
|
state.hadErrors = true;
|
|
272
274
|
}
|
|
273
|
-
function closeHooks(sessionId) {
|
|
274
|
-
|
|
275
|
-
|
|
275
|
+
function closeHooks(sessionId, legacyOnly = false) {
|
|
276
|
+
const pending = openHooks.get(sessionId);
|
|
277
|
+
for (const [key, hook] of pending ?? []) {
|
|
278
|
+
// Per-execution IDs allow results after turn closure without colliding with the next turn.
|
|
279
|
+
if (legacyOnly && hook.persistent)
|
|
280
|
+
continue;
|
|
281
|
+
const update = { level: 'WARNING', statusMessage: 'hook result not observed' };
|
|
282
|
+
reporter.endSpan(hook.span, update);
|
|
283
|
+
reporter.endSpan(hook.root ?? null, update);
|
|
284
|
+
pending?.delete(key);
|
|
276
285
|
}
|
|
277
|
-
|
|
286
|
+
if (pending?.size === 0)
|
|
287
|
+
openHooks.delete(sessionId);
|
|
278
288
|
}
|
|
279
289
|
/**
|
|
280
290
|
* The trace an observation for `sessionId` belongs to: the session's open
|
|
@@ -328,7 +338,13 @@ export function apply(ctx, config) {
|
|
|
328
338
|
delegationDepth: session.header.delegationDepth,
|
|
329
339
|
},
|
|
330
340
|
});
|
|
331
|
-
children.set(childId, {
|
|
341
|
+
children.set(childId, {
|
|
342
|
+
parentSessionId,
|
|
343
|
+
span,
|
|
344
|
+
hasInput: false,
|
|
345
|
+
turnTrace: children.get(parentSessionId)?.turnTrace ??
|
|
346
|
+
sessions.get(rootSessionId(parentSessionId) ?? '')?.trace,
|
|
347
|
+
});
|
|
332
348
|
});
|
|
333
349
|
ctx.on('subagent/start', (info) => {
|
|
334
350
|
const child = children.get(info.id);
|
|
@@ -364,16 +380,30 @@ export function apply(ctx, config) {
|
|
|
364
380
|
const hookEvent = event;
|
|
365
381
|
if (hookEvent.type === 'hook/invoked') {
|
|
366
382
|
const data = hookEvent.data;
|
|
367
|
-
if (!data
|
|
383
|
+
if (!isHookInvoked(data))
|
|
368
384
|
return;
|
|
369
385
|
const pending = openHooks.get(sessionId) ?? new Map();
|
|
370
|
-
|
|
371
|
-
|
|
386
|
+
const key = data.invocationId ? `run:${data.invocationId}` : `handler:${data.handlerId}`;
|
|
387
|
+
if (pending.has(key)) {
|
|
388
|
+
reporter.endSpan(pending.get(key)?.span ?? null, {
|
|
372
389
|
level: 'WARNING',
|
|
373
390
|
statusMessage: 'hook result not observed',
|
|
374
391
|
});
|
|
375
392
|
}
|
|
376
|
-
pending.
|
|
393
|
+
const previous = pending.get(key);
|
|
394
|
+
if (previous?.root)
|
|
395
|
+
reporter.endSpan(previous.root, { level: 'WARNING' });
|
|
396
|
+
const child = children.get(sessionId);
|
|
397
|
+
const inTurn = child ? child.turn === data.turn : sessions.get(sessionId)?.turn === data.turn;
|
|
398
|
+
const root = data.invocationId && !inTurn
|
|
399
|
+
? // A late hook must not consume context queued for the next turn.
|
|
400
|
+
reporter.openTrace({
|
|
401
|
+
name: config.traceName,
|
|
402
|
+
sessionId,
|
|
403
|
+
metadata: { turn: data.turn, detachedHook: true },
|
|
404
|
+
})
|
|
405
|
+
: undefined;
|
|
406
|
+
const span = reporter.startSpan(root === undefined ? parentFor(sessionId) : root, {
|
|
377
407
|
name: `hook:${data.point}`,
|
|
378
408
|
metadata: {
|
|
379
409
|
turn: data.turn,
|
|
@@ -381,43 +411,72 @@ export function apply(ctx, config) {
|
|
|
381
411
|
dialect: data.dialect,
|
|
382
412
|
matcher: data.matcher,
|
|
383
413
|
handlerId: data.handlerId,
|
|
414
|
+
...(data.invocationId ? { invocationId: data.invocationId } : {}),
|
|
415
|
+
...(config.captureContent && data.name ? { name: data.name } : {}),
|
|
384
416
|
},
|
|
385
|
-
})
|
|
417
|
+
});
|
|
418
|
+
pending.set(key, {
|
|
419
|
+
span,
|
|
420
|
+
root,
|
|
421
|
+
turn: data.turn,
|
|
422
|
+
point: data.point,
|
|
423
|
+
handlerId: data.handlerId,
|
|
424
|
+
persistent: !!data.invocationId,
|
|
425
|
+
turnTrace: root === undefined ? (child?.turnTrace ?? sessions.get(sessionId)?.trace) : undefined,
|
|
426
|
+
});
|
|
386
427
|
openHooks.set(sessionId, pending);
|
|
387
428
|
return;
|
|
388
429
|
}
|
|
389
430
|
if (hookEvent.type === 'hook/result') {
|
|
390
431
|
const data = hookEvent.data;
|
|
391
|
-
if (!data
|
|
432
|
+
if (!isHookResult(data))
|
|
392
433
|
return;
|
|
393
434
|
const pending = openHooks.get(sessionId);
|
|
394
|
-
const
|
|
435
|
+
const key = data.invocationId ? `run:${data.invocationId}` : `handler:${data.handlerId}`;
|
|
436
|
+
const hook = pending?.get(key);
|
|
437
|
+
if (!hook ||
|
|
438
|
+
hook.handlerId !== data.handlerId ||
|
|
439
|
+
hook.turn !== data.turn ||
|
|
440
|
+
hook.point !== data.point)
|
|
441
|
+
return;
|
|
442
|
+
const span = hook.span;
|
|
443
|
+
const failed = data.outcome
|
|
444
|
+
? data.outcome === 'failed' || data.outcome === 'timed-out'
|
|
445
|
+
: data.exitCode === undefined || (data.exitCode !== 0 && data.exitCode !== 2);
|
|
446
|
+
const level = failed
|
|
447
|
+
? 'ERROR'
|
|
448
|
+
: data.outcome === 'cancelled' || ['block', 'deny', 'stop'].includes(data.decision)
|
|
449
|
+
? 'WARNING'
|
|
450
|
+
: 'DEFAULT';
|
|
395
451
|
if (span) {
|
|
396
|
-
if (
|
|
452
|
+
if (failed && sessions.get(rootSessionId(sessionId) ?? '')?.trace === hook.turnTrace)
|
|
397
453
|
noteError(sessionId);
|
|
398
454
|
reporter.endSpan(span, {
|
|
399
455
|
output: {
|
|
400
456
|
decision: data.decision,
|
|
457
|
+
...(data.outcome ? { outcome: data.outcome } : {}),
|
|
458
|
+
...(data.signal ? { signal: data.signal } : {}),
|
|
401
459
|
exitCode: data.exitCode,
|
|
402
460
|
stderrSummary: config.captureContent ? data.stderrSummary : undefined,
|
|
403
461
|
},
|
|
404
|
-
level
|
|
405
|
-
? 'ERROR'
|
|
406
|
-
: data.decision === 'block' || data.decision === 'deny' || data.decision === 'stop'
|
|
407
|
-
? 'WARNING'
|
|
408
|
-
: 'DEFAULT',
|
|
462
|
+
level,
|
|
409
463
|
metadata: { durationMs: data.durationMs },
|
|
410
464
|
});
|
|
411
465
|
}
|
|
412
|
-
|
|
466
|
+
reporter.endSpan(hook.root ?? null, { level });
|
|
467
|
+
pending?.delete(key);
|
|
413
468
|
if (pending?.size === 0)
|
|
414
469
|
openHooks.delete(sessionId);
|
|
415
470
|
return;
|
|
416
471
|
}
|
|
417
472
|
if (event.type === 'turn/end')
|
|
418
|
-
closeHooks(sessionId);
|
|
473
|
+
closeHooks(sessionId, true);
|
|
419
474
|
const child = children.get(sessionId);
|
|
420
475
|
if (child) {
|
|
476
|
+
if (event.type === 'turn/start')
|
|
477
|
+
child.turn = event.data.turn;
|
|
478
|
+
if (event.type === 'turn/end')
|
|
479
|
+
child.turn = undefined;
|
|
421
480
|
// The delegated prompt (the child's first user/message) is the
|
|
422
481
|
// subagent span's input; the descriptor enriches the span name.
|
|
423
482
|
if (event.type === 'user/message' && !child.hasInput && config.captureContent) {
|
|
@@ -441,13 +500,13 @@ export function apply(ctx, config) {
|
|
|
441
500
|
// whose `turn/end` was never seen (crash-orphaned): end and replace
|
|
442
501
|
// it — v5 spans only export on end, so the stale root must close to
|
|
443
502
|
// keep what it already recorded.
|
|
444
|
-
closeHooks(sessionId);
|
|
503
|
+
closeHooks(sessionId, true);
|
|
445
504
|
const stale = sessions.get(sessionId);
|
|
446
505
|
if (stale)
|
|
447
506
|
afterMedia(stale.trace, (endedAt) => reporter.endSpan(stale.trace, {}, endedAt));
|
|
448
507
|
const trace = openTrace(sessionId, { turn: event.data.turn });
|
|
449
508
|
if (trace)
|
|
450
|
-
sessions.set(sessionId, { trace, hasInput: false });
|
|
509
|
+
sessions.set(sessionId, { trace, hasInput: false, turn: event.data.turn });
|
|
451
510
|
break;
|
|
452
511
|
}
|
|
453
512
|
case 'user/message': {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Session } from '@deepseek-ai/dsh-session';
|
|
2
|
+
/** Standard log-only hook records; producers own matching, execution and decisions. */
|
|
3
|
+
export interface HookInvoked {
|
|
4
|
+
turn: number;
|
|
5
|
+
point: string;
|
|
6
|
+
dialect: string;
|
|
7
|
+
matcher?: string;
|
|
8
|
+
handlerId: string;
|
|
9
|
+
/** Unique per execution, shared with its result; required for concurrent runs. */
|
|
10
|
+
invocationId?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface HookResult {
|
|
14
|
+
turn: number;
|
|
15
|
+
point: string;
|
|
16
|
+
handlerId: string;
|
|
17
|
+
invocationId?: string;
|
|
18
|
+
decision: string;
|
|
19
|
+
outcome?: 'success' | 'failed' | 'timed-out' | 'cancelled';
|
|
20
|
+
exitCode?: number;
|
|
21
|
+
signal?: string;
|
|
22
|
+
stderrSummary?: string;
|
|
23
|
+
durationMs: number;
|
|
24
|
+
}
|
|
25
|
+
interface HookEventMap {
|
|
26
|
+
'hook/invoked': HookInvoked;
|
|
27
|
+
'hook/result': HookResult;
|
|
28
|
+
}
|
|
29
|
+
export declare function isHookInvoked(data: unknown): data is HookInvoked;
|
|
30
|
+
export declare function isHookResult(data: unknown): data is HookResult;
|
|
31
|
+
/**
|
|
32
|
+
* Append standard telemetry without changing hook decisions.
|
|
33
|
+
* Runtime invariants still apply; rejected custom/late records return false.
|
|
34
|
+
*/
|
|
35
|
+
export declare function appendHookEvent<K extends keyof HookEventMap>(session: Session, type: K, data: HookEventMap[K]): boolean;
|
|
36
|
+
export {};
|
package/lib/types/index.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
32
32
|
import Schema from '@deepseek-ai/schemastery';
|
|
33
33
|
import { type LangfuseTraceContext } from './client.js';
|
|
34
34
|
export type { LangfuseTraceContext } from './client.js';
|
|
35
|
+
export { appendHookEvent, type HookInvoked, type HookResult } from './hooks.js';
|
|
35
36
|
declare module '@deepseek-ai/cordis' {
|
|
36
37
|
interface Events {
|
|
37
38
|
/** Bind standard labels/parent context before a session's next trace starts. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amaster.ai/dsh-langfuse",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "Langfuse observability plugin for DeepSeek Harness: one generation per LLM call with a nested llm-request span, one span per tool call, one trace per session turn, subagent child sessions nested under the parent's tree.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -67,6 +67,8 @@
|
|
|
67
67
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
68
68
|
"@deepseek-ai/dsh-agent": "0.2.0-rc.2",
|
|
69
69
|
"@deepseek-ai/dsh-attachment": "0.2.0-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-hook-protocol": "0.2.0-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-invariants": "0.2.0-rc.2",
|
|
70
72
|
"@deepseek-ai/dsh-llm": "0.2.0-rc.2",
|
|
71
73
|
"@deepseek-ai/dsh-ptc-runtime": "0.2.0-rc.2",
|
|
72
74
|
"@deepseek-ai/dsh-session": "0.2.0-rc.2",
|