@gigaai/newton 0.3.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 +142 -0
- package/dist/acp.js +257 -0
- package/dist/cli.js +220 -0
- package/dist/executor.js +287 -0
- package/dist/journal.js +72 -0
- package/dist/progress.js +61 -0
- package/dist/providers.js +255 -0
- package/dist/schema.js +184 -0
- package/dist/version.js +1 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Newton
|
|
2
|
+
|
|
3
|
+
Newton runs deterministic multi-agent workflows. A workflow is a plain
|
|
4
|
+
JavaScript file; the control flow (loops, fan-out, conditionals) is your code,
|
|
5
|
+
and the non-deterministic parts are delegated to coding agents over the
|
|
6
|
+
[Agent Client Protocol](https://agentclientprotocol.com) — Codex, Claude Code,
|
|
7
|
+
Gemini CLI, or any other ACP-speaking agent.
|
|
8
|
+
|
|
9
|
+
No config files, no server, no accounts. `newton run workflow.js` in a
|
|
10
|
+
workspace and it works.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g @gigaai/newton # or: npx @gigaai/newton
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or grab a standalone binary from
|
|
19
|
+
[Releases](https://github.com/GigaML/newton/releases) — no Node required.
|
|
20
|
+
|
|
21
|
+
Agents authenticate themselves: `openai` uses your existing `codex` login (or
|
|
22
|
+
`OPENAI_API_KEY` / `CODEX_API_KEY`). Check your environment with `newton doctor`.
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
// hello.js
|
|
28
|
+
export const meta = {
|
|
29
|
+
name: 'hello',
|
|
30
|
+
description: 'Two scouts in parallel, then a structured synthesis.',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
phase('Scout')
|
|
34
|
+
const notes = await parallel([
|
|
35
|
+
() => agent('Describe the layout of this repository.', { provider: 'openai', model: 'gpt-5.5', access: 'read' }),
|
|
36
|
+
() => agent('Find the entry points of this repository.', { provider: 'openai', model: 'gpt-5.5', access: 'read' }),
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
phase('Synthesize')
|
|
40
|
+
const report = await agent(`Merge these notes:\n\n${notes.filter(Boolean).join('\n\n')}`, {
|
|
41
|
+
provider: 'openai',
|
|
42
|
+
model: 'gpt-5.5',
|
|
43
|
+
access: 'read',
|
|
44
|
+
schema: { type: 'object', required: ['summary'], properties: { summary: { type: 'string' } } },
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
return { report }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
newton run hello.js
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Progress streams to stderr; the result JSON prints to stdout. Pass `--json`
|
|
55
|
+
for NDJSON events instead — built for driving Newton from another agent.
|
|
56
|
+
|
|
57
|
+
## Workflow API
|
|
58
|
+
|
|
59
|
+
A workflow file is `export const meta = { name, description }` followed by a
|
|
60
|
+
script body. The body runs as an async function with these in scope:
|
|
61
|
+
|
|
62
|
+
| Primitive | What it does |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `agent(prompt, opts?)` | Run one agent turn; returns its final text, or a schema-validated object when `opts.schema` is set. Returns `null` on agent failure — filter with `.filter(Boolean)`. |
|
|
65
|
+
| `parallel(thunks)` | Run `() => …` tasks concurrently; a failed task resolves to `null`. Barrier: waits for all. |
|
|
66
|
+
| `pipeline(items, ...stages)` | Each item flows through the stages independently — no barrier between stages. Stage signature: `(prev, item, index)`. |
|
|
67
|
+
| `phase(title)` | Group subsequent agents in progress output. |
|
|
68
|
+
| `log(message)` | Narrator line in progress output and the journal. |
|
|
69
|
+
| `args` | Whatever you passed via `--args` (JSON-parsed when possible). |
|
|
70
|
+
| `budget` | `{ total, spent(), remaining() }` for the `--budget` output-token ceiling. |
|
|
71
|
+
|
|
72
|
+
`agent()` options: `provider` (see below), `model`, `effort` (`low`–`xhigh`),
|
|
73
|
+
`access` (`read` | `write` | `full` — sandbox posture), `schema` (JSON Schema
|
|
74
|
+
for structured output, validated with automatic retries), `label`, `phase`,
|
|
75
|
+
`cwd`, `timeoutMs`, and `command` — an escape hatch to run any ACP agent,
|
|
76
|
+
e.g. `command: ['my-agent', '--acp']`.
|
|
77
|
+
|
|
78
|
+
## Providers
|
|
79
|
+
|
|
80
|
+
| Provider | Aliases | Model override | Runs via |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| `codex` | `openai` | yes | bundled codex-acp adapter |
|
|
83
|
+
| `claude` | `anthropic` | yes | claude-agent-acp adapter |
|
|
84
|
+
| `gemini` | `google` | yes | `gemini --experimental-acp` |
|
|
85
|
+
| `qwen` | `qwen-code` | yes | `qwen --acp` |
|
|
86
|
+
| `cursor` | `cursor-agent` | no | cursor-agent-acp adapter |
|
|
87
|
+
| `opencode` | | no | `opencode acp` |
|
|
88
|
+
| `goose` | | no | `goose acp` |
|
|
89
|
+
| `copilot` | `github-copilot` | no | copilot-language-server `--acp` |
|
|
90
|
+
|
|
91
|
+
Resolution order per provider: newton's own node_modules → PATH → `npx -y`.
|
|
92
|
+
Agents authenticate themselves with their normal logins; `newton agents`
|
|
93
|
+
detects which ones are installed and signed in on this machine.
|
|
94
|
+
|
|
95
|
+
Model selection works two ways: providers with a CLI/env knob get it passed
|
|
96
|
+
directly, and for everything else Newton matches `model:` against the models
|
|
97
|
+
the agent advertises over ACP (session config options) and selects it
|
|
98
|
+
protocol-side. `newton models <provider>` shows what an agent exposes;
|
|
99
|
+
requesting a model an agent doesn't offer fails with the available list.
|
|
100
|
+
|
|
101
|
+
`Date.now()`, argless `new Date()`, and `Math.random()` throw inside
|
|
102
|
+
workflows: nondeterminism would silently break `--resume` replay. Pass
|
|
103
|
+
timestamps in via `--args`.
|
|
104
|
+
|
|
105
|
+
## Runs, traces, resume
|
|
106
|
+
|
|
107
|
+
Every run writes durable artifacts to `.newton/runs/<runId>/` in the
|
|
108
|
+
workspace:
|
|
109
|
+
|
|
110
|
+
- `journal.jsonl` — every lifecycle event, including each agent's prompt and
|
|
111
|
+
full result.
|
|
112
|
+
- `agents/<n>.jsonl` — the complete ACP wire trace per agent: every message
|
|
113
|
+
chunk, tool call, and permission decision.
|
|
114
|
+
|
|
115
|
+
`newton run workflow.js --resume <runId>` replays successful agent results
|
|
116
|
+
from a previous run's journal — identical calls return instantly and only new
|
|
117
|
+
or changed calls hit real agents.
|
|
118
|
+
|
|
119
|
+
## CLI
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
newton run <workflow.js> --args <json> --cwd <dir> --resume <runId> --json
|
|
123
|
+
--budget <tokens> --max-agents <n> --timeout <seconds>
|
|
124
|
+
--concurrency <n>
|
|
125
|
+
newton validate <workflow.js> parse + compile without spawning agents
|
|
126
|
+
newton agents [--json] supported agents: installed? signed in? (alias: doctor)
|
|
127
|
+
newton models <provider> ask an agent which models/modes it exposes
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Exit codes: `0` success, `1` workflow error, `2` usage error.
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npm install
|
|
136
|
+
npm run check # build + validate the example
|
|
137
|
+
node dist/cli.js run examples/hello.js
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Standalone binaries are built with `bun build --compile` (see
|
|
141
|
+
`.github/workflows/release.yml`); pushing a `v*` tag publishes to npm and
|
|
142
|
+
attaches binaries to a GitHub Release.
|
package/dist/acp.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { Readable, Writable } from "node:stream";
|
|
5
|
+
import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION, } from "@agentclientprotocol/sdk";
|
|
6
|
+
import { resolveAgentCommand } from "./providers.js";
|
|
7
|
+
import { VERSION } from "./version.js";
|
|
8
|
+
const READ_ONLY_TOOL_KINDS = new Set(["read", "search", "fetch", "think", "other"]);
|
|
9
|
+
function pickPermissionOption(params, access) {
|
|
10
|
+
const readOnly = access === "read";
|
|
11
|
+
const toolKind = params.toolCall.kind ?? "other";
|
|
12
|
+
const wantAllow = !readOnly || READ_ONLY_TOOL_KINDS.has(toolKind);
|
|
13
|
+
const preference = wantAllow
|
|
14
|
+
? ["allow_always", "allow_once"]
|
|
15
|
+
: ["reject_once", "reject_always"];
|
|
16
|
+
for (const kind of preference) {
|
|
17
|
+
const option = params.options.find((o) => o.kind === kind);
|
|
18
|
+
if (option)
|
|
19
|
+
return { outcome: { outcome: "selected", optionId: option.optionId } };
|
|
20
|
+
}
|
|
21
|
+
const fallback = params.options[0];
|
|
22
|
+
if (fallback)
|
|
23
|
+
return { outcome: { outcome: "selected", optionId: fallback.optionId } };
|
|
24
|
+
return { outcome: { outcome: "cancelled" } };
|
|
25
|
+
}
|
|
26
|
+
const liveChildren = new Set();
|
|
27
|
+
function killAll() {
|
|
28
|
+
for (const child of liveChildren)
|
|
29
|
+
child.kill("SIGKILL");
|
|
30
|
+
}
|
|
31
|
+
process.once("exit", killAll);
|
|
32
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
33
|
+
process.once(signal, () => {
|
|
34
|
+
killAll();
|
|
35
|
+
process.exit(130);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Run one agent turn over ACP: spawn the agent, initialize, open a session,
|
|
40
|
+
* send the prompt, and collect streamed output until the turn stops.
|
|
41
|
+
* `promptAgain` allows follow-up turns in the same session (structured-output retries).
|
|
42
|
+
*/
|
|
43
|
+
export class AcpAgentSession {
|
|
44
|
+
child;
|
|
45
|
+
connection;
|
|
46
|
+
sessionId;
|
|
47
|
+
sessionInfo;
|
|
48
|
+
request;
|
|
49
|
+
text = "";
|
|
50
|
+
toolCalls = 0;
|
|
51
|
+
usage = null;
|
|
52
|
+
stderrTail = [];
|
|
53
|
+
constructor(request) {
|
|
54
|
+
this.request = request;
|
|
55
|
+
}
|
|
56
|
+
async prompt() {
|
|
57
|
+
try {
|
|
58
|
+
await this.ensureSession();
|
|
59
|
+
return await this.promptAgain(this.request.prompt);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return this.failure(error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Open a session and report what the agent exposes, without prompting. */
|
|
66
|
+
async inspect() {
|
|
67
|
+
await this.ensureSession();
|
|
68
|
+
return {
|
|
69
|
+
modes: this.sessionInfo?.modes ?? null,
|
|
70
|
+
configOptions: this.sessionInfo?.configOptions ?? [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async promptAgain(prompt) {
|
|
74
|
+
try {
|
|
75
|
+
const connection = this.connection;
|
|
76
|
+
const sessionId = this.sessionId;
|
|
77
|
+
this.text = "";
|
|
78
|
+
this.request.trace({ type: "prompt", prompt });
|
|
79
|
+
let timer;
|
|
80
|
+
const timedOut = new Promise((resolveTimeout) => {
|
|
81
|
+
timer = setTimeout(() => resolveTimeout("timeout"), this.request.timeoutMs);
|
|
82
|
+
});
|
|
83
|
+
const turn = connection.prompt({
|
|
84
|
+
sessionId,
|
|
85
|
+
prompt: [{ type: "text", text: prompt }],
|
|
86
|
+
});
|
|
87
|
+
const outcome = await Promise.race([turn, timedOut]);
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
if (outcome === "timeout") {
|
|
90
|
+
await connection.cancel({ sessionId }).catch(() => { });
|
|
91
|
+
// Give the agent a moment to flush final updates, then hard-stop.
|
|
92
|
+
await Promise.race([turn.catch(() => { }), new Promise((r) => setTimeout(r, 3000))]);
|
|
93
|
+
this.close();
|
|
94
|
+
return {
|
|
95
|
+
text: this.text,
|
|
96
|
+
stopReason: "timeout",
|
|
97
|
+
usage: this.usage,
|
|
98
|
+
toolCalls: this.toolCalls,
|
|
99
|
+
error: `agent timed out after ${this.request.timeoutMs}ms`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
this.usage = outcome.usage ?? this.usage;
|
|
103
|
+
return {
|
|
104
|
+
text: this.text,
|
|
105
|
+
stopReason: outcome.stopReason,
|
|
106
|
+
usage: this.usage,
|
|
107
|
+
toolCalls: this.toolCalls,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return this.failure(error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
close() {
|
|
115
|
+
if (this.child && !this.child.killed)
|
|
116
|
+
this.child.kill("SIGTERM");
|
|
117
|
+
if (this.child)
|
|
118
|
+
liveChildren.delete(this.child);
|
|
119
|
+
this.child = undefined;
|
|
120
|
+
}
|
|
121
|
+
failure(error) {
|
|
122
|
+
this.close();
|
|
123
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
124
|
+
const stderr = this.stderrTail.join("").trim();
|
|
125
|
+
this.request.trace({ type: "error", message, stderr });
|
|
126
|
+
return {
|
|
127
|
+
text: this.text,
|
|
128
|
+
stopReason: "error",
|
|
129
|
+
usage: this.usage,
|
|
130
|
+
toolCalls: this.toolCalls,
|
|
131
|
+
error: stderr ? `${message}\n${stderr.slice(-2000)}` : message,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async ensureSession() {
|
|
135
|
+
if (this.sessionId)
|
|
136
|
+
return;
|
|
137
|
+
const { spec, cwd, trace } = this.request;
|
|
138
|
+
const resolved = resolveAgentCommand(spec);
|
|
139
|
+
trace({ type: "spawn", command: resolved.command, args: resolved.args });
|
|
140
|
+
const child = spawn(resolved.command, resolved.args, {
|
|
141
|
+
cwd,
|
|
142
|
+
env: { ...process.env, ...resolved.env },
|
|
143
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
144
|
+
});
|
|
145
|
+
this.child = child;
|
|
146
|
+
liveChildren.add(child);
|
|
147
|
+
child.once("exit", () => liveChildren.delete(child));
|
|
148
|
+
child.stderr.setEncoding("utf8");
|
|
149
|
+
child.stderr.on("data", (data) => {
|
|
150
|
+
this.stderrTail.push(data);
|
|
151
|
+
if (this.stderrTail.length > 50)
|
|
152
|
+
this.stderrTail.shift();
|
|
153
|
+
trace({ type: "stderr", data });
|
|
154
|
+
});
|
|
155
|
+
const spawned = new Promise((resolveSpawn, rejectSpawn) => {
|
|
156
|
+
child.once("spawn", () => resolveSpawn());
|
|
157
|
+
child.once("error", (error) => rejectSpawn(error));
|
|
158
|
+
});
|
|
159
|
+
await spawned;
|
|
160
|
+
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
161
|
+
const clientHandler = {
|
|
162
|
+
requestPermission: (params) => {
|
|
163
|
+
const response = pickPermissionOption(params, this.request.spec.access);
|
|
164
|
+
trace({ type: "permission", request: params, response });
|
|
165
|
+
return response;
|
|
166
|
+
},
|
|
167
|
+
sessionUpdate: (params) => this.handleUpdate(params),
|
|
168
|
+
readTextFile: async (params) => {
|
|
169
|
+
const content = await readFile(resolve(cwd, params.path), "utf8");
|
|
170
|
+
return { content };
|
|
171
|
+
},
|
|
172
|
+
writeTextFile: async (params) => {
|
|
173
|
+
if (this.request.spec.access === "read") {
|
|
174
|
+
throw new Error("write rejected: agent has read-only access");
|
|
175
|
+
}
|
|
176
|
+
await writeFile(resolve(cwd, params.path), params.content, "utf8");
|
|
177
|
+
return {};
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
const connection = new ClientSideConnection(() => clientHandler, stream);
|
|
181
|
+
this.connection = connection;
|
|
182
|
+
await connection.initialize({
|
|
183
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
184
|
+
clientInfo: { name: "newton", version: VERSION },
|
|
185
|
+
clientCapabilities: {
|
|
186
|
+
fs: { readTextFile: true, writeTextFile: true },
|
|
187
|
+
session: { configOptions: {} },
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
const session = await connection.newSession({ cwd: resolve(cwd), mcpServers: [] });
|
|
191
|
+
this.sessionId = session.sessionId;
|
|
192
|
+
this.sessionInfo = session;
|
|
193
|
+
trace({ type: "session", sessionId: session.sessionId, configOptions: session.configOptions ?? null });
|
|
194
|
+
if (this.request.spec.model && !resolved.modelHandled) {
|
|
195
|
+
await this.selectModelViaProtocol(session, this.request.spec.model);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Providers without a CLI/env model knob may still expose model choice as an
|
|
200
|
+
* ACP session config option — match the requested model against it.
|
|
201
|
+
*/
|
|
202
|
+
async selectModelViaProtocol(session, model) {
|
|
203
|
+
const flatten = (options) => {
|
|
204
|
+
const values = options.options;
|
|
205
|
+
if (values.length > 0 && "group" in values[0]) {
|
|
206
|
+
return values.flatMap((g) => g.options);
|
|
207
|
+
}
|
|
208
|
+
return values;
|
|
209
|
+
};
|
|
210
|
+
const selects = (session.configOptions ?? []).filter((o) => o.type === "select" && (o.category === "model" || (!o.category && /model/i.test(o.id))));
|
|
211
|
+
const wanted = model.toLowerCase();
|
|
212
|
+
for (const option of selects) {
|
|
213
|
+
const values = flatten(option);
|
|
214
|
+
const match = values.find((v) => v.value === model) ??
|
|
215
|
+
values.find((v) => v.name.toLowerCase() === wanted) ??
|
|
216
|
+
values.find((v) => v.value.toLowerCase().includes(wanted) || v.name.toLowerCase().includes(wanted));
|
|
217
|
+
if (match) {
|
|
218
|
+
if (option.currentValue !== match.value) {
|
|
219
|
+
await this.connection.setSessionConfigOption({
|
|
220
|
+
sessionId: this.sessionId,
|
|
221
|
+
configId: option.id,
|
|
222
|
+
value: match.value,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
this.request.trace({ type: "model_selected", configId: option.id, value: match.value });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const available = selects.flatMap((o) => flatten(o).map((v) => v.value));
|
|
230
|
+
throw new Error(available.length
|
|
231
|
+
? `model "${model}" not available for this agent; available: ${available.join(", ")}`
|
|
232
|
+
: `model "${model}" requested but this agent exposes no model selection (drop the model option); check \`newton models ${this.request.spec.provider ?? ""}\``);
|
|
233
|
+
}
|
|
234
|
+
handleUpdate(params) {
|
|
235
|
+
this.request.trace({ type: "update", update: params.update });
|
|
236
|
+
const update = params.update;
|
|
237
|
+
switch (update.sessionUpdate) {
|
|
238
|
+
case "agent_message_chunk":
|
|
239
|
+
if (update.content.type === "text") {
|
|
240
|
+
this.text += update.content.text;
|
|
241
|
+
this.request.onActivity?.("message", update.content.text);
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
case "agent_thought_chunk":
|
|
245
|
+
if (update.content.type === "text") {
|
|
246
|
+
this.request.onActivity?.("thought", update.content.text);
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
case "tool_call":
|
|
250
|
+
this.toolCalls += 1;
|
|
251
|
+
this.request.onActivity?.("tool", update.title ?? update.kind ?? "tool");
|
|
252
|
+
break;
|
|
253
|
+
default:
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { accessSync, constants } from "node:fs";
|
|
3
|
+
import { AcpAgentSession } from "./acp.js";
|
|
4
|
+
import { compileWorkflow, loadWorkflow, runWorkflow } from "./executor.js";
|
|
5
|
+
import { HumanReporter, JsonReporter } from "./progress.js";
|
|
6
|
+
import { describeProviders } from "./providers.js";
|
|
7
|
+
import { VERSION } from "./version.js";
|
|
8
|
+
const USAGE = `newton — deterministic multi-agent workflows over ACP
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
newton run <workflow.js> [options] Run a workflow in the current directory
|
|
12
|
+
newton validate <workflow.js> Parse + compile a workflow without running agents
|
|
13
|
+
newton agents [--json] Supported agents: installed? signed in? (alias: doctor)
|
|
14
|
+
newton models <provider> [--json] Ask an agent which models it exposes
|
|
15
|
+
|
|
16
|
+
Run options:
|
|
17
|
+
--args <json> Value exposed to the script as \`args\`
|
|
18
|
+
--cwd <dir> Workspace agents operate in (default: current directory)
|
|
19
|
+
--resume <runId> Replay cached agent results from a previous run
|
|
20
|
+
--json NDJSON events on stdout instead of human output
|
|
21
|
+
--budget <tokens> Output-token ceiling; agent() throws once exhausted
|
|
22
|
+
--max-agents <n> Agent-call cap (default 200)
|
|
23
|
+
--timeout <seconds> Per-agent timeout (default 1200)
|
|
24
|
+
--concurrency <n> Concurrent agents (default min(16, cores-2))
|
|
25
|
+
|
|
26
|
+
Workflows are plain JavaScript: \`export const meta = { name, description }\`
|
|
27
|
+
followed by a script body using agent()/parallel()/pipeline()/phase()/log(),
|
|
28
|
+
with \`args\` and \`budget\` in scope. agent() opts: provider (codex | claude |
|
|
29
|
+
gemini | qwen | cursor | opencode | goose | copilot), model, effort, access
|
|
30
|
+
(read | write | full), schema, label, phase, cwd, command (any ACP agent,
|
|
31
|
+
e.g. ["my-agent", "--acp"]).
|
|
32
|
+
|
|
33
|
+
Runs are recorded under .newton/runs/<runId>/ — journal.jsonl plus a full
|
|
34
|
+
per-agent ACP trace.`;
|
|
35
|
+
function parseArgs(argv) {
|
|
36
|
+
const positional = [];
|
|
37
|
+
const flags = {};
|
|
38
|
+
for (let i = 0; i < argv.length; i++) {
|
|
39
|
+
const token = argv[i];
|
|
40
|
+
if (!token.startsWith("--")) {
|
|
41
|
+
positional.push(token);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const name = token.slice(2);
|
|
45
|
+
const next = argv[i + 1];
|
|
46
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
47
|
+
flags[name] = next;
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
flags[name] = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { positional, flags };
|
|
55
|
+
}
|
|
56
|
+
function fail(message) {
|
|
57
|
+
process.stderr.write(`error: ${message}\n`);
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
function intFlag(flags, name) {
|
|
61
|
+
const raw = flags[name];
|
|
62
|
+
if (raw === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
const value = Number(raw);
|
|
65
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
66
|
+
fail(`--${name} requires a positive integer`);
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
async function commandRun(positional, flags) {
|
|
70
|
+
const file = positional[0];
|
|
71
|
+
if (!file)
|
|
72
|
+
fail("newton run requires a workflow file");
|
|
73
|
+
let args;
|
|
74
|
+
if (typeof flags.args === "string") {
|
|
75
|
+
try {
|
|
76
|
+
args = JSON.parse(flags.args);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
args = flags.args; // plain-string args are fine; don't force JSON quoting
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const json = flags.json === true;
|
|
83
|
+
const reporter = json ? new JsonReporter() : new HumanReporter();
|
|
84
|
+
const timeoutSeconds = intFlag(flags, "timeout");
|
|
85
|
+
const result = await runWorkflow({
|
|
86
|
+
file,
|
|
87
|
+
args,
|
|
88
|
+
cwd: typeof flags.cwd === "string" ? flags.cwd : process.cwd(),
|
|
89
|
+
resumeRunId: typeof flags.resume === "string" ? flags.resume : undefined,
|
|
90
|
+
budgetTokens: intFlag(flags, "budget"),
|
|
91
|
+
maxAgents: intFlag(flags, "max-agents"),
|
|
92
|
+
agentTimeoutMs: timeoutSeconds ? timeoutSeconds * 1000 : undefined,
|
|
93
|
+
concurrency: intFlag(flags, "concurrency"),
|
|
94
|
+
reporter,
|
|
95
|
+
});
|
|
96
|
+
reporter.done(result);
|
|
97
|
+
if (!json) {
|
|
98
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
99
|
+
}
|
|
100
|
+
process.exit(result.status === "ok" ? 0 : 1);
|
|
101
|
+
}
|
|
102
|
+
function commandValidate(positional) {
|
|
103
|
+
const file = positional[0];
|
|
104
|
+
if (!file)
|
|
105
|
+
fail("newton validate requires a workflow file");
|
|
106
|
+
const workflow = loadWorkflow(file);
|
|
107
|
+
compileWorkflow(workflow);
|
|
108
|
+
process.stdout.write(`${JSON.stringify({ ok: true, name: workflow.meta.name, description: workflow.meta.description, phases: workflow.meta.phases ?? [] }, null, 2)}\n`);
|
|
109
|
+
}
|
|
110
|
+
function commandDoctor(flags) {
|
|
111
|
+
const providers = describeProviders();
|
|
112
|
+
let workspaceWritable = true;
|
|
113
|
+
try {
|
|
114
|
+
accessSync(process.cwd(), constants.W_OK);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
workspaceWritable = false;
|
|
118
|
+
}
|
|
119
|
+
if (flags.json === true) {
|
|
120
|
+
const report = { newton: VERSION, node: process.version, workspace: process.cwd(), workspaceWritable, providers };
|
|
121
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
process.stdout.write(`newton ${VERSION} · node ${process.version}\n`);
|
|
125
|
+
process.stdout.write(`workspace: ${process.cwd()}${workspaceWritable ? "" : " (NOT writable)"}\n\n`);
|
|
126
|
+
const rank = { ok: 0, unknown: 1, none: 2 };
|
|
127
|
+
const mark = { ok: "✔", unknown: "?", none: "✖" };
|
|
128
|
+
const width = Math.max(...providers.map((p) => p.provider.length));
|
|
129
|
+
for (const p of [...providers].sort((a, b) => rank[a.auth.state] - rank[b.auth.state])) {
|
|
130
|
+
const aliases = p.aliases.length ? ` (aka ${p.aliases.join(", ")})` : "";
|
|
131
|
+
process.stdout.write(`${mark[p.auth.state]} ${p.provider.padEnd(width)} ${p.auth.detail}${aliases}\n` +
|
|
132
|
+
` ${" ".repeat(width)} via ${p.resolvesVia}\n`);
|
|
133
|
+
}
|
|
134
|
+
process.stdout.write(`\n✔ signed in · ? undetectable · ✖ needs setup\n`);
|
|
135
|
+
}
|
|
136
|
+
async function commandModels(positional, flags) {
|
|
137
|
+
const provider = positional[0];
|
|
138
|
+
if (!provider)
|
|
139
|
+
fail("newton models requires a provider (see `newton agents`)");
|
|
140
|
+
const session = new AcpAgentSession({
|
|
141
|
+
spec: { provider },
|
|
142
|
+
prompt: "",
|
|
143
|
+
cwd: process.cwd(),
|
|
144
|
+
timeoutMs: 60_000,
|
|
145
|
+
trace: () => { },
|
|
146
|
+
});
|
|
147
|
+
try {
|
|
148
|
+
const info = await session.inspect();
|
|
149
|
+
if (flags.json === true) {
|
|
150
|
+
process.stdout.write(`${JSON.stringify(info, null, 2)}\n`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const selects = info.configOptions.filter((o) => o.type === "select");
|
|
154
|
+
if (selects.length === 0 && !info.modes) {
|
|
155
|
+
process.stdout.write(`${provider}: exposes no session options; it uses its own configured model\n`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
for (const option of selects) {
|
|
159
|
+
process.stdout.write(`${option.name} [${option.id}]${option.category ? ` (${option.category})` : ""}\n`);
|
|
160
|
+
const values = option.options.flatMap((v) => ("group" in v ? v.options : [v]));
|
|
161
|
+
for (const value of values) {
|
|
162
|
+
const current = value.value === option.currentValue ? " (current)" : "";
|
|
163
|
+
process.stdout.write(` ${value.value}${value.name !== value.value ? ` — ${value.name}` : ""}${current}\n`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (info.modes) {
|
|
167
|
+
process.stdout.write(`Modes [current: ${info.modes.currentModeId}]\n`);
|
|
168
|
+
for (const mode of info.modes.availableModes) {
|
|
169
|
+
process.stdout.write(` ${mode.id}${mode.name !== mode.id ? ` — ${mode.name}` : ""}\n`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
fail(error.message);
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
session.close();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function main() {
|
|
181
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
182
|
+
const { positional, flags } = parseArgs(rest);
|
|
183
|
+
switch (command) {
|
|
184
|
+
case "run":
|
|
185
|
+
await commandRun(positional, flags);
|
|
186
|
+
break;
|
|
187
|
+
case "validate":
|
|
188
|
+
try {
|
|
189
|
+
commandValidate(positional);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
fail(error.message);
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
case "models":
|
|
196
|
+
await commandModels(positional, flags);
|
|
197
|
+
break;
|
|
198
|
+
case "agents":
|
|
199
|
+
case "doctor":
|
|
200
|
+
commandDoctor(flags);
|
|
201
|
+
break;
|
|
202
|
+
case "version":
|
|
203
|
+
case "--version":
|
|
204
|
+
case "-v":
|
|
205
|
+
process.stdout.write(`${VERSION}\n`);
|
|
206
|
+
break;
|
|
207
|
+
case undefined:
|
|
208
|
+
case "help":
|
|
209
|
+
case "--help":
|
|
210
|
+
case "-h":
|
|
211
|
+
process.stdout.write(`${USAGE}\n`);
|
|
212
|
+
break;
|
|
213
|
+
default:
|
|
214
|
+
fail(`unknown command "${command}"\n\n${USAGE}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
main().catch((error) => {
|
|
218
|
+
process.stderr.write(`error: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
});
|