z0gcode 0.2.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/.env.example +41 -0
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/bin/z0g.mjs +1052 -0
- package/contracts/Z0gSession.json +424 -0
- package/contracts/Z0gSession.sol +106 -0
- package/package.json +60 -0
- package/skills/0g/CHAIN.md +229 -0
- package/skills/0g/COMPUTE.md +296 -0
- package/skills/0g/NETWORK_CONFIG.md +163 -0
- package/skills/0g/SECURITY.md +174 -0
- package/skills/0g/STORAGE.md +167 -0
- package/skills/0g/TESTING.md +263 -0
- package/src/agent.mjs +434 -0
- package/src/anchor.mjs +65 -0
- package/src/checkpoints.mjs +64 -0
- package/src/client.mjs +134 -0
- package/src/commands.mjs +93 -0
- package/src/config.mjs +74 -0
- package/src/context.mjs +55 -0
- package/src/crypto.mjs +47 -0
- package/src/env.mjs +47 -0
- package/src/goal.mjs +45 -0
- package/src/inft.mjs +79 -0
- package/src/mcp-server.mjs +38 -0
- package/src/mcp.mjs +91 -0
- package/src/media.mjs +52 -0
- package/src/models-info.mjs +97 -0
- package/src/plan.mjs +21 -0
- package/src/prompt.mjs +149 -0
- package/src/provenance.mjs +46 -0
- package/src/sessions.mjs +168 -0
- package/src/settings.mjs +37 -0
- package/src/skills.mjs +43 -0
- package/src/tools.mjs +481 -0
- package/src/ui.mjs +798 -0
- package/src/user-skills.mjs +111 -0
- package/src/worktree.mjs +61 -0
package/src/agent.mjs
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
// The agentic loop: reason on 0G, call tools, feed results back, repeat until done.
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { promises as fs } from "node:fs";
|
|
4
|
+
import { CONFIG, modelChain } from "./config.mjs";
|
|
5
|
+
import { completeStream } from "./client.mjs";
|
|
6
|
+
import { TOOL_DEFS, makeExecutor } from "./tools.mjs";
|
|
7
|
+
import { SYSTEM_0G } from "./skills.mjs";
|
|
8
|
+
import { skillsPromptBlock } from "./user-skills.mjs";
|
|
9
|
+
import { contextPromptBlock } from "./context.mjs";
|
|
10
|
+
import { makeProvenance } from "./provenance.mjs";
|
|
11
|
+
import { recordCheckpoint } from "./checkpoints.mjs";
|
|
12
|
+
import { isGitRepo, addWorktree, collectPatch, removeWorktree, applyPatch } from "./worktree.mjs";
|
|
13
|
+
import * as ui from "./ui.mjs";
|
|
14
|
+
|
|
15
|
+
function systemPrompt(cwd) {
|
|
16
|
+
return [
|
|
17
|
+
"You are z0gcode, a terminal coding agent whose brain runs on 0G Compute (0G's decentralized, private, verifiable inference).",
|
|
18
|
+
"You help developers build software and you are an expert at building on the 0G stack.",
|
|
19
|
+
"",
|
|
20
|
+
"Work like a careful engineer:",
|
|
21
|
+
"- If the task needs 3 or more steps, your FIRST tool call MUST be update_plan with a short checklist; then update it (exactly one step in_progress) as you complete each step.",
|
|
22
|
+
"- Use search_files (regex) to locate code instead of reading whole files.",
|
|
23
|
+
"- Inspect before you change: read files and list directories first.",
|
|
24
|
+
"- Make minimal, correct edits. Prefer edit_file over rewriting whole files.",
|
|
25
|
+
"- After changing code, verify it (run it with run_bash when allowed).",
|
|
26
|
+
"- Never print secrets and never hardcode private keys; use environment variables.",
|
|
27
|
+
"- When the task is complete, STOP calling tools and reply with a short summary of what you did.",
|
|
28
|
+
"",
|
|
29
|
+
SYSTEM_0G,
|
|
30
|
+
skillsPromptBlock(cwd),
|
|
31
|
+
contextPromptBlock(cwd),
|
|
32
|
+
].filter(Boolean).join("\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function argSummary(name, args) {
|
|
36
|
+
if (!args) return "";
|
|
37
|
+
if (name === "run_bash") return args.command || "";
|
|
38
|
+
if (name === "search_files") return args.query || "";
|
|
39
|
+
if (name === "update_plan") return `${(args.plan || []).length} steps`;
|
|
40
|
+
if (args.path) return args.path;
|
|
41
|
+
if (args.name) return args.name;
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Parse tool-call arguments defensively (open models occasionally emit slightly-off JSON).
|
|
46
|
+
function parseArgs(raw) {
|
|
47
|
+
if (!raw) return {};
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(raw);
|
|
50
|
+
} catch {
|
|
51
|
+
try {
|
|
52
|
+
// light repair: strip trailing commas
|
|
53
|
+
return JSON.parse(raw.replace(/,\s*([}\]])/g, "$1"));
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function runAgent({ client, task, cwd, sessionDir, allowBash, preferredModel, preferredEffort, preferredSubagents, preferredOnchain, onModel, history, mcp, quiet = false, toolNames = null, isSubagent = false }) {
|
|
61
|
+
const q = !!quiet;
|
|
62
|
+
// "" means an explicit unset (use the model's own default); undefined falls back.
|
|
63
|
+
const effort = preferredEffort === "" ? null : (preferredEffort || CONFIG.effort);
|
|
64
|
+
const subOn = preferredSubagents !== undefined ? preferredSubagents : CONFIG.subagents;
|
|
65
|
+
const onchainOn = preferredOnchain !== undefined ? preferredOnchain : CONFIG.onchain;
|
|
66
|
+
const provDir = sessionDir || path.join(cwd, ".z0g");
|
|
67
|
+
const execute = makeExecutor({ cwd, allowBash, sessionDir: provDir, onchain: onchainOn });
|
|
68
|
+
// Restrict the toolset for subagents (read-only), and drop spawn_subagents when
|
|
69
|
+
// it is a subagent (no recursion) or the toggle is off. Drop on-chain tools when
|
|
70
|
+
// the on-chain toggle is off so the agent never proposes a gas-spending action.
|
|
71
|
+
let baseTools = toolNames ? TOOL_DEFS.filter((t) => toolNames.includes(t.function.name)) : TOOL_DEFS;
|
|
72
|
+
if (isSubagent || !subOn) baseTools = baseTools.filter((t) => t.function.name !== "spawn_subagents" && t.function.name !== "spawn_write_subagents");
|
|
73
|
+
// Write subagents edit files and run shell in worktrees, so require --auto.
|
|
74
|
+
if (!allowBash) baseTools = baseTools.filter((t) => t.function.name !== "spawn_write_subagents");
|
|
75
|
+
if (!onchainOn) baseTools = baseTools.filter((t) => t.function.name !== "upload_0g_storage" && t.function.name !== "deploy_0g_chain");
|
|
76
|
+
const toolSet = !isSubagent && mcp?.tools?.length ? [...baseTools, ...mcp.tools] : baseTools;
|
|
77
|
+
const models = modelChain(preferredModel);
|
|
78
|
+
const prov = makeProvenance(provDir);
|
|
79
|
+
// A per-run id groups this turn's edits so `z0g undo` reverts them together.
|
|
80
|
+
const runId = "r" + Date.now().toString(36) + Math.floor(Math.random() * 46656).toString(36);
|
|
81
|
+
const messages = history && history.length
|
|
82
|
+
? [...history, { role: "user", content: task }]
|
|
83
|
+
: [{ role: "system", content: systemPrompt(cwd) }, { role: "user", content: task }];
|
|
84
|
+
|
|
85
|
+
const recent = []; // circuit breaker on repeated identical tool calls
|
|
86
|
+
const failCounts = {}; // per-tool failure counter, drives model escalation
|
|
87
|
+
let escalate = false;
|
|
88
|
+
let activeModel = models[0];
|
|
89
|
+
const usageTotal = { prompt: 0, completion: 0, total: 0 };
|
|
90
|
+
let finalText = "";
|
|
91
|
+
|
|
92
|
+
for (let step = 0; step < CONFIG.maxSteps; step++) {
|
|
93
|
+
// Choose model order: escalate a stuck turn to a stronger fallback instead of looping.
|
|
94
|
+
let order;
|
|
95
|
+
if (escalate) {
|
|
96
|
+
const stronger = models.find((m) => m !== activeModel) || activeModel;
|
|
97
|
+
order = [stronger, ...models.filter((m) => m !== stronger)];
|
|
98
|
+
if (!q) console.log(" " + ui.warn((ui.uiTTY ? "▲" : "!") + " escalating to " + stronger));
|
|
99
|
+
escalate = false;
|
|
100
|
+
} else {
|
|
101
|
+
order = [activeModel, ...models.filter((m) => m !== activeModel)];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!q) ui.thinking(order[0]);
|
|
105
|
+
let out;
|
|
106
|
+
let mdOut = null;
|
|
107
|
+
try {
|
|
108
|
+
out = await completeStream(client, {
|
|
109
|
+
models: order,
|
|
110
|
+
messages,
|
|
111
|
+
tools: toolSet,
|
|
112
|
+
effort,
|
|
113
|
+
onDelta: q
|
|
114
|
+
? undefined
|
|
115
|
+
: (t) => {
|
|
116
|
+
if (!mdOut) {
|
|
117
|
+
ui.clearThinking();
|
|
118
|
+
mdOut = ui.assistantStream(); // render the answer as markdown, line by line
|
|
119
|
+
}
|
|
120
|
+
mdOut.push(t);
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
} catch (e) {
|
|
124
|
+
if (!q) {
|
|
125
|
+
ui.clearThinking();
|
|
126
|
+
ui.error(`All 0G models failed: ${e.message}`);
|
|
127
|
+
}
|
|
128
|
+
usageTotal.total = usageTotal.prompt + usageTotal.completion;
|
|
129
|
+
return { ok: false, steps: step, messages, finalText, usageTotal };
|
|
130
|
+
}
|
|
131
|
+
if (!q) ui.clearThinking();
|
|
132
|
+
if (mdOut) mdOut.end();
|
|
133
|
+
activeModel = out.model;
|
|
134
|
+
if (onModel) onModel(out.model);
|
|
135
|
+
if (out.usage) {
|
|
136
|
+
usageTotal.prompt += out.usage.prompt_tokens || out.usage.input_tokens || 0;
|
|
137
|
+
usageTotal.completion += out.usage.completion_tokens || out.usage.output_tokens || 0;
|
|
138
|
+
}
|
|
139
|
+
const msg = out.message;
|
|
140
|
+
messages.push(msg);
|
|
141
|
+
|
|
142
|
+
const toolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
|
|
143
|
+
if (toolCalls.length === 0) {
|
|
144
|
+
finalText = (msg.content || "").trim();
|
|
145
|
+
if (!q) {
|
|
146
|
+
if (!mdOut) ui.assistant(msg.content || "(done)");
|
|
147
|
+
ui.hud(out.model, out.usage, effort);
|
|
148
|
+
}
|
|
149
|
+
usageTotal.total = usageTotal.prompt + usageTotal.completion;
|
|
150
|
+
return { ok: true, steps: step + 1, changes: prov.count(), messages, finalText, usageTotal };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const tc of toolCalls) {
|
|
154
|
+
const name = tc.function?.name;
|
|
155
|
+
const args = parseArgs(tc.function?.arguments);
|
|
156
|
+
if (args === null) {
|
|
157
|
+
if (!q) { ui.toolCall(name, "(bad args)"); ui.toolResult(false, "invalid JSON arguments"); }
|
|
158
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: "ERROR: could not parse JSON arguments. Re-emit the tool call with valid JSON." });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const key = `${name}:${JSON.stringify(args)}`;
|
|
163
|
+
recent.push(key);
|
|
164
|
+
if (recent.length > 6) recent.shift();
|
|
165
|
+
if (recent.filter((k) => k === key).length >= 3) {
|
|
166
|
+
if (!q) ui.toolResult(false, "loop detected, stopping");
|
|
167
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: "STOP: you called this exact tool 3 times. Change approach or finish." });
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Parallel subagents: routed here, not through the normal executor.
|
|
172
|
+
if (name === "spawn_subagents") {
|
|
173
|
+
if (isSubagent) {
|
|
174
|
+
if (!q) { ui.toolCall(name, ""); ui.toolResult(false, "subagents cannot spawn subagents"); }
|
|
175
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: "ERROR: a subagent cannot spawn subagents." });
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const subtasks = (Array.isArray(args.tasks) ? args.tasks.filter((t) => t && typeof t.prompt === "string" && t.prompt.trim()) : []).slice(0, 12);
|
|
179
|
+
if (!subtasks.length) {
|
|
180
|
+
if (!q) { ui.toolCall(name, ""); ui.toolResult(false, "no tasks"); }
|
|
181
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: "ERROR: provide tasks: [{ prompt }]." });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (!q) ui.subagentsStart(subtasks.length);
|
|
185
|
+
const subResults = await runSubagents({
|
|
186
|
+
client, tasks: subtasks, cwd, sessionDir: provDir,
|
|
187
|
+
preferredModel, preferredEffort,
|
|
188
|
+
onOne: q ? null : (r) => ui.subagentOne(r),
|
|
189
|
+
});
|
|
190
|
+
if (!q) ui.subagentsSummary(subResults);
|
|
191
|
+
// Bound the tool result so a big fan-out can't overflow the parent context.
|
|
192
|
+
// Full untruncated output stays in the saved per-subagent transcripts.
|
|
193
|
+
const PER_SUB = 8000;
|
|
194
|
+
const TOTAL_SUB = 40000;
|
|
195
|
+
const clip = (s, n) => (s && s.length > n ? s.slice(0, n) + "\n... [truncated]" : (s || ""));
|
|
196
|
+
let combined = subResults.map((r) => `## ${r.label} (${r.ok ? "ok" : "failed"})\n${clip(r.summary, PER_SUB)}`).join("\n\n");
|
|
197
|
+
if (combined.length > TOTAL_SUB) combined = combined.slice(0, TOTAL_SUB) + "\n... [more subagent output truncated; see .z0g/sessions/<id>/subagents/]";
|
|
198
|
+
const totalTok = subResults.reduce((a, r) => a + (r.tokens || 0), 0);
|
|
199
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: `Subagent results (${subResults.length} agents, ${totalTok} tokens):\n\n${combined}` });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Parallel WRITE subagents: each edits in an isolated git worktree, then
|
|
204
|
+
// its diff is merged back into the main tree.
|
|
205
|
+
if (name === "spawn_write_subagents") {
|
|
206
|
+
if (isSubagent) {
|
|
207
|
+
if (!q) { ui.toolCall(name, ""); ui.toolResult(false, "subagents cannot spawn subagents"); }
|
|
208
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: "ERROR: a subagent cannot spawn subagents." });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const subtasks = (Array.isArray(args.tasks) ? args.tasks.filter((t) => t && typeof t.prompt === "string" && t.prompt.trim()) : []).slice(0, 8);
|
|
212
|
+
if (!subtasks.length) {
|
|
213
|
+
if (!q) { ui.toolCall(name, ""); ui.toolResult(false, "no tasks"); }
|
|
214
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: "ERROR: provide tasks: [{ prompt }]." });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (!q) ui.subagentsStart(subtasks.length, "write · isolated git worktrees");
|
|
218
|
+
const out = await runWriteSubagents({
|
|
219
|
+
client, tasks: subtasks, cwd, sessionDir: provDir,
|
|
220
|
+
preferredModel, preferredEffort,
|
|
221
|
+
onOne: q ? null : (r) => ui.subagentOne(r),
|
|
222
|
+
});
|
|
223
|
+
if (!out.ok) {
|
|
224
|
+
if (!q) ui.toolResult(false, out.error);
|
|
225
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: "ERROR: " + out.error });
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const wr = out.results;
|
|
229
|
+
// Record checkpoints for merged edits so `z0g undo` reverts them too.
|
|
230
|
+
for (const r of wr) {
|
|
231
|
+
for (const ch of (r.changes || [])) {
|
|
232
|
+
await recordCheckpoint(provDir, {
|
|
233
|
+
runId, ts: new Date().toISOString(),
|
|
234
|
+
task: typeof task === "string" ? task.slice(0, 80) : "",
|
|
235
|
+
path: ch.path, before: ch.before, after: ch.after,
|
|
236
|
+
created: !!ch.created, tool: "spawn_write_subagents", model: activeModel,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (!q) ui.subagentsSummary(wr);
|
|
241
|
+
const PER = 6000, TOTAL = 40000;
|
|
242
|
+
const clip = (s, n) => (s && s.length > n ? s.slice(0, n) + "\n... [truncated]" : (s || ""));
|
|
243
|
+
const applied = wr.filter((r) => r.applied);
|
|
244
|
+
let combined = wr.map((r) => `## ${r.label} (${r.ok ? "ok" : "failed"}${r.applied ? ", merged" : ", " + (r.applyReason || "not merged")})\nfiles: ${r.files.join(", ") || "none"}\n${clip(r.summary, PER)}`).join("\n\n");
|
|
245
|
+
if (combined.length > TOTAL) combined = combined.slice(0, TOTAL) + "\n... [more output truncated; see .z0g/sessions/<id>/subagents/]";
|
|
246
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: `Write-subagent results (${wr.length} agents, ${applied.length} merged into the working tree):\n\n${combined}` });
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!q) ui.toolCall(name, argSummary(name, args));
|
|
251
|
+
const res = mcp && mcp.isMcp(name) ? await mcp.call(name, args) : await execute(name, args);
|
|
252
|
+
if (!q) ui.toolResult(res.ok, res.summary);
|
|
253
|
+
|
|
254
|
+
if (res.ok) {
|
|
255
|
+
failCounts[name] = 0;
|
|
256
|
+
if (!q && res.plan) ui.renderPlan(res.plan);
|
|
257
|
+
if (res.change) {
|
|
258
|
+
if (!q) {
|
|
259
|
+
const diff = ui.renderDiff(res.change.before, res.change.after);
|
|
260
|
+
if (diff) console.log(diff);
|
|
261
|
+
}
|
|
262
|
+
await prov.record({
|
|
263
|
+
pathRel: res.change.path,
|
|
264
|
+
before: res.change.before,
|
|
265
|
+
after: res.change.after,
|
|
266
|
+
model: out.model,
|
|
267
|
+
responseId: out.responseId,
|
|
268
|
+
trace: out.trace,
|
|
269
|
+
});
|
|
270
|
+
if (!isSubagent) {
|
|
271
|
+
await recordCheckpoint(provDir, {
|
|
272
|
+
runId, ts: new Date().toISOString(),
|
|
273
|
+
task: typeof task === "string" ? task.slice(0, 80) : "",
|
|
274
|
+
path: res.change.path, before: res.change.before, after: res.change.after,
|
|
275
|
+
created: !!res.change.created, tool: name, model: out.model,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
failCounts[name] = (failCounts[name] || 0) + 1;
|
|
281
|
+
if (failCounts[name] >= 2) escalate = true; // stuck on this tool: try a stronger model
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: String(res.content ?? (res.ok ? "OK" : "ERROR")) });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!q) ui.error(`Reached max steps (${CONFIG.maxSteps}).`);
|
|
289
|
+
usageTotal.total = usageTotal.prompt + usageTotal.completion;
|
|
290
|
+
return { ok: false, steps: CONFIG.maxSteps, changes: prov.count(), messages, finalText, usageTotal };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Run independent read-only subtasks in parallel (capped), each as an isolated
|
|
294
|
+
// subagent. Returns [{ label, ok, summary, tokens }]. Subagents cannot write,
|
|
295
|
+
// run shell, or spawn further subagents. Transcripts are saved per subagent.
|
|
296
|
+
const SUBAGENT_TOOLS = ["read_file", "search_files", "list_dir", "read_skill", "update_plan"];
|
|
297
|
+
|
|
298
|
+
export async function runSubagents({ client, tasks, cwd, sessionDir, preferredModel, preferredEffort, onOne }) {
|
|
299
|
+
const cap = Math.max(1, CONFIG.maxParallel);
|
|
300
|
+
const results = new Array(tasks.length);
|
|
301
|
+
// Each subagent gets its OWN directory so its plan/provenance stay isolated
|
|
302
|
+
// from the parent and from siblings; number after any existing subdirs so a
|
|
303
|
+
// second fan-out does not clobber the first. base is computed once up front.
|
|
304
|
+
const rootDir = sessionDir ? path.join(sessionDir, "subagents") : null;
|
|
305
|
+
let base = 0;
|
|
306
|
+
if (rootDir) {
|
|
307
|
+
try {
|
|
308
|
+
await fs.mkdir(rootDir, { recursive: true });
|
|
309
|
+
base = (await fs.readdir(rootDir)).filter((n) => /^\d+$/.test(n)).length;
|
|
310
|
+
} catch {}
|
|
311
|
+
}
|
|
312
|
+
let next = 0;
|
|
313
|
+
const runOne = async (i) => {
|
|
314
|
+
const t = tasks[i];
|
|
315
|
+
const label = (t.label && String(t.label).trim()) || `subagent ${i + 1}`;
|
|
316
|
+
const subDir = rootDir ? path.join(rootDir, String(base + i + 1)) : undefined;
|
|
317
|
+
let res = null;
|
|
318
|
+
try {
|
|
319
|
+
res = await runAgent({
|
|
320
|
+
client, task: t.prompt, cwd, sessionDir: subDir,
|
|
321
|
+
allowBash: false, preferredModel, preferredEffort,
|
|
322
|
+
quiet: true, toolNames: SUBAGENT_TOOLS, isSubagent: true,
|
|
323
|
+
});
|
|
324
|
+
results[i] = { label, ok: !!res.ok, summary: res.finalText || "(no summary)", tokens: res.usageTotal?.total || 0 };
|
|
325
|
+
} catch (e) {
|
|
326
|
+
results[i] = { label, ok: false, summary: "error: " + e.message, tokens: 0 };
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
if (subDir) {
|
|
330
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
331
|
+
await fs.writeFile(path.join(subDir, "transcript.json"), JSON.stringify({ task: t, result: results[i], messages: res?.messages || [] }, null, 2), "utf8");
|
|
332
|
+
}
|
|
333
|
+
} catch {}
|
|
334
|
+
if (onOne) onOne(results[i]);
|
|
335
|
+
};
|
|
336
|
+
const pool = async () => {
|
|
337
|
+
while (next < tasks.length) {
|
|
338
|
+
const i = next++;
|
|
339
|
+
await runOne(i);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
const workers = [];
|
|
343
|
+
for (let w = 0; w < Math.min(cap, tasks.length); w++) workers.push(pool());
|
|
344
|
+
await Promise.all(workers);
|
|
345
|
+
return results;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Run independent WRITE subtasks in parallel, each in its own git worktree, then
|
|
349
|
+
// apply each worktree's diff back to the main tree (non-overlapping edits merge;
|
|
350
|
+
// overlapping files are reported and skipped). Returns { ok, error?, results }.
|
|
351
|
+
// Each result: { label, ok, summary, files, applied, applyReason, changes, tokens }.
|
|
352
|
+
const WRITE_SUBAGENT_TOOLS = ["read_file", "search_files", "list_dir", "read_skill", "update_plan", "write_file", "edit_file", "run_bash"];
|
|
353
|
+
|
|
354
|
+
export async function runWriteSubagents({ client, tasks, cwd, sessionDir, preferredModel, preferredEffort, onOne }) {
|
|
355
|
+
if (!(await isGitRepo(cwd))) {
|
|
356
|
+
return { ok: false, error: "Write subagents require a git repository. Run `git init` and make a commit first." };
|
|
357
|
+
}
|
|
358
|
+
const rootDir = sessionDir ? path.join(sessionDir, "subagents") : null;
|
|
359
|
+
let base = 0;
|
|
360
|
+
if (rootDir) {
|
|
361
|
+
try { await fs.mkdir(rootDir, { recursive: true }); base = (await fs.readdir(rootDir)).filter((n) => /^\d+$/.test(n)).length; } catch {}
|
|
362
|
+
}
|
|
363
|
+
const stamp = Date.now().toString(36);
|
|
364
|
+
|
|
365
|
+
// 1) Create a worktree per subtask (sequential: git index is single-writer).
|
|
366
|
+
const wts = new Array(tasks.length);
|
|
367
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
368
|
+
try { wts[i] = await addWorktree(cwd, `${stamp}-${base + i + 1}`); }
|
|
369
|
+
catch (e) { wts[i] = { error: e.message }; }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 2) Run each subagent in its worktree, in parallel (pooled), and collect diffs.
|
|
373
|
+
const cap = Math.max(1, CONFIG.maxParallel);
|
|
374
|
+
const results = new Array(tasks.length);
|
|
375
|
+
let next = 0;
|
|
376
|
+
const runOne = async (i) => {
|
|
377
|
+
const t = tasks[i];
|
|
378
|
+
const label = (t.label && String(t.label).trim()) || `write-subagent ${i + 1}`;
|
|
379
|
+
const wt = wts[i];
|
|
380
|
+
if (!wt || wt.error) {
|
|
381
|
+
results[i] = { label, ok: false, summary: "worktree failed: " + (wt?.error || "unknown"), files: [], patch: "", tokens: 0 };
|
|
382
|
+
if (onOne) onOne(results[i]); return;
|
|
383
|
+
}
|
|
384
|
+
const subDir = rootDir ? path.join(rootDir, String(base + i + 1)) : undefined;
|
|
385
|
+
let res = null, patch = "", files = [];
|
|
386
|
+
try {
|
|
387
|
+
res = await runAgent({
|
|
388
|
+
client, task: t.prompt, cwd: wt.wtPath, sessionDir: subDir,
|
|
389
|
+
allowBash: true, preferredModel, preferredEffort,
|
|
390
|
+
quiet: true, toolNames: WRITE_SUBAGENT_TOOLS, isSubagent: true,
|
|
391
|
+
});
|
|
392
|
+
({ patch, files } = await collectPatch(wt.wtPath));
|
|
393
|
+
results[i] = { label, ok: !!res.ok, summary: res.finalText || "(no summary)", files, patch, tokens: res.usageTotal?.total || 0 };
|
|
394
|
+
} catch (e) {
|
|
395
|
+
results[i] = { label, ok: false, summary: "error: " + e.message, files: [], patch: "", tokens: 0 };
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
if (subDir) {
|
|
399
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
400
|
+
await fs.writeFile(path.join(subDir, "transcript.json"), JSON.stringify({ task: t, result: { ...results[i], patch: undefined }, messages: res?.messages || [] }, null, 2), "utf8");
|
|
401
|
+
}
|
|
402
|
+
} catch {}
|
|
403
|
+
try { await removeWorktree(cwd, wt.wtPath, wt.branch); } catch {}
|
|
404
|
+
if (onOne) onOne(results[i]);
|
|
405
|
+
};
|
|
406
|
+
const pool = async () => { while (next < tasks.length) { const i = next++; await runOne(i); } };
|
|
407
|
+
const workers = [];
|
|
408
|
+
for (let w = 0; w < Math.min(cap, tasks.length); w++) workers.push(pool());
|
|
409
|
+
await Promise.all(workers);
|
|
410
|
+
|
|
411
|
+
// 3) Apply each diff to the main tree in order, capturing before/after so the
|
|
412
|
+
// parent can checkpoint them (keeps `z0g undo` working for merged writes).
|
|
413
|
+
for (const r of results) {
|
|
414
|
+
if (!r.patch || !r.patch.trim()) { r.applied = false; r.applyReason = "no changes"; r.changes = []; delete r.patch; continue; }
|
|
415
|
+
const pre = [];
|
|
416
|
+
for (const f of r.files) {
|
|
417
|
+
let before = "", existed = true;
|
|
418
|
+
try { before = await fs.readFile(path.join(cwd, f), "utf8"); } catch { before = ""; existed = false; }
|
|
419
|
+
pre.push({ path: f, before, existed });
|
|
420
|
+
}
|
|
421
|
+
const a = await applyPatch(cwd, r.patch);
|
|
422
|
+
r.applied = a.ok; r.applyReason = a.reason;
|
|
423
|
+
r.changes = [];
|
|
424
|
+
if (a.ok) {
|
|
425
|
+
for (const p of pre) {
|
|
426
|
+
let after = "";
|
|
427
|
+
try { after = await fs.readFile(path.join(cwd, p.path), "utf8"); } catch { after = ""; }
|
|
428
|
+
if (after !== p.before) r.changes.push({ path: p.path, before: p.before, after, created: !p.existed && after !== "" });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
delete r.patch;
|
|
432
|
+
}
|
|
433
|
+
return { ok: true, results };
|
|
434
|
+
}
|
package/src/anchor.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// On-chain anchoring on 0G: upload a file to 0G Storage and anchor a hash on
|
|
2
|
+
// 0G Chain. Needs a funded ZOG_WALLET_KEY (a 0G mainnet private key).
|
|
3
|
+
const rpc = () => process.env.ZOG_EVM_RPC || "https://evmrpc.0g.ai";
|
|
4
|
+
const indexerUrl = () => process.env.ZOG_STORAGE_INDEXER || "https://indexer-storage-turbo.0g.ai";
|
|
5
|
+
|
|
6
|
+
function requireKey() {
|
|
7
|
+
const key = process.env.ZOG_WALLET_KEY;
|
|
8
|
+
if (!key) throw new Error("Set ZOG_WALLET_KEY to a funded 0G mainnet private key.");
|
|
9
|
+
return key;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Upload a file to 0G Storage. Returns { rootHash, txHash }.
|
|
13
|
+
export async function uploadFileToStorage(absPath) {
|
|
14
|
+
const key = requireKey();
|
|
15
|
+
const { ethers } = await import("ethers");
|
|
16
|
+
const { ZgFile, Indexer } = await import("@0gfoundation/0g-storage-ts-sdk");
|
|
17
|
+
const provider = new ethers.JsonRpcProvider(rpc());
|
|
18
|
+
const wallet = new ethers.Wallet(key, provider);
|
|
19
|
+
const indexer = new Indexer(indexerUrl());
|
|
20
|
+
const file = await ZgFile.fromFilePath(absPath);
|
|
21
|
+
try {
|
|
22
|
+
const [tree, treeErr] = await file.merkleTree();
|
|
23
|
+
if (treeErr) throw new Error(String(treeErr));
|
|
24
|
+
const root = tree.rootHash();
|
|
25
|
+
const [res, upErr] = await indexer.upload(file, rpc(), wallet);
|
|
26
|
+
if (upErr) throw new Error(upErr.message || String(upErr));
|
|
27
|
+
return { rootHash: res?.rootHash || root, txHash: res?.txHash || res };
|
|
28
|
+
} finally {
|
|
29
|
+
await file.close();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Download a file from 0G Storage by its content root, then verify the bytes
|
|
34
|
+
// by recomputing the Merkle root and confirming it matches. Reading is free
|
|
35
|
+
// (no wallet needed). Returns absOut. Throws on download error or root mismatch.
|
|
36
|
+
export async function downloadAndVerify(root, absOut) {
|
|
37
|
+
const { ZgFile, Indexer } = await import("@0gfoundation/0g-storage-ts-sdk");
|
|
38
|
+
const indexer = new Indexer(indexerUrl());
|
|
39
|
+
const err = await indexer.download(root, absOut, true);
|
|
40
|
+
if (err) throw new Error(err.message || String(err));
|
|
41
|
+
const file = await ZgFile.fromFilePath(absOut);
|
|
42
|
+
try {
|
|
43
|
+
const [tree, treeErr] = await file.merkleTree();
|
|
44
|
+
if (treeErr) throw new Error(String(treeErr));
|
|
45
|
+
const got = tree.rootHash();
|
|
46
|
+
if (String(got).toLowerCase() !== String(root).toLowerCase()) {
|
|
47
|
+
throw new Error(`root mismatch: got ${got}, expected ${root}`);
|
|
48
|
+
}
|
|
49
|
+
} finally {
|
|
50
|
+
await file.close();
|
|
51
|
+
}
|
|
52
|
+
return absOut;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Anchor a hash on 0G Chain via a small memo transaction. Returns { txHash, block }.
|
|
56
|
+
export async function anchorOnChain(hash) {
|
|
57
|
+
const key = requireKey();
|
|
58
|
+
const { ethers } = await import("ethers");
|
|
59
|
+
const provider = new ethers.JsonRpcProvider(rpc());
|
|
60
|
+
const wallet = new ethers.Wallet(key, provider);
|
|
61
|
+
const data = "0x" + Buffer.from("z0gcode session " + hash, "utf8").toString("hex");
|
|
62
|
+
const tx = await wallet.sendTransaction({ to: wallet.address, value: 0n, data });
|
|
63
|
+
const receipt = await tx.wait();
|
|
64
|
+
return { txHash: tx.hash, block: receipt?.blockNumber };
|
|
65
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Checkpoints: an append-only log of every file edit (with full before/after
|
|
2
|
+
// content) so `z0g undo` can revert what the agent did. Grouped by runId so a
|
|
3
|
+
// single undo reverts a whole turn (which may touch several files).
|
|
4
|
+
import { promises as fs } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const FILE = (dir) => path.join(dir, "checkpoints.jsonl");
|
|
8
|
+
|
|
9
|
+
export async function recordCheckpoint(dir, entry) {
|
|
10
|
+
await fs.mkdir(dir, { recursive: true });
|
|
11
|
+
await fs.appendFile(FILE(dir), JSON.stringify(entry) + "\n", "utf8");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function readCheckpointLog(dir) {
|
|
15
|
+
try {
|
|
16
|
+
const raw = await fs.readFile(FILE(dir), "utf8");
|
|
17
|
+
return raw.split("\n").filter(Boolean)
|
|
18
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
} catch { return []; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Active edit turns (runId -> edits), newest last, excluding undone runs.
|
|
24
|
+
export function activeTurns(log) {
|
|
25
|
+
const undone = new Set(log.filter((e) => e.op === "undo").map((e) => e.runId));
|
|
26
|
+
const turns = new Map();
|
|
27
|
+
for (const e of log) {
|
|
28
|
+
if (e.op) continue; // undo markers carry no edit
|
|
29
|
+
if (undone.has(e.runId)) continue;
|
|
30
|
+
if (!turns.has(e.runId)) turns.set(e.runId, { runId: e.runId, ts: e.ts, task: e.task, edits: [] });
|
|
31
|
+
turns.get(e.runId).edits.push(e);
|
|
32
|
+
}
|
|
33
|
+
return [...turns.values()];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Revert the most recent active turn. Restores each file's `before` (or deletes
|
|
37
|
+
// files the turn created), in reverse edit order. Returns a report or null.
|
|
38
|
+
export async function undoLastTurn(cwd, dir) {
|
|
39
|
+
const log = await readCheckpointLog(dir);
|
|
40
|
+
const turns = activeTurns(log);
|
|
41
|
+
if (!turns.length) return null;
|
|
42
|
+
const turn = turns[turns.length - 1];
|
|
43
|
+
const files = [];
|
|
44
|
+
for (const e of [...turn.edits].reverse()) {
|
|
45
|
+
const abs = path.resolve(cwd, e.path);
|
|
46
|
+
let cur = null;
|
|
47
|
+
try { cur = await fs.readFile(abs, "utf8"); } catch { cur = null; }
|
|
48
|
+
const diverged = cur !== null && cur !== e.after; // changed since the agent left it
|
|
49
|
+
try {
|
|
50
|
+
if (e.created) {
|
|
51
|
+
await fs.rm(abs, { force: true });
|
|
52
|
+
files.push({ path: e.path, action: "deleted", diverged });
|
|
53
|
+
} else {
|
|
54
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
55
|
+
await fs.writeFile(abs, e.before ?? "", "utf8");
|
|
56
|
+
files.push({ path: e.path, action: "restored", diverged });
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
files.push({ path: e.path, action: "failed", error: err.message });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
await recordCheckpoint(dir, { op: "undo", runId: turn.runId, ts: new Date().toISOString() });
|
|
63
|
+
return { runId: turn.runId, ts: turn.ts, task: turn.task, files };
|
|
64
|
+
}
|