Skip to content
Open
Show file tree
Hide file tree
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
85 changes: 76 additions & 9 deletions packages/core/src/__tests__/run-composition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
createRequestCompositionSnapshot,
COMPOSITION_MAX_TOOLS,
decodeRequestCompositionSnapshot,
decodeRunCompositionSnapshot,
REQUEST_COMPOSITION_SCHEMA_VERSION,
RUN_COMPOSITION_SCHEMA_VERSION,
} from '../run-composition.js';

test('Run Composition snapshots reject ambiguous toolsets and malformed hashes', () => {
test('Run Composition snapshots retain the persisted v1 bootstrap shape', () => {
const valid = {
schemaVersion: RUN_COMPOSITION_SCHEMA_VERSION,
composerId: 'maka.interactive',
Expand All @@ -38,23 +42,86 @@ test('Run Composition snapshots reject ambiguous toolsets and malformed hashes',
contextWindow: null,
};

assert.equal(decodeRunCompositionSnapshot(valid).schemaVersion, 1);

for (const candidate of [
{ ...valid, schemaVersion: 2 },
{ ...valid, baseSystemPromptHash: 'sha256:short' },
{ ...valid, toolNames: ['Write', 'Read'] },
{ ...valid, toolNames: ['Read', 'Read'] },
{
...valid,
sourceRevisions: [
{ id: 'skill-catalog', revision: 'skills-0' },
{ id: 'runtime-policy', revision: '1' },
],
},
{ ...valid, sourceRevisions: [{ id: 'skill-catalog', revision: '' }] },
]) {
assert.throws(() => decodeRunCompositionSnapshot(candidate));
}
});

test('Request Composition snapshots canonicalize complete model-visible tool surfaces', () => {
const snapshot = createRequestCompositionSnapshot(
{
compositionId: 'composition-1',
step: 1,
sourceRevisions: [{ id: 'skill-catalog', revision: 'skills-1' }],
systemPromptHash: hash('1'),
toolCatalogHash: hash('2'),
toolAvailabilityHash: hash('3'),
providerOptionsHash: hash('4'),
toolNames: ['Write', 'Read'],
toolSchemas: [
{ name: 'Write', description: 'write', inputSchema: { type: 'object' } },
{ name: 'Read', description: 'read', inputSchema: { type: 'object' } },
],
},
'change',
);
assert.equal(snapshot.schemaVersion, REQUEST_COMPOSITION_SCHEMA_VERSION);
assert.deepEqual(snapshot.toolNames, ['Read', 'Write']);
assert.deepEqual(
snapshot.toolSchemas.map((schema) => schema.name),
['Read', 'Write'],
);
assert.throws(() =>
decodeRequestCompositionSnapshot({ ...snapshot, toolNames: ['Read', 'Read'] }),
);
});

test('Request Composition applies one fail-closed bound to names and schemas', () => {
const toolNames = Array.from(
{ length: COMPOSITION_MAX_TOOLS },
(_, index) => `tool-${index.toString().padStart(3, '0')}`,
);
const toolSchemas = toolNames.map((name) => ({
name,
description: name,
inputSchema: { type: 'object' },
}));
const valid = {
schemaVersion: REQUEST_COMPOSITION_SCHEMA_VERSION,
compositionId: 'composition-bounded',
step: 0,
reason: 'initial',
sourceRevisions: [],
systemPromptHash: hash('1'),
toolCatalogHash: hash('2'),
toolAvailabilityHash: hash('3'),
providerOptionsHash: hash('4'),
toolNames,
toolSchemas,
} as const;

assert.equal(decodeRequestCompositionSnapshot(valid).toolNames.length, COMPOSITION_MAX_TOOLS);
assert.throws(() =>
decodeRequestCompositionSnapshot({ ...valid, toolNames: [...toolNames, 'tool-overflow'] }),
);
assert.throws(() =>
decodeRequestCompositionSnapshot({
...valid,
toolSchemas: [
...toolSchemas,
{ name: 'tool-overflow', description: 'overflow', inputSchema: { type: 'object' } },
],
}),
);
});

function hash(seed: string): `sha256:${string}` {
return `sha256:${seed.repeat(64)}`;
}
3 changes: 2 additions & 1 deletion packages/core/src/agent-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export interface AgentRunHeader {
agentSwarmAuthorization?: AgentSwarmAuthorizationSource;
/** Effective tool protocol for this run. Optional on legacy runs. */
toolMode?: ToolMode;
/** Immutable composer-owned prompt and tool-surface snapshot committed before provider dispatch. */
/** Immutable composer and provider bootstrap baseline committed before provider dispatch. */
runComposition?: RunCompositionSnapshot;
createdAt: number;
updatedAt: number;
Expand Down Expand Up @@ -429,6 +429,7 @@ export const AGENT_RUN_EVENT_TYPES = [
'sandbox_denial_detected',
'provider_request_captured',
'provider_request_attempt_recorded',
'request_composition_resolved',
'model_call_attempt_recorded',
'history_compact_checkpoint_recorded',
'model_projection_transition_recorded',
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/model-call-attempt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ export interface ModelCallAttempt {

/** Runtime tool-loop step index within the turn. */
step: number;
/** Logical request-composition snapshot used by this step and all of its retries. */
requestCompositionId?: string;
/** Retry ordinal within the logical call; 0 is the first dispatch. */
attempt: number;

Expand Down Expand Up @@ -224,6 +226,7 @@ const MODEL_CALL_ATTEMPT_SHAPE = defineObjectShape<ModelCallAttempt>()(
[
'connectionSlug',
'historyCompactRoute',
'requestCompositionId',
'contextWindow',
'captureArtifactId',
'requestObservation',
Expand Down Expand Up @@ -410,6 +413,7 @@ export function decodeModelCallAttempt(value: unknown): ModelCallAttempt {
isNonEmptyString(value.runId) &&
isNonEmptyString(value.turnId) &&
isNonNegativeInteger(value.step) &&
isOptionalString(value.requestCompositionId) &&
isNonNegativeInteger(value.attempt) &&
(MODEL_CALL_KINDS as readonly unknown[]).includes(value.callKind) &&
(value.historyCompactRoute === undefined ||
Expand Down
138 changes: 136 additions & 2 deletions packages/core/src/run-composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
import { defineObjectShape, hasExactShape, isRecord } from './record-schema.js';

export const RUN_COMPOSITION_SCHEMA_VERSION = 1 as const;
export const REQUEST_COMPOSITION_SCHEMA_VERSION = 1 as const;
// Composition evidence is exact: reject an over-bound provider surface rather
// than silently truncating the resolved snapshot.
export const COMPOSITION_MAX_TOOLS = 256;
export const COMPOSITION_MAX_TOOL_NAME_LENGTH = 128;
export const REQUEST_COMPOSITION_MAX_TOOL_DESCRIPTION_LENGTH = 16_384;

export interface RunCompositionSourceRevision {
readonly id: string;
Expand All @@ -39,6 +45,32 @@ export interface RunCompositionSnapshot {
readonly contextWindow: number | null;
}

export interface RequestCompositionToolSchema {
readonly name: string;
readonly description: string;
readonly inputSchema: Record<string, unknown>;
readonly providerTool?: Record<string, unknown>;
}

/**
* One model-visible request surface, frozen at a logical model-step boundary.
* A later step appends a new snapshot only when one of these effective fields
* changes; physical retries of the same step keep referring to this snapshot.
*/
export interface RequestCompositionSnapshot {
readonly schemaVersion: typeof REQUEST_COMPOSITION_SCHEMA_VERSION;
readonly compositionId: string;
readonly step: number;
readonly reason: 'initial' | 'change';
readonly sourceRevisions: readonly RunCompositionSourceRevision[];
readonly systemPromptHash: `sha256:${string}`;
readonly toolCatalogHash: `sha256:${string}`;
readonly toolAvailabilityHash: `sha256:${string}`;
readonly providerOptionsHash: `sha256:${string}`;
readonly toolNames: readonly string[];
readonly toolSchemas: readonly RequestCompositionToolSchema[];
}

const RUN_COMPOSITION_SHAPE = defineObjectShape<RunCompositionSnapshot>()(
[
'schemaVersion',
Expand All @@ -54,6 +86,26 @@ const RUN_COMPOSITION_SHAPE = defineObjectShape<RunCompositionSnapshot>()(
],
[],
);
const REQUEST_COMPOSITION_SHAPE = defineObjectShape<RequestCompositionSnapshot>()(
[
'schemaVersion',
'compositionId',
'step',
'reason',
'sourceRevisions',
'systemPromptHash',
'toolCatalogHash',
'toolAvailabilityHash',
'providerOptionsHash',
'toolNames',
'toolSchemas',
],
[],
);
const REQUEST_COMPOSITION_TOOL_SCHEMA_SHAPE = defineObjectShape<RequestCompositionToolSchema>()(
['name', 'description', 'inputSchema'],
['providerTool'],
);

const ID_PATTERN = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
const HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
Expand Down Expand Up @@ -105,6 +157,63 @@ export function createRunCompositionSnapshot(
});
}

export type RequestCompositionSnapshotInput = Omit<
RequestCompositionSnapshot,
'schemaVersion' | 'reason'
>;

export function createRequestCompositionSnapshot(
input: RequestCompositionSnapshotInput,
reason: RequestCompositionSnapshot['reason'],
): RequestCompositionSnapshot {
return decodeRequestCompositionSnapshot({
schemaVersion: REQUEST_COMPOSITION_SCHEMA_VERSION,
...input,
reason,
sourceRevisions: [...input.sourceRevisions].sort((left, right) =>
compareExactString(left.id, right.id),
),
toolNames: [...input.toolNames].sort(compareExactString),
toolSchemas: [...input.toolSchemas].sort((left, right) =>
compareExactString(left.name, right.name),
),
});
}

export function decodeRequestCompositionSnapshot(value: unknown): RequestCompositionSnapshot {
if (!isRecord(value) || !hasExactShape(value, REQUEST_COMPOSITION_SHAPE)) {
throw new Error('Invalid Request Composition snapshot schema');
}
const valid =
value.schemaVersion === REQUEST_COMPOSITION_SCHEMA_VERSION &&
boundedString(value.compositionId, 128) &&
Number.isSafeInteger(value.step) &&
(value.step as number) >= 0 &&
(value.reason === 'initial' || value.reason === 'change') &&
canonicalSourceRevisions(value.sourceRevisions) &&
hash(value.systemPromptHash) &&
hash(value.toolCatalogHash) &&
hash(value.toolAvailabilityHash) &&
hash(value.providerOptionsHash) &&
canonicalToolNames(value.toolNames) &&
canonicalToolSchemas(value.toolSchemas);
if (!valid) throw new Error('Invalid Request Composition snapshot schema');
return Object.freeze({
...(value as unknown as RequestCompositionSnapshot),
sourceRevisions: Object.freeze(
(value.sourceRevisions as RunCompositionSourceRevision[]).map((source) =>
Object.freeze({ ...source }),
),
),
toolNames: Object.freeze([...(value.toolNames as string[])]),
toolSchemas: Object.freeze(
(value.toolSchemas as RequestCompositionToolSchema[]).map((schema) =>
Object.freeze(structuredClone(schema)),
),
),
});
}

function compareExactString(left: string, right: string): number {
return left < right ? -1 : left > right ? 1 : 0;
}
Expand All @@ -128,15 +237,40 @@ function canonicalSourceRevisions(value: unknown): value is RunCompositionSource
}

function canonicalToolNames(value: unknown): value is string[] {
if (!Array.isArray(value) || value.length > 256) return false;
if (!Array.isArray(value) || value.length > COMPOSITION_MAX_TOOLS) return false;
let previous: string | undefined;
for (const name of value) {
if (!boundedString(name, 128) || (previous !== undefined && previous >= name)) return false;
if (
!boundedString(name, COMPOSITION_MAX_TOOL_NAME_LENGTH) ||
(previous !== undefined && previous >= name)
) {
return false;
}
previous = name;
}
return true;
}

function canonicalToolSchemas(value: unknown): value is RequestCompositionToolSchema[] {
if (!Array.isArray(value) || value.length > COMPOSITION_MAX_TOOLS) return false;
let previous: string | undefined;
for (const schema of value) {
if (
!isRecord(schema) ||
!hasExactShape(schema, REQUEST_COMPOSITION_TOOL_SCHEMA_SHAPE) ||
!boundedString(schema.name, COMPOSITION_MAX_TOOL_NAME_LENGTH) ||
!boundedString(schema.description, REQUEST_COMPOSITION_MAX_TOOL_DESCRIPTION_LENGTH) ||
!isRecord(schema.inputSchema) ||
(schema.providerTool !== undefined && !isRecord(schema.providerTool)) ||
(previous !== undefined && previous >= schema.name)
) {
return false;
}
previous = schema.name;
}
return true;
}

function hash(value: unknown): value is `sha256:${string}` {
return typeof value === 'string' && HASH_PATTERN.test(value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ test('unknown Client Capability loads, invokes, and rebinds after UDS reconnect'
abortSignal: new AbortController().signal,
emitOutput: () => undefined,
};
const activeTools = new Map<string, MakaTool>();
const activeTools = new Map<string, string>();
const availability = new ToolAvailabilityRuntime(
snapshot.tools,
{ groups: snapshot.groups },
Expand Down
Loading