dsh-context-core 0.1.1

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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/README.zh-CN.md +80 -0
  4. package/dist/compat/@deepseek-ai__dsh-session@0.1.7-rc.2.patch +25 -0
  5. package/dist/compat/DSH-LICENSE +21 -0
  6. package/dist/compat/README.md +28 -0
  7. package/dist/compat/README.zh-CN.md +28 -0
  8. package/dist/compat/dsh-session-source.patch +16 -0
  9. package/dist/config.d.ts +4 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +27 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/host.d.ts +30 -0
  14. package/dist/host.d.ts.map +1 -0
  15. package/dist/host.js +185 -0
  16. package/dist/host.js.map +1 -0
  17. package/dist/index.d.ts +8 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +5 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/pipeline.d.ts +104 -0
  22. package/dist/pipeline.d.ts.map +1 -0
  23. package/dist/pipeline.js +2 -0
  24. package/dist/pipeline.js.map +1 -0
  25. package/dist/plugin.d.ts +38 -0
  26. package/dist/plugin.d.ts.map +1 -0
  27. package/dist/plugin.js +180 -0
  28. package/dist/plugin.js.map +1 -0
  29. package/dist/tools/create-profile-patch.mjs +187 -0
  30. package/dist/transaction.d.ts +29 -0
  31. package/dist/transaction.d.ts.map +1 -0
  32. package/dist/transaction.js +122 -0
  33. package/dist/transaction.js.map +1 -0
  34. package/dist/types.d.ts +82 -0
  35. package/dist/types.d.ts.map +1 -0
  36. package/dist/types.js +2 -0
  37. package/dist/types.js.map +1 -0
  38. package/package.json +61 -0
  39. package/src/config.ts +18 -0
  40. package/src/host.ts +189 -0
  41. package/src/index.ts +7 -0
  42. package/src/pipeline.ts +114 -0
  43. package/src/plugin.ts +160 -0
  44. package/src/transaction.ts +121 -0
  45. package/src/types.ts +81 -0
package/src/plugin.ts ADDED
@@ -0,0 +1,160 @@
1
+ /** Cordis registration and lifecycle delegation, without agent-specific decisions. */
2
+ import { Context } from '@deepseek-ai/cordis';
3
+ import type {} from '@deepseek-ai/dsh-agent';
4
+ import { CompactionEngine, ManualCompactionError } from '@deepseek-ai/dsh-compaction';
5
+ import type { CompactionAgentContext, CompactionResult, CompactionTrigger, ManualCompactAgentContext } from '@deepseek-ai/dsh-compaction';
6
+ import type { CommandId } from '@deepseek-ai/dsh-commands/brand';
7
+ import { CONTEXT_WINDOW_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm';
8
+ import { Session, SessionId } from '@deepseek-ai/dsh-session';
9
+ import type { SessionSeq } from '@deepseek-ai/dsh-session';
10
+ import { validateConfig } from './config.js';
11
+ import { createHost } from './host.js';
12
+ import { assertInactive } from './transaction.js';
13
+ import type { ContextConfig } from './types.js';
14
+ import type { ContextPipeline, ContextTrigger, PipelineDefinition } from './pipeline.js';
15
+
16
+ /** Cordis plugin providing one context engine. */
17
+ export interface ContextPlugin {
18
+ name: string;
19
+ inject: string[];
20
+ apply(ctx: Context, config?: ContextConfig): void;
21
+ }
22
+
23
+ declare module '@deepseek-ai/cordis' {
24
+ interface Events {
25
+ /**
26
+ * Summarize an explicit abandoned branch under the caller's maintenance lock.
27
+ * @param agent - idle session owner whose durable log contains the selected events.
28
+ * @param seqs - branch message events in conversation order.
29
+ * @param signal - cancellation forwarded to the summary provider.
30
+ * @param next - delegate if this context plugin has no branch workflow.
31
+ * @mode waterfall
32
+ */
33
+ 'context-zoo/summarize-branch'(agent: CompactionAgentContext, seqs: readonly SessionSeq[], signal: AbortSignal, next: () => Promise<string | undefined>): Promise<string | undefined>;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Run a plugin's explicit branch workflow and flush its durable summary metadata.
39
+ * The navigation integration inserts the returned text in its new branch as a logged message.
40
+ * @param ctx - Cordis context with the selected plugin.
41
+ * @param agent - idle agent containing the abandoned branch events.
42
+ * @param seqs - message sequence numbers in branch order.
43
+ * @param signal - caller cancellation.
44
+ * @returns the branch summary, or undefined when the plugin has no branch workflow.
45
+ */
46
+ export function summarizeBranch(ctx: Context, agent: ManualCompactAgentContext, seqs: readonly SessionSeq[], signal: AbortSignal): Promise<string | undefined> {
47
+ signal.throwIfAborted();
48
+ return agent.runMaintenance(async agentSignal => {
49
+ const operationSignal = AbortSignal.any([agentSignal, signal]);
50
+ let summary: string | undefined;
51
+ let failed = false;
52
+ let failure: unknown;
53
+ try { summary = await ctx.waterfall('context-zoo/summarize-branch', agent, seqs, operationSignal, async () => undefined); }
54
+ catch (error) { failed = true; failure = error; }
55
+ try { await ctx.sessions.flush(agent.session); }
56
+ catch (error) { if (!failed) { failed = true; failure = error; } }
57
+ operationSignal.throwIfAborted();
58
+ if (failed) throw failure;
59
+ return summary;
60
+ });
61
+ }
62
+
63
+ /** Register a source-owned pipeline using the common DSH service and event protocols. */
64
+ export function createContextPlugin(definition: PipelineDefinition): ContextPlugin {
65
+ return {
66
+ name: `context-${definition.id}`,
67
+ inject: ['llm', 'tokenMeter', 'sessions'],
68
+ apply(ctx, config = {}) {
69
+ validateConfig(config);
70
+ const probe = Session.create(SessionId('context-zoo-capability-probe'));
71
+ const marker = probe.append('context-zoo/state', { plugin: definition.id, kind: 'probe', data: {} }, { ignorable: true });
72
+ if (marker.ignorable !== true) throw new Error('DSH Session.append needs the supplied ignorable-event patch; see dsh-context-core/dist/compat/README.md');
73
+ const settings = Object.freeze({ ...config });
74
+ const pipelines = new WeakMap<Session, ContextPipeline>();
75
+ const pipelineFor = (session: Session): ContextPipeline => {
76
+ let pipeline = pipelines.get(session);
77
+ if (!pipeline) { pipeline = definition.create(settings); pipelines.set(session, pipeline); }
78
+ return pipeline;
79
+ };
80
+ const run = async (agent: CompactionAgentContext, trigger: ContextTrigger, signal: AbortSignal, manual = false, sourceCommandId?: CommandId): Promise<CompactionResult | null> => {
81
+ signal.throwIfAborted();
82
+ assertInactive(agent.session);
83
+ return pipelineFor(agent.session).run(createHost(ctx, agent, definition.id, settings, signal, manual, sourceCommandId), trigger);
84
+ };
85
+ class PipelineEngine extends CompactionEngine {
86
+ override compactIfNeeded(agent: CompactionAgentContext, trigger: CompactionTrigger, signal: AbortSignal): Promise<CompactionResult | null> {
87
+ return run(agent, trigger, signal);
88
+ }
89
+ override async compactRegion(start: SessionSeq, end: SessionSeq, agent: CompactionAgentContext, signal = new AbortController().signal): Promise<CompactionResult> {
90
+ const host = createHost(ctx, agent, definition.id, settings, signal);
91
+ const snapshot = await host.snapshot();
92
+ const first = snapshot.entries.findIndex(entry => entry.seq === start);
93
+ const last = snapshot.entries.findIndex(entry => entry.seq === end);
94
+ if (first < 0 || last < first) throw new Error('Invalid context range');
95
+ const entries = snapshot.entries.slice(first, last + 1);
96
+ return host.compact(entries, () => pipelineFor(agent.session).summarizeRange(host, entries));
97
+ }
98
+ override compactNow(agent: ManualCompactAgentContext, signal: AbortSignal, sourceCommandId?: CommandId): Promise<CompactionResult | null> {
99
+ signal.throwIfAborted();
100
+ try {
101
+ return agent.runMaintenance(async agentSignal => {
102
+ const operationSignal = AbortSignal.any([agentSignal, signal]);
103
+ const before = agent.session.seq;
104
+ let result: CompactionResult | null = null;
105
+ let failed = false;
106
+ let failure: unknown;
107
+ try { result = await run(agent, 'manual', operationSignal, true, sourceCommandId); }
108
+ catch (error) { failed = true; failure = error; }
109
+ if (agent.session.seq !== before) {
110
+ try { await ctx.sessions.flush(agent.session); }
111
+ catch (error) { if (!failed) { failed = true; failure = new ManualCompactionError('persistence', 'Manual compaction could not be flushed', { cause: error }); } }
112
+ }
113
+ signal.throwIfAborted();
114
+ if (agentSignal.aborted) throw new ManualCompactionError('cancelled', 'Manual compaction was cancelled', { cause: operationSignal.reason });
115
+ if (failed) throw failure;
116
+ return result;
117
+ });
118
+ } catch (error) {
119
+ throw new ManualCompactionError('busy', 'Manual compaction requires an idle agent', { cause: error });
120
+ }
121
+ }
122
+ }
123
+ ctx.plugin(PipelineEngine);
124
+ ctx.on('context-zoo/summarize-branch', async (agent, seqs, signal, next) => {
125
+ const pipeline = pipelineFor(agent.session);
126
+ if (!pipeline.summarizeBranch) return next();
127
+ assertInactive(agent.session);
128
+ const host = createHost(ctx, agent, definition.id, settings, signal, true);
129
+ const snapshot = await host.snapshot();
130
+ const bySeq = new Map(snapshot.archive.map(entry => [entry.seq, entry]));
131
+ const entries = seqs.map(seq => {
132
+ const entry = bySeq.get(seq);
133
+ if (!entry) throw new Error(`Missing branch message ${seq}`);
134
+ return entry;
135
+ });
136
+ const summary = await pipeline.summarizeBranch(host, entries);
137
+ signal.throwIfAborted();
138
+ host.record('branch-summary', { seqs: [...seqs], summary });
139
+ return summary;
140
+ });
141
+ if (settings.auto === false) return;
142
+ ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
143
+ if (!signal.aborted) {
144
+ try { await run(agent, 'pressure', signal); }
145
+ catch (error) { if (!signal.aborted) ctx.logger.warn(`${definition.id} context preparation failed: ${error instanceof Error ? error.message : String(error)}`); }
146
+ }
147
+ return next();
148
+ });
149
+ ctx.on('agent/request-error', async ({ agent, failure, signal }, next) => {
150
+ const trigger = failure.status === 413 ? 'request-too-large' : failure.code === CONTEXT_WINDOW_EXCEEDED_CODE ? 'context-overflow' : undefined;
151
+ if (!trigger || signal.aborted) return next();
152
+ const generation = agent.session.surface.replaceGeneration;
153
+ try { await run(agent, trigger, signal); }
154
+ catch (error) { if (!signal.aborted) ctx.logger.warn(`${definition.id} context recovery failed: ${error instanceof Error ? error.message : String(error)}`); }
155
+ if (!signal.aborted && agent.session.surface.replaceGeneration > generation) return { kind: 'retry' };
156
+ return next();
157
+ });
158
+ },
159
+ };
160
+ }
@@ -0,0 +1,121 @@
1
+ /** DSH append protocol, exclusion and surface validation; selection belongs to plugins. */
2
+ import { randomUUID } from 'node:crypto';
3
+ import { isDeepStrictEqual } from 'node:util';
4
+ import { CompactionId, ManualCompactionError, compactCheckpointSource, toolPairingBalancedBefore, toolPairingBalancedAfter } from '@deepseek-ai/dsh-compaction';
5
+ import type { CompactionResult } from '@deepseek-ai/dsh-compaction';
6
+ import type { CommandId } from '@deepseek-ai/dsh-commands/brand';
7
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
8
+ import type { ContentBlock } from '@deepseek-ai/dsh-llm';
9
+ import type { Session, SessionSeq } from '@deepseek-ai/dsh-session';
10
+ import type { TokenMeter } from '@deepseek-ai/dsh-token-meter';
11
+ import type { Checkpoint, ContextEntry, SummaryResponse } from './pipeline.js';
12
+
13
+ /** Inspect current durable ownership, excluding interrupted constructor seeds. */
14
+ export function entryState(session: Session): { active: boolean; turn: number | null } {
15
+ let active = false;
16
+ let turn: number | null = null;
17
+ for (const event of session.snapshotEvents()) {
18
+ if (event.type === 'session/end-seed') { active = false; turn = null; }
19
+ if (event.type === 'compaction/start') active = true;
20
+ if (event.type === 'compaction/end') active = false;
21
+ if (event.type === 'turn/start') turn = event.data.turn;
22
+ if (event.type === 'turn/end') turn = null;
23
+ }
24
+ return { active, turn };
25
+ }
26
+
27
+ /** Reject competing mutation before any surface change. */
28
+ export function assertInactive(session: Session): void {
29
+ if (entryState(session).active) throw new ManualCompactionError('busy', 'A context compaction is already active');
30
+ }
31
+
32
+ function validateSpan(session: Session, seqs: readonly SessionSeq[]): void {
33
+ const start = seqs[0];
34
+ const end = seqs.at(-1);
35
+ if (start === undefined || end === undefined) throw new Error('Cannot compact an empty range');
36
+ const position = session.surface.nodes.indexOf(start);
37
+ if (position < 0 || !isDeepStrictEqual(session.surface.nodes.slice(position, position + seqs.length), [...seqs])) throw new ManualCompactionError('changed', 'The selected history is no longer contiguous on the surface');
38
+ if (!toolPairingBalancedBefore(session, start) || !toolPairingBalancedAfter(session, end)) throw new Error('The selected range splits a tool exchange or unfinished step');
39
+ for (const seq of seqs) {
40
+ const event = session.eventAt(seq);
41
+ const message = event && session.deriveEventMessage(event);
42
+ if (message?.role === 'system' || message?.role === 'developer') throw new Error('System and developer messages must remain outside the compacted range');
43
+ }
44
+ }
45
+
46
+ export interface TransactionOptions {
47
+ session: Session;
48
+ meter: TokenMeter;
49
+ entries: readonly ContextEntry[];
50
+ signal: AbortSignal;
51
+ manual: boolean;
52
+ sourceCommandId?: CommandId;
53
+ summarize(): Promise<Checkpoint>;
54
+ calls(): readonly (SummaryResponse | undefined)[];
55
+ target: { provider: string; model: string };
56
+ }
57
+
58
+ /** Commit the chosen range only after its summary and recovery have both succeeded. */
59
+ export async function compactTransaction(options: TransactionOptions): Promise<CompactionResult> {
60
+ const { session, meter, entries, signal, manual, sourceCommandId } = options;
61
+ signal.throwIfAborted();
62
+ assertInactive(session);
63
+ const seqs = entries.map(entry => entry.seq);
64
+ validateSpan(session, seqs);
65
+ const turn = entryState(session).turn;
66
+ if (manual ? turn !== null : turn === null) throw new ManualCompactionError('busy', manual ? 'Manual compaction requires an idle session' : 'Automatic compaction requires an open turn');
67
+ const before = meter.measure(session).nodes;
68
+ const priced = before.filter(node => seqs.includes(node.seq));
69
+ const shadowedRange = { start: seqs[0]!, end: seqs.at(-1)! };
70
+ const shadowedTokenCount = priced.reduce((sum, node) => sum + node.heuristicTokens, 0);
71
+ const compactionId = CompactionId(randomUUID());
72
+ const lifecycle = { compactionId, turn, ...sourceCommandId === undefined ? {} : { sourceCommandId } };
73
+ const startEvent = session.append('compaction/start', lifecycle);
74
+ let closing = false;
75
+ let stage: 'summary' | 'commit' = 'summary';
76
+ try {
77
+ const checkpoint = await options.summarize();
78
+ signal.throwIfAborted();
79
+ validateSpan(session, seqs);
80
+ const after = meter.measure(session).nodes;
81
+ if (!isDeepStrictEqual(manual ? after.filter(node => seqs.includes(node.seq)) : after, manual ? priced : before)) throw new ManualCompactionError('changed', 'Session history changed during summarization');
82
+ const summary: ContentBlock[] = [...checkpoint.beforeSummary ?? [], { type: 'text', text: checkpoint.summary }, ...checkpoint.restored ?? []];
83
+ const message = createUserMessage({ content: summary, source: compactCheckpointSource(compactionId, sourceCommandId) });
84
+ if (meter.estimateMessage(message) >= priced.reduce((sum, node) => sum + node.tokens, 0)) throw new Error('Summary and recovered context are not smaller than the selected history');
85
+ const calls = options.calls();
86
+ const call = calls.at(-1);
87
+ stage = 'commit';
88
+ const callRecord = calls.length === 1 && call ? { rawOutput: call.content, llmStreamCall: true as const } : {};
89
+ const summaryEvent = session.append('compaction/summary', {
90
+ compactionId, ...sourceCommandId === undefined ? {} : { sourceCommandId },
91
+ summary, shadowedRange, shadowedSeqs: seqs, shadowedTokenCount,
92
+ provider: call?.provider ?? options.target.provider, model: call?.model ?? options.target.model,
93
+ ...callRecord,
94
+ ...calls.length !== 1 || call === undefined ? {} : {
95
+ maxTokens: call.maxTokens,
96
+ ...call.usage === undefined ? {} : { usage: call.usage },
97
+ },
98
+ });
99
+ // The summary record and its replacement are adjacent in DSH's durable protocol.
100
+ session.append('user/message', message, {
101
+ surfaceOp: { op: 'replace', startSeq: shadowedRange.start, endSeq: shadowedRange.end },
102
+ sourceEventSeqs: [startEvent.seq, summaryEvent.seq, ...seqs],
103
+ });
104
+ closing = true;
105
+ const endEvent = session.append('compaction/end', lifecycle);
106
+ return {
107
+ compactionId, ...sourceCommandId === undefined ? {} : { sourceCommandId },
108
+ startSeq: startEvent.seq, summarySeq: summaryEvent.seq, endSeq: endEvent.seq,
109
+ summary, shadowedRange, shadowedSeqs: seqs, shadowedTokenCount,
110
+ };
111
+ } catch (error) {
112
+ let failure = error;
113
+ if (!closing) {
114
+ try { session.append('compaction/end', { ...lifecycle, error: error instanceof Error ? error.message : String(error) }); }
115
+ catch (closeError) { failure = closeError; stage = 'commit'; }
116
+ }
117
+ signal.throwIfAborted();
118
+ if (manual && !(failure instanceof ManualCompactionError)) throw new ManualCompactionError(stage, failure instanceof Error ? failure.message : String(failure), { cause: failure });
119
+ throw failure;
120
+ }
121
+ }
package/src/types.ts ADDED
@@ -0,0 +1,81 @@
1
+ /** Inputs shared by the source-specific budget functions. */
2
+ export interface BudgetInput {
3
+ contextWindow: number;
4
+ maxOutputTokens: number;
5
+ reserveTokens?: number;
6
+ thresholdRatio?: number;
7
+ keepRecentTokens?: number;
8
+ }
9
+
10
+ /** Source-specific budget metadata; the pipeline owns its token estimator and retention semantics. */
11
+ export interface ContextBudget {
12
+ triggerTokens: number;
13
+ retainTokens: number;
14
+ strict?: boolean;
15
+ /** Minimum recent messages, subject to retainTokens when tailMode is maximum. */
16
+ minRecentMessages?: number;
17
+ tailMode?: 'minimum' | 'maximum';
18
+ retainLastRound?: boolean;
19
+ }
20
+
21
+ /** Optional model-free removal of old textual tool results. */
22
+ export interface PrunePolicy {
23
+ enabled: boolean;
24
+ keepRecentTools: number;
25
+ protectTokens: number;
26
+ minSavingsTokens: number;
27
+ strictSavings?: boolean;
28
+ keepRecentTurns?: number;
29
+ protectedTools?: readonly string[];
30
+ eligibleTools?: readonly string[];
31
+ groupByAssistant?: boolean;
32
+ triggerRatio?: number;
33
+ triggerBufferTokens?: number;
34
+ }
35
+
36
+ /** One independently implemented adaptation of a pinned agent strategy. */
37
+ export interface ContextStrategy {
38
+ id: string;
39
+ name: string;
40
+ /** Original project URL; revision and license describe the inspected local fork or source reference. */
41
+ source: { url: string; revision: string; license: string };
42
+ /** What this adaptation deliberately leaves to the host or upstream product. */
43
+ limitations: readonly string[];
44
+ budget(input: BudgetInput): ContextBudget;
45
+ summaryInstructions: string;
46
+ maxSummaryTokens: number;
47
+ /** Maximum tool-result characters in the auxiliary summary request; zero leaves them intact. */
48
+ summaryToolChars: number;
49
+ prune: PrunePolicy;
50
+ }
51
+
52
+ /** User overrides for a strategy plugin. All token values are integers. */
53
+ export interface ContextConfig {
54
+ auto?: boolean;
55
+ reserveTokens?: number;
56
+ thresholdRatio?: number;
57
+ keepRecentTokens?: number;
58
+ maxSummaryTokens?: number;
59
+ summaryToolChars?: number;
60
+ summarizationProvider?: string;
61
+ summarizationModel?: string;
62
+ maxOverflowRetries?: number;
63
+ maxConsecutiveFailures?: number;
64
+ prune?: boolean;
65
+ keepRecentTools?: number;
66
+ protectToolTokens?: number;
67
+ minPruneTokens?: number;
68
+ screenshotTriggerImages?: number;
69
+ tailTurns?: number;
70
+ summaryRetryDelayMs?: number;
71
+ idleMinutes?: number;
72
+ toolHighWaterChars?: number;
73
+ toolLowWaterChars?: number;
74
+ maxSummaryAttempts?: number;
75
+ maxRestoredFiles?: number;
76
+ maxFileTokens?: number;
77
+ maxRestoreTokens?: number;
78
+ maxRestoredImages?: number;
79
+ maxSkillTokens?: number;
80
+ restoreContext?: boolean;
81
+ }