@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,540 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C6 compaction task pipeline: trigger creation, conversation text, summary
|
|
3
|
+
* generation, budget-aware pruning, and failure persistence. All behavior is
|
|
4
|
+
* moved verbatim from the pre-C6 `core/compaction.ts`; the safety invariants
|
|
5
|
+
* (minimum-message validation, trigger validation, boundary validation, and
|
|
6
|
+
* the main-session requirement enforced by the executor) stay hard errors
|
|
7
|
+
* and are not configurable.
|
|
8
|
+
*
|
|
9
|
+
* Named core edges (AST-gated by `compaction-domain-no-core`): model
|
|
10
|
+
* construction (`core/model-utils`) and provider discovery
|
|
11
|
+
* (`core/provider-utils`) stay in core until C7.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { streamText as aiStreamText } from 'ai';
|
|
15
|
+
import { randomUUID } from 'crypto';
|
|
16
|
+
import type {
|
|
17
|
+
AssistantMessage, CompactionPart, MessageWithParts, TextPart, ToolPart } from '@capekai/types';
|
|
18
|
+
import { getModelWithMetadata } from '../core/model-utils';
|
|
19
|
+
import { findProviderFromModel } from '../core/provider-utils';
|
|
20
|
+
import { getModelsConfig } from '../configuration/runtime';
|
|
21
|
+
import { emitRuntimeEvent, type BroadcastFn } from '../runtime/host-dependencies';
|
|
22
|
+
import {
|
|
23
|
+
buildEffectiveContextHistory,
|
|
24
|
+
createMessage,
|
|
25
|
+
createPart,
|
|
26
|
+
getPartsBySession,
|
|
27
|
+
listMessagesWithParts,
|
|
28
|
+
updatePart,
|
|
29
|
+
} from '../storage/runtime';
|
|
30
|
+
import type {
|
|
31
|
+
CompactionPolicy,
|
|
32
|
+
CompactionTrigger,
|
|
33
|
+
CompactionTriggerReason,
|
|
34
|
+
CompactionTaskResult,
|
|
35
|
+
GenerateSummaryFn,
|
|
36
|
+
} from './contracts';
|
|
37
|
+
|
|
38
|
+
const COMPACTION_PROMPT_FIRST = `Summarize the following conversation for context continuity.
|
|
39
|
+
|
|
40
|
+
Structure your response with these sections:
|
|
41
|
+
- **Decisions**: Key choices made and rationale
|
|
42
|
+
- **Changes**: Files/functions created or modified (with paths)
|
|
43
|
+
- **Context**: Important state, configurations, or patterns established
|
|
44
|
+
- **Open items**: Unresolved issues or planned next steps
|
|
45
|
+
|
|
46
|
+
Be specific with file paths, function names, and technical details.
|
|
47
|
+
|
|
48
|
+
Conversation to summarize:
|
|
49
|
+
|
|
50
|
+
{CONVERSATION}`;
|
|
51
|
+
|
|
52
|
+
const COMPACTION_PROMPT_INCREMENTAL = `The following is a previous conversation summary, followed by new messages since that summary.
|
|
53
|
+
|
|
54
|
+
Produce an UPDATED summary that incorporates the new information. Keep it concise and structured.
|
|
55
|
+
|
|
56
|
+
Structure your response with these sections:
|
|
57
|
+
- **Decisions**: Key choices made and rationale
|
|
58
|
+
- **Changes**: Files/functions created or modified (with paths)
|
|
59
|
+
- **Context**: Important state, configurations, or patterns established
|
|
60
|
+
- **Open items**: Unresolved issues or planned next steps
|
|
61
|
+
|
|
62
|
+
Previous summary:
|
|
63
|
+
{PREVIOUS_SUMMARY}
|
|
64
|
+
|
|
65
|
+
New messages since that summary:
|
|
66
|
+
{CONVERSATION}`;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Creates a compaction trigger message and returns it.
|
|
70
|
+
* The trigger is persisted to the database as a user message with a standard CompactionPart.
|
|
71
|
+
*/
|
|
72
|
+
export async function createCompactionTrigger(
|
|
73
|
+
sessionId: string,
|
|
74
|
+
reason: CompactionTriggerReason,
|
|
75
|
+
): Promise<CompactionTrigger> {
|
|
76
|
+
// Minimum validation: at least 1 user + 1 assistant message needed for meaningful compaction.
|
|
77
|
+
// A single agent turn with heavy tool use can produce enough context to warrant compaction.
|
|
78
|
+
const { messages: effectiveHistory } = await buildEffectiveContextHistory(sessionId);
|
|
79
|
+
const nonSystemCount = effectiveHistory.filter(
|
|
80
|
+
(m: MessageWithParts) => m.message.role !== 'system',
|
|
81
|
+
).length;
|
|
82
|
+
|
|
83
|
+
if (nonSystemCount < 2) {
|
|
84
|
+
throw new Error('Not enough messages for compaction (need at least a user and assistant turn)');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const triggerMessageId = randomUUID();
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
|
|
90
|
+
// Create a trigger message (user role to indicate it came from the user/system)
|
|
91
|
+
const triggerMessage = {
|
|
92
|
+
id: triggerMessageId,
|
|
93
|
+
sessionId,
|
|
94
|
+
role: 'user' as const,
|
|
95
|
+
createdAt: now,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await createMessage(triggerMessage);
|
|
99
|
+
|
|
100
|
+
// Create a standard CompactionPart (metadata-only per spec)
|
|
101
|
+
const compactionPart: CompactionPart = {
|
|
102
|
+
id: randomUUID(),
|
|
103
|
+
messageId: triggerMessageId,
|
|
104
|
+
createdAt: now,
|
|
105
|
+
type: 'compaction',
|
|
106
|
+
auto: reason !== 'manual',
|
|
107
|
+
overflow: reason === 'overflow',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
await createPart(compactionPart, sessionId);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
messageId: triggerMessageId,
|
|
114
|
+
reason,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function buildConversationText(messages: MessageWithParts[]): string {
|
|
119
|
+
const lines: string[] = [];
|
|
120
|
+
|
|
121
|
+
for (const { message, parts } of messages) {
|
|
122
|
+
if (message.role === 'system') continue;
|
|
123
|
+
|
|
124
|
+
lines.push(`\n--- ${message.role.toUpperCase()} ---`);
|
|
125
|
+
|
|
126
|
+
for (const part of parts) {
|
|
127
|
+
if (part.type === 'text') {
|
|
128
|
+
lines.push((part as { text: string }).text);
|
|
129
|
+
} else if (part.type === 'tool') {
|
|
130
|
+
const toolPart = part as {
|
|
131
|
+
name: string;
|
|
132
|
+
state: { input: unknown; output?: unknown; status: string; error?: string };
|
|
133
|
+
};
|
|
134
|
+
lines.push(`\n[TOOL: ${toolPart.name}]`);
|
|
135
|
+
lines.push(`Input: ${JSON.stringify(toolPart.state.input, null, 2)}`);
|
|
136
|
+
if (toolPart.state.status === 'completed') {
|
|
137
|
+
lines.push(`Output: ${formatOutput(toolPart.state.output)}`);
|
|
138
|
+
} else if (toolPart.state.status === 'error') {
|
|
139
|
+
lines.push(`Error: ${toolPart.state.error}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return lines.join('\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function formatOutput(output: unknown): string {
|
|
149
|
+
if (typeof output === 'string') {
|
|
150
|
+
return output.length > 500
|
|
151
|
+
? output.slice(0, 500) + '...(truncated)'
|
|
152
|
+
: output;
|
|
153
|
+
}
|
|
154
|
+
const str = JSON.stringify(output, null, 2);
|
|
155
|
+
return str.length > 500 ? str.slice(0, 500) + '...(truncated)' : str;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Estimate the character size of a tool's output.
|
|
160
|
+
* Uses cheap serialization - no need for accurate tokenization here.
|
|
161
|
+
*/
|
|
162
|
+
export function estimateToolOutputSize(output: unknown): number {
|
|
163
|
+
if (output === null || output === undefined) {
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
if (typeof output === 'string') {
|
|
167
|
+
return output.length;
|
|
168
|
+
}
|
|
169
|
+
// For objects/arrays, serialize to JSON and measure
|
|
170
|
+
try {
|
|
171
|
+
return JSON.stringify(output).length;
|
|
172
|
+
} catch {
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Mark tool results as compacted after a successful compaction.
|
|
179
|
+
* WS4: Budget-aware pruning - selectively marks tools based on policy.
|
|
180
|
+
*
|
|
181
|
+
* Pruning strategy:
|
|
182
|
+
* 1. Always protect 'skill' tool outputs
|
|
183
|
+
* 2. Protect small outputs (below preserveSmallToolChars)
|
|
184
|
+
* 3. Protect the N most recent eligible tools (preserveRecentToolCount)
|
|
185
|
+
* 4. Clear older/larger outputs that exceed toolClearCharsThreshold
|
|
186
|
+
* 5. Respect maxPrunedToolCount limit
|
|
187
|
+
*
|
|
188
|
+
* This preserves important recent context while still reducing context size
|
|
189
|
+
* for older, larger tool outputs that are less likely to be relevant.
|
|
190
|
+
*/
|
|
191
|
+
async function markToolsAsCompacted(
|
|
192
|
+
sessionId: string,
|
|
193
|
+
compactedMessageIds: string[],
|
|
194
|
+
policy: CompactionPolicy,
|
|
195
|
+
): Promise<void> {
|
|
196
|
+
const allParts = await getPartsBySession(sessionId);
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
|
|
199
|
+
// Gather eligible completed tool parts within compacted messages
|
|
200
|
+
const eligibleTools: Array<{
|
|
201
|
+
part: ToolPart;
|
|
202
|
+
outputSize: number;
|
|
203
|
+
createdAt: number;
|
|
204
|
+
}> = [];
|
|
205
|
+
|
|
206
|
+
for (const part of allParts) {
|
|
207
|
+
if (part.type !== 'tool') continue;
|
|
208
|
+
|
|
209
|
+
const toolPart = part as ToolPart;
|
|
210
|
+
|
|
211
|
+
// Skip non-completed tools
|
|
212
|
+
if (toolPart.state.status !== 'completed') continue;
|
|
213
|
+
|
|
214
|
+
// Skip tools not in compacted messages
|
|
215
|
+
if (!compactedMessageIds.includes(toolPart.messageId)) continue;
|
|
216
|
+
|
|
217
|
+
// Always protect skill tool outputs
|
|
218
|
+
if (toolPart.name === 'skill') continue;
|
|
219
|
+
|
|
220
|
+
// Estimate output size
|
|
221
|
+
const outputSize = estimateToolOutputSize((toolPart.state as { output?: unknown }).output);
|
|
222
|
+
|
|
223
|
+
// Protect small outputs below threshold
|
|
224
|
+
if (outputSize <= policy.preserveSmallToolChars) continue;
|
|
225
|
+
|
|
226
|
+
eligibleTools.push({
|
|
227
|
+
part: toolPart,
|
|
228
|
+
outputSize,
|
|
229
|
+
createdAt: part.createdAt,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Sort by createdAt descending (most recent first)
|
|
234
|
+
eligibleTools.sort((a, b) => b.createdAt - a.createdAt);
|
|
235
|
+
|
|
236
|
+
// Skip the N most recent eligible tools (preserveRecentToolCount) - they stay protected.
|
|
237
|
+
// The remainder (older tools) become candidates for pruning.
|
|
238
|
+
// Process candidates from oldest to newest to be conservative about what gets cleared.
|
|
239
|
+
const candidatesForPruning = eligibleTools
|
|
240
|
+
.slice(policy.preserveRecentToolCount)
|
|
241
|
+
.sort((a, b) => a.createdAt - b.createdAt);
|
|
242
|
+
|
|
243
|
+
// Apply maxPrunedToolCount limit - only prune up to this many tools
|
|
244
|
+
const toolsToPrune = candidatesForPruning.slice(0, policy.maxPrunedToolCount);
|
|
245
|
+
|
|
246
|
+
// Mark older/larger tools as compacted
|
|
247
|
+
for (const candidate of toolsToPrune) {
|
|
248
|
+
// Only clear tools that exceed the clear threshold
|
|
249
|
+
// (already know they exceed preserveSmallToolChars since we filtered above)
|
|
250
|
+
if (candidate.outputSize > policy.toolClearCharsThreshold) {
|
|
251
|
+
await updatePart(candidate.part.id, {
|
|
252
|
+
state: {
|
|
253
|
+
...candidate.part.state,
|
|
254
|
+
compactedAt: now,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Default implementation that uses AI SDK streamText.
|
|
263
|
+
* Uses streamText universally; required for providers like Codex/OpenAI
|
|
264
|
+
* Responses API that reject non-streaming calls.
|
|
265
|
+
*/
|
|
266
|
+
async function defaultGenerateSummary(
|
|
267
|
+
prompt: string,
|
|
268
|
+
policy: CompactionPolicy,
|
|
269
|
+
sessionId: string,
|
|
270
|
+
abortSignal?: AbortSignal,
|
|
271
|
+
): Promise<{
|
|
272
|
+
text: string;
|
|
273
|
+
usage: {
|
|
274
|
+
prompt: number;
|
|
275
|
+
completion: number;
|
|
276
|
+
cacheRead?: number;
|
|
277
|
+
cacheWrite?: number;
|
|
278
|
+
noCache?: number;
|
|
279
|
+
};
|
|
280
|
+
effectiveModelId: string;
|
|
281
|
+
effectiveProviderId: string;
|
|
282
|
+
}> {
|
|
283
|
+
const { model, omitMaxOutputTokens, providerOptions } = await getModelWithMetadata({
|
|
284
|
+
modelId: policy.modelId ?? undefined,
|
|
285
|
+
providerId: policy.providerId ?? undefined,
|
|
286
|
+
systemPrompt: prompt,
|
|
287
|
+
sessionId,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const effectiveModelId = policy.modelId || getModelsConfig().defaultModel;
|
|
291
|
+
let effectiveProviderId = policy.providerId;
|
|
292
|
+
if (!effectiveProviderId) {
|
|
293
|
+
effectiveProviderId = findProviderFromModel(effectiveModelId);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const stream = aiStreamText({
|
|
297
|
+
model,
|
|
298
|
+
prompt,
|
|
299
|
+
abortSignal,
|
|
300
|
+
maxOutputTokens: omitMaxOutputTokens ? undefined : policy.maxOutputTokens,
|
|
301
|
+
providerOptions: providerOptions as unknown as Parameters<typeof aiStreamText>[0]['providerOptions'],
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
let text: string;
|
|
305
|
+
try {
|
|
306
|
+
text = await stream.text;
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.error('[compaction] streamText failed:', err);
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const streamUsage = await stream.usage;
|
|
313
|
+
const streamFinishReason = await stream.finishReason;
|
|
314
|
+
|
|
315
|
+
if (streamFinishReason === 'length') {
|
|
316
|
+
console.warn('[compaction] Summary was truncated (hit maxOutputTokens limit). Some context may be lost.');
|
|
317
|
+
text += '\n\n[Note: Summary was truncated due to token limit. Some context may be incomplete.]';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
text,
|
|
322
|
+
usage: {
|
|
323
|
+
prompt: streamUsage.inputTokens ?? 0,
|
|
324
|
+
completion: streamUsage.outputTokens ?? 0,
|
|
325
|
+
cacheRead: streamUsage.inputTokenDetails.cacheReadTokens ?? 0,
|
|
326
|
+
cacheWrite: streamUsage.inputTokenDetails.cacheWriteTokens ?? 0,
|
|
327
|
+
noCache: streamUsage.inputTokenDetails.noCacheTokens ?? 0,
|
|
328
|
+
},
|
|
329
|
+
effectiveModelId,
|
|
330
|
+
effectiveProviderId,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Processes a compaction task from a trigger message.
|
|
336
|
+
* Creates an assistant message with the summary text.
|
|
337
|
+
*/
|
|
338
|
+
export async function processCompactionTask(
|
|
339
|
+
sessionId: string,
|
|
340
|
+
triggerMessageId: string,
|
|
341
|
+
policy: CompactionPolicy,
|
|
342
|
+
generateSummaryFn?: GenerateSummaryFn,
|
|
343
|
+
abortSignal?: AbortSignal,
|
|
344
|
+
): Promise<CompactionTaskResult> {
|
|
345
|
+
// Get the trigger message
|
|
346
|
+
const allMessages = await listMessagesWithParts(sessionId);
|
|
347
|
+
const triggerMsgWithParts = allMessages.find((m: MessageWithParts) => m.message.id === triggerMessageId);
|
|
348
|
+
|
|
349
|
+
if (!triggerMsgWithParts) {
|
|
350
|
+
throw new Error('Trigger message not found');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Get the CompactionPart to determine reason
|
|
354
|
+
const triggerPart = triggerMsgWithParts.parts.find((p: MessageWithParts['parts'][number]) => p.type === 'compaction');
|
|
355
|
+
if (!triggerPart) {
|
|
356
|
+
throw new Error('Trigger message does not have a compaction part');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const compactionPart = triggerPart as CompactionPart;
|
|
360
|
+
const reason: CompactionTriggerReason = compactionPart.overflow
|
|
361
|
+
? 'overflow'
|
|
362
|
+
: compactionPart.auto
|
|
363
|
+
? 'auto'
|
|
364
|
+
: 'manual';
|
|
365
|
+
|
|
366
|
+
// Get the trigger message's boundary (all messages before the trigger)
|
|
367
|
+
// Find the index of the trigger in the full session history
|
|
368
|
+
const triggerIdx = allMessages.findIndex((m: MessageWithParts) => m.message.id === triggerMessageId);
|
|
369
|
+
|
|
370
|
+
// Check for a previous compaction summary BEFORE the trigger to avoid re-summarizing
|
|
371
|
+
// already-compactored content. When a previous summary exists, only compact messages
|
|
372
|
+
// AFTER the summary. This prevents re-sending the entire pre-compaction history to the
|
|
373
|
+
// LLM, which is especially important for forked sessions that inherit compaction artifacts.
|
|
374
|
+
let previousSummaryText: string | null = null;
|
|
375
|
+
let compactStartIdx = 0;
|
|
376
|
+
|
|
377
|
+
for (let i = triggerIdx - 1; i >= 0; i--) {
|
|
378
|
+
const m = allMessages[i];
|
|
379
|
+
if (
|
|
380
|
+
m.message.role === 'assistant' &&
|
|
381
|
+
(m.message as AssistantMessage).summary === true &&
|
|
382
|
+
(m.message as AssistantMessage).mode === 'compaction'
|
|
383
|
+
) {
|
|
384
|
+
// Extract text from the summary
|
|
385
|
+
const textParts = m.parts.filter((p: MessageWithParts['parts'][number]) => p.type === 'text');
|
|
386
|
+
if (textParts.length > 0) {
|
|
387
|
+
previousSummaryText = textParts.map((p: MessageWithParts['parts'][number]) => (p as { text: string }).text).join('\n');
|
|
388
|
+
compactStartIdx = i + 1;
|
|
389
|
+
}
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const messagesToCompact = allMessages
|
|
395
|
+
.slice(compactStartIdx, triggerIdx)
|
|
396
|
+
.filter((m: MessageWithParts) => m.message.role !== 'system');
|
|
397
|
+
|
|
398
|
+
if (messagesToCompact.length === 0) {
|
|
399
|
+
throw new Error('No messages to compact');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Validate: there must be at least one user message to serve as a meaningful boundary.
|
|
403
|
+
// This replaces the fragile hasNestedCompaction guard.
|
|
404
|
+
const hasUserMessage = messagesToCompact.some(
|
|
405
|
+
(m: MessageWithParts) => m.message.role === 'user',
|
|
406
|
+
);
|
|
407
|
+
if (!hasUserMessage) {
|
|
408
|
+
throw new Error('Compaction boundary must contain at least one user message');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Build the prompt
|
|
412
|
+
const conversationText = buildConversationText(messagesToCompact);
|
|
413
|
+
|
|
414
|
+
const prompt = previousSummaryText
|
|
415
|
+
? COMPACTION_PROMPT_INCREMENTAL
|
|
416
|
+
.replace('{PREVIOUS_SUMMARY}', previousSummaryText)
|
|
417
|
+
.replace('{CONVERSATION}', conversationText)
|
|
418
|
+
: COMPACTION_PROMPT_FIRST.replace('{CONVERSATION}', conversationText);
|
|
419
|
+
|
|
420
|
+
console.log('[compaction] modelId:', policy.modelId, 'providerId:', policy.providerId);
|
|
421
|
+
|
|
422
|
+
const generateSummary = generateSummaryFn ?? defaultGenerateSummary;
|
|
423
|
+
const { text: summary, usage, effectiveModelId, effectiveProviderId } = await generateSummary(
|
|
424
|
+
prompt,
|
|
425
|
+
policy,
|
|
426
|
+
sessionId,
|
|
427
|
+
abortSignal,
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
const now = Date.now();
|
|
431
|
+
const msgId = randomUUID();
|
|
432
|
+
|
|
433
|
+
// Create an assistant message with summary metadata
|
|
434
|
+
// Record the effective model/provider that was actually used for generation
|
|
435
|
+
const assistantMessage: AssistantMessage = {
|
|
436
|
+
id: msgId,
|
|
437
|
+
sessionId,
|
|
438
|
+
role: 'assistant',
|
|
439
|
+
status: 'completed',
|
|
440
|
+
modelId: effectiveModelId,
|
|
441
|
+
providerId: effectiveProviderId,
|
|
442
|
+
tokens: {
|
|
443
|
+
prompt: usage.prompt,
|
|
444
|
+
completion: usage.completion,
|
|
445
|
+
cacheRead: usage.cacheRead ?? 0,
|
|
446
|
+
cacheWrite: usage.cacheWrite ?? 0,
|
|
447
|
+
noCache: usage.noCache ?? 0,
|
|
448
|
+
},
|
|
449
|
+
cost: 0,
|
|
450
|
+
summary: true,
|
|
451
|
+
mode: 'compaction',
|
|
452
|
+
parentId: triggerMessageId,
|
|
453
|
+
createdAt: now,
|
|
454
|
+
completedAt: now,
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
await createMessage(assistantMessage);
|
|
458
|
+
|
|
459
|
+
// Create a text part with the summary content
|
|
460
|
+
const textPartId = randomUUID();
|
|
461
|
+
const textPart: TextPart = {
|
|
462
|
+
id: textPartId,
|
|
463
|
+
messageId: msgId,
|
|
464
|
+
createdAt: now,
|
|
465
|
+
type: 'text',
|
|
466
|
+
text: summary,
|
|
467
|
+
};
|
|
468
|
+
await createPart(textPart, sessionId);
|
|
469
|
+
|
|
470
|
+
// Mark tool results as compacted so they can be pruned in future context
|
|
471
|
+
// WS4: Now passes policy for budget-aware pruning
|
|
472
|
+
const compactedMessageIds = messagesToCompact.map((m: MessageWithParts) => m.message.id);
|
|
473
|
+
await markToolsAsCompacted(sessionId, compactedMessageIds, policy);
|
|
474
|
+
|
|
475
|
+
const trigger: CompactionTrigger = {
|
|
476
|
+
messageId: triggerMessageId,
|
|
477
|
+
reason,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
trigger,
|
|
482
|
+
summaryMessage: assistantMessage,
|
|
483
|
+
textParts: [textPart],
|
|
484
|
+
tokensUsed: usage,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Persist a compaction failure as an append-only assistant message.
|
|
490
|
+
* Creates an assistant message with status='error', mode='compact_failed',
|
|
491
|
+
* parentId pointing to the trigger, and a text part with the error explanation.
|
|
492
|
+
* Broadcasts the failure via standard message/part events.
|
|
493
|
+
*
|
|
494
|
+
* NOTE: This should only be called AFTER a trigger has been created.
|
|
495
|
+
* If validation fails before trigger creation, do not call this function.
|
|
496
|
+
*/
|
|
497
|
+
export async function persistCompactionFailure(
|
|
498
|
+
sessionId: string,
|
|
499
|
+
triggerMessageId: string,
|
|
500
|
+
errorMessage: string,
|
|
501
|
+
broadcast: BroadcastFn = emitRuntimeEvent,
|
|
502
|
+
): Promise<void> {
|
|
503
|
+
const now = Date.now();
|
|
504
|
+
const msgId = randomUUID();
|
|
505
|
+
|
|
506
|
+
const assistantMessage: AssistantMessage = {
|
|
507
|
+
id: msgId,
|
|
508
|
+
sessionId,
|
|
509
|
+
role: 'assistant',
|
|
510
|
+
status: 'error',
|
|
511
|
+
modelId: '',
|
|
512
|
+
providerId: '',
|
|
513
|
+
tokens: {
|
|
514
|
+
prompt: 0,
|
|
515
|
+
completion: 0,
|
|
516
|
+
},
|
|
517
|
+
cost: 0,
|
|
518
|
+
mode: 'compact_failed',
|
|
519
|
+
parentId: triggerMessageId,
|
|
520
|
+
createdAt: now,
|
|
521
|
+
completedAt: now,
|
|
522
|
+
error: errorMessage,
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
await createMessage(assistantMessage);
|
|
526
|
+
|
|
527
|
+
const textPartId = randomUUID();
|
|
528
|
+
const textPart: TextPart = {
|
|
529
|
+
id: textPartId,
|
|
530
|
+
messageId: msgId,
|
|
531
|
+
createdAt: now,
|
|
532
|
+
type: 'text',
|
|
533
|
+
text: `Compaction failed: ${errorMessage}`,
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
await createPart(textPart, sessionId);
|
|
537
|
+
|
|
538
|
+
broadcast({ kind: 'message', action: 'created', message: assistantMessage });
|
|
539
|
+
broadcast({ kind: 'part', action: 'created', sessionId, part: textPart });
|
|
540
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface ModelCapabilities {
|
|
2
|
+
input?: {
|
|
3
|
+
text?: boolean;
|
|
4
|
+
image?: boolean;
|
|
5
|
+
video?: boolean;
|
|
6
|
+
file?: string[];
|
|
7
|
+
};
|
|
8
|
+
structuredOutput?: { mode: 'native' | 'prompt' };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ModelDefinition {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
contextWindow: number;
|
|
15
|
+
maxOutputTokens?: number;
|
|
16
|
+
tier: 'budget' | 'standard' | 'premium';
|
|
17
|
+
variants?: Record<string, { providerOptions: Record<string, unknown> }>;
|
|
18
|
+
capabilities?: ModelCapabilities;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ProviderDefinition {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
models: ModelDefinition[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ModelWithProvider extends ModelDefinition {
|
|
28
|
+
providerId: string;
|
|
29
|
+
providerName: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ModelsConfig {
|
|
33
|
+
providers: ProviderDefinition[];
|
|
34
|
+
defaultModel: string;
|
|
35
|
+
defaultProvider: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RuntimeConfiguration {
|
|
39
|
+
findModel(modelId: string, providerId?: string): ModelWithProvider | undefined;
|
|
40
|
+
getMaxOutputTokens(modelId?: string): number;
|
|
41
|
+
findModelVariant(modelId: string, variantKey: string, providerId?: string): Record<string, unknown> | undefined;
|
|
42
|
+
getModelsConfig(): ModelsConfig;
|
|
43
|
+
getLLMTemperature(): number;
|
|
44
|
+
getLLMMaxSteps(): number;
|
|
45
|
+
getLLMSubagentMaxSteps(): number;
|
|
46
|
+
getLLMBaseUrl(): string | undefined;
|
|
47
|
+
getApiKey(providerId: string): string | undefined;
|
|
48
|
+
getCompactionModel(): string | undefined;
|
|
49
|
+
getCompactionProvider(): string | undefined;
|
|
50
|
+
getCompactionMaxTokens(): number;
|
|
51
|
+
getCompactionPreserveRecentToolCount(): number;
|
|
52
|
+
getCompactionPreserveSmallToolChars(): number;
|
|
53
|
+
getCompactionToolClearCharsThreshold(): number;
|
|
54
|
+
getCompactionMaxPrunedToolCount(): number;
|
|
55
|
+
getCompactionAutoThresholdRatio(): number;
|
|
56
|
+
getCompactionAutoReserveCapTokens(): number;
|
|
57
|
+
getCompactionAutoSafetyMarginTokens(): number;
|
|
58
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RuntimeConfiguration } from './contracts';
|
|
2
|
+
|
|
3
|
+
const OUTPUT_TOKEN_MAX = 32000;
|
|
4
|
+
|
|
5
|
+
export function createDefaultRuntimeConfiguration(): RuntimeConfiguration {
|
|
6
|
+
return {
|
|
7
|
+
findModel: () => undefined,
|
|
8
|
+
getMaxOutputTokens: () => OUTPUT_TOKEN_MAX,
|
|
9
|
+
findModelVariant: () => undefined,
|
|
10
|
+
getModelsConfig: () => ({ providers: [], defaultModel: '', defaultProvider: '' }),
|
|
11
|
+
getLLMTemperature: () => 0.7,
|
|
12
|
+
getLLMMaxSteps: () => 10,
|
|
13
|
+
getLLMSubagentMaxSteps: () => 50,
|
|
14
|
+
getLLMBaseUrl: () => undefined,
|
|
15
|
+
getApiKey: () => undefined,
|
|
16
|
+
getCompactionModel: () => undefined,
|
|
17
|
+
getCompactionProvider: () => undefined,
|
|
18
|
+
getCompactionMaxTokens: () => 8000,
|
|
19
|
+
getCompactionPreserveRecentToolCount: () => 3,
|
|
20
|
+
getCompactionPreserveSmallToolChars: () => 200,
|
|
21
|
+
getCompactionToolClearCharsThreshold: () => 1000,
|
|
22
|
+
getCompactionMaxPrunedToolCount: () => 50,
|
|
23
|
+
getCompactionAutoThresholdRatio: () => 0.75,
|
|
24
|
+
getCompactionAutoReserveCapTokens: () => 32000,
|
|
25
|
+
getCompactionAutoSafetyMarginTokens: () => 20000,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import type { RuntimeConfiguration } from './contracts';
|
|
3
|
+
import { createDefaultRuntimeConfiguration } from './defaults';
|
|
4
|
+
|
|
5
|
+
let configuration = createDefaultRuntimeConfiguration();
|
|
6
|
+
const scopedConfiguration = new AsyncLocalStorage<RuntimeConfiguration>();
|
|
7
|
+
|
|
8
|
+
function activeConfiguration(): RuntimeConfiguration {
|
|
9
|
+
return scopedConfiguration.getStore() ?? configuration;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function withRuntimeConfiguration<T>(value: RuntimeConfiguration, callback: () => T): T {
|
|
13
|
+
return scopedConfiguration.run(value, callback);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function configureRuntimeConfiguration(value?: RuntimeConfiguration): void {
|
|
17
|
+
configuration = value ?? createDefaultRuntimeConfiguration();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getRuntimeConfiguration(): RuntimeConfiguration {
|
|
21
|
+
return activeConfiguration();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const findModel = (...args: Parameters<RuntimeConfiguration['findModel']>) => activeConfiguration().findModel(...args);
|
|
25
|
+
export const getMaxOutputTokens = (...args: Parameters<RuntimeConfiguration['getMaxOutputTokens']>) => activeConfiguration().getMaxOutputTokens(...args);
|
|
26
|
+
export const findModelVariant = (...args: Parameters<RuntimeConfiguration['findModelVariant']>) => activeConfiguration().findModelVariant(...args);
|
|
27
|
+
export const getModelsConfig = () => activeConfiguration().getModelsConfig();
|
|
28
|
+
export const getLLMTemperature = () => activeConfiguration().getLLMTemperature();
|
|
29
|
+
export const getLLMMaxSteps = () => activeConfiguration().getLLMMaxSteps();
|
|
30
|
+
export const getLLMSubagentMaxSteps = () => activeConfiguration().getLLMSubagentMaxSteps();
|
|
31
|
+
export const getLLMBaseUrl = () => activeConfiguration().getLLMBaseUrl();
|
|
32
|
+
export const getApiKeyForProvider = (providerId: string) => activeConfiguration().getApiKey(providerId);
|
|
33
|
+
export const getCompactionModel = () => activeConfiguration().getCompactionModel();
|
|
34
|
+
export const getCompactionProvider = () => activeConfiguration().getCompactionProvider();
|
|
35
|
+
export const getCompactionMaxTokens = () => activeConfiguration().getCompactionMaxTokens();
|
|
36
|
+
export const getCompactionPreserveRecentToolCount = () => activeConfiguration().getCompactionPreserveRecentToolCount();
|
|
37
|
+
export const getCompactionPreserveSmallToolChars = () => activeConfiguration().getCompactionPreserveSmallToolChars();
|
|
38
|
+
export const getCompactionToolClearCharsThreshold = () => activeConfiguration().getCompactionToolClearCharsThreshold();
|
|
39
|
+
export const getCompactionMaxPrunedToolCount = () => activeConfiguration().getCompactionMaxPrunedToolCount();
|
|
40
|
+
export const getCompactionAutoThresholdRatio = () => activeConfiguration().getCompactionAutoThresholdRatio();
|
|
41
|
+
export const getCompactionAutoReserveCapTokens = () => activeConfiguration().getCompactionAutoReserveCapTokens();
|
|
42
|
+
export const getCompactionAutoSafetyMarginTokens = () => activeConfiguration().getCompactionAutoSafetyMarginTokens();
|