@capekai/core 1.0.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.
- package/README.md +12 -0
- package/package.json +105 -0
- package/src/adapters/ai-sdk.ts +84 -0
- package/src/compaction/contracts.ts +82 -0
- package/src/compaction/executor.ts +161 -0
- package/src/compaction/policy.ts +318 -0
- package/src/compaction/recovery.ts +139 -0
- package/src/compaction/task.ts +540 -0
- package/src/configuration/contracts.ts +58 -0
- package/src/configuration/defaults.ts +27 -0
- package/src/configuration/runtime.ts +42 -0
- package/src/configuration/single-model.ts +75 -0
- package/src/context/assembler.ts +112 -0
- package/src/context/index.ts +2 -0
- package/src/context/sources.ts +119 -0
- package/src/context/workspace.ts +63 -0
- package/src/core/agent.ts +401 -0
- package/src/core/build-tools.ts +139 -0
- package/src/core/chat-handler.ts +858 -0
- package/src/core/error-handling.ts +18 -0
- package/src/core/fork.ts +103 -0
- package/src/core/interrupt.ts +192 -0
- package/src/core/message-utils.ts +261 -0
- package/src/core/model-utils.ts +149 -0
- package/src/core/part-utils.ts +88 -0
- package/src/core/provider-utils.ts +67 -0
- package/src/core/revert.ts +46 -0
- package/src/core/step-handlers.ts +157 -0
- package/src/core/stream/finalization.ts +65 -0
- package/src/core/stream/stream-config.ts +82 -0
- package/src/core/stream-handlers.ts +242 -0
- package/src/core/structured-output.ts +68 -0
- package/src/core/tool-builders/agent-tools.ts +71 -0
- package/src/core/tool-builders/external-tools.ts +179 -0
- package/src/core/tool-builders/types.ts +16 -0
- package/src/core/tool-builders/workspace-tools.ts +293 -0
- package/src/core/tool-capabilities.ts +65 -0
- package/src/goals/evaluator.ts +171 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/loop.ts +167 -0
- package/src/goals/service.ts +39 -0
- package/src/index.ts +10 -0
- package/src/internal/ask-authority.ts +29 -0
- package/src/internal/composition.ts +44 -0
- package/src/internal/configuration.ts +22 -0
- package/src/internal/execution.ts +108 -0
- package/src/internal/hosts.ts +64 -0
- package/src/internal/plugins.ts +71 -0
- package/src/internal/providers.ts +32 -0
- package/src/internal/sandbox.ts +19 -0
- package/src/internal/tools.ts +48 -0
- package/src/internal/workspace.ts +25 -0
- package/src/kernel/diagnostics.ts +249 -0
- package/src/kernel/errors.ts +120 -0
- package/src/kernel/events.ts +82 -0
- package/src/kernel/index.ts +72 -0
- package/src/kernel/kernel.ts +62 -0
- package/src/kernel/lifecycle.ts +72 -0
- package/src/kernel/plugin.ts +218 -0
- package/src/kernel/registry.ts +493 -0
- package/src/kernel/scope.ts +776 -0
- package/src/kernel/service-key.ts +19 -0
- package/src/kernel/types.ts +317 -0
- package/src/memory/index.ts +2 -0
- package/src/memory/memory-tool.ts +75 -0
- package/src/memory/registry.ts +172 -0
- package/src/permission/ask-user-api.ts +70 -0
- package/src/permission/contracts.ts +135 -0
- package/src/permission/permission-request-manager.ts +58 -0
- package/src/permission/policy.ts +277 -0
- package/src/permission/runtime.ts +612 -0
- package/src/plugins/compaction-policy.ts +46 -0
- package/src/plugins/compose.ts +171 -0
- package/src/plugins/context-sections.ts +246 -0
- package/src/plugins/default-agent-driver.ts +14 -0
- package/src/plugins/facade-plugins.ts +129 -0
- package/src/plugins/goal-domain.ts +82 -0
- package/src/plugins/legacy-system-message.ts +152 -0
- package/src/plugins/loaded-tools.ts +23 -0
- package/src/plugins/memory-domain.ts +264 -0
- package/src/plugins/orchestrator-session.ts +29 -0
- package/src/plugins/permission-policy.ts +49 -0
- package/src/plugins/retry-policy.ts +28 -0
- package/src/plugins/scheduler-domain.ts +192 -0
- package/src/plugins/service-keys.ts +294 -0
- package/src/plugins/session-search-domain.ts +238 -0
- package/src/plugins/skills-domain.ts +272 -0
- package/src/plugins/subagent-domain.ts +287 -0
- package/src/plugins/tool-catalog.ts +78 -0
- package/src/plugins/tool-output-policy.ts +52 -0
- package/src/plugins/value-plugins.ts +150 -0
- package/src/plugins/workflow-domain.ts +198 -0
- package/src/plugins/workspace-policy.ts +37 -0
- package/src/providers/registry.ts +63 -0
- package/src/providers/types.ts +44 -0
- package/src/retry/policy.ts +282 -0
- package/src/retry/stream-chat.ts +312 -0
- package/src/runtime/agent-runtime.ts +83 -0
- package/src/runtime/default-agent-driver.ts +23 -0
- package/src/runtime/domain-tool-source.ts +156 -0
- package/src/runtime/events.ts +61 -0
- package/src/runtime/host-dependencies.ts +71 -0
- package/src/runtime/host-guidance.ts +22 -0
- package/src/runtime/host-layout.ts +23 -0
- package/src/runtime/host.ts +129 -0
- package/src/runtime/standalone-host.ts +118 -0
- package/src/sandbox/controller.ts +204 -0
- package/src/sandbox/model.ts +305 -0
- package/src/sandbox/provider.ts +53 -0
- package/src/sandbox/types.ts +110 -0
- package/src/scheduler/host.ts +22 -0
- package/src/scheduler/scheduler-tool.ts +172 -0
- package/src/session-search/host.ts +56 -0
- package/src/session-search/index.ts +23 -0
- package/src/session-search/session-search-tool.ts +151 -0
- package/src/skills/index.ts +3 -0
- package/src/skills/registry.ts +63 -0
- package/src/skills/skill-manage-tool.ts +205 -0
- package/src/skills/skill-tool.ts +42 -0
- package/src/storage/contracts.ts +159 -0
- package/src/storage/memory.ts +321 -0
- package/src/storage/options.ts +75 -0
- package/src/storage/runtime.ts +115 -0
- package/src/storage/sqlite-tool-output-artifacts.ts +106 -0
- package/src/storage/sqlite.ts +321 -0
- package/src/storage/tool-output-artifacts.ts +75 -0
- package/src/storage.ts +31 -0
- package/src/subagent/child-session.ts +282 -0
- package/src/subagent/guidance.ts +8 -0
- package/src/subagent/policy.ts +198 -0
- package/src/subagent/task-tool.ts +584 -0
- package/src/tool-output/contracts.ts +111 -0
- package/src/tool-output/policy.ts +410 -0
- package/src/tool.ts +1 -0
- package/src/tools/executor.ts +258 -0
- package/src/tools/install-manifest.ts +40 -0
- package/src/tools/llm-api.ts +77 -0
- package/src/tools/registry.ts +206 -0
- package/src/tools/tool-artifact.ts +182 -0
- package/src/tools/tool-source.ts +53 -0
- package/src/utils/errors.ts +334 -0
- package/src/utils/strip-visualization.ts +50 -0
- package/src/workflow/decomposer.ts +139 -0
- package/src/workflow/execution.ts +523 -0
- package/src/workflow/orchestrator-session.ts +161 -0
- package/src/workflow/synthesizer.ts +130 -0
- package/src/workspace/contracts.ts +135 -0
- package/src/workspace/policy.ts +327 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C6 compaction policy contract and default provider.
|
|
3
|
+
*
|
|
4
|
+
* The agent-scoped compaction service owns the current policy decisions:
|
|
5
|
+
* policy resolution, the hybrid threshold formula, pruning knobs, the
|
|
6
|
+
* failure cooldown, the replay selection, and the per-session concurrency
|
|
7
|
+
* guard (all previously module-global state). The trigger creation, summary
|
|
8
|
+
* generation, pruning, and failure persistence live in `task.ts` with their
|
|
9
|
+
* safety invariants non-configurable.
|
|
10
|
+
*
|
|
11
|
+
* Environment variables become provider options at composition: the plugin
|
|
12
|
+
* layer reads the composed `capek.runtime-configuration` service once at
|
|
13
|
+
* setup and freezes the values into this service's options. Consumers that
|
|
14
|
+
* run outside a composed scope (the current Jean2 server path) fall back to
|
|
15
|
+
* one lazily created process-default service that keeps reading the active
|
|
16
|
+
* configuration live, exactly like the pre-C6 module reads, until C8 retires
|
|
17
|
+
* the compat surface.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
21
|
+
import {
|
|
22
|
+
findModel,
|
|
23
|
+
getMaxOutputTokens,
|
|
24
|
+
getRuntimeConfiguration,
|
|
25
|
+
} from '../configuration/runtime';
|
|
26
|
+
import { listMessagesWithParts } from '../storage/runtime';
|
|
27
|
+
import type {
|
|
28
|
+
AutoThresholdResult,
|
|
29
|
+
CompactionPolicy,
|
|
30
|
+
} from './contracts';
|
|
31
|
+
|
|
32
|
+
/** Fully resolved provider options. Every field holds the env-translated
|
|
33
|
+
* value; nothing in this interface reads the environment. */
|
|
34
|
+
export interface CompactionServiceOptions {
|
|
35
|
+
modelId: string | null;
|
|
36
|
+
providerId: string | null;
|
|
37
|
+
maxOutputTokens: number;
|
|
38
|
+
preserveRecentToolCount: number;
|
|
39
|
+
preserveSmallToolChars: number;
|
|
40
|
+
toolClearCharsThreshold: number;
|
|
41
|
+
maxPrunedToolCount: number;
|
|
42
|
+
autoThresholdRatio: number;
|
|
43
|
+
autoReserveCapTokens: number;
|
|
44
|
+
autoSafetyMarginTokens: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Optional test seams. `failureCooldownState` replaces the owned cooldown
|
|
48
|
+
* map; `now` replaces Date.now for cooldown window tests. Neither is used by
|
|
49
|
+
* production wiring. */
|
|
50
|
+
export interface CompactionServiceCreateOptions {
|
|
51
|
+
id?: string;
|
|
52
|
+
/** When omitted (the process-default fallback), options resolve live from
|
|
53
|
+
* the active runtime configuration, preserving the pre-C6 unscoped
|
|
54
|
+
* behavior. Composed scopes always pass frozen composition-time options. */
|
|
55
|
+
options?: CompactionServiceOptions;
|
|
56
|
+
failureCooldownState?: Map<string, { count: number; lastFailureAt: number }>;
|
|
57
|
+
now?: () => number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* C6 compaction service contract. Agent-scoped behind
|
|
62
|
+
* `capekCompactionServiceKey`. The default provider reproduces the exact
|
|
63
|
+
* pre-C6 behavior; a custom provider may vary the policy resolution, pruning
|
|
64
|
+
* numbers, threshold formula, cooldown, or replay selection without the
|
|
65
|
+
* executor or task pipeline changing. The safety invariants (main-session
|
|
66
|
+
* requirement, message-count validation, trigger validation, and boundary
|
|
67
|
+
* validation) stay inside the task pipeline and are not options.
|
|
68
|
+
*/
|
|
69
|
+
export interface CompactionService {
|
|
70
|
+
readonly id: string;
|
|
71
|
+
/** Effective provider options for this scope. */
|
|
72
|
+
readonly options: Readonly<CompactionServiceOptions>;
|
|
73
|
+
/** Resolves a CompactionPolicy from optional overrides and session
|
|
74
|
+
* defaults. Service options take precedence, then overrides, then session
|
|
75
|
+
* values, then defaults. */
|
|
76
|
+
resolvePolicy(
|
|
77
|
+
sessionModelId: string | undefined,
|
|
78
|
+
sessionProviderId: string | undefined,
|
|
79
|
+
overrides?: Partial<CompactionPolicy>,
|
|
80
|
+
): CompactionPolicy;
|
|
81
|
+
/** Hybrid auto-compaction threshold formula. */
|
|
82
|
+
computeThreshold(modelId: string | undefined, policy?: CompactionPolicy): AutoThresholdResult;
|
|
83
|
+
/** Failure cooldown: true when the session recently hit the consecutive
|
|
84
|
+
* failure limit and compaction should be skipped. */
|
|
85
|
+
shouldSkipCompaction(sessionId: string): boolean;
|
|
86
|
+
recordCompactionFailure(sessionId: string): void;
|
|
87
|
+
clearCompactionFailure(sessionId: string): void;
|
|
88
|
+
/** Replay policy: the replay text used after a successful compaction, or
|
|
89
|
+
* null when the history has no prior user turn to replay. */
|
|
90
|
+
buildReplayText(sessionId: string): Promise<string | null>;
|
|
91
|
+
/** Per-session compaction concurrency guard. */
|
|
92
|
+
isCompactionActive(sessionId: string): boolean;
|
|
93
|
+
beginCompaction(sessionId: string): void;
|
|
94
|
+
endCompaction(sessionId: string): void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const COMPACTION_FAILURE_COOLDOWN_MS = 60_000;
|
|
98
|
+
const COMPACTION_MAX_CONSECUTIVE_FAILURES = 2;
|
|
99
|
+
|
|
100
|
+
function resolveOptionsLive(): CompactionServiceOptions {
|
|
101
|
+
const configuration = getRuntimeConfiguration();
|
|
102
|
+
return {
|
|
103
|
+
modelId: configuration.getCompactionModel() ?? null,
|
|
104
|
+
providerId: configuration.getCompactionProvider() ?? null,
|
|
105
|
+
maxOutputTokens: configuration.getCompactionMaxTokens(),
|
|
106
|
+
preserveRecentToolCount: configuration.getCompactionPreserveRecentToolCount(),
|
|
107
|
+
preserveSmallToolChars: configuration.getCompactionPreserveSmallToolChars(),
|
|
108
|
+
toolClearCharsThreshold: configuration.getCompactionToolClearCharsThreshold(),
|
|
109
|
+
maxPrunedToolCount: configuration.getCompactionMaxPrunedToolCount(),
|
|
110
|
+
autoThresholdRatio: configuration.getCompactionAutoThresholdRatio(),
|
|
111
|
+
autoReserveCapTokens: configuration.getCompactionAutoReserveCapTokens(),
|
|
112
|
+
autoSafetyMarginTokens: configuration.getCompactionAutoSafetyMarginTokens(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The C6 default provider wrapping the exact pre-C6 behavior. */
|
|
117
|
+
export function createCompactionService(
|
|
118
|
+
createOptions: CompactionServiceCreateOptions = {},
|
|
119
|
+
): CompactionService {
|
|
120
|
+
const id = createOptions.id ?? 'compaction.default';
|
|
121
|
+
const frozenOptions = createOptions.options;
|
|
122
|
+
const failureCooldownState = createOptions.failureCooldownState
|
|
123
|
+
?? new Map<string, { count: number; lastFailureAt: number }>();
|
|
124
|
+
const activeSessions = new Set<string>();
|
|
125
|
+
const now = createOptions.now ?? Date.now;
|
|
126
|
+
|
|
127
|
+
function currentOptions(): CompactionServiceOptions {
|
|
128
|
+
return frozenOptions ?? resolveOptionsLive();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
id,
|
|
133
|
+
get options(): Readonly<CompactionServiceOptions> {
|
|
134
|
+
return currentOptions();
|
|
135
|
+
},
|
|
136
|
+
resolvePolicy(
|
|
137
|
+
sessionModelId: string | undefined,
|
|
138
|
+
sessionProviderId: string | undefined,
|
|
139
|
+
overrides?: Partial<CompactionPolicy>,
|
|
140
|
+
): CompactionPolicy {
|
|
141
|
+
const current = currentOptions();
|
|
142
|
+
return {
|
|
143
|
+
modelId: current.modelId ?? overrides?.modelId ?? sessionModelId ?? null,
|
|
144
|
+
providerId: current.providerId ?? overrides?.providerId ?? sessionProviderId ?? null,
|
|
145
|
+
maxOutputTokens: overrides?.maxOutputTokens ?? current.maxOutputTokens,
|
|
146
|
+
overflowThresholdRatio: overrides?.overflowThresholdRatio ?? null,
|
|
147
|
+
// WS4: Budget-aware pruning - options take precedence, then overrides
|
|
148
|
+
preserveRecentToolCount: overrides?.preserveRecentToolCount ?? current.preserveRecentToolCount,
|
|
149
|
+
preserveSmallToolChars: overrides?.preserveSmallToolChars ?? current.preserveSmallToolChars,
|
|
150
|
+
toolClearCharsThreshold: overrides?.toolClearCharsThreshold ?? current.toolClearCharsThreshold,
|
|
151
|
+
maxPrunedToolCount: overrides?.maxPrunedToolCount ?? current.maxPrunedToolCount,
|
|
152
|
+
// Hybrid formula - options take precedence, then overrides
|
|
153
|
+
autoThresholdRatio: overrides?.autoThresholdRatio ?? current.autoThresholdRatio,
|
|
154
|
+
autoReserveCapTokens: overrides?.autoReserveCapTokens ?? current.autoReserveCapTokens,
|
|
155
|
+
autoSafetyMarginTokens: overrides?.autoSafetyMarginTokens ?? current.autoSafetyMarginTokens,
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
computeThreshold(modelId: string | undefined, policy?: CompactionPolicy): AutoThresholdResult {
|
|
159
|
+
const modelDef = modelId ? findModel(modelId) : undefined;
|
|
160
|
+
const contextWindow = modelDef?.contextWindow;
|
|
161
|
+
|
|
162
|
+
if (!contextWindow) {
|
|
163
|
+
return { threshold: 0, contextWindow: undefined };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const modelMaxOutputTokens = getMaxOutputTokens(modelId!);
|
|
167
|
+
const current = currentOptions();
|
|
168
|
+
|
|
169
|
+
const autoThresholdRatio = policy?.autoThresholdRatio ?? current.autoThresholdRatio;
|
|
170
|
+
const autoReserveCapTokens = policy?.autoReserveCapTokens ?? current.autoReserveCapTokens;
|
|
171
|
+
const autoSafetyMarginTokens = policy?.autoSafetyMarginTokens ?? current.autoSafetyMarginTokens;
|
|
172
|
+
|
|
173
|
+
const reserve = Math.min(modelMaxOutputTokens, autoReserveCapTokens);
|
|
174
|
+
const ratioBasedThreshold = Math.floor(contextWindow * autoThresholdRatio);
|
|
175
|
+
const safeThreshold = contextWindow - reserve - autoSafetyMarginTokens;
|
|
176
|
+
|
|
177
|
+
const threshold = Math.max(0, Math.min(ratioBasedThreshold, safeThreshold));
|
|
178
|
+
|
|
179
|
+
return { threshold, contextWindow };
|
|
180
|
+
},
|
|
181
|
+
shouldSkipCompaction(sessionId: string): boolean {
|
|
182
|
+
const tracker = failureCooldownState.get(sessionId);
|
|
183
|
+
if (!tracker) return false;
|
|
184
|
+
|
|
185
|
+
const elapsed = now() - tracker.lastFailureAt;
|
|
186
|
+
if (elapsed > COMPACTION_FAILURE_COOLDOWN_MS) {
|
|
187
|
+
failureCooldownState.delete(sessionId);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return tracker.count >= COMPACTION_MAX_CONSECUTIVE_FAILURES;
|
|
192
|
+
},
|
|
193
|
+
recordCompactionFailure(sessionId: string): void {
|
|
194
|
+
const existing = failureCooldownState.get(sessionId);
|
|
195
|
+
if (existing) {
|
|
196
|
+
existing.count++;
|
|
197
|
+
existing.lastFailureAt = now();
|
|
198
|
+
} else {
|
|
199
|
+
failureCooldownState.set(sessionId, { count: 1, lastFailureAt: now() });
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
clearCompactionFailure(sessionId: string): void {
|
|
203
|
+
failureCooldownState.delete(sessionId);
|
|
204
|
+
},
|
|
205
|
+
async buildReplayText(sessionId: string): Promise<string | null> {
|
|
206
|
+
const allMessages = await listMessagesWithParts(sessionId);
|
|
207
|
+
|
|
208
|
+
for (let i = allMessages.length - 2; i >= 0; i--) {
|
|
209
|
+
const m = allMessages[i];
|
|
210
|
+
if (m.message.role !== 'user') continue;
|
|
211
|
+
if (m.parts.every((p) => p.type === 'compaction')) continue;
|
|
212
|
+
|
|
213
|
+
const texts: string[] = [];
|
|
214
|
+
for (const p of m.parts) {
|
|
215
|
+
if (p.type === 'text' && p.text !== undefined) {
|
|
216
|
+
if (!p.text.startsWith('Continue:') && !p.text.startsWith('Continue from')) {
|
|
217
|
+
texts.push(p.text);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const text = texts.join(' ').trim();
|
|
222
|
+
if (text) {
|
|
223
|
+
return `Replay: ${text}`;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return null;
|
|
228
|
+
},
|
|
229
|
+
isCompactionActive(sessionId: string): boolean {
|
|
230
|
+
return activeSessions.has(sessionId);
|
|
231
|
+
},
|
|
232
|
+
beginCompaction(sessionId: string): void {
|
|
233
|
+
activeSessions.add(sessionId);
|
|
234
|
+
},
|
|
235
|
+
endCompaction(sessionId: string): void {
|
|
236
|
+
activeSessions.delete(sessionId);
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const scopedService = new AsyncLocalStorage<CompactionService>();
|
|
242
|
+
let processDefaultService: CompactionService | undefined;
|
|
243
|
+
|
|
244
|
+
/** Resolves the service seeded for the active agent scope, falling back to
|
|
245
|
+
* one lazily created process-default service for consumers that run outside
|
|
246
|
+
* a composed scope (the current Jean2 server path). The process default
|
|
247
|
+
* keeps reading the active configuration live, exactly like the pre-C6
|
|
248
|
+
* module reads.
|
|
249
|
+
*
|
|
250
|
+
* The executor and the recovery policy both resolve through this accessor,
|
|
251
|
+
* so outside a composed scope they share the same process-default instance:
|
|
252
|
+
* one active-session set (the concurrency guard) and one failure-cooldown
|
|
253
|
+
* map for the whole process, exactly like the pre-C6 module globals. */
|
|
254
|
+
export function getCompactionService(): CompactionService {
|
|
255
|
+
return scopedService.getStore()
|
|
256
|
+
?? (processDefaultService ??= createCompactionService({ id: 'compaction.process-default' }));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Seeds a service for the callback duration. `enterAgentScope` seeds the
|
|
260
|
+
* composed agent scope's service here. */
|
|
261
|
+
export function withCompactionService<T>(service: CompactionService, callback: () => T): T {
|
|
262
|
+
return scopedService.run(service, callback);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Test-only reset of the lazily created process default. Exported from this
|
|
266
|
+
* module only; no package subpath re-exports it. */
|
|
267
|
+
export function resetDefaultCompactionServiceForTests(): void {
|
|
268
|
+
processDefaultService = undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Compatibility function over the scoped service: resolves the default
|
|
272
|
+
* policy with legacy-compatible defaults (model/provider stay null; env
|
|
273
|
+
* translation happens through the service options).
|
|
274
|
+
*
|
|
275
|
+
* Scope dependence: inside a composed agent scope this reflects the scope's
|
|
276
|
+
* frozen composition-time options; outside any scope it reads the live
|
|
277
|
+
* active configuration through the process-default service. The pre-C6
|
|
278
|
+
* module function read the live configuration unconditionally, so the
|
|
279
|
+
* unscoped path is the exact compat behavior. */
|
|
280
|
+
export function getDefaultCompactionPolicy(): CompactionPolicy {
|
|
281
|
+
const current = getCompactionService().options;
|
|
282
|
+
return {
|
|
283
|
+
modelId: null,
|
|
284
|
+
providerId: null,
|
|
285
|
+
maxOutputTokens: current.maxOutputTokens,
|
|
286
|
+
overflowThresholdRatio: null,
|
|
287
|
+
preserveRecentToolCount: current.preserveRecentToolCount,
|
|
288
|
+
preserveSmallToolChars: current.preserveSmallToolChars,
|
|
289
|
+
toolClearCharsThreshold: current.toolClearCharsThreshold,
|
|
290
|
+
maxPrunedToolCount: current.maxPrunedToolCount,
|
|
291
|
+
autoThresholdRatio: current.autoThresholdRatio,
|
|
292
|
+
autoReserveCapTokens: current.autoReserveCapTokens,
|
|
293
|
+
autoSafetyMarginTokens: current.autoSafetyMarginTokens,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Compatibility function over the scoped service. Scope dependence:
|
|
298
|
+
* composed scopes resolve through their frozen composition-time options;
|
|
299
|
+
* the unscoped path reads the live active configuration exactly like the
|
|
300
|
+
* pre-C6 module function. */
|
|
301
|
+
export function resolveCompactionPolicy(
|
|
302
|
+
sessionModelId: string | undefined,
|
|
303
|
+
sessionProviderId: string | undefined,
|
|
304
|
+
overrides?: Partial<CompactionPolicy>,
|
|
305
|
+
): CompactionPolicy {
|
|
306
|
+
return getCompactionService().resolvePolicy(sessionModelId, sessionProviderId, overrides);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Compatibility function over the scoped service. Scope dependence:
|
|
310
|
+
* composed scopes resolve through their frozen composition-time options;
|
|
311
|
+
* the unscoped path reads the live active configuration exactly like the
|
|
312
|
+
* pre-C6 module function. */
|
|
313
|
+
export function computeAutoThreshold(
|
|
314
|
+
modelId: string | undefined,
|
|
315
|
+
policy?: CompactionPolicy,
|
|
316
|
+
): AutoThresholdResult {
|
|
317
|
+
return getCompactionService().computeThreshold(modelId, policy);
|
|
318
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C6 compaction recovery policy. Owns the pre-C6 reconciliation decisions
|
|
3
|
+
* that previously lived in the server store module: skip while compaction is
|
|
4
|
+
* genuinely in flight, clear a stuck compacting flag, and persist one
|
|
5
|
+
* append-only failure per orphaned trigger. The storage queries stay
|
|
6
|
+
* host-provided through `CompactionRecoveryDeps`, so the Capek policy never
|
|
7
|
+
* imports server SQL; the server store module is now a thin compat wiring
|
|
8
|
+
* over this domain.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Message, Session } from '@capekai/types';
|
|
12
|
+
import type { BroadcastFn } from '../runtime/host-dependencies';
|
|
13
|
+
import { getCompactionService } from './policy';
|
|
14
|
+
import { persistCompactionFailure } from './task';
|
|
15
|
+
|
|
16
|
+
/** Inward-facing storage port the host (the current Jean2 server store)
|
|
17
|
+
* fulfills. Shapes are SDK structural copies; no SQL crosses the boundary. */
|
|
18
|
+
export interface CompactionRecoveryDeps {
|
|
19
|
+
/** True when the session's compacting flag is set (a stuck state). */
|
|
20
|
+
isSessionCompacting(sessionId: string): boolean;
|
|
21
|
+
/** Clears the stuck compacting flag and returns the updated session, or
|
|
22
|
+
* null when the session no longer exists. */
|
|
23
|
+
clearSessionCompacting(sessionId: string): Session | null;
|
|
24
|
+
/** Orphaned compaction triggers: user messages with a compaction part and
|
|
25
|
+
* no assistant outcome (assistant message with parentId pointing at it). */
|
|
26
|
+
listOrphanedCompactionTriggers(sessionId: string): Message[];
|
|
27
|
+
/** All session ids for startup-wide reconciliation. */
|
|
28
|
+
listSessionIds(): string[];
|
|
29
|
+
/** Broadcasts runtime events for the persisted failure records. */
|
|
30
|
+
broadcast: BroadcastFn;
|
|
31
|
+
/** Broadcasts session updates after clearing the stuck flag. */
|
|
32
|
+
broadcastSessionUpdated(session: Session): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ReconcileOptions {
|
|
36
|
+
/**
|
|
37
|
+
* When false, skips broadcasting session.updated after clearing the compacting
|
|
38
|
+
* flag. Use this at startup before the broadcast callback is registered.
|
|
39
|
+
* @default true
|
|
40
|
+
*/
|
|
41
|
+
broadcast?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reconcile a single session's compaction state.
|
|
46
|
+
*
|
|
47
|
+
* This function:
|
|
48
|
+
* 1. Finds orphaned compaction triggers (triggers without any outcome)
|
|
49
|
+
* 2. Persists failure records for each orphaned trigger (idempotent)
|
|
50
|
+
* 3. Clears the compacting flag if set
|
|
51
|
+
*
|
|
52
|
+
* Returns the number of orphaned triggers reconciled.
|
|
53
|
+
*/
|
|
54
|
+
export async function reconcileSessionCompaction(
|
|
55
|
+
sessionId: string,
|
|
56
|
+
deps: CompactionRecoveryDeps,
|
|
57
|
+
options: ReconcileOptions = {},
|
|
58
|
+
): Promise<number> {
|
|
59
|
+
const { broadcast = true } = options;
|
|
60
|
+
const broadcastFn: BroadcastFn = broadcast ? deps.broadcast : () => {};
|
|
61
|
+
const broadcastSessUpdate = broadcast ? deps.broadcastSessionUpdated : () => {};
|
|
62
|
+
|
|
63
|
+
// If compaction is genuinely in-flight (tracked in-memory), skip reconciliation entirely.
|
|
64
|
+
// This prevents false "Compaction interrupted" failures when the user switches sessions
|
|
65
|
+
// while compaction is still running on the server.
|
|
66
|
+
if (getCompactionService().isCompactionActive(sessionId)) {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Always clear the compacting flag - it's stuck if we're recovering
|
|
71
|
+
if (deps.isSessionCompacting(sessionId)) {
|
|
72
|
+
const session = deps.clearSessionCompacting(sessionId);
|
|
73
|
+
if (broadcast && session) {
|
|
74
|
+
broadcastSessUpdate(session);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Find orphaned triggers
|
|
79
|
+
const orphanedTriggers = deps.listOrphanedCompactionTriggers(sessionId);
|
|
80
|
+
const count = orphanedTriggers.length;
|
|
81
|
+
|
|
82
|
+
if (count === 0) {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Persist failure for each orphaned trigger
|
|
87
|
+
for (const trigger of orphanedTriggers) {
|
|
88
|
+
// Idempotent: once persisted, the failure message becomes the outcome,
|
|
89
|
+
// so the orphan query (NOT EXISTS outcome.parent_id) stops matching.
|
|
90
|
+
await persistCompactionFailure(
|
|
91
|
+
sessionId,
|
|
92
|
+
trigger.id,
|
|
93
|
+
'Compaction interrupted (session recovered after crash or interruption)',
|
|
94
|
+
broadcastFn,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(
|
|
99
|
+
`[compaction-recovery] Reconciled ${count} orphaned trigger(s) for session ${sessionId}`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return count;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Run one-shot recovery across all sessions at startup.
|
|
107
|
+
*
|
|
108
|
+
* This scans all sessions to find orphaned compaction triggers (user messages
|
|
109
|
+
* with a compaction part that have no outcome). Once persisted, the failure
|
|
110
|
+
* message becomes the outcome, so the orphan query stops matching.
|
|
111
|
+
*
|
|
112
|
+
* Returns total count of orphaned triggers reconciled.
|
|
113
|
+
*/
|
|
114
|
+
export async function reconcileAllSessionsCompaction(deps: CompactionRecoveryDeps): Promise<number> {
|
|
115
|
+
const sessionIds = new Set(deps.listSessionIds());
|
|
116
|
+
|
|
117
|
+
if (sessionIds.size === 0) {
|
|
118
|
+
console.log('[compaction-recovery] No sessions requiring compaction reconciliation found');
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(
|
|
123
|
+
`[compaction-recovery] Reconciling ${sessionIds.size} session(s) for compaction state`,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
let totalReconciled = 0;
|
|
127
|
+
|
|
128
|
+
// Startup path: disable broadcasting since the broadcast callback may not
|
|
129
|
+
// be registered yet when this runs at server startup.
|
|
130
|
+
for (const sessionId of sessionIds) {
|
|
131
|
+
totalReconciled += await reconcileSessionCompaction(sessionId, deps, { broadcast: false });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log(
|
|
135
|
+
`[compaction-recovery] Startup recovery complete: ${totalReconciled} orphaned trigger(s) reconciled`,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return totalReconciled;
|
|
139
|
+
}
|