@gigaai/newton 0.3.0 → 0.4.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/README.md +29 -5
- package/dist/acp.js +38 -2
- package/dist/cli.js +185 -2
- package/dist/control.js +169 -0
- package/dist/executor.js +227 -45
- package/dist/journal.js +40 -3
- package/dist/mcp.js +102 -0
- package/dist/sandbox.js +157 -0
- package/dist/version.js +1 -1
- package/dist/worktree.js +51 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,14 +66,21 @@ script body. The body runs as an async function with these in scope:
|
|
|
66
66
|
| `pipeline(items, ...stages)` | Each item flows through the stages independently — no barrier between stages. Stage signature: `(prev, item, index)`. |
|
|
67
67
|
| `phase(title)` | Group subsequent agents in progress output. |
|
|
68
68
|
| `log(message)` | Narrator line in progress output and the journal. |
|
|
69
|
+
| `workflow(nameOrPath, args?)` | Run one child workflow. Bare names resolve from `.newton/workflows/<name>.js`; paths resolve relative to the calling workflow. Nesting is limited to one child level. |
|
|
69
70
|
| `args` | Whatever you passed via `--args` (JSON-parsed when possible). |
|
|
70
71
|
| `budget` | `{ total, spent(), remaining() }` for the `--budget` output-token ceiling. |
|
|
71
72
|
|
|
72
73
|
`agent()` options: `provider` (see below), `model`, `effort` (`low`–`xhigh`),
|
|
73
74
|
`access` (`read` | `write` | `full` — sandbox posture), `schema` (JSON Schema
|
|
74
75
|
for structured output, validated with automatic retries), `label`, `phase`,
|
|
75
|
-
`cwd`, `timeoutMs`,
|
|
76
|
-
|
|
76
|
+
`cwd`, `timeoutMs`, `isolation: 'worktree'` for disposable per-agent git
|
|
77
|
+
worktrees, `mcp` (`true` for all `.mcp.json` servers, or a list of names/inline
|
|
78
|
+
servers), `config` (ACP `configId` to string value), and `command` — an escape
|
|
79
|
+
hatch to run any ACP agent, e.g. `command: ['my-agent', '--acp']`.
|
|
80
|
+
|
|
81
|
+
Workflow scripts run in a sandboxed JavaScript realm: Node globals such as
|
|
82
|
+
`process`, `fetch`, and `setTimeout` are not available, and `console.log()`
|
|
83
|
+
routes through `log()` into progress output and the journal.
|
|
77
84
|
|
|
78
85
|
## Providers
|
|
79
86
|
|
|
@@ -98,7 +105,7 @@ the agent advertises over ACP (session config options) and selects it
|
|
|
98
105
|
protocol-side. `newton models <provider>` shows what an agent exposes;
|
|
99
106
|
requesting a model an agent doesn't offer fails with the available list.
|
|
100
107
|
|
|
101
|
-
`Date.now()`, argless `new Date()`, and `Math.random()` throw inside
|
|
108
|
+
`Date.now()`, argless `new Date()`, and `Math.random()` also throw inside
|
|
102
109
|
workflows: nondeterminism would silently break `--resume` replay. Pass
|
|
103
110
|
timestamps in via `--args`.
|
|
104
111
|
|
|
@@ -113,15 +120,32 @@ workspace:
|
|
|
113
120
|
chunk, tool call, and permission decision.
|
|
114
121
|
|
|
115
122
|
`newton run workflow.js --resume <runId>` replays successful agent results
|
|
116
|
-
from a previous run's journal
|
|
117
|
-
|
|
123
|
+
from a previous run's journal. The default `--resume-mode prefix` replays by
|
|
124
|
+
call position until the first changed agent call, then runs that call and
|
|
125
|
+
everything after it live. `--resume-mode hash` uses the legacy hash replay:
|
|
126
|
+
identical calls return instantly no matter where they appear.
|
|
127
|
+
|
|
128
|
+
Long runs can be detached and controlled from another shell:
|
|
129
|
+
|
|
130
|
+
- `newton run workflow.js --detach` starts in the background and prints
|
|
131
|
+
`{ runId, pid }`.
|
|
132
|
+
- `newton runs [--json]` lists `.newton/runs/*` with status, agent count,
|
|
133
|
+
output tokens, and pid.
|
|
134
|
+
- `newton watch <runId> [--json]` replays the journal from the beginning and
|
|
135
|
+
keeps tailing until the run ends.
|
|
136
|
+
- `newton skip <runId> <agentId>` asks a queued or live agent to cancel;
|
|
137
|
+
`agent()` returns `null` and the workflow continues.
|
|
118
138
|
|
|
119
139
|
## CLI
|
|
120
140
|
|
|
121
141
|
```
|
|
122
142
|
newton run <workflow.js> --args <json> --cwd <dir> --resume <runId> --json
|
|
143
|
+
--resume-mode <prefix|hash> --detach
|
|
123
144
|
--budget <tokens> --max-agents <n> --timeout <seconds>
|
|
124
145
|
--concurrency <n>
|
|
146
|
+
newton runs [--json] list runs under .newton/runs
|
|
147
|
+
newton watch <runId> [--json] replay/tail a run's journal
|
|
148
|
+
newton skip <runId> <agentId> cancel one agent; workflow continues
|
|
125
149
|
newton validate <workflow.js> parse + compile without spawning agents
|
|
126
150
|
newton agents [--json] supported agents: installed? signed in? (alias: doctor)
|
|
127
151
|
newton models <provider> ask an agent which models/modes it exposes
|
package/dist/acp.js
CHANGED
|
@@ -50,8 +50,14 @@ export class AcpAgentSession {
|
|
|
50
50
|
toolCalls = 0;
|
|
51
51
|
usage = null;
|
|
52
52
|
stderrTail = [];
|
|
53
|
+
aborted;
|
|
54
|
+
abortSignal;
|
|
55
|
+
resolveAbortSignal;
|
|
53
56
|
constructor(request) {
|
|
54
57
|
this.request = request;
|
|
58
|
+
this.abortSignal = new Promise((resolveAbort) => {
|
|
59
|
+
this.resolveAbortSignal = resolveAbort;
|
|
60
|
+
});
|
|
55
61
|
}
|
|
56
62
|
async prompt() {
|
|
57
63
|
try {
|
|
@@ -72,6 +78,15 @@ export class AcpAgentSession {
|
|
|
72
78
|
}
|
|
73
79
|
async promptAgain(prompt) {
|
|
74
80
|
try {
|
|
81
|
+
if (this.aborted) {
|
|
82
|
+
return {
|
|
83
|
+
text: this.text,
|
|
84
|
+
stopReason: "cancelled",
|
|
85
|
+
usage: this.usage,
|
|
86
|
+
toolCalls: this.toolCalls,
|
|
87
|
+
error: this.aborted,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
75
90
|
const connection = this.connection;
|
|
76
91
|
const sessionId = this.sessionId;
|
|
77
92
|
this.text = "";
|
|
@@ -84,7 +99,7 @@ export class AcpAgentSession {
|
|
|
84
99
|
sessionId,
|
|
85
100
|
prompt: [{ type: "text", text: prompt }],
|
|
86
101
|
});
|
|
87
|
-
const outcome = await Promise.race([turn, timedOut]);
|
|
102
|
+
const outcome = await Promise.race([turn, timedOut, this.abortSignal]);
|
|
88
103
|
clearTimeout(timer);
|
|
89
104
|
if (outcome === "timeout") {
|
|
90
105
|
await connection.cancel({ sessionId }).catch(() => { });
|
|
@@ -99,6 +114,19 @@ export class AcpAgentSession {
|
|
|
99
114
|
error: `agent timed out after ${this.request.timeoutMs}ms`,
|
|
100
115
|
};
|
|
101
116
|
}
|
|
117
|
+
if (outcome === "aborted") {
|
|
118
|
+
await connection.cancel({ sessionId }).catch(() => { });
|
|
119
|
+
// Give the agent a moment to flush final updates, then hard-stop.
|
|
120
|
+
await Promise.race([turn.catch(() => { }), new Promise((r) => setTimeout(r, 3000))]);
|
|
121
|
+
this.close();
|
|
122
|
+
return {
|
|
123
|
+
text: this.text,
|
|
124
|
+
stopReason: "cancelled",
|
|
125
|
+
usage: this.usage,
|
|
126
|
+
toolCalls: this.toolCalls,
|
|
127
|
+
error: this.aborted,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
102
130
|
this.usage = outcome.usage ?? this.usage;
|
|
103
131
|
return {
|
|
104
132
|
text: this.text,
|
|
@@ -111,6 +139,10 @@ export class AcpAgentSession {
|
|
|
111
139
|
return this.failure(error);
|
|
112
140
|
}
|
|
113
141
|
}
|
|
142
|
+
abort(reason) {
|
|
143
|
+
this.aborted = reason;
|
|
144
|
+
this.resolveAbortSignal("aborted");
|
|
145
|
+
}
|
|
114
146
|
close() {
|
|
115
147
|
if (this.child && !this.child.killed)
|
|
116
148
|
this.child.kill("SIGTERM");
|
|
@@ -187,13 +219,17 @@ export class AcpAgentSession {
|
|
|
187
219
|
session: { configOptions: {} },
|
|
188
220
|
},
|
|
189
221
|
});
|
|
190
|
-
const session = await connection.newSession({ cwd: resolve(cwd), mcpServers: [] });
|
|
222
|
+
const session = await connection.newSession({ cwd: resolve(cwd), mcpServers: this.request.mcpServers ?? [] });
|
|
191
223
|
this.sessionId = session.sessionId;
|
|
192
224
|
this.sessionInfo = session;
|
|
193
225
|
trace({ type: "session", sessionId: session.sessionId, configOptions: session.configOptions ?? null });
|
|
194
226
|
if (this.request.spec.model && !resolved.modelHandled) {
|
|
195
227
|
await this.selectModelViaProtocol(session, this.request.spec.model);
|
|
196
228
|
}
|
|
229
|
+
for (const [configId, value] of Object.entries(this.request.config ?? {})) {
|
|
230
|
+
await connection.setSessionConfigOption({ sessionId: this.sessionId, configId, value });
|
|
231
|
+
trace({ type: "config_set", configId, value });
|
|
232
|
+
}
|
|
197
233
|
}
|
|
198
234
|
/**
|
|
199
235
|
* Providers without a CLI/env model knob may still expose model choice as an
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { accessSync, closeSync, constants, mkdirSync, openSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
5
|
import { AcpAgentSession } from "./acp.js";
|
|
6
|
+
import { listRuns, readStatus, requestSkip, tailJournal } from "./control.js";
|
|
4
7
|
import { compileWorkflow, loadWorkflow, runWorkflow } from "./executor.js";
|
|
8
|
+
import { newRunId } from "./journal.js";
|
|
5
9
|
import { HumanReporter, JsonReporter } from "./progress.js";
|
|
6
10
|
import { describeProviders } from "./providers.js";
|
|
7
11
|
import { VERSION } from "./version.js";
|
|
@@ -9,6 +13,10 @@ const USAGE = `newton — deterministic multi-agent workflows over ACP
|
|
|
9
13
|
|
|
10
14
|
Usage:
|
|
11
15
|
newton run <workflow.js> [options] Run a workflow in the current directory
|
|
16
|
+
newton run <workflow.js> --detach Start in the background; prints { runId, pid }
|
|
17
|
+
newton runs [--json] List runs under .newton/runs with status
|
|
18
|
+
newton watch <runId> [--json] Tail a run's journal as live progress
|
|
19
|
+
newton skip <runId> <agentId> Cancel one agent; agent() returns null, run continues
|
|
12
20
|
newton validate <workflow.js> Parse + compile a workflow without running agents
|
|
13
21
|
newton agents [--json] Supported agents: installed? signed in? (alias: doctor)
|
|
14
22
|
newton models <provider> [--json] Ask an agent which models it exposes
|
|
@@ -17,6 +25,8 @@ Run options:
|
|
|
17
25
|
--args <json> Value exposed to the script as \`args\`
|
|
18
26
|
--cwd <dir> Workspace agents operate in (default: current directory)
|
|
19
27
|
--resume <runId> Replay cached agent results from a previous run
|
|
28
|
+
--resume-mode <prefix|hash> Resume matching: positional prefix (default) or hash replay
|
|
29
|
+
--detach Start the run in the background
|
|
20
30
|
--json NDJSON events on stdout instead of human output
|
|
21
31
|
--budget <tokens> Output-token ceiling; agent() throws once exhausted
|
|
22
32
|
--max-agents <n> Agent-call cap (default 200)
|
|
@@ -28,7 +38,11 @@ followed by a script body using agent()/parallel()/pipeline()/phase()/log(),
|
|
|
28
38
|
with \`args\` and \`budget\` in scope. agent() opts: provider (codex | claude |
|
|
29
39
|
gemini | qwen | cursor | opencode | goose | copilot), model, effort, access
|
|
30
40
|
(read | write | full), schema, label, phase, cwd, command (any ACP agent,
|
|
31
|
-
e.g. ["my-agent", "--acp"])
|
|
41
|
+
e.g. ["my-agent", "--acp"]), isolation ('worktree'), mcp (true | names/servers
|
|
42
|
+
from .mcp.json), config ({configId: value}).
|
|
43
|
+
Workflows may call workflow(nameOrPath, args) one level deep; bare names resolve
|
|
44
|
+
from .newton/workflows/<name>.js.
|
|
45
|
+
Scripts run sandboxed: no process/fetch/setTimeout; console.log routes to log().
|
|
32
46
|
|
|
33
47
|
Runs are recorded under .newton/runs/<runId>/ — journal.jsonl plus a full
|
|
34
48
|
per-agent ACP trace.`;
|
|
@@ -66,10 +80,47 @@ function intFlag(flags, name) {
|
|
|
66
80
|
fail(`--${name} requires a positive integer`);
|
|
67
81
|
return value;
|
|
68
82
|
}
|
|
83
|
+
function resumeModeFlag(flags) {
|
|
84
|
+
const raw = flags["resume-mode"];
|
|
85
|
+
if (raw === undefined)
|
|
86
|
+
return undefined;
|
|
87
|
+
if (raw === "prefix" || raw === "hash")
|
|
88
|
+
return raw;
|
|
89
|
+
fail("--resume-mode requires prefix or hash");
|
|
90
|
+
}
|
|
91
|
+
function commandRunDetached(file, flags) {
|
|
92
|
+
const cwd = typeof flags.cwd === "string" ? resolve(flags.cwd) : process.cwd();
|
|
93
|
+
const runId = newRunId();
|
|
94
|
+
const runDir = join(cwd, ".newton", "runs", runId);
|
|
95
|
+
mkdirSync(runDir, { recursive: true });
|
|
96
|
+
const fd = openSync(join(runDir, "detach.log"), "a");
|
|
97
|
+
const argv = [process.argv[1], "run", resolve(file), "--json", "--run-id", runId, "--cwd", cwd];
|
|
98
|
+
for (const name of ["args", "resume", "resume-mode", "budget", "max-agents", "timeout", "concurrency"]) {
|
|
99
|
+
const value = flags[name];
|
|
100
|
+
if (typeof value === "string")
|
|
101
|
+
argv.push(`--${name}`, value);
|
|
102
|
+
}
|
|
103
|
+
let child;
|
|
104
|
+
try {
|
|
105
|
+
child = spawn(process.execPath, argv, { detached: true, stdio: ["ignore", fd, fd] });
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
closeSync(fd);
|
|
109
|
+
}
|
|
110
|
+
if (!child)
|
|
111
|
+
fail("failed to start detached run");
|
|
112
|
+
child.unref();
|
|
113
|
+
process.stdout.write(`${JSON.stringify({ runId, pid: child.pid }, null, 2)}\n`);
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
69
116
|
async function commandRun(positional, flags) {
|
|
70
117
|
const file = positional[0];
|
|
71
118
|
if (!file)
|
|
72
119
|
fail("newton run requires a workflow file");
|
|
120
|
+
const resumeMode = resumeModeFlag(flags);
|
|
121
|
+
if (flags.detach === true) {
|
|
122
|
+
commandRunDetached(file, flags);
|
|
123
|
+
}
|
|
73
124
|
let args;
|
|
74
125
|
if (typeof flags.args === "string") {
|
|
75
126
|
try {
|
|
@@ -87,6 +138,9 @@ async function commandRun(positional, flags) {
|
|
|
87
138
|
args,
|
|
88
139
|
cwd: typeof flags.cwd === "string" ? flags.cwd : process.cwd(),
|
|
89
140
|
resumeRunId: typeof flags.resume === "string" ? flags.resume : undefined,
|
|
141
|
+
resumeMode,
|
|
142
|
+
runId: typeof flags["run-id"] === "string" ? flags["run-id"] : undefined,
|
|
143
|
+
detached: typeof flags["run-id"] === "string",
|
|
90
144
|
budgetTokens: intFlag(flags, "budget"),
|
|
91
145
|
maxAgents: intFlag(flags, "max-agents"),
|
|
92
146
|
agentTimeoutMs: timeoutSeconds ? timeoutSeconds * 1000 : undefined,
|
|
@@ -99,6 +153,126 @@ async function commandRun(positional, flags) {
|
|
|
99
153
|
}
|
|
100
154
|
process.exit(result.status === "ok" ? 0 : 1);
|
|
101
155
|
}
|
|
156
|
+
function commandRuns(flags) {
|
|
157
|
+
const workspace = process.cwd();
|
|
158
|
+
const rows = listRuns(workspace);
|
|
159
|
+
if (flags.json === true) {
|
|
160
|
+
process.stdout.write(`${JSON.stringify(rows.map(({ runId, stale, status }) => ({ runId, stale, ...(status ?? {}) })), null, 2)}\n`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (rows.length === 0) {
|
|
164
|
+
process.stdout.write("no runs under .newton/runs\n");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const table = [
|
|
168
|
+
["runId", "status", "agents", "out-tok", "pid", "name"],
|
|
169
|
+
...rows.map(({ runId, stale, status }) => [
|
|
170
|
+
runId,
|
|
171
|
+
stale ? "stale" : (status?.status ?? "—"),
|
|
172
|
+
status ? String(status.agents) : "",
|
|
173
|
+
status ? String(status.outputTokens) : "",
|
|
174
|
+
status ? String(status.pid) : "",
|
|
175
|
+
status?.name ?? "",
|
|
176
|
+
]),
|
|
177
|
+
];
|
|
178
|
+
const widths = table[0].map((_, index) => Math.max(...table.map((row) => row[index].length)));
|
|
179
|
+
for (const row of table) {
|
|
180
|
+
process.stdout.write(`${row.map((value, index) => value.padEnd(widths[index])).join(" ")}\n`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function commandWatch(positional, flags) {
|
|
184
|
+
const runId = positional[0];
|
|
185
|
+
if (!runId)
|
|
186
|
+
fail("newton watch requires a run id");
|
|
187
|
+
const runDir = join(process.cwd(), ".newton", "runs", runId);
|
|
188
|
+
try {
|
|
189
|
+
accessSync(runDir, constants.F_OK);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
fail(`no run "${runId}"`);
|
|
193
|
+
}
|
|
194
|
+
const journalPath = join(runDir, "journal.jsonl");
|
|
195
|
+
const json = flags.json === true;
|
|
196
|
+
const reporter = new HumanReporter();
|
|
197
|
+
let startT;
|
|
198
|
+
let exitCode = 0;
|
|
199
|
+
const keepAlive = setInterval(() => { }, 1 << 30);
|
|
200
|
+
let handle;
|
|
201
|
+
handle = tailJournal(journalPath, (entry) => {
|
|
202
|
+
if (json) {
|
|
203
|
+
process.stdout.write(`${JSON.stringify(entry)}\n`);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
switch (entry.type) {
|
|
207
|
+
case "workflow_start":
|
|
208
|
+
startT = Date.parse(String(entry.t));
|
|
209
|
+
reporter.start({ name: String(entry.name), description: "" }, String(entry.runId), dirname(journalPath));
|
|
210
|
+
break;
|
|
211
|
+
case "phase":
|
|
212
|
+
reporter.phase(String(entry.title));
|
|
213
|
+
break;
|
|
214
|
+
case "log":
|
|
215
|
+
reporter.log(String(entry.message));
|
|
216
|
+
break;
|
|
217
|
+
case "agent_start":
|
|
218
|
+
reporter.agentStart(Number(entry.id), String(entry.label), entry.spec, entry.phase, false);
|
|
219
|
+
break;
|
|
220
|
+
case "agent_end": {
|
|
221
|
+
if (entry.cached === true) {
|
|
222
|
+
reporter.agentStart(Number(entry.id), String(entry.label), {}, entry.phase, true);
|
|
223
|
+
}
|
|
224
|
+
const ok = entry.status === "ok";
|
|
225
|
+
reporter.agentEnd(Number(entry.id), ok, Number(entry.seconds ?? 0), ok ? (entry.cached ? "cached" : `${entry.toolCalls ?? 0} tools`) : String(entry.error ?? "error"));
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case "workflow_child_start":
|
|
229
|
+
reporter.log(`▸ workflow ${entry.name}`);
|
|
230
|
+
break;
|
|
231
|
+
case "workflow_child_end":
|
|
232
|
+
reporter.log(`${entry.status === "ok" ? "✔" : "✖"} workflow ${entry.name}`);
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (entry.type === "workflow_end") {
|
|
237
|
+
const endT = Date.parse(String(entry.t));
|
|
238
|
+
exitCode = entry.status === "ok" ? 0 : 1;
|
|
239
|
+
if (!json) {
|
|
240
|
+
reporter.done({
|
|
241
|
+
runId,
|
|
242
|
+
status: entry.status,
|
|
243
|
+
result: entry.result,
|
|
244
|
+
error: entry.error,
|
|
245
|
+
agents: Number(entry.agents ?? 0),
|
|
246
|
+
outputTokens: Number(entry.outputTokens ?? 0),
|
|
247
|
+
durationMs: startT ? Math.max(0, endT - startT) : 0,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
clearInterval(keepAlive);
|
|
251
|
+
handle.stop();
|
|
252
|
+
process.exit(exitCode);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
function commandSkip(positional) {
|
|
257
|
+
const runId = positional[0];
|
|
258
|
+
const rawAgentId = positional[1];
|
|
259
|
+
if (!runId || !rawAgentId)
|
|
260
|
+
fail("newton skip requires a run id and agent id");
|
|
261
|
+
const agentId = Number(rawAgentId);
|
|
262
|
+
if (!Number.isInteger(agentId) || agentId <= 0)
|
|
263
|
+
fail("agentId requires a positive integer");
|
|
264
|
+
try {
|
|
265
|
+
requestSkip(process.cwd(), runId, agentId);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
fail(error.message);
|
|
269
|
+
}
|
|
270
|
+
process.stdout.write(`skip requested: run ${runId} agent #${agentId}\n`);
|
|
271
|
+
const status = readStatus(join(process.cwd(), ".newton", "runs", runId));
|
|
272
|
+
if (status?.status !== "running") {
|
|
273
|
+
process.stderr.write(`warning: run ${runId} status is ${status?.status ?? "unknown"}; skip may have no effect\n`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
102
276
|
function commandValidate(positional) {
|
|
103
277
|
const file = positional[0];
|
|
104
278
|
if (!file)
|
|
@@ -184,6 +358,15 @@ async function main() {
|
|
|
184
358
|
case "run":
|
|
185
359
|
await commandRun(positional, flags);
|
|
186
360
|
break;
|
|
361
|
+
case "runs":
|
|
362
|
+
commandRuns(flags);
|
|
363
|
+
break;
|
|
364
|
+
case "watch":
|
|
365
|
+
await commandWatch(positional, flags);
|
|
366
|
+
break;
|
|
367
|
+
case "skip":
|
|
368
|
+
commandSkip(positional);
|
|
369
|
+
break;
|
|
187
370
|
case "validate":
|
|
188
371
|
try {
|
|
189
372
|
commandValidate(positional);
|
package/dist/control.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { closeSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function writeStatus(runDir, status) {
|
|
4
|
+
const tmpPath = join(runDir, "status.json.tmp");
|
|
5
|
+
const statusPath = join(runDir, "status.json");
|
|
6
|
+
writeFileSync(tmpPath, `${JSON.stringify(status, null, 2)}\n`, "utf8");
|
|
7
|
+
renameSync(tmpPath, statusPath);
|
|
8
|
+
}
|
|
9
|
+
export function readStatus(runDir) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(join(runDir, "status.json"), "utf8"));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function pidAlive(pid) {
|
|
18
|
+
try {
|
|
19
|
+
process.kill(pid, 0);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return errorCode(error) === "EPERM";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function listRuns(workspace) {
|
|
27
|
+
const runsDir = join(workspace, ".newton", "runs");
|
|
28
|
+
let runIds;
|
|
29
|
+
try {
|
|
30
|
+
runIds = readdirSync(runsDir, { withFileTypes: true })
|
|
31
|
+
.filter((entry) => entry.isDirectory())
|
|
32
|
+
.map((entry) => entry.name)
|
|
33
|
+
.sort()
|
|
34
|
+
.reverse();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
return runIds.map((runId) => {
|
|
40
|
+
const status = readStatus(join(runsDir, runId));
|
|
41
|
+
return {
|
|
42
|
+
runId,
|
|
43
|
+
status,
|
|
44
|
+
stale: status?.status === "running" && !pidAlive(status.pid),
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function requestSkip(workspace, runId, agentId) {
|
|
49
|
+
const runDir = join(workspace, ".newton", "runs", runId);
|
|
50
|
+
try {
|
|
51
|
+
if (!statSync(runDir).isDirectory()) {
|
|
52
|
+
throw new Error("not a directory");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new Error(`no run "${runId}" under .newton/runs`);
|
|
57
|
+
}
|
|
58
|
+
const controlDir = join(runDir, "control");
|
|
59
|
+
mkdirSync(controlDir, { recursive: true });
|
|
60
|
+
writeFileSync(join(controlDir, `skip-${agentId}`), "", "utf8");
|
|
61
|
+
}
|
|
62
|
+
export function readSkipRequests(runDir) {
|
|
63
|
+
const ids = new Set();
|
|
64
|
+
let entries;
|
|
65
|
+
try {
|
|
66
|
+
entries = readdirSync(join(runDir, "control"));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return ids;
|
|
70
|
+
}
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const match = /^skip-(\d+)$/.exec(entry);
|
|
73
|
+
if (!match)
|
|
74
|
+
continue;
|
|
75
|
+
const id = Number(match[1]);
|
|
76
|
+
if (Number.isInteger(id))
|
|
77
|
+
ids.add(id);
|
|
78
|
+
}
|
|
79
|
+
return ids;
|
|
80
|
+
}
|
|
81
|
+
export function tailJournal(journalPath, onEntry, pollMs = 300) {
|
|
82
|
+
let offset = 0;
|
|
83
|
+
let partial = Buffer.alloc(0);
|
|
84
|
+
let stopped = false;
|
|
85
|
+
function emitLine(line) {
|
|
86
|
+
const text = line.toString("utf8");
|
|
87
|
+
if (!text.trim())
|
|
88
|
+
return;
|
|
89
|
+
let entry;
|
|
90
|
+
try {
|
|
91
|
+
entry = JSON.parse(text);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Ignore incomplete/corrupt journal lines; later valid lines still stream.
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (entry !== null && typeof entry === "object" && !Array.isArray(entry)) {
|
|
98
|
+
onEntry(entry);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function consume(chunk) {
|
|
102
|
+
const data = partial.length === 0 ? chunk : Buffer.concat([partial, chunk]);
|
|
103
|
+
let lineStart = 0;
|
|
104
|
+
for (let i = 0; i < data.length; i++) {
|
|
105
|
+
if (data[i] !== 10)
|
|
106
|
+
continue;
|
|
107
|
+
emitLine(data.subarray(lineStart, i));
|
|
108
|
+
lineStart = i + 1;
|
|
109
|
+
}
|
|
110
|
+
partial = lineStart < data.length ? data.subarray(lineStart) : Buffer.alloc(0);
|
|
111
|
+
}
|
|
112
|
+
function tick() {
|
|
113
|
+
if (stopped)
|
|
114
|
+
return;
|
|
115
|
+
let size;
|
|
116
|
+
try {
|
|
117
|
+
const stat = statSync(journalPath);
|
|
118
|
+
if (!stat.isFile())
|
|
119
|
+
return;
|
|
120
|
+
size = stat.size;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (size < offset) {
|
|
126
|
+
offset = 0;
|
|
127
|
+
partial = Buffer.alloc(0);
|
|
128
|
+
}
|
|
129
|
+
if (size === offset)
|
|
130
|
+
return;
|
|
131
|
+
let fd;
|
|
132
|
+
try {
|
|
133
|
+
fd = openSync(journalPath, "r");
|
|
134
|
+
const length = size - offset;
|
|
135
|
+
const chunk = Buffer.alloc(length);
|
|
136
|
+
const bytesRead = readSync(fd, chunk, 0, length, offset);
|
|
137
|
+
if (bytesRead <= 0)
|
|
138
|
+
return;
|
|
139
|
+
offset += bytesRead;
|
|
140
|
+
consume(bytesRead === chunk.length ? chunk : chunk.subarray(0, bytesRead));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
if (fd !== undefined) {
|
|
147
|
+
try {
|
|
148
|
+
closeSync(fd);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// A close failure should not stop journal polling.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const timer = setInterval(tick, pollMs);
|
|
157
|
+
timer.unref();
|
|
158
|
+
return {
|
|
159
|
+
stop() {
|
|
160
|
+
stopped = true;
|
|
161
|
+
clearInterval(timer);
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function errorCode(error) {
|
|
166
|
+
return typeof error === "object" && error !== null && "code" in error
|
|
167
|
+
? error.code
|
|
168
|
+
: undefined;
|
|
169
|
+
}
|
package/dist/executor.js
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import { cpus } from "node:os";
|
|
3
|
-
import { resolve } from "node:path";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { AcpAgentSession } from "./acp.js";
|
|
5
|
-
import {
|
|
5
|
+
import { readSkipRequests, writeStatus } from "./control.js";
|
|
6
|
+
import { agentCallKey, loadResumeCache, loadResumeCalls, RunJournal } from "./journal.js";
|
|
7
|
+
import { resolveMcpServers } from "./mcp.js";
|
|
8
|
+
import { checkCompile, createRealm } from "./sandbox.js";
|
|
6
9
|
import { balancedSlice, extractJson, INVALID, structuredOutputInstruction, structuredOutputRetryPrompt, validateAgainstSchema, } from "./schema.js";
|
|
7
|
-
|
|
8
|
-
const SCRIPT_PARAMS = ["agent", "parallel", "pipeline", "phase", "log", "args", "budget", "
|
|
10
|
+
import { createWorktree, finishWorktree } from "./worktree.js";
|
|
11
|
+
const SCRIPT_PARAMS = ["agent", "parallel", "pipeline", "phase", "log", "args", "budget", "workflow"];
|
|
12
|
+
function errorMessage(error) {
|
|
13
|
+
return typeof error === "object" &&
|
|
14
|
+
error !== null &&
|
|
15
|
+
"message" in error &&
|
|
16
|
+
typeof error.message === "string"
|
|
17
|
+
? error.message
|
|
18
|
+
: String(error);
|
|
19
|
+
}
|
|
20
|
+
function isPlainObject(value) {
|
|
21
|
+
if (Object.prototype.toString.call(value) !== "[object Object]")
|
|
22
|
+
return false;
|
|
23
|
+
const proto = Object.getPrototypeOf(value);
|
|
24
|
+
return proto === null || Object.getPrototypeOf(proto) === null;
|
|
25
|
+
}
|
|
26
|
+
function validateConfig(value) {
|
|
27
|
+
if (value === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (!isPlainObject(value) || Object.values(value).some((entry) => typeof entry !== "string")) {
|
|
30
|
+
throw new Error("agent() config takes an object of configId → string value");
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
9
34
|
export function loadWorkflow(file) {
|
|
10
35
|
const absolute = resolve(file);
|
|
11
36
|
const source = readFileSync(absolute, "utf8");
|
|
@@ -33,33 +58,8 @@ export function loadWorkflow(file) {
|
|
|
33
58
|
}
|
|
34
59
|
/** Compile without running — catches syntax errors and non-literal meta. */
|
|
35
60
|
export function compileWorkflow(workflow) {
|
|
36
|
-
|
|
61
|
+
checkCompile(SCRIPT_PARAMS, workflow.body);
|
|
37
62
|
}
|
|
38
|
-
// Wall-clock and randomness would silently break resume replay, so scripts get
|
|
39
|
-
// guarded shims instead of the real globals. Pass timestamps in via --args.
|
|
40
|
-
const guardedDate = new Proxy(Date, {
|
|
41
|
-
get(target, prop, receiver) {
|
|
42
|
-
if (prop === "now") {
|
|
43
|
-
return () => {
|
|
44
|
-
throw new Error("Date.now() is unavailable in workflows (breaks resume); pass timestamps via --args");
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
return Reflect.get(target, prop, receiver);
|
|
48
|
-
},
|
|
49
|
-
construct(target, argArray) {
|
|
50
|
-
if (argArray.length === 0) {
|
|
51
|
-
throw new Error("new Date() without arguments is unavailable in workflows (breaks resume)");
|
|
52
|
-
}
|
|
53
|
-
return Reflect.construct(target, argArray);
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
const guardedMath = Object.create(Math, {
|
|
57
|
-
random: {
|
|
58
|
-
value: () => {
|
|
59
|
-
throw new Error("Math.random() is unavailable in workflows (breaks resume); vary prompts by index instead");
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
63
|
class Semaphore {
|
|
64
64
|
queue = [];
|
|
65
65
|
available;
|
|
@@ -89,11 +89,13 @@ const AGENT_PREAMBLE = [
|
|
|
89
89
|
].join("\n");
|
|
90
90
|
export async function runWorkflow(options) {
|
|
91
91
|
const startedAt = Date.now();
|
|
92
|
+
const statusStartedAt = new Date().toISOString();
|
|
92
93
|
const workflow = loadWorkflow(options.file);
|
|
93
|
-
const scriptFn = new AsyncFunction(...SCRIPT_PARAMS, workflow.body);
|
|
94
94
|
const workspace = resolve(options.cwd);
|
|
95
|
-
const journal = new RunJournal(workspace);
|
|
96
|
-
const
|
|
95
|
+
const journal = new RunJournal(workspace, options.runId);
|
|
96
|
+
const resumeMode = options.resumeMode ?? "prefix";
|
|
97
|
+
const resumeCache = options.resumeRunId && resumeMode === "hash" ? loadResumeCache(workspace, options.resumeRunId) : undefined;
|
|
98
|
+
const priorCalls = options.resumeRunId && resumeMode !== "hash" ? loadResumeCalls(workspace, options.resumeRunId) : undefined;
|
|
97
99
|
const reporter = options.reporter;
|
|
98
100
|
const maxAgents = options.maxAgents ?? 200;
|
|
99
101
|
const agentTimeoutMs = options.agentTimeoutMs ?? 20 * 60 * 1000;
|
|
@@ -103,6 +105,33 @@ export async function runWorkflow(options) {
|
|
|
103
105
|
let agentCounter = 0;
|
|
104
106
|
let outputTokens = 0;
|
|
105
107
|
let currentPhase;
|
|
108
|
+
let prefixIntact = true;
|
|
109
|
+
const liveSessions = new Map();
|
|
110
|
+
const skipped = new Set();
|
|
111
|
+
const controlTimer = setInterval(() => {
|
|
112
|
+
for (const id of readSkipRequests(journal.runDir)) {
|
|
113
|
+
if (skipped.has(id))
|
|
114
|
+
continue;
|
|
115
|
+
skipped.add(id);
|
|
116
|
+
liveSessions.get(id)?.abort("skipped via newton skip");
|
|
117
|
+
}
|
|
118
|
+
}, 500);
|
|
119
|
+
controlTimer.unref();
|
|
120
|
+
function syncStatus(status, extra = {}) {
|
|
121
|
+
writeStatus(journal.runDir, {
|
|
122
|
+
runId: journal.runId,
|
|
123
|
+
pid: process.pid,
|
|
124
|
+
file: workflow.file,
|
|
125
|
+
name: workflow.meta.name,
|
|
126
|
+
detached: options.detached ?? false,
|
|
127
|
+
startedAt: statusStartedAt,
|
|
128
|
+
updatedAt: new Date().toISOString(),
|
|
129
|
+
status,
|
|
130
|
+
agents: agentCounter,
|
|
131
|
+
outputTokens,
|
|
132
|
+
...extra,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
106
135
|
journal.append({
|
|
107
136
|
type: "workflow_start",
|
|
108
137
|
runId: journal.runId,
|
|
@@ -110,8 +139,10 @@ export async function runWorkflow(options) {
|
|
|
110
139
|
name: workflow.meta.name,
|
|
111
140
|
args: options.args ?? null,
|
|
112
141
|
resumedFrom: options.resumeRunId ?? null,
|
|
142
|
+
resumeMode: options.resumeRunId ? resumeMode : null,
|
|
113
143
|
});
|
|
114
144
|
reporter.start(workflow.meta, journal.runId, journal.runDir);
|
|
145
|
+
syncStatus("running");
|
|
115
146
|
const budget = {
|
|
116
147
|
total: budgetTotal,
|
|
117
148
|
spent: () => outputTokens,
|
|
@@ -135,31 +166,107 @@ export async function runWorkflow(options) {
|
|
|
135
166
|
if (budgetTotal !== null && outputTokens >= budgetTotal) {
|
|
136
167
|
throw new Error(`token budget exhausted (${outputTokens}/${budgetTotal} output tokens)`);
|
|
137
168
|
}
|
|
138
|
-
const
|
|
169
|
+
const config = validateConfig(callOptions.config);
|
|
170
|
+
const key = agentCallKey(prompt, { ...spec, isolation: callOptions.isolation, mcp: callOptions.mcp, config }, callOptions.schema ?? null);
|
|
139
171
|
const label = callOptions.label ?? prompt.replace(/\s+/g, " ").trim().slice(0, 64);
|
|
140
172
|
const phase = callOptions.phase ?? currentPhase;
|
|
173
|
+
if (priorCalls && prefixIntact) {
|
|
174
|
+
const prior = priorCalls.get(id);
|
|
175
|
+
if (!prior || prior.key !== key) {
|
|
176
|
+
prefixIntact = false;
|
|
177
|
+
}
|
|
178
|
+
else if (prior.ok) {
|
|
179
|
+
journal.append({ type: "agent_end", id, key, label, phase, status: "ok", cached: true, result: prior.result });
|
|
180
|
+
syncStatus("running");
|
|
181
|
+
reporter.agentStart(id, label, spec, phase, true);
|
|
182
|
+
reporter.agentEnd(id, true, 0, "cached");
|
|
183
|
+
return prior.result;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
141
186
|
const cachedQueue = resumeCache?.get(key);
|
|
142
187
|
if (cachedQueue && cachedQueue.length > 0) {
|
|
143
188
|
const result = cachedQueue.shift();
|
|
144
189
|
journal.append({ type: "agent_end", id, key, label, phase, status: "ok", cached: true, result });
|
|
190
|
+
syncStatus("running");
|
|
145
191
|
reporter.agentStart(id, label, spec, phase, true);
|
|
146
192
|
reporter.agentEnd(id, true, 0, "cached");
|
|
147
193
|
return result;
|
|
148
194
|
}
|
|
195
|
+
const mcpServers = callOptions.mcp !== undefined ? resolveMcpServers(workspace, callOptions.mcp) : undefined;
|
|
149
196
|
await semaphore.acquire();
|
|
150
197
|
const agentStartedAt = Date.now();
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
198
|
+
let session;
|
|
199
|
+
let wt;
|
|
200
|
+
let worktreeFinished = false;
|
|
201
|
+
// Idempotent so the finally can guarantee cleanup on a throw without
|
|
202
|
+
// double-finalizing the worktree that the normal path already removed.
|
|
203
|
+
const finishCreatedWorktree = async () => {
|
|
204
|
+
if (!wt || worktreeFinished)
|
|
205
|
+
return undefined;
|
|
206
|
+
worktreeFinished = true;
|
|
207
|
+
const wtInfo = await finishWorktree(workspace, wt);
|
|
208
|
+
const worktree = { path: wt.path, dirty: wtInfo.dirty, removed: wtInfo.removed };
|
|
209
|
+
if (wtInfo.dirty)
|
|
210
|
+
reporter.log(`worktree kept: ${wt.path}`);
|
|
211
|
+
return worktree;
|
|
212
|
+
};
|
|
154
213
|
try {
|
|
214
|
+
reporter.agentStart(id, label, spec, phase, false);
|
|
215
|
+
journal.append({ type: "agent_start", id, key, label, phase, spec, prompt });
|
|
216
|
+
const trace = journal.traceWriter(id);
|
|
217
|
+
if (skipped.has(id)) {
|
|
218
|
+
journal.append({ type: "agent_end", id, key, label, phase, status: "error", error: "skipped via newton skip", seconds: 0 });
|
|
219
|
+
syncStatus("running");
|
|
220
|
+
reporter.agentEnd(id, false, 0, "skipped");
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
if (callOptions.isolation === "worktree") {
|
|
224
|
+
try {
|
|
225
|
+
wt = await createWorktree(workspace, journal.runId, id);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
const message = errorMessage(error);
|
|
229
|
+
journal.append({ type: "agent_end", id, key, label, phase, status: "error", error: message, seconds: 0 });
|
|
230
|
+
syncStatus("running");
|
|
231
|
+
reporter.agentEnd(id, false, 0, message);
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (skipped.has(id)) {
|
|
236
|
+
const worktree = await finishCreatedWorktree();
|
|
237
|
+
journal.append({
|
|
238
|
+
type: "agent_end",
|
|
239
|
+
id,
|
|
240
|
+
key,
|
|
241
|
+
label,
|
|
242
|
+
phase,
|
|
243
|
+
status: "error",
|
|
244
|
+
error: "skipped via newton skip",
|
|
245
|
+
seconds: 0,
|
|
246
|
+
...(worktree ? { worktree } : {}),
|
|
247
|
+
});
|
|
248
|
+
syncStatus("running");
|
|
249
|
+
reporter.agentEnd(id, false, 0, "skipped");
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
155
252
|
const fullPrompt = AGENT_PREAMBLE + prompt + (callOptions.schema ? structuredOutputInstruction(callOptions.schema) : "");
|
|
156
|
-
const
|
|
253
|
+
const cwd = wt
|
|
254
|
+
? callOptions.cwd
|
|
255
|
+
? resolve(wt.path, callOptions.cwd)
|
|
256
|
+
: wt.path
|
|
257
|
+
: callOptions.cwd
|
|
258
|
+
? resolve(workspace, callOptions.cwd)
|
|
259
|
+
: workspace;
|
|
260
|
+
session = new AcpAgentSession({
|
|
157
261
|
spec,
|
|
158
262
|
prompt: fullPrompt,
|
|
159
|
-
cwd
|
|
263
|
+
cwd,
|
|
160
264
|
timeoutMs: callOptions.timeoutMs ?? agentTimeoutMs,
|
|
265
|
+
mcpServers,
|
|
266
|
+
config,
|
|
161
267
|
trace,
|
|
162
268
|
});
|
|
269
|
+
liveSessions.set(id, session);
|
|
163
270
|
let turn = await session.prompt();
|
|
164
271
|
let result = turn.text;
|
|
165
272
|
let failure = turn.error;
|
|
@@ -185,11 +292,23 @@ export async function runWorkflow(options) {
|
|
|
185
292
|
}
|
|
186
293
|
}
|
|
187
294
|
session.close();
|
|
295
|
+
const worktree = await finishCreatedWorktree();
|
|
188
296
|
const seconds = Math.round((Date.now() - agentStartedAt) / 1000);
|
|
189
297
|
const turnTokens = turn.usage?.outputTokens ?? 0;
|
|
190
298
|
outputTokens += turnTokens;
|
|
191
299
|
if (failure) {
|
|
192
|
-
journal.append({
|
|
300
|
+
journal.append({
|
|
301
|
+
type: "agent_end",
|
|
302
|
+
id,
|
|
303
|
+
key,
|
|
304
|
+
label,
|
|
305
|
+
phase,
|
|
306
|
+
status: "error",
|
|
307
|
+
error: failure,
|
|
308
|
+
seconds,
|
|
309
|
+
...(worktree ? { worktree } : {}),
|
|
310
|
+
});
|
|
311
|
+
syncStatus("running");
|
|
193
312
|
reporter.agentEnd(id, false, seconds, failure);
|
|
194
313
|
return null;
|
|
195
314
|
}
|
|
@@ -204,11 +323,19 @@ export async function runWorkflow(options) {
|
|
|
204
323
|
toolCalls: turn.toolCalls,
|
|
205
324
|
usage: turn.usage,
|
|
206
325
|
result,
|
|
326
|
+
...(worktree ? { worktree } : {}),
|
|
207
327
|
});
|
|
328
|
+
syncStatus("running");
|
|
208
329
|
reporter.agentEnd(id, true, seconds, turn.usage ? `${turn.toolCalls} tools · ${turnTokens} out-tok` : `${turn.toolCalls} tools`);
|
|
209
330
|
return result;
|
|
210
331
|
}
|
|
211
332
|
finally {
|
|
333
|
+
if (session) {
|
|
334
|
+
liveSessions.delete(id);
|
|
335
|
+
session.close();
|
|
336
|
+
}
|
|
337
|
+
// No-op unless a throw skipped the normal finalize (guard above).
|
|
338
|
+
await finishCreatedWorktree().catch(() => { });
|
|
212
339
|
semaphore.release();
|
|
213
340
|
}
|
|
214
341
|
}
|
|
@@ -219,7 +346,7 @@ export async function runWorkflow(options) {
|
|
|
219
346
|
return Promise.all(thunks.map((thunk) => Promise.resolve()
|
|
220
347
|
.then(thunk)
|
|
221
348
|
.catch((error) => {
|
|
222
|
-
const message =
|
|
349
|
+
const message = errorMessage(error);
|
|
223
350
|
journal.append({ type: "log", message: `parallel task failed: ${message}` });
|
|
224
351
|
reporter.log(`parallel task failed: ${message}`);
|
|
225
352
|
return null;
|
|
@@ -238,7 +365,7 @@ export async function runWorkflow(options) {
|
|
|
238
365
|
previous = await stage(previous, item, index);
|
|
239
366
|
}
|
|
240
367
|
catch (error) {
|
|
241
|
-
const message =
|
|
368
|
+
const message = errorMessage(error);
|
|
242
369
|
journal.append({ type: "log", message: `pipeline item ${index} dropped: ${message}` });
|
|
243
370
|
reporter.log(`pipeline item ${index} dropped: ${message}`);
|
|
244
371
|
return null;
|
|
@@ -253,14 +380,64 @@ export async function runWorkflow(options) {
|
|
|
253
380
|
currentPhase = title;
|
|
254
381
|
journal.append({ type: "phase", title });
|
|
255
382
|
reporter.phase(title);
|
|
383
|
+
syncStatus("running");
|
|
256
384
|
}
|
|
257
385
|
function logFn(message) {
|
|
258
386
|
const text = typeof message === "string" ? message : JSON.stringify(message);
|
|
259
387
|
journal.append({ type: "log", message: text });
|
|
260
388
|
reporter.log(text);
|
|
261
389
|
}
|
|
390
|
+
const realm = createRealm((message) => logFn(message));
|
|
391
|
+
async function executeScript(loaded, scriptArgs, depth) {
|
|
392
|
+
const scriptFn = realm.compile(SCRIPT_PARAMS, loaded.body, loaded.file);
|
|
393
|
+
return scriptFn(agentFn, parallelFn, pipelineFn, phaseFn, logFn, scriptArgs, budget, makeWorkflowFn(loaded.file, depth));
|
|
394
|
+
}
|
|
395
|
+
function makeWorkflowFn(callerFile, depth) {
|
|
396
|
+
return async (nameOrPath, childArgs) => {
|
|
397
|
+
if (depth >= 1)
|
|
398
|
+
throw new Error("workflow() nesting is limited to one level");
|
|
399
|
+
if (typeof nameOrPath !== "string")
|
|
400
|
+
throw new Error("workflow() takes a workflow name or path string");
|
|
401
|
+
const registryDir = join(workspace, ".newton", "workflows");
|
|
402
|
+
const file = /\.(js|mjs)$/.test(nameOrPath) || nameOrPath.includes("/")
|
|
403
|
+
? resolve(dirname(callerFile), nameOrPath)
|
|
404
|
+
: join(registryDir, `${nameOrPath}.js`);
|
|
405
|
+
if (!existsSync(file)) {
|
|
406
|
+
let names = [];
|
|
407
|
+
try {
|
|
408
|
+
names = readdirSync(registryDir, { withFileTypes: true })
|
|
409
|
+
.filter((entry) => entry.isFile() && /\.(js|mjs)$/.test(entry.name))
|
|
410
|
+
.map((entry) => entry.name.replace(/\.(?:js|mjs)$/, ""))
|
|
411
|
+
.sort();
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
names = [];
|
|
415
|
+
}
|
|
416
|
+
throw new Error(`workflow "${nameOrPath}" not found at ${file}; registered workflows: ${names.length > 0 ? names.join(", ") : "(none)"}`);
|
|
417
|
+
}
|
|
418
|
+
const child = loadWorkflow(file);
|
|
419
|
+
journal.append({ type: "workflow_child_start", name: child.meta.name, file: child.file, args: childArgs ?? null });
|
|
420
|
+
reporter.log(`▸ workflow ${child.meta.name}`);
|
|
421
|
+
const savedPhase = currentPhase;
|
|
422
|
+
try {
|
|
423
|
+
const result = await executeScript(child, childArgs, depth + 1);
|
|
424
|
+
journal.append({ type: "workflow_child_end", name: child.meta.name, status: "ok" });
|
|
425
|
+
reporter.log(`✔ workflow ${child.meta.name}`);
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
const message = errorMessage(error);
|
|
430
|
+
journal.append({ type: "workflow_child_end", name: child.meta.name, status: "error", error: message });
|
|
431
|
+
reporter.log(`✖ workflow ${child.meta.name}: ${message}`);
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
434
|
+
finally {
|
|
435
|
+
currentPhase = savedPhase;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
262
439
|
try {
|
|
263
|
-
const result = await
|
|
440
|
+
const result = await executeScript(workflow, options.args, 0);
|
|
264
441
|
const runResult = {
|
|
265
442
|
runId: journal.runId,
|
|
266
443
|
status: "ok",
|
|
@@ -270,11 +447,13 @@ export async function runWorkflow(options) {
|
|
|
270
447
|
durationMs: Date.now() - startedAt,
|
|
271
448
|
};
|
|
272
449
|
journal.append({ type: "workflow_end", status: "ok", result: runResult.result, agents: agentCounter, outputTokens });
|
|
450
|
+
syncStatus("ok", { result: runResult.result });
|
|
273
451
|
return runResult;
|
|
274
452
|
}
|
|
275
453
|
catch (error) {
|
|
276
|
-
const message =
|
|
454
|
+
const message = errorMessage(error);
|
|
277
455
|
journal.append({ type: "workflow_end", status: "error", error: message, agents: agentCounter, outputTokens });
|
|
456
|
+
syncStatus("error", { error: message });
|
|
278
457
|
return {
|
|
279
458
|
runId: journal.runId,
|
|
280
459
|
status: "error",
|
|
@@ -284,4 +463,7 @@ export async function runWorkflow(options) {
|
|
|
284
463
|
durationMs: Date.now() - startedAt,
|
|
285
464
|
};
|
|
286
465
|
}
|
|
466
|
+
finally {
|
|
467
|
+
clearInterval(controlTimer);
|
|
468
|
+
}
|
|
287
469
|
}
|
package/dist/journal.js
CHANGED
|
@@ -27,7 +27,7 @@ export class RunJournal {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
function newRunId() {
|
|
30
|
+
export function newRunId() {
|
|
31
31
|
const now = new Date();
|
|
32
32
|
const stamp = now
|
|
33
33
|
.toISOString()
|
|
@@ -43,8 +43,14 @@ export function agentCallKey(prompt, spec, schema) {
|
|
|
43
43
|
.slice(0, 32);
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
|
-
* Resume
|
|
47
|
-
*
|
|
46
|
+
* Resume has two modes:
|
|
47
|
+
* - prefix: load prior calls by positional id via loadResumeCalls.
|
|
48
|
+
* - hash: load successful results by (prompt, spec, schema) hash here.
|
|
49
|
+
*
|
|
50
|
+
* Invariant: journal `id` is the positional sequence. Agent ids increment at
|
|
51
|
+
* every agent() call, including cached calls.
|
|
52
|
+
*
|
|
53
|
+
* Duplicate hash keys replay in call order.
|
|
48
54
|
*/
|
|
49
55
|
export function loadResumeCache(workspace, runId) {
|
|
50
56
|
const journalPath = join(workspace, ".newton", "runs", runId, "journal.jsonl");
|
|
@@ -70,3 +76,34 @@ export function loadResumeCache(workspace, runId) {
|
|
|
70
76
|
}
|
|
71
77
|
return cache;
|
|
72
78
|
}
|
|
79
|
+
export function loadResumeCalls(workspace, runId) {
|
|
80
|
+
const journalPath = join(workspace, ".newton", "runs", runId, "journal.jsonl");
|
|
81
|
+
if (!existsSync(journalPath)) {
|
|
82
|
+
throw new Error(`No journal found for run "${runId}" (looked at ${journalPath})`);
|
|
83
|
+
}
|
|
84
|
+
const calls = new Map();
|
|
85
|
+
for (const line of readFileSync(journalPath, "utf8").split("\n")) {
|
|
86
|
+
if (!line.trim())
|
|
87
|
+
continue;
|
|
88
|
+
let entry;
|
|
89
|
+
try {
|
|
90
|
+
entry = JSON.parse(line);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const id = entry.id;
|
|
96
|
+
const key = entry.key;
|
|
97
|
+
if (typeof id !== "number" || !Number.isFinite(id) || typeof key !== "string")
|
|
98
|
+
continue;
|
|
99
|
+
if (entry.type === "agent_start") {
|
|
100
|
+
if (!calls.has(id)) {
|
|
101
|
+
calls.set(id, { key, ok: false });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (entry.type === "agent_end") {
|
|
105
|
+
calls.set(id, { key, ok: entry.status === "ok", result: entry.result });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return calls;
|
|
109
|
+
}
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function errorMessage(error) {
|
|
7
|
+
return isRecord(error) && typeof error.message === "string" ? error.message : String(error);
|
|
8
|
+
}
|
|
9
|
+
function readMcpConfig(workspace) {
|
|
10
|
+
const path = join(workspace, ".mcp.json");
|
|
11
|
+
let parsed;
|
|
12
|
+
try {
|
|
13
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (isRecord(error) && error.code === "ENOENT") {
|
|
17
|
+
throw new Error(`MCP config file not found at ${path}`);
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`Could not read ${path}: ${errorMessage(error)}`);
|
|
20
|
+
}
|
|
21
|
+
if (!isRecord(parsed) || !isRecord(parsed.mcpServers)) {
|
|
22
|
+
throw new Error(`${path}: expected { "mcpServers": { ... } }`);
|
|
23
|
+
}
|
|
24
|
+
return { path, servers: parsed.mcpServers };
|
|
25
|
+
}
|
|
26
|
+
function availableNames(servers) {
|
|
27
|
+
const names = Object.keys(servers).sort();
|
|
28
|
+
return names.length > 0 ? names.join(", ") : "(none)";
|
|
29
|
+
}
|
|
30
|
+
function optionalStringArray(value, field) {
|
|
31
|
+
if (value === undefined || value === null)
|
|
32
|
+
return [];
|
|
33
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
34
|
+
throw new Error(`${field} must be an array of strings`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function nameValueEntries(value, field) {
|
|
39
|
+
if (value === undefined || value === null)
|
|
40
|
+
return [];
|
|
41
|
+
if (!isRecord(value))
|
|
42
|
+
throw new Error(`${field} must be an object`);
|
|
43
|
+
return Object.entries(value).map(([name, entryValue]) => ({ name, value: String(entryValue) }));
|
|
44
|
+
}
|
|
45
|
+
function toMcpServer(name, value, source) {
|
|
46
|
+
if (!isRecord(value))
|
|
47
|
+
throw new Error(`${source} must be an object`);
|
|
48
|
+
if (value.type !== undefined) {
|
|
49
|
+
if (value.type !== "http" && value.type !== "sse") {
|
|
50
|
+
throw new Error(`${source} type must be "http" or "sse"`);
|
|
51
|
+
}
|
|
52
|
+
if (typeof value.url !== "string" || !value.url) {
|
|
53
|
+
throw new Error(`${source} missing url`);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
type: value.type,
|
|
57
|
+
name,
|
|
58
|
+
url: value.url,
|
|
59
|
+
headers: nameValueEntries(value.headers, `${source} headers`),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (typeof value.command !== "string" || !value.command) {
|
|
63
|
+
throw new Error(`${source} missing command`);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
name,
|
|
67
|
+
command: value.command,
|
|
68
|
+
args: optionalStringArray(value.args, `${source} args`),
|
|
69
|
+
env: nameValueEntries(value.env, `${source} env`),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function resolveMcpServers(workspace, opt) {
|
|
73
|
+
if (opt === true) {
|
|
74
|
+
const config = readMcpConfig(workspace);
|
|
75
|
+
return Object.entries(config.servers).map(([name, server]) => toMcpServer(name, server, `MCP server "${name}"`));
|
|
76
|
+
}
|
|
77
|
+
if (!Array.isArray(opt)) {
|
|
78
|
+
throw new Error("agent() mcp option takes true or an array of names/servers");
|
|
79
|
+
}
|
|
80
|
+
let config;
|
|
81
|
+
const getConfig = () => {
|
|
82
|
+
config ??= readMcpConfig(workspace);
|
|
83
|
+
return config;
|
|
84
|
+
};
|
|
85
|
+
return opt.map((entry) => {
|
|
86
|
+
if (typeof entry === "string") {
|
|
87
|
+
const loaded = getConfig();
|
|
88
|
+
const server = loaded.servers[entry];
|
|
89
|
+
if (server === undefined) {
|
|
90
|
+
throw new Error(`MCP server "${entry}" not found in ${loaded.path}; available: ${availableNames(loaded.servers)}`);
|
|
91
|
+
}
|
|
92
|
+
return toMcpServer(entry, server, `MCP server "${entry}"`);
|
|
93
|
+
}
|
|
94
|
+
if (!isRecord(entry)) {
|
|
95
|
+
throw new Error("agent() mcp option takes true or an array of names/servers");
|
|
96
|
+
}
|
|
97
|
+
if (typeof entry.name !== "string" || !entry.name.trim()) {
|
|
98
|
+
throw new Error("inline MCP server missing name");
|
|
99
|
+
}
|
|
100
|
+
return toMcpServer(entry.name, entry, `inline MCP server "${entry.name}"`);
|
|
101
|
+
});
|
|
102
|
+
}
|
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as vm from "node:vm";
|
|
2
|
+
export function createRealm(consoleLog) {
|
|
3
|
+
const context = vm.createContext({}, { codeGeneration: { strings: false, wasm: false } });
|
|
4
|
+
const hostCallables = new Map();
|
|
5
|
+
let nextCallableId = 1;
|
|
6
|
+
let makeRealmCallable;
|
|
7
|
+
let makeRealmArray;
|
|
8
|
+
let makeRealmObject;
|
|
9
|
+
vm.runInContext(`
|
|
10
|
+
{
|
|
11
|
+
const RealmDate = Date;
|
|
12
|
+
const dateNowUnavailable = () => {
|
|
13
|
+
throw new Error("Date.now() is unavailable in workflows (breaks resume); pass timestamps via --args");
|
|
14
|
+
};
|
|
15
|
+
const mathRandomUnavailable = () => {
|
|
16
|
+
throw new Error("Math.random() is unavailable in workflows (breaks resume); vary prompts by index instead");
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const GuardedDate = new Proxy(RealmDate, {
|
|
20
|
+
get(target, prop, receiver) {
|
|
21
|
+
if (prop === "now") return dateNowUnavailable;
|
|
22
|
+
return Reflect.get(target, prop, receiver);
|
|
23
|
+
},
|
|
24
|
+
construct(target, argArray) {
|
|
25
|
+
if (argArray.length === 0) {
|
|
26
|
+
throw new Error("new Date() without arguments is unavailable in workflows (breaks resume)");
|
|
27
|
+
}
|
|
28
|
+
return Reflect.construct(target, argArray);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
globalThis.Date = GuardedDate;
|
|
32
|
+
Object.defineProperty(RealmDate.prototype, "constructor", {
|
|
33
|
+
value: GuardedDate,
|
|
34
|
+
writable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
Math.random = mathRandomUnavailable;
|
|
39
|
+
}
|
|
40
|
+
`, context);
|
|
41
|
+
const bridge = (id, args) => {
|
|
42
|
+
const callable = hostCallables.get(id);
|
|
43
|
+
if (!callable)
|
|
44
|
+
throw new Error(`unknown workflow host function ${id}`);
|
|
45
|
+
return callable(...args);
|
|
46
|
+
};
|
|
47
|
+
context.__newtonBridge = bridge;
|
|
48
|
+
const helpers = vm.runInContext(`
|
|
49
|
+
(() => {
|
|
50
|
+
const bridge = globalThis.__newtonBridge;
|
|
51
|
+
delete globalThis.__newtonBridge;
|
|
52
|
+
|
|
53
|
+
const importValue = (value, seen = new WeakMap()) => {
|
|
54
|
+
if (value === null || value === undefined) return value;
|
|
55
|
+
const kind = typeof value;
|
|
56
|
+
if (kind !== "object") return kind === "function" ? undefined : value;
|
|
57
|
+
const existing = seen.get(value);
|
|
58
|
+
if (existing) return existing;
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
const copy = [];
|
|
61
|
+
seen.set(value, copy);
|
|
62
|
+
for (let i = 0; i < value.length; i++) copy[i] = importValue(value[i], seen);
|
|
63
|
+
return copy;
|
|
64
|
+
}
|
|
65
|
+
const copy = {};
|
|
66
|
+
seen.set(value, copy);
|
|
67
|
+
for (const key of Object.keys(value)) copy[key] = importValue(value[key], seen);
|
|
68
|
+
return copy;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const importError = (error) =>
|
|
72
|
+
new Error(error && typeof error === "object" && typeof error.message === "string" ? error.message : String(error));
|
|
73
|
+
|
|
74
|
+
const makeCallable = (id) => (...args) => {
|
|
75
|
+
try {
|
|
76
|
+
const result = bridge(id, args);
|
|
77
|
+
if (result && typeof result === "object" && typeof result.then === "function") {
|
|
78
|
+
return Promise.resolve(result).then(
|
|
79
|
+
(value) => importValue(value),
|
|
80
|
+
(error) => {
|
|
81
|
+
throw importError(error);
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return importValue(result);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw importError(error);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
makeCallable,
|
|
93
|
+
makeArray: (items) => Array.from(items),
|
|
94
|
+
makeObject: (entries) => {
|
|
95
|
+
const object = {};
|
|
96
|
+
for (const [key, value] of entries) object[key] = value;
|
|
97
|
+
return object;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
})()
|
|
101
|
+
`, context);
|
|
102
|
+
makeRealmCallable = helpers.makeCallable;
|
|
103
|
+
makeRealmArray = helpers.makeArray;
|
|
104
|
+
makeRealmObject = helpers.makeObject;
|
|
105
|
+
const wrapHostCallable = (callable) => {
|
|
106
|
+
const id = nextCallableId++;
|
|
107
|
+
hostCallables.set(id, callable);
|
|
108
|
+
return makeRealmCallable(id);
|
|
109
|
+
};
|
|
110
|
+
const toRealmValue = (value) => {
|
|
111
|
+
if (value === null || value === undefined)
|
|
112
|
+
return value;
|
|
113
|
+
if (typeof value === "function")
|
|
114
|
+
return wrapHostCallable(value);
|
|
115
|
+
if (typeof value !== "object")
|
|
116
|
+
return value;
|
|
117
|
+
if (Array.isArray(value))
|
|
118
|
+
return makeRealmArray(value.map((item) => toRealmValue(item)));
|
|
119
|
+
return makeRealmObject(Object.entries(value).map(([key, entry]) => [key, toRealmValue(entry)]));
|
|
120
|
+
};
|
|
121
|
+
const writeConsole = (...args) => {
|
|
122
|
+
consoleLog(args.map(formatConsoleArg).join(" "));
|
|
123
|
+
};
|
|
124
|
+
context.__newtonConsole = makeRealmObject([
|
|
125
|
+
["log", wrapHostCallable(writeConsole)],
|
|
126
|
+
["info", wrapHostCallable(writeConsole)],
|
|
127
|
+
["warn", wrapHostCallable(writeConsole)],
|
|
128
|
+
["error", wrapHostCallable(writeConsole)],
|
|
129
|
+
["debug", wrapHostCallable(writeConsole)],
|
|
130
|
+
]);
|
|
131
|
+
vm.runInContext(`
|
|
132
|
+
globalThis.console = globalThis.__newtonConsole;
|
|
133
|
+
delete globalThis.__newtonConsole;
|
|
134
|
+
`, context);
|
|
135
|
+
return {
|
|
136
|
+
compile(params, body, filename) {
|
|
137
|
+
const scriptFn = vm.runInContext(`(async function(${params.join(",")}) {"use strict";\n${body}\n})`, context, {
|
|
138
|
+
filename,
|
|
139
|
+
});
|
|
140
|
+
return (...args) => scriptFn(...args.map((arg) => toRealmValue(arg)));
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function checkCompile(params, body) {
|
|
145
|
+
new vm.Script('(async function(' + params.join(",") + ') {"use strict";\n' + body + "\n})");
|
|
146
|
+
}
|
|
147
|
+
function formatConsoleArg(arg) {
|
|
148
|
+
if (typeof arg === "string")
|
|
149
|
+
return arg;
|
|
150
|
+
try {
|
|
151
|
+
const json = JSON.stringify(arg);
|
|
152
|
+
return json === undefined ? String(arg) : json;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return String(arg);
|
|
156
|
+
}
|
|
157
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.4.0";
|
package/dist/worktree.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdirSync, rmdirSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
export async function createWorktree(workspace, runId, agentId) {
|
|
7
|
+
const path = join(workspace, ".newton", "worktrees", runId, `agent-${agentId}`);
|
|
8
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
9
|
+
try {
|
|
10
|
+
// Detached HEAD avoids branch-name coordination across parallel agents.
|
|
11
|
+
await git(["-C", workspace, "worktree", "add", "--detach", path, "HEAD"]);
|
|
12
|
+
const baseHead = (await git(["-C", path, "rev-parse", "HEAD"])).trim();
|
|
13
|
+
return { path, baseHead };
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
throw new Error(`worktree setup failed: ${gitStderr(error)}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function finishWorktree(workspace, wt) {
|
|
20
|
+
try {
|
|
21
|
+
const status = await git(["-C", wt.path, "status", "--porcelain"]);
|
|
22
|
+
const head = (await git(["-C", wt.path, "rev-parse", "HEAD"])).trim();
|
|
23
|
+
const dirty = status.trim().length > 0 || head !== wt.baseHead;
|
|
24
|
+
if (dirty)
|
|
25
|
+
return { dirty: true, removed: false };
|
|
26
|
+
// Force is intentional after the clean check: this is disposable isolation.
|
|
27
|
+
await git(["-C", workspace, "worktree", "remove", "--force", wt.path]);
|
|
28
|
+
try {
|
|
29
|
+
rmdirSync(dirname(wt.path));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Best-effort cleanup of the per-run directory.
|
|
33
|
+
}
|
|
34
|
+
return { dirty: false, removed: true };
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { dirty: true, removed: false };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function git(args) {
|
|
41
|
+
const { stdout } = await execFileAsync("git", args);
|
|
42
|
+
return stdout.toString();
|
|
43
|
+
}
|
|
44
|
+
function gitStderr(error) {
|
|
45
|
+
if (typeof error === "object" && error !== null && "stderr" in error) {
|
|
46
|
+
const stderr = String(error.stderr ?? "").trim();
|
|
47
|
+
if (stderr)
|
|
48
|
+
return stderr;
|
|
49
|
+
}
|
|
50
|
+
return error instanceof Error ? error.message : String(error);
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gigaai/newton",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Deterministic multi-agent workflow executor. Write workflows in plain JavaScript; Newton drives coding agents over the Agent Client Protocol.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|