blob: 862e7e716c3dae59b7f462c35eee672266424a0e [file]
import {
createSyntheticComputerScenario,
canonicalizeSyntheticComputerArgs,
SYNTHETIC_COMPUTER_ALLOWED_KEYS,
SYNTHETIC_COMPUTER_KNOWN_KEYS,
SYNTHETIC_COMPUTER_TOOL_PROPERTIES,
} from './cu-synthetic-model-scenario.mjs';
const baseUrl = process.env.MAKA_CU_ANTHROPIC_BASE_URL ?? 'http://127.0.0.1:8537';
const model = process.env.MAKA_CU_ANTHROPIC_MODEL ?? 'claude-sonnet-4-6';
const authToken = process.env.MAKA_CU_ANTHROPIC_TOKEN ?? 'coproxy';
const scenario = createSyntheticComputerScenario();
const rejections = [];
const messages = [
{
role: 'user',
content:
'Use maka_computer to set "CUA Lab Set Value Field" in "Codex CUA Lab" ' +
'to "model-e2e". Start with list_apps, observe the exact app/window, ' +
'use set_value with IDs from that observation, verify, then finish.',
},
];
for (let turn = 1; turn <= 8; turn += 1) {
const response = await fetch(`${baseUrl.replace(/\/+$/, '')}/v1/messages`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-api-key': authToken,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model,
max_tokens: 1024,
system:
'Operate only the synthetic fixture. Treat tool results as authoritative. ' +
'Never invent observation or element IDs. Never use coordinate input.',
tools: [
{
name: 'maka_computer',
description:
'Accessibility-first desktop control. Start with list_apps or observe. ' +
'Use set_value or click_element with IDs from the latest observation.',
input_schema: {
type: 'object',
properties: SYNTHETIC_COMPUTER_TOOL_PROPERTIES,
required: ['action'],
additionalProperties: false,
},
},
],
messages,
}),
});
const body = await response.text();
if (!response.ok) {
throw new Error(`Anthropic model request failed ${response.status}: ${bounded(body)}`);
}
const decoded = JSON.parse(body);
const toolUses = decoded.content?.filter((item) => item.type === 'tool_use') ?? [];
if (toolUses.length === 0) {
const text =
decoded.content
?.filter((item) => item.type === 'text')
.map((item) => item.text)
.join('') ?? '';
if (scenario.state.value !== 'model-e2e') {
throw new Error(`model finished before verified mutation: ${bounded(text)}`);
}
process.stdout.write(
`${JSON.stringify(
{
ok: true,
provider: 'anthropic',
model,
turns: turn,
calls: scenario.calls,
rejections,
finalValue: scenario.state.value,
finalTextPresent: text.trim().length > 0,
finalTextChars: text.length,
},
null,
2,
)}\n`,
);
process.exit(0);
}
if (toolUses.length !== 1) {
throw new Error(`expected one serial tool use, got ${toolUses.length}`);
}
const toolUse = toolUses[0];
if (toolUse.name !== 'maka_computer') {
throw new Error(`unexpected tool ${toolUse.name}`);
}
let result;
try {
const { args, discardedKeys } = projectArgs(toolUse.input);
result = scenario.execute(canonicalizeSyntheticComputerArgs(args), discardedKeys);
} catch (error) {
const message = bounded(error instanceof Error ? error.message : error);
result = {
kind: 'tool_error',
error: 'invalid_semantic_binding',
message,
recovery:
'Call observe again, then repeat the semantic action with the exact ' +
'observation_id and element_id from that observation.',
};
rejections.push({ turn, tool: toolUse.name, message });
}
messages.push({ role: 'assistant', content: decoded.content });
messages.push({
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: toolUse.id,
content: JSON.stringify(result),
...(result.kind === 'tool_error' ? { is_error: true } : {}),
},
],
});
}
throw new Error('Anthropic model loop exceeded 8 turns');
function projectArgs(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error('Anthropic tool arguments must be an object');
}
const unknownKeys = Object.keys(value).filter(
(key) => !SYNTHETIC_COMPUTER_KNOWN_KEYS.includes(key),
);
if (unknownKeys.length > 0) {
throw new Error(`unknown Anthropic argument keys: ${unknownKeys.join(',')}`);
}
const action = value.action;
if (typeof action !== 'string' || !SYNTHETIC_COMPUTER_ALLOWED_KEYS[action]) {
throw new Error(`unsupported Anthropic action ${String(action)}`);
}
const allowed = SYNTHETIC_COMPUTER_ALLOWED_KEYS[action];
return {
args: Object.fromEntries(Object.entries(value).filter(([key]) => allowed.includes(key))),
discardedKeys: Object.keys(value)
.filter((key) => !allowed.includes(key))
.sort(),
};
}
function bounded(value) {
const text = String(value).replace(/\s+/g, ' ').trim();
return text.length <= 300 ? text : `${text.slice(0, 300)}...[truncated]`;
}