ucode-agent 1.50.0 → 1.52.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/package.json +1 -1
- package/src/core/loop.js +25 -0
- package/src/core/provider.js +15 -2
- package/src/tools/index.js +13 -1
- package/src/tools/scaffold.js +12 -0
package/package.json
CHANGED
package/src/core/loop.js
CHANGED
|
@@ -1911,6 +1911,31 @@ export class Agent {
|
|
|
1911
1911
|
this.reads.set(key, seen + 1);
|
|
1912
1912
|
}
|
|
1913
1913
|
|
|
1914
|
+
// create_app again for the app this turn already made. DeepSeek scaffolded
|
|
1915
|
+
// tasker with no files, then called create_app twice more — once with no
|
|
1916
|
+
// folder or name at all — and was refused both times while the empty
|
|
1917
|
+
// starter sat there. The folder and name are known, so fill them in; with
|
|
1918
|
+
// no files there is nothing to create, and the next step is batch_write.
|
|
1919
|
+
if (call.name === 'create_app' && this.apps?.length === 1) {
|
|
1920
|
+
const made = this.apps[0];
|
|
1921
|
+
const args = (call.args ??= {});
|
|
1922
|
+
if (!args.folder) args.folder = path.relative(this.cwd, made) || '.';
|
|
1923
|
+
if (!args.name) args.name = path.basename(made);
|
|
1924
|
+
const files = args.files;
|
|
1925
|
+
const none = files == null || files === ''
|
|
1926
|
+
|| (Array.isArray(files) ? !files.length : typeof files === 'object' && !Object.keys(files).length);
|
|
1927
|
+
if (none && path.resolve(this.cwd, String(args.folder)) === made) {
|
|
1928
|
+
const show = path.relative(this.cwd, made) || '.';
|
|
1929
|
+
throw new ToolFailure({
|
|
1930
|
+
kind: 'already_made',
|
|
1931
|
+
attempted: `creating ${show}`,
|
|
1932
|
+
failed: `${show} was already created this turn, with the starter files in it.`,
|
|
1933
|
+
fix: `Do not call create_app again. Write the app into ${show} with one batch_write, `
|
|
1934
|
+
+ 'replacing the starter files with the finished ones.',
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1914
1939
|
// Starting a second app instead of fixing the first.
|
|
1915
1940
|
//
|
|
1916
1941
|
// A traced build hit a problem in todo/, abandoned it and made todo-fixed/
|
package/src/core/provider.js
CHANGED
|
@@ -94,6 +94,10 @@ export const MODELS = {
|
|
|
94
94
|
'deepseek/deepseek-v4-flash-0731:free': {
|
|
95
95
|
name: 'DeepSeek V4 Flash',
|
|
96
96
|
context: 1_048_576,
|
|
97
|
+
// Left to itself it thinks for minutes before every step — over two
|
|
98
|
+
// minutes of nothing on screen before its first word. Low effort answers
|
|
99
|
+
// in seconds and still plans the build.
|
|
100
|
+
reasoning: { effort: 'low' },
|
|
97
101
|
note: 'fast DeepSeek coder, 1M context — on trial',
|
|
98
102
|
},
|
|
99
103
|
'qwen/qwen3.8-27b:free': {
|
|
@@ -162,6 +166,9 @@ export const stallLimit = () => Number(process.env.UCODE_STALL_MS) || 60_000;
|
|
|
162
166
|
|
|
163
167
|
/** Freezes in a row before ucode stops asking and says to switch models. */
|
|
164
168
|
export const MAX_STALLS = 3;
|
|
169
|
+
|
|
170
|
+
/** How long a reply may go quiet once it has started writing a tool call. */
|
|
171
|
+
const writingLimit = () => Number(process.env.UCODE_STALL_MS) || 300_000;
|
|
165
172
|
let stalls = 0;
|
|
166
173
|
|
|
167
174
|
let current = process.env.UCODE_MODEL || DEFAULT_MODEL;
|
|
@@ -632,7 +639,8 @@ export async function ask(messages, tools = [], opts = {}) {
|
|
|
632
639
|
}
|
|
633
640
|
if (opts.temperature !== undefined) request.temperature = opts.temperature;
|
|
634
641
|
if (opts.maxOutputTokens) request.max_tokens = opts.maxOutputTokens;
|
|
635
|
-
|
|
642
|
+
const reasoning = opts.reasoning ?? MODELS[id]?.reasoning;
|
|
643
|
+
if (reasoning) request.reasoning = reasoning;
|
|
636
644
|
|
|
637
645
|
// A side call (the design review) passes fewer: it is better skipped than
|
|
638
646
|
// waited on through a string of rate-limit pauses.
|
|
@@ -728,9 +736,14 @@ async function streamed(request, opts, id) {
|
|
|
728
736
|
opts.signal?.addEventListener('abort', stop, { once: true });
|
|
729
737
|
let stalled = false;
|
|
730
738
|
let timer;
|
|
739
|
+
// Once a tool call has begun, the silence may be the call being written.
|
|
740
|
+
// DeepSeek's free upstream holds a create_app back until the whole app is
|
|
741
|
+
// done — two or three quiet minutes — and a one-minute watchdog killed every
|
|
742
|
+
// build it started. So a call in progress gets five minutes instead.
|
|
731
743
|
const alive = () => {
|
|
732
744
|
clearTimeout(timer);
|
|
733
|
-
|
|
745
|
+
const limit = partial.size ? writingLimit() : stallLimit();
|
|
746
|
+
timer = setTimeout(() => { stalled = true; quiet.abort(); }, limit);
|
|
734
747
|
};
|
|
735
748
|
|
|
736
749
|
let text = '';
|
package/src/tools/index.js
CHANGED
|
@@ -581,6 +581,18 @@ function check(name, args) {
|
|
|
581
581
|
return ['the arguments must be a JSON object'];
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
+
// One file named as "path" where the tool takes a list called "paths".
|
|
585
|
+
// DeepSeek did it to four read_files at once, a whole round trip spent being
|
|
586
|
+
// told a plural it could simply have been given.
|
|
587
|
+
for (const key of schema.required ?? []) {
|
|
588
|
+
const one = key.endsWith('s') ? key.slice(0, -1) : '';
|
|
589
|
+
if (args[key] == null && one && !schema.properties[one] && args[one] != null
|
|
590
|
+
&& schema.properties[key]?.type === 'array') {
|
|
591
|
+
args[key] = Array.isArray(args[one]) ? args[one] : [args[one]];
|
|
592
|
+
delete args[one];
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
584
596
|
for (const key of schema.required ?? []) {
|
|
585
597
|
if (args[key] === undefined || args[key] === null) problems.push(`"${key}" is required and missing`);
|
|
586
598
|
}
|
|
@@ -709,7 +721,7 @@ export function describe(name, args = {}) {
|
|
|
709
721
|
// untrue on screen while the opposite happens on disk.
|
|
710
722
|
return `Creating ${clip(args.name || args.folder, 30)} from the ` +
|
|
711
723
|
`${args.template === 'next-shadcn' ? 'Next.js' : 'HTML'} starter` +
|
|
712
|
-
`${files ? ` with ${files} file${files === 1 ? '' : 's'}` : ''}`;
|
|
724
|
+
`${files ? ` with ${files} file${files === 1 ? '' : 's'}` : ''}`;
|
|
713
725
|
}
|
|
714
726
|
case 'look_at_app':
|
|
715
727
|
return `Looking at ${clip(args.url, 40)} on a phone and a desktop`;
|
package/src/tools/scaffold.js
CHANGED
|
@@ -431,6 +431,18 @@ export async function createApp({ folder, name, description, template = 'plain-h
|
|
|
431
431
|
// The app's own files, written in this same call. Two round trips become
|
|
432
432
|
// one, and round trips are nearly all of the time a build takes.
|
|
433
433
|
// Next.js only: paths that have missed src/ are corrected before they land.
|
|
434
|
+
// Paths are relative to the project root, but a model will name a file
|
|
435
|
+
// "index.html" meaning the app's own. DeepSeek did: its tasker went to the
|
|
436
|
+
// project root and the starter was left in tasker/ as the app. A path that
|
|
437
|
+
// is not already under the folder is put under it.
|
|
438
|
+
const home = resolveIn(folder, 'create_app', 'folder').abs;
|
|
439
|
+
for (const f of given) {
|
|
440
|
+
const abs = resolveIn(f.path, 'create_app', 'files').abs;
|
|
441
|
+
const rel = path.relative(home, abs);
|
|
442
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
443
|
+
f.path = path.join(String(folder), String(f.path).replace(/^[./\\]+/, ''));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
434
446
|
const placed = template === 'next-shadcn' ? placeForNext(given, path.basename(target.abs)) : { files: given, moved: [] };
|
|
435
447
|
const mine = placed.files;
|
|
436
448
|
const wrote = mine.length ? await batchWrite({ files: mine }) : null;
|