@lite-agent/core 0.1.0 → 0.2.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/dist/index.d.ts +104 -1
- package/dist/index.js +375 -6
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -250,6 +250,7 @@ interface CreateAgentConfig {
|
|
|
250
250
|
maxTokens?: number;
|
|
251
251
|
sandbox?: Sandbox;
|
|
252
252
|
input?: InputHandler;
|
|
253
|
+
store?: Store;
|
|
253
254
|
}
|
|
254
255
|
type RunOptions = {
|
|
255
256
|
signal?: AbortSignal;
|
|
@@ -275,6 +276,108 @@ declare function fakeProvider(turns: FakeTurn[]): ModelProvider;
|
|
|
275
276
|
|
|
276
277
|
declare function noopSandbox(): Sandbox;
|
|
277
278
|
|
|
279
|
+
declare function memoryStore(): Store;
|
|
280
|
+
|
|
281
|
+
interface RetryOptions {
|
|
282
|
+
/** Retries after the first attempt. Default 2 (→ up to 3 total attempts). */
|
|
283
|
+
maxRetries?: number;
|
|
284
|
+
/** ms to wait before retry N (1-based). Default exponential 250ms→8s. */
|
|
285
|
+
backoff?: (attempt: number) => number;
|
|
286
|
+
/** Whether an error is retryable. Default: transient ProviderError. */
|
|
287
|
+
retryOn?: (err: unknown) => boolean;
|
|
288
|
+
}
|
|
289
|
+
declare function retry(opts?: RetryOptions): Middleware;
|
|
290
|
+
|
|
291
|
+
declare const SPILL_PREFIX = "[spilled:";
|
|
292
|
+
interface CompactPass {
|
|
293
|
+
readonly name: string;
|
|
294
|
+
apply(messages: Message[]): Message[];
|
|
295
|
+
}
|
|
296
|
+
declare function runPipeline(passes: CompactPass[], messages: Message[]): Message[];
|
|
297
|
+
declare function estimateTokens(messages: Message[]): number;
|
|
298
|
+
|
|
299
|
+
interface MicroPassOptions {
|
|
300
|
+
/** How many of the most recent tool_results keep their full body. Default 3. */
|
|
301
|
+
keepRecent?: number;
|
|
302
|
+
placeholder?: string;
|
|
303
|
+
}
|
|
304
|
+
declare function microPass(opts?: MicroPassOptions): CompactPass;
|
|
305
|
+
|
|
306
|
+
interface SnipPassOptions {
|
|
307
|
+
/** Only snip when the transcript exceeds this many messages. Default 50. */
|
|
308
|
+
maxMessages?: number;
|
|
309
|
+
/** How many leading turns to always keep. Default 1. */
|
|
310
|
+
headTurns?: number;
|
|
311
|
+
/** Keep trailing turns until at least this many messages are retained. Default 20. */
|
|
312
|
+
tailKeep?: number;
|
|
313
|
+
}
|
|
314
|
+
declare function splitTurns(messages: Message[]): Message[][];
|
|
315
|
+
declare function snipPass(opts?: SnipPassOptions): CompactPass;
|
|
316
|
+
|
|
317
|
+
interface SpillStore {
|
|
318
|
+
put(content: string): string;
|
|
319
|
+
get(ref: string): string | null;
|
|
320
|
+
}
|
|
321
|
+
declare function memorySpillStore(): SpillStore;
|
|
322
|
+
interface ToolResultBudgetOptions {
|
|
323
|
+
store: SpillStore;
|
|
324
|
+
/** Spill the largest tool_results until total body bytes ≤ this. Default 200_000. */
|
|
325
|
+
budgetBytes?: number;
|
|
326
|
+
}
|
|
327
|
+
declare function toolResultBudgetPass(opts: ToolResultBudgetOptions): CompactPass;
|
|
328
|
+
|
|
329
|
+
interface DefaultCompactorOptions {
|
|
330
|
+
/** snip: only snip beyond this many messages. Default 50. */
|
|
331
|
+
maxMessages?: number;
|
|
332
|
+
/** snip: leading turns always kept. Default 1. */
|
|
333
|
+
headTurns?: number;
|
|
334
|
+
/** snip: keep trailing turns until this many messages retained. Default 20. */
|
|
335
|
+
tailKeep?: number;
|
|
336
|
+
/** micro: how many recent tool_results keep full bodies. Default 3. */
|
|
337
|
+
keepRecentToolResults?: number;
|
|
338
|
+
/** L3: when set, spill oversized tool_results to this store (runs first). */
|
|
339
|
+
spillStore?: SpillStore;
|
|
340
|
+
/** L3: spill largest tool_results until total body bytes ≤ this. Default 200_000. */
|
|
341
|
+
budgetBytes?: number;
|
|
342
|
+
/** Replace the whole pass pipeline (hot-swap). Defaults to [spill?] → snip → micro. */
|
|
343
|
+
passes?: CompactPass[];
|
|
344
|
+
}
|
|
345
|
+
declare function defaultCompactor(opts?: DefaultCompactorOptions): Compactor;
|
|
346
|
+
|
|
347
|
+
declare function compaction(compactor: Compactor): Middleware;
|
|
348
|
+
|
|
349
|
+
interface ReactiveTrimOptions {
|
|
350
|
+
/** Max recent turns to keep. Default 2. */
|
|
351
|
+
keepTurns?: number;
|
|
352
|
+
/** Approx token cap on the kept tail. Default 4000. */
|
|
353
|
+
tokenBudget?: number;
|
|
354
|
+
}
|
|
355
|
+
declare function reactiveTrim(messages: Message[], opts?: ReactiveTrimOptions): Message[];
|
|
356
|
+
interface ReactiveCompactionOptions {
|
|
357
|
+
/** How to trim on overflow. Default reactiveTrim with its defaults. */
|
|
358
|
+
trim?: (messages: Message[]) => Message[];
|
|
359
|
+
/** Classify an error as a context-overflow. Default: ProviderError 413 / prompt_too_long. */
|
|
360
|
+
isOverflow?: (err: unknown) => boolean;
|
|
361
|
+
/** Reactive retries after the first failure. Default 1. */
|
|
362
|
+
maxAttempts?: number;
|
|
363
|
+
}
|
|
364
|
+
declare function reactiveCompaction(opts?: ReactiveCompactionOptions): Middleware;
|
|
365
|
+
|
|
366
|
+
interface LlmCompactorOptions {
|
|
367
|
+
provider: ModelProvider;
|
|
368
|
+
model: string;
|
|
369
|
+
/** Deterministic compactor run first. Default defaultCompactor(). */
|
|
370
|
+
base?: Compactor;
|
|
371
|
+
/** Summarize via the LLM once the estimate exceeds this. Default 120_000. */
|
|
372
|
+
tokenThreshold?: number;
|
|
373
|
+
/** Recent turns kept verbatim; older ones get summarized. Default 3. */
|
|
374
|
+
keepRecentTurns?: number;
|
|
375
|
+
summaryPrompt?: string;
|
|
376
|
+
/** Consecutive LLM failures before the circuit opens (falls back to base). Default 2. */
|
|
377
|
+
maxFailures?: number;
|
|
378
|
+
}
|
|
379
|
+
declare function llmCompactor(opts: LlmCompactorOptions): Compactor;
|
|
380
|
+
|
|
278
381
|
interface PolicyOptions {
|
|
279
382
|
allow?: string[];
|
|
280
383
|
ask?: string[];
|
|
@@ -284,4 +387,4 @@ interface PolicyOptions {
|
|
|
284
387
|
declare function policy(opts?: PolicyOptions): PermissionPolicy;
|
|
285
388
|
declare function permission(pol: PermissionPolicy, approval?: ApprovalHandler): Middleware;
|
|
286
389
|
|
|
287
|
-
export { AbortError, type Agent, type AgentContext, AgentError, type AgentEvent, type ApprovalHandler, type AssistantMessage, CodecError, type CompactResult, type Compactor, type ContentBlock, type CreateAgentConfig, type Decision, type FakeTurn, type InputHandler, MaxTurnsError, type Message, type Middleware, type ModelCall, type ModelChunk, type ModelProvider, type ModelRequest, type PermissionPolicy, type PolicyContext, type PolicyOptions, ProviderError, type Role, type RunOptions, type RunResult, type Sandbox, type SandboxWrapOptions, type StopReason, type Store, type TextBlock, type Tool, type ToolCall, type ToolCallBlock, type ToolCallCodec, type ToolCallContext, type ToolContext, ToolError, type ToolExec, type ToolResult, type ToolResultBlock, type ToolSpec, type Usage, type UserAnswer, type UserQuestion, composeModelCall, composeToolCall, createAgent, defineTool, fakeProvider, isTextBlock, isToolCallBlock, nativeCodec, noopSandbox, permission, policy, runLifecycle, textBlock, toToolSpec, toolResultBlock };
|
|
390
|
+
export { AbortError, type Agent, type AgentContext, AgentError, type AgentEvent, type ApprovalHandler, type AssistantMessage, CodecError, type CompactPass, type CompactResult, type Compactor, type ContentBlock, type CreateAgentConfig, type Decision, type DefaultCompactorOptions, type FakeTurn, type InputHandler, type LlmCompactorOptions, MaxTurnsError, type Message, type MicroPassOptions, type Middleware, type ModelCall, type ModelChunk, type ModelProvider, type ModelRequest, type PermissionPolicy, type PolicyContext, type PolicyOptions, ProviderError, type ReactiveCompactionOptions, type ReactiveTrimOptions, type RetryOptions, type Role, type RunOptions, type RunResult, SPILL_PREFIX, type Sandbox, type SandboxWrapOptions, type SnipPassOptions, type SpillStore, type StopReason, type Store, type TextBlock, type Tool, type ToolCall, type ToolCallBlock, type ToolCallCodec, type ToolCallContext, type ToolContext, ToolError, type ToolExec, type ToolResult, type ToolResultBlock, type ToolResultBudgetOptions, type ToolSpec, type Usage, type UserAnswer, type UserQuestion, compaction, composeModelCall, composeToolCall, createAgent, defaultCompactor, defineTool, estimateTokens, fakeProvider, isTextBlock, isToolCallBlock, llmCompactor, memorySpillStore, memoryStore, microPass, nativeCodec, noopSandbox, permission, policy, reactiveCompaction, reactiveTrim, retry, runLifecycle, runPipeline, snipPass, splitTurns, textBlock, toToolSpec, toolResultBlock, toolResultBudgetPass };
|
package/dist/index.js
CHANGED
|
@@ -74,6 +74,13 @@ function toToolSpec(tool) {
|
|
|
74
74
|
// src/kernel.ts
|
|
75
75
|
async function* runKernel(cfg, input, signal, sessionId) {
|
|
76
76
|
let messages = typeof input === "string" ? [{ role: "user", content: input }] : [...input];
|
|
77
|
+
if (cfg.store) {
|
|
78
|
+
const saved = await cfg.store.load(sessionId);
|
|
79
|
+
if (saved) messages = [...saved, ...messages];
|
|
80
|
+
}
|
|
81
|
+
const persist = async () => {
|
|
82
|
+
if (cfg.store) await cfg.store.save(sessionId, messages);
|
|
83
|
+
};
|
|
77
84
|
const queue = [];
|
|
78
85
|
const emit = (ev) => {
|
|
79
86
|
queue.push(ev);
|
|
@@ -99,11 +106,17 @@ async function* runKernel(cfg, input, signal, sessionId) {
|
|
|
99
106
|
await runLifecycle(cfg.middleware, "beforeModel", ctx);
|
|
100
107
|
yield* drain();
|
|
101
108
|
messages = ctx.messages;
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
const modelCall = composeModelCall(
|
|
110
|
+
cfg.middleware,
|
|
111
|
+
ctx,
|
|
112
|
+
() => cfg.provider.stream(
|
|
113
|
+
cfg.codec.encode(
|
|
114
|
+
{ model: cfg.model, system: cfg.system, messages: ctx.messages, maxTokens: cfg.maxTokens },
|
|
115
|
+
toolSpecs
|
|
116
|
+
),
|
|
117
|
+
signal
|
|
118
|
+
)
|
|
105
119
|
);
|
|
106
|
-
const modelCall = composeModelCall(cfg.middleware, ctx, () => cfg.provider.stream(req, signal));
|
|
107
120
|
let assistant;
|
|
108
121
|
for await (const chunk of modelCall()) {
|
|
109
122
|
if (chunk.type === "text_delta") yield { type: "text_delta", text: chunk.text };
|
|
@@ -115,6 +128,7 @@ async function* runKernel(cfg, input, signal, sessionId) {
|
|
|
115
128
|
};
|
|
116
129
|
}
|
|
117
130
|
}
|
|
131
|
+
messages = ctx.messages;
|
|
118
132
|
yield* drain();
|
|
119
133
|
if (!assistant) throw new ProviderError("provider produced no message_done chunk");
|
|
120
134
|
ctx.messages.push(assistant);
|
|
@@ -146,10 +160,12 @@ async function* runKernel(cfg, input, signal, sessionId) {
|
|
|
146
160
|
yield { type: "tool_result", result: result2 };
|
|
147
161
|
}
|
|
148
162
|
ctx.messages.push({ role: "user", content: resultBlocks });
|
|
163
|
+
await persist();
|
|
149
164
|
yield { type: "turn_end", turn, stopReason: "tool_use" };
|
|
150
165
|
}
|
|
151
166
|
await runLifecycle(cfg.middleware, "afterAgent", mkCtx(0));
|
|
152
167
|
yield* drain();
|
|
168
|
+
await persist();
|
|
153
169
|
const result = { messages, text: lastAssistantText(messages), usage, stopReason };
|
|
154
170
|
yield { type: "done", reason: stopReason, result };
|
|
155
171
|
return result;
|
|
@@ -177,7 +193,8 @@ function createAgent(cfg) {
|
|
|
177
193
|
maxTurns: cfg.maxTurns ?? 50,
|
|
178
194
|
maxTokens: cfg.maxTokens,
|
|
179
195
|
sandbox: cfg.sandbox ?? noopSandbox(),
|
|
180
|
-
input: cfg.input
|
|
196
|
+
input: cfg.input,
|
|
197
|
+
store: cfg.store
|
|
181
198
|
};
|
|
182
199
|
const agent = {
|
|
183
200
|
run(input, opts) {
|
|
@@ -232,6 +249,343 @@ function fakeProvider(turns) {
|
|
|
232
249
|
};
|
|
233
250
|
}
|
|
234
251
|
|
|
252
|
+
// src/store.ts
|
|
253
|
+
function memoryStore() {
|
|
254
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
255
|
+
return {
|
|
256
|
+
async load(id) {
|
|
257
|
+
return sessions.get(id) ?? null;
|
|
258
|
+
},
|
|
259
|
+
async save(id, messages) {
|
|
260
|
+
sessions.set(id, structuredClone(messages));
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/retry.ts
|
|
266
|
+
var TRANSIENT = /* @__PURE__ */ new Set([408, 409, 425, 429, 500, 502, 503, 504]);
|
|
267
|
+
var defaultRetryOn = (err) => err instanceof ProviderError && (err.status === void 0 || TRANSIENT.has(err.status));
|
|
268
|
+
var defaultBackoff = (attempt) => Math.min(250 * 2 ** (attempt - 1), 8e3);
|
|
269
|
+
function retry(opts = {}) {
|
|
270
|
+
const maxRetries = opts.maxRetries ?? 2;
|
|
271
|
+
const backoff = opts.backoff ?? defaultBackoff;
|
|
272
|
+
const retryOn = opts.retryOn ?? defaultRetryOn;
|
|
273
|
+
return {
|
|
274
|
+
name: "retry",
|
|
275
|
+
async *wrapModelCall(ctx, next) {
|
|
276
|
+
let attempt = 0;
|
|
277
|
+
while (true) {
|
|
278
|
+
let started = false;
|
|
279
|
+
try {
|
|
280
|
+
for await (const chunk of next()) {
|
|
281
|
+
started = true;
|
|
282
|
+
yield chunk;
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
} catch (err) {
|
|
286
|
+
if (started || attempt >= maxRetries || !retryOn(err)) throw err;
|
|
287
|
+
attempt++;
|
|
288
|
+
const ms = backoff(attempt);
|
|
289
|
+
if (ms > 0) await new Promise((r) => setTimeout(r, ms));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/compaction/types.ts
|
|
297
|
+
var SPILL_PREFIX = "[spilled:";
|
|
298
|
+
function runPipeline(passes, messages) {
|
|
299
|
+
return passes.reduce((msgs, pass) => pass.apply(msgs), messages);
|
|
300
|
+
}
|
|
301
|
+
function estimateTokens(messages) {
|
|
302
|
+
let chars = 0;
|
|
303
|
+
for (const m of messages) {
|
|
304
|
+
if (typeof m.content === "string") {
|
|
305
|
+
chars += m.content.length;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
for (const b of m.content) {
|
|
309
|
+
if (b.type === "text") chars += b.text.length;
|
|
310
|
+
else if (b.type === "tool_result") chars += b.content.length;
|
|
311
|
+
else if (b.type === "tool_call") chars += JSON.stringify(b.input).length;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return Math.ceil(chars / 4);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/compaction/micro.ts
|
|
318
|
+
function microPass(opts = {}) {
|
|
319
|
+
const keepRecent = opts.keepRecent ?? 3;
|
|
320
|
+
const placeholder = opts.placeholder ?? "[tool result omitted to save context]";
|
|
321
|
+
return {
|
|
322
|
+
name: "micro",
|
|
323
|
+
apply(messages) {
|
|
324
|
+
const positions = [];
|
|
325
|
+
messages.forEach((m, mi) => {
|
|
326
|
+
if (Array.isArray(m.content)) {
|
|
327
|
+
m.content.forEach((b, bi) => {
|
|
328
|
+
if (b.type === "tool_result") positions.push([mi, bi]);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
if (positions.length <= keepRecent) return messages;
|
|
333
|
+
const omit = new Set(positions.slice(0, positions.length - keepRecent).map(([mi, bi]) => `${mi}:${bi}`));
|
|
334
|
+
let anyChanged = false;
|
|
335
|
+
const out = messages.map((m, mi) => {
|
|
336
|
+
if (!Array.isArray(m.content)) return m;
|
|
337
|
+
let changed = false;
|
|
338
|
+
const content = m.content.map((b, bi) => {
|
|
339
|
+
if (b.type === "tool_result" && omit.has(`${mi}:${bi}`) && b.content !== placeholder && !b.content.startsWith(SPILL_PREFIX)) {
|
|
340
|
+
changed = true;
|
|
341
|
+
return { ...b, content: placeholder };
|
|
342
|
+
}
|
|
343
|
+
return b;
|
|
344
|
+
});
|
|
345
|
+
if (!changed) return m;
|
|
346
|
+
anyChanged = true;
|
|
347
|
+
return { ...m, content };
|
|
348
|
+
});
|
|
349
|
+
return anyChanged ? out : messages;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/compaction/snip.ts
|
|
355
|
+
function splitTurns(messages) {
|
|
356
|
+
const turns = [];
|
|
357
|
+
let cur = [];
|
|
358
|
+
for (const m of messages) {
|
|
359
|
+
if (m.role === "user" && typeof m.content === "string" && cur.length > 0) {
|
|
360
|
+
turns.push(cur);
|
|
361
|
+
cur = [];
|
|
362
|
+
}
|
|
363
|
+
cur.push(m);
|
|
364
|
+
}
|
|
365
|
+
if (cur.length) turns.push(cur);
|
|
366
|
+
return turns;
|
|
367
|
+
}
|
|
368
|
+
function snipPass(opts = {}) {
|
|
369
|
+
const maxMessages = opts.maxMessages ?? 50;
|
|
370
|
+
const headTurns = opts.headTurns ?? 1;
|
|
371
|
+
const tailKeep = opts.tailKeep ?? 20;
|
|
372
|
+
return {
|
|
373
|
+
name: "snip",
|
|
374
|
+
apply(messages) {
|
|
375
|
+
if (messages.length <= maxMessages) return messages;
|
|
376
|
+
const turns = splitTurns(messages);
|
|
377
|
+
let tailTurnCount = 0;
|
|
378
|
+
let tailMsgCount = 0;
|
|
379
|
+
for (let i = turns.length - 1; i >= 0 && tailMsgCount < tailKeep; i--) {
|
|
380
|
+
tailMsgCount += turns[i].length;
|
|
381
|
+
tailTurnCount++;
|
|
382
|
+
}
|
|
383
|
+
if (headTurns + tailTurnCount >= turns.length) return messages;
|
|
384
|
+
const head = turns.slice(0, headTurns).flat();
|
|
385
|
+
const tail = turns.slice(turns.length - tailTurnCount).flat();
|
|
386
|
+
const omitted = turns.length - headTurns - tailTurnCount;
|
|
387
|
+
const placeholder = { role: "user", content: `[${omitted} earlier turn(s) omitted to save context]` };
|
|
388
|
+
return [...head, placeholder, ...tail];
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/compaction/budget.ts
|
|
394
|
+
function memorySpillStore() {
|
|
395
|
+
const blobs = /* @__PURE__ */ new Map();
|
|
396
|
+
let n = 0;
|
|
397
|
+
return {
|
|
398
|
+
put(content) {
|
|
399
|
+
const ref = `m${++n}`;
|
|
400
|
+
blobs.set(ref, content);
|
|
401
|
+
return ref;
|
|
402
|
+
},
|
|
403
|
+
get(ref) {
|
|
404
|
+
return blobs.get(ref) ?? null;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
var isSpilled = (s) => s.startsWith(SPILL_PREFIX);
|
|
409
|
+
var marker = (ref, bytes) => `${SPILL_PREFIX}${ref}] ${bytes} bytes moved off-context \u2014 call read_spilled({ ref: "${ref}" }) to view the full content.`;
|
|
410
|
+
function toolResultBudgetPass(opts) {
|
|
411
|
+
const budget = opts.budgetBytes ?? 2e5;
|
|
412
|
+
return {
|
|
413
|
+
name: "toolResultBudget",
|
|
414
|
+
apply(messages) {
|
|
415
|
+
const results = [];
|
|
416
|
+
let total = 0;
|
|
417
|
+
messages.forEach((m, mi) => {
|
|
418
|
+
if (Array.isArray(m.content)) {
|
|
419
|
+
m.content.forEach((b, bi) => {
|
|
420
|
+
if (b.type === "tool_result" && !isSpilled(b.content)) {
|
|
421
|
+
results.push({ mi, bi, bytes: b.content.length });
|
|
422
|
+
total += b.content.length;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
if (total <= budget) return messages;
|
|
428
|
+
results.sort((a, b) => b.bytes - a.bytes);
|
|
429
|
+
const spill = /* @__PURE__ */ new Set();
|
|
430
|
+
for (const r of results) {
|
|
431
|
+
if (total <= budget) break;
|
|
432
|
+
spill.add(`${r.mi}:${r.bi}`);
|
|
433
|
+
total -= r.bytes;
|
|
434
|
+
}
|
|
435
|
+
if (spill.size === 0) return messages;
|
|
436
|
+
return messages.map((m, mi) => {
|
|
437
|
+
if (!Array.isArray(m.content)) return m;
|
|
438
|
+
let changed = false;
|
|
439
|
+
const content = m.content.map((b, bi) => {
|
|
440
|
+
if (b.type === "tool_result" && spill.has(`${mi}:${bi}`)) {
|
|
441
|
+
changed = true;
|
|
442
|
+
const ref = opts.store.put(b.content);
|
|
443
|
+
return { ...b, content: marker(ref, b.content.length) };
|
|
444
|
+
}
|
|
445
|
+
return b;
|
|
446
|
+
});
|
|
447
|
+
return changed ? { ...m, content } : m;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/compaction/defaultCompactor.ts
|
|
454
|
+
function defaultCompactor(opts = {}) {
|
|
455
|
+
const passes = opts.passes ?? [
|
|
456
|
+
...opts.spillStore ? [toolResultBudgetPass({ store: opts.spillStore, budgetBytes: opts.budgetBytes })] : [],
|
|
457
|
+
snipPass({ maxMessages: opts.maxMessages, headTurns: opts.headTurns, tailKeep: opts.tailKeep }),
|
|
458
|
+
microPass({ keepRecent: opts.keepRecentToolResults })
|
|
459
|
+
];
|
|
460
|
+
return {
|
|
461
|
+
async maybeCompact(messages) {
|
|
462
|
+
const before = estimateTokens(messages);
|
|
463
|
+
const out = runPipeline(passes, messages);
|
|
464
|
+
if (out === messages) return { messages, before, after: before };
|
|
465
|
+
return { messages: out, kind: "micro", before, after: estimateTokens(out) };
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/compaction/middleware.ts
|
|
471
|
+
var ZERO_USAGE = { inputTokens: 0, outputTokens: 0 };
|
|
472
|
+
function compaction(compactor) {
|
|
473
|
+
return {
|
|
474
|
+
name: "compaction",
|
|
475
|
+
async beforeModel(ctx) {
|
|
476
|
+
const r = await compactor.maybeCompact(ctx.messages, ZERO_USAGE);
|
|
477
|
+
if (r.messages !== ctx.messages) {
|
|
478
|
+
ctx.emit({ type: "compaction", kind: r.kind ?? "micro", before: r.before ?? 0, after: r.after ?? 0 });
|
|
479
|
+
ctx.messages = r.messages;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/compaction/reactive.ts
|
|
486
|
+
function reactiveTrim(messages, opts = {}) {
|
|
487
|
+
const keepTurns = opts.keepTurns ?? 2;
|
|
488
|
+
const budget = opts.tokenBudget ?? 4e3;
|
|
489
|
+
const turns = splitTurns(messages);
|
|
490
|
+
const kept = [];
|
|
491
|
+
let toks = 0;
|
|
492
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
493
|
+
const t = turns[i];
|
|
494
|
+
if (kept.length >= 1 && (kept.length >= keepTurns || toks + estimateTokens(t) > budget)) break;
|
|
495
|
+
kept.unshift(t);
|
|
496
|
+
toks += estimateTokens(t);
|
|
497
|
+
}
|
|
498
|
+
const dropped = turns.length - kept.length;
|
|
499
|
+
if (dropped <= 0) return messages;
|
|
500
|
+
const placeholder = { role: "user", content: `[${dropped} earlier turn(s) dropped \u2014 context overflow]` };
|
|
501
|
+
return [placeholder, ...kept.flat()];
|
|
502
|
+
}
|
|
503
|
+
function defaultIsOverflow(err) {
|
|
504
|
+
if (!(err instanceof ProviderError)) return false;
|
|
505
|
+
if (err.status === 413) return true;
|
|
506
|
+
return /prompt[\s_]?too[\s_]?long|context[\s_]?length|too many tokens|maximum context/i.test(err.message);
|
|
507
|
+
}
|
|
508
|
+
function reactiveCompaction(opts = {}) {
|
|
509
|
+
const trim = opts.trim ?? ((m) => reactiveTrim(m));
|
|
510
|
+
const isOverflow = opts.isOverflow ?? defaultIsOverflow;
|
|
511
|
+
const maxAttempts = opts.maxAttempts ?? 1;
|
|
512
|
+
return {
|
|
513
|
+
name: "reactive-compaction",
|
|
514
|
+
async *wrapModelCall(ctx, next) {
|
|
515
|
+
let attempts = 0;
|
|
516
|
+
while (true) {
|
|
517
|
+
let started = false;
|
|
518
|
+
try {
|
|
519
|
+
for await (const chunk of next()) {
|
|
520
|
+
started = true;
|
|
521
|
+
yield chunk;
|
|
522
|
+
}
|
|
523
|
+
return;
|
|
524
|
+
} catch (err) {
|
|
525
|
+
if (started || !isOverflow(err) || attempts >= maxAttempts) throw err;
|
|
526
|
+
attempts++;
|
|
527
|
+
const before = estimateTokens(ctx.messages);
|
|
528
|
+
ctx.messages = trim(ctx.messages);
|
|
529
|
+
ctx.emit({ type: "compaction", kind: "auto", before, after: estimateTokens(ctx.messages) });
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/compaction/llm.ts
|
|
537
|
+
var DEFAULT_SUMMARY_PROMPT = "You are a context-compaction assistant. Summarize the conversation so far into a concise note that preserves key facts, decisions, file paths, and the current task state. Output only the summary.";
|
|
538
|
+
function llmCompactor(opts) {
|
|
539
|
+
const base = opts.base ?? defaultCompactor();
|
|
540
|
+
const threshold = opts.tokenThreshold ?? 12e4;
|
|
541
|
+
const keepRecentTurns = opts.keepRecentTurns ?? 3;
|
|
542
|
+
const summaryPrompt = opts.summaryPrompt ?? DEFAULT_SUMMARY_PROMPT;
|
|
543
|
+
const maxFailures = opts.maxFailures ?? 2;
|
|
544
|
+
let failures = 0;
|
|
545
|
+
let circuitOpen = false;
|
|
546
|
+
async function summarize(older) {
|
|
547
|
+
const req = {
|
|
548
|
+
model: opts.model,
|
|
549
|
+
system: summaryPrompt,
|
|
550
|
+
messages: [...older, { role: "user", content: "Summarize the conversation above as instructed." }]
|
|
551
|
+
};
|
|
552
|
+
let assistant;
|
|
553
|
+
for await (const chunk of opts.provider.stream(req)) {
|
|
554
|
+
if (chunk.type === "message_done") assistant = chunk.message;
|
|
555
|
+
}
|
|
556
|
+
if (!assistant || !Array.isArray(assistant.content)) return "";
|
|
557
|
+
return assistant.content.filter(isTextBlock).map((b) => b.text).join("").trim();
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
async maybeCompact(messages, usage) {
|
|
561
|
+
const before = estimateTokens(messages);
|
|
562
|
+
const baseResult = await base.maybeCompact(messages, usage);
|
|
563
|
+
const msgs = baseResult.messages;
|
|
564
|
+
if (circuitOpen || estimateTokens(msgs) <= threshold) {
|
|
565
|
+
return { ...baseResult, before, after: estimateTokens(msgs) };
|
|
566
|
+
}
|
|
567
|
+
const turns = splitTurns(msgs);
|
|
568
|
+
if (turns.length <= keepRecentTurns + 1) {
|
|
569
|
+
return { messages: msgs, kind: baseResult.kind, before, after: estimateTokens(msgs) };
|
|
570
|
+
}
|
|
571
|
+
const recent = turns.slice(turns.length - keepRecentTurns).flat();
|
|
572
|
+
const older = turns.slice(0, turns.length - keepRecentTurns).flat();
|
|
573
|
+
try {
|
|
574
|
+
const summary = await summarize(older);
|
|
575
|
+
failures = 0;
|
|
576
|
+
const summaryMsg = { role: "user", content: `[Summary of earlier conversation]
|
|
577
|
+
${summary}` };
|
|
578
|
+
const out = [summaryMsg, ...recent];
|
|
579
|
+
return { messages: out, kind: "auto", before, after: estimateTokens(out) };
|
|
580
|
+
} catch {
|
|
581
|
+
failures++;
|
|
582
|
+
if (failures >= maxFailures) circuitOpen = true;
|
|
583
|
+
return { messages: msgs, kind: baseResult.kind, before, after: estimateTokens(msgs) };
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
235
589
|
// src/permission.ts
|
|
236
590
|
function globToRegExp(pattern) {
|
|
237
591
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
@@ -276,20 +630,35 @@ export {
|
|
|
276
630
|
CodecError,
|
|
277
631
|
MaxTurnsError,
|
|
278
632
|
ProviderError,
|
|
633
|
+
SPILL_PREFIX,
|
|
279
634
|
ToolError,
|
|
635
|
+
compaction,
|
|
280
636
|
composeModelCall,
|
|
281
637
|
composeToolCall,
|
|
282
638
|
createAgent,
|
|
639
|
+
defaultCompactor,
|
|
283
640
|
defineTool,
|
|
641
|
+
estimateTokens,
|
|
284
642
|
fakeProvider,
|
|
285
643
|
isTextBlock,
|
|
286
644
|
isToolCallBlock,
|
|
645
|
+
llmCompactor,
|
|
646
|
+
memorySpillStore,
|
|
647
|
+
memoryStore,
|
|
648
|
+
microPass,
|
|
287
649
|
nativeCodec,
|
|
288
650
|
noopSandbox,
|
|
289
651
|
permission,
|
|
290
652
|
policy,
|
|
653
|
+
reactiveCompaction,
|
|
654
|
+
reactiveTrim,
|
|
655
|
+
retry,
|
|
291
656
|
runLifecycle,
|
|
657
|
+
runPipeline,
|
|
658
|
+
snipPass,
|
|
659
|
+
splitTurns,
|
|
292
660
|
textBlock,
|
|
293
661
|
toToolSpec,
|
|
294
|
-
toolResultBlock
|
|
662
|
+
toolResultBlock,
|
|
663
|
+
toolResultBudgetPass
|
|
295
664
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lite-agent/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Pluggable, event-driven agent core: kernel, strategy interfaces, middleware pipeline, and normalized types.",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"type": "module",
|
|
@@ -34,11 +34,6 @@
|
|
|
34
34
|
"publishConfig": {
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
|
-
"scripts": {
|
|
38
|
-
"build": "tsup src/index.ts --format esm --dts --clean --tsconfig tsconfig.build.json",
|
|
39
|
-
"test": "vitest run",
|
|
40
|
-
"typecheck": "tsc --noEmit"
|
|
41
|
-
},
|
|
42
37
|
"dependencies": {
|
|
43
38
|
"zod": "^4.3.6"
|
|
44
39
|
},
|
|
@@ -47,5 +42,10 @@
|
|
|
47
42
|
"tsup": "^8.3.0",
|
|
48
43
|
"typescript": "^6.0.2",
|
|
49
44
|
"vitest": "^2.1.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup src/index.ts --format esm --dts --clean --tsconfig tsconfig.build.json",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"typecheck": "tsc --noEmit"
|
|
50
50
|
}
|
|
51
|
-
}
|
|
51
|
+
}
|