Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions packages/runtime/src/__tests__/latest-context-commit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ import { join } from 'node:path';
import { test } from 'node:test';
import { MockLanguageModelV4, simulateReadableStream } from 'ai/test';
import type { LanguageModelV4StreamPart } from '@ai-sdk/provider';
import { z } from 'zod';
import {
decodeModelCallAttempt,
PROMPT_COMPOSITION_MAX_TOOLS,
type ModelCallAttempt,
type PromptComposition,
} from '@maka/core/model-call-attempt';
import { createSqliteAgentRunStore } from '@maka/storage/agent-run-store';
import { createWorkspaceRuntimeStore } from '@maka/storage/runtime-event-persistence';
Expand Down Expand Up @@ -78,7 +80,11 @@ test('a real send seals its observation into SQLite and reconstructs it after re
apiKey: 'sk-test',
modelId: 'mock-model-id',
modelFactory: () => answeringModel(),
tools: [],
// More tools than the composition names, because the cap is only a
// real cap when something is actually over it: with an empty list the
// row that crosses the chain has no tool rows at all, and every
// assertion about them holds for free.
tools: overflowingToolset(),
// The seams the kernel hands a real backend, forwarded exactly as the
// production composition forwards them — this is the hop that broke.
...(ctx.recordModelCallAttempt
Expand Down Expand Up @@ -137,6 +143,7 @@ test('a real send seals its observation into SQLite and reconstructs it after re
'and the request describes what it was made of',
);
assert.equal(scanned, 0, 'the row was committed by the send, not rebuilt by the read');
assertToolsAccountedFor(diagnostics.composition);

await manager.stopSession(session.id, { source: 'stop_button' });
runStore.close?.();
Expand All @@ -157,7 +164,7 @@ test('a real send seals its observation into SQLite and reconstructs it after re
const composition = canonicalAttempts[0]?.promptComposition;
assert.ok(composition);
assert.ok(composition.segments.length > 0);
assert.ok((composition.tools?.length ?? 0) <= PROMPT_COMPOSITION_MAX_TOOLS);
assertToolsAccountedFor(composition);

let coldScans = 0;
const cold = await readLatestContextDiagnostics(
Expand Down Expand Up @@ -268,6 +275,51 @@ test('a turn aborted before dispatch does not create a canonical sent attempt',
}
});

const TOOLS_OVER_THE_CAP = 5;

/** More tools than the composition names, each a different size. */
function overflowingToolset() {
return Array.from({ length: PROMPT_COMPOSITION_MAX_TOOLS + TOOLS_OVER_THE_CAP }, (_, index) => ({
name: `tool-${String(index).padStart(3, '0')}`,
description: `probe ${'d'.repeat(index * 8)}`,
parameters: z.object({ q: z.string() }),
impl: async () => ({ ok: true }),
}));
}

/**
* The tool rows, checked the way the panel has to be able to trust them.
*
* The cap is what keeps one MCP server's 1000 tools out of every attempt, so
* the row a reader gets is by design not the whole toolset. What it must still
* be is honest about that: the named ones are the largest, the rest are
* counted, and the two together account for every tool byte the segment claims.
*/
function assertToolsAccountedFor(composition: PromptComposition | undefined): void {
assert.ok(composition);
const tools = composition.tools ?? [];
assert.equal(tools.length, PROMPT_COMPOSITION_MAX_TOOLS);
assert.equal(composition.remainingTools?.count, TOOLS_OVER_THE_CAP);

const namedBytes = tools.reduce((total, tool) => total + tool.bytes, 0);
assert.equal(
namedBytes + (composition.remainingTools?.bytes ?? 0),
composition.segments.find((segment) => segment.kind === 'tool_definitions')?.bytes,
'the named tools and the remainder add up to the tool bytes the segment reports',
);

// Largest first, so the top of the list is what a reader could remove.
const largest = Array.from(
{ length: PROMPT_COMPOSITION_MAX_TOOLS },
(_, index) =>
`tool-${String(PROMPT_COMPOSITION_MAX_TOOLS + TOOLS_OVER_THE_CAP - 1 - index).padStart(3, '0')}`,
);
assert.deepEqual(
tools.map((tool) => tool.name),
largest,
);
}

function answeringModel(): MockLanguageModelV4 {
return new MockLanguageModelV4({
doStream: async () => ({
Expand Down