67220a14ea
Prevent phase2/phase3/all reruns from burning DashScope tokens unless GOOSE_V149_ALLOW_REAL_LLM=1 is explicitly set with human approval. Co-authored-by: Cursor <cursoragent@cursor.com>
143 lines
4.7 KiB
JavaScript
143 lines
4.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Multi-turn provider smoke: update_provider then two consecutive /reply rounds.
|
|
*/
|
|
import { randomUUID } from 'node:crypto';
|
|
import { Readable } from 'node:stream';
|
|
import { Agent, fetch } from 'undici';
|
|
|
|
import { enforceRealLlmGate } from './goose-v149-real-llm-gate.mjs';
|
|
|
|
enforceRealLlmGate('check-goosed-v149-multiturn-provider.mjs');
|
|
|
|
const port = process.env.GOOSE_V149_PORT || '18049';
|
|
const host = process.env.GOOSE_V149_HOST || '127.0.0.1';
|
|
const secret = process.env.GOOSE_SERVER__SECRET_KEY || 'local-v149-dev-secret';
|
|
const workingDir = process.env.GOOSE_V149_WORKING_DIR || process.cwd();
|
|
// Align with portal-resume default (300s); 90s was too tight under queued agent-run load.
|
|
const timeoutMs = Number(process.env.GOOSE_V149_MULTITURN_TIMEOUT_MS || 300_000);
|
|
const provider = process.env.GOOSE_V149_TEST_PROVIDER || 'custom_qwen3-flash';
|
|
const model = process.env.GOOSE_V149_TEST_MODEL || 'qwen3.8-flash';
|
|
const restartBetweenTurns = process.env.GOOSE_V149_MULTITURN_RESTART === '1';
|
|
|
|
const base = `https://${host}:${port}`;
|
|
const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });
|
|
|
|
async function apiJson(pathname, body) {
|
|
const response = await fetch(`${base}${pathname}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-Secret-Key': secret,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(body),
|
|
dispatcher,
|
|
});
|
|
const text = await response.text();
|
|
if (!response.ok) {
|
|
throw new Error(`${pathname} ${response.status}: ${text.slice(0, 400)}`);
|
|
}
|
|
return text.trim() ? JSON.parse(text) : {};
|
|
}
|
|
|
|
async function waitForTerminal(sessionId, requestId) {
|
|
const eventsResponse = await fetch(`${base}/sessions/${sessionId}/events`, {
|
|
headers: { Accept: 'text/event-stream', 'X-Secret-Key': secret },
|
|
dispatcher,
|
|
});
|
|
if (!eventsResponse.ok || !eventsResponse.body) {
|
|
throw new Error(`events ${eventsResponse.status}`);
|
|
}
|
|
|
|
const replyResponse = await fetch(`${base}/sessions/${sessionId}/reply`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-Secret-Key': secret,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
request_id: requestId,
|
|
user_message: {
|
|
role: 'user',
|
|
created: Date.now(),
|
|
content: [{ type: 'text', text: `multiturn ping ${requestId.slice(0, 8)}` }],
|
|
metadata: { userVisible: true, agentVisible: true, displayText: 'multiturn ping' },
|
|
},
|
|
}),
|
|
dispatcher,
|
|
});
|
|
if (!replyResponse.ok) {
|
|
const text = await replyResponse.text().catch(() => '');
|
|
throw new Error(`reply ${replyResponse.status}: ${text.slice(0, 300)}`);
|
|
}
|
|
replyResponse.body?.cancel?.();
|
|
|
|
const reader = Readable.fromWeb(eventsResponse.body);
|
|
const decoder = new TextDecoder();
|
|
let buffer = '';
|
|
const deadline = Date.now() + timeoutMs;
|
|
|
|
for await (const chunk of reader) {
|
|
if (Date.now() > deadline) break;
|
|
buffer += decoder.decode(chunk, { stream: true });
|
|
const frames = buffer.split('\n\n');
|
|
buffer = frames.pop() ?? '';
|
|
for (const frame of frames) {
|
|
let data = '';
|
|
for (const line of frame.split('\n')) {
|
|
if (line.startsWith('data:')) data += line.slice(5).trim();
|
|
}
|
|
if (!data) continue;
|
|
let event;
|
|
try {
|
|
event = JSON.parse(data);
|
|
} catch {
|
|
continue;
|
|
}
|
|
const routingId = event.chat_request_id ?? event.request_id;
|
|
if (routingId && routingId !== requestId) continue;
|
|
if (event.type === 'Finish') return 'finish';
|
|
if (event.type === 'Error') {
|
|
return `error:${String(event.error ?? '').slice(0, 200)}`;
|
|
}
|
|
}
|
|
}
|
|
return 'timeout';
|
|
}
|
|
|
|
async function main() {
|
|
const session = await apiJson('/agent/start', { working_dir: workingDir });
|
|
if (!session?.id) throw new Error('missing session id');
|
|
|
|
await apiJson('/agent/update_provider', {
|
|
session_id: session.id,
|
|
provider,
|
|
model,
|
|
});
|
|
|
|
const outcomes = [];
|
|
for (let turn = 1; turn <= 2; turn += 1) {
|
|
const requestId = randomUUID();
|
|
const outcome = await waitForTerminal(session.id, requestId);
|
|
outcomes.push({ turn, requestId, outcome });
|
|
console.log(`GOOSE_V149_MULTITURN_EVENT: turn=${turn} outcome=${outcome}`);
|
|
if (outcome !== 'finish') {
|
|
throw new Error(`turn ${turn} failed: ${outcome}`);
|
|
}
|
|
if (restartBetweenTurns && turn === 1) {
|
|
await apiJson('/agent/restart', { session_id: session.id });
|
|
console.log('GOOSE_V149_MULTITURN_EVENT: restart=ok');
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`GOOSE_V149_MULTITURN_OK: session=${session.id} provider=${provider}/${model} `
|
|
+ `turns=${outcomes.length}`,
|
|
);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(`GOOSE_V149_MULTITURN_FAIL: ${error.message}`);
|
|
process.exit(1);
|
|
});
|