File size: 4,182 Bytes
e720a94
 
 
 
 
 
 
fce80d1
 
e720a94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fce80d1
e720a94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import { estimateTokens } from './sse.mjs';

/**
 * Simulated engine — development and CI only, never production
 * (assertProductionReady refuses SIMULATED_ENGINE_ENABLED=true in prod).
 *
 * It is NOT a language model. It exercises the full engine loop offline:
 * gateway auth  conversation history  engine call  tool_calls  command 
 * device  tool result  engine continuation  streaming  usage  audit.
 *
 * Tool-call trigger: a user message containing `tool: <name>` (e.g. `tool: sysinfo`)
 * issues one tool_call for the named device tool; the tool result is echoed into
 * the final answer. Everything else is a deterministic stand-in response.
 */
const BANNER = '**Simulated engine response** — no language model was called. Auth, the command loop, streaming and usage accounting are real.';

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

function seedOf(text) {
  let h = 2166136261;
  for (let i = 0; i < text.length; i++) { h ^= text.charCodeAt(i); h = Math.imul(h, 16777619); }
  return (h >>> 0) / 4294967295;
}

const TOOL_RE = /(?:^|\s)tool:\s*([a-z][a-z0-9_.-]*)/i;

function compose({ messages, system, agentName }) {
  const last = [...messages].reverse().find(m => m.role === 'user')?.content || '';
  const words = last.trim().split(/\s+/).filter(Boolean);
  const lines = [BANNER, ''];
  lines.push(`Received ${words.length} words across ${messages.length} context messages.`);
  lines.push(`Agent: **${agentName || 'unnamed'}** · system prompt: ${system ? `${system.length} chars` : 'none'}.`);
  lines.push('');
  lines.push(`The mona.expert engine would answer here and pick the model itself.`);
  lines.push(`Echo for verification: *${words.slice(0, 12).join(' ')}${words.length > 12 ? '…' : ''}*`);
  if (/hi|hello|hey/i.test(last)) {
    lines.push('', 'Connected end to end: device  gateway  engine  back. Try `tool: sysinfo` to run a real command on the device.');
  }
  return lines.join('\n');
}

function composeToolFollowup({ toolResult, messages }) {
  const lines = ['**Simulated engine** — tool executed on the device.', ''];
  lines.push('```');
  lines.push(String(toolResult).slice(0, 800));
  lines.push('```');
  return lines.join('\n');
}

export class SimulatedEngine {
  constructor({ log = null } = {}) { this.log = log; }
  get mode() { return 'simulated'; }
  get name() { return 'Mona Simulator (dev only)'; }

  async authenticate() { return { ok: true, detail: 'Simulated engine needs no key.' }; }

  async chat(req) {
    const started = Date.now();
    const text = compose(req);
    await sleep(80 + Math.floor(seedOf(text) * 200));
    return {
      text, finishReason: 'stop', model: 'mona-sim',
      usage: { inputTokens: estimateTokens(JSON.stringify(req.messages)), outputTokens: estimateTokens(text), estimated: true },
      latencyMs: Date.now() - started,
    };
  }

  async *stream(req) {
    // Tool-call turns: a "tool" role message means the engine is deciding what to do with a result.
    const hasToolResult = req.messages?.some(m => m.role === 'tool');
    const text = hasToolResult
      ? composeToolFollowup({ toolResult: req.messages.at(-1)?.content ?? '', messages: req.messages })
      : compose(req);

    // Issue one tool_call when the user explicitly asks for a device tool.
    if (!hasToolResult) {
      const last = [...req.messages].reverse().find(m => m.role === 'user')?.content || '';
      const m = TOOL_RE.exec(last);
      if (m && req.tools?.some(t => (t.name || t) === m[1])) {
        yield { type: 'tool_call', id: `sim-${Date.now()}`, tool: m[1], args: {} };
        return;
      }
    }

    await sleep(70 + Math.floor(seedOf(text) * 180));
    const chunks = text.match(/\s*\S+|\s+/g) || [text];
    for (const chunk of chunks) {
      if (req.signal?.aborted) { yield { type: 'done', finishReason: 'stopped' }; return; }
      await sleep(4);
      yield { type: 'delta', text: chunk };
    }
    yield { type: 'usage', usage: { inputTokens: estimateTokens(JSON.stringify(req.messages) + (req.system || '')), outputTokens: estimateTokens(text), estimated: true, model: 'mona-sim' } };
    yield { type: 'done', finishReason: 'stop', text };
  }
}