From 602060d8571d37554676581ad5111cf57c603020 Mon Sep 17 00:00:00 2001 From: Yuriy Mayatnikov Date: Wed, 27 May 2026 18:22:30 +0300 Subject: [PATCH] feat(scripts): TimeWeb smoke test + bump default LLM timeout to 20s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/check-timeweb.js — three probes: plain text, JSON mode, full fast-advisor stack (registry injection + skill validation). Loads .env, prints {ok, latency, reply preview} for each. Doesn't touch bot state. Bumped DEFAULT_TIMEOUT_MS 8s → 20s in runtime/llm/provider.js. TimeWeb's hosted agent endpoint takes 5-15s for the fast-advisor prompt (registry block + snapshot context), so 8s was producing spurious timeouts. OpenAI direct returns much faster; env var TIMEWEB_TIMEOUT_MS overrides if needed. Smoke verified live (PR #27 branch): probe 1: 6.3s, plain prompt → "pepa hears you" probe 2: 5.4s, JSON mode → {"alive":true,"name":"pepa"} probe 3: 14.9s, advise() → action=switch_skill, skill=recovery.tunnel-out (correct registered skill, sensible rationale — registry injection successfully prevents hallucination) Co-Authored-By: Claude Opus 4.7 --- runtime/llm/provider.js | 6 ++- scripts/check-timeweb.js | 97 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 scripts/check-timeweb.js diff --git a/runtime/llm/provider.js b/runtime/llm/provider.js index 0f73b25..035d079 100644 --- a/runtime/llm/provider.js +++ b/runtime/llm/provider.js @@ -24,7 +24,11 @@ const ENV = { }; const DEFAULT_BASE_URL = "https://api.openai.com/v1"; -const DEFAULT_TIMEOUT_MS = 8000; +// 20s default — TimeWeb's hosted agent endpoint takes 5-15s for the +// fast-advisor prompt (registry block + snapshot context). 8s was too +// tight and produced spurious timeouts in smoke tests. OpenAI direct +// returns much faster (<2s); the env var overrides if needed. +const DEFAULT_TIMEOUT_MS = 20000; export function isAvailable() { return !!process.env[ENV.API_KEY]; diff --git a/scripts/check-timeweb.js b/scripts/check-timeweb.js new file mode 100644 index 0000000..17c3b51 --- /dev/null +++ b/scripts/check-timeweb.js @@ -0,0 +1,97 @@ +// Smoke test for the TIMEWEB_* fast-LLM env vars. +// +// Loads .env, asks the model a tiny structured question, prints +// {ok, latency, code, first 200 chars of reply}. No bot state is +// touched — this is purely a connectivity check. +// +// Usage: +// node scripts/check-timeweb.js + +import { config as loadDotenv } from "dotenv"; +loadDotenv(); + +import { complete, isAvailable, getConfig } from "../runtime/llm/provider.js"; +import { advise, _resetForTest as resetAdvisor } from "../runtime/coach/fast-advisor.js"; + +function redact(key) { + if (!key) return "(unset)"; + if (key.length < 12) return "(set, short)"; + return `${key.slice(0, 6)}…${key.slice(-4)} (${key.length} chars)`; +} + +async function main() { + console.log("=== TimeWeb / fast-advisor smoke test ==="); + const cfg = getConfig(); + console.log(`BASE_URL: ${cfg.baseUrl || "(unset)"}`); + console.log(`API_KEY: ${redact(cfg.apiKey)}`); + console.log(`MODEL: ${cfg.model || "(unset)"}`); + console.log(`TIMEOUT: ${cfg.timeoutMs}ms`); + console.log(`isAvailable: ${isAvailable()}`); + console.log(""); + + if (!isAvailable()) { + console.error("ERROR: TIMEWEB_API_KEY not set in .env — aborting."); + process.exit(1); + } + + console.log("→ probe 1: plain prompt, no JSON mode"); + const r1 = await complete({ + system: "Reply in 5 words or less.", + user: "Say 'pepa hears you'.", + json: false, + }); + logResult(r1); + + console.log(""); + console.log("→ probe 2: JSON mode with a tiny structured request"); + const r2 = await complete({ + system: "Reply with strict JSON only.", + user: 'Return {"alive": true, "name": "pepa"}', + json: true, + }); + logResult(r2); + + console.log(""); + console.log("→ probe 3: full fast-advisor stack (registry injection + skill validation)"); + resetAdvisor(); + const r3 = await advise({ + snapshot: { + position: { x: 608, y: 90, z: 91 }, + health: 14, food: 18, isDay: true, + inventory: { dirt: 4 }, + activeSkill: "explore.far", + }, + reason: "wedged_60s", + recentSkillIds: ["explore.far", "explore.far", "explore.far", "explore.far"], + lessonsTail: [ + { text: "Если позиция почти не меняется и инвентарь не растёт, прекращай текущий exploration skill." }, + ], + force: true, + }); + console.log(` ok: ${r3.ok}`); + console.log(` latency: ${r3.latencyMs}ms`); + if (r3.ok) { + console.log(` action: ${r3.action}`); + console.log(` skillId: ${r3.skillId ?? "(n/a)"}`); + console.log(` why: ${r3.rationale}`); + } else { + console.log(` code: ${r3.code}`); + console.log(` detail: ${String(r3.detail).slice(0, 200)}`); + } +} + +function logResult(r) { + console.log(` ok: ${r.ok}`); + console.log(` latency: ${r.latencyMs}ms`); + if (!r.ok) { + console.log(` code: ${r.code}`); + console.log(` detail: ${String(r.detail).slice(0, 400)}`); + return; + } + console.log(` reply: ${typeof r.text === "string" ? r.text.slice(0, 200) : JSON.stringify(r.text).slice(0, 200)}`); +} + +main().catch((e) => { + console.error("UNHANDLED:", e?.message ?? e); + process.exit(2); +});