moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,328 @@
/**
* Task 2.5 Stage 2-Retry §1.4 — agent-loop tool-exhaustion fallback tests.
*
* Stage 2 N=20 showed 2/20 agentic instances reached maxTurns=3 with every
* turn spent on a search_memory call, leaving `resp.content` empty; the
* judge scored those as incorrect. Stage 2-Retry §1.4 adds a runtime-side
* forced-answer fallback in the agentic cell wrapper: on empty-content +
* non-empty toolsUsed, the cell makes ONE additional direct LLM call
* (no tools, SYSTEM_AGENTIC_FORCED_FALLBACK) with the accumulated search
* context and returns that answer.
*
* Test matrix per brief:
* (a) normal 1-call-1-answer → fallback NOT fired
* (b) 2-call-1-answer → fallback NOT fired
* (c) 3-call-1-answer → fallback NOT fired
* (d) 3 tools + empty content → fallback FIRED, returns forced answer
* Plus:
* (e) fallback preserves accumulated tool context in the user prompt
* (f) fallback counts its tokens into returned cost (no cost-leak)
*/
import { describe, expect, it } from 'vitest';
import type { AgentLoopConfig, AgentResponse } from '@waggle/agent';
import type { LlmCallInput, LlmCallResult, LlmClient } from '../src/llm.js';
import type { DatasetInstance, ModelSpec } from '../src/types.js';
import {
cells,
SYSTEM_AGENTIC,
SYSTEM_AGENTIC_FORCED_FALLBACK,
} from '../src/cells.js';
import { createSubstrate } from '../src/substrate.js';
import type { Embedder } from '@waggle/core';
const VEC_DIMS = 1024;
function createFakeEmbedder(): Embedder {
// Deterministic hash-seeded 1024-dim embedder — same pattern as other tests.
const fnv1a = (s: string): number => {
let h = 2166136261 >>> 0;
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i);
h = Math.imul(h, 16777619) >>> 0;
}
return h || 1;
};
const embedOne = (text: string): Float32Array => {
let state = fnv1a(text);
const v = new Float32Array(VEC_DIMS);
for (let i = 0; i < VEC_DIMS; i++) {
state ^= state << 13; state >>>= 0;
state ^= state >>> 17;
state ^= state << 5; state >>>= 0;
v[i] = ((state >>> 0) / 0x100000000) * 2 - 1;
}
let mag = 0;
for (let i = 0; i < VEC_DIMS; i++) mag += v[i] * v[i];
mag = Math.sqrt(mag);
if (mag > 0) for (let i = 0; i < VEC_DIMS; i++) v[i] /= mag;
return v;
};
return {
dimensions: VEC_DIMS,
async embed(t) { return embedOne(t); },
async embedBatch(ts) { return ts.map(embedOne); },
};
}
const MODEL: ModelSpec = {
id: 'test-subject',
displayName: 'Test',
provider: 'alibaba',
litellmModel: 'test/model',
pricePerMillionInput: 0.1,
pricePerMillionOutput: 0.4,
contextWindow: 32_000,
};
const INSTANCE: DatasetInstance = {
instance_id: 'test_q001',
question: 'When did the event happen?',
context: 'irrelevant for this test',
expected: ['2023'],
conversation_id: 'conv-test',
};
/** Programmable mock runAgentLoop that simulates different tool-use and
* content patterns per-test. Also exercises the onToolResult callback so
* the cell wrapper's context capture is tested end-to-end. */
function makeScriptedAgentLoop(script: {
toolCallResults: string[]; // results the agent-loop would return to the agent
finalContent: string; // final resp.content
usageInput?: number;
usageOutput?: number;
}): (config: AgentLoopConfig) => Promise<AgentResponse> {
return async (config: AgentLoopConfig): Promise<AgentResponse> => {
// Fire onToolResult for each simulated tool call in order. Triggers the
// cell wrapper's capturedToolResults accumulator.
for (let i = 0; i < script.toolCallResults.length; i++) {
config.onToolResult?.('search_memory', { query: `simulated-${i}` }, script.toolCallResults[i]);
}
return {
content: script.finalContent,
toolsUsed: script.toolCallResults.map(() => 'search_memory'),
usage: {
inputTokens: script.usageInput ?? 200,
outputTokens: script.usageOutput ?? 10,
},
};
};
}
/** Capturing LlmClient — records every direct llm.call made by the cell
* wrapper. The forced-fallback pass hits this (not the mock agent-loop). */
function makeCapturingLlm(response: Partial<LlmCallResult> = {}): {
client: LlmClient;
calls: LlmCallInput[];
} {
const calls: LlmCallInput[] = [];
const client: LlmClient = {
async call(input) {
calls.push(input);
return {
text: response.text ?? 'FORCED_ANSWER',
inputTokens: response.inputTokens ?? 500,
outputTokens: response.outputTokens ?? 3,
latencyMs: response.latencyMs ?? 50,
costUsd: response.costUsd ?? 0.0002,
failureMode: response.failureMode ?? null,
};
},
};
return { client, calls };
}
describe('agent-loop tool-exhaustion fallback — Stage 2-Retry §1.4', () => {
it('case (a): 1 tool call + answer → fallback NOT fired', async () => {
const substrate = createSubstrate({ embedder: createFakeEmbedder() });
try {
const runFn = makeScriptedAgentLoop({
toolCallResults: ['[1] Caroline: painted in 2023'],
finalContent: '2023',
});
const { client, calls } = makeCapturingLlm();
const result = await cells.agentic({
instance: INSTANCE, model: MODEL, llm: client, turnId: 't',
substrate, litellm: { url: 'u', apiKey: 'k' }, runAgentLoopFn: runFn,
});
expect(result.text).toBe('2023');
expect(result.failureMode).toBeNull();
// No fallback call.
expect(calls).toHaveLength(0);
} finally {
substrate.close();
}
});
it('case (b): 2 tool calls + answer → fallback NOT fired', async () => {
const substrate = createSubstrate({ embedder: createFakeEmbedder() });
try {
const runFn = makeScriptedAgentLoop({
toolCallResults: [
'[1] some result',
'[2] refined result',
],
finalContent: '2023',
});
const { client, calls } = makeCapturingLlm();
const result = await cells.agentic({
instance: INSTANCE, model: MODEL, llm: client, turnId: 't',
substrate, litellm: { url: 'u', apiKey: 'k' }, runAgentLoopFn: runFn,
});
expect(result.text).toBe('2023');
expect(calls).toHaveLength(0);
} finally {
substrate.close();
}
});
it('case (c): 3 tool calls + answer (all turns used, but content present) → fallback NOT fired', async () => {
const substrate = createSubstrate({ embedder: createFakeEmbedder() });
try {
const runFn = makeScriptedAgentLoop({
toolCallResults: ['r1', 'r2', 'r3'],
finalContent: 'best-effort-answer',
});
const { client, calls } = makeCapturingLlm();
const result = await cells.agentic({
instance: INSTANCE, model: MODEL, llm: client, turnId: 't',
substrate, litellm: { url: 'u', apiKey: 'k' }, runAgentLoopFn: runFn,
});
expect(result.text).toBe('best-effort-answer');
expect(calls).toHaveLength(0);
} finally {
substrate.close();
}
});
it('case (d): 3 tool calls + EMPTY content → fallback FIRED, forced answer returned', async () => {
const substrate = createSubstrate({ embedder: createFakeEmbedder() });
try {
const runFn = makeScriptedAgentLoop({
toolCallResults: ['r1', 'r2', 'r3'],
finalContent: '', // agent exhausted turns, no answer
});
const { client, calls } = makeCapturingLlm({ text: 'FALLBACK-2023' });
const result = await cells.agentic({
instance: INSTANCE, model: MODEL, llm: client, turnId: 't',
substrate, litellm: { url: 'u', apiKey: 'k' }, runAgentLoopFn: runFn,
});
expect(result.text).toBe('FALLBACK-2023');
expect(result.failureMode).toBeNull();
// Exactly one fallback call.
expect(calls).toHaveLength(1);
expect(calls[0].systemPrompt).toBe(SYSTEM_AGENTIC_FORCED_FALLBACK);
} finally {
substrate.close();
}
});
it('case (e): fallback user prompt includes question + every captured tool result', async () => {
const substrate = createSubstrate({ embedder: createFakeEmbedder() });
try {
const runFn = makeScriptedAgentLoop({
toolCallResults: [
'search_memory hit A',
'search_memory hit B',
'search_memory hit C',
],
finalContent: '',
});
const { client, calls } = makeCapturingLlm({ text: 'forced' });
await cells.agentic({
instance: INSTANCE, model: MODEL, llm: client, turnId: 't',
substrate, litellm: { url: 'u', apiKey: 'k' }, runAgentLoopFn: runFn,
});
const user = calls[0].userPrompt;
expect(user).toContain(INSTANCE.question);
expect(user).toContain('search_memory hit A');
expect(user).toContain('search_memory hit B');
expect(user).toContain('search_memory hit C');
// Each call result labelled with its call number.
expect(user).toContain('## search_memory call 1');
expect(user).toContain('## search_memory call 2');
expect(user).toContain('## search_memory call 3');
} finally {
substrate.close();
}
});
it('case (f): fallback token counts fold into the returned LlmCallResult cost', async () => {
const substrate = createSubstrate({ embedder: createFakeEmbedder() });
try {
const runFn = makeScriptedAgentLoop({
toolCallResults: ['r1', 'r2', 'r3'],
finalContent: '',
usageInput: 1_000_000,
usageOutput: 100_000,
});
// Fallback call accounts for another 500_000 input + 50_000 output.
const { client, calls } = makeCapturingLlm({
text: 'forced-final',
inputTokens: 500_000,
outputTokens: 50_000,
});
const result = await cells.agentic({
instance: INSTANCE, model: MODEL, llm: client, turnId: 't',
substrate, litellm: { url: 'u', apiKey: 'k' }, runAgentLoopFn: runFn,
});
// Token sums: 1_500_000 input, 150_000 output.
expect(result.inputTokens).toBe(1_500_000);
expect(result.outputTokens).toBe(150_000);
// Cost = (1.5 × $0.1/M input) + (0.15 × $0.4/M output) = $0.15 + $0.06 = $0.21
expect(result.costUsd).toBeCloseTo(0.21, 5);
// And one fallback llm.call happened.
expect(calls).toHaveLength(1);
} finally {
substrate.close();
}
});
it('case (g): empty content + ZERO tool calls → fallback does NOT fire (honest abstain)', async () => {
const substrate = createSubstrate({ embedder: createFakeEmbedder() });
try {
// Agent answered directly without searching — and its content was empty.
// This is an honest abstain case, not tool-exhaustion; no fallback.
const runFn = makeScriptedAgentLoop({
toolCallResults: [],
finalContent: '',
});
const { client, calls } = makeCapturingLlm();
const result = await cells.agentic({
instance: INSTANCE, model: MODEL, llm: client, turnId: 't',
substrate, litellm: { url: 'u', apiKey: 'k' }, runAgentLoopFn: runFn,
});
expect(result.text).toBe('');
expect(calls).toHaveLength(0);
} finally {
substrate.close();
}
});
it('SYSTEM_AGENTIC_FORCED_FALLBACK is exported and non-empty', () => {
expect(SYSTEM_AGENTIC_FORCED_FALLBACK.length).toBeGreaterThan(50);
expect(SYSTEM_AGENTIC_FORCED_FALLBACK).toContain('commit to your best');
expect(SYSTEM_AGENTIC_FORCED_FALLBACK).toContain('Do not call tools');
});
it('SYSTEM_AGENTIC (softened Stage 2-Retry) is still the prompt agentic uses', async () => {
const substrate = createSubstrate({ embedder: createFakeEmbedder() });
try {
let capturedSystem = '';
const runFn = async (config: AgentLoopConfig): Promise<AgentResponse> => {
capturedSystem = config.systemPrompt;
return { content: 'x', toolsUsed: [], usage: { inputTokens: 0, outputTokens: 0 } };
};
const { client } = makeCapturingLlm();
await cells.agentic({
instance: INSTANCE, model: MODEL, llm: client, turnId: 't',
substrate, litellm: { url: 'u', apiKey: 'k' }, runAgentLoopFn: runFn,
});
expect(capturedSystem).toBe(SYSTEM_AGENTIC);
// Sanity check on the softened surface language that diverges from Stage 1.
expect(capturedSystem).toContain('Protocol (you SHOULD follow)');
expect(capturedSystem).not.toContain('Protocol (you MUST follow)');
expect(capturedSystem).toContain('general knowledge');
} finally {
substrate.close();
}
});
});