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
package/package.json
CHANGED
|
@@ -11,7 +11,10 @@ function getDoingTasksSection() {
|
|
|
11
11
|
- Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs).
|
|
12
12
|
- Don't create helpers, utilities, or abstractions for one-time operations. Three similar lines of code is better than a premature abstraction.
|
|
13
13
|
- Follow workspace conventions and project instructions (AGENTS.md) when present.
|
|
14
|
-
- Prefer concrete code edits and verifiable outcomes over explanations
|
|
14
|
+
- Prefer concrete code edits and verifiable outcomes over explanations.
|
|
15
|
+
- For simple, single-goal work, execute directly with read/write/edit/bash.
|
|
16
|
+
- For complex work — multiple goals, several subsystems, long multi-step delivery, or clear parallel tracks — automatically decompose before diving in: split into concrete sub-objectives, then start one or more TaskRuns via task_run (standalone; no Plan Mode required). Use plan_graph only when durable dependencies, checkpoints, or a shared executable plan are needed.
|
|
17
|
+
- When decomposing, each TaskRun objective should be independently verifiable; prefer a few sharp tasks over one vague mega-task.`;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
module.exports = { getDoingTasksSection };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tool description for read_image (workspace vision).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const READ_IMAGE_TOOL_NAME = "read_image";
|
|
8
|
+
|
|
9
|
+
function getReadImageToolDescription() {
|
|
10
|
+
return `Read an image file from the workspace so the model can see it.
|
|
11
|
+
|
|
12
|
+
Usage notes:
|
|
13
|
+
- The path parameter is relative to the workspace root.
|
|
14
|
+
- Supports png, jpeg, gif, and webp. Max size ~5MB.
|
|
15
|
+
- Use this instead of read for screenshots, UI mocks, diagrams, and other binary images.
|
|
16
|
+
- Text files still use read. Do not call read_image on non-image files.
|
|
17
|
+
- Vision is attached for the current model call only; call read_image again if you need the image later.`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
READ_IMAGE_TOOL_NAME,
|
|
22
|
+
getReadImageToolDescription,
|
|
23
|
+
};
|
|
@@ -166,6 +166,7 @@ function createCommandExecutor(options = {}) {
|
|
|
166
166
|
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
167
167
|
schedule = (fn, ms) => setTimeout(fn, ms),
|
|
168
168
|
clearLog = null,
|
|
169
|
+
fetchModelsImpl = null,
|
|
169
170
|
} = options;
|
|
170
171
|
|
|
171
172
|
if (!projectRoot) {
|
|
@@ -1623,7 +1624,35 @@ function createCommandExecutor(options = {}) {
|
|
|
1623
1624
|
logMessage("system", ` • url: ${url || "(unset)"}`);
|
|
1624
1625
|
logMessage("system", ` • key: ${maskSecret(key)}`);
|
|
1625
1626
|
logMessage("system", ` • transport: ${transport} (auto)`);
|
|
1627
|
+
try {
|
|
1628
|
+
const { currentThinkingLevel } = require("../../code/modelCommand");
|
|
1629
|
+
logMessage("system", ` • thinking: ${currentThinkingLevel({})}`);
|
|
1630
|
+
} catch {
|
|
1631
|
+
// ignore
|
|
1632
|
+
}
|
|
1626
1633
|
logMessage("system", " • tip: url supports generic gateway base, transport is auto-detected");
|
|
1634
|
+
try {
|
|
1635
|
+
const { listUcodeModels } = require("../../code/modelCommand");
|
|
1636
|
+
const listed = await listUcodeModels({
|
|
1637
|
+
provider,
|
|
1638
|
+
model,
|
|
1639
|
+
}, {
|
|
1640
|
+
workspaceRoot: getActiveProjectRoot() || projectRoot,
|
|
1641
|
+
fetchImpl: fetchModelsImpl || undefined,
|
|
1642
|
+
});
|
|
1643
|
+
if (listed.ok && listed.models.length > 0) {
|
|
1644
|
+
const sample = listed.models.slice(0, 10);
|
|
1645
|
+
logMessage("system", ` • models route: ${listed.models.length} available`);
|
|
1646
|
+
logMessage("system", ` • models: ${sample.join(", ")}${listed.models.length > 10 ? "…" : ""}`);
|
|
1647
|
+
if (model && !listed.models.includes(model)) {
|
|
1648
|
+
logMessage("system", ` • warning: configured model "${model}" is not in the provider catalog`);
|
|
1649
|
+
}
|
|
1650
|
+
} else if (listed.error) {
|
|
1651
|
+
logMessage("system", ` • models route: ${listed.error}`);
|
|
1652
|
+
}
|
|
1653
|
+
} catch (err) {
|
|
1654
|
+
logMessage("system", ` • models route: ${err && err.message ? err.message : "unavailable"}`);
|
|
1655
|
+
}
|
|
1627
1656
|
return;
|
|
1628
1657
|
}
|
|
1629
1658
|
|
|
@@ -1645,6 +1674,52 @@ function createCommandExecutor(options = {}) {
|
|
|
1645
1674
|
logMessage("error", "{white-fg}✗{/white-fg} Usage: /settings ucode set provider=<openai|anthropic> model=<id> url=<baseUrl> key=<apiKey>");
|
|
1646
1675
|
return;
|
|
1647
1676
|
}
|
|
1677
|
+
|
|
1678
|
+
// Preview the config that would apply after this set, then confirm the
|
|
1679
|
+
// model id against the provider /models route when a model is present.
|
|
1680
|
+
const preview = {
|
|
1681
|
+
...(loadUcodeConfig() || {}),
|
|
1682
|
+
...updates,
|
|
1683
|
+
};
|
|
1684
|
+
const modelToConfirm = String(preview.ucodeModel || "").trim();
|
|
1685
|
+
if (modelToConfirm) {
|
|
1686
|
+
try {
|
|
1687
|
+
const { confirmModelSupported } = require("../../code/providers/modelsCatalog");
|
|
1688
|
+
const { resolveRuntimeConfig } = require("../../code/nativeRunner");
|
|
1689
|
+
const runtime = resolveRuntimeConfig({
|
|
1690
|
+
workspaceRoot: getActiveProjectRoot() || projectRoot,
|
|
1691
|
+
provider: preview.ucodeProvider || "",
|
|
1692
|
+
model: modelToConfirm,
|
|
1693
|
+
});
|
|
1694
|
+
// Prefer the previewed url/key over env-only resolution so a single
|
|
1695
|
+
// set line that changes url+model validates against the new gateway.
|
|
1696
|
+
const confirmation = await confirmModelSupported({
|
|
1697
|
+
provider: runtime.provider,
|
|
1698
|
+
transport: inferUcodeTransport(preview.ucodeProvider, preview.ucodeBaseUrl || runtime.baseUrl),
|
|
1699
|
+
baseUrl: String(preview.ucodeBaseUrl || runtime.baseUrl || "").trim(),
|
|
1700
|
+
apiKey: String(preview.ucodeApiKey || runtime.apiKey || "").trim(),
|
|
1701
|
+
model: modelToConfirm,
|
|
1702
|
+
strict: false,
|
|
1703
|
+
fetchImpl: fetchModelsImpl || undefined,
|
|
1704
|
+
});
|
|
1705
|
+
if (!confirmation.allowed) {
|
|
1706
|
+
logMessage("error", `{white-fg}✗{/white-fg} ${escapeBlessed(confirmation.error || "model not supported")}`);
|
|
1707
|
+
if (confirmation.models && confirmation.models.length > 0) {
|
|
1708
|
+
const sample = confirmation.models.slice(0, 10).join(", ");
|
|
1709
|
+
logMessage("system", ` • available: ${escapeBlessed(sample)}${confirmation.models.length > 10 ? "…" : ""}`);
|
|
1710
|
+
}
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
if (confirmation.warning) {
|
|
1714
|
+
logMessage("system", `{gray-fg}note:{/gray-fg} ${escapeBlessed(confirmation.warning)}`);
|
|
1715
|
+
} else if (confirmation.ok) {
|
|
1716
|
+
logMessage("system", `{gray-fg}models route:{/gray-fg} confirmed ${escapeBlessed(modelToConfirm)}`);
|
|
1717
|
+
}
|
|
1718
|
+
} catch (err) {
|
|
1719
|
+
logMessage("system", `{gray-fg}note:{/gray-fg} models route check skipped (${escapeBlessed(err && err.message ? err.message : "unavailable")})`);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1648
1723
|
saveUcodeConfig(updates);
|
|
1649
1724
|
logMessage("system", "{white-fg}✓{/white-fg} ucode config updated (global)");
|
|
1650
1725
|
if (Object.prototype.hasOwnProperty.call(updates, "ucodeProvider")) {
|
|
@@ -1664,6 +1739,41 @@ function createCommandExecutor(options = {}) {
|
|
|
1664
1739
|
return;
|
|
1665
1740
|
}
|
|
1666
1741
|
|
|
1742
|
+
if (action === "models") {
|
|
1743
|
+
try {
|
|
1744
|
+
const { listUcodeModels } = require("../../code/modelCommand");
|
|
1745
|
+
const config = loadUcodeConfig() || {};
|
|
1746
|
+
const listed = await listUcodeModels({
|
|
1747
|
+
provider: config.ucodeProvider || "",
|
|
1748
|
+
model: config.ucodeModel || "",
|
|
1749
|
+
}, {
|
|
1750
|
+
workspaceRoot: getActiveProjectRoot() || projectRoot,
|
|
1751
|
+
skipCache: true,
|
|
1752
|
+
fetchImpl: fetchModelsImpl || undefined,
|
|
1753
|
+
});
|
|
1754
|
+
if (!listed.ok) {
|
|
1755
|
+
logMessage("error", `{white-fg}✗{/white-fg} ${escapeBlessed(listed.error || "models route failed")}`);
|
|
1756
|
+
if (listed.url) logMessage("system", ` • url: ${escapeBlessed(listed.url)}`);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
logMessage("system", `{cyan-fg}ucode models:{/cyan-fg} ${listed.models.length} from ${escapeBlessed(listed.url)}`);
|
|
1760
|
+
if (listed.models.length === 0) {
|
|
1761
|
+
logMessage("system", " • (empty catalog)");
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
listed.models.slice(0, 40).forEach((id) => {
|
|
1765
|
+
const current = String(config.ucodeModel || "").trim() === id ? " {gray-fg}(current){/gray-fg}" : "";
|
|
1766
|
+
logMessage("system", ` • ${escapeBlessed(id)}${current}`);
|
|
1767
|
+
});
|
|
1768
|
+
if (listed.models.length > 40) {
|
|
1769
|
+
logMessage("system", ` • … ${listed.models.length - 40} more`);
|
|
1770
|
+
}
|
|
1771
|
+
} catch (err) {
|
|
1772
|
+
logMessage("error", `{white-fg}✗{/white-fg} ${escapeBlessed(err && err.message ? err.message : "models route failed")}`);
|
|
1773
|
+
}
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1667
1777
|
if (action === "clear") {
|
|
1668
1778
|
const fieldsRaw = args.slice(1).map((item) => String(item || "").trim().toLowerCase()).filter(Boolean);
|
|
1669
1779
|
const fields = fieldsRaw.length === 0 ? ["all"] : fieldsRaw;
|
|
@@ -1682,7 +1792,7 @@ function createCommandExecutor(options = {}) {
|
|
|
1682
1792
|
return;
|
|
1683
1793
|
}
|
|
1684
1794
|
|
|
1685
|
-
logMessage("error", "{white-fg}✗{/white-fg} Unknown settings ucode action. Use: show, set, clear");
|
|
1795
|
+
logMessage("error", "{white-fg}✗{/white-fg} Unknown settings ucode action. Use: show, set, models, clear");
|
|
1686
1796
|
}
|
|
1687
1797
|
|
|
1688
1798
|
async function executeCommand(text) {
|
package/src/app/chat/commands.js
CHANGED
|
@@ -135,7 +135,8 @@ const COMMAND_TREE = {
|
|
|
135
135
|
children: {
|
|
136
136
|
show: { desc: "Show ucode provider/model/url/key", order: 1 },
|
|
137
137
|
set: { desc: "Set ucode provider/model/url/key", order: 2 },
|
|
138
|
-
|
|
138
|
+
models: { desc: "List models from the provider /models route", order: 3 },
|
|
139
|
+
clear: { desc: "Clear ucode provider/model/url/key", order: 4 },
|
|
139
140
|
},
|
|
140
141
|
},
|
|
141
142
|
},
|
|
@@ -416,7 +416,7 @@ function createDaemonMessageRouter(options = {}) {
|
|
|
416
416
|
const publisher = report.agent_id || data.publisher || "ufoo-agent";
|
|
417
417
|
const displayName = resolveAgentDisplayName(publisher);
|
|
418
418
|
const detail = report.summary || report.message || data.message || report.task_id || "report";
|
|
419
|
-
logMessage("
|
|
419
|
+
logMessage("report", `${speakerPrefix(displayName)}${escapeBlessed(detail)}`, data);
|
|
420
420
|
requestStatus();
|
|
421
421
|
renderScreen();
|
|
422
422
|
return true;
|
|
@@ -33,8 +33,9 @@ function createInputSubmitHandler(options = {}) {
|
|
|
33
33
|
|
|
34
34
|
function userEcho(text, targetLabel = "") {
|
|
35
35
|
const body = escapeBlessed(text);
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// Match ucode › prompt echo so history reload and live log share a prefix.
|
|
37
|
+
if (!targetLabel) return `› ${body}`;
|
|
38
|
+
return `› {magenta-fg}@${escapeBlessed(targetLabel)}{/magenta-fg} ${body}`;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
async function tryActivateTargetAgent(agentId) {
|
package/src/code/commands.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
const UCODE_COMMAND_REGISTRY = [
|
|
8
8
|
{ cmd: "/help", desc: "Show available commands", order: 10 },
|
|
9
9
|
{ cmd: "/status", desc: "Show session / usage status", order: 20 },
|
|
10
|
-
{ cmd: "/model", desc: "Show or switch
|
|
10
|
+
{ cmd: "/model", desc: "Show or switch model (+ thinking intensity)", order: 25 },
|
|
11
11
|
{ cmd: "/plan", desc: "Show plan progress or set plan mode", order: 27 },
|
|
12
12
|
{ cmd: "/ubus", desc: "Check pending bus messages", order: 30 },
|
|
13
13
|
{ cmd: "/resume", desc: "Resume a saved session", order: 40 },
|
|
@@ -20,7 +20,7 @@ const UCODE_COMMAND_TREE = {
|
|
|
20
20
|
"/help": { desc: "Show available commands" },
|
|
21
21
|
"/status": { desc: "Show session / usage status" },
|
|
22
22
|
"/model": {
|
|
23
|
-
desc: "Show or switch
|
|
23
|
+
desc: "Show or switch model, then pick thinking intensity",
|
|
24
24
|
hasArguments: true,
|
|
25
25
|
optionalArguments: true,
|
|
26
26
|
},
|
|
@@ -59,7 +59,7 @@ function listUcodeCommandsForHelp() {
|
|
|
59
59
|
" /exit|/quit",
|
|
60
60
|
" /ubus",
|
|
61
61
|
" /status",
|
|
62
|
-
" /model [model-id]",
|
|
62
|
+
" /model [model-id] [off|low|medium|high|max]",
|
|
63
63
|
" /plan [on|off|show|hide|focus|debug|clear]",
|
|
64
64
|
" /skills [list]",
|
|
65
65
|
" /skills show <name>",
|
|
@@ -34,6 +34,7 @@ const {
|
|
|
34
34
|
const { renderExecutionSegmentContext } = require("./executionSegment");
|
|
35
35
|
const { renderPlanModeContext } = require("./planMode");
|
|
36
36
|
const { drainAgentMailboxForTurn } = require("../runtime/agentWakeup");
|
|
37
|
+
const { stripVisionBase64, degradeVisionContent } = require("../providers/visionBlocks");
|
|
37
38
|
|
|
38
39
|
const DEFAULT_TRANSCRIPT_WINDOW = 12;
|
|
39
40
|
const DEFAULT_RECENT_TOOL_EVENTS = 4;
|
|
@@ -291,6 +292,19 @@ function sanitizeModelMessages(messages = []) {
|
|
|
291
292
|
continue;
|
|
292
293
|
}
|
|
293
294
|
|
|
295
|
+
// Drop ephemeral OpenAI vision companion user messages (image_url data URIs)
|
|
296
|
+
// when rebuilding history so base64 does not re-enter later turns.
|
|
297
|
+
if (role === "user" && Array.isArray(message.content)) {
|
|
298
|
+
const hasImageUrl = message.content.some((block) => (
|
|
299
|
+
block && String(block.type || "").trim().toLowerCase() === "image_url"
|
|
300
|
+
));
|
|
301
|
+
if (hasImageUrl) {
|
|
302
|
+
const degraded = degradeVisionContent(message.content);
|
|
303
|
+
out.push({ role: "user", content: degraded });
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
294
308
|
out.push(message);
|
|
295
309
|
}
|
|
296
310
|
return out;
|
|
@@ -435,16 +449,18 @@ function persistToolResultToContext({
|
|
|
435
449
|
rawResult = {},
|
|
436
450
|
segmentId = "",
|
|
437
451
|
} = {}) {
|
|
452
|
+
const rawForStorage = stripVisionBase64(rawResult);
|
|
438
453
|
const saved = saveArtifact(workspaceRoot, sessionId, {
|
|
439
454
|
type: "tool_result",
|
|
440
455
|
tool,
|
|
441
456
|
args,
|
|
442
|
-
raw:
|
|
457
|
+
raw: rawForStorage,
|
|
443
458
|
createdBy: tool,
|
|
444
459
|
});
|
|
445
460
|
const artifactId = saved.artifact && saved.artifact.artifactId
|
|
446
461
|
? saved.artifact.artifactId
|
|
447
462
|
: "";
|
|
463
|
+
// Reduce from the live result so vision base64 stays available for this turn.
|
|
448
464
|
const reduced = reduceToolResult(tool, rawResult, artifactId, args);
|
|
449
465
|
return {
|
|
450
466
|
artifactId,
|
|
@@ -181,7 +181,7 @@ function formatPlanModeStatus(executionState = null) {
|
|
|
181
181
|
lines.push("Rules while ON:");
|
|
182
182
|
lines.push(" - Use plan_graph to create/expand/complete the plan");
|
|
183
183
|
lines.push(" - write / edit / bash are blocked as direct tools");
|
|
184
|
-
lines.push(" - read / artifact_read allowed for exploration");
|
|
184
|
+
lines.push(" - read / read_image / artifact_read allowed for exploration");
|
|
185
185
|
lines.push(" - Runtime auto-advances ready tool nodes after plan_graph");
|
|
186
186
|
lines.push(" - User leaves with /plan off (agents cannot toggle Plan Mode)");
|
|
187
187
|
} else {
|
|
@@ -223,7 +223,7 @@ function renderPlanModeContext(executionState = null) {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
lines.push(
|
|
226
|
-
"Allowed direct tools: read, artifact_read, plan_graph, ask_user.",
|
|
226
|
+
"Allowed direct tools: read, read_image, artifact_read, plan_graph, ask_user.",
|
|
227
227
|
"Blocked direct tools: write, edit, bash (route them as plan_graph tool nodes or task_loop).",
|
|
228
228
|
"Plan Mode constrains the Agent Loop only; running TaskLoops are not paused or reconfigured by /plan off.",
|
|
229
229
|
"Only the user can leave Plan Mode (/plan off). That does not cancel the graph or TaskRuns — use cancel_graph / control.cancel_task.",
|
|
@@ -31,7 +31,7 @@ const {
|
|
|
31
31
|
} = require("../skills");
|
|
32
32
|
const { hashContent } = require("./artifacts");
|
|
33
33
|
|
|
34
|
-
const PROMPT_VERSION = "native-
|
|
34
|
+
const PROMPT_VERSION = "native-v7";
|
|
35
35
|
|
|
36
36
|
function buildImmutablePrefix() {
|
|
37
37
|
return [
|
|
@@ -44,22 +44,24 @@ function buildImmutablePrefix() {
|
|
|
44
44
|
getOutputEfficiencySection(),
|
|
45
45
|
[
|
|
46
46
|
"Tool calling grammar:",
|
|
47
|
-
"- Use read, write, edit, bash, and artifact_read for direct work, even when it takes several tool calls.
|
|
48
|
-
"-
|
|
47
|
+
"- Use read, write, edit, bash, and artifact_read for direct, single-goal text work, even when it takes several tool calls.",
|
|
48
|
+
"- Use read_image to load workspace png/jpeg/gif/webp images for vision. Do not use read on binary images. Vision is attached for the active model call only; call read_image again if you need the image later.",
|
|
49
|
+
"- TaskRuns are orthogonal to Plan Mode. A TaskRun does not require Plan Mode or a plan_graph. Use task_run operation=start with an objective for a standalone single-point TaskRun; it starts asynchronously and returns immediately.",
|
|
50
|
+
"- On complex or multi-goal requests, automatically decompose into concrete sub-objectives and start TaskRun(s) via task_run. Prefer task_run for independent or loosely coupled tracks; use plan_graph only when you need durable dependencies, checkpoints, or a shared executable plan.",
|
|
51
|
+
"- Use plan_graph for durable graph structure: create, patch, inspect, cancel_graph, and control. Graph-bound TaskRuns use plan_graph control.start_task on execution.kind=task_loop nodes.",
|
|
52
|
+
"- Plan Mode is a runtime posture for the Agent Loop, not an agent tool. While Plan Mode is ON, direct write, edit, and bash calls from the Agent Loop are blocked; read, read_image, and artifact_read remain available. TaskRuns still run independently of Plan Mode.",
|
|
49
53
|
"- In the Agent Loop, plan_graph operation=create automatically enables Plan Mode. The user may also use /plan on or /plan off.",
|
|
50
|
-
"- Turning Plan Mode off does not cancel an existing graph or running TaskRuns.
|
|
54
|
+
"- Turning Plan Mode off does not cancel an existing graph or running TaskRuns. Cancel with task_run (standalone) or plan_graph operation=cancel_graph / control.cancel_task (graph-bound).",
|
|
51
55
|
"- When the user enables Plan Mode and no active graph exists, create a plan_graph before performing side effects.",
|
|
52
|
-
"- Use plan_graph for durable graph structure and TaskRun lifecycle: create, patch, inspect, cancel_graph, and control.",
|
|
53
56
|
"- After an accepted plan_graph create or patch, Runtime automatically advances ready tool nodes. Never invent or request an execute_graph tool.",
|
|
54
|
-
"- Do not call plan_graph together with read, write, edit, bash, or artifact_read in the same assistant turn.",
|
|
57
|
+
"- Do not call plan_graph or task_run together with read, read_image, write, edit, bash, or artifact_read in the same assistant turn.",
|
|
55
58
|
"- When an active graph is waiting on a task, advance that node through plan_graph instead of bypassing it with direct workspace tools: use patch.expand_node for execution.kind=expand, control.complete_task (nodeId) for execution.kind=inline_llm, or control.start_task for execution.kind=task_loop.",
|
|
56
|
-
"- control.complete_task with nodeId completes a waiting_llm inline_llm task for the current Graph owner. control.complete_task with taskRunId is reserved for the owning TaskLoop. Do not directly complete expand or aggregate tasks.",
|
|
57
|
-
"- While Plan Mode is ON, workspace mutations must be represented as plan_graph tool nodes or performed inside a running task_loop.",
|
|
59
|
+
"- control.complete_task with nodeId completes a waiting_llm inline_llm task for the current Graph owner. control.complete_task with taskRunId (or task_run complete) is reserved for the owning TaskLoop. Do not directly complete expand or aggregate tasks.",
|
|
60
|
+
"- While Plan Mode is ON, workspace mutations must be represented as plan_graph tool nodes or performed inside a running TaskRun/task_loop.",
|
|
58
61
|
"- Treat a User reminder as the latest user instruction. Reconcile it before continuing from tool results. If it is compatible with the active plan, resume the waiting plan node; otherwise patch, cancel, or replan first.",
|
|
59
|
-
"- Use execution.kind=task_loop for work that should continue asynchronously without occupying the Agent Loop. plan_graph operation=control action=start_task starts the TaskRun and returns immediately.",
|
|
60
62
|
"- TaskLoops do not consume User reminders. The Agent Loop is woken by runtime task_started, task_succeeded, task_failed, and task_cancelled events.",
|
|
61
63
|
"- Runtime enforces TaskRun concurrency limits and workspace write leases. Direct Agent write, edit, or bash calls may be rejected while writing TaskRuns are active.",
|
|
62
|
-
"- Tool results may contain an artifactId. Use artifact_read to hydrate raw stored output or a slice of it; use read for workspace
|
|
64
|
+
"- Tool results may contain an artifactId. Use artifact_read to hydrate raw stored output or a slice of it; use read for workspace text paths; use read_image for workspace images.",
|
|
63
65
|
"- Use ask_user only when user input is required to proceed. It must be the only tool call in the turn. Use kind=approval for yes/no confirmation, kind=choice for numbered options, and kind=chat for free text.",
|
|
64
66
|
"- The answer to ask_user is returned only as that tool's result, not as a separate user message or pending User reminder. Continue from the returned answer and do not repeat the question.",
|
|
65
67
|
"- ask_user is available only to the Agent Loop. It pauses the Agent Loop, but running TaskRuns continue unless explicitly cancelled.",
|
|
@@ -287,9 +287,43 @@ function reduceArtifactReadResult(raw = {}, artifactId = "") {
|
|
|
287
287
|
};
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
function reduceReadImageResult(raw = {}, artifactId = "") {
|
|
291
|
+
const source = raw && typeof raw === "object" ? raw : {};
|
|
292
|
+
const pathText = String(source.path || "").trim();
|
|
293
|
+
const mediaType = String(source.mediaType || "").trim();
|
|
294
|
+
const bytes = Number.isFinite(source.bytes) ? source.bytes : null;
|
|
295
|
+
const preview = source.ok === false
|
|
296
|
+
? clipText(String(source.error || "read_image failed"), PREVIEW_MAX_CHARS)
|
|
297
|
+
: clipText(
|
|
298
|
+
`image ${pathText || "file"} (${mediaType || "unknown"}, ${bytes != null ? `${bytes} bytes` : "size?"})`,
|
|
299
|
+
PREVIEW_MAX_CHARS,
|
|
300
|
+
);
|
|
301
|
+
const modelPayload = {
|
|
302
|
+
ok: source.ok !== false,
|
|
303
|
+
kind: "image",
|
|
304
|
+
artifactId,
|
|
305
|
+
path: pathText,
|
|
306
|
+
mediaType,
|
|
307
|
+
bytes,
|
|
308
|
+
preview,
|
|
309
|
+
};
|
|
310
|
+
if (source.error) modelPayload.error = String(source.error);
|
|
311
|
+
// Keep base64 in the in-memory model payload for the current turn only.
|
|
312
|
+
// Artifacts / transcript strip it via stripVisionBase64 before persistence.
|
|
313
|
+
if (modelPayload.ok && source.base64) {
|
|
314
|
+
modelPayload.base64 = String(source.base64);
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
preview,
|
|
318
|
+
summary: `read_image ${pathText || "file"} (${mediaType || "image"})`,
|
|
319
|
+
modelPayload,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
290
323
|
function reduceToolResult(tool = "", raw = {}, artifactId = "", args = {}) {
|
|
291
324
|
const name = String(tool || "").trim().toLowerCase();
|
|
292
325
|
if (name === "read") return reduceReadResult(raw, artifactId);
|
|
326
|
+
if (name === "read_image") return reduceReadImageResult(raw, artifactId);
|
|
293
327
|
if (name === "bash") return reduceBashResult(raw, artifactId, args);
|
|
294
328
|
if (name === "write") return reduceWriteResult(raw, artifactId);
|
|
295
329
|
if (name === "edit") return reduceEditResult(raw, artifactId);
|
|
@@ -318,6 +352,7 @@ module.exports = {
|
|
|
318
352
|
parseSearchMatches,
|
|
319
353
|
reduceToolResult,
|
|
320
354
|
reduceReadResult,
|
|
355
|
+
reduceReadImageResult,
|
|
321
356
|
reduceBashResult,
|
|
322
357
|
reduceTestResult,
|
|
323
358
|
reduceGitDiffResult,
|
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
createTranscriptEventId,
|
|
6
6
|
appendTranscriptEvent,
|
|
7
7
|
} = require("./transcript");
|
|
8
|
+
const { stripVisionBase64, degradeVisionContent } = require("../providers/visionBlocks");
|
|
8
9
|
|
|
9
10
|
function messageRole(message = {}) {
|
|
10
11
|
return String(message && message.role || "").trim().toLowerCase();
|
|
@@ -35,6 +36,24 @@ function parseToolArtifactContent(content = "") {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
function contentForStorage(content) {
|
|
40
|
+
if (typeof content === "string") return content;
|
|
41
|
+
if (Array.isArray(content)) {
|
|
42
|
+
const hasVision = content.some((block) => {
|
|
43
|
+
if (!block || typeof block !== "object") return false;
|
|
44
|
+
const type = String(block.type || "").trim().toLowerCase();
|
|
45
|
+
return type === "image" || type === "image_url"
|
|
46
|
+
|| (type === "tool_result" && Array.isArray(block.content));
|
|
47
|
+
});
|
|
48
|
+
if (hasVision) return degradeVisionContent(stripVisionBase64(content));
|
|
49
|
+
return stripVisionBase64(content);
|
|
50
|
+
}
|
|
51
|
+
if (content && typeof content === "object") {
|
|
52
|
+
return stripVisionBase64(content);
|
|
53
|
+
}
|
|
54
|
+
return content;
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
function messageToTranscriptEventForStorage(message = {}, extra = {}) {
|
|
39
58
|
if (!message || typeof message !== "object") return null;
|
|
40
59
|
const role = messageRole(message);
|
|
@@ -60,9 +79,10 @@ function messageToTranscriptEventForStorage(message = {}, extra = {}) {
|
|
|
60
79
|
toolCallId: message.tool_call_id ? String(message.tool_call_id) : undefined,
|
|
61
80
|
});
|
|
62
81
|
}
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
const stored = contentForStorage(message.content);
|
|
83
|
+
const preview = typeof stored === "string"
|
|
84
|
+
? stored.slice(0, 600)
|
|
85
|
+
: JSON.stringify(stored).slice(0, 600);
|
|
66
86
|
return normalizeTranscriptEvent({
|
|
67
87
|
...base,
|
|
68
88
|
role: "tool",
|
|
@@ -75,14 +95,14 @@ function messageToTranscriptEventForStorage(message = {}, extra = {}) {
|
|
|
75
95
|
if (role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
|
|
76
96
|
return normalizeTranscriptEvent({
|
|
77
97
|
...base,
|
|
78
|
-
content: message.content,
|
|
98
|
+
content: contentForStorage(message.content),
|
|
79
99
|
toolCalls: message.tool_calls,
|
|
80
100
|
});
|
|
81
101
|
}
|
|
82
102
|
|
|
83
103
|
return normalizeTranscriptEvent({
|
|
84
104
|
...base,
|
|
85
|
-
content: message.content,
|
|
105
|
+
content: contentForStorage(message.content),
|
|
86
106
|
});
|
|
87
107
|
}
|
|
88
108
|
|
package/src/code/dispatch.js
CHANGED
|
@@ -1,31 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const { runReadTool } = require("./tools/read");
|
|
4
|
+
const { runReadImageTool } = require("./tools/readImage");
|
|
4
5
|
const { runWriteTool } = require("./tools/write");
|
|
5
6
|
const { runEditTool } = require("./tools/edit");
|
|
6
7
|
const { runBashTool } = require("./tools/bash");
|
|
7
8
|
const { runArtifactReadTool } = require("./tools/artifactRead");
|
|
8
9
|
const { runPlanGraphTool } = require("./tools/planGraph");
|
|
10
|
+
const { runTaskRunTool } = require("./tools/taskRun");
|
|
9
11
|
const { runAskUserTool } = require("./tools/askUser");
|
|
10
12
|
|
|
11
13
|
const TOOL_NAMES = [
|
|
12
14
|
"read",
|
|
15
|
+
"read_image",
|
|
13
16
|
"write",
|
|
14
17
|
"edit",
|
|
15
18
|
"bash",
|
|
16
19
|
"artifact_read",
|
|
17
20
|
"plan_graph",
|
|
21
|
+
"task_run",
|
|
18
22
|
"ask_user",
|
|
19
23
|
];
|
|
20
24
|
|
|
21
25
|
function normalizeToolName(value = "") {
|
|
22
26
|
const text = String(value || "").trim().toLowerCase();
|
|
23
27
|
if (text === "read") return "read";
|
|
28
|
+
if (text === "read_image" || text === "read-image" || text === "readimage") return "read_image";
|
|
24
29
|
if (text === "write") return "write";
|
|
25
30
|
if (text === "edit") return "edit";
|
|
26
31
|
if (text === "bash") return "bash";
|
|
27
32
|
if (text === "artifact_read" || text === "artifact-read" || text === "artifactread") return "artifact_read";
|
|
28
33
|
if (text === "plan_graph" || text === "plan-graph" || text === "plangraph") return "plan_graph";
|
|
34
|
+
if (text === "task_run" || text === "task-run" || text === "taskrun") return "task_run";
|
|
29
35
|
if (text === "ask_user" || text === "ask-user" || text === "askuser") return "ask_user";
|
|
30
36
|
return "";
|
|
31
37
|
}
|
|
@@ -41,10 +47,12 @@ function runToolCall(input = {}, options = {}) {
|
|
|
41
47
|
};
|
|
42
48
|
}
|
|
43
49
|
if (tool === "read") return runReadTool(args, options);
|
|
50
|
+
if (tool === "read_image") return runReadImageTool(args, options);
|
|
44
51
|
if (tool === "write") return runWriteTool(args, options);
|
|
45
52
|
if (tool === "edit") return runEditTool(args, options);
|
|
46
53
|
if (tool === "artifact_read") return runArtifactReadTool(args, options);
|
|
47
54
|
if (tool === "plan_graph") return runPlanGraphTool(args, options);
|
|
55
|
+
if (tool === "task_run") return runTaskRunTool(args, options);
|
|
48
56
|
if (tool === "ask_user") return runAskUserTool(args, options);
|
|
49
57
|
return runBashTool(args, options);
|
|
50
58
|
}
|