z0gcode 0.2.0 → 0.3.1
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/README.md +8 -6
- package/bin/z0g.mjs +59 -8
- package/package.json +1 -1
- package/src/agent.mjs +32 -5
- package/src/config.mjs +9 -0
- package/src/goal.mjs +2 -2
- package/src/tools.mjs +24 -3
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
</p>
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
|
+
<a href="https://www.npmjs.com/package/z0gcode"><img src="https://img.shields.io/npm/v/z0gcode?color=A78BFF&label=npm" alt="npm"></a>
|
|
14
15
|
<img src="https://img.shields.io/badge/license-MIT-A78BFF" alt="MIT license">
|
|
15
16
|
<img src="https://img.shields.io/badge/node-18%2B-6B7080" alt="Node 18+">
|
|
16
17
|
<img src="https://img.shields.io/badge/brain-0G_Compute_TEE-A78BFF" alt="0G Compute">
|
|
@@ -60,7 +61,7 @@ npm install && npm link # or run directly with: node bin/z0g.mjs
|
|
|
60
61
|
|
|
61
62
|
```bash
|
|
62
63
|
z0g "add a /health endpoint to server.js and test it" # one-shot task
|
|
63
|
-
z0g --auto "scaffold a Fastify app and run it" #
|
|
64
|
+
z0g --auto "scaffold a Fastify app and run it" # auto mode: runs commands without asking (else it asks you first)
|
|
64
65
|
z0g goal --auto "make the failing tests pass" # iterate until a verify command passes
|
|
65
66
|
z0g --continue "now add input validation" # resume the most recent chat
|
|
66
67
|
z0g --resume # pick a chat to resume (arrow-key picker)
|
|
@@ -74,21 +75,22 @@ z0g transcribe memo.mp3 # transcribe audio on 0
|
|
|
74
75
|
z0g serve --mcp # expose z0gcode's 0G tools over MCP
|
|
75
76
|
```
|
|
76
77
|
|
|
77
|
-
**In the REPL**, type `/` then **Tab** to autocomplete slash commands: `/chats`, `/new`, `/rename`, `/init`, `/goal`, `/model`, `/effort`, `/subagents`, `/onchain`, `/skills`, `/attest`, `/share`, `/pull`, `/mint`, `/plan`, `/verify`, `/clear`, `/help`, `/exit`. `/chats` opens an arrow-key session picker (type to search, `ctrl-r` rename, `ctrl-x` delete); `/new [title]` starts a chat and `/rename <title>` renames the current one. `/model` opens the model picker (saved to `~/.z0gcode/settings.json`); `/effort low|medium|high` (or `default`) tunes reasoning depth vs speed and cost; `/subagents on|off` toggles parallel subagents; `/onchain on|off` toggles gas-spending on-chain actions (off by default); `/skills` lists and toggles your skills; `/share [anchor]` exports the session (encrypted) to 0G Storage (and anchors it on 0G Chain), and `/pull <root>` fetches, verifies, and decrypts one back. A short intro animation and a "thinking on 0G" indicator play on a color TTY; set `Z0G_NO_ANIM=1` to disable. Each turn is separated by a divider carrying a running session token and cost counter.
|
|
78
|
+
**In the REPL**, type `/` then **Tab** to autocomplete slash commands: `/chats`, `/new`, `/rename`, `/init`, `/goal`, `/mode`, `/model`, `/effort`, `/subagents`, `/onchain`, `/skills`, `/attest`, `/share`, `/pull`, `/mint`, `/plan`, `/verify`, `/clear`, `/help`, `/exit`. `/chats` opens an arrow-key session picker (type to search, `ctrl-r` rename, `ctrl-x` delete); `/new [title]` starts a chat and `/rename <title>` renames the current one. `/mode ask|auto|plan` switches the permission mode; `/model` opens the model picker (saved to `~/.z0gcode/settings.json`); `/effort low|medium|high` (or `default`) tunes reasoning depth vs speed and cost; `/subagents on|off` toggles parallel subagents; `/onchain on|off` toggles gas-spending on-chain actions (off by default); `/escalate on|off` toggles switching to a stronger model on repeated failures; `/skills` lists and toggles your skills; `/share [anchor]` exports the session (encrypted) to 0G Storage (and anchors it on 0G Chain), and `/pull <root>` fetches, verifies, and decrypts one back. A short intro animation and a "thinking on 0G" indicator play on a color TTY; set `Z0G_NO_ANIM=1` to disable. Each turn is separated by a divider carrying a running session token and cost counter.
|
|
78
79
|
|
|
79
80
|
**Options:** `--auto`, `--onchain`, `--continue`, `--resume`, `--new`, `--model <id>`, `--effort low|medium|high`, `--no-subagents`, `--verify "<cmd>"`, `--auto-verify`, `--max-steps <n>`, `--cwd <dir>`, and `--json` (with `models`).
|
|
80
81
|
|
|
81
82
|
## Features
|
|
82
83
|
|
|
83
84
|
**The agent**
|
|
84
|
-
- Agentic loop with tools: `search_files` (regex + glob), `read_file`, `write_file`, `edit_file`, `list_dir`, `run_bash` (gated by
|
|
85
|
+
- Agentic loop with tools: `search_files` (regex + glob), `read_file`, `write_file`, `edit_file`, `list_dir`, `run_bash` (gated by the permission mode below), `update_plan`, and `read_skill`.
|
|
86
|
+
- **Permission modes** (`/mode`, like Claude Code): **ask** (default) prompts before each shell command and on-chain action (`y` / `n` / `a` = always), and "always" choices are remembered in `~/.z0gcode/settings.json` so it never asks again for that program; **auto** (`--auto`) runs without asking; **plan** is read-only, the agent explores and proposes a plan and touches nothing until you switch to ask or auto.
|
|
85
87
|
- Colored diffs for every change, an inference HUD (tokens, answering model, `0G Compute (TEE)`), and a visible planning checklist on multi-step tasks.
|
|
86
88
|
- Streaming answers rendered as terminal markdown (bold, headings, lists, tables, inline code, and syntax-highlighted code blocks for JS/TS, Python, Solidity, Go, Rust, shell, JSON, and more); piped output stays raw and greppable.
|
|
87
89
|
- Multiple chats per project, each isolating its own history, plan, and provenance under `.z0g/sessions/`. On open, an arrow-key picker (with search, rename, delete) resumes a chat; `--continue` resumes the most recent, `--resume` shows the picker, `/chats` switches mid-session. Plus a goal loop (`z0g goal` re-runs until a verify command passes) and auto-verify.
|
|
88
90
|
- **Project context**: `AGENTS.md` (and `.z0g/context.md`) are auto-loaded into the agent's system prompt on every run, so it follows your conventions and uses your real build/test/run commands. `z0g init` (or `/init`) analyzes the repo and writes an accurate `AGENTS.md` for you.
|
|
89
91
|
- **Checkpoints and undo**: every file edit is logged with its before/after content per turn, so `z0g undo` (or `/undo`) reverts the last turn's changes (restoring files, deleting ones it created); `z0g checkpoints` lists what you can step back through.
|
|
90
92
|
- **Custom commands and hooks**: drop `.z0g/commands/<name>.md` to add a `/<name>` slash command (the file is a prompt template; `$ARGUMENTS` is substituted, optional `description:` frontmatter, Tab-completed and listed by `/commands`). Define lifecycle hooks in `.z0g/hooks.json` (`preRun` / `postRun` shell commands, e.g. run your formatter or tests after each turn); hooks run shell, so they only fire with `--auto`.
|
|
91
|
-
- Reliability on a decentralized backend: app-level multi-model fallback, retry and backoff, tool-JSON repair, a loop breaker, and model escalation.
|
|
93
|
+
- Reliability on a decentralized backend: app-level multi-model fallback, retry and backoff, tool-JSON repair, a loop breaker (stops on repeated identical calls, or the same tool result with no progress, so it hands control back instead of spinning), and model escalation (when a tool fails repeatedly it moves to a stronger fallback for the rest of the turn; on by default after 3 failures, toggle with `/escalate on|off` or `--no-escalate`).
|
|
92
94
|
- **Parallel subagents**: `spawn_subagents` fans out independent, read-only subtasks (review many files, research, audit, map a codebase) as isolated agents running in parallel, capped by `ZOG_MAX_PARALLEL`. The parent synthesizes the results and each subagent's transcript is saved. With 0G's cheap inference, fanning out to many agents costs cents: massively parallel agents at 0G prices. On by default; toggle with `/subagents on|off` or `--no-subagents`.
|
|
93
95
|
- **Parallel write subagents**: with `--auto` in a git repo, `spawn_write_subagents` fans out subtasks that WRITE code, each in its own isolated **git worktree**, then merges every diff back into the working tree. Disjoint file sets merge automatically; overlapping edits are reported and skipped (never half-applied), and the merged changes are checkpointed so `z0g undo` reverts them. Verified end-to-end on 0G.
|
|
94
96
|
|
|
@@ -117,7 +119,7 @@ Every on-chain action, storage upload, chain deploy, the encrypted session `shar
|
|
|
117
119
|
|
|
118
120
|
## Roadmap
|
|
119
121
|
|
|
120
|
-
Shipped: streaming with markdown rendering, multiple chat sessions per project (resume picker with search), planning, slash commands, the goal loop and auto-verify, in-agent `deploy_0g_chain` and `upload_0g_storage` (mainnet-verified, opt-in), the private verifiable session (`z0g share`, wallet-encrypted to 0G Storage + `--anchor` on 0G Chain, and `z0g pull` to fetch, verify, and decrypt, mainnet-verified), the session INFT (`z0g mint`, mainnet-verified), parallel subagents, media on 0G (image + transcription), MCP both ways, the model catalog and arrow-key picker, and
|
|
122
|
+
Shipped: streaming with markdown rendering, multiple chat sessions per project (resume picker with search), planning, slash commands, the goal loop and auto-verify, in-agent `deploy_0g_chain` and `upload_0g_storage` (mainnet-verified, opt-in), the private verifiable session (`z0g share`, wallet-encrypted to 0G Storage + `--anchor` on 0G Chain, and `z0g pull` to fetch, verify, and decrypt, mainnet-verified), the session INFT (`z0g mint`, mainnet-verified), parallel subagents, media on 0G (image + transcription), MCP both ways, the model catalog and arrow-key picker, user skills, and a published npm package (`npm i -g z0gcode`).
|
|
121
123
|
|
|
122
124
|
Next:
|
|
123
125
|
- **Share a session, three ways.** Today `z0g share` is a *private* backup: the bundle is encrypted to your wallet, so only you can `pull` it. The plan keeps that and adds two ways to actually hand a session to someone, without ever trusting the storage layer with a key:
|
|
@@ -128,7 +130,7 @@ Next:
|
|
|
128
130
|
Together these are the on-ramp to full ERC-7857 authorized users.
|
|
129
131
|
- Full ERC-7857 (encrypted metadata + oracle transfer) on top of the current session INFT.
|
|
130
132
|
- Full TEE-quote verification of the provenance manifest (not just model + response id).
|
|
131
|
-
-
|
|
133
|
+
- A shareable starter pack of user skills.
|
|
132
134
|
|
|
133
135
|
## Development
|
|
134
136
|
|
package/bin/z0g.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from "../src/sessions.mjs";
|
|
20
20
|
import { loadPlan } from "../src/plan.mjs";
|
|
21
21
|
import { loadMcp } from "../src/mcp.mjs";
|
|
22
|
-
import { saveSetting } from "../src/settings.mjs";
|
|
22
|
+
import { saveSetting, loadSettings } from "../src/settings.mjs";
|
|
23
23
|
import { fetchModels, orderChatModels } from "../src/models-info.mjs";
|
|
24
24
|
import { discoverSkills, setSkillEnabled } from "../src/user-skills.mjs";
|
|
25
25
|
import { generateImage, transcribeAudio } from "../src/media.mjs";
|
|
@@ -54,12 +54,13 @@ function helpText() {
|
|
|
54
54
|
["z0g serve --mcp", "Expose z0gcode's 0G tools as an MCP server"],
|
|
55
55
|
]],
|
|
56
56
|
["Options", [
|
|
57
|
-
["--auto", "
|
|
57
|
+
["--auto", "Start in auto mode (run commands without asking; else /mode ask prompts)"],
|
|
58
58
|
["--onchain", "Allow gas-spending on-chain actions (off by default)"],
|
|
59
59
|
["--continue", "Continue the saved session in this directory"],
|
|
60
60
|
["--model <id>", "Override the model (default " + CONFIG.model + ")"],
|
|
61
61
|
["--effort <l>", "Reasoning effort: low, medium, high (default: model's own)"],
|
|
62
62
|
["--no-subagents", "Disable parallel subagents for this run"],
|
|
63
|
+
["--no-escalate", "Do not switch to a stronger model on repeated tool failures"],
|
|
63
64
|
['--verify "<cmd>"', "Run, then verify and self-correct with this command"],
|
|
64
65
|
["--auto-verify", "Same, auto-detecting the verify command"],
|
|
65
66
|
["--max-steps <n>", "Max agent steps (default " + CONFIG.maxSteps + ")"],
|
|
@@ -89,9 +90,11 @@ const SLASH_COMMANDS = [
|
|
|
89
90
|
["/rename", "Rename the current chat (/rename <title>)"],
|
|
90
91
|
["/init", "Analyze the project and write an AGENTS.md context file"],
|
|
91
92
|
["/model", "Pick the active 0G model (saved to settings)"],
|
|
93
|
+
["/mode", "Permission mode: ask (approve each) | auto (run all) | plan (read-only)"],
|
|
92
94
|
["/effort", "Set reasoning effort (low|medium|high|default)"],
|
|
93
95
|
["/subagents", "Enable or disable parallel subagents (on|off)"],
|
|
94
96
|
["/onchain", "Enable or disable gas-spending on-chain actions (on|off)"],
|
|
97
|
+
["/escalate", "Toggle switching to a stronger model on repeated failures (on|off)"],
|
|
95
98
|
["/skills", "List skills; /skills enable|disable <name>"],
|
|
96
99
|
["/commands", "List project custom commands (.z0g/commands/*.md)"],
|
|
97
100
|
["/attest", "Show the provenance manifest"],
|
|
@@ -248,6 +251,7 @@ function parse(argv) {
|
|
|
248
251
|
else if (a === "--no-subagents") flags.subagents = false;
|
|
249
252
|
else if (a === "--onchain") flags.onchain = true;
|
|
250
253
|
else if (a === "--no-onchain") flags.onchain = false;
|
|
254
|
+
else if (a === "--no-escalate") flags.escalate = false;
|
|
251
255
|
else if (a === "--force") flags.force = true;
|
|
252
256
|
else if (a === "--import") flags.import = true;
|
|
253
257
|
else if (a === "--verify") flags.verify = argv[++i];
|
|
@@ -713,13 +717,13 @@ async function runTask(task, flags) {
|
|
|
713
717
|
// Auto-verify: a normal run becomes self-correcting when a verify command is present.
|
|
714
718
|
const verifyCmd = flags.verify || (flags.autoVerify ? detectVerifyCmd(cwd) : null);
|
|
715
719
|
if (verifyCmd) {
|
|
716
|
-
await runGoal({ client: makeClient(), objective: task, cwd, sessionId, sessionDir: sessionDirPath, allowBash: flags.auto, preferredModel: flags.model, preferredEffort: flags.effort, preferredSubagents: flags.subagents, preferredOnchain: flags.onchain, verifyCmd, maxIters: 3, history: opened.history });
|
|
720
|
+
await runGoal({ client: makeClient(), objective: task, cwd, sessionId, sessionDir: sessionDirPath, allowBash: flags.auto, preferredModel: flags.model, preferredEffort: flags.effort, preferredSubagents: flags.subagents, preferredOnchain: flags.onchain, preferredEscalate: flags.escalate, verifyCmd, maxIters: 3, history: opened.history });
|
|
717
721
|
return;
|
|
718
722
|
}
|
|
719
723
|
const client = makeClient();
|
|
720
724
|
const mcp = await loadMcp(cwd);
|
|
721
725
|
if (mcp?.count) ui.info(`MCP: ${mcp.count} tool(s) from configured servers`);
|
|
722
|
-
const res = await runAgent({ client, task, cwd, sessionDir: sessionDirPath, allowBash: flags.auto, preferredModel: flags.model, preferredEffort: flags.effort, preferredSubagents: flags.subagents, preferredOnchain: flags.onchain, history: opened.history, mcp });
|
|
726
|
+
const res = await runAgent({ client, task, cwd, sessionDir: sessionDirPath, allowBash: flags.auto, preferredModel: flags.model, preferredEffort: flags.effort, preferredSubagents: flags.subagents, preferredOnchain: flags.onchain, preferredEscalate: flags.escalate, history: opened.history, mcp });
|
|
723
727
|
if (res?.messages) await saveMessages(cwd, sessionId, res.messages);
|
|
724
728
|
await mcp?.close();
|
|
725
729
|
} finally {
|
|
@@ -742,7 +746,7 @@ async function cmdGoal(objective, flags) {
|
|
|
742
746
|
try {
|
|
743
747
|
const verifyCmd = flags.verify || detectVerifyCmd(cwd);
|
|
744
748
|
if (!flags.auto) ui.info("tip: run goal with --auto so the agent can run and verify its own work.");
|
|
745
|
-
await runGoal({ client, objective, cwd, sessionId: opened.id, sessionDir: opened.dir, allowBash: flags.auto, preferredModel: flags.model, preferredEffort: flags.effort, preferredSubagents: flags.subagents, preferredOnchain: flags.onchain, verifyCmd, maxIters: 3, history: opened.history });
|
|
749
|
+
await runGoal({ client, objective, cwd, sessionId: opened.id, sessionDir: opened.dir, allowBash: flags.auto, preferredModel: flags.model, preferredEffort: flags.effort, preferredSubagents: flags.subagents, preferredOnchain: flags.onchain, preferredEscalate: flags.escalate, verifyCmd, maxIters: 3, history: opened.history });
|
|
746
750
|
} finally {
|
|
747
751
|
process.removeListener("SIGINT", onSig);
|
|
748
752
|
pruneEmptySync(cwd, opened.id);
|
|
@@ -761,6 +765,10 @@ async function repl(flags) {
|
|
|
761
765
|
let effort = flags.effort;
|
|
762
766
|
let subagents = flags.subagents;
|
|
763
767
|
let onchain = flags.onchain;
|
|
768
|
+
let escalate = flags.escalate;
|
|
769
|
+
// Permission mode: auto (run all) | ask (approve each) | plan (read-only).
|
|
770
|
+
let mode = flags.auto ? "auto" : "ask";
|
|
771
|
+
const allowedCmds = new Set(loadSettings(cwd).allowedCommands || []);
|
|
764
772
|
let sessTokens = { in: 0, out: 0 };
|
|
765
773
|
let priceMap = null;
|
|
766
774
|
fetchModels(client).then((all) => { priceMap = Object.fromEntries(all.map((m) => [m.id, m])); }).catch(() => {});
|
|
@@ -782,6 +790,25 @@ async function repl(flags) {
|
|
|
782
790
|
const activeModel = () => model || CONFIG.model;
|
|
783
791
|
const activeEffort = () => (effort === "" ? null : (effort || CONFIG.effort));
|
|
784
792
|
const activeOnchain = () => (onchain !== undefined ? onchain : CONFIG.onchain);
|
|
793
|
+
const activeEscalate = () => (escalate !== undefined ? escalate : CONFIG.escalate);
|
|
794
|
+
// Approve a gated action in "ask" mode. Remembers "always" choices in settings
|
|
795
|
+
// so it never asks again for that program (or for on-chain actions).
|
|
796
|
+
const approve = async (kind, desc) => {
|
|
797
|
+
const key = kind === "run_bash" ? "bash:" + (String(desc).trim().split(/\s+/)[0] || "?") : "@onchain";
|
|
798
|
+
if (allowedCmds.has(key)) return true;
|
|
799
|
+
rl.pause();
|
|
800
|
+
const label = kind === "run_bash" ? ui.strong(String(desc).slice(0, 80)) : "on-chain: " + desc;
|
|
801
|
+
console.log("\n " + ui.warn(ui.uiTTY ? "▲" : "!") + " " + (kind === "run_bash" ? "run " : "") + label);
|
|
802
|
+
const a = (await ask(" allow? " + ui.accent("y") + "es " + ui.accent("n") + "o " + ui.accent("a") + "lways ")).trim().toLowerCase();
|
|
803
|
+
rl.resume();
|
|
804
|
+
if (a === "a" || a === "always") {
|
|
805
|
+
allowedCmds.add(key);
|
|
806
|
+
saveSetting("allowedCommands", [...allowedCmds]);
|
|
807
|
+
ui.info("saved: won't ask again for " + (kind === "run_bash" ? key.slice(5) : "on-chain actions"));
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
return a === "y" || a === "yes";
|
|
811
|
+
};
|
|
785
812
|
const costOf = () => {
|
|
786
813
|
const m = priceMap && priceMap[activeModel()];
|
|
787
814
|
if (!m || m.inPerM == null) return null;
|
|
@@ -799,13 +826,14 @@ async function repl(flags) {
|
|
|
799
826
|
// Run one agent turn: hooks, the agent, then persist history + tokens.
|
|
800
827
|
const runTurn = async (task) => {
|
|
801
828
|
await runHooks(cwd, "preRun", hooks, flags.auto, task);
|
|
802
|
-
const res = await runAgent({ client, task, cwd, sessionDir: sessionDirPath,
|
|
829
|
+
const res = await runAgent({ client, task, cwd, sessionDir: sessionDirPath, preferredMode: mode, approve, preferredModel: model, preferredEffort: effort, preferredSubagents: subagents, preferredOnchain: activeOnchain(), preferredEscalate: activeEscalate(), history, mcp });
|
|
803
830
|
history = res.messages;
|
|
804
831
|
if (res.usageTotal) { sessTokens.in += res.usageTotal.prompt || 0; sessTokens.out += res.usageTotal.completion || 0; }
|
|
805
832
|
await saveMessages(cwd, sessionId, history);
|
|
806
833
|
await runHooks(cwd, "postRun", hooks, flags.auto, task);
|
|
807
834
|
};
|
|
808
835
|
ui.info("Interactive session. Type a task, or / then Tab for commands.");
|
|
836
|
+
ui.info("mode: " + ui.strong(mode) + (mode === "ask" ? " (I ask before running commands)" : mode === "plan" ? " (read-only until you /mode auto)" : " (runs commands without asking)") + " · /mode ask|auto|plan");
|
|
809
837
|
if (customCmds.length) ui.info(customCmds.length + " custom command(s): " + customCmds.map((c) => "/" + c.name).join(", "));
|
|
810
838
|
if (hasHooks(hooks) && !flags.auto) ui.info("hooks are configured; run with --auto to enable them");
|
|
811
839
|
// Blinking block cursor at the prompt (matches the demo); restore on any exit.
|
|
@@ -903,6 +931,29 @@ async function repl(flags) {
|
|
|
903
931
|
console.log(ui.warn("usage: /onchain on|off"));
|
|
904
932
|
}
|
|
905
933
|
}
|
|
934
|
+
else if (cmd === "escalate") {
|
|
935
|
+
const a = arg.toLowerCase().trim();
|
|
936
|
+
if (a === "on" || a === "off") {
|
|
937
|
+
escalate = a === "on"; saveSetting("escalate", escalate);
|
|
938
|
+
ui.info("escalate " + (escalate ? "on" : "off") + " (saved)" + (escalate ? " · switches to a stronger model after " + CONFIG.escalateAfter + " tool failures" : " · stays on the chosen model"));
|
|
939
|
+
} else if (!a) {
|
|
940
|
+
ui.info("escalate: " + (activeEscalate() ? "on" : "off") + " · after " + CONFIG.escalateAfter + " failures · usage: /escalate on|off");
|
|
941
|
+
} else {
|
|
942
|
+
console.log(ui.warn("usage: /escalate on|off"));
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
else if (cmd === "mode") {
|
|
946
|
+
const a = arg.toLowerCase().trim();
|
|
947
|
+
if (["ask", "auto", "plan"].includes(a)) {
|
|
948
|
+
mode = a;
|
|
949
|
+
const desc = a === "auto" ? "runs commands without asking" : a === "plan" ? "read-only: explores and plans, no writes or commands" : "asks before each command / on-chain action";
|
|
950
|
+
ui.info("mode: " + ui.strong(a) + " · " + desc);
|
|
951
|
+
} else if (!a) {
|
|
952
|
+
ui.info("mode: " + ui.strong(mode) + " · usage: /mode ask|auto|plan" + (allowedCmds.size ? " · " + allowedCmds.size + " always-allowed" : ""));
|
|
953
|
+
} else {
|
|
954
|
+
console.log(ui.warn("usage: /mode ask|auto|plan"));
|
|
955
|
+
}
|
|
956
|
+
}
|
|
906
957
|
else if (cmd === "skills") cmdSkills(cwd, arg);
|
|
907
958
|
else if (cmd === "chats") {
|
|
908
959
|
if (Array.isArray(history) && history.length) await saveMessages(cwd, sessionId, history);
|
|
@@ -967,7 +1018,7 @@ async function repl(flags) {
|
|
|
967
1018
|
else { console.log(ui.section("Custom commands", customCmds.length + " loaded")); for (const c of customCmds) console.log(" " + ui.accent("/" + c.name) + " " + ui.muted(c.description)); }
|
|
968
1019
|
}
|
|
969
1020
|
else if (cmd === "goal") {
|
|
970
|
-
await runGoal({ client, objective: arg, cwd, sessionId, sessionDir: sessionDirPath,
|
|
1021
|
+
await runGoal({ client, objective: arg, cwd, sessionId, sessionDir: sessionDirPath, preferredMode: mode, approve, preferredModel: model, preferredEffort: effort, preferredSubagents: subagents, preferredOnchain: activeOnchain(), preferredEscalate: activeEscalate(), verifyCmd: flags.verify || detectVerifyCmd(cwd), maxIters: 3, history });
|
|
971
1022
|
history = await readMessages(cwd, sessionId) || history;
|
|
972
1023
|
}
|
|
973
1024
|
else if (customMap[cmd]) {
|
|
@@ -994,7 +1045,7 @@ async function main() {
|
|
|
994
1045
|
if (flags.version) {
|
|
995
1046
|
let v = "";
|
|
996
1047
|
try { v = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version; } catch {}
|
|
997
|
-
console.log("z0gcode " + (v || "0.
|
|
1048
|
+
console.log("z0gcode " + (v || "0.3.1"));
|
|
998
1049
|
return;
|
|
999
1050
|
}
|
|
1000
1051
|
|
package/package.json
CHANGED
package/src/agent.mjs
CHANGED
|
@@ -57,22 +57,30 @@ function parseArgs(raw) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
export async function runAgent({ client, task, cwd, sessionDir, allowBash, preferredModel, preferredEffort, preferredSubagents, preferredOnchain, onModel, history, mcp, quiet = false, toolNames = null, isSubagent = false }) {
|
|
60
|
+
export async function runAgent({ client, task, cwd, sessionDir, allowBash, preferredMode, approve, preferredModel, preferredEffort, preferredSubagents, preferredOnchain, preferredEscalate, onModel, history, mcp, quiet = false, toolNames = null, isSubagent = false }) {
|
|
61
61
|
const q = !!quiet;
|
|
62
62
|
// "" means an explicit unset (use the model's own default); undefined falls back.
|
|
63
63
|
const effort = preferredEffort === "" ? null : (preferredEffort || CONFIG.effort);
|
|
64
64
|
const subOn = preferredSubagents !== undefined ? preferredSubagents : CONFIG.subagents;
|
|
65
65
|
const onchainOn = preferredOnchain !== undefined ? preferredOnchain : CONFIG.onchain;
|
|
66
66
|
const provDir = sessionDir || path.join(cwd, ".z0g");
|
|
67
|
-
|
|
67
|
+
// Permission mode: auto | ask | plan. Legacy: allowBash -> auto, else ask.
|
|
68
|
+
const mode = preferredMode || (allowBash ? "auto" : "ask");
|
|
69
|
+
const canBash = mode !== "plan"; // shell available in auto/ask (gated at exec)
|
|
70
|
+
const execute = makeExecutor({ cwd, allowBash, sessionDir: provDir, onchain: onchainOn, mode, approve });
|
|
68
71
|
// Restrict the toolset for subagents (read-only), and drop spawn_subagents when
|
|
69
72
|
// it is a subagent (no recursion) or the toggle is off. Drop on-chain tools when
|
|
70
73
|
// the on-chain toggle is off so the agent never proposes a gas-spending action.
|
|
71
74
|
let baseTools = toolNames ? TOOL_DEFS.filter((t) => toolNames.includes(t.function.name)) : TOOL_DEFS;
|
|
72
75
|
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
|
|
74
|
-
if (!
|
|
76
|
+
// Write subagents edit files and run shell in worktrees, so they need shell.
|
|
77
|
+
if (!canBash) baseTools = baseTools.filter((t) => t.function.name !== "spawn_write_subagents");
|
|
75
78
|
if (!onchainOn) baseTools = baseTools.filter((t) => t.function.name !== "upload_0g_storage" && t.function.name !== "deploy_0g_chain");
|
|
79
|
+
// Plan mode is read-only: drop every acting tool so the agent explores and plans.
|
|
80
|
+
if (mode === "plan") {
|
|
81
|
+
const PLAN_TOOLS = new Set(["read_file", "search_files", "list_dir", "read_skill", "update_plan", "spawn_subagents"]);
|
|
82
|
+
baseTools = baseTools.filter((t) => PLAN_TOOLS.has(t.function.name));
|
|
83
|
+
}
|
|
76
84
|
const toolSet = !isSubagent && mcp?.tools?.length ? [...baseTools, ...mcp.tools] : baseTools;
|
|
77
85
|
const models = modelChain(preferredModel);
|
|
78
86
|
const prov = makeProvenance(provDir);
|
|
@@ -83,7 +91,10 @@ export async function runAgent({ client, task, cwd, sessionDir, allowBash, prefe
|
|
|
83
91
|
: [{ role: "system", content: systemPrompt(cwd) }, { role: "user", content: task }];
|
|
84
92
|
|
|
85
93
|
const recent = []; // circuit breaker on repeated identical tool calls
|
|
94
|
+
const results = []; // no-progress breaker: repeated identical tool RESULTS
|
|
86
95
|
const failCounts = {}; // per-tool failure counter, drives model escalation
|
|
96
|
+
const escalateOn = preferredEscalate !== undefined ? preferredEscalate : CONFIG.escalate;
|
|
97
|
+
const escalateAfter = CONFIG.escalateAfter;
|
|
87
98
|
let escalate = false;
|
|
88
99
|
let activeModel = models[0];
|
|
89
100
|
const usageTotal = { prompt: 0, completion: 0, total: 0 };
|
|
@@ -150,6 +161,7 @@ export async function runAgent({ client, task, cwd, sessionDir, allowBash, prefe
|
|
|
150
161
|
return { ok: true, steps: step + 1, changes: prov.count(), messages, finalText, usageTotal };
|
|
151
162
|
}
|
|
152
163
|
|
|
164
|
+
let stuck = false;
|
|
153
165
|
for (const tc of toolCalls) {
|
|
154
166
|
const name = tc.function?.name;
|
|
155
167
|
const args = parseArgs(tc.function?.arguments);
|
|
@@ -278,10 +290,25 @@ export async function runAgent({ client, task, cwd, sessionDir, allowBash, prefe
|
|
|
278
290
|
}
|
|
279
291
|
} else {
|
|
280
292
|
failCounts[name] = (failCounts[name] || 0) + 1;
|
|
281
|
-
if (failCounts[name] >=
|
|
293
|
+
if (escalateOn && failCounts[name] >= escalateAfter) escalate = true; // stuck on this tool: try a stronger model
|
|
282
294
|
}
|
|
283
295
|
|
|
284
296
|
messages.push({ role: "tool", tool_call_id: tc.id, content: String(res.content ?? (res.ok ? "OK" : "ERROR")) });
|
|
297
|
+
|
|
298
|
+
// No-progress breaker: the same tool returning the same result repeatedly
|
|
299
|
+
// means the agent is spinning (e.g. re-checking a state only a human can
|
|
300
|
+
// change). Stop cleanly and hand control back, instead of burning steps.
|
|
301
|
+
const rkey = `${name}|${res.summary || ""}|${String(res.content ?? "").slice(0, 300)}`;
|
|
302
|
+
results.push(rkey);
|
|
303
|
+
if (results.length > 8) results.shift();
|
|
304
|
+
if (results.filter((k) => k === rkey).length >= 3) stuck = true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (stuck) {
|
|
308
|
+
finalText = "Stopped: I kept getting the same result without new progress. If this needs a manual step (a permission or setting change), do it and ask me again, or refine the task.";
|
|
309
|
+
if (!q) { ui.error("Stopped: no progress (same tool result 3 times)."); ui.assistant(finalText); }
|
|
310
|
+
usageTotal.total = usageTotal.prompt + usageTotal.completion;
|
|
311
|
+
return { ok: false, steps: step + 1, changes: prov.count(), messages, finalText, usageTotal };
|
|
285
312
|
}
|
|
286
313
|
}
|
|
287
314
|
|
package/src/config.mjs
CHANGED
|
@@ -65,6 +65,15 @@ export const CONFIG = {
|
|
|
65
65
|
if (e !== undefined) return e;
|
|
66
66
|
return typeof settings.onchain === "boolean" ? settings.onchain : false;
|
|
67
67
|
})(),
|
|
68
|
+
|
|
69
|
+
// Escalate to a stronger fallback model when a tool fails repeatedly. ON by
|
|
70
|
+
// default (env > settings > on); escalateAfter is the failure threshold.
|
|
71
|
+
escalate: (() => {
|
|
72
|
+
const e = boolOf(process.env.ZOG_ESCALATE);
|
|
73
|
+
if (e !== undefined) return e;
|
|
74
|
+
return typeof settings.escalate === "boolean" ? settings.escalate : true;
|
|
75
|
+
})(),
|
|
76
|
+
escalateAfter: Math.max(2, Number(process.env.ZOG_ESCALATE_AFTER || settings.escalateAfter || 3)),
|
|
68
77
|
};
|
|
69
78
|
|
|
70
79
|
export function modelChain(preferred) {
|
package/src/goal.mjs
CHANGED
|
@@ -14,12 +14,12 @@ function runCmd(cmd, cwd) {
|
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export async function runGoal({ client, objective, cwd, sessionId, sessionDir, allowBash, preferredModel, preferredEffort, preferredSubagents, preferredOnchain, verifyCmd, maxIters = 3, history: historyParam = null }) {
|
|
17
|
+
export async function runGoal({ client, objective, cwd, sessionId, sessionDir, allowBash, preferredMode, approve, preferredModel, preferredEffort, preferredSubagents, preferredOnchain, preferredEscalate, verifyCmd, maxIters = 3, history: historyParam = null }) {
|
|
18
18
|
let history = historyParam ?? null;
|
|
19
19
|
let task = objective;
|
|
20
20
|
for (let iter = 1; iter <= maxIters; iter++) {
|
|
21
21
|
console.log(ui.section("Goal", "iteration " + iter + "/" + maxIters));
|
|
22
|
-
const res = await runAgent({ client, task, cwd, sessionDir, allowBash, preferredModel, preferredEffort, preferredSubagents, preferredOnchain, history });
|
|
22
|
+
const res = await runAgent({ client, task, cwd, sessionDir, allowBash, preferredMode, approve, preferredModel, preferredEffort, preferredSubagents, preferredOnchain, preferredEscalate, history });
|
|
23
23
|
history = res.messages;
|
|
24
24
|
if (sessionId && Array.isArray(history)) {
|
|
25
25
|
try { await saveMessages(cwd, sessionId, history); } catch {}
|
package/src/tools.mjs
CHANGED
|
@@ -247,8 +247,20 @@ function safeResolve(cwd, p) {
|
|
|
247
247
|
return abs;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
export function makeExecutor({ cwd, allowBash, sessionDir, onchain = false }) {
|
|
250
|
+
export function makeExecutor({ cwd, allowBash, sessionDir, onchain = false, mode, approve }) {
|
|
251
251
|
const planDir = sessionDir || path.join(cwd, ".z0g");
|
|
252
|
+
// Permission mode: "auto" runs everything, "ask" prompts via approve(), "plan"
|
|
253
|
+
// is read-only. Legacy callers pass allowBash -> auto/ask.
|
|
254
|
+
const effMode = mode || (allowBash ? "auto" : "ask");
|
|
255
|
+
const readOnly = effMode === "plan";
|
|
256
|
+
const planDeny = (what) => ({ ok: false, summary: "plan mode", content: `Read-only plan mode: I can explore and plan but will not ${what}. Switch with /mode auto or /mode ask to let me act.` });
|
|
257
|
+
// Approve a gated action (run_bash / on-chain). Returns true if allowed.
|
|
258
|
+
const permit = async (kind, desc) => {
|
|
259
|
+
if (readOnly) return false;
|
|
260
|
+
if (effMode === "auto") return true;
|
|
261
|
+
if (typeof approve === "function") return !!(await approve(kind, desc));
|
|
262
|
+
return false;
|
|
263
|
+
};
|
|
252
264
|
return async function execute(name, args) {
|
|
253
265
|
try {
|
|
254
266
|
switch (name) {
|
|
@@ -263,6 +275,7 @@ export function makeExecutor({ cwd, allowBash, sessionDir, onchain = false }) {
|
|
|
263
275
|
return { ok: true, summary: `search "${args.query}" (${res.count})`, content: res.text };
|
|
264
276
|
}
|
|
265
277
|
case "write_file": {
|
|
278
|
+
if (readOnly) return planDeny("write files");
|
|
266
279
|
const abs = safeResolve(cwd, args.path);
|
|
267
280
|
let before = "", existed = true;
|
|
268
281
|
try { before = await fs.readFile(abs, "utf8"); } catch { before = ""; existed = false; }
|
|
@@ -272,6 +285,7 @@ export function makeExecutor({ cwd, allowBash, sessionDir, onchain = false }) {
|
|
|
272
285
|
return { ok: true, summary: `wrote ${args.path} (${after.length} bytes)`, content: "OK", change: { path: args.path, before, after, created: !existed } };
|
|
273
286
|
}
|
|
274
287
|
case "edit_file": {
|
|
288
|
+
if (readOnly) return planDeny("edit files");
|
|
275
289
|
const abs = safeResolve(cwd, args.path);
|
|
276
290
|
const cur = await fs.readFile(abs, "utf8");
|
|
277
291
|
if (!args.old_string || !cur.includes(args.old_string)) {
|
|
@@ -291,16 +305,23 @@ export function makeExecutor({ cwd, allowBash, sessionDir, onchain = false }) {
|
|
|
291
305
|
return { ok: true, summary: `list ${args.path || "."} (${lines.length})`, content: lines.join("\n") || "(empty)" };
|
|
292
306
|
}
|
|
293
307
|
case "run_bash": {
|
|
294
|
-
if (
|
|
295
|
-
|
|
308
|
+
if (readOnly) return planDeny("run commands");
|
|
309
|
+
if (!(await permit("run_bash", args.command))) {
|
|
310
|
+
return { ok: false, summary: "bash not allowed", content: effMode === "ask" ? "You declined this command." : "run_bash needs approval: run z0g interactively (mode ask) and approve it, or with --auto." };
|
|
296
311
|
}
|
|
297
312
|
const out = await runBash(args.command, cwd);
|
|
298
313
|
return { ok: out.code === 0, summary: `bash exit ${out.code}`, content: out.text };
|
|
299
314
|
}
|
|
300
315
|
case "upload_0g_storage": {
|
|
316
|
+
if (onchain && !(await permit("on-chain", "upload to 0G Storage"))) {
|
|
317
|
+
return { ok: false, summary: "on-chain not allowed", content: readOnly ? "Read-only plan mode." : "You declined the on-chain action." };
|
|
318
|
+
}
|
|
301
319
|
return await uploadToStorage(cwd, args.path, onchain);
|
|
302
320
|
}
|
|
303
321
|
case "deploy_0g_chain": {
|
|
322
|
+
if (onchain && !(await permit("on-chain", "deploy to 0G Chain"))) {
|
|
323
|
+
return { ok: false, summary: "on-chain not allowed", content: readOnly ? "Read-only plan mode." : "You declined the on-chain action." };
|
|
324
|
+
}
|
|
304
325
|
return await deployToChain(cwd, args, onchain);
|
|
305
326
|
}
|
|
306
327
|
case "update_plan": {
|