blob: 21baf9cdf465af6d61522f40f9bb5de91b707621 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
createOpenAIStrictObjectSchema,
projectOpenAIStrictFunctionArgs,
} from '../../packages/runtime/dist/openai-strict-function.js';
import {
createSyntheticComputerScenario,
canonicalizeSyntheticComputerArgs,
SYNTHETIC_COMPUTER_ALLOWED_KEYS,
SYNTHETIC_COMPUTER_KNOWN_KEYS,
SYNTHETIC_COMPUTER_TOOL_PROPERTIES,
} from './synthetic-model-scenario.mjs';
const baseUrl = process.env.MAKA_CU_MODEL_BASE_URL ?? 'http://127.0.0.1:8538/v1';
const model = process.env.MAKA_CU_MODEL_ID ?? 'gpt-5.6-sol';
const maxTurns = 8;
const scenario = createSyntheticComputerScenario();
const { state, calls } = scenario;
let previousResponseId;
let input = [
{
role: 'user',
content: [
{
type: 'input_text',
text:
'Use maka_computer to set the field labeled "CUA Lab Set Value Field" ' +
'in the app "Codex CUA Lab" to "model-e2e". Start with list_apps, ' +
'observe the exact app/window, use set_value with IDs from that observation, ' +
'verify the fresh observation, then finish.',
},
],
},
];
const tool = {
type: 'function',
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. ' +
'Coordinate click, scroll, drag, press_key, type, and pixel fallback are disabled.',
parameters: createOpenAIStrictObjectSchema({
properties: SYNTHETIC_COMPUTER_TOOL_PROPERTIES,
}),
strict: true,
};
for (let turn = 1; turn <= maxTurns; turn += 1) {
const response = await fetch(`${baseUrl.replace(/\/+$/, '')}/responses`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
model,
instructions:
'Operate only the synthetic fixture through maka_computer. Treat tool output as ' +
'authoritative. Never invent observation or element IDs. Never use coordinate input.',
tools: [tool],
tool_choice: 'auto',
parallel_tool_calls: false,
input,
...(previousResponseId ? { previous_response_id: previousResponseId } : {}),
store: false,
}),
});
const body = await response.text();
if (!response.ok) {
throw new Error(`model request failed ${response.status}: ${bounded(body)}`);
}
const decoded = JSON.parse(body);
previousResponseId = decoded.id;
const toolCalls = decoded.output?.filter((item) => item.type === 'function_call') ?? [];
if (toolCalls.length === 0) {
const text =
decoded.output
?.flatMap((item) => item.content ?? [])
.filter((part) => part.type === 'output_text')
.map((part) => part.text)
.join('') ?? '';
if (state.value !== 'model-e2e') {
throw new Error(`model finished before verified mutation: ${bounded(text)}`);
}
process.stdout.write(
`${JSON.stringify(
{
ok: true,
model,
turns: turn,
calls,
finalValue: state.value,
finalTextPresent: text.trim().length > 0,
finalTextChars: text.length,
},
null,
2,
)}\n`,
);
process.exit(0);
}
if (toolCalls.length !== 1) {
throw new Error(`expected one serial tool call, got ${toolCalls.length}`);
}
const call = toolCalls[0];
if (call.name !== 'maka_computer') {
throw new Error(`unexpected tool ${call.name}`);
}
const { args, discardedKeys } = normalizeArgs(JSON.parse(call.arguments));
const result = scenario.execute(canonicalizeSyntheticComputerArgs(args), discardedKeys);
input = [
{
type: 'function_call_output',
call_id: call.call_id,
output: JSON.stringify(result),
},
];
}
throw new Error(`model loop exceeded ${maxTurns} turns`);
function normalizeArgs(value) {
return projectOpenAIStrictFunctionArgs({
value,
knownKeys: SYNTHETIC_COMPUTER_KNOWN_KEYS,
allowedKeysByAction: SYNTHETIC_COMPUTER_ALLOWED_KEYS,
});
}
function bounded(value) {
const text = String(value).replace(/\s+/g, ' ').trim();
return text.length <= 300 ? text : `${text.slice(0, 300)}...[truncated]`;
}