u-foo 2.5.14 → 3.0.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/agents/prompts/native/environment.js +20 -8
- package/src/code/agent.js +517 -112
- package/src/code/commands.js +77 -0
- package/src/code/context/artifactGc.js +292 -0
- package/src/code/context/artifactIndex.js +161 -0
- package/src/code/context/artifacts.js +183 -0
- package/src/code/context/assembler.js +703 -0
- package/src/code/context/executionSegment.js +292 -0
- package/src/code/context/index.js +28 -0
- package/src/code/context/planGraph.js +1410 -0
- package/src/code/context/planGraphService.js +857 -0
- package/src/code/context/planMode.js +398 -0
- package/src/code/context/planProjection.js +432 -0
- package/src/code/context/projectSnapshot.js +201 -0
- package/src/code/context/promptLayers.js +175 -0
- package/src/code/context/reducers.js +328 -0
- package/src/code/context/stableJson.js +29 -0
- package/src/code/context/stateCommit.js +414 -0
- package/src/code/context/toolRuntime.js +172 -0
- package/src/code/context/transcript.js +182 -0
- package/src/code/context/transcriptSync.js +106 -0
- package/src/code/context/userInteraction.js +457 -0
- package/src/code/context/userNudge.js +116 -0
- package/src/code/context/workingSet.js +323 -0
- package/src/code/dispatch.js +20 -1
- package/src/code/index.js +8 -0
- package/src/code/modelCommand.js +87 -0
- package/src/code/nativeRunner.js +625 -34
- package/src/code/repl.js +196 -50
- package/src/code/runtime/agentWakeup.js +58 -0
- package/src/code/runtime/graphOwner.js +41 -0
- package/src/code/runtime/graphYieldRouter.js +42 -0
- package/src/code/runtime/index.js +15 -0
- package/src/code/runtime/loopMailbox.js +124 -0
- package/src/code/runtime/runtimeEvents.js +39 -0
- package/src/code/runtime/taskControl.js +565 -0
- package/src/code/runtime/taskFocus.js +165 -0
- package/src/code/runtime/taskLoop.js +383 -0
- package/src/code/runtime/taskRun.js +187 -0
- package/src/code/runtime/toolProvenance.js +70 -0
- package/src/code/runtime/workspaceLease.js +208 -0
- package/src/code/sessionStore.js +217 -15
- package/src/code/skills/index.js +10 -0
- package/src/code/skills/injection.js +66 -3
- package/src/code/skills/loader.js +21 -0
- package/src/code/skills/manifest.js +87 -0
- package/src/code/skills/render.js +15 -1
- package/src/code/taskDecomposer.js +56 -2
- package/src/code/tools/artifactRead.js +40 -0
- package/src/code/tools/askUser.js +11 -0
- package/src/code/tools/planGraph.js +29 -0
- package/src/code/tui.js +2 -0
- package/src/code/usageStore.js +15 -0
- package/src/ui/format/index.js +285 -45
- package/src/ui/format/markdownRenderer.js +436 -71
- package/src/ui/ink/ChatApp.js +39 -8
- package/src/ui/ink/UcodeApp.js +592 -43
- package/src/ui/ink/chatLogModel.js +102 -21
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
getIdentitySection,
|
|
5
|
+
} = require("../../agents/prompts/native/identity");
|
|
6
|
+
const {
|
|
7
|
+
getSystemSection,
|
|
8
|
+
} = require("../../agents/prompts/native/system");
|
|
9
|
+
const {
|
|
10
|
+
getDoingTasksSection,
|
|
11
|
+
} = require("../../agents/prompts/native/tasks");
|
|
12
|
+
const {
|
|
13
|
+
getActionsSection,
|
|
14
|
+
} = require("../../agents/prompts/native/actions");
|
|
15
|
+
const {
|
|
16
|
+
getSafetySection,
|
|
17
|
+
} = require("../../agents/prompts/native/safety");
|
|
18
|
+
const {
|
|
19
|
+
getOutputEfficiencySection,
|
|
20
|
+
} = require("../../agents/prompts/native/efficiency");
|
|
21
|
+
const {
|
|
22
|
+
getUfooIntegrationSection,
|
|
23
|
+
} = require("../../agents/prompts/native/ufoo");
|
|
24
|
+
const {
|
|
25
|
+
getSessionStableEnvironmentSection,
|
|
26
|
+
getTurnDynamicEnvironmentSection,
|
|
27
|
+
} = require("../../agents/prompts/native/environment");
|
|
28
|
+
const {
|
|
29
|
+
listUcodeSkills,
|
|
30
|
+
renderSkillsSection,
|
|
31
|
+
} = require("../skills");
|
|
32
|
+
const { hashContent } = require("./artifacts");
|
|
33
|
+
|
|
34
|
+
const PROMPT_VERSION = "native-v5";
|
|
35
|
+
|
|
36
|
+
function buildImmutablePrefix() {
|
|
37
|
+
return [
|
|
38
|
+
`promptVersion: ${PROMPT_VERSION}`,
|
|
39
|
+
getIdentitySection(),
|
|
40
|
+
getSystemSection(),
|
|
41
|
+
getDoingTasksSection(),
|
|
42
|
+
getActionsSection(),
|
|
43
|
+
getSafetySection(),
|
|
44
|
+
getOutputEfficiencySection(),
|
|
45
|
+
[
|
|
46
|
+
"Tool calling grammar:",
|
|
47
|
+
"- Use read, write, edit, bash, and artifact_read for direct work, even when it takes several tool calls. Use plan_graph only when the work needs a durable semantic/executable plan, explicit dependencies or checkpoints, or asynchronous TaskRuns.",
|
|
48
|
+
"- 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 and artifact_read remain available.",
|
|
49
|
+
"- 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. Use plan_graph operation=cancel_graph or operation=control with cancel_task to stop them.",
|
|
51
|
+
"- 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
|
+
"- 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.",
|
|
55
|
+
"- 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.",
|
|
58
|
+
"- 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
|
+
"- TaskLoops do not consume User reminders. The Agent Loop is woken by runtime task_started, task_succeeded, task_failed, and task_cancelled events.",
|
|
61
|
+
"- 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 file paths.",
|
|
63
|
+
"- 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
|
+
"- 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
|
+
"- ask_user is available only to the Agent Loop. It pauses the Agent Loop, but running TaskRuns continue unless explicitly cancelled.",
|
|
66
|
+
"- Prefer structured argument references when passing upstream node outputs or artifacts into downstream tool arguments. Use string templates only when string interpolation is required, and reference only existing upstream nodes.",
|
|
67
|
+
"State commit schema (optional at segment end):",
|
|
68
|
+
'{"stateCommit":{"factsAdd":[],"hypothesesUpdate":[],"decisionsAdd":[],"questionsClose":[],"nextObjective":""},"contextPlan":{"retainRaw":[],"retainRegions":[],"summarize":[],"evict":[],"rehydrateNext":[]}}',
|
|
69
|
+
].join("\n"),
|
|
70
|
+
].join("\n\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildSkillCatalogVersion(skills = []) {
|
|
74
|
+
const names = (Array.isArray(skills) ? skills : [])
|
|
75
|
+
.map((s) => `${s.name}:${s.path}`)
|
|
76
|
+
.sort()
|
|
77
|
+
.join("|");
|
|
78
|
+
return hashContent(names || "empty");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function buildSessionStablePrefix({
|
|
82
|
+
workspaceRoot = "",
|
|
83
|
+
provider = "",
|
|
84
|
+
model = "",
|
|
85
|
+
sessionStableExtras = "",
|
|
86
|
+
} = {}) {
|
|
87
|
+
const root = workspaceRoot || process.cwd();
|
|
88
|
+
const outcome = listUcodeSkills({ workspaceRoot: root });
|
|
89
|
+
const catalogVersion = buildSkillCatalogVersion(outcome.skills);
|
|
90
|
+
const parts = [
|
|
91
|
+
getUfooIntegrationSection(),
|
|
92
|
+
getSessionStableEnvironmentSection({ workspaceRoot: root, provider, model }),
|
|
93
|
+
renderSkillsSection(outcome.skills),
|
|
94
|
+
`skillCatalogVersion: ${catalogVersion}`,
|
|
95
|
+
];
|
|
96
|
+
if (sessionStableExtras) parts.push(String(sessionStableExtras).trim());
|
|
97
|
+
return parts.filter(Boolean).join("\n\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildLayeredSystemPrompt({
|
|
101
|
+
workspaceRoot = "",
|
|
102
|
+
model = "",
|
|
103
|
+
provider = "",
|
|
104
|
+
appendSystemPrompt = "",
|
|
105
|
+
overrideSystemPrompt = "",
|
|
106
|
+
epochDynamic = "",
|
|
107
|
+
turnDynamic = "",
|
|
108
|
+
sessionStableExtras = "",
|
|
109
|
+
} = {}) {
|
|
110
|
+
if (overrideSystemPrompt) {
|
|
111
|
+
return {
|
|
112
|
+
blocks: [{ layer: "override", text: overrideSystemPrompt, cacheable: false }],
|
|
113
|
+
flatText: overrideSystemPrompt,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const immutable = buildImmutablePrefix();
|
|
118
|
+
const sessionStable = buildSessionStablePrefix({
|
|
119
|
+
workspaceRoot,
|
|
120
|
+
provider,
|
|
121
|
+
model,
|
|
122
|
+
sessionStableExtras,
|
|
123
|
+
});
|
|
124
|
+
const epochText = String(epochDynamic || "").trim();
|
|
125
|
+
const turnParts = [
|
|
126
|
+
getTurnDynamicEnvironmentSection({ workspaceRoot: workspaceRoot || process.cwd() }),
|
|
127
|
+
String(turnDynamic || "").trim(),
|
|
128
|
+
String(appendSystemPrompt || "").trim(),
|
|
129
|
+
].filter(Boolean);
|
|
130
|
+
|
|
131
|
+
const blocks = [
|
|
132
|
+
{ layer: "immutable", text: immutable, cacheable: true },
|
|
133
|
+
{ layer: "sessionStable", text: sessionStable, cacheable: true },
|
|
134
|
+
];
|
|
135
|
+
if (epochText) {
|
|
136
|
+
blocks.push({
|
|
137
|
+
layer: "epoch",
|
|
138
|
+
text: epochText,
|
|
139
|
+
cacheable: true,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (turnParts.length > 0) {
|
|
143
|
+
blocks.push({
|
|
144
|
+
layer: "turnDynamic",
|
|
145
|
+
text: turnParts.join("\n\n"),
|
|
146
|
+
cacheable: false,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
blocks,
|
|
152
|
+
flatText: blocks.map((b) => b.text).filter(Boolean).join("\n\n"),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function systemBlocksToAnthropicPayload(blocks = []) {
|
|
157
|
+
const list = (Array.isArray(blocks) ? blocks : []).filter((b) => b && b.text);
|
|
158
|
+
const ANTHROPIC_CACHE_CONTROL = { type: "ephemeral" };
|
|
159
|
+
// Place cache breakpoints on every cacheable layer so Anthropic can reuse
|
|
160
|
+
// Immutable → SessionStable → Epoch prefixes independently. Turn-dynamic
|
|
161
|
+
// never gets cache_control.
|
|
162
|
+
return list.map((block) => {
|
|
163
|
+
const entry = { type: "text", text: block.text };
|
|
164
|
+
if (block.cacheable) entry.cache_control = { ...ANTHROPIC_CACHE_CONTROL };
|
|
165
|
+
return entry;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
PROMPT_VERSION,
|
|
171
|
+
buildImmutablePrefix,
|
|
172
|
+
buildSessionStablePrefix,
|
|
173
|
+
buildLayeredSystemPrompt,
|
|
174
|
+
systemBlocksToAnthropicPayload,
|
|
175
|
+
};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const PREVIEW_MAX_CHARS = 600;
|
|
4
|
+
const MODEL_PAYLOAD_MAX_CHARS = 4000;
|
|
5
|
+
|
|
6
|
+
function clipText(value = "", maxChars = PREVIEW_MAX_CHARS) {
|
|
7
|
+
const text = String(value || "");
|
|
8
|
+
if (text.length <= maxChars) return text;
|
|
9
|
+
return `${text.slice(0, maxChars)}\n...[truncated]`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function tailLines(text = "", count = 20) {
|
|
13
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
14
|
+
if (lines.length <= count) return lines.join("\n");
|
|
15
|
+
return lines.slice(-count).join("\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isTestCommand(command = "", stdout = "") {
|
|
19
|
+
return /\b(npm test|pnpm test|yarn test|npx jest|jest|vitest|mocha|pytest|cargo test|go test)\b/i.test(command)
|
|
20
|
+
|| /\b\d+\s+(passed|failed)\b/i.test(stdout)
|
|
21
|
+
|| /FAIL|PASS|Tests:/i.test(stdout);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isGitDiffCommand(command = "") {
|
|
25
|
+
return /\bgit\s+(?:diff|show)\b/i.test(String(command || ""));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isSearchCommand(command = "") {
|
|
29
|
+
return /\b(rg|ripgrep|grep|ag|ack)\b/i.test(String(command || ""));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractTestFailures(stdout = "", stderr = "") {
|
|
33
|
+
const text = `${stdout}\n${stderr}`;
|
|
34
|
+
const failures = [];
|
|
35
|
+
const patterns = [
|
|
36
|
+
/●\s+([^\n]+)/g,
|
|
37
|
+
/FAIL\s+([^\n]+)/g,
|
|
38
|
+
/(?:AssertionError|Error):\s*([^\n]+)/g,
|
|
39
|
+
/FAILED\s+([^\n]+)/g,
|
|
40
|
+
];
|
|
41
|
+
for (const re of patterns) {
|
|
42
|
+
let match;
|
|
43
|
+
while ((match = re.exec(text))) {
|
|
44
|
+
const title = String(match[1] || "").trim();
|
|
45
|
+
if (!title || failures.some((item) => item.title === title)) continue;
|
|
46
|
+
failures.push({ title: title.slice(0, 240) });
|
|
47
|
+
if (failures.length >= 12) return failures;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return failures;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseGitDiffFiles(stdout = "") {
|
|
54
|
+
const files = [];
|
|
55
|
+
const lines = String(stdout || "").split(/\r?\n/);
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
const diffMatch = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
58
|
+
if (diffMatch) {
|
|
59
|
+
const pathText = String(diffMatch[2] || diffMatch[1] || "").trim();
|
|
60
|
+
if (pathText && !files.includes(pathText)) files.push(pathText);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const statusMatch = line.match(/^[AMD]\t(.+)$/);
|
|
64
|
+
if (statusMatch) {
|
|
65
|
+
const pathText = String(statusMatch[1] || "").trim();
|
|
66
|
+
if (pathText && !files.includes(pathText)) files.push(pathText);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return files.slice(0, 80);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseSearchMatches(stdout = "") {
|
|
73
|
+
const matches = [];
|
|
74
|
+
const lines = String(stdout || "").split(/\r?\n/).filter(Boolean);
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
const match = line.match(/^([^:]+):(\d+)(?::(\d+))?:(.*)$/);
|
|
77
|
+
if (!match) continue;
|
|
78
|
+
matches.push({
|
|
79
|
+
path: match[1],
|
|
80
|
+
line: Number(match[2]),
|
|
81
|
+
column: match[3] ? Number(match[3]) : undefined,
|
|
82
|
+
text: String(match[4] || "").trim().slice(0, 200),
|
|
83
|
+
});
|
|
84
|
+
if (matches.length >= 40) break;
|
|
85
|
+
}
|
|
86
|
+
return matches;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function reduceReadResult(raw = {}, artifactId = "") {
|
|
90
|
+
const source = raw && typeof raw === "object" ? raw : {};
|
|
91
|
+
const content = String(source.content || "");
|
|
92
|
+
const preview = clipText(content, PREVIEW_MAX_CHARS);
|
|
93
|
+
const modelPayload = {
|
|
94
|
+
ok: source.ok !== false,
|
|
95
|
+
artifactId,
|
|
96
|
+
path: source.path || "",
|
|
97
|
+
startLine: source.startLine,
|
|
98
|
+
endLine: source.endLine,
|
|
99
|
+
totalLines: source.totalLines,
|
|
100
|
+
truncated: Boolean(source.truncated),
|
|
101
|
+
fileHash: source.fileHash || "",
|
|
102
|
+
preview,
|
|
103
|
+
};
|
|
104
|
+
if (content && content.length <= MODEL_PAYLOAD_MAX_CHARS) {
|
|
105
|
+
modelPayload.content = content;
|
|
106
|
+
} else if (content) {
|
|
107
|
+
modelPayload.content = clipText(content, MODEL_PAYLOAD_MAX_CHARS);
|
|
108
|
+
modelPayload.contentTruncated = true;
|
|
109
|
+
}
|
|
110
|
+
if (source.error) modelPayload.error = String(source.error);
|
|
111
|
+
return {
|
|
112
|
+
preview,
|
|
113
|
+
summary: `read ${source.path || "file"} (${source.totalLines || "?"} lines)`,
|
|
114
|
+
modelPayload,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function reduceTestResult(raw = {}, artifactId = "", args = {}) {
|
|
119
|
+
const source = raw && typeof raw === "object" ? raw : {};
|
|
120
|
+
const stdout = String(source.stdout || "");
|
|
121
|
+
const stderr = String(source.stderr || "");
|
|
122
|
+
const failedMatch = stdout.match(/(\d+)\s+failed/i) || stderr.match(/(\d+)\s+failed/i);
|
|
123
|
+
const passedMatch = stdout.match(/(\d+)\s+passed/i) || stderr.match(/(\d+)\s+passed/i);
|
|
124
|
+
const failures = extractTestFailures(stdout, stderr);
|
|
125
|
+
const preview = clipText(
|
|
126
|
+
[stdout ? `stdout:\n${tailLines(stdout, 12)}` : "", stderr ? `stderr:\n${tailLines(stderr, 8)}` : ""]
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
.join("\n"),
|
|
129
|
+
PREVIEW_MAX_CHARS,
|
|
130
|
+
);
|
|
131
|
+
return {
|
|
132
|
+
preview,
|
|
133
|
+
summary: `test exit=${source.code ?? source.exitCode ?? "?"} failed=${failedMatch ? failedMatch[1] : failures.length}`,
|
|
134
|
+
modelPayload: {
|
|
135
|
+
ok: source.ok !== false,
|
|
136
|
+
artifactId,
|
|
137
|
+
kind: "test",
|
|
138
|
+
exitCode: source.code ?? source.exitCode ?? null,
|
|
139
|
+
passed: passedMatch ? Number(passedMatch[1]) : undefined,
|
|
140
|
+
failed: failedMatch ? Number(failedMatch[1]) : failures.length || undefined,
|
|
141
|
+
failures,
|
|
142
|
+
stdoutTail: tailLines(stdout, 24),
|
|
143
|
+
stderrTail: tailLines(stderr, 12),
|
|
144
|
+
error: source.error ? String(source.error) : undefined,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function reduceGitDiffResult(raw = {}, artifactId = "", args = {}) {
|
|
150
|
+
const source = raw && typeof raw === "object" ? raw : {};
|
|
151
|
+
const stdout = String(source.stdout || "");
|
|
152
|
+
const files = parseGitDiffFiles(stdout);
|
|
153
|
+
const preview = clipText(
|
|
154
|
+
files.length > 0
|
|
155
|
+
? `git diff files (${files.length}):\n${files.slice(0, 20).join("\n")}`
|
|
156
|
+
: tailLines(stdout, 20),
|
|
157
|
+
PREVIEW_MAX_CHARS,
|
|
158
|
+
);
|
|
159
|
+
return {
|
|
160
|
+
preview,
|
|
161
|
+
summary: `git diff files=${files.length}`,
|
|
162
|
+
modelPayload: {
|
|
163
|
+
ok: source.ok !== false,
|
|
164
|
+
artifactId,
|
|
165
|
+
kind: "git_diff",
|
|
166
|
+
exitCode: source.code ?? source.exitCode ?? null,
|
|
167
|
+
files,
|
|
168
|
+
// Current diff stays complete in artifact; model gets file list + short preview.
|
|
169
|
+
stdoutTail: tailLines(stdout, 40),
|
|
170
|
+
stderrTail: tailLines(String(source.stderr || ""), 8),
|
|
171
|
+
error: source.error ? String(source.error) : undefined,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function reduceSearchResult(raw = {}, artifactId = "", args = {}) {
|
|
177
|
+
const source = raw && typeof raw === "object" ? raw : {};
|
|
178
|
+
const stdout = String(source.stdout || "");
|
|
179
|
+
const matches = parseSearchMatches(stdout);
|
|
180
|
+
const preview = clipText(
|
|
181
|
+
matches.length > 0
|
|
182
|
+
? matches.slice(0, 12).map((m) => `${m.path}:${m.line}: ${m.text}`).join("\n")
|
|
183
|
+
: tailLines(stdout, 20),
|
|
184
|
+
PREVIEW_MAX_CHARS,
|
|
185
|
+
);
|
|
186
|
+
return {
|
|
187
|
+
preview,
|
|
188
|
+
summary: `search matches=${matches.length}`,
|
|
189
|
+
modelPayload: {
|
|
190
|
+
ok: source.ok !== false,
|
|
191
|
+
artifactId,
|
|
192
|
+
kind: "search",
|
|
193
|
+
exitCode: source.code ?? source.exitCode ?? null,
|
|
194
|
+
matchCount: matches.length,
|
|
195
|
+
matches,
|
|
196
|
+
stdoutTail: tailLines(stdout, 20),
|
|
197
|
+
stderrTail: tailLines(String(source.stderr || ""), 8),
|
|
198
|
+
error: source.error ? String(source.error) : undefined,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function reduceBashResult(raw = {}, artifactId = "", args = {}) {
|
|
204
|
+
const source = raw && typeof raw === "object" ? raw : {};
|
|
205
|
+
const stdout = String(source.stdout || "");
|
|
206
|
+
const stderr = String(source.stderr || "");
|
|
207
|
+
const command = String((args && args.command) || source.command || "");
|
|
208
|
+
|
|
209
|
+
if (isTestCommand(command, stdout)) {
|
|
210
|
+
return reduceTestResult(raw, artifactId, args);
|
|
211
|
+
}
|
|
212
|
+
if (isGitDiffCommand(command)) {
|
|
213
|
+
return reduceGitDiffResult(raw, artifactId, args);
|
|
214
|
+
}
|
|
215
|
+
if (isSearchCommand(command)) {
|
|
216
|
+
return reduceSearchResult(raw, artifactId, args);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const preview = clipText(
|
|
220
|
+
[stdout ? `stdout:\n${tailLines(stdout, 8)}` : "", stderr ? `stderr:\n${tailLines(stderr, 8)}` : ""]
|
|
221
|
+
.filter(Boolean)
|
|
222
|
+
.join("\n"),
|
|
223
|
+
PREVIEW_MAX_CHARS,
|
|
224
|
+
);
|
|
225
|
+
return {
|
|
226
|
+
preview,
|
|
227
|
+
summary: `bash exit=${source.code ?? source.exitCode ?? "?"}`,
|
|
228
|
+
modelPayload: {
|
|
229
|
+
ok: source.ok !== false,
|
|
230
|
+
artifactId,
|
|
231
|
+
exitCode: source.code ?? source.exitCode ?? null,
|
|
232
|
+
stdoutTail: tailLines(stdout, 20),
|
|
233
|
+
stderrTail: tailLines(stderr, 20),
|
|
234
|
+
error: source.error ? String(source.error) : undefined,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function reduceWriteResult(raw = {}, artifactId = "") {
|
|
240
|
+
const source = raw && typeof raw === "object" ? raw : {};
|
|
241
|
+
const preview = `write ${source.path || "file"} (${source.bytes || 0} bytes)`;
|
|
242
|
+
return {
|
|
243
|
+
preview,
|
|
244
|
+
summary: preview,
|
|
245
|
+
modelPayload: {
|
|
246
|
+
ok: source.ok !== false,
|
|
247
|
+
artifactId,
|
|
248
|
+
path: source.path || "",
|
|
249
|
+
mode: source.mode,
|
|
250
|
+
bytes: source.bytes,
|
|
251
|
+
error: source.error ? String(source.error) : undefined,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function reduceEditResult(raw = {}, artifactId = "") {
|
|
257
|
+
const source = raw && typeof raw === "object" ? raw : {};
|
|
258
|
+
const preview = `edit ${source.path || "file"} changed=${Boolean(source.changed)}`;
|
|
259
|
+
return {
|
|
260
|
+
preview,
|
|
261
|
+
summary: preview,
|
|
262
|
+
modelPayload: {
|
|
263
|
+
ok: source.ok !== false,
|
|
264
|
+
artifactId,
|
|
265
|
+
path: source.path || "",
|
|
266
|
+
changed: Boolean(source.changed),
|
|
267
|
+
replacements: source.replacements,
|
|
268
|
+
error: source.error ? String(source.error) : undefined,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function reduceArtifactReadResult(raw = {}, artifactId = "") {
|
|
274
|
+
const source = raw && typeof raw === "object" ? raw : {};
|
|
275
|
+
const content = String(source.content || "");
|
|
276
|
+
return {
|
|
277
|
+
preview: clipText(content, PREVIEW_MAX_CHARS),
|
|
278
|
+
summary: `artifact_read ${source.artifactId || artifactId}`,
|
|
279
|
+
modelPayload: {
|
|
280
|
+
ok: source.ok !== false,
|
|
281
|
+
artifactId: source.artifactId || artifactId,
|
|
282
|
+
content: clipText(content, MODEL_PAYLOAD_MAX_CHARS),
|
|
283
|
+
range: source.range,
|
|
284
|
+
truncated: Boolean(source.truncated),
|
|
285
|
+
error: source.error ? String(source.error) : undefined,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function reduceToolResult(tool = "", raw = {}, artifactId = "", args = {}) {
|
|
291
|
+
const name = String(tool || "").trim().toLowerCase();
|
|
292
|
+
if (name === "read") return reduceReadResult(raw, artifactId);
|
|
293
|
+
if (name === "bash") return reduceBashResult(raw, artifactId, args);
|
|
294
|
+
if (name === "write") return reduceWriteResult(raw, artifactId);
|
|
295
|
+
if (name === "edit") return reduceEditResult(raw, artifactId);
|
|
296
|
+
if (name === "artifact_read") return reduceArtifactReadResult(raw, artifactId);
|
|
297
|
+
const text = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
298
|
+
return {
|
|
299
|
+
preview: clipText(text, PREVIEW_MAX_CHARS),
|
|
300
|
+
summary: `${name || "tool"} result`,
|
|
301
|
+
modelPayload: {
|
|
302
|
+
ok: raw && raw.ok !== false,
|
|
303
|
+
artifactId,
|
|
304
|
+
preview: clipText(text, MODEL_PAYLOAD_MAX_CHARS),
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = {
|
|
310
|
+
PREVIEW_MAX_CHARS,
|
|
311
|
+
MODEL_PAYLOAD_MAX_CHARS,
|
|
312
|
+
clipText,
|
|
313
|
+
isTestCommand,
|
|
314
|
+
isGitDiffCommand,
|
|
315
|
+
isSearchCommand,
|
|
316
|
+
extractTestFailures,
|
|
317
|
+
parseGitDiffFiles,
|
|
318
|
+
parseSearchMatches,
|
|
319
|
+
reduceToolResult,
|
|
320
|
+
reduceReadResult,
|
|
321
|
+
reduceBashResult,
|
|
322
|
+
reduceTestResult,
|
|
323
|
+
reduceGitDiffResult,
|
|
324
|
+
reduceSearchResult,
|
|
325
|
+
reduceWriteResult,
|
|
326
|
+
reduceEditResult,
|
|
327
|
+
reduceArtifactReadResult,
|
|
328
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function stableValue(value) {
|
|
4
|
+
if (value === null || typeof value !== "object") return value;
|
|
5
|
+
if (Array.isArray(value)) return value.map((item) => stableValue(item));
|
|
6
|
+
const keys = Object.keys(value).sort();
|
|
7
|
+
const out = {};
|
|
8
|
+
for (const key of keys) {
|
|
9
|
+
out[key] = stableValue(value[key]);
|
|
10
|
+
}
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function stableStringify(value) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.stringify(stableValue(value));
|
|
17
|
+
} catch {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
} catch {
|
|
21
|
+
return String(value || "");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
stableValue,
|
|
28
|
+
stableStringify,
|
|
29
|
+
};
|