@nyxa/nyx-agent 0.4.1 → 0.6.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 +58 -9
- package/dist/cli.js +13 -16
- package/dist/commands/init.js +112 -462
- package/dist/commands/run.js +17 -3
- package/dist/commands/update.js +1 -0
- package/dist/config/loadConfig.js +17 -3
- package/dist/config/schema.js +29 -146
- package/dist/runtime/files.js +1 -0
- package/dist/runtime/git.js +1 -0
- package/dist/runtime/gitLifecycle.js +19 -57
- package/dist/runtime/harness.js +26 -0
- package/dist/runtime/ledger.js +1 -0
- package/dist/runtime/paths.js +1 -12
- package/dist/runtime/prompts.js +103 -0
- package/dist/runtime/runPhase.js +85 -254
- package/dist/runtime/runPipeline.js +479 -0
- package/dist/runtime/schemas.js +52 -0
- package/dist/runtime/scm.js +80 -0
- package/dist/runtime/time.js +1 -0
- package/dist/runtime/validateResult.js +2 -3
- package/dist/runtime/workItems.js +43 -118
- package/package.json +2 -5
- package/dist/runtime/buildPrompt.js +0 -54
- package/dist/runtime/effectiveConfig.js +0 -14
- package/dist/runtime/renderTemplate.js +0 -28
- package/dist/runtime/runWorkflow.js +0 -680
- package/dist/runtime/validateWorkItem.js +0 -212
- package/dist/runtime/workItemAnnotations.js +0 -39
- package/docs/nyxagent-v0-spec.md +0 -742
- package/templates/default/prompts/closure.md +0 -30
- package/templates/default/prompts/execution.md +0 -11
- package/templates/default/prompts/finalize.md +0 -7
- package/templates/default/prompts/global-review.md +0 -24
- package/templates/default/prompts/global-revision.md +0 -9
- package/templates/default/prompts/pull-request.md +0 -23
- package/templates/default/prompts/repair-result.md +0 -29
- package/templates/default/prompts/review.md +0 -18
- package/templates/default/prompts/revision.md +0 -7
- package/templates/default/prompts/selection.md +0 -46
- package/templates/default/schemas/closure.schema.json +0 -35
- package/templates/default/schemas/global-review.schema.json +0 -60
- package/templates/default/schemas/pull-request.schema.json +0 -44
- package/templates/default/schemas/review.schema.json +0 -60
- package/templates/default/schemas/selection.schema.json +0 -135
package/dist/runtime/runPhase.js
CHANGED
|
@@ -1,122 +1,65 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { execa } from "execa";
|
|
3
|
-
import {
|
|
4
|
-
import { getEffectiveHarness, getEffectiveModel } from "./effectiveConfig.js";
|
|
5
|
-
import { ensureDir, readText, writeJson, writeText } from "./files.js";
|
|
3
|
+
import { ensureDir, writeJson, writeText } from "./files.js";
|
|
6
4
|
import { getGitSnapshot } from "./git.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { renderTemplate } from "./renderTemplate.js";
|
|
5
|
+
import { buildHarnessInvocation } from "./harness.js";
|
|
6
|
+
import { parseNyxAgentResult } from "./parseResult.js";
|
|
10
7
|
import { validateAgainstSchema } from "./validateResult.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
config: input.config,
|
|
23
|
-
phase: input.phase,
|
|
24
|
-
context
|
|
25
|
-
});
|
|
26
|
-
const attempt = await runHarnessAttempt({
|
|
27
|
-
attemptDir: path.join(phaseDir, "attempt-001"),
|
|
28
|
-
prompt,
|
|
29
|
-
context,
|
|
30
|
-
config: input.config,
|
|
31
|
-
phase: input.phase,
|
|
32
|
-
workdir: input.workdir ?? input.projectRoot
|
|
8
|
+
/**
|
|
9
|
+
* Run one agent phase: invoke the harness with the prompt on stdin, and — when a
|
|
10
|
+
* schema is given — parse, validate, and (on failure) repair the structured
|
|
11
|
+
* result. Decoupled from any workflow graph: the pipeline owns control flow.
|
|
12
|
+
*/
|
|
13
|
+
export async function runAgentPhase(input) {
|
|
14
|
+
await ensureDir(input.phaseDir);
|
|
15
|
+
const attempt = await invokeHarness({
|
|
16
|
+
attemptDir: path.join(input.phaseDir, "attempt-001"),
|
|
17
|
+
input,
|
|
18
|
+
prompt: input.prompt
|
|
33
19
|
});
|
|
34
20
|
if (attempt.exitCode !== 0) {
|
|
35
|
-
await writeAttemptMeta(attempt, {
|
|
36
|
-
failure: `Harness exited with code ${attempt.exitCode}`
|
|
37
|
-
});
|
|
38
21
|
return {
|
|
39
22
|
ok: false,
|
|
40
|
-
error: `Phase "${input.
|
|
23
|
+
error: `Phase "${input.phaseId}" failed with exit code ${attempt.exitCode}`
|
|
41
24
|
};
|
|
42
25
|
}
|
|
43
|
-
if (!
|
|
44
|
-
await writeAttemptMeta(attempt);
|
|
26
|
+
if (!input.schema) {
|
|
45
27
|
return { ok: true };
|
|
46
28
|
}
|
|
47
|
-
const parsed =
|
|
48
|
-
input,
|
|
49
|
-
phaseDir,
|
|
50
|
-
attempt
|
|
51
|
-
});
|
|
29
|
+
const parsed = parseAndValidate(input.schema, attempt.stdout);
|
|
52
30
|
if (parsed.ok) {
|
|
31
|
+
await writeJson(path.join(input.phaseDir, "result.json"), parsed.result);
|
|
53
32
|
return parsed;
|
|
54
33
|
}
|
|
55
|
-
await
|
|
56
|
-
result_error: parsed.error
|
|
57
|
-
});
|
|
58
|
-
const repaired = await repairStructuredResult({
|
|
34
|
+
const repaired = await repairResult({
|
|
59
35
|
input,
|
|
60
|
-
|
|
61
|
-
originalPrompt: prompt,
|
|
62
|
-
originalAttempt: attempt,
|
|
36
|
+
originalStdout: attempt.stdout,
|
|
63
37
|
validationError: parsed.error
|
|
64
38
|
});
|
|
65
|
-
if (repaired.ok) {
|
|
66
|
-
|
|
39
|
+
if (repaired.ok && repaired.result !== undefined) {
|
|
40
|
+
await writeJson(path.join(input.phaseDir, "result.json"), repaired.result);
|
|
67
41
|
}
|
|
68
|
-
return
|
|
69
|
-
ok: false,
|
|
70
|
-
error: repaired.error
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
function buildContext(input) {
|
|
74
|
-
const model = getEffectiveModel(input.config, input.phase);
|
|
75
|
-
const harness = getEffectiveHarness(input.config, input.phase);
|
|
76
|
-
return {
|
|
77
|
-
project_root: input.projectRoot,
|
|
78
|
-
run_dir: input.runDir,
|
|
79
|
-
iteration_dir: input.iterationDir,
|
|
80
|
-
phase_dir: input.phaseDir,
|
|
81
|
-
state_file: input.stateFile,
|
|
82
|
-
phase: input.phase,
|
|
83
|
-
workflow: input.config.workflow,
|
|
84
|
-
work_items: input.config.work_items ?? {},
|
|
85
|
-
available_work_items: input.state.available_work_items ?? [],
|
|
86
|
-
recommended_work_item_queue: input.state.recommended_work_item_queue ?? [],
|
|
87
|
-
selected_work_item_queue: input.state.selected_work_item_queue ?? [],
|
|
88
|
-
work_item_annotations: input.state.work_item_annotations ?? [],
|
|
89
|
-
work_item: input.state.work_item ?? {},
|
|
90
|
-
seen_work_item_keys: input.state.seen_work_item_keys ?? [],
|
|
91
|
-
completed_work_item_keys: input.state.completed_work_item_keys ?? [],
|
|
92
|
-
last_completed_work_item: input.state.last_completed_work_item ?? null,
|
|
93
|
-
phase_results: input.state.phase_results ?? {},
|
|
94
|
-
state: input.state,
|
|
95
|
-
model,
|
|
96
|
-
harness,
|
|
97
|
-
workdir: input.workdir ?? input.projectRoot,
|
|
98
|
-
git: input.git ?? {}
|
|
99
|
-
};
|
|
42
|
+
return repaired;
|
|
100
43
|
}
|
|
101
|
-
async function
|
|
102
|
-
await ensureDir(
|
|
103
|
-
await writeText(path.join(
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
44
|
+
async function invokeHarness(args) {
|
|
45
|
+
await ensureDir(args.attemptDir);
|
|
46
|
+
await writeText(path.join(args.attemptDir, "prompt.md"), args.prompt);
|
|
47
|
+
const invocation = buildHarnessInvocation({
|
|
48
|
+
harness: args.input.harness,
|
|
49
|
+
capability: args.forceReadonly ? "readonly" : args.input.capability,
|
|
50
|
+
model: args.input.model,
|
|
51
|
+
reasoning: args.input.reasoning
|
|
52
|
+
});
|
|
110
53
|
const startedAt = new Date().toISOString();
|
|
111
54
|
const started = Date.now();
|
|
112
|
-
const gitBefore = await getGitSnapshot(input.workdir);
|
|
55
|
+
const gitBefore = await getGitSnapshot(args.input.workdir);
|
|
113
56
|
let stdout = "";
|
|
114
57
|
let stderr = "";
|
|
115
58
|
let exitCode = 0;
|
|
116
59
|
try {
|
|
117
|
-
const result = await execa(command, args, {
|
|
118
|
-
cwd: input.workdir,
|
|
119
|
-
input:
|
|
60
|
+
const result = await execa(invocation.command, invocation.args, {
|
|
61
|
+
cwd: args.input.workdir,
|
|
62
|
+
input: args.prompt,
|
|
120
63
|
reject: false
|
|
121
64
|
});
|
|
122
65
|
stdout = result.stdout;
|
|
@@ -127,186 +70,74 @@ async function runHarnessAttempt(input) {
|
|
|
127
70
|
exitCode = 127;
|
|
128
71
|
stderr = error instanceof Error ? error.message : String(error);
|
|
129
72
|
}
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
await
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
args,
|
|
143
|
-
startedAt,
|
|
144
|
-
endedAt,
|
|
145
|
-
durationMs,
|
|
146
|
-
gitBefore,
|
|
147
|
-
gitAfter
|
|
148
|
-
};
|
|
149
|
-
await writeAttemptMeta(attempt);
|
|
150
|
-
return attempt;
|
|
151
|
-
}
|
|
152
|
-
async function writeAttemptMeta(attempt, extra = {}) {
|
|
153
|
-
await writeJson(attempt.metaPath, {
|
|
154
|
-
command: attempt.command,
|
|
155
|
-
args: attempt.args,
|
|
156
|
-
started_at: attempt.startedAt,
|
|
157
|
-
ended_at: attempt.endedAt,
|
|
158
|
-
duration_ms: attempt.durationMs,
|
|
159
|
-
exit_code: attempt.exitCode,
|
|
160
|
-
git_before: attempt.gitBefore,
|
|
161
|
-
git_after: attempt.gitAfter,
|
|
162
|
-
...extra
|
|
73
|
+
const gitAfter = await getGitSnapshot(args.input.workdir);
|
|
74
|
+
await writeText(path.join(args.attemptDir, "stdout.log"), stdout);
|
|
75
|
+
await writeText(path.join(args.attemptDir, "stderr.log"), stderr);
|
|
76
|
+
await writeJson(path.join(args.attemptDir, "meta.json"), {
|
|
77
|
+
command: invocation.command,
|
|
78
|
+
args: invocation.args,
|
|
79
|
+
started_at: startedAt,
|
|
80
|
+
ended_at: new Date().toISOString(),
|
|
81
|
+
duration_ms: Date.now() - started,
|
|
82
|
+
exit_code: exitCode,
|
|
83
|
+
git_before: gitBefore,
|
|
84
|
+
git_after: gitAfter
|
|
163
85
|
});
|
|
86
|
+
return { stdout, stderr, exitCode };
|
|
164
87
|
}
|
|
165
|
-
|
|
166
|
-
const parsed = parseNyxAgentResult(
|
|
88
|
+
function parseAndValidate(schema, stdout) {
|
|
89
|
+
const parsed = parseNyxAgentResult(stdout);
|
|
167
90
|
if (!parsed.ok) {
|
|
168
91
|
return parsed;
|
|
169
92
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (!validation.ok) {
|
|
174
|
-
return validation;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
if (input.input.phase.transitions && !getOutcome(parsed.value)) {
|
|
178
|
-
return {
|
|
179
|
-
ok: false,
|
|
180
|
-
error: `Phase "${input.input.phase.id}" has transitions but result has no string outcome`
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
const workItem = readObjectProperty(parsed.value, "work_item");
|
|
184
|
-
if (workItem !== undefined) {
|
|
185
|
-
const currentWorkItemKey = readObjectProperty(readObjectProperty(input.input.state, "work_item"), "key");
|
|
186
|
-
const workItemValidation = validateWorkItemIdentity({
|
|
187
|
-
config: input.input.config,
|
|
188
|
-
workItem,
|
|
189
|
-
availableWorkItems: readWorkItemCandidates(input.input.state.available_work_items),
|
|
190
|
-
seenWorkItemKeys: readStringArray(input.input.state.seen_work_item_keys),
|
|
191
|
-
completedWorkItemKeys: readStringArray(input.input.state.completed_work_item_keys),
|
|
192
|
-
allowKnownKey: typeof currentWorkItemKey === "string" ? currentWorkItemKey : undefined
|
|
193
|
-
});
|
|
194
|
-
if (!workItemValidation.ok) {
|
|
195
|
-
return workItemValidation;
|
|
196
|
-
}
|
|
93
|
+
const validation = validateAgainstSchema(schema, parsed.value);
|
|
94
|
+
if (!validation.ok) {
|
|
95
|
+
return validation;
|
|
197
96
|
}
|
|
198
|
-
|
|
199
|
-
if (workItems !== undefined) {
|
|
200
|
-
const workItemQueueValidation = validateWorkItemQueue({
|
|
201
|
-
config: input.input.config,
|
|
202
|
-
workItems,
|
|
203
|
-
availableWorkItems: readWorkItemCandidates(input.input.state.available_work_items),
|
|
204
|
-
seenWorkItemKeys: readStringArray(input.input.state.seen_work_item_keys),
|
|
205
|
-
completedWorkItemKeys: readStringArray(input.input.state.completed_work_item_keys)
|
|
206
|
-
});
|
|
207
|
-
if (!workItemQueueValidation.ok) {
|
|
208
|
-
return workItemQueueValidation;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
await writeJson(path.join(input.phaseDir, "result.json"), parsed.value);
|
|
212
|
-
return {
|
|
213
|
-
ok: true,
|
|
214
|
-
result: parsed.value,
|
|
215
|
-
outcome: getOutcome(parsed.value)
|
|
216
|
-
};
|
|
97
|
+
return { ok: true, result: parsed.value };
|
|
217
98
|
}
|
|
218
|
-
async function
|
|
219
|
-
const maxAttempts =
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
ok: false,
|
|
223
|
-
error: input.validationError
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
const repairPromptTemplate = await readText(resolveNyxPath(input.input.projectRoot, input.input.config.repair.prompt, "repair prompt"));
|
|
227
|
-
let lastError = input.validationError;
|
|
99
|
+
async function repairResult(args) {
|
|
100
|
+
const maxAttempts = args.input.repairAttempts ?? 1;
|
|
101
|
+
let lastError = args.validationError;
|
|
228
102
|
for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
|
|
229
|
-
const repairContext = buildContext({
|
|
230
|
-
...input.input,
|
|
231
|
-
phaseDir: input.phaseDir
|
|
232
|
-
});
|
|
233
|
-
const renderedRepairPrompt = renderTemplate(repairPromptTemplate, {
|
|
234
|
-
...repairContext,
|
|
235
|
-
original_prompt: input.originalPrompt,
|
|
236
|
-
original_stdout: input.originalAttempt.stdout,
|
|
237
|
-
original_stderr: input.originalAttempt.stderr,
|
|
238
|
-
validation_error: lastError
|
|
239
|
-
});
|
|
240
103
|
const repairPrompt = [
|
|
241
|
-
"# NyxAgent
|
|
104
|
+
"# NyxAgent result repair",
|
|
105
|
+
"",
|
|
106
|
+
"Your previous response did not produce a valid structured result.",
|
|
107
|
+
"Do not redo the work and do not modify any files. Re-emit only the result.",
|
|
242
108
|
"",
|
|
243
|
-
"
|
|
244
|
-
"
|
|
245
|
-
|
|
109
|
+
"Validation error:",
|
|
110
|
+
"```",
|
|
111
|
+
lastError,
|
|
112
|
+
"```",
|
|
246
113
|
"",
|
|
247
|
-
|
|
114
|
+
"Previous response:",
|
|
115
|
+
"```",
|
|
116
|
+
args.originalStdout,
|
|
117
|
+
"```",
|
|
118
|
+
"",
|
|
119
|
+
"Original prompt:",
|
|
120
|
+
"",
|
|
121
|
+
args.input.prompt
|
|
248
122
|
].join("\n");
|
|
249
|
-
const
|
|
250
|
-
attemptDir: path.join(input.phaseDir, `repair-${String(attemptNumber).padStart(3, "0")}`),
|
|
123
|
+
const attempt = await invokeHarness({
|
|
124
|
+
attemptDir: path.join(args.input.phaseDir, `repair-${String(attemptNumber).padStart(3, "0")}`),
|
|
125
|
+
input: args.input,
|
|
251
126
|
prompt: repairPrompt,
|
|
252
|
-
|
|
253
|
-
config: input.input.config,
|
|
254
|
-
phase: input.input.phase,
|
|
255
|
-
workdir: input.input.workdir ?? input.input.projectRoot
|
|
127
|
+
forceReadonly: true
|
|
256
128
|
});
|
|
257
|
-
if (
|
|
258
|
-
lastError = `Repair harness exited with code ${
|
|
259
|
-
await writeAttemptMeta(repairAttempt, {
|
|
260
|
-
failure: lastError
|
|
261
|
-
});
|
|
129
|
+
if (attempt.exitCode !== 0) {
|
|
130
|
+
lastError = `Repair harness exited with code ${attempt.exitCode}`;
|
|
262
131
|
continue;
|
|
263
132
|
}
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
attempt: repairAttempt
|
|
268
|
-
});
|
|
269
|
-
if (repaired.ok) {
|
|
270
|
-
return repaired;
|
|
133
|
+
const parsed = parseAndValidate(args.input.schema, attempt.stdout);
|
|
134
|
+
if (parsed.ok) {
|
|
135
|
+
return parsed;
|
|
271
136
|
}
|
|
272
|
-
lastError =
|
|
273
|
-
await writeAttemptMeta(repairAttempt, {
|
|
274
|
-
result_error: repaired.error
|
|
275
|
-
});
|
|
137
|
+
lastError = parsed.error;
|
|
276
138
|
}
|
|
277
139
|
return {
|
|
278
140
|
ok: false,
|
|
279
|
-
error: lastError
|
|
141
|
+
error: `Phase "${args.input.phaseId}" produced an invalid result: ${lastError}`
|
|
280
142
|
};
|
|
281
143
|
}
|
|
282
|
-
function readObjectProperty(value, key) {
|
|
283
|
-
if (value &&
|
|
284
|
-
typeof value === "object" &&
|
|
285
|
-
Object.prototype.hasOwnProperty.call(value, key)) {
|
|
286
|
-
return value[key];
|
|
287
|
-
}
|
|
288
|
-
return undefined;
|
|
289
|
-
}
|
|
290
|
-
function readStringArray(value) {
|
|
291
|
-
if (!Array.isArray(value)) {
|
|
292
|
-
return undefined;
|
|
293
|
-
}
|
|
294
|
-
return value.filter((item) => typeof item === "string");
|
|
295
|
-
}
|
|
296
|
-
function readWorkItemCandidates(value) {
|
|
297
|
-
if (!Array.isArray(value)) {
|
|
298
|
-
return undefined;
|
|
299
|
-
}
|
|
300
|
-
return value.filter(isWorkItemCandidate);
|
|
301
|
-
}
|
|
302
|
-
function isWorkItemCandidate(value) {
|
|
303
|
-
const key = readObjectProperty(value, "key");
|
|
304
|
-
const title = readObjectProperty(value, "title");
|
|
305
|
-
const source = readObjectProperty(value, "source");
|
|
306
|
-
const type = readObjectProperty(source, "type");
|
|
307
|
-
const locator = readObjectProperty(source, "locator");
|
|
308
|
-
return (typeof key === "string" &&
|
|
309
|
-
typeof title === "string" &&
|
|
310
|
-
(type === "local" || type === "github") &&
|
|
311
|
-
typeof locator === "string");
|
|
312
|
-
}
|