ucode-agent 1.61.0 → 1.62.0
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/README.md +3 -0
- package/package.json +1 -1
- package/src/core/doctor.js +6 -17
- package/src/core/loop.js +25 -1
- package/src/core/provider.js +11 -1
package/README.md
CHANGED
|
@@ -96,6 +96,9 @@ Served by Google (aistudio.google.com), free with a key.
|
|
|
96
96
|
|
|
97
97
|
`/model` shows them and switches. `ucode -m gemini-3.5-flash` starts on one.
|
|
98
98
|
|
|
99
|
+
When Google is overloaded and Flash-Lite stops answering, ucode carries on with
|
|
100
|
+
3.5 Flash by itself and goes back to Flash-Lite a few minutes later.
|
|
101
|
+
|
|
99
102
|
## What it does
|
|
100
103
|
|
|
101
104
|
**Twenty-one tools.** `create_app`, `read_file`, `read_files`, `write_file`,
|
package/package.json
CHANGED
package/src/core/doctor.js
CHANGED
|
@@ -12,7 +12,7 @@ import os from 'node:os';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { theme, dim, blue } from '../ui/theme.js';
|
|
14
14
|
import { VERSION } from './version.js';
|
|
15
|
-
import {
|
|
15
|
+
import { ENV_FILE, BASE_URL, providerKey } from './provider.js';
|
|
16
16
|
import { newer } from './updater.js';
|
|
17
17
|
|
|
18
18
|
const DEADLINE = 6000;
|
|
@@ -27,22 +27,11 @@ async function checkKey() {
|
|
|
27
27
|
const key = providerKey();
|
|
28
28
|
if (!key) return { ok: false, name: 'API key', detail: 'not set', fix: `Add GEMINI_API_KEY=... to ${ENV_FILE} (free at aistudio.google.com/apikey)` };
|
|
29
29
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
});
|
|
36
|
-
if (r.status === 401 || r.status === 403) return { ok: false, name: 'API key', detail: 'rejected', fix: 'Check the key for typos, or make a new one' };
|
|
37
|
-
const body = await r.json().catch(() => ({}));
|
|
38
|
-
const meta = body?.error?.metadata?.headers ?? {};
|
|
39
|
-
const left = r.headers.get('x-ratelimit-remaining') ?? meta['X-RateLimit-Remaining'];
|
|
40
|
-
const reset = Number(r.headers.get('x-ratelimit-reset') ?? meta['X-RateLimit-Reset']);
|
|
41
|
-
if (r.status === 429 && /per[- ]day|daily/i.test(`${body?.error?.message} ${body?.error?.metadata?.limit_source}`)) {
|
|
42
|
-
const at = Number.isFinite(reset) ? new Date(reset).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : 'tomorrow';
|
|
43
|
-
return { ok: false, name: 'Requests today', detail: 'daily free limit used up', fix: `It resets at ${at}; adding credit raises the limit` };
|
|
44
|
-
}
|
|
45
|
-
return { ok: true, name: 'API key', detail: left != null ? `works · ${left} free requests left today` : 'works' };
|
|
30
|
+
// Listing models checks the key in under a second; a chat request can queue for half a minute.
|
|
31
|
+
const r = await fetch(`${BASE_URL}/models`, { headers: { Authorization: `Bearer ${key}` }, signal: timed() });
|
|
32
|
+
if ([400, 401, 403].includes(r.status)) return { ok: false, name: 'API key', detail: 'rejected', fix: 'Check the key for typos, or make a new one' };
|
|
33
|
+
if (!r.ok) return { ok: false, name: 'API key', detail: `could not check (HTTP ${r.status})`, fix: 'Try again in a minute' };
|
|
34
|
+
return { ok: true, name: 'API key', detail: 'works' };
|
|
46
35
|
} catch (err) {
|
|
47
36
|
return { ok: false, name: 'API key', detail: `could not check (${err.name === 'TimeoutError' ? 'no answer' : err.message})`, fix: 'Check your internet connection' };
|
|
48
37
|
}
|
package/src/core/loop.js
CHANGED
|
@@ -27,7 +27,7 @@ import { spawn } from 'node:child_process';
|
|
|
27
27
|
|
|
28
28
|
import {
|
|
29
29
|
ask, model, setModel, modelName, modelList, contextLimit, rateLimits,
|
|
30
|
-
estimateConversation, MODELS, DEFAULT_MODEL, PROVIDER, fallbackFor,
|
|
30
|
+
estimateConversation, MODELS, DEFAULT_MODEL, PROVIDER, fallbackFor, backupFor,
|
|
31
31
|
} from './provider.js';
|
|
32
32
|
import {
|
|
33
33
|
tools, runTool, describe, setRoot, setConfirm, setRequest,
|
|
@@ -1946,6 +1946,26 @@ export class Agent {
|
|
|
1946
1946
|
.map((f) => ({ abs: path.resolve(this.cwd, f.path), size: f.content.length, path: f.path }));
|
|
1947
1947
|
}
|
|
1948
1948
|
|
|
1949
|
+
/**
|
|
1950
|
+
* A model that has just made todo-app/ sometimes writes app.js beside it
|
|
1951
|
+
* instead of in it: the page never gets its script, and the hand-over, which
|
|
1952
|
+
* watches the app folder, never fires. A bare name the app already has, and
|
|
1953
|
+
* this folder does not, goes into the app.
|
|
1954
|
+
*/
|
|
1955
|
+
intoApp(call) {
|
|
1956
|
+
const app = this.apps?.at(-1);
|
|
1957
|
+
if (!app) return;
|
|
1958
|
+
const fix = (p) => {
|
|
1959
|
+
if (typeof p !== 'string' || path.isAbsolute(p) || existsSync(path.resolve(this.cwd, p))) return p;
|
|
1960
|
+
const inside = path.join(app, p);
|
|
1961
|
+
if (path.relative(app, inside).startsWith('..') || !existsSync(inside)) return p;
|
|
1962
|
+
return path.relative(this.cwd, inside).split(path.sep).join('/');
|
|
1963
|
+
};
|
|
1964
|
+
const a = call.args ?? {};
|
|
1965
|
+
if (call.name === 'write_file') a.path = fix(a.path);
|
|
1966
|
+
else if (call.name === 'batch_write') a.files = normaliseFiles(a.files).map((f) => (f && typeof f === 'object' ? { ...f, path: fix(f.path) } : f));
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1949
1969
|
async dispatch(call, offered = this.offering) {
|
|
1950
1970
|
// The model emitted arguments that were not valid JSON. Hand the parser's
|
|
1951
1971
|
// own complaint straight back so it can correct itself next step.
|
|
@@ -2053,6 +2073,8 @@ export class Agent {
|
|
|
2053
2073
|
}
|
|
2054
2074
|
}
|
|
2055
2075
|
|
|
2076
|
+
if (call.name === 'write_file' || call.name === 'batch_write') this.intoApp(call);
|
|
2077
|
+
|
|
2056
2078
|
if (call.name === 'load_skill') return this.loadSkill(call.args?.name);
|
|
2057
2079
|
if (call.name === 'update_plan') return this.updatePlan(call.args?.items);
|
|
2058
2080
|
if (call.name === 'delegate') return this.delegate(call.args?.tasks);
|
|
@@ -2073,6 +2095,8 @@ export class Agent {
|
|
|
2073
2095
|
if (++this.failovers > MAX_FAILOVERS) return false;
|
|
2074
2096
|
const from = model();
|
|
2075
2097
|
let next = fallbackFor(from, this.tried);
|
|
2098
|
+
const backup = err.kind !== 'rate_limit' && backupFor(from);
|
|
2099
|
+
if (!next && backup && !this.tried?.has(backup)) next = backup;
|
|
2076
2100
|
|
|
2077
2101
|
const why = err.kind === 'rate_limit' ? 'busy' : err.kind === 'timeout' ? 'too slow to answer' : 'not answering';
|
|
2078
2102
|
|
package/src/core/provider.js
CHANGED
|
@@ -104,6 +104,15 @@ export function fallbackFor(id, tried = new Set()) {
|
|
|
104
104
|
return null;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Where a Flash-Lite model goes when Google is overloaded (503) or it freezes.
|
|
109
|
+
* Unlike fallbackFor this is on by default: both Lite models go down together
|
|
110
|
+
* at peak hours while Flash keeps answering, and a build that waits on them
|
|
111
|
+
* can wait for hours. UCODE_BACKUP=0 turns it off.
|
|
112
|
+
*/
|
|
113
|
+
export const backupFor = (id) =>
|
|
114
|
+
/flash-lite/.test(id) && process.env.UCODE_BACKUP !== '0' ? 'gemini-3.5-flash' : null;
|
|
115
|
+
|
|
107
116
|
/** Seconds to wait on successive rate limits that come with no retry-after. */
|
|
108
117
|
const RATE_LIMIT_BACKOFF = [5, 10, 20];
|
|
109
118
|
|
|
@@ -711,7 +720,7 @@ export async function ask(messages, tools = [], opts = {}) {
|
|
|
711
720
|
kind: 'stalled',
|
|
712
721
|
attempted: `asking ${modelName(id)} for a reply`,
|
|
713
722
|
failed: `${modelName(id)} froze ${MAX_STALLS} times in a row — it took the request and then sent nothing.`,
|
|
714
|
-
fix: '
|
|
723
|
+
fix: 'Google is struggling with it right now. Run /model and pick another one.',
|
|
715
724
|
cause: problem,
|
|
716
725
|
});
|
|
717
726
|
}
|
|
@@ -741,6 +750,7 @@ export async function ask(messages, tools = [], opts = {}) {
|
|
|
741
750
|
if (!worthRetrying || attempt === attempts || opts.signal?.aborted) break;
|
|
742
751
|
if (printed > 0) break; // half an answer is on screen; do not print it twice
|
|
743
752
|
if (problem.detail?.handed) break; // its first tool calls are already running
|
|
753
|
+
if (problem.kind === 'timeout' && backupFor(id)) break; // one freeze is enough: the loop moves to the backup
|
|
744
754
|
|
|
745
755
|
// A stalled provider needs longer to come back than a dropped socket
|
|
746
756
|
// does, and the wait is narrated so a slow turn never looks like a hang.
|