source.fact.ngo a coherence.ngo project

scripts/ask.js

raw ↗ · AGPL-3.0

#!/usr/bin/env node // ask.js — the interviewer's mouth and ears. Sends one user turn to a model via // the Cloudflare Workers AI REST API, appends the exchange to the session file, // and prints the response plus usage/cost. The agent never touches credentials // directly; auth is resolved here from env or opencode's auth.json. import fs from "node:fs"; import path from "node:path"; import { getCloudflareAuth, parseArgs, nowIso, PROTOCOL_VERSION, modelSlug, REPO_ROOT } from "./lib.js"; import { findModel } from "./models.js"; const { args, positional } = parseArgs(process.argv.slice(2), { model: { takes: "value", default: null }, session: { takes: "value", default: null }, domain: { takes: "value", default: "ad-hoc" }, lens: { takes: "value", default: "ad-hoc" }, message: { takes: "value", default: null }, system: { takes: "value", default: null }, system_file: { takes: "value", default: null }, temperature: { takes: "value", default: "0.7" }, max_tokens: { takes: "value", default: "2048" }, show: { takes: "value", default: null }, new: { takes: "none", default: false }, }); if (args.show) { if (!args.session) { console.error("--show requires --session"); process.exit(2); } if (!fs.existsSync(args.session)) { console.error("session file not found"); process.exit(1); } const s = JSON.parse(fs.readFileSync(args.session, "utf8")); const n = parseInt(args.show, 10); for (const t of s.turns.slice(-n)) { console.log(`--- turn ${t.turn} [${t.role}] ---\n${t.content}\n`); } process.exit(0); } if (!args.model) { console.error("--model is required"); process.exit(2); } if (!args.session) { console.error("--session is required"); process.exit(2); } let message = args.message; if (message === "-" || (!message && !process.stdin.isTTY)) { message = fs.readFileSync(0, "utf8"); } if (!message) { console.error("--message (text, or - for stdin) is required"); process.exit(2); } const auth = getCloudflareAuth(); if (!auth.token) { console.error("No Cloudflare API token found. Set CLOUDFLARE_API_TOKEN or populate " + "~/.local/share/opencode/auth.json with a cloudflare-workers-ai key."); process.exit(1); } if (!auth.account) { console.error("CLOUDFLARE_ACCOUNT_ID is not set."); process.exit(1); } const temperature = parseFloat(args.temperature); const maxTokens = parseInt(args.max_tokens, 10); // Load or create session let session; const exists = fs.existsSync(args.session); if (exists && !args.new) { session = JSON.parse(fs.readFileSync(args.session, "utf8")); if (session.model_id !== args.model) { console.error(`Session belongs to ${session.model_id}, refusing to ask ${args.model}. Use --new.`); process.exit(1); } } else { fs.mkdirSync(path.dirname(args.session), { recursive: true }); const seq = exists ? 2 : 1; session = { session_id: `${modelSlug(args.model)}.${args.domain}.${args.lens}.${seq}`, model_id: args.model, domain: args.domain, lens: args.lens, protocol_version: PROTOCOL_VERSION, temperature, started: nowIso(), ended: null, turns: [], totals: { tokens_in: 0, tokens_out: 0, cost_usd_estimate: 0 }, }; } // System prompt: --system > --system-file > protocol default let system = args.system; if (!system && args.system_file) system = fs.readFileSync(args.system_file, "utf8"); if (!system) system = fs.readFileSync(path.join(REPO_ROOT, "protocol/prompts/system.md"), "utf8"); const messages = [ { role: "system", content: system }, ...session.turns.map((t) => ({ role: t.role, content: t.content })), { role: "user", content: message }, ]; const url = `https://api.cloudflare.com/client/v4/accounts/${auth.account}/ai/run/${args.model}`; const res = await fetch(url, { method: "POST", headers: { Authorization: `Bearer ${auth.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ messages, temperature, max_tokens: maxTokens }), }); if (!res.ok) { console.error(`API error ${res.status}: ${(await res.text()).slice(0, 500)}`); process.exit(1); } const data = await res.json(); if (!data.success) { console.error(`API returned failure: ${JSON.stringify(data.errors || data).slice(0, 500)}`); process.exit(1); } const text = data.result?.choices?.[0]?.message?.content ?? data.result?.response ?? data.result?.message?.content ?? ""; const usage = data.result?.usage ?? {}; const tokensIn = usage.prompt_tokens ?? null; const tokensOut = usage.completion_tokens ?? null; const neurons = usage.neurons ?? null; const model = findModel(args.model); let callCost = 0; if (model && tokensIn != null && tokensOut != null) { callCost = (tokensIn / 1e6) * model.in + (tokensOut / 1e6) * model.out; session.totals.tokens_in += tokensIn; session.totals.tokens_out += tokensOut; session.totals.cost_usd_estimate += callCost; } const userTurnNo = session.turns.length + 1; session.turns.push({ turn: userTurnNo, role: "user", content: message, tokens_in: tokensIn, tokens_out: null, timestamp: nowIso() }); session.turns.push({ turn: userTurnNo + 1, role: "assistant", content: text, tokens_in: null, tokens_out: tokensOut, timestamp: nowIso() }); fs.writeFileSync(args.session, JSON.stringify(session, null, 2) + "\n", "utf8"); console.log(text); if (tokensIn != null) { console.error(`\n[usage] in=${tokensIn} out=${tokensOut} call≈$${callCost.toFixed(4)} session≈$${session.totals.cost_usd_estimate.toFixed(4)} turns=${session.turns.length}${neurons != null ? ` neurons=${neurons}` : ""}`); } else { console.error(`\n[usage] not reported by API; turns=${session.turns.length}`); }