/* * 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 { _electron as electron, chromium } from '@playwright/test'; import { execFile, spawn } from 'node:child_process'; import { randomUUID } from 'node:crypto'; import { promisify } from 'node:util'; import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { createServer } from 'node:net'; import { homedir, tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { evaluateCuE2eScenarioState, getCuE2eScenario } from './e2e-scenarios.mjs'; import { validateRealReport } from './provider-matrix.mjs'; import { sanitizeCuActionRecord, sanitizeCuReport, sanitizeCuTrace } from './report-sanitize.mjs'; import { createSqliteAgentRunStore } from '../../packages/storage/dist/agent-run-store.js'; import { resolveStorageRoot, tryAcquireInteractiveRootOwner, } from '../../packages/storage/dist/root-authority.js'; import { openInteractiveRuntimePolicyStoresForWrite } from '../../packages/storage/dist/runtime-policy-stores.js'; const here = dirname(fileURLToPath(import.meta.url)); const repoRoot = join(here, '../..'); const execFileAsync = promisify(execFile); const sourceWorkspace = join( homedir(), 'Library', 'Application Support', 'Maka', 'workspaces', 'default', ); const scenario = getCuE2eScenario(process.env.MAKA_CU_E2E_SCENARIO ?? 'l0-observe-only'); if (!scenario.realRunEnabled) { throw new Error(`scenario ${scenario.id} is not enabled for real-model runs`); } if (scenario.runner) { throw new Error(`scenario ${scenario.id} requires dedicated runner ${scenario.runner}`); } const availableCapabilities = new Set( String(process.env.MAKA_CU_EXECUTION_CAPABILITIES ?? '') .split(',') .map((value) => value.trim()) .filter(Boolean), ); for (const capability of scenario.requiresExecutionCapabilities) { if (!availableCapabilities.has(capability)) { throw new Error(`scenario ${scenario.id} requires unavailable capability ${capability}`); } } const timeoutMs = Number(process.env.MAKA_CU_REAL_MODEL_TIMEOUT_MS ?? 180_000); const keepProfile = process.env.MAKA_CU_KEEP_PROFILE === '1'; const providerOverride = process.env.MAKA_CU_PROVIDER; const reportPath = process.env.MAKA_CU_REAL_MODEL_REPORT ?? join(repoRoot, '.agents-workspace-data', 'cu-real-model', `report-${Date.now()}.json`); const runPrompt = 'Use the maka_computer tool to complete this task. ' + scenario.prompt; async function reportLineage() { const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, }); const generatedAt = new Date().toISOString(); const gitRevision = stdout.trim(); return { runId: randomUUID(), gitRevision, generatedAt, contentLineage: { generator: 'scripts/computer-use/real-model.mjs', gitRevision, generatedAt, }, }; } async function reservePort() { const server = createServer(); await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); }); const address = server.address(); const port = typeof address === 'object' && address ? address.port : 0; await new Promise((resolve) => server.close(resolve)); if (!port) throw new Error('failed to reserve loopback port'); return port; } async function copyProfileFile(name, workspace) { await cp(join(sourceWorkspace, name), join(workspace, name), { errorOnExist: true, }); } async function copyProfileFileIfPresent(name, workspace) { try { await copyProfileFile(name, workspace); } catch (error) { if (error && error.code === 'ENOENT') return; throw error; } } async function prepareProviderProfile(workspace) { if (!providerOverride) { await copyProfileFile('connection-catalog.json', workspace); await Promise.all([ copyProfileFileIfPresent('credential-vault.json', workspace), copyProfileFileIfPresent('credentials.json', workspace), copyProfileFile('settings.json', workspace), ]); return; } if (providerOverride !== 'openai') { throw new Error(`unsupported MAKA_CU_PROVIDER ${providerOverride}`); } await copyProfileFile('settings.json', workspace); const modelId = process.env.MAKA_CU_OPENAI_MODEL ?? 'gpt-5.4'; const capability = await resolveStorageRoot({ path: workspace, kind: 'interactive' }); const owner = await tryAcquireInteractiveRootOwner(capability); if (!owner) throw new Error('Unable to acquire the Computer Use catalog root'); try { const stores = await openInteractiveRuntimePolicyStoresForWrite(owner.lease); const created = await stores.connectionCatalog.create({ expectedCatalogRevision: 0, connection: { slug: 'cu-real-openai', name: 'Computer Use real-model OpenAI', providerType: 'openai', baseUrl: process.env.MAKA_CU_OPENAI_BASE_URL ?? 'http://127.0.0.1:8538/v1', enabled: true, enabledModelIds: [modelId], }, }); if (created.kind !== 'committed') { throw new Error(`Computer Use catalog create failed: ${created.kind}`); } const connection = created.snapshot.connections.find((item) => item.slug === 'cu-real-openai'); if (!connection) throw new Error('Computer Use catalog create omitted cu-real-openai'); const vault = await stores.credentialVault.set({ locator: { scope: 'connection', connectionId: connection.connectionId, kind: 'api_key', }, expected: null, secret: process.env.MAKA_CU_OPENAI_API_KEY ?? 'local-bridge', }); if (vault.kind !== 'committed') { throw new Error(`Computer Use vault set failed: ${vault.kind}`); } const defaulted = await stores.connectionCatalog.setDefaultTarget({ expectedCatalogRevision: created.snapshot.revision, target: { connectionId: connection.connectionId, modelId }, }); if (defaulted.kind !== 'committed') { throw new Error(`Computer Use default target failed: ${defaulted.kind}`); } } finally { await owner.close(); } } async function waitForLine(child, marker, timeout) { return new Promise((resolve, reject) => { let stdout = ''; const timer = setTimeout(() => reject(new Error(`timed out waiting for ${marker}`)), timeout); const onData = (chunk) => { stdout += String(chunk); process.stdout.write(chunk); const matched = marker === 'CU_FIXTURE_READY' ? /^CU_FIXTURE_READY\s+\d+\s*$/m.test(stdout) : stdout.includes(marker); if (matched) { clearTimeout(timer); child.stdout.off('data', onData); resolve(stdout); } }; child.stdout.on('data', onData); child.stderr.on('data', (chunk) => process.stderr.write(chunk)); child.once('error', reject); child.once('exit', (code, signal) => { clearTimeout(timer); reject(new Error(`fixture exited before ready: ${signal ?? `code ${code}`}`)); }); }); } export function parseFixtureReady(text, childPid) { const matches = typeof text === 'string' ? [...text.matchAll(/^CU_FIXTURE_READY\s+(\d+)\s*$/gm)] : []; if (matches.length !== 1) { throw new Error('fixture READY identity is missing or ambiguous'); } const readyPid = Number(matches[0][1]); if (!Number.isInteger(childPid) || childPid <= 0 || readyPid !== childPid) { throw new Error(`fixture READY pid ${readyPid} does not match launcher child pid ${childPid}`); } return readyPid; } export function parseTargetEvidence(text) { if (typeof text !== 'string') return undefined; const candidates = [text]; for (const markerText of ['Fresh observation:\n', 'Fresh observation: ']) { const marker = text.lastIndexOf(markerText); if (marker >= 0) candidates.push(text.slice(marker + markerText.length)); } for (const candidate of candidates.reverse()) { try { const value = JSON.parse(candidate); if ( value && typeof value === 'object' && Number.isInteger(value.pid) && Number.isInteger(value.window_id) ) { return { ...(typeof value.observation_id === 'string' ? { observationId: value.observation_id } : {}), pid: value.pid, windowId: value.window_id, }; } } catch { // Continue to the next privacy-safe projection candidate. } } return undefined; } export function actionRecords(events) { const starts = new Map(); const records = []; for (const event of events) { if (event.type === 'tool_start' && event.toolName === 'maka_computer') { starts.set(event.toolUseId, event); } if (event.type === 'tool_result' && starts.has(event.toolUseId)) { const start = starts.get(event.toolUseId); const text = event.content?.kind === 'text' ? event.content.text : undefined; const resultCode = typeof text === 'string' ? text.match(/\bfailed:\s*([a-z][a-z0-9_]{1,63})\b/i)?.[1] : undefined; const target = parseTargetEvidence(text); records.push( sanitizeCuActionRecord({ action: start.args, toolCallId: event.toolUseId, sourceObservationId: start.args?.observation_id, resultObservationId: target?.observationId, targetPid: target?.pid, targetWindowId: target?.windowId, durationMs: event.durationMs, text, success: event.isError === false && !resultCode, }), ); } } return records; } export async function waitForTraceFlush(path, expectedToolCallIds, timeoutMs = 2_000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const current = await readFile(path, 'utf8').catch((error) => { if (error?.code === 'ENOENT') return ''; throw error; }); const traces = []; let incompleteTrace = false; for (const line of current.split('\n').filter(Boolean)) { try { traces.push(JSON.parse(line)); } catch (error) { if (!(error instanceof SyntaxError)) throw error; incompleteTrace = true; } } const observedToolCallIds = new Set( traces.filter((trace) => trace.type === 'dispatch').map((trace) => trace.toolCallId), ); if ( !incompleteTrace && expectedToolCallIds.every((toolCallId) => observedToolCallIds.has(toolCallId)) ) { return traces; } await new Promise((resolve) => setTimeout(resolve, 50)); } throw new Error( `timed out waiting for Computer Use dispatch traces: ${expectedToolCallIds.join(',')}`, ); } export async function discoverFixtureIdentity( fixturePid, windowSpecs, { listApps, timeoutMs = 2_000, pollIntervalMs = 50 } = {}, ) { if (!Number.isInteger(fixturePid) || fixturePid <= 0) { throw new Error('fixture discovery requires a valid launcher-owned pid'); } if (typeof listApps !== 'function') { throw new Error('fixture discovery requires an independent window lister'); } const expectedTitles = windowSpecs?.map((window) => window.title); if ( !Array.isArray(expectedTitles) || expectedTitles.length === 0 || expectedTitles.some((title) => typeof title !== 'string' || title.length === 0) || new Set(expectedTitles).size !== expectedTitles.length ) { throw new Error('fixture discovery requires unique expected window titles'); } const deadline = Date.now() + timeoutMs; let lastFailure = 'fixture app was not found'; while (Date.now() < deadline) { const apps = await listApps(); const fixtureApps = Array.isArray(apps) ? apps.filter((app) => Number(app?.pid) === fixturePid) : []; if (fixtureApps.length === 1) { const windows = Array.isArray(fixtureApps[0].windows) ? fixtureApps[0].windows : []; const windowIds = []; let complete = true; for (const title of expectedTitles) { const matches = windows.filter( (window) => window?.title === title && Number.isInteger(window?.windowId) && window.windowId > 0, ); if (matches.length !== 1) { complete = false; lastFailure = `expected one fixture window "${title}", got ${matches.length}`; break; } windowIds.push(matches[0].windowId); } if (complete && new Set(windowIds).size === windowIds.length) { return { instances: [ { pid: fixturePid, windowIds, }, ], }; } if (complete) lastFailure = 'fixture discovery returned duplicate window ids'; } else { lastFailure = `expected one fixture app for pid ${fixturePid}, got ${fixtureApps.length}`; } await new Promise((resolvePromise) => setTimeout(resolvePromise, pollIntervalMs)); } throw new Error(`fixture identity discovery failed: ${lastFailure}`); } async function discoverLauncherFixtureIdentity(fixturePid, windowSpecs) { const [{ createMakaCuBackend }, manifestText] = await Promise.all([ import('../../packages/computer-use/dist/index.js'), readFile(join(repoRoot, 'apps', 'desktop', 'bundled-tools.json'), 'utf8'), ]); const manifest = JSON.parse(manifestText); const expectedBinarySha256 = manifest?.makaCu?.binarySha256; if (typeof expectedBinarySha256 !== 'string' || !/^[a-f0-9]{64}$/.test(expectedBinarySha256)) { throw new Error('fixture discovery cannot verify bundled maka-cu identity'); } const backend = createMakaCuBackend({ binaryPath: join(repoRoot, 'apps', 'desktop', 'resources', 'bin', 'maka-cu'), expectedBinarySha256, timeoutMs: 10_000, }); try { return await discoverFixtureIdentity(fixturePid, windowSpecs, { listApps: () => backend.listApps(new AbortController().signal), timeoutMs: 5_000, }); } finally { backend.dispose(); } } function safeEvent(event) { if (event.type === 'tool_start') { const safeToolName = event.toolName === 'tool_search' || event.toolName === 'maka_computer' ? event.toolName : 'other'; return { type: event.type, toolName: safeToolName, ...(event.toolName === 'maka_computer' ? { actionType: event.args?.action ?? 'unknown' } : {}), ts: event.ts, }; } if (event.type === 'tool_result') { return { type: event.type, isError: event.isError, durationMs: event.durationMs, ts: event.ts, }; } if (event.type === 'complete' || event.type === 'abort' || event.type === 'error') { const safeCode = [event.code, event.reason].find( (value) => typeof value === 'string' && /^[a-zA-Z][a-zA-Z0-9_.-]{0,63}$/.test(value), ); return { type: event.type, ...(event.stopReason ? { stopReason: event.stopReason } : {}), ...(safeCode ? { code: safeCode } : {}), ts: event.ts, }; } return null; } function safeFailureMetadata(message) { if (typeof message !== 'string') return undefined; const status = message.match(/\b([45]\d\d)\b/)?.[1]; const errorName = message.match(/\b([A-Z][A-Za-z]+Error)\b/)?.[1]; const providerType = message .match( /\b(api_error|authentication_error|billing_error|invalid_request_error|overloaded_error|permission_error|rate_limit_error)\b/i, )?.[1] ?.toLowerCase(); const result = { ...(status ? { httpStatus: Number(status) } : {}), ...(errorName ? { errorName } : {}), ...(providerType ? { providerErrorType: providerType } : {}), }; return Object.keys(result).length > 0 ? result : undefined; } async function run() { const lineage = await reportLineage(); const userData = await mkdtemp(join(tmpdir(), 'maka-cu-real-model-')); const workspace = join(userData, 'workspaces', 'default'); const tracePath = join(userData, 'computer-use-trace.jsonl'); const fixturePort = await reservePort(); const electronBinary = join(repoRoot, 'node_modules', '.bin', 'electron'); let fixture; let desktop; let fixtureBrowser; try { await mkdir(workspace, { recursive: true }); await mkdir(dirname(reportPath), { recursive: true }); await prepareProviderProfile(workspace); fixture = spawn( electronBinary, [ `--remote-debugging-port=${fixturePort}`, '--remote-allow-origins=*', join(here, 'real-model-fixture.mjs'), ], { cwd: repoRoot, env: { ...process.env, MAKA_CU_E2E_SCENARIO: scenario.id, }, stdio: ['ignore', 'pipe', 'pipe'], }, ); const readyOutput = await waitForLine(fixture, 'CU_FIXTURE_READY', 30_000); const fixturePid = parseFixtureReady(readyOutput, fixture.pid); const fixtureIdentity = await discoverLauncherFixtureIdentity( fixturePid, activeWindowSpecs(scenario), ); desktop = await electron.launch({ args: ['apps/desktop'], cwd: repoRoot, env: { ...process.env, MAKA_CU_REAL_MODEL_E2E: '1', MAKA_E2E_USER_DATA_DIR: userData, MAKA_CU_REAL_MODEL_POLICY: JSON.stringify({ allowedActions: scenario.allowedActions, maxTotalActions: scenario.maxTotalActions, maxActionCounts: scenario.maxActionCounts ?? Object.fromEntries( scenario.allowedActions.map((action) => [action, scenario.maxTotalActions]), ), allowedApps: activeWindowSpecs(scenario).map((window) => window.title), }), MAKA_CU_REAL_MODEL_TRACE: tracePath, }, timeout: 30_000, }); const page = await desktop.firstWindow(); await page.waitForFunction(() => Boolean(window.maka?.sessions)); const runResult = await page.evaluate( async ({ prompt, timeout }) => { const connections = await window.maka.connections.list(); const defaultSlug = await window.maka.connections.getDefault(); const connection = connections.find((entry) => entry.slug === defaultSlug); if (!connection?.defaultModel) { throw new Error('isolated profile has no ready default model'); } const session = await window.maka.sessions.create({ backend: 'ai-sdk', llmConnectionSlug: connection.slug, model: connection.defaultModel, permissionMode: 'bypass', name: 'Computer Use real-model E2E', labels: ['computer-use', 'real-model-e2e'], }); const events = []; const terminal = new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('real-model turn timed out')), timeout); const unsubscribe = window.maka.sessions.subscribeEvents(session.id, (event) => { events.push(event); if (event.type === 'complete' || event.type === 'abort' || event.type === 'error') { clearTimeout(timer); unsubscribe(); resolve(event); } }); }); const turnId = crypto.randomUUID(); await window.maka.sessions.send(session.id, { type: 'send', turnId, text: prompt, }); const terminalEvent = await terminal; return { connection: { slug: connection.slug, providerType: connection.providerType, model: connection.defaultModel, }, sessionId: session.id, turnId, events, terminalEvent, }; }, { prompt: runPrompt, timeout: timeoutMs }, ); fixtureBrowser = await chromium.connectOverCDP(`http://127.0.0.1:${fixturePort}`); const fixturePages = fixtureBrowser.contexts().flatMap((context) => context.pages()); const fixtureState = {}; for (const windowSpec of activeWindowSpecs(scenario)) { const pageForTitle = await Promise.all( fixturePages.map(async (candidate) => ({ candidate, title: await candidate.title(), })), ).then((entries) => entries.find((entry) => entry.title === windowSpec.title)?.candidate); if (!pageForTitle) throw new Error(`missing fixture page ${windowSpec.title}`); fixtureState[windowSpec.id] = await pageForTitle.evaluate( () => globalThis.__makaCuFixtureState?.() ?? null, ); } const evaluation = evaluateCuE2eScenarioState(scenario, fixtureState); const events = runResult.events.map(safeEvent).filter(Boolean); const rawActions = actionRecords(runResult.events); const expectedDispatchToolCallIds = rawActions .filter( (action) => action.success === true && !['list_apps', 'observe', 'screenshot', 'cursor_position', 'wait'].includes(action.type), ) .map((action) => action.toolCallId) .filter((toolCallId) => typeof toolCallId === 'string'); const driverTraces = await waitForTraceFlush(tracePath, expectedDispatchToolCallIds); const actions = bindActionTargets(rawActions, driverTraces, fixtureIdentity); const runStore = createSqliteAgentRunStore(workspace); await runStore.ready?.(); let runHeader; try { runHeader = await waitForRunHeader(runStore, runResult.sessionId, runResult.turnId); } finally { runStore.close?.(); } const qualifyingActions = actions.filter( (action) => action.success === true && action.targetOwned === true && scenario.allowedActions.includes(action.type), ); const actionCounts = Object.fromEntries( actions.reduce((counts, action) => { counts.set(action.type, (counts.get(action.type) ?? 0) + 1); return counts; }, new Map()), ); const qualifyingActionCounts = Object.fromEntries( scenario.allowedActions.map((action) => [ action, qualifyingActions.filter((record) => record.type === action).length, ]), ); const minimumActionsPassed = Object.entries(scenario.minimumActionCounts ?? {}).every( ([action, minimum]) => (qualifyingActionCounts[action] ?? 0) >= minimum, ); const terminalPassed = runResult.terminalEvent.type === 'complete' && runResult.terminalEvent.stopReason === 'end_turn'; const actionsWithinBudget = actions.length <= scenario.maxTotalActions && actions.every((action) => scenario.allowedActions.includes(action.type)) && Object.entries(scenario.maxActionCounts ?? {}).every( ([action, maximum]) => actions.filter((record) => record.type === action).length <= maximum, ); const dispatchPathPassed = requiredDispatchPathPassed(scenario, driverTraces); const ownershipPassed = allActionTargetsOwned(actions); const localChecksPassed = terminalPassed && minimumActionsPassed && actionsWithinBudget && dispatchPathPassed && ownershipPassed && evaluation.pass; const reportInput = { schemaVersion: 1, ...lineage, evidenceClass: 'real-runtime', policyMode: 'bypassed', qualificationEligible: true, toolExposure: 'direct-e2e', scenarioId: scenario.id, producer: 'computer-use/real-model', transportClass: 'live-network', provider: runResult.connection.providerType, model: runResult.connection.model, fixtureIdentity, terminal: safeEvent(runResult.terminalEvent), run: runHeader ? { status: runHeader.status, failureClass: runHeader.failureClass, failure: safeFailureMetadata(runHeader.failureMessage), durationMs: runHeader.completedAt !== undefined ? Math.max(0, runHeader.completedAt - runHeader.createdAt) : undefined, } : undefined, actionAttempts: actions.length, actionCount: actions.length, actionCounts, minimumActionsPassed, actionsWithinBudget, dispatchPathPassed, actions, fixtureState, expectedState: evaluation.expected, forbiddenEffects: { status: evaluation.forbidden.every((entry) => entry.pass) ? 'pass' : 'fail', violations: evaluation.forbidden.filter((entry) => !entry.pass), }, status: localChecksPassed ? 'pass' : terminalPassed && minimumActionsPassed ? 'fail' : 'inconclusive', traces: events.map(sanitizeCuTrace).filter(Boolean), driverTraces: driverTraces.map(sanitizeCuTrace).filter(Boolean), }; const provider = { id: runResult.connection.providerType, producer: 'computer-use/real-model', model: runResult.connection.model, }; const provisionalReport = sanitizeCuReport(reportInput); const qualificationErrors = validateRealReport( { ...provisionalReport, status: 'pass' }, provider, scenario, ); const report = sanitizeCuReport({ ...reportInput, status: qualificationErrors.length === 0 ? 'pass' : 'fail', }); const validationErrors = validateRealReport(report, provider, scenario); await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, { flag: 'wx', mode: 0o600, }); process.stdout.write(`Real-model Computer Use report: ${reportPath}\n`); process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); if (validationErrors.length > 0) { process.stderr.write( `Real-model Computer Use qualification failed: ${validationErrors.join('; ')}\n`, ); process.exitCode = 1; } } finally { await fixtureBrowser?.close().catch(() => {}); await desktop?.close().catch(() => {}); if (fixture && fixture.exitCode === null) fixture.kill('SIGTERM'); await new Promise((resolve) => setTimeout(resolve, 250)); if (fixture && fixture.exitCode === null) fixture.kill('SIGKILL'); if (keepProfile) { process.stderr.write(`Retained isolated debug profile: ${userData}\n`); } else { await rm(userData, { recursive: true, force: true }); } } } async function handleRunFailure(error) { const lineage = await reportLineage().catch(() => undefined); const failure = sanitizeCuReport({ schemaVersion: 1, ...lineage, evidenceClass: 'real-runtime', scenarioId: scenario.id, producer: 'computer-use/real-model', status: 'inconclusive', terminal: { type: 'error' }, run: { status: 'failed', failureClass: typeof error?.code === 'string' ? error.code : error instanceof Error ? error.name : 'unknown', }, }); await mkdir(dirname(reportPath), { recursive: true }).catch(() => {}); await writeFile(reportPath, `${JSON.stringify(failure, null, 2)}\n`, { flag: 'wx', mode: 0o600, }).catch(() => {}); console.error('Real-model Computer Use E2E failed'); process.exitCode = 1; } function requiredDispatchPathPassed(scenario, traces) { const mutationActions = scenario.allowedActions.filter( (action) => !['list_apps', 'observe', 'screenshot', 'cursor_position', 'wait'].includes(action), ); if (mutationActions.length === 0) return true; return traces.some( (trace) => trace.type === 'dispatch' && (trace.address === 'ax' || trace.address === 'semantic'), ); } function fixtureOwnsTarget(fixtureIdentity, pid, windowId) { return ( fixtureIdentity?.instances?.some( (instance) => instance.pid === pid && instance.windowIds?.includes(windowId), ) === true ); } export function bindActionTargets(actions, traces, fixtureIdentity) { const dispatches = traces.filter( (trace) => trace.type === 'dispatch' && (trace.address === 'ax' || trace.address === 'semantic'), ); const observationTargets = new Map( actions.flatMap((action) => (action.type === 'observe' || action.type === 'screenshot') && typeof action.resultObservationId === 'string' && Number.isInteger(action.targetPid) && Number.isInteger(action.targetWindowId) ? [ [ action.resultObservationId, { pid: action.targetPid, windowId: action.targetWindowId, }, ], ] : [], ), ); const consumed = new Set(); return actions.map((action) => { if (['list_apps', 'wait', 'cursor_position'].includes(action.type)) { return { ...action, targetOwned: false }; } const index = dispatches.findIndex( (trace, traceIndex) => !consumed.has(traceIndex) && trace.toolCallId === action.toolCallId && trace.actionType === action.type, ); if (index >= 0) consumed.add(index); const dispatch = index >= 0 ? dispatches[index] : undefined; const sourceTarget = typeof action.sourceObservationId === 'string' ? observationTargets.get(action.sourceObservationId) : undefined; const directObservationTarget = (action.type === 'observe' || action.type === 'screenshot') && Number.isInteger(action.targetPid) && Number.isInteger(action.targetWindowId) ? { pid: action.targetPid, windowId: action.targetWindowId } : undefined; const target = dispatch ? { pid: dispatch.pid, windowId: dispatch.windowId } : sourceTarget ? sourceTarget : directObservationTarget; return { ...action, targetOwned: target ? fixtureOwnsTarget(fixtureIdentity, target.pid, target.windowId) : false, ...(target ? { targetPid: target.pid, targetWindowId: target.windowId, } : {}), }; }); } export function allActionTargetsOwned(actions) { return actions.every( (action) => ['list_apps', 'wait', 'cursor_position'].includes(action.type) || action.targetOwned === true, ); } function activeWindowSpecs(scenario) { const specs = new Map(scenario.fixtureSetup.windows.map((window) => [window.id, window])); for (const transition of scenario.fixtureSetup.transitions ?? []) { specs.delete(transition.removeWindowId); specs.set(transition.addWindow.id, transition.addWindow); } return [...specs.values()]; } async function waitForRunHeader(store, sessionId, turnId) { const deadline = Date.now() + 2_000; let latest; do { latest = (await store.listSessionRuns(sessionId)).find((entry) => entry.turnId === turnId); if ( latest && latest.status !== 'created' && latest.status !== 'running' && latest.status !== 'waiting_for_user' ) return latest; await new Promise((resolve) => setTimeout(resolve, 50)); } while (Date.now() < deadline); return latest; } if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) { run().catch(handleRunFailure); }