File size: 6,314 Bytes
430bcda 5cdc985 430bcda 5727bed 430bcda 5cdc985 430bcda 5cdc985 430bcda 5cdc985 4b88879 5cdc985 430bcda 5727bed 5cdc985 5727bed 430bcda 5727bed 430bcda 5727bed 430bcda 5727bed 430bcda 5727bed 5cdc985 430bcda 5727bed 20765cf 430bcda 854b98d bf62c9f 5cdc985 5727bed bf62c9f 430bcda 5727bed 430bcda 5cdc985 430bcda 5cdc985 854b98d 430bcda 5cdc985 430bcda 5cdc985 854b98d 5cdc985 854b98d 5cdc985 430bcda 5cdc985 5727bed 854b98d 5727bed 430bcda 5cdc985 854b98d 5cdc985 20765cf 5cdc985 5727bed 181b8c1 9b04a00 181b8c1 9b04a00 181b8c1 20765cf 181b8c1 20765cf 181b8c1 854b98d | 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 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | // Cloud API client β all LLM reasoning runs remotely on agent.mona.expert.
// Supports both sngine-based (agent.mona.expert) and Docker-based platforms.
// This device sends prompts up and receives streamed reasoning back.
// No LLM provider keys are ever stored or used locally.
import { CLOUD, DEFAULTS } from './config.js';
import { log } from './log.js';
const UA = `mona-agent/${DEFAULTS.version}`;
const P = CLOUD.paths; // platform-aware API paths
// ββ Generic API fetch βββββββββββββββββββββββββββββββββββββββββββββ
async function apiFetch(path, { apiKey, method = 'POST', body, signal, headers: extraHeaders } = {}) {
const url = CLOUD.base + path;
const headers = {
'Authorization': `Bearer ${apiKey}`,
'content-type': 'application/json',
'user-agent': UA,
'x-mona-agent': DEFAULTS.version,
...(extraHeaders || {}),
};
const res = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
signal,
});
if (!res.ok) {
const text = await res.text().catch(() => '');
const err = new Error(`Cloud API ${res.status}: ${text.slice(0, 200)}`);
err.status = res.status;
throw err;
}
return res;
}
// ββ Verify API key ββββββββββββββββββββββββββββββββββββββββββββββββ
export async function verifyKey(apiKey) {
const res = await apiFetch(P.verifyKey, { apiKey, method: 'GET' });
return res.json();
}
// ββ WAF-safe bodies ββββββββββββββββββββββββββββββββββββββββββββββββ
// Shared-hosting WAFs (ModSecurity on LiteSpeed) intermittently 403 JSON
// bodies that contain shell-like strings (tool results ride inside think
// payloads). Base64-encoding keeps the transport neutral; the server
// decodes before processing. Auth and validation stay server-side.
function b64Body(obj) {
return { b64: Buffer.from(JSON.stringify(obj)).toString('base64') };
}
// ββ Stream reasoning from cloud brain βββββββββββββββββββββββββββββ
// Calls the cloud LLM endpoint and streams tokens back via SSE.
// onChunk(text) β called per delta token
// onUsage(usage) β called with final token counts (if provided)
// Returns { text, usage, model, provider } β usage is null when the
// cloud did not report it (older server or plain JSON without usage).
export async function think({ apiKey, messages, tools, onChunk, onUsage, signal, temperature, profile }) {
const res = await apiFetch(P.think, {
apiKey,
body: b64Body({ messages, tools, stream: true, temperature, profile }),
signal,
});
// Handle both SSE streaming and plain JSON responses
const contentType = res.headers.get('content-type') || '';
if (contentType.includes('text/event-stream')) {
// SSE streaming
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = '';
let full = '';
let usage = null;
let model = null;
let provider = null;
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split('\n');
buf = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data:')) continue;
const payload = line.slice(5).trim();
if (payload === '[DONE]') {
return { text: full, usage, model, provider };
}
try {
const j = JSON.parse(payload);
if (j.delta) {
full += j.delta;
onChunk?.(j.delta);
}
if (j.usage) { usage = j.usage; onUsage?.(j.usage); }
if (j.model) model = j.model;
if (j.provider) provider = j.provider;
} catch {
// Skip malformed or keepalive lines
}
}
}
} finally {
reader.releaseLock();
}
return { text: full, usage, model, provider };
}
// Plain JSON response
const data = await res.json();
return {
text: data.content || data.text || JSON.stringify(data),
usage: data.usage || null,
model: data.model || null,
provider: data.provider || null,
};
}
// ββ Report tool result to cloud βββββββββββββββββββββββββββββββββββ
export async function reportToolResult(apiKey, agentId, tool, result) {
return apiFetch(P.toolResult, {
apiKey,
body: b64Body({ agentId, tool, result }),
});
}
// ββ Cloud task queue (sngine platform β device polls for work) ββββ
// The response carries the task rows plus the owner's brain config
// (step budget, temperature, extra rules) so the loop can tune itself.
export async function pollTasks(apiKey) {
const res = await apiFetch('/api/v1/agent/tasks', { apiKey, method: 'GET' });
const data = await res.json();
return data || { tasks: [] };
}
export async function claimTask(apiKey, id) {
return apiFetch('/api/v1/agent/tasks/claim', { apiKey, body: { id } });
}
export async function taskResult(apiKey, id, { result, steps }) {
return apiFetch(`/api/v1/agent/tasks/${id}/result`, { apiKey, body: b64Body({ result, steps }) });
}
export async function postActivity(apiKey, type, detail, runId, agentId) {
return apiFetch('/api/v1/agent/activity', { apiKey, body: b64Body({ type, detail, runId, agentId }) });
}
// ββ Run trace lifecycle (deep insight: per-step usage, tokens, cost) ββ
// Best-effort: the task loop never depends on these succeeding.
export async function runStart(apiKey, { runId, agentId, taskId, message }) {
return apiFetch('/api/v1/agent/runs', { apiKey, body: b64Body({ runId, agentId, taskId, message }) });
}
export async function runStep(apiKey, runId, step) {
return apiFetch(`/api/v1/agent/runs/${runId}/step`, { apiKey, body: b64Body(step) });
}
export async function runFinish(apiKey, runId, fin) {
return apiFetch(`/api/v1/agent/runs/${runId}/finish`, { apiKey, body: b64Body(fin) });
}
|