mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
test(agent-core): consolidate test helpers into AgentTestContext (#106)
This commit is contained in:
parent
d599183c8e
commit
d1c381f38a
4 changed files with 504 additions and 461 deletions
|
|
@ -104,9 +104,9 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
appendExchange(ctx, 2, 'old user two', 'old assistant two', 40);
|
||||
appendExchange(ctx, 3, 'recent user three', 'recent assistant three', 120);
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(2, 'old user two', 'old assistant two', 40);
|
||||
ctx.appendExchange(3, 'recent user three', 'recent assistant three', 120);
|
||||
const compacted = new Promise<void>((resolve) => {
|
||||
ctx.emitter.once('context.apply_compaction', () => {
|
||||
resolve();
|
||||
|
|
@ -140,7 +140,7 @@ describe('Agent compaction', () => {
|
|||
assistant: text "old assistant two"
|
||||
user: text <compaction-instruction>
|
||||
`);
|
||||
expect(compactHistory(ctx)).toMatchInlineSnapshot(`
|
||||
expect(ctx.compactHistory()).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"role": "assistant",
|
||||
|
|
@ -235,7 +235,7 @@ describe('Agent compaction', () => {
|
|||
max_context_tokens: maxContextTokens,
|
||||
},
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', maxContextTokens - 100);
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', maxContextTokens - 100);
|
||||
const compacted = new Promise<void>((resolve) => {
|
||||
ctx.emitter.once('context.apply_compaction', () => {
|
||||
resolve();
|
||||
|
|
@ -254,12 +254,12 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: { type: 'step.begin', uuid: 'empty-placeholder', turnId: '', step: 2 },
|
||||
});
|
||||
appendExchange(ctx, 3, 'old user two', 'old assistant two', 40);
|
||||
ctx.appendExchange(3, 'old user two', 'old assistant two', 40);
|
||||
const compacted = new Promise<void>((resolve) => {
|
||||
ctx.emitter.once('context.apply_compaction', () => {
|
||||
resolve();
|
||||
|
|
@ -313,9 +313,9 @@ describe('Agent compaction', () => {
|
|||
ctx.configure();
|
||||
await ctx.rpc.setModel({ model: 'kimi-code' });
|
||||
ctx.newEvents();
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
appendExchange(ctx, 2, 'recent user two', 'recent assistant two', 80);
|
||||
const outcome = onceAny(ctx, ['context.apply_compaction', 'error']);
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(2, 'recent user two', 'recent assistant two', 80);
|
||||
const outcome = ctx.onceAny(['context.apply_compaction', 'error']);
|
||||
|
||||
await ctx.rpc.beginCompaction({});
|
||||
|
||||
|
|
@ -334,21 +334,21 @@ describe('Agent compaction', () => {
|
|||
);
|
||||
expect(authKeys).toEqual(['fresh-token', 'forced-refresh-token']);
|
||||
expect(tokenCalls).toEqual([undefined, undefined, true]);
|
||||
expect(compactHistory(ctx)).toEqual([
|
||||
expect(ctx.compactHistory()).toEqual([
|
||||
{ role: 'user', text: 'old user one' },
|
||||
{ role: 'assistant', text: 'old assistant one' },
|
||||
{ role: 'user', text: 'recent user two' },
|
||||
{ role: 'assistant', text: 'recent assistant two' },
|
||||
]);
|
||||
|
||||
const retryOutcome = onceAny(ctx, ['context.apply_compaction', 'error']);
|
||||
const retryOutcome = ctx.onceAny(['context.apply_compaction', 'error']);
|
||||
|
||||
await ctx.rpc.beginCompaction({});
|
||||
|
||||
expect(await retryOutcome).toBe('context.apply_compaction');
|
||||
expect(authKeys).toEqual(['fresh-token', 'forced-refresh-token', 'fresh-token']);
|
||||
expect(tokenCalls).toEqual([undefined, undefined, true, undefined]);
|
||||
expect(compactHistory(ctx)).toEqual([
|
||||
expect(ctx.compactHistory()).toEqual([
|
||||
{ role: 'assistant', text: 'Recovered compacted summary.' },
|
||||
{ role: 'user', text: 'recent user two' },
|
||||
{ role: 'assistant', text: 'recent assistant two' },
|
||||
|
|
@ -373,10 +373,10 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
appendExchange(ctx, 2, 'old user two', 'old assistant two', 40);
|
||||
appendExchange(ctx, 3, 'recent user three', 'recent assistant three', 120);
|
||||
const compacted = once(ctx, 'context.apply_compaction');
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(2, 'old user two', 'old assistant two', 40);
|
||||
ctx.appendExchange(3, 'recent user three', 'recent assistant three', 120);
|
||||
const compacted = ctx.once('context.apply_compaction');
|
||||
|
||||
ctx.mockNextResponse({ type: 'text', text: 'Compacted summary.' });
|
||||
ctx.agent.fullCompaction.begin({ source: 'auto', instruction: undefined });
|
||||
|
|
@ -426,14 +426,14 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
appendExchange(ctx, 2, 'recent user two', 'recent assistant two', 80);
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(2, 'recent user two', 'recent assistant two', 80);
|
||||
|
||||
ctx.agent.fullCompaction.begin({ source: 'manual', instruction: undefined });
|
||||
await vi.waitFor(() => {
|
||||
expect(preCompactSignal).toBeInstanceOf(AbortSignal);
|
||||
});
|
||||
const canceled = once(ctx, 'compaction.cancelled');
|
||||
const canceled = ctx.once('compaction.cancelled');
|
||||
ctx.agent.fullCompaction.cancel();
|
||||
await canceled;
|
||||
|
||||
|
|
@ -463,9 +463,9 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
appendExchange(ctx, 2, 'recent user two', 'recent assistant two', 80);
|
||||
const compacted = once(ctx, 'context.apply_compaction');
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(2, 'recent user two', 'recent assistant two', 80);
|
||||
const compacted = ctx.once('context.apply_compaction');
|
||||
|
||||
await ctx.rpc.beginCompaction({});
|
||||
await compacted;
|
||||
|
|
@ -499,9 +499,9 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
appendExchange(ctx, 2, 'recent user two', 'recent assistant two', 80);
|
||||
const compacted = once(ctx, 'context.apply_compaction');
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(2, 'recent user two', 'recent assistant two', 80);
|
||||
const compacted = ctx.once('context.apply_compaction');
|
||||
|
||||
await ctx.rpc.beginCompaction({});
|
||||
await firstEmptySummary.promise;
|
||||
|
|
@ -509,7 +509,7 @@ describe('Agent compaction', () => {
|
|||
await compacted;
|
||||
|
||||
expect(attempts).toBe(3);
|
||||
expect(compactHistory(ctx)).toEqual([
|
||||
expect(ctx.compactHistory()).toEqual([
|
||||
{ role: 'assistant', text: 'Recovered compacted summary.' },
|
||||
{ role: 'user', text: 'recent user two' },
|
||||
{ role: 'assistant', text: 'recent assistant two' },
|
||||
|
|
@ -541,9 +541,9 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
appendExchange(ctx, 2, 'recent user two', 'recent assistant two', 80);
|
||||
const compacted = once(ctx, 'context.apply_compaction');
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(2, 'recent user two', 'recent assistant two', 80);
|
||||
const compacted = ctx.once('context.apply_compaction');
|
||||
|
||||
await ctx.rpc.beginCompaction({});
|
||||
await firstAttemptFailed.promise;
|
||||
|
|
@ -574,9 +574,9 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
appendExchange(ctx, 2, 'recent user two', 'recent assistant two', 80);
|
||||
const cancelled = once(ctx, 'compaction.cancelled');
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(2, 'recent user two', 'recent assistant two', 80);
|
||||
const cancelled = ctx.once('compaction.cancelled');
|
||||
|
||||
await ctx.rpc.beginCompaction({});
|
||||
await firstAttemptFailed.promise;
|
||||
|
|
@ -599,9 +599,9 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
appendExchange(ctx, 2, 'recent user two', 'recent assistant two', 80);
|
||||
const failed = once(ctx, 'error');
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(2, 'recent user two', 'recent assistant two', 80);
|
||||
const failed = ctx.once('error');
|
||||
|
||||
await ctx.rpc.beginCompaction({});
|
||||
await failed;
|
||||
|
|
@ -615,7 +615,7 @@ describe('Agent compaction', () => {
|
|||
]),
|
||||
);
|
||||
expect(eventIndex(events, 'compaction.cancelled')).toBeLessThan(eventIndex(events, 'error'));
|
||||
expect(compactHistory(ctx)).toEqual([
|
||||
expect(ctx.compactHistory()).toEqual([
|
||||
{ role: 'user', text: 'old user one' },
|
||||
{ role: 'assistant', text: 'old assistant one' },
|
||||
{ role: 'user', text: 'recent user two' },
|
||||
|
|
@ -649,9 +649,9 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
appendExchange(ctx, 2, 'recent user two', 'recent assistant two', 80);
|
||||
const failed = once(ctx, 'error');
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(2, 'recent user two', 'recent assistant two', 80);
|
||||
const failed = ctx.once('error');
|
||||
|
||||
await ctx.rpc.beginCompaction({});
|
||||
await failed;
|
||||
|
|
@ -676,7 +676,7 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendRichToolExchange(ctx);
|
||||
ctx.appendRichToolExchange();
|
||||
const compacted = new Promise<void>((resolve) => {
|
||||
ctx.emitter.once('context.apply_compaction', () => {
|
||||
resolve();
|
||||
|
|
@ -724,9 +724,9 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
appendPartiallyResolvedParallelToolExchange(ctx);
|
||||
const compacted = once(ctx, 'context.apply_compaction');
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendPartiallyResolvedParallelToolExchange();
|
||||
const compacted = ctx.once('context.apply_compaction');
|
||||
|
||||
ctx.mockNextResponse({ type: 'text', text: 'Compacted before open tools.' });
|
||||
await ctx.rpc.beginCompaction({ instruction: 'Keep stable facts.' });
|
||||
|
|
@ -755,9 +755,9 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
appendExchange(ctx, 2, 'recent user two', 'recent assistant two', 80);
|
||||
const compacted = once(ctx, 'context.apply_compaction');
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(2, 'recent user two', 'recent assistant two', 80);
|
||||
const compacted = ctx.once('context.apply_compaction');
|
||||
|
||||
ctx.mockNextResponse({ type: 'text', text: 'Compacted prefix.' });
|
||||
await ctx.rpc.beginCompaction({});
|
||||
|
|
@ -785,7 +785,7 @@ describe('Agent compaction', () => {
|
|||
assistant: text "old assistant one"
|
||||
user: text <compaction-instruction>
|
||||
`);
|
||||
expect(compactHistory(ctx)).toMatchInlineSnapshot(`
|
||||
expect(ctx.compactHistory()).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"role": "assistant",
|
||||
|
|
@ -814,9 +814,9 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
appendExchange(ctx, 2, 'recent user two', 'recent assistant two', 80);
|
||||
const canceled = once(ctx, 'full_compaction.cancel');
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(2, 'recent user two', 'recent assistant two', 80);
|
||||
const canceled = ctx.once('full_compaction.cancel');
|
||||
|
||||
ctx.mockNextResponse({ type: 'text', text: 'Stale summary.' });
|
||||
await ctx.rpc.beginCompaction({});
|
||||
|
|
@ -843,7 +843,7 @@ describe('Agent compaction', () => {
|
|||
assistant: text "old assistant one"
|
||||
user: text <compaction-instruction>
|
||||
`);
|
||||
expect(compactHistory(ctx)).toMatchInlineSnapshot(`[]`);
|
||||
expect(ctx.compactHistory()).toMatchInlineSnapshot(`[]`);
|
||||
await ctx.expectResumeMatches();
|
||||
});
|
||||
|
||||
|
|
@ -854,9 +854,9 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 100);
|
||||
appendExchange(ctx, 2, 'old user two', 'old assistant two', 200);
|
||||
appendExchange(ctx, 3, 'recent user three', 'recent assistant three', 950_000);
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 100);
|
||||
ctx.appendExchange(2, 'old user two', 'old assistant two', 200);
|
||||
ctx.appendExchange(3, 'recent user three', 'recent assistant three', 950_000);
|
||||
|
||||
ctx.mockNextResponse({ type: 'text', text: 'Auto compacted summary.' });
|
||||
ctx.mockNextResponse({ type: 'text', text: 'I can answer after compaction.' });
|
||||
|
|
@ -947,7 +947,7 @@ describe('Agent compaction', () => {
|
|||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
ctx.agent.context.appendUserMessage([{ type: 'text', text: 'only pending user' }]);
|
||||
const canceled = once(ctx, 'compaction.cancelled');
|
||||
const canceled = ctx.once('compaction.cancelled');
|
||||
|
||||
await ctx.rpc.beginCompaction({});
|
||||
await canceled;
|
||||
|
|
@ -955,16 +955,16 @@ describe('Agent compaction', () => {
|
|||
expect(ctx.llmCalls).toHaveLength(0);
|
||||
|
||||
ctx.agent.context.clear();
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
appendExchange(ctx, 2, 'recent user two', 'recent assistant two', 80);
|
||||
const compacted = once(ctx, 'context.apply_compaction');
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(2, 'recent user two', 'recent assistant two', 80);
|
||||
const compacted = ctx.once('context.apply_compaction');
|
||||
|
||||
ctx.mockNextResponse({ type: 'text', text: 'Compacted after no-op cancel.' });
|
||||
await ctx.rpc.beginCompaction({});
|
||||
await compacted;
|
||||
|
||||
expect(ctx.llmCalls).toHaveLength(1);
|
||||
expect(compactHistory(ctx)).toEqual([
|
||||
expect(ctx.compactHistory()).toEqual([
|
||||
{ role: 'assistant', text: 'Compacted after no-op cancel.' },
|
||||
{ role: 'user', text: 'recent user two' },
|
||||
{ role: 'assistant', text: 'recent assistant two' },
|
||||
|
|
@ -989,7 +989,7 @@ describe('Agent compaction', () => {
|
|||
max_context_tokens: 32_000,
|
||||
},
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 1_000);
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 1_000);
|
||||
|
||||
ctx.mockNextResponse({ type: 'text', text: 'I can answer without reserved compaction.' });
|
||||
await ctx.rpc.prompt({ input: [{ type: 'text', text: 'small prompt' }] });
|
||||
|
|
@ -1019,7 +1019,7 @@ describe('Agent compaction', () => {
|
|||
max_context_tokens: 2_000,
|
||||
},
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 1_400);
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 1_400);
|
||||
|
||||
ctx.mockNextResponse({ type: 'text', text: 'Reserved compacted summary.' });
|
||||
ctx.mockNextResponse({ type: 'text', text: 'I can answer after reserved compaction.' });
|
||||
|
|
@ -1042,7 +1042,7 @@ describe('Agent compaction', () => {
|
|||
max_context_tokens: 2_000,
|
||||
},
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 1_650);
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 1_650);
|
||||
const oversizedPrompt = `keep-this-pending-verbatim:${'x'.repeat(1_800)}`;
|
||||
|
||||
ctx.mockNextResponse({ type: 'text', text: 'Oversized prompt summary.' });
|
||||
|
|
@ -1069,7 +1069,7 @@ describe('Agent compaction', () => {
|
|||
max_context_tokens: 1_000_000,
|
||||
},
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 840_000);
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 840_000);
|
||||
const pendingPrompt = `ratio-pending-verbatim:${'x'.repeat(60_000)}`;
|
||||
|
||||
ctx.mockNextResponse({ type: 'text', text: 'Ratio compacted summary.' });
|
||||
|
|
@ -1114,7 +1114,7 @@ describe('Agent compaction', () => {
|
|||
provider: CATALOGUED_PROVIDER,
|
||||
modelCapabilities: CATALOGUED_MODEL_CAPABILITIES,
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.newEvents();
|
||||
|
||||
await ctx.rpc.prompt({ input: [{ type: 'text', text: 'Retry after provider overflow' }] });
|
||||
|
|
@ -1197,7 +1197,7 @@ describe('Agent compaction', () => {
|
|||
: { ...resolved, modelCapabilities: UNKNOWN_CAPABILITY };
|
||||
};
|
||||
expect(ctx.agent.config.modelCapabilities.max_context_tokens).toBe(0);
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
ctx.newEvents();
|
||||
|
||||
await ctx.rpc.prompt({ input: [{ type: 'text', text: 'Retry without known model window' }] });
|
||||
|
|
@ -1243,7 +1243,7 @@ describe('Agent compaction', () => {
|
|||
max_context_tokens: 2_000,
|
||||
},
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 20);
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 20);
|
||||
const oversizedPrompt = `uncompactable-pending:${'x'.repeat(9_000)}`;
|
||||
ctx.newEvents();
|
||||
|
||||
|
|
@ -1297,7 +1297,7 @@ describe('Agent compaction', () => {
|
|||
max_context_tokens: 14,
|
||||
},
|
||||
});
|
||||
appendExchange(ctx, 1, 'old user one', 'old assistant one', 1);
|
||||
ctx.appendExchange(1, 'old user one', 'old assistant one', 1);
|
||||
const promptThatFitsWithoutPlaceholder = 'x'.repeat(40);
|
||||
ctx.newEvents();
|
||||
|
||||
|
|
@ -1388,24 +1388,6 @@ afterEach(() => {
|
|||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function once(ctx: TestAgentContext, type: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
ctx.emitter.once(type, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onceAny(ctx: TestAgentContext, types: readonly string[]): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
for (const type of types) {
|
||||
ctx.emitter.once(type, () => {
|
||||
resolve(type);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
|
|
@ -1468,51 +1450,6 @@ function textResult(text: string): Awaited<ReturnType<GenerateFn>> {
|
|||
};
|
||||
}
|
||||
|
||||
function appendExchange(
|
||||
ctx: TestAgentContext,
|
||||
step: number,
|
||||
userText: string,
|
||||
assistantText: string,
|
||||
tokenTotal: number,
|
||||
) {
|
||||
const stepUuid = `step-${String(step)}`;
|
||||
ctx.agent.context.appendUserMessage([{ type: 'text', text: userText }]);
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: { type: 'step.begin', uuid: stepUuid, turnId: '', step },
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'content.part',
|
||||
uuid: `part-${String(step)}`,
|
||||
turnId: '',
|
||||
step,
|
||||
stepUuid,
|
||||
part: {
|
||||
type: 'text',
|
||||
text: assistantText,
|
||||
},
|
||||
},
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'step.end',
|
||||
uuid: stepUuid,
|
||||
turnId: '',
|
||||
step,
|
||||
usage: {
|
||||
inputOther: tokenTotal - 1,
|
||||
output: 1,
|
||||
inputCacheRead: 0,
|
||||
inputCacheCreation: 0,
|
||||
},
|
||||
finishReason: 'end_turn',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const alwaysCompactOnce: CompactionStrategy = {
|
||||
shouldCompact: () => true,
|
||||
shouldBlock: () => true,
|
||||
|
|
@ -1530,13 +1467,6 @@ function missingToolCall(): ToolCall {
|
|||
};
|
||||
}
|
||||
|
||||
function compactHistory(ctx: TestAgentContext) {
|
||||
return ctx.agent.context.history.map((message) => ({
|
||||
role: message.role,
|
||||
text: message.content.map((part) => (part.type === 'text' ? part.text : '')).join(''),
|
||||
}));
|
||||
}
|
||||
|
||||
function testCompactionStrategy(): DefaultCompactionStrategy {
|
||||
return new DefaultCompactionStrategy({
|
||||
triggerRatio: 0.85,
|
||||
|
|
@ -1604,135 +1534,3 @@ function inputHistorySnapshot(history: readonly Message[]): string[] {
|
|||
function normalizeInputText(text: string): string {
|
||||
return text.includes('compact this conversation context') ? '<compaction-instruction>' : text;
|
||||
}
|
||||
|
||||
function appendRichToolExchange(ctx: TestAgentContext) {
|
||||
const stepUuid = 'rich-step';
|
||||
ctx.agent.context.appendUserMessage([
|
||||
{ type: 'text', text: 'inspect this image' },
|
||||
{ type: 'image_url', imageUrl: { url: 'ms://image-1', id: 'image-1' } },
|
||||
]);
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: { type: 'step.begin', uuid: stepUuid, turnId: '', step: 1 },
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'content.part',
|
||||
uuid: 'rich-think',
|
||||
turnId: '',
|
||||
step: 1,
|
||||
stepUuid,
|
||||
part: {
|
||||
type: 'think',
|
||||
think: 'checking metadata',
|
||||
},
|
||||
},
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'content.part',
|
||||
uuid: 'rich-text',
|
||||
turnId: '',
|
||||
step: 1,
|
||||
stepUuid,
|
||||
part: {
|
||||
type: 'text',
|
||||
text: 'I will call Lookup.',
|
||||
},
|
||||
},
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.call',
|
||||
uuid: 'rich-tool-call',
|
||||
turnId: '',
|
||||
step: 1,
|
||||
stepUuid,
|
||||
toolCallId: 'call_lookup',
|
||||
name: 'Lookup',
|
||||
args: {
|
||||
query: 'moon',
|
||||
limit: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'step.end',
|
||||
uuid: stepUuid,
|
||||
turnId: '',
|
||||
step: 1,
|
||||
usage: {
|
||||
inputOther: 50,
|
||||
output: 10,
|
||||
inputCacheRead: 0,
|
||||
inputCacheCreation: 0,
|
||||
},
|
||||
finishReason: 'tool_use',
|
||||
},
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.result',
|
||||
parentUuid: 'rich-tool-call',
|
||||
toolCallId: 'call_lookup',
|
||||
result: {
|
||||
output: [
|
||||
{ type: 'text', text: 'lookup result' },
|
||||
{ type: 'video_url', videoUrl: { url: 'ms://video-1', id: 'video-1' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function appendPartiallyResolvedParallelToolExchange(ctx: TestAgentContext) {
|
||||
const stepUuid = 'partial-tool-step';
|
||||
ctx.agent.context.appendUserMessage([{ type: 'text', text: 'run both tools' }]);
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: { type: 'step.begin', uuid: stepUuid, turnId: '', step: 2 },
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.call',
|
||||
uuid: 'call_open_one',
|
||||
turnId: '',
|
||||
step: 2,
|
||||
stepUuid,
|
||||
toolCallId: 'call_open_one',
|
||||
name: 'LookupOne',
|
||||
args: { query: 'one' },
|
||||
},
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.call',
|
||||
uuid: 'call_open_two',
|
||||
turnId: '',
|
||||
step: 2,
|
||||
stepUuid,
|
||||
toolCallId: 'call_open_two',
|
||||
name: 'LookupTwo',
|
||||
args: { query: 'two' },
|
||||
},
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.result',
|
||||
parentUuid: 'call_open_one',
|
||||
toolCallId: 'call_open_one',
|
||||
result: {
|
||||
output: 'one result',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { sliceCompleteMessages } from '../../src/agent/context/complete-slice';
|
|||
import { renderNotificationXml } from '../../src/agent/context/notification-xml';
|
||||
import { project } from '../../src/agent/context/projector';
|
||||
import { estimateTokensForMessages } from '../../src/utils/tokens';
|
||||
import type { TestAgentContext } from './harness/agent';
|
||||
import { testAgent } from './harness/agent';
|
||||
|
||||
describe('Agent context', () => {
|
||||
|
|
@ -159,8 +158,8 @@ describe('Agent context', () => {
|
|||
it('projects user, assistant, tool call, and tool result records into LLM history', async () => {
|
||||
const ctx = testAgent();
|
||||
ctx.configure();
|
||||
appendAssistantText(ctx, 1, 'earlier assistant');
|
||||
appendToolExchange(ctx);
|
||||
ctx.appendAssistantText(1, 'earlier assistant');
|
||||
ctx.appendToolExchange();
|
||||
|
||||
ctx.mockNextResponse({ type: 'text', text: 'done' });
|
||||
await ctx.rpc.prompt({ input: [{ type: 'text', text: 'continue' }] });
|
||||
|
|
@ -302,7 +301,7 @@ describe('Agent context', () => {
|
|||
ctx.configure();
|
||||
|
||||
ctx.agent.context.appendUserMessage([{ type: 'text', text: 'old prompt' }]);
|
||||
appendPartiallyResolvedParallelToolExchange(ctx);
|
||||
ctx.appendContextPartiallyResolvedParallelToolExchange();
|
||||
|
||||
ctx.agent.context.appendSystemReminder('first reminder', {
|
||||
kind: 'injection',
|
||||
|
|
@ -403,7 +402,7 @@ describe('Agent context', () => {
|
|||
it('includes new user messages as pending until the next usage update', () => {
|
||||
const ctx = testAgent();
|
||||
ctx.configure();
|
||||
appendAssistantTextWithUsage(ctx, 1, 'previous answer', 1_000);
|
||||
ctx.appendAssistantTextWithUsage(1, 'previous answer', 1_000);
|
||||
expect(ctx.agent.context.tokenCountWithPending).toBe(1_000);
|
||||
|
||||
ctx.agent.context.appendUserMessage([{ type: 'text', text: 'next user prompt'.repeat(20) }]);
|
||||
|
|
@ -558,161 +557,6 @@ describe('Agent context notification projection', () => {
|
|||
});
|
||||
});
|
||||
|
||||
function appendAssistantText(ctx: TestAgentContext, step: number, text: string) {
|
||||
appendAssistantTextWithUsage(ctx, step, text);
|
||||
}
|
||||
|
||||
function appendAssistantTextWithUsage(
|
||||
ctx: TestAgentContext,
|
||||
step: number,
|
||||
text: string,
|
||||
tokenTotal?: number,
|
||||
) {
|
||||
const stepUuid = `context-step-${String(step)}`;
|
||||
const usage =
|
||||
tokenTotal === undefined
|
||||
? undefined
|
||||
: {
|
||||
inputOther: tokenTotal - 1,
|
||||
output: 1,
|
||||
inputCacheRead: 0,
|
||||
inputCacheCreation: 0,
|
||||
};
|
||||
ctx.agent.context.appendUserMessage([{ type: 'text', text: `user before step ${String(step)}` }]);
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: { type: 'step.begin', uuid: stepUuid, turnId: '', step },
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'content.part',
|
||||
uuid: `context-part-${String(step)}`,
|
||||
turnId: '',
|
||||
step,
|
||||
stepUuid,
|
||||
part: {
|
||||
type: 'text',
|
||||
text,
|
||||
},
|
||||
},
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'step.end',
|
||||
uuid: stepUuid,
|
||||
turnId: '',
|
||||
step,
|
||||
usage,
|
||||
finishReason: 'end_turn',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function appendToolExchange(ctx: TestAgentContext) {
|
||||
const stepUuid = 'context-tool-step';
|
||||
ctx.agent.context.appendUserMessage([{ type: 'text', text: 'lookup something' }]);
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: { type: 'step.begin', uuid: stepUuid, turnId: '', step: 2 },
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'content.part',
|
||||
uuid: 'context-tool-part',
|
||||
turnId: '',
|
||||
step: 2,
|
||||
stepUuid,
|
||||
part: {
|
||||
type: 'text',
|
||||
text: 'I will call Lookup.',
|
||||
},
|
||||
},
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.call',
|
||||
uuid: 'context-tool-call',
|
||||
turnId: '',
|
||||
step: 2,
|
||||
stepUuid,
|
||||
toolCallId: 'call_lookup',
|
||||
name: 'Lookup',
|
||||
args: {
|
||||
query: 'moon',
|
||||
},
|
||||
},
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'step.end',
|
||||
uuid: stepUuid,
|
||||
turnId: '',
|
||||
step: 2,
|
||||
finishReason: 'tool_use',
|
||||
},
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.result',
|
||||
parentUuid: 'context-tool-call',
|
||||
toolCallId: 'call_lookup',
|
||||
result: { output: 'lookup result' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function appendPartiallyResolvedParallelToolExchange(ctx: TestAgentContext) {
|
||||
const stepUuid = 'context-partial-tool-step';
|
||||
ctx.agent.context.appendUserMessage([{ type: 'text', text: 'run both tools' }]);
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: { type: 'step.begin', uuid: stepUuid, turnId: '', step: 2 },
|
||||
});
|
||||
for (const [toolCallId, name] of [
|
||||
['call_open_one', 'LookupOne'],
|
||||
['call_open_two', 'LookupTwo'],
|
||||
] as const) {
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.call',
|
||||
uuid: toolCallId,
|
||||
turnId: '',
|
||||
step: 2,
|
||||
stepUuid,
|
||||
toolCallId,
|
||||
name,
|
||||
args: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'step.end',
|
||||
uuid: stepUuid,
|
||||
turnId: '',
|
||||
step: 2,
|
||||
finishReason: 'tool_use',
|
||||
},
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.result',
|
||||
parentUuid: 'call_open_one',
|
||||
toolCallId: 'call_open_one',
|
||||
result: { output: 'one result' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function userMessage(text: string): Message {
|
||||
return {
|
||||
role: 'user',
|
||||
|
|
|
|||
|
|
@ -275,6 +275,436 @@ export class AgentTestContext {
|
|||
}
|
||||
}
|
||||
|
||||
once(type: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this.emitter.once(type, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onceAny(types: readonly string[]): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
for (const type of types) {
|
||||
this.emitter.once(type, () => {
|
||||
resolve(type);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
appendExchange(
|
||||
step: number,
|
||||
userText: string,
|
||||
assistantText: string,
|
||||
tokenTotal: number,
|
||||
): void {
|
||||
const stepUuid = `step-${String(step)}`;
|
||||
this.agent.context.appendUserMessage([{ type: 'text', text: userText }]);
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: { type: 'step.begin', uuid: stepUuid, turnId: '', step },
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'content.part',
|
||||
uuid: `part-${String(step)}`,
|
||||
turnId: '',
|
||||
step,
|
||||
stepUuid,
|
||||
part: {
|
||||
type: 'text',
|
||||
text: assistantText,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'step.end',
|
||||
uuid: stepUuid,
|
||||
turnId: '',
|
||||
step,
|
||||
usage: {
|
||||
inputOther: tokenTotal - 1,
|
||||
output: 1,
|
||||
inputCacheRead: 0,
|
||||
inputCacheCreation: 0,
|
||||
},
|
||||
finishReason: 'end_turn',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
appendAssistantText(step: number, text: string): void {
|
||||
this.appendAssistantTextWithUsage(step, text);
|
||||
}
|
||||
|
||||
appendAssistantTextWithUsage(step: number, text: string, tokenTotal?: number): void {
|
||||
const stepUuid = `context-step-${String(step)}`;
|
||||
const usage =
|
||||
tokenTotal === undefined
|
||||
? undefined
|
||||
: {
|
||||
inputOther: tokenTotal - 1,
|
||||
output: 1,
|
||||
inputCacheRead: 0,
|
||||
inputCacheCreation: 0,
|
||||
};
|
||||
this.agent.context.appendUserMessage([
|
||||
{ type: 'text', text: `user before step ${String(step)}` },
|
||||
]);
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: { type: 'step.begin', uuid: stepUuid, turnId: '', step },
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'content.part',
|
||||
uuid: `context-part-${String(step)}`,
|
||||
turnId: '',
|
||||
step,
|
||||
stepUuid,
|
||||
part: {
|
||||
type: 'text',
|
||||
text,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'step.end',
|
||||
uuid: stepUuid,
|
||||
turnId: '',
|
||||
step,
|
||||
usage,
|
||||
finishReason: 'end_turn',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
appendAssistantTurn(step: number, text: string): void {
|
||||
const stepUuid = `plan-injection-step-${String(step)}`;
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: { type: 'step.begin', uuid: stepUuid, turnId: '', step },
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'content.part',
|
||||
uuid: `plan-injection-part-${String(step)}`,
|
||||
turnId: '',
|
||||
step,
|
||||
stepUuid,
|
||||
part: { type: 'text', text },
|
||||
},
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'step.end',
|
||||
uuid: stepUuid,
|
||||
turnId: '',
|
||||
step,
|
||||
finishReason: 'end_turn',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
appendToolExchange(): void {
|
||||
const stepUuid = 'context-tool-step';
|
||||
this.agent.context.appendUserMessage([{ type: 'text', text: 'lookup something' }]);
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: { type: 'step.begin', uuid: stepUuid, turnId: '', step: 2 },
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'content.part',
|
||||
uuid: 'context-tool-part',
|
||||
turnId: '',
|
||||
step: 2,
|
||||
stepUuid,
|
||||
part: {
|
||||
type: 'text',
|
||||
text: 'I will call Lookup.',
|
||||
},
|
||||
},
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.call',
|
||||
uuid: 'context-tool-call',
|
||||
turnId: '',
|
||||
step: 2,
|
||||
stepUuid,
|
||||
toolCallId: 'call_lookup',
|
||||
name: 'Lookup',
|
||||
args: {
|
||||
query: 'moon',
|
||||
},
|
||||
},
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'step.end',
|
||||
uuid: stepUuid,
|
||||
turnId: '',
|
||||
step: 2,
|
||||
finishReason: 'tool_use',
|
||||
},
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.result',
|
||||
parentUuid: 'context-tool-call',
|
||||
toolCallId: 'call_lookup',
|
||||
result: { output: 'lookup result' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
appendUnresolvedToolExchange(resolvedToolResults: 0 | 1): void {
|
||||
const stepUuid = `unresolved-tool-step-${String(resolvedToolResults)}`;
|
||||
this.agent.context.appendUserMessage([{ type: 'text', text: 'run unresolved tools' }]);
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: { type: 'step.begin', uuid: stepUuid, turnId: '', step: 2 },
|
||||
});
|
||||
for (const [toolCallId, name] of [
|
||||
['call_unresolved_one', 'LookupOne'],
|
||||
['call_unresolved_two', 'LookupTwo'],
|
||||
] as const) {
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.call',
|
||||
uuid: toolCallId,
|
||||
turnId: '',
|
||||
step: 2,
|
||||
stepUuid,
|
||||
toolCallId,
|
||||
name,
|
||||
args: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'step.end',
|
||||
uuid: stepUuid,
|
||||
turnId: '',
|
||||
step: 2,
|
||||
finishReason: 'tool_use',
|
||||
},
|
||||
});
|
||||
if (resolvedToolResults === 1) {
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.result',
|
||||
parentUuid: 'call_unresolved_one',
|
||||
toolCallId: 'call_unresolved_one',
|
||||
result: { output: 'one result' },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
appendRichToolExchange(): void {
|
||||
const stepUuid = 'rich-step';
|
||||
this.agent.context.appendUserMessage([
|
||||
{ type: 'text', text: 'inspect this image' },
|
||||
{ type: 'image_url', imageUrl: { url: 'ms://image-1', id: 'image-1' } },
|
||||
]);
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: { type: 'step.begin', uuid: stepUuid, turnId: '', step: 1 },
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'content.part',
|
||||
uuid: 'rich-think',
|
||||
turnId: '',
|
||||
step: 1,
|
||||
stepUuid,
|
||||
part: {
|
||||
type: 'think',
|
||||
think: 'checking metadata',
|
||||
},
|
||||
},
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'content.part',
|
||||
uuid: 'rich-text',
|
||||
turnId: '',
|
||||
step: 1,
|
||||
stepUuid,
|
||||
part: {
|
||||
type: 'text',
|
||||
text: 'I will call Lookup.',
|
||||
},
|
||||
},
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.call',
|
||||
uuid: 'rich-tool-call',
|
||||
turnId: '',
|
||||
step: 1,
|
||||
stepUuid,
|
||||
toolCallId: 'call_lookup',
|
||||
name: 'Lookup',
|
||||
args: {
|
||||
query: 'moon',
|
||||
limit: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'step.end',
|
||||
uuid: stepUuid,
|
||||
turnId: '',
|
||||
step: 1,
|
||||
usage: {
|
||||
inputOther: 50,
|
||||
output: 10,
|
||||
inputCacheRead: 0,
|
||||
inputCacheCreation: 0,
|
||||
},
|
||||
finishReason: 'tool_use',
|
||||
},
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.result',
|
||||
parentUuid: 'rich-tool-call',
|
||||
toolCallId: 'call_lookup',
|
||||
result: {
|
||||
output: [
|
||||
{ type: 'text', text: 'lookup result' },
|
||||
{ type: 'video_url', videoUrl: { url: 'ms://video-1', id: 'video-1' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
appendContextPartiallyResolvedParallelToolExchange(): void {
|
||||
const stepUuid = 'context-partial-tool-step';
|
||||
this.agent.context.appendUserMessage([{ type: 'text', text: 'run both tools' }]);
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: { type: 'step.begin', uuid: stepUuid, turnId: '', step: 2 },
|
||||
});
|
||||
for (const [toolCallId, name] of [
|
||||
['call_open_one', 'LookupOne'],
|
||||
['call_open_two', 'LookupTwo'],
|
||||
] as const) {
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.call',
|
||||
uuid: toolCallId,
|
||||
turnId: '',
|
||||
step: 2,
|
||||
stepUuid,
|
||||
toolCallId,
|
||||
name,
|
||||
args: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'step.end',
|
||||
uuid: stepUuid,
|
||||
turnId: '',
|
||||
step: 2,
|
||||
finishReason: 'tool_use',
|
||||
},
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.result',
|
||||
parentUuid: 'call_open_one',
|
||||
toolCallId: 'call_open_one',
|
||||
result: { output: 'one result' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
appendPartiallyResolvedParallelToolExchange(): void {
|
||||
const stepUuid = 'partial-tool-step';
|
||||
this.agent.context.appendUserMessage([{ type: 'text', text: 'run both tools' }]);
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: { type: 'step.begin', uuid: stepUuid, turnId: '', step: 2 },
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.call',
|
||||
uuid: 'call_open_one',
|
||||
turnId: '',
|
||||
step: 2,
|
||||
stepUuid,
|
||||
toolCallId: 'call_open_one',
|
||||
name: 'LookupOne',
|
||||
args: { query: 'one' },
|
||||
},
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.call',
|
||||
uuid: 'call_open_two',
|
||||
turnId: '',
|
||||
step: 2,
|
||||
stepUuid,
|
||||
toolCallId: 'call_open_two',
|
||||
name: 'LookupTwo',
|
||||
args: { query: 'two' },
|
||||
},
|
||||
});
|
||||
this.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'tool.result',
|
||||
parentUuid: 'call_open_one',
|
||||
toolCallId: 'call_open_one',
|
||||
result: {
|
||||
output: 'one result',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
compactHistory(): Array<{ readonly role: string; readonly text: string }> {
|
||||
return this.agent.context.history.map((message) => ({
|
||||
role: message.role,
|
||||
text: message.content.map((part) => (part.type === 'text' ? part.text : '')).join(''),
|
||||
}));
|
||||
}
|
||||
|
||||
async expectResumeMatches(): Promise<void> {
|
||||
const resumed = testAgent({
|
||||
runtime: {
|
||||
|
|
|
|||
|
|
@ -536,8 +536,8 @@ describe('plan mode injection cadence', () => {
|
|||
await ctx.agent.injection.inject();
|
||||
expect(ctx.agent.context.history).toHaveLength(afterFull);
|
||||
|
||||
appendAssistantTurn(ctx, 1, 'assistant one');
|
||||
appendAssistantTurn(ctx, 2, 'assistant two');
|
||||
ctx.appendAssistantTurn(1, 'assistant one');
|
||||
ctx.appendAssistantTurn(2, 'assistant two');
|
||||
await ctx.agent.injection.inject();
|
||||
|
||||
expect(lastUserText(ctx.agent.context.history)).toContain('Plan mode still active');
|
||||
|
|
@ -585,35 +585,6 @@ function delay(ms: number): Promise<void> {
|
|||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function appendAssistantTurn(ctx: ReturnType<typeof testAgent>, step: number, text: string): void {
|
||||
const stepUuid = `plan-injection-step-${String(step)}`;
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: { type: 'step.begin', uuid: stepUuid, turnId: '', step },
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'content.part',
|
||||
uuid: `plan-injection-part-${String(step)}`,
|
||||
turnId: '',
|
||||
step,
|
||||
stepUuid,
|
||||
part: { type: 'text', text },
|
||||
},
|
||||
});
|
||||
ctx.dispatch({
|
||||
type: 'context.append_loop_event',
|
||||
event: {
|
||||
type: 'step.end',
|
||||
uuid: stepUuid,
|
||||
turnId: '',
|
||||
step,
|
||||
finishReason: 'end_turn',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function lastUserText(history: readonly { role: string; content: readonly unknown[] }[]): string {
|
||||
const message = history.findLast((item) => item.role === 'user');
|
||||
if (message === undefined) return '';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue