@germanescobar/anita 0.3.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 +353 -0
- package/dist/agent/agents.d.ts +16 -0
- package/dist/agent/agents.js +115 -0
- package/dist/agent/context-budget.d.ts +7 -0
- package/dist/agent/context-budget.js +17 -0
- package/dist/agent/context-builder.d.ts +34 -0
- package/dist/agent/context-builder.js +175 -0
- package/dist/agent/executor.d.ts +13 -0
- package/dist/agent/executor.js +65 -0
- package/dist/agent/loop.d.ts +54 -0
- package/dist/agent/loop.js +548 -0
- package/dist/agent/policies.d.ts +25 -0
- package/dist/agent/policies.js +177 -0
- package/dist/agent/session.d.ts +12 -0
- package/dist/agent/session.js +42 -0
- package/dist/attachments.d.ts +3 -0
- package/dist/attachments.js +73 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.js +327 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/models/anthropic.d.ts +15 -0
- package/dist/models/anthropic.js +195 -0
- package/dist/models/openai-responses.d.ts +62 -0
- package/dist/models/openai-responses.js +377 -0
- package/dist/models/openai.d.ts +32 -0
- package/dist/models/openai.js +330 -0
- package/dist/models/provider.d.ts +33 -0
- package/dist/models/provider.js +1 -0
- package/dist/models/resolve.d.ts +48 -0
- package/dist/models/resolve.js +211 -0
- package/dist/security/sensitive-content.d.ts +6 -0
- package/dist/security/sensitive-content.js +59 -0
- package/dist/skills/skills.d.ts +62 -0
- package/dist/skills/skills.js +371 -0
- package/dist/storage/event-store.d.ts +7 -0
- package/dist/storage/event-store.js +36 -0
- package/dist/storage/session-store.d.ts +11 -0
- package/dist/storage/session-store.js +64 -0
- package/dist/tools/delete-file.d.ts +2 -0
- package/dist/tools/delete-file.js +25 -0
- package/dist/tools/edit-file.d.ts +2 -0
- package/dist/tools/edit-file.js +50 -0
- package/dist/tools/read-file.d.ts +2 -0
- package/dist/tools/read-file.js +122 -0
- package/dist/tools/registry.d.ts +9 -0
- package/dist/tools/registry.js +122 -0
- package/dist/tools/run-command.d.ts +2 -0
- package/dist/tools/run-command.js +103 -0
- package/dist/tools/write-file.d.ts +2 -0
- package/dist/tools/write-file.js +29 -0
- package/dist/types/agent.d.ts +44 -0
- package/dist/types/agent.js +1 -0
- package/dist/types/conversation.d.ts +43 -0
- package/dist/types/conversation.js +201 -0
- package/dist/types/events.d.ts +8 -0
- package/dist/types/events.js +1 -0
- package/dist/types/messages.d.ts +39 -0
- package/dist/types/messages.js +1 -0
- package/dist/types/output.d.ts +19 -0
- package/dist/types/output.js +1 -0
- package/dist/types/stream.d.ts +55 -0
- package/dist/types/stream.js +1 -0
- package/dist/types/tools.d.ts +28 -0
- package/dist/types/tools.js +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { getModelContextWindowTokens } from "../models/resolve.js";
|
|
3
|
+
import { contentBlocksToConversationItems, conversationItemToText, conversationItemsToMessages, messagesToConversationItems, } from "../types/conversation.js";
|
|
4
|
+
const MAX_ITERATIONS = 300;
|
|
5
|
+
const COMPACTION_SUMMARY_HEADER = "Previous conversation summary:";
|
|
6
|
+
const COMPACTION_SUMMARIZER_SYSTEM_PROMPT = `You update a rolling summary for an AI coding agent conversation.
|
|
7
|
+
|
|
8
|
+
Preserve durable facts, user intent, decisions, constraints, files changed or inspected, tool results that matter, errors, and unresolved next steps.
|
|
9
|
+
Remove repetition, incidental chatter, and details that no longer affect future work.
|
|
10
|
+
Return only the updated summary text.`;
|
|
11
|
+
export const DEFAULT_CONTEXT_BUDGET = {
|
|
12
|
+
compactAtRatio: 0.8,
|
|
13
|
+
reservedResponseTokens: 16_000,
|
|
14
|
+
keepRecentTokens: 24_000,
|
|
15
|
+
minSummarizableTokens: 8_000,
|
|
16
|
+
targetSummaryTokens: 3_000,
|
|
17
|
+
};
|
|
18
|
+
export class AgentLoop {
|
|
19
|
+
provider;
|
|
20
|
+
executor;
|
|
21
|
+
contextBuilder;
|
|
22
|
+
registry;
|
|
23
|
+
eventStore;
|
|
24
|
+
sessionStore;
|
|
25
|
+
streamJson;
|
|
26
|
+
contextBudget;
|
|
27
|
+
pendingTerminalDelta = false;
|
|
28
|
+
constructor(provider, executor, contextBuilder, registry, eventStore, sessionStore, streamJson = false, contextBudget = DEFAULT_CONTEXT_BUDGET) {
|
|
29
|
+
this.provider = provider;
|
|
30
|
+
this.executor = executor;
|
|
31
|
+
this.contextBuilder = contextBuilder;
|
|
32
|
+
this.registry = registry;
|
|
33
|
+
this.eventStore = eventStore;
|
|
34
|
+
this.sessionStore = sessionStore;
|
|
35
|
+
this.streamJson = streamJson;
|
|
36
|
+
this.contextBudget = contextBudget;
|
|
37
|
+
}
|
|
38
|
+
async run(session, userMessage, attachments = [], signal) {
|
|
39
|
+
try {
|
|
40
|
+
this.normalizeSession(session);
|
|
41
|
+
session.conversationItems.push({
|
|
42
|
+
type: "message",
|
|
43
|
+
role: "user",
|
|
44
|
+
content: userMessage,
|
|
45
|
+
contentFormat: attachments.length > 0 ? "block" : undefined,
|
|
46
|
+
});
|
|
47
|
+
session.conversationItems.push(...attachments.map((attachment) => ({
|
|
48
|
+
type: "attachment",
|
|
49
|
+
role: "user",
|
|
50
|
+
attachment,
|
|
51
|
+
})));
|
|
52
|
+
if (!session.title) {
|
|
53
|
+
session.title = this.generateTitle(userMessage);
|
|
54
|
+
}
|
|
55
|
+
await this.eventStore.append(session.id, "user_message", {
|
|
56
|
+
text: userMessage,
|
|
57
|
+
attachments: attachments.map((attachment) => ({
|
|
58
|
+
type: attachment.type,
|
|
59
|
+
name: attachment.name,
|
|
60
|
+
mediaType: attachment.type === "file"
|
|
61
|
+
? attachment.mediaType
|
|
62
|
+
: attachment.source.type === "data"
|
|
63
|
+
? attachment.source.mediaType
|
|
64
|
+
: undefined,
|
|
65
|
+
sourceType: attachment.source.type,
|
|
66
|
+
})),
|
|
67
|
+
});
|
|
68
|
+
await this.saveSession(session);
|
|
69
|
+
const systemPrompt = this.contextBuilder.buildSystemPrompt();
|
|
70
|
+
const tools = this.registry.toSchemas();
|
|
71
|
+
let finalStopReason = "max_iterations";
|
|
72
|
+
let status = "max_iterations";
|
|
73
|
+
this.emit({
|
|
74
|
+
type: "run.started",
|
|
75
|
+
sessionId: session.id,
|
|
76
|
+
model: session.model,
|
|
77
|
+
workingDirectory: session.workingDirectory,
|
|
78
|
+
timestamp: new Date().toISOString(),
|
|
79
|
+
});
|
|
80
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
81
|
+
if (signal?.aborted) {
|
|
82
|
+
await this.handleCancellation(session);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const modelContextItems = await this.buildModelContextItems(session);
|
|
86
|
+
const conversationItems = await this.contextBuilder.buildItemsWithDynamicContext(modelContextItems);
|
|
87
|
+
const response = await this.getModelResponse({
|
|
88
|
+
systemPrompt,
|
|
89
|
+
sessionId: session.id,
|
|
90
|
+
conversationItems,
|
|
91
|
+
messages: conversationItemsToMessages(conversationItems),
|
|
92
|
+
tools,
|
|
93
|
+
signal,
|
|
94
|
+
});
|
|
95
|
+
if (response.reasoning) {
|
|
96
|
+
await this.eventStore.append(session.id, "assistant_reasoning", {
|
|
97
|
+
text: response.reasoning,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
await this.eventStore.append(session.id, "assistant_response", {
|
|
101
|
+
stopReason: response.stopReason,
|
|
102
|
+
content: response.content,
|
|
103
|
+
reasoning: response.reasoning,
|
|
104
|
+
usage: response.usage,
|
|
105
|
+
});
|
|
106
|
+
this.updateContextBudget(session, response.usage);
|
|
107
|
+
const assistantItems = contentBlocksToConversationItems(response.content, response.reasoning, response.reasoningItems);
|
|
108
|
+
if (response.reasoning && !response.streamed) {
|
|
109
|
+
this.emit({ type: "assistant.reasoning", text: response.reasoning });
|
|
110
|
+
}
|
|
111
|
+
// Print any text blocks
|
|
112
|
+
if (!response.streamed) {
|
|
113
|
+
for (const block of response.content) {
|
|
114
|
+
if (block.type === "text") {
|
|
115
|
+
this.emit({ type: "assistant.text", text: block.text });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// If no tool use, we're done
|
|
120
|
+
finalStopReason = response.stopReason;
|
|
121
|
+
if (response.stopReason !== "tool_use") {
|
|
122
|
+
session.conversationItems.push(...assistantItems);
|
|
123
|
+
await this.saveSession(session);
|
|
124
|
+
status = "completed";
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
// Execute tool calls
|
|
128
|
+
const toolUseBlocks = response.content.filter((b) => b.type === "tool_use");
|
|
129
|
+
const resultBlocks = [];
|
|
130
|
+
for (let toolIndex = 0; toolIndex < toolUseBlocks.length; toolIndex++) {
|
|
131
|
+
if (signal?.aborted) {
|
|
132
|
+
await this.handleCancellation(session);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const toolUse = toolUseBlocks[toolIndex];
|
|
136
|
+
this.emit({
|
|
137
|
+
type: "tool.call",
|
|
138
|
+
id: toolUse.id,
|
|
139
|
+
name: toolUse.name,
|
|
140
|
+
input: toolUse.input,
|
|
141
|
+
});
|
|
142
|
+
let result;
|
|
143
|
+
try {
|
|
144
|
+
result = await this.executor.executeTool(session.id, {
|
|
145
|
+
id: toolUse.id,
|
|
146
|
+
name: toolUse.name,
|
|
147
|
+
input: toolUse.input,
|
|
148
|
+
}, { signal });
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
if (signal?.aborted || isAbortError(err)) {
|
|
152
|
+
await this.handleCancellation(session);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
resultBlocks.push(this.createErrorToolResult(toolUse, `Tool "${toolUse.name}" failed before returning a result: ${err instanceof Error ? err.message : String(err)}`));
|
|
156
|
+
for (const skippedToolUse of toolUseBlocks.slice(toolIndex + 1)) {
|
|
157
|
+
resultBlocks.push(this.createErrorToolResult(skippedToolUse, `Tool "${skippedToolUse.name}" was not executed because a previous tool failed.`));
|
|
158
|
+
}
|
|
159
|
+
session.conversationItems.push(...assistantItems);
|
|
160
|
+
session.conversationItems.push(...contentBlocksToConversationItems(resultBlocks));
|
|
161
|
+
await this.saveSession(session);
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
// A tool may return a cancelled result rather than throwing (e.g.
|
|
165
|
+
// run_command). Check again before persisting so the session stays
|
|
166
|
+
// at its last coherent point on cancellation.
|
|
167
|
+
if (signal?.aborted) {
|
|
168
|
+
await this.handleCancellation(session);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.emit({
|
|
172
|
+
type: "tool.result",
|
|
173
|
+
id: toolUse.id,
|
|
174
|
+
name: toolUse.name,
|
|
175
|
+
content: result.content,
|
|
176
|
+
isError: Boolean(result.isError),
|
|
177
|
+
metadata: result.metadata,
|
|
178
|
+
});
|
|
179
|
+
resultBlocks.push({
|
|
180
|
+
type: "tool_result",
|
|
181
|
+
toolUseId: toolUse.id,
|
|
182
|
+
content: result.content,
|
|
183
|
+
isError: result.isError,
|
|
184
|
+
metadata: result.metadata,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// Append tool results as user message
|
|
188
|
+
session.conversationItems.push(...assistantItems);
|
|
189
|
+
session.conversationItems.push(...contentBlocksToConversationItems(resultBlocks));
|
|
190
|
+
await this.saveSession(session);
|
|
191
|
+
}
|
|
192
|
+
if (status === "max_iterations") {
|
|
193
|
+
throw new Error(`Agent stopped after ${MAX_ITERATIONS} iterations before producing a final response. Last stop reason: ${finalStopReason}.`);
|
|
194
|
+
}
|
|
195
|
+
await this.saveSession(session);
|
|
196
|
+
this.emit({
|
|
197
|
+
type: "run.completed",
|
|
198
|
+
sessionId: session.id,
|
|
199
|
+
status,
|
|
200
|
+
stopReason: finalStopReason,
|
|
201
|
+
timestamp: new Date().toISOString(),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
if (signal?.aborted || isAbortError(err)) {
|
|
206
|
+
await this.handleCancellation(session);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
await this.saveSession(session);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// Preserve the original run failure.
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
await this.eventStore.append(session.id, "error", {
|
|
217
|
+
message: err instanceof Error ? err.message : String(err),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Preserve the original run failure.
|
|
222
|
+
}
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Record a coherent cancellation: persist the session at its last consistent
|
|
228
|
+
* point (no partial assistant turn or tool batch is appended), log the event,
|
|
229
|
+
* and notify the stream.
|
|
230
|
+
*/
|
|
231
|
+
async handleCancellation(session) {
|
|
232
|
+
try {
|
|
233
|
+
await this.saveSession(session);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// Preserve cancellation handling even if the save fails.
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
await this.eventStore.append(session.id, "run_cancelled", {
|
|
240
|
+
reason: "user_interrupt",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Preserve cancellation handling even if the append fails.
|
|
245
|
+
}
|
|
246
|
+
this.emit({
|
|
247
|
+
type: "run.cancelled",
|
|
248
|
+
sessionId: session.id,
|
|
249
|
+
reason: "user_interrupt",
|
|
250
|
+
timestamp: new Date().toISOString(),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
generateTitle(message) {
|
|
254
|
+
const firstLine = message.split('\n')[0].trim();
|
|
255
|
+
if (!firstLine)
|
|
256
|
+
return 'Chat session';
|
|
257
|
+
if (firstLine.length <= 72)
|
|
258
|
+
return firstLine;
|
|
259
|
+
return firstLine.slice(0, 69) + '...';
|
|
260
|
+
}
|
|
261
|
+
async saveSession(session) {
|
|
262
|
+
this.normalizeSession(session);
|
|
263
|
+
session.lastActiveAt = new Date().toISOString();
|
|
264
|
+
await this.sessionStore.save(session);
|
|
265
|
+
}
|
|
266
|
+
async buildModelContextItems(session) {
|
|
267
|
+
this.normalizeSession(session);
|
|
268
|
+
const thresholdTokens = this.getCompactionThresholdTokens(session.model);
|
|
269
|
+
this.updateContextBudget(session);
|
|
270
|
+
const existingSummary = session.contextBudget?.compactionSummary;
|
|
271
|
+
const summarizedItemCount = Math.min(session.contextBudget?.summarizedItemCount ?? 0, session.conversationItems.length);
|
|
272
|
+
const currentModelItems = this.buildCompactedItems(existingSummary, session.conversationItems.slice(summarizedItemCount));
|
|
273
|
+
const beforeTokens = this.estimateConversationTokens(currentModelItems);
|
|
274
|
+
if (beforeTokens <= thresholdTokens)
|
|
275
|
+
return currentModelItems;
|
|
276
|
+
const desiredSplitIndex = this.findRecentTailSplitIndex(session.conversationItems, this.contextBudget.keepRecentTokens);
|
|
277
|
+
const splitIndex = this.findSafeCompactionSplitIndex(session.conversationItems, desiredSplitIndex);
|
|
278
|
+
const nextItemsToSummarize = session.conversationItems.slice(summarizedItemCount, splitIndex);
|
|
279
|
+
const summarizableTokens = this.estimateConversationTokens(nextItemsToSummarize);
|
|
280
|
+
if (splitIndex <= summarizedItemCount ||
|
|
281
|
+
summarizableTokens < this.contextBudget.minSummarizableTokens) {
|
|
282
|
+
await this.appendCompactionSkipEvent(session, beforeTokens, currentModelItems, "eligible_prefix_too_small");
|
|
283
|
+
return currentModelItems;
|
|
284
|
+
}
|
|
285
|
+
const summary = await this.generateCompactionSummary(existingSummary, nextItemsToSummarize);
|
|
286
|
+
const recentItems = session.conversationItems.slice(splitIndex);
|
|
287
|
+
const compactedItems = this.buildCompactedItems(summary, recentItems);
|
|
288
|
+
const afterTokens = this.estimateConversationTokens(compactedItems);
|
|
289
|
+
if (afterTokens >= beforeTokens) {
|
|
290
|
+
await this.appendCompactionSkipEvent(session, beforeTokens, currentModelItems, "no_meaningful_reduction");
|
|
291
|
+
return currentModelItems;
|
|
292
|
+
}
|
|
293
|
+
const summaryTokens = this.estimateConversationTokens([
|
|
294
|
+
{ type: "compaction_summary", summary },
|
|
295
|
+
]);
|
|
296
|
+
const preservedRecentTokens = this.estimateConversationTokens(recentItems);
|
|
297
|
+
const compactedAt = new Date().toISOString();
|
|
298
|
+
session.contextBudget = {
|
|
299
|
+
...session.contextBudget,
|
|
300
|
+
approximateTokens: this.estimateConversationTokens(session.conversationItems),
|
|
301
|
+
thresholdTokens,
|
|
302
|
+
compactAtRatio: this.contextBudget.compactAtRatio,
|
|
303
|
+
reservedResponseTokens: this.contextBudget.reservedResponseTokens,
|
|
304
|
+
keepRecentTokens: this.contextBudget.keepRecentTokens,
|
|
305
|
+
minSummarizableTokens: this.contextBudget.minSummarizableTokens,
|
|
306
|
+
targetSummaryTokens: this.contextBudget.targetSummaryTokens,
|
|
307
|
+
preservedRecentTokens,
|
|
308
|
+
summaryTokens,
|
|
309
|
+
compactionSummary: summary,
|
|
310
|
+
summarizedItemCount: splitIndex,
|
|
311
|
+
compactedAt,
|
|
312
|
+
};
|
|
313
|
+
await this.eventStore.append(session.id, "conversation_compaction", {
|
|
314
|
+
beforeApproximateTokens: beforeTokens,
|
|
315
|
+
afterApproximateTokens: afterTokens,
|
|
316
|
+
summarizedMessages: nextItemsToSummarize.length,
|
|
317
|
+
preservedRecentTokens,
|
|
318
|
+
summaryTokens,
|
|
319
|
+
});
|
|
320
|
+
await this.saveSession(session);
|
|
321
|
+
return compactedItems;
|
|
322
|
+
}
|
|
323
|
+
updateContextBudget(session, usage) {
|
|
324
|
+
const thresholdTokens = this.getCompactionThresholdTokens(session.model);
|
|
325
|
+
session.contextBudget = {
|
|
326
|
+
...session.contextBudget,
|
|
327
|
+
approximateTokens: this.estimateConversationTokens(session.conversationItems),
|
|
328
|
+
thresholdTokens,
|
|
329
|
+
compactAtRatio: this.contextBudget.compactAtRatio,
|
|
330
|
+
reservedResponseTokens: this.contextBudget.reservedResponseTokens,
|
|
331
|
+
keepRecentTokens: this.contextBudget.keepRecentTokens,
|
|
332
|
+
minSummarizableTokens: this.contextBudget.minSummarizableTokens,
|
|
333
|
+
targetSummaryTokens: this.contextBudget.targetSummaryTokens,
|
|
334
|
+
lastProviderUsage: usage ?? session.contextBudget?.lastProviderUsage,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
estimateConversationTokens(items) {
|
|
338
|
+
const characters = items.reduce((total, item) => total + conversationItemToText(item).length, 0);
|
|
339
|
+
return Math.ceil(characters / 4);
|
|
340
|
+
}
|
|
341
|
+
async generateCompactionSummary(existingSummary, items) {
|
|
342
|
+
const priorSummary = existingSummary
|
|
343
|
+
? `Existing rolling summary:\n${existingSummary}`
|
|
344
|
+
: "Existing rolling summary: none";
|
|
345
|
+
const transcript = items
|
|
346
|
+
.map((item) => `- ${item.type}: ${conversationItemToText(item)}`)
|
|
347
|
+
.join("\n");
|
|
348
|
+
const response = await this.provider.chat({
|
|
349
|
+
systemPrompt: `${COMPACTION_SUMMARIZER_SYSTEM_PROMPT}\n\nTarget about ${this.contextBudget.targetSummaryTokens} tokens.`,
|
|
350
|
+
conversationItems: [
|
|
351
|
+
{
|
|
352
|
+
type: "message",
|
|
353
|
+
role: "user",
|
|
354
|
+
content: `${priorSummary}\n\nNew transcript segment to fold into the rolling summary:\n${transcript}`,
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
messages: [
|
|
358
|
+
{
|
|
359
|
+
role: "user",
|
|
360
|
+
content: `${priorSummary}\n\nNew transcript segment to fold into the rolling summary:\n${transcript}`,
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
tools: [],
|
|
364
|
+
});
|
|
365
|
+
const summary = response.content
|
|
366
|
+
.filter((block) => block.type === "text")
|
|
367
|
+
.map((block) => block.text.trim())
|
|
368
|
+
.filter(Boolean)
|
|
369
|
+
.join("\n\n");
|
|
370
|
+
if (!summary) {
|
|
371
|
+
throw new Error("Compaction summary model response did not include text.");
|
|
372
|
+
}
|
|
373
|
+
return summary.startsWith(COMPACTION_SUMMARY_HEADER)
|
|
374
|
+
? summary
|
|
375
|
+
: `${COMPACTION_SUMMARY_HEADER}\n${summary}`;
|
|
376
|
+
}
|
|
377
|
+
buildCompactedItems(summary, recentItems) {
|
|
378
|
+
if (!summary)
|
|
379
|
+
return recentItems;
|
|
380
|
+
return [{ type: "compaction_summary", summary }, ...recentItems];
|
|
381
|
+
}
|
|
382
|
+
findRecentTailSplitIndex(items, keepRecentTokens) {
|
|
383
|
+
let recentTokens = 0;
|
|
384
|
+
for (let index = items.length; index > 0; index--) {
|
|
385
|
+
const itemTokens = this.estimateConversationTokens([items[index - 1]]);
|
|
386
|
+
if (recentTokens > 0 && recentTokens + itemTokens > keepRecentTokens) {
|
|
387
|
+
return index;
|
|
388
|
+
}
|
|
389
|
+
recentTokens += itemTokens;
|
|
390
|
+
}
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
393
|
+
getCompactionThresholdTokens(model) {
|
|
394
|
+
const contextWindowTokens = getModelContextWindowTokens(model);
|
|
395
|
+
const usableTokens = Math.max(1, contextWindowTokens - this.contextBudget.reservedResponseTokens);
|
|
396
|
+
return Math.floor(usableTokens * this.contextBudget.compactAtRatio);
|
|
397
|
+
}
|
|
398
|
+
async appendCompactionSkipEvent(session, beforeTokens, modelItems, skipReason) {
|
|
399
|
+
await this.eventStore.append(session.id, "conversation_compaction", {
|
|
400
|
+
beforeApproximateTokens: beforeTokens,
|
|
401
|
+
afterApproximateTokens: this.estimateConversationTokens(modelItems),
|
|
402
|
+
summarizedMessages: 0,
|
|
403
|
+
preservedRecentTokens: this.estimateConversationTokens(modelItems),
|
|
404
|
+
summaryTokens: session.contextBudget?.summaryTokens ?? 0,
|
|
405
|
+
skipReason,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
findSafeCompactionSplitIndex(items, desiredSplitIndex) {
|
|
409
|
+
let splitIndex = Math.max(0, Math.min(items.length, desiredSplitIndex));
|
|
410
|
+
while (splitIndex > 0 &&
|
|
411
|
+
this.startsInsideFunctionBatch(items, splitIndex)) {
|
|
412
|
+
splitIndex--;
|
|
413
|
+
}
|
|
414
|
+
return splitIndex;
|
|
415
|
+
}
|
|
416
|
+
startsInsideFunctionBatch(items, splitIndex) {
|
|
417
|
+
const item = items[splitIndex];
|
|
418
|
+
if (!item || !this.isFunctionBatchItem(item))
|
|
419
|
+
return false;
|
|
420
|
+
const previous = items[splitIndex - 1];
|
|
421
|
+
return Boolean(previous && this.isFunctionBatchItem(previous));
|
|
422
|
+
}
|
|
423
|
+
isFunctionBatchItem(item) {
|
|
424
|
+
return (item.type === "reasoning" ||
|
|
425
|
+
item.type === "function_call" ||
|
|
426
|
+
item.type === "function_output");
|
|
427
|
+
}
|
|
428
|
+
createErrorToolResult(toolUse, content) {
|
|
429
|
+
return {
|
|
430
|
+
type: "tool_result",
|
|
431
|
+
toolUseId: toolUse.id,
|
|
432
|
+
content,
|
|
433
|
+
isError: true,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
normalizeSession(session) {
|
|
437
|
+
session.conversationItems ??= [];
|
|
438
|
+
if (session.conversationItems.length === 0 && session.messages.length > 0) {
|
|
439
|
+
session.conversationItems = messagesToConversationItems(session.messages);
|
|
440
|
+
}
|
|
441
|
+
session.messages = conversationItemsToMessages(session.conversationItems);
|
|
442
|
+
}
|
|
443
|
+
async getModelResponse(params) {
|
|
444
|
+
if (!this.provider.streamChat) {
|
|
445
|
+
return this.provider.chat(params);
|
|
446
|
+
}
|
|
447
|
+
let response;
|
|
448
|
+
for await (const event of this.provider.streamChat(params)) {
|
|
449
|
+
if (event.type === "response") {
|
|
450
|
+
response = event.response;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
this.emitModelStreamEvent(event);
|
|
454
|
+
}
|
|
455
|
+
if (!response) {
|
|
456
|
+
throw new Error("Streaming model provider completed without a final response.");
|
|
457
|
+
}
|
|
458
|
+
return { ...response, streamed: true };
|
|
459
|
+
}
|
|
460
|
+
emitModelStreamEvent(event) {
|
|
461
|
+
switch (event.type) {
|
|
462
|
+
case "assistant_text_delta":
|
|
463
|
+
this.emit({ type: "assistant.text.delta", text: event.text });
|
|
464
|
+
return;
|
|
465
|
+
case "assistant_reasoning_delta":
|
|
466
|
+
this.emit({ type: "assistant.reasoning.delta", text: event.text });
|
|
467
|
+
return;
|
|
468
|
+
case "tool_call_delta":
|
|
469
|
+
this.emit({
|
|
470
|
+
type: "tool.call.delta",
|
|
471
|
+
index: event.index,
|
|
472
|
+
id: event.id,
|
|
473
|
+
name: event.name,
|
|
474
|
+
inputDelta: event.inputDelta,
|
|
475
|
+
});
|
|
476
|
+
return;
|
|
477
|
+
case "response":
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
emit(event) {
|
|
482
|
+
if (this.streamJson) {
|
|
483
|
+
console.log(JSON.stringify(event));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
switch (event.type) {
|
|
487
|
+
case "run.started":
|
|
488
|
+
return;
|
|
489
|
+
case "run.completed":
|
|
490
|
+
case "run.failed":
|
|
491
|
+
this.finishPendingTerminalDelta();
|
|
492
|
+
return;
|
|
493
|
+
case "run.cancelled":
|
|
494
|
+
this.finishPendingTerminalDelta();
|
|
495
|
+
console.log(chalk.yellow("Run cancelled."));
|
|
496
|
+
return;
|
|
497
|
+
case "assistant.text.delta":
|
|
498
|
+
process.stdout.write(chalk.cyan(event.text));
|
|
499
|
+
this.pendingTerminalDelta = true;
|
|
500
|
+
return;
|
|
501
|
+
case "assistant.text":
|
|
502
|
+
this.finishPendingTerminalDelta();
|
|
503
|
+
console.log(chalk.cyan(event.text));
|
|
504
|
+
return;
|
|
505
|
+
case "assistant.reasoning.delta":
|
|
506
|
+
process.stdout.write(chalk.magenta(event.text));
|
|
507
|
+
this.pendingTerminalDelta = true;
|
|
508
|
+
return;
|
|
509
|
+
case "assistant.reasoning":
|
|
510
|
+
this.finishPendingTerminalDelta();
|
|
511
|
+
console.log(chalk.magenta(event.text));
|
|
512
|
+
return;
|
|
513
|
+
case "tool.call.delta":
|
|
514
|
+
this.finishPendingTerminalDelta();
|
|
515
|
+
if (event.name) {
|
|
516
|
+
console.log(chalk.yellow(`→ ${event.name}(...)`));
|
|
517
|
+
}
|
|
518
|
+
return;
|
|
519
|
+
case "tool.call":
|
|
520
|
+
this.finishPendingTerminalDelta();
|
|
521
|
+
console.log(chalk.yellow(`→ ${event.name}(${JSON.stringify(event.input)})`));
|
|
522
|
+
return;
|
|
523
|
+
case "tool.result": {
|
|
524
|
+
this.finishPendingTerminalDelta();
|
|
525
|
+
const preview = event.content.slice(0, 500);
|
|
526
|
+
console.log(event.isError ? chalk.red(` ✗ ${preview}`) : chalk.gray(` ${preview}`));
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
finishPendingTerminalDelta() {
|
|
532
|
+
if (!this.pendingTerminalDelta)
|
|
533
|
+
return;
|
|
534
|
+
process.stdout.write("\n");
|
|
535
|
+
this.pendingTerminalDelta = false;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Detects abort errors thrown by provider SDKs or aborted child processes,
|
|
540
|
+
* which surface with varying names (AbortError, APIUserAbortError, ABORT_ERR).
|
|
541
|
+
*/
|
|
542
|
+
function isAbortError(err) {
|
|
543
|
+
if (!(err instanceof Error))
|
|
544
|
+
return false;
|
|
545
|
+
return (err.name === "AbortError" ||
|
|
546
|
+
err.name === "APIUserAbortError" ||
|
|
547
|
+
err.code === "ABORT_ERR");
|
|
548
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type PolicyDecision = "allow" | "deny" | "ask";
|
|
2
|
+
export type ApprovalMode = "prompt" | "auto";
|
|
3
|
+
export interface PolicyContextRule {
|
|
4
|
+
toolName: string;
|
|
5
|
+
decision: PolicyDecision;
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
8
|
+
export interface PolicyContext {
|
|
9
|
+
defaultDecision: PolicyDecision;
|
|
10
|
+
rules: PolicyContextRule[];
|
|
11
|
+
}
|
|
12
|
+
interface PolicyRule {
|
|
13
|
+
toolName: string;
|
|
14
|
+
decide: (input: Record<string, unknown>) => PolicyDecision;
|
|
15
|
+
}
|
|
16
|
+
export declare class PolicyEngine {
|
|
17
|
+
private policyContext;
|
|
18
|
+
private rules;
|
|
19
|
+
constructor(policyContext?: PolicyContext);
|
|
20
|
+
addRule(rule: PolicyRule): void;
|
|
21
|
+
evaluate(toolName: string, input: Record<string, unknown>): PolicyDecision;
|
|
22
|
+
describe(): PolicyContext;
|
|
23
|
+
static withDefaults(): PolicyEngine;
|
|
24
|
+
}
|
|
25
|
+
export {};
|