ucode-agent 1.3.0 → 1.5.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 +42 -12
- package/package.json +5 -3
- package/skills/build-app/SKILL.md +23 -3
- package/skills/ui-ux/SKILL.md +5 -1
- package/src/core/loop.js +181 -20
- package/src/core/provider.js +40 -12
- package/src/core/updater.js +95 -0
- package/src/tools/browser.js +258 -0
- package/src/tools/index.js +51 -1
- package/src/tools/scaffold.js +130 -0
- package/src/tools/shell.js +8 -1
- package/src/ui/screen.js +20 -6
- package/templates/next-shadcn/AGENTS.md +9 -0
- package/templates/next-shadcn/README.md +9 -0
- package/templates/next-shadcn/TEMPLATE.md +39 -0
- package/templates/next-shadcn/_gitignore +41 -0
- package/templates/next-shadcn/_package-lock.json +10278 -0
- package/templates/next-shadcn/components.json +25 -0
- package/templates/next-shadcn/eslint.config.mjs +18 -0
- package/templates/next-shadcn/next-env.d.ts +5 -0
- package/templates/next-shadcn/next.config.ts +7 -0
- package/templates/next-shadcn/package.json +37 -0
- package/templates/next-shadcn/postcss.config.mjs +7 -0
- package/templates/next-shadcn/src/app/favicon.ico +0 -0
- package/templates/next-shadcn/src/app/globals.css +155 -0
- package/templates/next-shadcn/src/app/layout.tsx +42 -0
- package/templates/next-shadcn/src/app/page.tsx +14 -0
- package/templates/next-shadcn/src/components/theme-provider.tsx +7 -0
- package/templates/next-shadcn/src/components/theme-toggle.tsx +27 -0
- package/templates/next-shadcn/src/components/ui/alert-dialog.tsx +187 -0
- package/templates/next-shadcn/src/components/ui/avatar.tsx +108 -0
- package/templates/next-shadcn/src/components/ui/badge.tsx +51 -0
- package/templates/next-shadcn/src/components/ui/button.tsx +57 -0
- package/templates/next-shadcn/src/components/ui/calendar.tsx +221 -0
- package/templates/next-shadcn/src/components/ui/card.tsx +102 -0
- package/templates/next-shadcn/src/components/ui/checkbox.tsx +28 -0
- package/templates/next-shadcn/src/components/ui/command.tsx +196 -0
- package/templates/next-shadcn/src/components/ui/dialog.tsx +160 -0
- package/templates/next-shadcn/src/components/ui/dropdown-menu.tsx +267 -0
- package/templates/next-shadcn/src/components/ui/input-group.tsx +158 -0
- package/templates/next-shadcn/src/components/ui/input.tsx +19 -0
- package/templates/next-shadcn/src/components/ui/label.tsx +19 -0
- package/templates/next-shadcn/src/components/ui/popover.tsx +89 -0
- package/templates/next-shadcn/src/components/ui/progress.tsx +82 -0
- package/templates/next-shadcn/src/components/ui/scroll-area.tsx +54 -0
- package/templates/next-shadcn/src/components/ui/select.tsx +200 -0
- package/templates/next-shadcn/src/components/ui/separator.tsx +24 -0
- package/templates/next-shadcn/src/components/ui/sheet.tsx +138 -0
- package/templates/next-shadcn/src/components/ui/skeleton.tsx +13 -0
- package/templates/next-shadcn/src/components/ui/sonner.tsx +49 -0
- package/templates/next-shadcn/src/components/ui/switch.tsx +31 -0
- package/templates/next-shadcn/src/components/ui/tabs.tsx +81 -0
- package/templates/next-shadcn/src/components/ui/textarea.tsx +17 -0
- package/templates/next-shadcn/src/components/ui/tooltip.tsx +65 -0
- package/templates/next-shadcn/src/lib/utils.ts +1 -0
- package/templates/next-shadcn/tsconfig.json +34 -0
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ corner:
|
|
|
19
19
|
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
20
20
|
│ › Ask anything… │
|
|
21
21
|
│ │
|
|
22
|
-
│ ◆ Build ·
|
|
22
|
+
│ ◆ Build · North Mini Code 0% │
|
|
23
23
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
24
24
|
|
|
25
25
|
|
|
@@ -44,7 +44,7 @@ the input, so your own words are easy to find in a long session:
|
|
|
44
44
|
╭──────────────────────────────────────────────────────────────────────────────────╮
|
|
45
45
|
│ › now add a dark mode toggle │
|
|
46
46
|
│ │
|
|
47
|
-
│ ◆ Build ·
|
|
47
|
+
│ ◆ Build · North Mini Code 4% │
|
|
48
48
|
╰──────────────────────────────────────────────────────────────────────────────────╯
|
|
49
49
|
```
|
|
50
50
|
|
|
@@ -88,27 +88,43 @@ properly, which is the thing an agent actually depends on.
|
|
|
88
88
|
|
|
89
89
|
| Model | Context | For |
|
|
90
90
|
| --- | --- | --- |
|
|
91
|
-
|
|
|
91
|
+
| Nemotron 3 Ultra | 1M | deepest reasoning, slowest to first token |
|
|
92
92
|
| Nemotron 3.5 Lightning | 1M | the same enormous window, answers much sooner |
|
|
93
93
|
| Nemotron 3 Super | 262k | strong all-rounder, quick to start |
|
|
94
94
|
| Nemotron 3 Nano Omni | 256k | small, fast, reasoning tuned |
|
|
95
|
-
| **North Mini Code** ★ | 256k |
|
|
95
|
+
| **North Mini Code** ★ | 256k | the default — built for code and interface work, quick to answer |
|
|
96
96
|
|
|
97
97
|
`/model` shows them and switches. `ucode -m cohere/north-mini-code:free`
|
|
98
98
|
starts on one.
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
North Mini Code is the default: it is built for code and interfaces, which is most
|
|
101
|
+
of what ucode is asked to do, and it answers far sooner than the big reasoning
|
|
102
|
+
models. Switch to Ultra when a problem needs the million-token window more than
|
|
103
|
+
the speed.
|
|
104
|
+
|
|
105
|
+
**A busy model never stops a build.** Free endpoints are shared, and "too many
|
|
106
|
+
requests" is routine. ucode waits it out with growing pauses, and if the model
|
|
107
|
+
stays busy it carries on with the next one — North Mini Code, then Nemotron 3.5
|
|
108
|
+
Lightning, Super, Ultra — from exactly where it was, and tells you it switched.
|
|
109
|
+
If every model is busy at once it waits a minute and goes round again. Your
|
|
110
|
+
chosen model gets another go a few minutes later.
|
|
103
111
|
|
|
104
112
|
## What it does
|
|
105
113
|
|
|
106
|
-
**
|
|
107
|
-
`edit_file`, `multi_edit`, `edit_files`, `list_dir`, `glob`,
|
|
108
|
-
`run_command`, `run_commands`, `web_search`. Read-only calls run in parallel,
|
|
114
|
+
**Fifteen tools.** `create_app`, `read_file`, `read_files`, `write_file`,
|
|
115
|
+
`batch_write`, `edit_file`, `multi_edit`, `edit_files`, `list_dir`, `glob`,
|
|
116
|
+
`grep`, `run_command`, `run_commands`, `look_at_app`, `web_search`. Read-only calls run in parallel,
|
|
109
117
|
and start the moment the model finishes writing them — while the rest of its
|
|
110
118
|
reply is still arriving. Anything that writes runs on its own, in order.
|
|
111
119
|
|
|
120
|
+
**Apps start from a ready-made starter.** Setting up Next.js and shadcn from
|
|
121
|
+
nothing takes about four minutes — `create-next-app` and the shadcn CLI measured
|
|
122
|
+
at 116s and 130s — plus a dozen model round trips. `create_app` copies ucode's
|
|
123
|
+
starter instead: Next.js 16, TypeScript, Tailwind 4, shadcn/ui with 25 common
|
|
124
|
+
components, light/dark mode, toasts and a considered theme, already known to
|
|
125
|
+
build. The copy takes under a second, and its install runs in the background
|
|
126
|
+
while the model writes the first components.
|
|
127
|
+
|
|
112
128
|
**Parallel workers.** When a build splits into parts that touch different files
|
|
113
129
|
— the API route, the upload component, the results view — the model hands them
|
|
114
130
|
to up to three workers that build at the same time, each line in the transcript
|
|
@@ -119,6 +135,14 @@ written, its install starts in the background while the rest of the app is
|
|
|
119
135
|
still being written. An install the model asks for later waits for that one
|
|
120
136
|
instead of running twice, and anything run in that folder waits for it too.
|
|
121
137
|
|
|
138
|
+
**It looks at what it built.** `look_at_app` opens the running app in a real
|
|
139
|
+
browser — the Edge or Chrome already on your machine, so there is nothing extra
|
|
140
|
+
to download — at 375px and 1440px. It reports console errors, failed requests,
|
|
141
|
+
content that spills off a phone screen, broken images and unlabeled controls,
|
|
142
|
+
saves screenshots to `.ucode/screenshots`, and has Nemotron Nano Omni review them
|
|
143
|
+
the way a designer would. The model fixes what it finds before calling the app
|
|
144
|
+
done.
|
|
145
|
+
|
|
122
146
|
**Errors fixed before you see them.** When the model says it is done, ucode
|
|
123
147
|
type-checks every file it changed — `tsc --noEmit` for TypeScript projects,
|
|
124
148
|
a syntax check for JavaScript and Python — and hands any errors back to fix,
|
|
@@ -154,7 +178,12 @@ them; the chip inside the input box says which is live.
|
|
|
154
178
|
|
|
155
179
|
**Sessions.** Everything is on disk under `~/.ucode/sessions`, saved after every
|
|
156
180
|
step. `/resume` lists them with what each one was actually about, the ones from
|
|
157
|
-
this folder first.
|
|
181
|
+
this folder first. Press `d` twice on one to delete it — the list stays open, so
|
|
182
|
+
clearing out several is quick — or `/session delete 2,5`.
|
|
183
|
+
|
|
184
|
+
**It updates itself.** Each launch checks npm in the background and, if there is
|
|
185
|
+
a newer version, installs it while you work. The next launch is the new one.
|
|
186
|
+
Set `UCODE_NO_UPDATE=1` to turn that off.
|
|
158
187
|
|
|
159
188
|
**A context window that folds rather than forgets.** Past 75% the oldest turns
|
|
160
189
|
are summarised instead of dropped, never cutting between a tool call and its
|
|
@@ -207,6 +236,7 @@ Everything after the frontmatter is the instruction.
|
|
|
207
236
|
| `/help` | the list |
|
|
208
237
|
| `/model` | show the models and switch — `/models` does the same |
|
|
209
238
|
| `/resume` | pick up an earlier conversation — `/session`, `/sessions` too |
|
|
239
|
+
| `/session delete 2,5` | delete saved conversations by number (or `d d` in the list) |
|
|
210
240
|
| `/new` | save this one and start fresh |
|
|
211
241
|
| `/remember <note>` | add a standing note to this project's `UCODE.md` |
|
|
212
242
|
| `/skills` | what it knows how to do, and what is loaded |
|
|
@@ -245,7 +275,7 @@ ucode [options]
|
|
|
245
275
|
Environment overrides: `UCODE_MODEL`, `UCODE_WORKER_MODEL` (a faster model for
|
|
246
276
|
parallel workers), `UCODE_WORKER_STEPS`, `UCODE_MAX_CONTEXT_TOKENS`,
|
|
247
277
|
`UCODE_MAX_STEPS`, `UCODE_MAX_TOOL_OUTPUT`, `UCODE_REQUEST_TIMEOUT_MS`,
|
|
248
|
-
`UCODE_BASE_URL`.
|
|
278
|
+
`UCODE_BASE_URL`, `UCODE_NO_UPDATE`.
|
|
249
279
|
|
|
250
280
|
Web search needs a Tavily key — free, 1000 searches a month, no card. Without
|
|
251
281
|
one, ucode answers from what it knows and says that it could not check.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ucode-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "ucode - a terminal coding agent that reads, edits and runs your code, on NVIDIA and Cohere models.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "ucode.js",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"ucode.js",
|
|
12
12
|
"src/",
|
|
13
|
-
"skills/"
|
|
13
|
+
"skills/",
|
|
14
|
+
"templates/"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
16
17
|
"start": "node ucode.js",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"dotenv": "^17.4.2",
|
|
49
50
|
"marked": "^15.0.12",
|
|
50
51
|
"marked-terminal": "^7.3.0",
|
|
51
|
-
"openai": "^7.4.0"
|
|
52
|
+
"openai": "^7.4.0",
|
|
53
|
+
"playwright-core": "^1.63.0"
|
|
52
54
|
}
|
|
53
55
|
}
|
|
@@ -31,7 +31,25 @@ If there is a user interface, the `ui-ux` skill is already loaded. Decide the
|
|
|
31
31
|
design direction now, not after the logic works. If the app calls a model,
|
|
32
32
|
load `ai-features`; if it has accounts, keys or uploads, load `security`.
|
|
33
33
|
|
|
34
|
-
## 2.
|
|
34
|
+
## 2. Start from the starter
|
|
35
|
+
|
|
36
|
+
**For a Next.js app, call `create_app`** — one step, about a second:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
create_app({ folder: "my-app", name: "My App", description: "…" })
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
It copies ucode's ready-made starter — Next.js 16, TypeScript, Tailwind 4,
|
|
43
|
+
shadcn/ui with 25 common components, light/dark mode, toasts, a considered
|
|
44
|
+
theme — which is already known to build, and starts `npm install` in the
|
|
45
|
+
background. Read the `TEMPLATE.md` it lists, then start writing components
|
|
46
|
+
straight away; commands in that folder wait for the install on their own.
|
|
47
|
+
Re-tint the palette in `globals.css` and swap the font for the app's direction.
|
|
48
|
+
|
|
49
|
+
Never run `create-next-app` or `shadcn init` for a Next.js app — that is
|
|
50
|
+
four minutes and a dozen steps the starter already did.
|
|
51
|
+
|
|
52
|
+
### Other stacks
|
|
35
53
|
|
|
36
54
|
Nothing you run has a keyboard. A scaffolder that asks "Would you like to use
|
|
37
55
|
TypeScript?" gets no answer and fails, so give it every answer up front:
|
|
@@ -141,8 +159,10 @@ Use `batch_write` for the skeleton — one call, every file.
|
|
|
141
159
|
- `npm run build` — it type-checks and lints; a build that fails is not done.
|
|
142
160
|
- Start it: `npm run dev` goes to the background on its own and comes back with
|
|
143
161
|
the URL once ready. Do not start it twice.
|
|
144
|
-
- Exercise it: `curl` the API route with real input,
|
|
145
|
-
|
|
162
|
+
- Exercise it: `curl` the API route with real input, then `look_at_app` on every
|
|
163
|
+
page — it loads them in a real browser at phone and desktop width and reports
|
|
164
|
+
errors, overflow and a visual review. A clean build proves it compiles, not
|
|
165
|
+
that it works.
|
|
146
166
|
- Fix what you find and check again.
|
|
147
167
|
|
|
148
168
|
## 7. Definition of done
|
package/skills/ui-ux/SKILL.md
CHANGED
|
@@ -283,7 +283,11 @@ because it was first to hand means you were not deciding.
|
|
|
283
283
|
|
|
284
284
|
## 9. Look at it, then report
|
|
285
285
|
|
|
286
|
-
Run it (`npm run dev` starts in the background and returns the URL)
|
|
286
|
+
Run it (`npm run dev` starts in the background and returns the URL), then call
|
|
287
|
+
`look_at_app` with that URL. It opens the app in a real browser at 375px and
|
|
288
|
+
1440px, reports console errors, failed requests, overflow, broken images and
|
|
289
|
+
unlabeled controls, and returns a designer's review of the screenshots. Fix what
|
|
290
|
+
it finds and look again. Then check:
|
|
287
291
|
|
|
288
292
|
1. **375px wide** — no horizontal scroll, nothing overlapping or clipped. If you
|
|
289
293
|
wrote no responsive rules at all, you have not done this.
|
package/src/core/loop.js
CHANGED
|
@@ -17,14 +17,16 @@ import { spawn } from 'node:child_process';
|
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
19
|
ask, model, setModel, modelName, modelList, contextLimit, rateLimits,
|
|
20
|
-
MODELS, DEFAULT_MODEL, PROVIDER,
|
|
20
|
+
MODELS, DEFAULT_MODEL, PROVIDER, fallbackFor,
|
|
21
21
|
} from './provider.js';
|
|
22
22
|
import {
|
|
23
23
|
tools, runTool, describe, setRoot, setConfirm, PARALLEL_SAFE, WRITES, FILE_WRITES,
|
|
24
24
|
} from '../tools/index.js';
|
|
25
25
|
import { projectMap, loadMemory, remember, MEMORY_FILE } from './context.js';
|
|
26
|
+
import { autoUpdate } from './updater.js';
|
|
27
|
+
import { closeBrowser } from '../tools/browser.js';
|
|
26
28
|
import {
|
|
27
|
-
newSession, save, load, list, removeAll, titleFrom,
|
|
29
|
+
newSession, save, load, list, remove, removeAll, titleFrom,
|
|
28
30
|
} from './history.js';
|
|
29
31
|
import { fold, usage, tooBig, SUMMARY_PROMPT, forSummary } from './window.js';
|
|
30
32
|
import { loadSkills, catalogue, findSkill, skillMessage, autoLoadFor } from './skills.js';
|
|
@@ -64,6 +66,17 @@ const MAX_FIX_ROUNDS = 3;
|
|
|
64
66
|
/** Files worth checking after they change. */
|
|
65
67
|
const CHECKABLE = /\.(?:[cm]?[jt]sx?|py)$/i;
|
|
66
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Failures that are the provider's and not the model's: busy, slow, down, or
|
|
71
|
+
* unreachable. None of them should end a build — the turn moves to another
|
|
72
|
+
* model and carries on from exactly where it was.
|
|
73
|
+
*/
|
|
74
|
+
const TRANSIENT = new Set(['rate_limit', 'timeout', 'server', 'network', 'no_content']);
|
|
75
|
+
const MAX_FAILOVERS = 8;
|
|
76
|
+
const COOLDOWN = 5 * 60_000;
|
|
77
|
+
|
|
78
|
+
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
79
|
+
|
|
67
80
|
/** Parallel workers at once, and how many steps each may take. */
|
|
68
81
|
const MAX_WORKERS = 3;
|
|
69
82
|
const WORKER_STEPS = Number(process.env.UCODE_WORKER_STEPS) || 60;
|
|
@@ -129,7 +142,7 @@ const delegateTool = {
|
|
|
129
142
|
};
|
|
130
143
|
|
|
131
144
|
/** The instructions a parallel worker starts with. */
|
|
132
|
-
function workerPrompt({ cwd, name, memory, skills }) {
|
|
145
|
+
function workerPrompt({ cwd, name, memory, skills, map }) {
|
|
133
146
|
return [
|
|
134
147
|
`You are a ucode worker called "${name}" - one of several building parts of the same project at the same time.`,
|
|
135
148
|
'',
|
|
@@ -145,7 +158,11 @@ function workerPrompt({ cwd, name, memory, skills }) {
|
|
|
145
158
|
'- Finish what you build: real content, every state handled, no TODOs.',
|
|
146
159
|
'- When done, reply with two or three sentences: what you built, in which files, and',
|
|
147
160
|
' anything the lead has to wire up.',
|
|
161
|
+
'- The files you own may not exist yet - create them. Do not go looking for them',
|
|
162
|
+
' first. The project map below shows what does exist; read only the files whose',
|
|
163
|
+
' interfaces you must match, then start writing within two or three steps.',
|
|
148
164
|
...(memory ? ['', '## Project memory', '', memory] : []),
|
|
165
|
+
...(map ? ['', '## Project map', '', map] : []),
|
|
149
166
|
...(skills ? ['', '## Instructions in force', '', skills] : []),
|
|
150
167
|
].join('\n');
|
|
151
168
|
}
|
|
@@ -223,6 +240,8 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
|
|
|
223
240
|
' one at a time when you already know which ones you want.',
|
|
224
241
|
'- Put independent calls in the same message — several greps, a glob and a read.',
|
|
225
242
|
' Read-only calls in one message run at the same time.',
|
|
243
|
+
'- New Next.js app? create_app - one step, never create-next-app or shadcn init. It',
|
|
244
|
+
' copies a starter that already builds and installs it in the background.',
|
|
226
245
|
'- batch_write to lay out several new files at once, multi_edit for several changes',
|
|
227
246
|
' to one file, edit_files for a change that spans several files.',
|
|
228
247
|
'- For work with three or more steps, keep a short plan with update_plan - at most',
|
|
@@ -234,6 +253,9 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
|
|
|
234
253
|
' writing files. Running the install yourself afterwards just waits for that one.',
|
|
235
254
|
'- When you finish, ucode type-checks what you changed and hands you the errors, so',
|
|
236
255
|
' there is no need to run tsc yourself.',
|
|
256
|
+
'- Once the dev server is ready, run look_at_app on the pages you built and fix what',
|
|
257
|
+
' it reports - errors, layout that overflows a phone, and the visual review - then',
|
|
258
|
+
' look again. Do not call an interface finished before it has been looked at.',
|
|
237
259
|
'- Nothing you run has a keyboard. Pass the non-interactive flag to anything that',
|
|
238
260
|
' would ask a question, or it fails instead of waiting: create-next-app --yes,',
|
|
239
261
|
' npx shadcn@latest init -d -y, npx shadcn@latest add <names> -y, npm init -y.',
|
|
@@ -373,6 +395,14 @@ export class Agent {
|
|
|
373
395
|
|
|
374
396
|
this.showHeader();
|
|
375
397
|
this.installSignals();
|
|
398
|
+
|
|
399
|
+
// Checked in the background; nothing here waits on it.
|
|
400
|
+
autoUpdate({
|
|
401
|
+
onUpdated: (version) => {
|
|
402
|
+
this.ui.setFacts?.({ update: version });
|
|
403
|
+
if (!this.ui.welcoming?.()) this.ui.note(`updated to v${version} — it takes over the next time you start ucode`);
|
|
404
|
+
},
|
|
405
|
+
});
|
|
376
406
|
await this.repl();
|
|
377
407
|
}
|
|
378
408
|
|
|
@@ -485,6 +515,7 @@ export class Agent {
|
|
|
485
515
|
}
|
|
486
516
|
|
|
487
517
|
async shutdown() {
|
|
518
|
+
await closeBrowser().catch(() => {});
|
|
488
519
|
this.ui.stopSpinner();
|
|
489
520
|
if (this.session.messages.length) {
|
|
490
521
|
await this.persist();
|
|
@@ -559,6 +590,14 @@ export class Agent {
|
|
|
559
590
|
]);
|
|
560
591
|
await this.persist();
|
|
561
592
|
|
|
593
|
+
// A busy model was swapped for a fallback earlier; after a few minutes the
|
|
594
|
+
// one the user chose gets another go.
|
|
595
|
+
this.preferred ??= model();
|
|
596
|
+
if (model() !== this.preferred && Date.now() > (this.cooldownUntil ?? 0)) {
|
|
597
|
+
setModel(this.preferred);
|
|
598
|
+
if (this.full) this.showHeader({ clear: false });
|
|
599
|
+
}
|
|
600
|
+
|
|
562
601
|
this.busy = true;
|
|
563
602
|
this.abort = new AbortController();
|
|
564
603
|
|
|
@@ -591,6 +630,8 @@ export class Agent {
|
|
|
591
630
|
let askedToVerify = false;
|
|
592
631
|
let askedToSpeak = false;
|
|
593
632
|
let fixRounds = 0;
|
|
633
|
+
this.failovers = 0;
|
|
634
|
+
this.tried = new Set([model()]);
|
|
594
635
|
|
|
595
636
|
this.touched = new Set();
|
|
596
637
|
this.sinceCheck = new Set();
|
|
@@ -668,6 +709,10 @@ export class Agent {
|
|
|
668
709
|
});
|
|
669
710
|
continue;
|
|
670
711
|
}
|
|
712
|
+
|
|
713
|
+
// Busy, slow or down: move to the next model and carry on, rather
|
|
714
|
+
// than ending a half-built app with an error.
|
|
715
|
+
if (TRANSIENT.has(err.kind) && !this.abort.signal.aborted && (await this.failover(err))) continue;
|
|
671
716
|
throw err;
|
|
672
717
|
}
|
|
673
718
|
|
|
@@ -917,6 +962,38 @@ export class Agent {
|
|
|
917
962
|
});
|
|
918
963
|
}
|
|
919
964
|
|
|
965
|
+
/**
|
|
966
|
+
* Switch to the next model after a provider failure. Returns false once
|
|
967
|
+
* there is nothing sensible left to try. When every model is busy at once,
|
|
968
|
+
* it waits a minute and goes round again rather than giving up.
|
|
969
|
+
*/
|
|
970
|
+
async failover(err) {
|
|
971
|
+
if (++this.failovers > MAX_FAILOVERS) return false;
|
|
972
|
+
const from = model();
|
|
973
|
+
let next = fallbackFor(from, this.tried);
|
|
974
|
+
|
|
975
|
+
if (!next) {
|
|
976
|
+
const until = Date.now() + 60_000;
|
|
977
|
+
this.ui.startSpinner('every model is busy');
|
|
978
|
+
while (Date.now() < until && !this.abort?.signal.aborted) {
|
|
979
|
+
this.ui.updateSpinner(`every model is busy — trying again in ${Math.ceil((until - Date.now()) / 1000)}s`);
|
|
980
|
+
await wait(1000);
|
|
981
|
+
}
|
|
982
|
+
this.ui.stopSpinner();
|
|
983
|
+
if (this.abort?.signal.aborted) return false;
|
|
984
|
+
this.tried = new Set();
|
|
985
|
+
next = this.preferred && this.preferred !== from ? this.preferred : fallbackFor(from, this.tried) ?? from;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
this.tried.add(next);
|
|
989
|
+
setModel(next);
|
|
990
|
+
this.cooldownUntil = Date.now() + COOLDOWN;
|
|
991
|
+
const why = err.kind === 'rate_limit' ? 'busy' : err.kind === 'timeout' ? 'too slow to answer' : 'not answering';
|
|
992
|
+
this.ui.note(`${modelName(from)} is ${why} — carrying on with ${modelName(next)}`);
|
|
993
|
+
if (this.full) this.showHeader({ clear: false });
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
|
|
920
997
|
/** Run a call and settle to { out } or { err } — never throws. */
|
|
921
998
|
execute(call) {
|
|
922
999
|
return this.dispatch(call).then((out) => ({ out }), (err) => ({ err }));
|
|
@@ -964,10 +1041,18 @@ export class Agent {
|
|
|
964
1041
|
|
|
965
1042
|
for (const r of results) for (const f of r.touched) { this.touched.add(f); this.sinceCheck.add(f); }
|
|
966
1043
|
|
|
1044
|
+
// A worker that wrote nothing has not done its part, whatever it said.
|
|
1045
|
+
// The lead builds those itself rather than leaving holes in the app.
|
|
1046
|
+
const empty = results.filter((r) => !r.touched.length).map((r) => r.name);
|
|
1047
|
+
|
|
967
1048
|
return {
|
|
968
1049
|
content: results
|
|
969
1050
|
.map((r) => `## ${r.name}\n${r.summary}\nFiles changed: ${r.touched.join(', ') || 'none'}`)
|
|
970
|
-
.join('\n\n')
|
|
1051
|
+
.join('\n\n') +
|
|
1052
|
+
(empty.length
|
|
1053
|
+
? `\n\n${empty.join(', ')} wrote no files. Build ${empty.length === 1 ? 'that part' : 'those parts'} ` +
|
|
1054
|
+
'yourself now, directly - do not delegate them again.'
|
|
1055
|
+
: ''),
|
|
971
1056
|
summary: results.map((r) => `${r.name} · ${r.touched.length} file${r.touched.length === 1 ? '' : 's'}`).join(' '),
|
|
972
1057
|
};
|
|
973
1058
|
}
|
|
@@ -980,16 +1065,39 @@ export class Agent {
|
|
|
980
1065
|
.map((s) => `--- ${s.name} ---\n${s.body}`)
|
|
981
1066
|
.join('\n\n');
|
|
982
1067
|
const messages = [
|
|
983
|
-
{ role: 'system', content: workerPrompt({ cwd: this.cwd, name, memory: this.memory, skills }) },
|
|
1068
|
+
{ role: 'system', content: workerPrompt({ cwd: this.cwd, name, memory: this.memory, skills, map: this.map }) },
|
|
984
1069
|
{ role: 'user', content: String(task.instructions) },
|
|
985
1070
|
];
|
|
986
1071
|
const available = this.toolsNow().filter((t) => !WORKER_EXCLUDED.has(t.name));
|
|
987
1072
|
const wanted = process.env.UCODE_WORKER_MODEL;
|
|
988
|
-
|
|
1073
|
+
let workerModel = wanted && MODELS[wanted] ? wanted : model();
|
|
1074
|
+
const tried = new Set([workerModel]);
|
|
1075
|
+
let failovers = 0;
|
|
1076
|
+
|
|
1077
|
+
// Start a moment apart. Three requests in the same instant is exactly what
|
|
1078
|
+
// trips a free endpoint's rate limit, and the stagger costs a second or two.
|
|
1079
|
+
if (index) await wait(index * 1500);
|
|
989
1080
|
|
|
990
1081
|
for (let step = 0; step < WORKER_STEPS; step++) {
|
|
991
1082
|
if (this.abort?.signal.aborted) break;
|
|
992
|
-
|
|
1083
|
+
|
|
1084
|
+
let reply;
|
|
1085
|
+
try {
|
|
1086
|
+
reply = await ask(messages, available, { signal: this.abort?.signal, model: workerModel });
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
// Same rule as the lead: a busy model is swapped, not a reason to stop.
|
|
1089
|
+
if (TRANSIENT.has(err.kind) && failovers < 6 && !this.abort?.signal.aborted) {
|
|
1090
|
+
failovers++;
|
|
1091
|
+
let next = fallbackFor(workerModel, tried);
|
|
1092
|
+
if (!next) { tried.clear(); await wait(20_000); next = fallbackFor(workerModel, tried) ?? workerModel; }
|
|
1093
|
+
tried.add(next);
|
|
1094
|
+
this.ui.note(`${name}: ${modelName(workerModel)} is busy — switching to ${modelName(next)}`);
|
|
1095
|
+
workerModel = next;
|
|
1096
|
+
step--;
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
throw err;
|
|
1100
|
+
}
|
|
993
1101
|
this.record(reply.usage);
|
|
994
1102
|
|
|
995
1103
|
if (!reply.toolCalls.length) {
|
|
@@ -1220,6 +1328,7 @@ export class Agent {
|
|
|
1220
1328
|
if (arg) {
|
|
1221
1329
|
try {
|
|
1222
1330
|
setModel(arg);
|
|
1331
|
+
this.preferred = model();
|
|
1223
1332
|
} catch (err) {
|
|
1224
1333
|
this.ui.error(err, { debug: this.debug });
|
|
1225
1334
|
return;
|
|
@@ -1247,6 +1356,7 @@ export class Agent {
|
|
|
1247
1356
|
if (chosen === null) return;
|
|
1248
1357
|
|
|
1249
1358
|
setModel(all[chosen].id);
|
|
1359
|
+
this.preferred = model();
|
|
1250
1360
|
this.session.model = model();
|
|
1251
1361
|
this.ui.note(`now using ${modelName()}`);
|
|
1252
1362
|
this.showHeader({ clear: false });
|
|
@@ -1285,7 +1395,7 @@ export class Agent {
|
|
|
1285
1395
|
* and how far it got, and the ones from this folder are marked, because that
|
|
1286
1396
|
* is nearly always the one being looked for.
|
|
1287
1397
|
*/
|
|
1288
|
-
describeSession(s, width) {
|
|
1398
|
+
describeSession(s, width, i) {
|
|
1289
1399
|
const room = Math.max(24, Math.min(46, width - 34));
|
|
1290
1400
|
const mark = s.mine ? blue('●') : dim('○');
|
|
1291
1401
|
const when = relativeTime(s.updatedAt).padEnd(9);
|
|
@@ -1293,7 +1403,8 @@ export class Agent {
|
|
|
1293
1403
|
const where = s.mine ? 'here' : shortenPath(s.cwd, 26);
|
|
1294
1404
|
|
|
1295
1405
|
return {
|
|
1296
|
-
|
|
1406
|
+
// Numbered in the picker, so /session delete 3 has something to point at.
|
|
1407
|
+
label: `${i === undefined ? '' : `${dim(String(i + 1).padStart(2))} `}${mark} ${clip(s.title, room).padEnd(room)} ${dim(when)}${dim(turns)}${dim(where)}`,
|
|
1297
1408
|
sub: s.preview ? dim(` ${clip(s.preview, width - 10)}`) : '',
|
|
1298
1409
|
};
|
|
1299
1410
|
}
|
|
@@ -1314,6 +1425,10 @@ export class Agent {
|
|
|
1314
1425
|
return;
|
|
1315
1426
|
}
|
|
1316
1427
|
|
|
1428
|
+
// /session delete 3 or /session delete 2,5,7
|
|
1429
|
+
const del = /^(?:delete|del|rm|remove)\b\s*(.*)$/i.exec(arg ?? '');
|
|
1430
|
+
if (del) return this.deleteSessions(del[1]);
|
|
1431
|
+
|
|
1317
1432
|
const sessions = await list({ cwd: this.cwd });
|
|
1318
1433
|
if (!sessions.length) {
|
|
1319
1434
|
this.ui.note('no saved conversations yet');
|
|
@@ -1336,22 +1451,48 @@ export class Agent {
|
|
|
1336
1451
|
}
|
|
1337
1452
|
index = n - 1;
|
|
1338
1453
|
} else if (this.ui.pick) {
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1454
|
+
// The picker stays open while you delete, so clearing out several old
|
|
1455
|
+
// conversations is d d, d d, d d — then Enter on the one you want.
|
|
1456
|
+
let active = 0;
|
|
1457
|
+
for (;;) {
|
|
1458
|
+
const here = shown.filter((s) => s.mine).length;
|
|
1459
|
+
const picked = await this.ui.pick(
|
|
1460
|
+
shown.map((s, i) => this.describeSession(s, width, i)),
|
|
1461
|
+
{
|
|
1462
|
+
active,
|
|
1463
|
+
deletable: true,
|
|
1464
|
+
hint:
|
|
1465
|
+
`↑↓ move · enter to continue · d twice to delete · esc to cancel` +
|
|
1466
|
+
(here ? ` — ${here} from this folder` : ''),
|
|
1467
|
+
}
|
|
1468
|
+
);
|
|
1469
|
+
if (picked === null) return;
|
|
1470
|
+
if (typeof picked === 'object' && picked.delete !== undefined) {
|
|
1471
|
+
const doomed = shown[picked.delete];
|
|
1472
|
+
active = picked.delete;
|
|
1473
|
+
if (doomed.id === this.session.id) {
|
|
1474
|
+
this.ui.flash?.('that is the conversation you are in — /new first, then delete it');
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
await remove(doomed.id);
|
|
1478
|
+
shown.splice(picked.delete, 1);
|
|
1479
|
+
this.ui.flash?.(`deleted · ${clip(doomed.title, 50)}`);
|
|
1480
|
+
if (!shown.length) {
|
|
1481
|
+
this.ui.note('no saved conversations left');
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
active = Math.min(active, shown.length - 1);
|
|
1485
|
+
continue;
|
|
1347
1486
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1487
|
+
index = picked;
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1350
1490
|
} else {
|
|
1351
1491
|
this.ui.blank();
|
|
1492
|
+
this.ui.note('/session delete <number> removes one, or several: /session delete 2,5');
|
|
1352
1493
|
index = await this.ui.choose(
|
|
1353
1494
|
'continue which?',
|
|
1354
|
-
shown.map((s) => this.describeSession(s, width).label)
|
|
1495
|
+
shown.map((s, i) => this.describeSession(s, width, i).label)
|
|
1355
1496
|
);
|
|
1356
1497
|
if (index === null) return;
|
|
1357
1498
|
}
|
|
@@ -1363,6 +1504,26 @@ export class Agent {
|
|
|
1363
1504
|
}
|
|
1364
1505
|
}
|
|
1365
1506
|
|
|
1507
|
+
/** /session delete 3, or 2,5,7 — numbers as the session list shows them. */
|
|
1508
|
+
async deleteSessions(spec) {
|
|
1509
|
+
const sessions = (await list({ cwd: this.cwd })).slice(0, 25);
|
|
1510
|
+
const numbers = [...new Set(String(spec).split(/[\s,]+/).filter(Boolean).map(Number))];
|
|
1511
|
+
const bad = numbers.filter((n) => !Number.isInteger(n) || n < 1 || n > sessions.length);
|
|
1512
|
+
if (!numbers.length || bad.length) {
|
|
1513
|
+
this.ui.write(theme.warn(` usage: /session delete <number>[,<number>…] — numbers from 1 to ${sessions.length}`));
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
for (const n of numbers) {
|
|
1517
|
+
const s = sessions[n - 1];
|
|
1518
|
+
if (s.id === this.session.id) {
|
|
1519
|
+
this.ui.note(`skipped ${n} — that is the conversation you are in`);
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
await remove(s.id);
|
|
1523
|
+
this.ui.note(`deleted ${n} · ${s.title}`);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1366
1527
|
async resume(id) {
|
|
1367
1528
|
try {
|
|
1368
1529
|
const loaded = await load(id);
|
package/src/core/provider.js
CHANGED
|
@@ -58,7 +58,7 @@ export const MODELS = {
|
|
|
58
58
|
name: 'Nemotron 3 Ultra',
|
|
59
59
|
context: 1_000_000,
|
|
60
60
|
star: true,
|
|
61
|
-
note: 'deepest reasoning, 1M context —
|
|
61
|
+
note: 'deepest reasoning, 1M context — slowest to answer',
|
|
62
62
|
},
|
|
63
63
|
'nvidia/nemotron-3.5-lightning:free': {
|
|
64
64
|
name: 'Nemotron 3.5 Lightning',
|
|
@@ -79,18 +79,43 @@ export const MODELS = {
|
|
|
79
79
|
name: 'North Mini Code',
|
|
80
80
|
context: 256_000,
|
|
81
81
|
star: true,
|
|
82
|
-
note: '
|
|
82
|
+
note: 'the default — built for code and interface work, quick to answer',
|
|
83
83
|
},
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* to Lightning or North Mini Code when the wait stops being worth it.
|
|
87
|
+
* North Mini Code is the default: it is built for code and interface work,
|
|
88
|
+
* which is what ucode is mostly asked to do, and it answers far sooner than
|
|
89
|
+
* the big reasoning models. /model moves to Ultra when a problem needs the
|
|
90
|
+
* million-token window and the long think more than it needs the speed.
|
|
92
91
|
*/
|
|
93
|
-
export const DEFAULT_MODEL = '
|
|
92
|
+
export const DEFAULT_MODEL = 'cohere/north-mini-code:free';
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Where to go when a model is busy, in order of preference. Each is served by
|
|
96
|
+
* a different upstream, so a rate limit on one rarely means a limit on the
|
|
97
|
+
* next — which is what lets a long build keep going instead of stopping at
|
|
98
|
+
* the first "too many requests".
|
|
99
|
+
*/
|
|
100
|
+
export const FALLBACKS = [
|
|
101
|
+
'cohere/north-mini-code:free',
|
|
102
|
+
'nvidia/nemotron-3.5-lightning:free',
|
|
103
|
+
'nvidia/nemotron-3-super-120b-a12b:free',
|
|
104
|
+
'nvidia/nemotron-3-ultra-550b-a55b:free',
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
/** The next model to try after `id`, skipping any already tried this round. */
|
|
108
|
+
export function fallbackFor(id, tried = new Set()) {
|
|
109
|
+
const start = Math.max(0, FALLBACKS.indexOf(id));
|
|
110
|
+
for (let i = 1; i <= FALLBACKS.length; i++) {
|
|
111
|
+
const next = FALLBACKS[(start + i) % FALLBACKS.length];
|
|
112
|
+
if (next !== id && !tried.has(next)) return next;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Seconds to wait on successive rate limits that come with no retry-after. */
|
|
118
|
+
const RATE_LIMIT_BACKOFF = [5, 10, 20];
|
|
94
119
|
|
|
95
120
|
let current = process.env.UCODE_MODEL || DEFAULT_MODEL;
|
|
96
121
|
let client = null;
|
|
@@ -578,12 +603,15 @@ export async function ask(messages, tools = [], opts = {}) {
|
|
|
578
603
|
problem = explain(err, id);
|
|
579
604
|
|
|
580
605
|
// A per-minute limit is a wait, not a failure. Sit it out rather than
|
|
581
|
-
// making the user retype their message.
|
|
582
|
-
|
|
606
|
+
// making the user retype their message. Free endpoints often refuse
|
|
607
|
+
// without saying how long to wait, so when there is no retry-after the
|
|
608
|
+
// pauses grow on their own — 5s, 10s, 20s — and only then does the
|
|
609
|
+
// error go up to the loop, which moves to another model.
|
|
610
|
+
const told = problem.detail?.retryAfter;
|
|
611
|
+
const wait = Number.isFinite(told) && told > 0 && told <= 90 ? told : RATE_LIMIT_BACKOFF[attempt - 1];
|
|
583
612
|
if (
|
|
584
613
|
problem.kind === 'rate_limit' && !problem.detail?.daily &&
|
|
585
|
-
|
|
586
|
-
attempt < attempts && !opts.signal?.aborted
|
|
614
|
+
wait && attempt < attempts && printed === 0 && !opts.signal?.aborted
|
|
587
615
|
) {
|
|
588
616
|
const until = Date.now() + wait * 1000;
|
|
589
617
|
while (Date.now() < until && !opts.signal?.aborted) {
|