crates/engine/tests/fixtures/acp-options-agent.mjs
Rama main · réplica de lectura del repositorio original.
// A deterministic Agent Client Protocol agent that advertises its own models,
// a mode, a config option and one control Codewhale cannot write, and reports
// token usage. Behavior is steered by `options-fixture.json` in the session
// cwd; everything it receives is appended to `options-received.jsonl` there.
import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
const plan = () => {
const path = join(process.cwd(), 'options-fixture.json');
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : {};
};
const record = (entry) =>
appendFileSync(join(process.cwd(), 'options-received.jsonl'), `${JSON.stringify({ pid: process.pid, ...entry })}\n`);
writeFileSync(join(process.cwd(), 'options-env.json'), JSON.stringify(process.env));
const MODELS = [
{ value: 'model-a', name: 'Model A', description: 'Fast' },
{ value: 'model-b', name: 'Model B', description: 'Careful' },
{ value: 'model-c', name: 'Model C' },
];
const EFFORTS = [
{ value: 'low', name: 'Low' },
{ value: 'high', name: 'High' },
];
const MODES = [
{ id: 'default', name: 'Default' },
{ id: 'review', name: 'Review only', description: 'Reads, never edits' },
];
let buffer = '';
let sessionId = null;
const state = { model: 'model-a', effort: 'low', mode: 'default' };
const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
const result = (id, value) => send({ jsonrpc: '2.0', id, result: value });
const failure = (id, code, message) => send({ jsonrpc: '2.0', id, error: { code, message } });
const notify = (method, params) => send({ jsonrpc: '2.0', method, params });
function offer() {
const withdrawn = new Set(plan().withdraw ?? []);
const models = MODELS.filter((model) => !withdrawn.has(model.value));
const configOptions = [
{ id: 'model', name: 'Model', category: 'model', type: 'select', currentValue: state.model, options: models },
{ id: 'effort', name: 'Reasoning effort', category: 'thought_level', type: 'select', currentValue: state.effort, options: EFFORTS },
{ id: 'verbose', name: 'Verbose logging', type: 'boolean', currentValue: false },
];
const reply = {
configOptions,
modes: { currentModeId: state.mode, availableModes: MODES },
};
if (plan().malformed) {
reply.configOptions.push({ id: '', name: 'No id', type: 'select', currentValue: 'x', options: [] });
reply.configOptions.push({ id: 'empty', name: 'Empty', type: 'select', currentValue: 'x', options: [] });
}
if (plan().huge) {
reply.configOptions.push({
id: 'many',
name: 'Many',
type: 'select',
currentValue: 'v0',
options: Array.from({ length: 600 }, (_, index) => ({ value: `v${index}`, name: `Value ${index}` })),
});
}
return reply;
}
function offered(config, value) {
const found = offer().configOptions.find((option) => option.id === config);
return Boolean(found && found.type === 'select' && found.options.some((option) => option.value === value));
}
function prompt(request) {
const text = (request.params?.prompt ?? []).map((block) => block.text ?? '').join('');
record({ method: 'session/prompt', text, state: { ...state } });
notify('session/update', {
sessionId,
update: {
sessionUpdate: 'agent_message_chunk',
content: { type: 'text', text: `model=${state.model} effort=${state.effort} mode=${state.mode}` },
},
});
if (!plan().no_usage) {
notify('session/update', { sessionId, update: { sessionUpdate: 'usage_update', used: 1200, size: 200000 } });
}
result(request.id, {
stopReason: 'end_turn',
...(plan().no_usage ? {} : { usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150, thoughtTokens: 7 } }),
});
}
function handle(message) {
if (message.id !== undefined && message.method === undefined) return;
const params = message.params ?? {};
switch (message.method) {
case 'initialize':
result(message.id, {
protocolVersion: 1,
agentInfo: { name: 'options-fixture', version: '1.0.0' },
authMethods: [],
agentCapabilities: {
loadSession: true,
promptCapabilities: { image: true, embeddedContext: true },
sessionCapabilities: { fork: {} },
},
});
break;
case 'session/new':
if (plan().auth_required) {
failure(message.id, -32000, 'Authentication required');
break;
}
sessionId = `options-session-${process.pid}`;
record({ method: 'session/new', cwd: params.cwd });
if (plan().hang) break;
result(message.id, { sessionId, ...offer() });
break;
case 'session/load':
case 'session/resume':
sessionId = params.sessionId ?? null;
record({ method: message.method, sessionId });
result(message.id, offer());
break;
case 'session/set_config_option':
record({ method: message.method, configId: params.configId, value: params.value });
if (!offered(params.configId, params.value)) {
failure(message.id, -32602, 'value not offered');
break;
}
state[params.configId] = params.value;
result(message.id, { configOptions: offer().configOptions });
break;
case 'session/set_mode':
record({ method: message.method, modeId: params.modeId });
if (!MODES.some((mode) => mode.id === params.modeId)) {
failure(message.id, -32602, 'mode not offered');
break;
}
state.mode = params.modeId;
result(message.id, {});
break;
case 'session/prompt':
prompt(message);
break;
case 'session/cancel':
break;
default:
if (message.id !== undefined) failure(message.id, -32601, `no method ${message.method}`);
}
}
process.stdin.on('data', (chunk) => {
buffer += chunk;
let index;
while ((index = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, index);
buffer = buffer.slice(index + 1);
if (!line.trim()) continue;
try {
handle(JSON.parse(line));
} catch (error) {
process.stderr.write(`options fixture: bad line: ${String(error)}\n`);
}
}
});
process.stdin.on('end', () => process.exit(0));