openzoo 0.48.12 → 0.48.13
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 +31 -2
- package/lib/podagent.mjs +23 -5
- package/package.json +1 -1
package/lib/grokui.mjs
CHANGED
|
@@ -560,6 +560,27 @@ 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
|
+
return `${label}\n${head}\n\n…[${s.length - head.length - tail.length} chars elided — the FULL output is bound to this thread's holographic context; ask for any part of it and it will be recalled]…\n\n${tail}`;
|
|
582
|
+
}
|
|
583
|
+
|
|
563
584
|
// threadId -> open SSE responses. A Set because the same thread can be open in
|
|
564
585
|
// two tabs, and both should see the same tokens.
|
|
565
586
|
const streamListeners = new Map();
|
|
@@ -1288,12 +1309,19 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
1288
1309
|
t.lastActivityAt = Date.now();
|
|
1289
1310
|
saveThreads();
|
|
1290
1311
|
if (t.autoSteps < AUTO_MAX_STEPS) {
|
|
1291
|
-
|
|
1312
|
+
// BIND BEFORE CHAINING. bindThread only ran at the end of a normal
|
|
1313
|
+
// turn, and both auto paths return before reaching it — so in auto
|
|
1314
|
+
// mode nothing was ever bound, exactly when the agent produces the
|
|
1315
|
+
// most material (command output, GLOB results, MCP tool lists). The
|
|
1316
|
+
// holographic context stopped growing precisely when it mattered.
|
|
1317
|
+
bindThread(t).catch(() => {});
|
|
1318
|
+
runTurn(threadId, condense('(command output)', output), onEvent).catch(() => {});
|
|
1292
1319
|
} else {
|
|
1293
1320
|
const note = `(auto-run stopped after ${AUTO_MAX_STEPS} chained commands — say "continue" to keep going)`;
|
|
1294
1321
|
t.history.push({ who: 'bot', text: note });
|
|
1295
1322
|
onEvent?.({ type: 'final', name: t.name, color: t.color, text: note });
|
|
1296
1323
|
saveThreads();
|
|
1324
|
+
bindThread(t).catch(() => {});
|
|
1297
1325
|
}
|
|
1298
1326
|
return;
|
|
1299
1327
|
}
|
|
@@ -1330,7 +1358,8 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
1330
1358
|
if (t.runMode === 'auto' && ack !== null && ack !== undefined) {
|
|
1331
1359
|
t.autoSteps = (t.autoSteps || 0) + 1;
|
|
1332
1360
|
if (t.autoSteps < AUTO_MAX_STEPS) {
|
|
1333
|
-
|
|
1361
|
+
bindThread(t).catch(() => {}); // bind every hop, not just the last one
|
|
1362
|
+
runTurn(threadId, condense('(directive result)', ack), onEvent).catch(() => {});
|
|
1334
1363
|
return;
|
|
1335
1364
|
}
|
|
1336
1365
|
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.13",
|
|
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",
|