u-foo 3.0.0 → 3.0.2

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.
Files changed (47) hide show
  1. package/package.json +1 -1
  2. package/src/agents/prompts/native/tasks.js +4 -1
  3. package/src/app/chat/commandExecutor.js +111 -1
  4. package/src/app/chat/commands.js +2 -1
  5. package/src/app/chat/daemonMessageRouter.js +1 -1
  6. package/src/app/chat/inputSubmitHandler.js +3 -2
  7. package/src/code/agent.js +17 -3
  8. package/src/code/commands.js +3 -3
  9. package/src/code/context/executionSegment.js +5 -0
  10. package/src/code/context/planMode.js +8 -1
  11. package/src/code/context/promptLayers.js +10 -9
  12. package/src/code/dispatch.js +4 -0
  13. package/src/code/index.js +2 -0
  14. package/src/code/modelCommand.js +199 -23
  15. package/src/code/nativeRunner.js +299 -225
  16. package/src/code/protocol/controlPlane.js +93 -0
  17. package/src/code/protocol/faultHarness.js +90 -0
  18. package/src/code/protocol/index.js +20 -0
  19. package/src/code/protocol/loopEvents.js +102 -0
  20. package/src/code/protocol/materialize.js +107 -0
  21. package/src/code/protocol/messageFixtures.js +116 -0
  22. package/src/code/protocol/ownership.js +147 -0
  23. package/src/code/protocol/protocolValidator.js +165 -0
  24. package/src/code/protocol/suspension.js +173 -0
  25. package/src/code/protocol/toolCallLedger.js +222 -0
  26. package/src/code/protocol/transitions.js +97 -0
  27. package/src/code/providers/anthropicMessagesTransport.js +93 -0
  28. package/src/code/providers/index.js +8 -0
  29. package/src/code/providers/modelsCatalog.js +304 -0
  30. package/src/code/providers/openaiChatTransport.js +98 -0
  31. package/src/code/providers/transportContract.js +46 -0
  32. package/src/code/repl.js +45 -29
  33. package/src/code/runtime/taskControl.js +177 -53
  34. package/src/code/runtime/taskFocus.js +30 -10
  35. package/src/code/runtime/taskLoop.js +25 -3
  36. package/src/code/runtime/taskRun.js +172 -2
  37. package/src/code/runtime/workspaceLease.js +41 -0
  38. package/src/code/sessionStore.js +1 -0
  39. package/src/code/taskRoute.js +73 -0
  40. package/src/code/thinkingLevels.js +132 -0
  41. package/src/code/tools/taskRun.js +118 -0
  42. package/src/config.js +10 -1
  43. package/src/ui/format/index.js +48 -3
  44. package/src/ui/ink/ChatApp.js +137 -25
  45. package/src/ui/ink/UcodeApp.js +38 -30
  46. package/src/ui/ink/chatLogModel.js +238 -32
  47. package/src/ui/ink/chatReducer.js +18 -6
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * plan_graph control-plane actions:
5
- * start_task / cancel_task / fail_task / complete_task / skip_node / cancel_subtree.
4
+ * Control-plane TaskRun lifecycle:
5
+ * - startTask: graph-bound TaskRun from a plan_graph task_loop node
6
+ * - startStandaloneTask: single-point TaskRun (no plan graph / Plan Mode required)
7
+ * - cancel/fail/complete by nodeId or taskRunId
6
8
  *
7
9
  * complete_task:
8
10
  * - taskRunId → owning TaskLoop submitting TaskRun result
@@ -59,6 +61,44 @@ function dependenciesSatisfied(parent = null, node = null) {
59
61
  return { ok: unmet.length === 0, dependencies: unmet };
60
62
  }
61
63
 
64
+ function rejectMaxConcurrent(executionState = null) {
65
+ const activeCount = listActiveWritingTaskRuns(executionState).length;
66
+ const leaseCount = countWriteLeases(executionState);
67
+ if (activeCount >= MAX_CONCURRENT_WRITE_LEASES || leaseCount >= MAX_CONCURRENT_WRITE_LEASES) {
68
+ return {
69
+ status: "rejected",
70
+ ok: false,
71
+ errors: [{
72
+ code: "MAX_CONCURRENT_TASKS",
73
+ message: `At most ${MAX_CONCURRENT_WRITE_LEASES} concurrent writing TaskRuns`,
74
+ max: MAX_CONCURRENT_WRITE_LEASES,
75
+ current: Math.max(activeCount, leaseCount),
76
+ }],
77
+ };
78
+ }
79
+ return null;
80
+ }
81
+
82
+ function resolveActiveRun(executionState = null, {
83
+ nodeId = "",
84
+ taskRunId = "",
85
+ } = {}) {
86
+ const runId = String(taskRunId || "").trim();
87
+ if (runId) {
88
+ const run = getTaskRun(executionState, runId);
89
+ if (!run) return { run: null, errorCode: "TASK_RUN_NOT_FOUND" };
90
+ if (run.status === "queued" || run.status === "running" || run.status === "cancelling") {
91
+ return { run, errorCode: "" };
92
+ }
93
+ return { run, errorCode: "TASK_ALREADY_TERMINAL" };
94
+ }
95
+ const id = String(nodeId || "").trim();
96
+ if (!id) return { run: null, errorCode: "TASK_NOT_RUNNING" };
97
+ const active = findActiveTaskRunForNode(executionState, id);
98
+ if (active) return { run: active, errorCode: "" };
99
+ return { run: null, errorCode: "TASK_NOT_RUNNING" };
100
+ }
101
+
62
102
  function startTask(executionState = null, {
63
103
  nodeId = "",
64
104
  commandId = "",
@@ -125,20 +165,8 @@ function startTask(executionState = null, {
125
165
  };
126
166
  }
127
167
 
128
- const activeCount = listActiveWritingTaskRuns(executionState).length;
129
- const leaseCount = countWriteLeases(executionState);
130
- if (activeCount >= MAX_CONCURRENT_WRITE_LEASES || leaseCount >= MAX_CONCURRENT_WRITE_LEASES) {
131
- return {
132
- status: "rejected",
133
- ok: false,
134
- errors: [{
135
- code: "MAX_CONCURRENT_TASKS",
136
- message: `At most ${MAX_CONCURRENT_WRITE_LEASES} concurrent writing TaskRuns`,
137
- max: MAX_CONCURRENT_WRITE_LEASES,
138
- current: Math.max(activeCount, leaseCount),
139
- }],
140
- };
141
- }
168
+ const limited = rejectMaxConcurrent(executionState);
169
+ if (limited) return limited;
142
170
 
143
171
  // Freeze spec snapshot on node.runtime
144
172
  if (!node.runtime || typeof node.runtime !== "object") node.runtime = {};
@@ -151,16 +179,20 @@ function startTask(executionState = null, {
151
179
  : { kind: "task_loop" },
152
180
  };
153
181
 
182
+ const objective = node.objective || node.title || id;
154
183
  const run = createTaskRun({
184
+ kind: "graph_node",
155
185
  parentGraphId: parent.graphId || "",
156
186
  parentNodeId: id,
157
187
  attempt: (Number(node.attempt) || 0) + 1,
188
+ objective,
189
+ title: node.title || objective,
158
190
  });
159
191
  const child = createChildGraphState({
160
192
  parentGraphId: parent.graphId || "",
161
193
  parentNodeId: id,
162
194
  taskRunId: run.id,
163
- objective: node.objective || node.title || id,
195
+ objective,
164
196
  });
165
197
  run.childGraphId = child.graphId;
166
198
  putTaskRun(executionState, run);
@@ -192,37 +224,116 @@ function startTask(executionState = null, {
192
224
  return payload;
193
225
  }
194
226
 
227
+ /**
228
+ * Start a TaskRun that is not attached to any plan_graph node.
229
+ * Orthogonal to Plan Mode: never enters or requires Plan Mode.
230
+ */
231
+ function startStandaloneTask(executionState = null, {
232
+ objective = "",
233
+ title = "",
234
+ commandId = "",
235
+ runTool = null,
236
+ knownTools = null,
237
+ processImmediately = true,
238
+ } = {}) {
239
+ const cached = getCachedControlCommand(executionState, commandId);
240
+ if (cached) return { ...cached, idempotentReplay: true };
241
+
242
+ ensureGraphs(executionState);
243
+
244
+ const goal = String(objective || title || "").trim();
245
+ if (!goal) {
246
+ return {
247
+ status: "rejected",
248
+ ok: false,
249
+ errors: [{ code: "OBJECTIVE_REQUIRED", message: "standalone task requires objective" }],
250
+ };
251
+ }
252
+
253
+ const limited = rejectMaxConcurrent(executionState);
254
+ if (limited) return limited;
255
+
256
+ const run = createTaskRun({
257
+ kind: "standalone",
258
+ parentGraphId: "",
259
+ parentNodeId: "",
260
+ attempt: 1,
261
+ objective: goal,
262
+ title: String(title || goal).trim(),
263
+ });
264
+ const child = createChildGraphState({
265
+ parentGraphId: "",
266
+ parentNodeId: "",
267
+ taskRunId: run.id,
268
+ objective: goal,
269
+ });
270
+ run.childGraphId = child.graphId;
271
+ putTaskRun(executionState, run);
272
+ setGraph(executionState, child);
273
+
274
+ const payload = {
275
+ status: "started",
276
+ ok: true,
277
+ kind: "standalone",
278
+ graphId: "",
279
+ nodeId: "",
280
+ taskRunId: run.id,
281
+ childGraphId: child.graphId,
282
+ objective: goal,
283
+ title: run.title,
284
+ parentNodeStatus: "",
285
+ };
286
+ cacheControlCommand(executionState, commandId, payload);
287
+
288
+ enqueueTaskEvent(executionState, run.id, { kind: "advance" });
289
+
290
+ if (processImmediately) {
291
+ processTaskRun(executionState, run.id, { runTool, knownTools });
292
+ }
293
+
294
+ return payload;
295
+ }
296
+
195
297
  function cancelTask(executionState = null, {
196
298
  nodeId = "",
299
+ taskRunId = "",
197
300
  reason = "",
198
301
  commandId = "",
199
302
  } = {}) {
200
303
  const cached = getCachedControlCommand(executionState, commandId);
201
304
  if (cached) return { ...cached, idempotentReplay: true };
202
305
 
203
- const id = String(nodeId || "").trim();
204
- const active = findActiveTaskRunForNode(executionState, id);
205
- if (!active) {
206
- const { node } = findParentNode(executionState, id);
207
- if (node && (node.status === "succeeded" || node.status === "failed" || node.status === "cancelled")) {
208
- return {
209
- status: "rejected",
210
- ok: false,
211
- errors: [{
212
- code: "TASK_ALREADY_TERMINAL",
213
- message: `task ${id} already ${node.status}`,
214
- currentStatus: node.status,
215
- }],
216
- };
306
+ const resolved = resolveActiveRun(executionState, { nodeId, taskRunId });
307
+ const active = resolved.run;
308
+ if (!active || resolved.errorCode === "TASK_RUN_NOT_FOUND") {
309
+ const id = String(nodeId || "").trim();
310
+ if (id) {
311
+ const { node } = findParentNode(executionState, id);
312
+ if (node && (node.status === "succeeded" || node.status === "failed" || node.status === "cancelled")) {
313
+ return {
314
+ status: "rejected",
315
+ ok: false,
316
+ errors: [{
317
+ code: "TASK_ALREADY_TERMINAL",
318
+ message: `task ${id} already ${node.status}`,
319
+ currentStatus: node.status,
320
+ }],
321
+ };
322
+ }
217
323
  }
218
324
  return {
219
325
  status: "rejected",
220
326
  ok: false,
221
- errors: [{ code: "TASK_NOT_RUNNING", message: `no active run for ${id}` }],
327
+ errors: [{
328
+ code: resolved.errorCode || "TASK_NOT_RUNNING",
329
+ message: taskRunId
330
+ ? `no active run for taskRunId ${taskRunId}`
331
+ : `no active run for ${nodeId || "(missing id)"}`,
332
+ }],
222
333
  };
223
334
  }
224
335
 
225
- if (isTerminalTaskRun(active)) {
336
+ if (isTerminalTaskRun(active) || resolved.errorCode === "TASK_ALREADY_TERMINAL") {
226
337
  return {
227
338
  status: "rejected",
228
339
  ok: false,
@@ -252,7 +363,7 @@ function cancelTask(executionState = null, {
252
363
  const payload = {
253
364
  status: "accepted",
254
365
  ok: Boolean(done.ok),
255
- nodeId: id,
366
+ nodeId: active.parentNodeId || "",
256
367
  taskRunId: active.id,
257
368
  parentNodeStatus: done.run ? done.run.status : "cancelled",
258
369
  };
@@ -262,34 +373,43 @@ function cancelTask(executionState = null, {
262
373
 
263
374
  function failTask(executionState = null, {
264
375
  nodeId = "",
376
+ taskRunId = "",
265
377
  reason = "",
266
378
  commandId = "",
267
379
  } = {}) {
268
380
  const cached = getCachedControlCommand(executionState, commandId);
269
381
  if (cached) return { ...cached, idempotentReplay: true };
270
382
 
271
- const id = String(nodeId || "").trim();
272
- const active = findActiveTaskRunForNode(executionState, id);
273
- if (!active) {
274
- const { node } = findParentNode(executionState, id);
275
- if (node && (node.status === "succeeded" || node.status === "failed" || node.status === "cancelled")) {
276
- return {
277
- status: "rejected",
278
- ok: false,
279
- errors: [{
280
- code: "TASK_ALREADY_TERMINAL",
281
- message: `task ${id} already ${node.status}`,
282
- currentStatus: node.status,
283
- }],
284
- };
383
+ const resolved = resolveActiveRun(executionState, { nodeId, taskRunId });
384
+ const active = resolved.run;
385
+ if (!active || resolved.errorCode === "TASK_RUN_NOT_FOUND") {
386
+ const id = String(nodeId || "").trim();
387
+ if (id) {
388
+ const { node } = findParentNode(executionState, id);
389
+ if (node && (node.status === "succeeded" || node.status === "failed" || node.status === "cancelled")) {
390
+ return {
391
+ status: "rejected",
392
+ ok: false,
393
+ errors: [{
394
+ code: "TASK_ALREADY_TERMINAL",
395
+ message: `task ${id} already ${node.status}`,
396
+ currentStatus: node.status,
397
+ }],
398
+ };
399
+ }
285
400
  }
286
401
  return {
287
402
  status: "rejected",
288
403
  ok: false,
289
- errors: [{ code: "TASK_NOT_RUNNING", message: `no active run for ${id}` }],
404
+ errors: [{
405
+ code: resolved.errorCode || "TASK_NOT_RUNNING",
406
+ message: taskRunId
407
+ ? `no active run for taskRunId ${taskRunId}`
408
+ : `no active run for ${nodeId || "(missing id)"}`,
409
+ }],
290
410
  };
291
411
  }
292
- if (isTerminalTaskRun(active)) {
412
+ if (isTerminalTaskRun(active) || resolved.errorCode === "TASK_ALREADY_TERMINAL") {
293
413
  return {
294
414
  status: "rejected",
295
415
  ok: false,
@@ -313,7 +433,7 @@ function failTask(executionState = null, {
313
433
  const payload = {
314
434
  status: done.ok ? "accepted" : "rejected",
315
435
  ok: Boolean(done.ok),
316
- nodeId: id,
436
+ nodeId: active.parentNodeId || "",
317
437
  taskRunId: active.id,
318
438
  parentNodeStatus: done.run ? done.run.status : "failed",
319
439
  errors: done.ok ? undefined : [{ code: done.code || "CAS_FAILED", currentStatus: done.currentStatus }],
@@ -485,14 +605,16 @@ function runControlActions(executionState = null, {
485
605
  } else if (op === "cancel_task") {
486
606
  results.push(cancelTask(executionState, {
487
607
  nodeId: action.nodeId,
608
+ taskRunId: action.taskRunId,
488
609
  reason: action.reason,
489
- commandId: commandId && list.length === 1 ? commandId : `${commandId}:${op}:${action.nodeId}`,
610
+ commandId: commandId && list.length === 1 ? commandId : `${commandId}:${op}:${action.nodeId || action.taskRunId}`,
490
611
  }));
491
612
  } else if (op === "fail_task" || op === "mark_task_failed") {
492
613
  results.push(failTask(executionState, {
493
614
  nodeId: action.nodeId,
615
+ taskRunId: action.taskRunId,
494
616
  reason: action.reason,
495
- commandId: commandId && list.length === 1 ? commandId : `${commandId}:${op}:${action.nodeId}`,
617
+ commandId: commandId && list.length === 1 ? commandId : `${commandId}:${op}:${action.nodeId || action.taskRunId}`,
496
618
  }));
497
619
  } else if (op === "complete_task") {
498
620
  const taskRunId = String(action.taskRunId || "").trim();
@@ -528,6 +650,7 @@ function runControlActions(executionState = null, {
528
650
  } else if (op === "fail_current_task") {
529
651
  results.push(failTask(executionState, {
530
652
  nodeId: action.nodeId || (getTaskRun(executionState, action.taskRunId) || {}).parentNodeId,
653
+ taskRunId: action.taskRunId,
531
654
  reason: action.reason,
532
655
  commandId: commandId && list.length === 1 ? commandId : `${commandId}:${op}`,
533
656
  }));
@@ -554,6 +677,7 @@ function runControlActions(executionState = null, {
554
677
 
555
678
  module.exports = {
556
679
  startTask,
680
+ startStandaloneTask,
557
681
  cancelTask,
558
682
  failTask,
559
683
  completeTaskFromLoop,
@@ -82,6 +82,7 @@ function buildTaskFocus({
82
82
  currentNodeId = "",
83
83
  taskRunsById = {},
84
84
  recentlyChangedFiles = [],
85
+ standaloneTask = null,
85
86
  } = {}) {
86
87
  const byId = new Map((Array.isArray(nodes) ? nodes : []).map((n) => [n.id, n]));
87
88
  const current = byId.get(currentNodeId);
@@ -96,19 +97,38 @@ function buildTaskFocus({
96
97
  });
97
98
  const writers = Object.values(runs)
98
99
  .filter((r) => r && (r.status === "running" || r.status === "cancelling"))
99
- .map((r) => r.parentNodeId)
100
+ .map((r) => r.parentNodeId || r.id)
100
101
  .filter(Boolean);
101
102
 
103
+ let currentTask;
104
+ if (standaloneTask && typeof standaloneTask === "object") {
105
+ currentTask = {
106
+ id: standaloneTask.id || currentNodeId || "standalone",
107
+ objective: standaloneTask.objective || standaloneTask.title || "",
108
+ title: standaloneTask.title || standaloneTask.objective || standaloneTask.id || "standalone",
109
+ status: standaloneTask.status || (currentRun && currentRun.status) || "running",
110
+ };
111
+ } else if (current) {
112
+ currentTask = {
113
+ id: current.id,
114
+ objective: current.objective || current.title || "",
115
+ title: current.title || current.objective || current.id,
116
+ status: (currentRun && currentRun.status) || current.status,
117
+ };
118
+ } else {
119
+ currentTask = {
120
+ id: currentNodeId || "standalone",
121
+ objective: "",
122
+ title: currentNodeId || "standalone",
123
+ status: "unknown",
124
+ };
125
+ }
126
+
102
127
  return {
103
- currentTask: current
104
- ? {
105
- id: current.id,
106
- objective: current.objective || current.title || "",
107
- title: current.title || current.objective || current.id,
108
- status: (currentRun && currentRun.status) || current.status,
109
- }
110
- : { id: currentNodeId, objective: "", title: currentNodeId, status: "unknown" },
111
- dependencies: listDependencySummaries(nodes, currentNodeId, runs),
128
+ currentTask,
129
+ dependencies: currentNodeId
130
+ ? listDependencySummaries(nodes, currentNodeId, runs)
131
+ : [],
112
132
  parallelSiblings: siblings,
113
133
  workspace: {
114
134
  concurrentWriters: writers,
@@ -242,12 +242,23 @@ function processTaskRun(executionState = null, taskRunId = "", options = {}) {
242
242
  }
243
243
  }
244
244
 
245
- const parent = getGraph(executionState, live.parentGraphId) || executionState.planGraph;
245
+ const parent = live.parentGraphId
246
+ ? (getGraph(executionState, live.parentGraphId) || executionState.planGraph)
247
+ : null;
248
+ const standalone = !live.parentNodeId;
246
249
  const focus = buildTaskFocus({
247
250
  nodes: parent && parent.nodes ? parent.nodes : [],
248
251
  currentNodeId: live.parentNodeId,
249
252
  taskRunsById: (executionState.taskRuns && executionState.taskRuns.byId) || {},
250
253
  recentlyChangedFiles: executionState.modifiedFiles || [],
254
+ standaloneTask: standalone
255
+ ? {
256
+ id: live.id,
257
+ objective: live.objective || live.title || "",
258
+ title: live.title || live.objective || live.id,
259
+ status: live.status,
260
+ }
261
+ : null,
251
262
  });
252
263
  live.lastFocusText = renderTaskFocusText(focus);
253
264
  putTaskRun(executionState, live);
@@ -357,17 +368,28 @@ function processTaskRun(executionState = null, taskRunId = "", options = {}) {
357
368
 
358
369
  /**
359
370
  * Resume queued/running TaskRuns after process restart (executionState restored).
371
+ * Requeues interrupted mid-tool/model runs, releases stale leases, then advances.
360
372
  */
361
373
  function resumePersistedTaskRuns(executionState = null, options = {}) {
362
374
  const state = ensureGraphs(executionState);
375
+ const {
376
+ recoverTaskRunsAfterRestart,
377
+ } = require("./taskRun");
378
+ const { releaseStaleWriteLeases } = require("./workspaceLease");
379
+ const recovery = recoverTaskRunsAfterRestart(state);
380
+ const leases = releaseStaleWriteLeases(state, {
381
+ maxAgeMs: options.leaseStaleMs,
382
+ });
363
383
  const byId = state.taskRuns && state.taskRuns.byId ? state.taskRuns.byId : {};
364
384
  const results = [];
365
385
  for (const run of Object.values(byId)) {
366
386
  if (!run) continue;
367
- if (run.status !== "queued" && run.status !== "running") continue;
387
+ if (run.status !== "queued" && run.status !== "running" && run.status !== "cancelling") {
388
+ continue;
389
+ }
368
390
  results.push(processTaskRun(state, run.id, options));
369
391
  }
370
- return results;
392
+ return { recovery, leases, results };
371
393
  }
372
394
 
373
395
  module.exports = {