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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ucode-agent",
3
- "version": "1.50.0",
3
+ "version": "1.52.0",
4
4
  "description": "ucode - a terminal coding agent that reads, edits and runs your code, on NVIDIA, Cohere, Nex AGI, DeepSeek and Qwen models.",
5
5
  "type": "module",
6
6
  "main": "ucode.js",
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/
@@ -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
- if (opts.reasoning) request.reasoning = opts.reasoning;
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
- timer = setTimeout(() => { stalled = true; quiet.abort(); }, stallLimit());
745
+ const limit = partial.size ? writingLimit() : stallLimit();
746
+ timer = setTimeout(() => { stalled = true; quiet.abort(); }, limit);
734
747
  };
735
748
 
736
749
  let text = '';
@@ -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`;
@@ -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;