u-foo 3.0.1 → 3.0.3
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/agents/prompts/native/tasks.js +4 -1
- package/src/agents/prompts/native/toolDescriptions/readImage.js +23 -0
- package/src/app/chat/commandExecutor.js +111 -1
- package/src/app/chat/commands.js +2 -1
- package/src/app/chat/daemonMessageRouter.js +1 -1
- package/src/app/chat/inputSubmitHandler.js +3 -2
- package/src/code/commands.js +3 -3
- package/src/code/context/assembler.js +17 -1
- package/src/code/context/planMode.js +2 -2
- package/src/code/context/promptLayers.js +12 -10
- package/src/code/context/reducers.js +35 -0
- package/src/code/context/transcriptSync.js +25 -5
- package/src/code/dispatch.js +8 -0
- package/src/code/imageIngest.js +367 -0
- package/src/code/modelCommand.js +199 -23
- package/src/code/nativeRunner.js +184 -20
- package/src/code/protocol/protocolValidator.js +3 -3
- package/src/code/providers/anthropicMessagesTransport.js +28 -1
- package/src/code/providers/index.js +2 -0
- package/src/code/providers/modelsCatalog.js +304 -0
- package/src/code/providers/openaiChatTransport.js +19 -1
- package/src/code/providers/visionBlocks.js +110 -0
- package/src/code/repl.js +37 -8
- package/src/code/runtime/taskControl.js +177 -53
- package/src/code/runtime/taskFocus.js +30 -10
- package/src/code/runtime/taskLoop.js +12 -1
- package/src/code/runtime/taskRun.js +10 -1
- package/src/code/thinkingLevels.js +132 -0
- package/src/code/tools/readImage.js +110 -0
- package/src/code/tools/taskRun.js +118 -0
- package/src/config.js +10 -1
- package/src/ui/format/index.js +103 -5
- package/src/ui/ink/ChatApp.js +137 -25
- package/src/ui/ink/MultilineInput.js +38 -2
- package/src/ui/ink/UcodeApp.js +102 -14
- package/src/ui/ink/chatLogModel.js +238 -32
- package/src/ui/ink/chatReducer.js +18 -6
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const { randomUUID } = require("crypto");
|
|
4
4
|
const { assertTransport } = require("./transportContract");
|
|
5
|
+
const {
|
|
6
|
+
extractVisionPayload,
|
|
7
|
+
stripVisionBase64,
|
|
8
|
+
visionSummaryText,
|
|
9
|
+
toOpenAiImagePart,
|
|
10
|
+
} = require("./visionBlocks");
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
13
|
* OpenAI-compatible chat-completions transport adapter.
|
|
@@ -82,11 +88,23 @@ function createOpenAiChatTransport(deps = {}) {
|
|
|
82
88
|
}));
|
|
83
89
|
},
|
|
84
90
|
appendToolResult({ messages, call, toolResult }) {
|
|
91
|
+
const vision = extractVisionPayload(toolResult);
|
|
92
|
+
const payload = vision ? stripVisionBase64(toolResult) : toolResult;
|
|
85
93
|
messages.push({
|
|
86
94
|
role: "tool",
|
|
87
95
|
tool_call_id: call.source.id,
|
|
88
|
-
content: clipText(toJsonString(
|
|
96
|
+
content: clipText(toJsonString(payload), 12000),
|
|
89
97
|
});
|
|
98
|
+
if (vision) {
|
|
99
|
+
const imagePart = toOpenAiImagePart(vision);
|
|
100
|
+
messages.push({
|
|
101
|
+
role: "user",
|
|
102
|
+
content: [
|
|
103
|
+
{ type: "text", text: visionSummaryText(vision, toolResult) },
|
|
104
|
+
imagePart,
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
90
108
|
},
|
|
91
109
|
};
|
|
92
110
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helpers for expanding read_image tool results into provider vision blocks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function extractVisionPayload(toolResult = null) {
|
|
8
|
+
if (!toolResult || typeof toolResult !== "object") return null;
|
|
9
|
+
const base64 = String(toolResult.base64 || "").trim();
|
|
10
|
+
const mediaType = String(toolResult.mediaType || "").trim().toLowerCase();
|
|
11
|
+
if (!base64 || !mediaType.startsWith("image/")) return null;
|
|
12
|
+
if (toolResult.ok === false) return null;
|
|
13
|
+
return {
|
|
14
|
+
path: String(toolResult.path || "").trim(),
|
|
15
|
+
mediaType,
|
|
16
|
+
bytes: Number.isFinite(toolResult.bytes) ? toolResult.bytes : null,
|
|
17
|
+
base64,
|
|
18
|
+
artifactId: String(toolResult.artifactId || "").trim(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isVisionToolResult(toolResult = null) {
|
|
23
|
+
return Boolean(extractVisionPayload(toolResult));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stripVisionBase64(value) {
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return value.map((item) => stripVisionBase64(item));
|
|
29
|
+
}
|
|
30
|
+
if (!value || typeof value !== "object") return value;
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
33
|
+
if (key === "base64") continue;
|
|
34
|
+
out[key] = stripVisionBase64(entry);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function visionSummaryText(vision = null, toolResult = null) {
|
|
40
|
+
const pathText = (vision && vision.path) || (toolResult && toolResult.path) || "image";
|
|
41
|
+
const mediaType = (vision && vision.mediaType) || (toolResult && toolResult.mediaType) || "image/*";
|
|
42
|
+
const bytes = (vision && vision.bytes) != null
|
|
43
|
+
? vision.bytes
|
|
44
|
+
: (toolResult && toolResult.bytes);
|
|
45
|
+
const parts = [
|
|
46
|
+
`Image loaded for vision: ${pathText}`,
|
|
47
|
+
`mediaType=${mediaType}`,
|
|
48
|
+
];
|
|
49
|
+
if (Number.isFinite(bytes)) parts.push(`bytes=${bytes}`);
|
|
50
|
+
if (vision && vision.artifactId) parts.push(`artifactId=${vision.artifactId}`);
|
|
51
|
+
parts.push("Visual content is attached for this model call only; call read_image again later if needed.");
|
|
52
|
+
return parts.join(" | ");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toAnthropicImageBlock(vision = null) {
|
|
56
|
+
if (!vision) return null;
|
|
57
|
+
return {
|
|
58
|
+
type: "image",
|
|
59
|
+
source: {
|
|
60
|
+
type: "base64",
|
|
61
|
+
media_type: vision.mediaType,
|
|
62
|
+
data: vision.base64,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toOpenAiImagePart(vision = null) {
|
|
68
|
+
if (!vision) return null;
|
|
69
|
+
return {
|
|
70
|
+
type: "image_url",
|
|
71
|
+
image_url: {
|
|
72
|
+
url: `data:${vision.mediaType};base64,${vision.base64}`,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function degradeVisionContent(content) {
|
|
78
|
+
if (typeof content === "string") return content;
|
|
79
|
+
if (!Array.isArray(content)) return content;
|
|
80
|
+
const texts = [];
|
|
81
|
+
for (const block of content) {
|
|
82
|
+
if (!block || typeof block !== "object") continue;
|
|
83
|
+
const type = String(block.type || "").trim().toLowerCase();
|
|
84
|
+
if (type === "text" && block.text) {
|
|
85
|
+
texts.push(String(block.text));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (type === "image" || type === "image_url") {
|
|
89
|
+
const pathHint = block.path
|
|
90
|
+
|| (block.source && block.source.path)
|
|
91
|
+
|| "";
|
|
92
|
+
texts.push(pathHint ? `[image: ${pathHint}]` : "[image]");
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (type === "tool_result" && Array.isArray(block.content)) {
|
|
96
|
+
texts.push(degradeVisionContent(block.content));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return texts.filter(Boolean).join("\n") || "[multimodal content]";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
extractVisionPayload,
|
|
104
|
+
isVisionToolResult,
|
|
105
|
+
stripVisionBase64,
|
|
106
|
+
visionSummaryText,
|
|
107
|
+
toAnthropicImageBlock,
|
|
108
|
+
toOpenAiImagePart,
|
|
109
|
+
degradeVisionContent,
|
|
110
|
+
};
|
package/src/code/repl.js
CHANGED
|
@@ -113,21 +113,24 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
|
|
|
113
113
|
}
|
|
114
114
|
const modelMatch = text.match(/^(?:\/model|model)(?:\s+(.*))?$/i);
|
|
115
115
|
if (modelMatch) {
|
|
116
|
-
const
|
|
117
|
-
if (!
|
|
116
|
+
const rest = String(modelMatch[1] || "").trim();
|
|
117
|
+
if (!rest) {
|
|
118
118
|
return { kind: "model", action: "show" };
|
|
119
119
|
}
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
const parts = rest.split(/\s+/).filter(Boolean);
|
|
121
|
+
const modelId = parts[0] || "";
|
|
122
|
+
const thinking = parts[1] || "";
|
|
123
|
+
if (!modelId || parts.length > 2) {
|
|
122
124
|
return {
|
|
123
125
|
kind: "error",
|
|
124
|
-
output: "usage: /model [model-id]",
|
|
126
|
+
output: "usage: /model [model-id] [off|low|medium|high|max]",
|
|
125
127
|
};
|
|
126
128
|
}
|
|
127
129
|
return {
|
|
128
130
|
kind: "model",
|
|
129
131
|
action: "set",
|
|
130
|
-
model:
|
|
132
|
+
model: modelId,
|
|
133
|
+
thinking,
|
|
131
134
|
};
|
|
132
135
|
}
|
|
133
136
|
const planMatch = text.match(/^(?:\/plan|plan)(?:\s+(.*))?$/i);
|
|
@@ -291,10 +294,29 @@ async function runUcodeCoreAgent({
|
|
|
291
294
|
provider,
|
|
292
295
|
model,
|
|
293
296
|
});
|
|
297
|
+
const {
|
|
298
|
+
currentThinkingLevel,
|
|
299
|
+
} = require("./modelCommand");
|
|
300
|
+
const {
|
|
301
|
+
resolveThinkingFromEnvAndConfig,
|
|
302
|
+
applyThinkingLevelToEnv,
|
|
303
|
+
} = require("./thinkingLevels");
|
|
304
|
+
const { loadGlobalUcodeConfig } = require("../config");
|
|
305
|
+
let initialThinking = "";
|
|
306
|
+
try {
|
|
307
|
+
initialThinking = String((loadGlobalUcodeConfig() || {}).ucodeThinking || "").trim();
|
|
308
|
+
} catch {
|
|
309
|
+
initialThinking = "";
|
|
310
|
+
}
|
|
311
|
+
const thinkingResolved = resolveThinkingFromEnvAndConfig({
|
|
312
|
+
env: process.env,
|
|
313
|
+
configLevel: initialThinking,
|
|
314
|
+
});
|
|
294
315
|
const state = {
|
|
295
316
|
workspaceRoot: resolvedWorkspaceRoot,
|
|
296
317
|
provider: resolvedUcode.provider,
|
|
297
318
|
model: resolvedUcode.model,
|
|
319
|
+
thinking: currentThinkingLevel({ thinking: initialThinking }),
|
|
298
320
|
engine: "ufoo-core",
|
|
299
321
|
context: buildNlContext({
|
|
300
322
|
appendSystemPrompt,
|
|
@@ -308,6 +330,10 @@ async function runUcodeCoreAgent({
|
|
|
308
330
|
timeoutMs: resolveNlTaskTimeoutMs(Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : NaN),
|
|
309
331
|
jsonOutput,
|
|
310
332
|
};
|
|
333
|
+
// Named levels sync into env; leave an explicit numeric budget override alone.
|
|
334
|
+
if (thinkingResolved.source !== "env-budget") {
|
|
335
|
+
applyThinkingLevelToEnv(state.thinking, process.env);
|
|
336
|
+
}
|
|
311
337
|
persistSessionState(state);
|
|
312
338
|
|
|
313
339
|
if (shouldUseUcodeTui({
|
|
@@ -466,7 +492,9 @@ async function runUcodeCoreAgent({
|
|
|
466
492
|
}
|
|
467
493
|
}
|
|
468
494
|
if (result.kind === "model") {
|
|
469
|
-
const applied = applyUcodeModelCommand(state, result
|
|
495
|
+
const applied = await applyUcodeModelCommand(state, result, {
|
|
496
|
+
workspaceRoot: runtimeWorkspace,
|
|
497
|
+
});
|
|
470
498
|
stdout.write(`${applied.output}\n`);
|
|
471
499
|
if (applied.ok && result.action === "set") {
|
|
472
500
|
persistSessionState(state);
|
|
@@ -770,4 +798,5 @@ module.exports = {
|
|
|
770
798
|
applyUcodeModelCommand,
|
|
771
799
|
applyUcodePlanCommand,
|
|
772
800
|
suggestUcodeModels,
|
|
773
|
-
|
|
801
|
+
suggestUcodeThinkingLevels: require("./modelCommand").suggestUcodeThinkingLevels,
|
|
802
|
+
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
129
|
-
|
|
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
|
|
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
|
|
204
|
-
const active =
|
|
205
|
-
if (!active) {
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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: [{
|
|
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:
|
|
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
|
|
272
|
-
const active =
|
|
273
|
-
if (!active) {
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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: [{
|
|
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:
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 =
|
|
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);
|
|
@@ -93,13 +93,22 @@ function createTaskRun({
|
|
|
93
93
|
parentNodeId = "",
|
|
94
94
|
childGraphId = "",
|
|
95
95
|
attempt = 1,
|
|
96
|
+
kind = "",
|
|
97
|
+
objective = "",
|
|
98
|
+
title = "",
|
|
96
99
|
} = {}) {
|
|
97
100
|
const now = new Date().toISOString();
|
|
101
|
+
const parentNode = String(parentNodeId || "").trim();
|
|
102
|
+
const resolvedKind = String(kind || "").trim()
|
|
103
|
+
|| (parentNode ? "graph_node" : "standalone");
|
|
98
104
|
return {
|
|
99
105
|
id: createTaskRunId(),
|
|
106
|
+
kind: resolvedKind,
|
|
100
107
|
parentGraphId: String(parentGraphId || "").trim(),
|
|
101
|
-
parentNodeId:
|
|
108
|
+
parentNodeId: parentNode,
|
|
102
109
|
childGraphId: String(childGraphId || "").trim(),
|
|
110
|
+
objective: String(objective || "").trim(),
|
|
111
|
+
title: String(title || objective || "").trim(),
|
|
103
112
|
status: "queued",
|
|
104
113
|
phase: "initializing",
|
|
105
114
|
attempt: Number.isFinite(attempt) ? Math.max(1, Math.floor(attempt)) : 1,
|