openzoo 0.48.12 → 0.48.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/grokui.mjs +37 -2
- package/lib/podagent.mjs +23 -5
- package/package.json +1 -1
package/lib/grokui.mjs
CHANGED
|
@@ -560,6 +560,33 @@ const SLASH_COMMANDS = [
|
|
|
560
560
|
|
|
561
561
|
const usd = (n) => (n >= 0.01 || n === 0 ? '$' + n.toFixed(2) : '$' + n.toFixed(5));
|
|
562
562
|
|
|
563
|
+
// BIND, DON'T PASTE — the whole point of the thing this runs on.
|
|
564
|
+
//
|
|
565
|
+
// Directive results get fed back to the model as a user message, verbatim. A
|
|
566
|
+
// GLOB over a 670-part upload, an LS of a big tree or an MCP tool dump is
|
|
567
|
+
// thousands of tokens, and in an auto chain it is re-sent on EVERY subsequent
|
|
568
|
+
// hop as conversation history: the window grows quadratically and the user
|
|
569
|
+
// pays for it each time. Meanwhile the full text is already going into
|
|
570
|
+
// t.history, which bindThread pushes into the thread's leCore context — so
|
|
571
|
+
// the model can recall any of it on demand.
|
|
572
|
+
//
|
|
573
|
+
// So: feed back the head and tail (where the answer nearly always is) and say
|
|
574
|
+
// plainly that the middle is recallable rather than lost.
|
|
575
|
+
const FEEDBACK_MAX = Number(process.env.OZ_FEEDBACK_MAX || 3000);
|
|
576
|
+
function condense(label, text) {
|
|
577
|
+
const s = String(text ?? '');
|
|
578
|
+
if (s.length <= FEEDBACK_MAX) return `${label}\n${s}`;
|
|
579
|
+
const head = s.slice(0, Math.floor(FEEDBACK_MAX * 0.7));
|
|
580
|
+
const tail = s.slice(-Math.floor(FEEDBACK_MAX * 0.3));
|
|
581
|
+
// The wording matters. Retrieval is AUTOMATIC — the thread's context id
|
|
582
|
+
// rides on every call as x-hrr-context and leCore injects whatever slice is
|
|
583
|
+
// relevant to what you say next. Telling the model to "ask for it" invites
|
|
584
|
+
// it to invent a RECALL directive that does not exist, which is the exact
|
|
585
|
+
// failure mode this whole harness keeps hitting: a model fabricating a
|
|
586
|
+
// mechanism instead of using the real one.
|
|
587
|
+
return `${label}\n${head}\n\n…[${s.length - head.length - tail.length} chars elided from THIS message — the full output is bound to this thread's holographic memory. It is not lost: mention what you need in your next message and the relevant part is retrieved automatically. Do not invent a command to fetch it.]…\n\n${tail}`;
|
|
588
|
+
}
|
|
589
|
+
|
|
563
590
|
// threadId -> open SSE responses. A Set because the same thread can be open in
|
|
564
591
|
// two tabs, and both should see the same tokens.
|
|
565
592
|
const streamListeners = new Map();
|
|
@@ -1288,12 +1315,19 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
1288
1315
|
t.lastActivityAt = Date.now();
|
|
1289
1316
|
saveThreads();
|
|
1290
1317
|
if (t.autoSteps < AUTO_MAX_STEPS) {
|
|
1291
|
-
|
|
1318
|
+
// BIND BEFORE CHAINING. bindThread only ran at the end of a normal
|
|
1319
|
+
// turn, and both auto paths return before reaching it — so in auto
|
|
1320
|
+
// mode nothing was ever bound, exactly when the agent produces the
|
|
1321
|
+
// most material (command output, GLOB results, MCP tool lists). The
|
|
1322
|
+
// holographic context stopped growing precisely when it mattered.
|
|
1323
|
+
bindThread(t).catch(() => {});
|
|
1324
|
+
runTurn(threadId, condense('(command output)', output), onEvent).catch(() => {});
|
|
1292
1325
|
} else {
|
|
1293
1326
|
const note = `(auto-run stopped after ${AUTO_MAX_STEPS} chained commands — say "continue" to keep going)`;
|
|
1294
1327
|
t.history.push({ who: 'bot', text: note });
|
|
1295
1328
|
onEvent?.({ type: 'final', name: t.name, color: t.color, text: note });
|
|
1296
1329
|
saveThreads();
|
|
1330
|
+
bindThread(t).catch(() => {});
|
|
1297
1331
|
}
|
|
1298
1332
|
return;
|
|
1299
1333
|
}
|
|
@@ -1330,7 +1364,8 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
1330
1364
|
if (t.runMode === 'auto' && ack !== null && ack !== undefined) {
|
|
1331
1365
|
t.autoSteps = (t.autoSteps || 0) + 1;
|
|
1332
1366
|
if (t.autoSteps < AUTO_MAX_STEPS) {
|
|
1333
|
-
|
|
1367
|
+
bindThread(t).catch(() => {}); // bind every hop, not just the last one
|
|
1368
|
+
runTurn(threadId, condense('(directive result)', ack), onEvent).catch(() => {});
|
|
1334
1369
|
return;
|
|
1335
1370
|
}
|
|
1336
1371
|
const note = `(auto stopped after ${AUTO_MAX_STEPS} steps — say "continue" to keep going)`;
|
package/lib/podagent.mjs
CHANGED
|
@@ -184,6 +184,10 @@ function execFrame(command, cwd = '/tmp') {
|
|
|
184
184
|
// on a text-only model silently ignoring pasted images, any message with
|
|
185
185
|
// multimodal (image_url) content routes to a model KNOWN to support vision.
|
|
186
186
|
const VISION_MODEL = process.env.OZ_VISION_MODEL || 'anthropic/claude-sonnet-5';
|
|
187
|
+
// Output budget per turn. Reasoning models spend this on their chain of
|
|
188
|
+
// thought BEFORE emitting any content, so a budget that is merely "enough for
|
|
189
|
+
// the answer" produces no answer at all on a hard prompt.
|
|
190
|
+
const MAX_TOKENS = Number(process.env.OZ_MAX_TOKENS || 4096);
|
|
187
191
|
const msgHasImage = (m) => Array.isArray(m.content) && m.content.some((c) => c?.type === 'image_url');
|
|
188
192
|
|
|
189
193
|
// How far back an image still counts as "being discussed". Beyond this the
|
|
@@ -305,12 +309,13 @@ export async function brain(messages, contextId, modelOverride) {
|
|
|
305
309
|
/** Same call, but streamed — invokes onDelta(text) as tokens arrive (for a
|
|
306
310
|
* live-typing UI) and resolves with the full accumulated text at the end, so
|
|
307
311
|
* callers that need to parse a directive out of the complete reply still can. */
|
|
308
|
-
export async function brainStream(messages, onDelta, contextId, modelOverride) {
|
|
312
|
+
export async function brainStream(messages, onDelta, contextId, modelOverride, maxTokens) {
|
|
309
313
|
const vision = hasImages(messages);
|
|
310
314
|
const model = vision ? VISION_MODEL : (modelOverride || MODEL);
|
|
311
315
|
messages = vision ? messages : stripImages(messages);
|
|
316
|
+
const budget = maxTokens || MAX_TOKENS;
|
|
312
317
|
const r = await postChat(
|
|
313
|
-
{ model, max_tokens:
|
|
318
|
+
{ model, max_tokens: budget, messages: withModelId(messages, model), plugins: [{ id: 'web' }], stream: true },
|
|
314
319
|
contextId,
|
|
315
320
|
);
|
|
316
321
|
if (!r.ok || !r.body) {
|
|
@@ -322,7 +327,7 @@ export async function brainStream(messages, onDelta, contextId, modelOverride) {
|
|
|
322
327
|
}
|
|
323
328
|
const reader = r.body.getReader();
|
|
324
329
|
const decoder = new TextDecoder();
|
|
325
|
-
let buf = '', full = '';
|
|
330
|
+
let buf = '', full = '', reasonedChars = 0;
|
|
326
331
|
for (;;) {
|
|
327
332
|
const { value, done } = await reader.read();
|
|
328
333
|
if (done) break;
|
|
@@ -335,11 +340,24 @@ export async function brainStream(messages, onDelta, contextId, modelOverride) {
|
|
|
335
340
|
const payload = s.slice(5).trim();
|
|
336
341
|
if (payload === '[DONE]') continue;
|
|
337
342
|
try {
|
|
338
|
-
const
|
|
339
|
-
if (
|
|
343
|
+
const d = JSON.parse(payload)?.choices?.[0]?.delta;
|
|
344
|
+
if (d?.content) { full += d.content; onDelta(d.content); }
|
|
345
|
+
// Reasoning models emit their chain of thought on a SEPARATE field and
|
|
346
|
+
// only then start producing content. Count it — not to show it, but to
|
|
347
|
+
// tell "the model said nothing" apart from "the model spent its whole
|
|
348
|
+
// budget thinking and got cut off".
|
|
349
|
+
else if (d?.reasoning || d?.reasoning_content) reasonedChars += (d.reasoning || d.reasoning_content).length;
|
|
340
350
|
} catch { /* keep-alive line or partial JSON — ignore */ }
|
|
341
351
|
}
|
|
342
352
|
}
|
|
353
|
+
|
|
354
|
+
// EMPTY CONTENT AFTER HEAVY REASONING is a truncation, not an answer. It
|
|
355
|
+
// surfaced in the UI as a bare "(no response)" bubble that cost the user a
|
|
356
|
+
// turn and explained nothing — on exactly the long, complex prompts where a
|
|
357
|
+
// reasoning model thinks the most. Retry ONCE with a bigger budget.
|
|
358
|
+
if (!full && reasonedChars > 0 && !maxTokens) {
|
|
359
|
+
return brainStream(messages, onDelta, contextId, modelOverride, budget * 4);
|
|
360
|
+
}
|
|
343
361
|
return full;
|
|
344
362
|
}
|
|
345
363
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.14",
|
|
4
4
|
"description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|