@mingchuno/agent-workflows 0.4.0 → 0.5.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/dist/src/adapters/agents.js +9 -0
- package/dist/src/cli.js +14 -1
- package/dist/src/config.d.ts +10 -0
- package/dist/src/config.js +9 -11
- package/dist/src/domain.d.ts +6 -0
- package/dist/src/invocation.d.ts +2 -4
- package/dist/src/invocation.js +204 -107
- package/dist/src/operations.d.ts +1 -2
- package/dist/src/operations.js +18 -33
- package/dist/src/recovery.d.ts +20 -2
- package/dist/src/recovery.js +49 -1
- package/dist/src/runner.d.ts +6 -2
- package/dist/src/runner.js +81 -83
- package/dist/src/runtime/process.d.ts +1 -0
- package/dist/src/runtime/process.js +6 -2
- package/dist/src/store.d.ts +4 -2
- package/dist/src/store.js +69 -51
- package/dist/src/tui/data.d.ts +1 -1
- package/dist/src/tui/dialogs.js +1 -0
- package/dist/src/tui/monitor-navigation.d.ts +76 -0
- package/dist/src/tui/monitor-navigation.js +187 -0
- package/dist/src/tui/monitor.js +70 -183
- package/dist/src/tui/projection.d.ts +22 -0
- package/dist/src/tui/projection.js +49 -0
- package/dist/src/validation-selection.d.ts +6 -0
- package/dist/src/validation-selection.js +29 -0
- package/dist/src/workspace.js +3 -2
- package/docs/api.md +1 -1
- package/docs/configuration.md +33 -0
- package/docs/operations.md +4 -3
- package/package.json +1 -1
- package/dist/src/tui/actions.d.ts +0 -16
- package/dist/src/tui/actions.js +0 -23
|
@@ -84,6 +84,7 @@ export class SDKAgent {
|
|
|
84
84
|
const worker = join(dirname(ownPath), `agent-worker.${source ? "ts" : "js"}`);
|
|
85
85
|
let buffer = "", output = "", events = Promise.resolve();
|
|
86
86
|
let eventError;
|
|
87
|
+
let stderr = "";
|
|
87
88
|
const persistenceFailure = new AbortController();
|
|
88
89
|
try {
|
|
89
90
|
await command(process.execPath, [
|
|
@@ -97,6 +98,9 @@ export class SDKAgent {
|
|
|
97
98
|
...(signal ? [signal] : []),
|
|
98
99
|
]),
|
|
99
100
|
captureOutput: false,
|
|
101
|
+
onStderr: (chunk) => {
|
|
102
|
+
stderr = (stderr + chunk).slice(-16_384);
|
|
103
|
+
},
|
|
100
104
|
processFile: invocation?.processFile,
|
|
101
105
|
timeoutMs: invocation
|
|
102
106
|
? (invocation.timeoutMs ?? defaultStageTimeoutMs)
|
|
@@ -128,6 +132,11 @@ export class SDKAgent {
|
|
|
128
132
|
});
|
|
129
133
|
}
|
|
130
134
|
},
|
|
135
|
+
}).catch((error) => {
|
|
136
|
+
const detail = stderr.trim();
|
|
137
|
+
throw detail
|
|
138
|
+
? new Error(`${String(error)}\nWorker stderr (last 16 KiB):\n${detail}`)
|
|
139
|
+
: error;
|
|
131
140
|
});
|
|
132
141
|
await events;
|
|
133
142
|
if (eventError)
|
package/dist/src/cli.js
CHANGED
|
@@ -165,6 +165,9 @@ program
|
|
|
165
165
|
const record = await store.run(run);
|
|
166
166
|
const invocations = (await store.invocations(run)).filter((item) => !options.invocation || item.id === options.invocation);
|
|
167
167
|
const paths = [
|
|
168
|
+
...(!options.invocation
|
|
169
|
+
? (record.stageLogs?.map((item) => item.path) ?? [])
|
|
170
|
+
: []),
|
|
168
171
|
...invocations.map((item) => item.log),
|
|
169
172
|
...(!options.invocation
|
|
170
173
|
? (record.validation?.map((check) => check.log) ?? [])
|
|
@@ -180,7 +183,7 @@ program
|
|
|
180
183
|
}
|
|
181
184
|
}
|
|
182
185
|
}));
|
|
183
|
-
for (const kind of ["pause", "resume", "stop", "
|
|
186
|
+
for (const kind of ["pause", "resume", "stop", "recover"])
|
|
184
187
|
program
|
|
185
188
|
.command(`${kind} <target>`)
|
|
186
189
|
.description(`${kind} project or run through the active runner`)
|
|
@@ -190,6 +193,16 @@ for (const kind of ["pause", "resume", "stop", "retry", "recover"])
|
|
|
190
193
|
status: "pending",
|
|
191
194
|
}));
|
|
192
195
|
}));
|
|
196
|
+
program
|
|
197
|
+
.command("retry <target>")
|
|
198
|
+
.description("retry a run through the active runner")
|
|
199
|
+
.option("--refresh-issue", "use the current hosted issue for the new run")
|
|
200
|
+
.action(async (target, options) => withStore(async (store) => {
|
|
201
|
+
console.log(JSON.stringify({
|
|
202
|
+
commandId: await store.request(options.refreshIssue ? "retry-refresh" : "retry", target),
|
|
203
|
+
status: "pending",
|
|
204
|
+
}));
|
|
205
|
+
}));
|
|
193
206
|
program
|
|
194
207
|
.command("monitor")
|
|
195
208
|
.option("--notify", "notify when an observed execution reaches an outcome")
|
package/dist/src/config.d.ts
CHANGED
|
@@ -51,6 +51,11 @@ export declare const projectSchema: z.ZodObject<{
|
|
|
51
51
|
args: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
52
52
|
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
53
53
|
}, z.core.$strict>>>;
|
|
54
|
+
validationProfiles: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
|
|
55
|
+
command: z.ZodString;
|
|
56
|
+
args: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
57
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
58
|
+
}, z.core.$strict>>>>;
|
|
54
59
|
includeAgentCoAuthors: z.ZodDefault<z.ZodBoolean>;
|
|
55
60
|
agent: z.ZodObject<{
|
|
56
61
|
provider: z.ZodEnum<{
|
|
@@ -144,6 +149,11 @@ export declare const configSchema: z.ZodObject<{
|
|
|
144
149
|
args: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
145
150
|
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
146
151
|
}, z.core.$strict>>>;
|
|
152
|
+
validationProfiles: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
|
|
153
|
+
command: z.ZodString;
|
|
154
|
+
args: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
155
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
156
|
+
}, z.core.$strict>>>>;
|
|
147
157
|
includeAgentCoAuthors: z.ZodDefault<z.ZodBoolean>;
|
|
148
158
|
agent: z.ZodObject<{
|
|
149
159
|
provider: z.ZodEnum<{
|
package/dist/src/config.js
CHANGED
|
@@ -29,6 +29,11 @@ export const stageSchema = z
|
|
|
29
29
|
: undefined,
|
|
30
30
|
})
|
|
31
31
|
.refine((stage) => stage.prompt === undefined || stage.promptFile === undefined, "Specify either prompt or promptFile, never both");
|
|
32
|
+
const validationCommandSchema = z.strictObject({
|
|
33
|
+
command: z.string().min(1),
|
|
34
|
+
args: z.array(z.string()).default([]),
|
|
35
|
+
timeoutMs: z.number().int().positive().default(defaultValidationTimeoutMs),
|
|
36
|
+
});
|
|
32
37
|
export const projectSchema = z.strictObject({
|
|
33
38
|
id: z.string().regex(/^[a-zA-Z0-9_-]+$/),
|
|
34
39
|
checkout: z.string().min(1),
|
|
@@ -49,17 +54,10 @@ export const projectSchema = z.strictObject({
|
|
|
49
54
|
.includes("{issue}")
|
|
50
55
|
.default("agent/{issue}-{attempt}"),
|
|
51
56
|
pollIntervalMs: z.number().int().min(100).default(30_000),
|
|
52
|
-
validation: z
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
timeoutMs: z
|
|
57
|
-
.number()
|
|
58
|
-
.int()
|
|
59
|
-
.positive()
|
|
60
|
-
.default(defaultValidationTimeoutMs),
|
|
61
|
-
}))
|
|
62
|
-
.default([]),
|
|
57
|
+
validation: z.array(validationCommandSchema).default([]),
|
|
58
|
+
validationProfiles: z
|
|
59
|
+
.record(z.string().regex(/^[a-zA-Z0-9_-]+$/), z.array(validationCommandSchema).min(1))
|
|
60
|
+
.default({}),
|
|
63
61
|
includeAgentCoAuthors: z.boolean().default(true),
|
|
64
62
|
agent: profileSchema,
|
|
65
63
|
stages: z
|
package/dist/src/domain.d.ts
CHANGED
|
@@ -135,6 +135,7 @@ export interface RunRecord {
|
|
|
135
135
|
attempt: number;
|
|
136
136
|
retryOf?: string;
|
|
137
137
|
issue: Issue;
|
|
138
|
+
validationProfile?: string;
|
|
138
139
|
outcome: Outcome;
|
|
139
140
|
phase: string;
|
|
140
141
|
createdAt: string;
|
|
@@ -152,6 +153,11 @@ export interface RunRecord {
|
|
|
152
153
|
reviewHead?: string;
|
|
153
154
|
error?: string;
|
|
154
155
|
failedStep?: number;
|
|
156
|
+
stageLogs?: Array<{
|
|
157
|
+
executionId: string;
|
|
158
|
+
step: string;
|
|
159
|
+
path: string;
|
|
160
|
+
}>;
|
|
155
161
|
executions?: ExecutionRecord[];
|
|
156
162
|
}
|
|
157
163
|
export declare class BlockedError extends Error {
|
package/dist/src/invocation.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type Stage } from "./config.js";
|
|
3
|
-
import { type
|
|
3
|
+
import { type RunRecord } from "./domain.js";
|
|
4
4
|
import { type ChangeEvidence } from "./evidence.js";
|
|
5
5
|
import type { OperationDependencies } from "./operations.js";
|
|
6
6
|
export interface InvocationTask {
|
|
@@ -17,9 +17,7 @@ interface StageExecution {
|
|
|
17
17
|
task: InvocationTask;
|
|
18
18
|
stepId: number;
|
|
19
19
|
dependencies: OperationDependencies;
|
|
20
|
-
saveImplementationSnapshot: (runId: string, snapshot: Snapshot, provider: string) => Promise<ContributionCandidate | undefined>;
|
|
21
|
-
acceptContribution: (runId: string, candidate: ContributionCandidate) => Promise<void>;
|
|
22
20
|
}
|
|
23
21
|
/** One logical stage; only returned format errors admit a second response attempt. */
|
|
24
|
-
export declare function invokeStage(
|
|
22
|
+
export declare function invokeStage(execution: StageExecution): Promise<string>;
|
|
25
23
|
export {};
|
package/dist/src/invocation.js
CHANGED
|
@@ -8,8 +8,46 @@ import { verifyEvidence } from "./evidence.js";
|
|
|
8
8
|
import { resolveStagePrompt, sha256 } from "./prompts.js";
|
|
9
9
|
const maxInvocationAttempts = 2;
|
|
10
10
|
/** One logical stage; only returned format errors admit a second response attempt. */
|
|
11
|
-
export async function invokeStage(
|
|
12
|
-
const {
|
|
11
|
+
export async function invokeStage(execution) {
|
|
12
|
+
const { run, name, stepId, dependencies } = execution;
|
|
13
|
+
const directory = join(dependencies.artifacts, run.id);
|
|
14
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
15
|
+
const path = join(directory, `stage-${name}-${stepId}.log`);
|
|
16
|
+
const executionId = run.executions?.at(-1)?.id ?? run.id;
|
|
17
|
+
await dependencies.store.patchRun(run.id, {
|
|
18
|
+
stageLogs: [
|
|
19
|
+
...(run.stageLogs ?? []).filter((item) => item.executionId !== executionId || item.step !== name),
|
|
20
|
+
{ executionId, step: name, path },
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
const note = (message) => appendFile(path, `${new Date().toISOString()} ${dependencies.redact(message)}\n`, { mode: 0o600 });
|
|
24
|
+
try {
|
|
25
|
+
await note(`Preparing ${name} agent stage`);
|
|
26
|
+
const prepared = await prepareStage(execution, note);
|
|
27
|
+
await note(`Agent preflight passed: ${prepared.profile.provider}`);
|
|
28
|
+
let attempt = { number: 1, correction: "" };
|
|
29
|
+
while (attempt.number <= maxInvocationAttempts) {
|
|
30
|
+
await note(`Starting invocation ${attempt.number}`);
|
|
31
|
+
const result = await invokeAttempt(execution, prepared, attempt);
|
|
32
|
+
if (result.kind === "completed") {
|
|
33
|
+
await note(`${name} agent stage completed`);
|
|
34
|
+
return result.output;
|
|
35
|
+
}
|
|
36
|
+
attempt = {
|
|
37
|
+
number: attempt.number + 1,
|
|
38
|
+
correction: result.correction,
|
|
39
|
+
contribution: result.contribution,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
throw new Error("Format correction exhausted");
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
await note(`Failed: ${String(error)}`);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function refusePreviousInvocations(execution) {
|
|
50
|
+
const { run, name, stepId, dependencies: { store }, } = execution;
|
|
13
51
|
const previous = (await store.invocations(run.id)).filter((item) => item.stepId === stepId);
|
|
14
52
|
if (previous.length) {
|
|
15
53
|
for (const record of previous) {
|
|
@@ -23,11 +61,10 @@ export async function invokeStage({ run, name, stage, task, stepId, dependencies
|
|
|
23
61
|
}
|
|
24
62
|
throw new BlockedError(`Interrupted agent stage ${name}; inspect existing sessions before explicit retry`);
|
|
25
63
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
await verifyEvidence(task.evidence);
|
|
64
|
+
}
|
|
65
|
+
function prepareRequest(execution) {
|
|
66
|
+
const { run, stage, task, dependencies } = execution;
|
|
67
|
+
const { project } = dependencies;
|
|
31
68
|
const resolved = resolveStagePrompt(stage, task.defaultPrompt, dependencies.promptBaseDirectory);
|
|
32
69
|
const outputSchema = task.outputContract
|
|
33
70
|
? z.toJSONSchema(task.outputContract)
|
|
@@ -44,6 +81,19 @@ export async function invokeStage({ run, name, stage, task, stepId, dependencies
|
|
|
44
81
|
contract,
|
|
45
82
|
].join("\n\n");
|
|
46
83
|
const profile = resolveProfile(project.agent, stage.profile);
|
|
84
|
+
return { resolved, outputSchema, fullPrompt, profile };
|
|
85
|
+
}
|
|
86
|
+
async function prepareStage(execution, note) {
|
|
87
|
+
const { run, stage, task, dependencies } = execution;
|
|
88
|
+
const { project, agents, signal, workspace } = dependencies;
|
|
89
|
+
await refusePreviousInvocations(execution);
|
|
90
|
+
if (!run.snapshot)
|
|
91
|
+
throw new BlockedError("Missing workspace snapshot");
|
|
92
|
+
await workspace.verify(project, run.snapshot);
|
|
93
|
+
if (task.evidence)
|
|
94
|
+
await verifyEvidence(task.evidence);
|
|
95
|
+
const request = prepareRequest(execution);
|
|
96
|
+
const { profile } = request;
|
|
47
97
|
const adapter = agents[profile.provider];
|
|
48
98
|
if (!adapter)
|
|
49
99
|
throw new Error(`Missing agent adapter ${profile.provider}`);
|
|
@@ -52,115 +102,162 @@ export async function invokeStage({ run, name, stage, task, stepId, dependencies
|
|
|
52
102
|
signal,
|
|
53
103
|
AbortSignal.timeout(stage.timeoutMs),
|
|
54
104
|
]);
|
|
105
|
+
await note(`Validating ${profile.provider} agent profile`);
|
|
55
106
|
const effective = await adapter.validate(profile, invocationSignal);
|
|
56
107
|
const directory = join(dependencies.artifacts, run.id);
|
|
57
108
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
109
|
+
return {
|
|
110
|
+
...request,
|
|
111
|
+
adapter,
|
|
112
|
+
effective,
|
|
113
|
+
deadline,
|
|
114
|
+
invocationSignal,
|
|
115
|
+
directory,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function invokeAttempt(execution, prepared, attempt) {
|
|
119
|
+
const { run, name, task, stepId, dependencies } = execution;
|
|
120
|
+
const { store, project, workspace, redact } = dependencies;
|
|
121
|
+
const { profile, effective, resolved, outputSchema, fullPrompt, directory, invocationSignal, } = prepared;
|
|
122
|
+
invocationSignal.throwIfAborted();
|
|
123
|
+
const expected = (await store.run(run.id)).snapshot;
|
|
124
|
+
await workspace.verify(project, expected);
|
|
125
|
+
if (task.evidence)
|
|
126
|
+
await verifyEvidence(task.evidence);
|
|
127
|
+
const id = randomUUID();
|
|
128
|
+
const prompt = fullPrompt + attempt.correction;
|
|
129
|
+
const readOnly = task.readOnly === true || attempt.number === maxInvocationAttempts;
|
|
130
|
+
const record = {
|
|
131
|
+
id,
|
|
132
|
+
runId: run.id,
|
|
133
|
+
projectId: project.id,
|
|
134
|
+
step: name,
|
|
135
|
+
stepId,
|
|
136
|
+
attempt: attempt.number,
|
|
137
|
+
provider: profile.provider,
|
|
138
|
+
sessionId: null,
|
|
139
|
+
sessionState: "pending",
|
|
140
|
+
requested: profile,
|
|
141
|
+
effective,
|
|
142
|
+
prompt: redact(prompt),
|
|
143
|
+
taskPrompt: { ...resolved, content: redact(resolved.content) },
|
|
144
|
+
outputContract: outputSchema
|
|
145
|
+
? sha256(JSON.stringify(outputSchema))
|
|
146
|
+
: undefined,
|
|
147
|
+
evidence: task.evidence,
|
|
148
|
+
outcome: "running",
|
|
149
|
+
startedAt: new Date().toISOString(),
|
|
150
|
+
log: join(directory, `${id}.jsonl`),
|
|
151
|
+
};
|
|
152
|
+
await store.saveInvocation(record);
|
|
153
|
+
try {
|
|
154
|
+
invocationSignal.throwIfAborted();
|
|
155
|
+
const output = await invokeProvider(execution, prepared, {
|
|
156
|
+
record,
|
|
157
|
+
prompt,
|
|
158
|
+
readOnly,
|
|
159
|
+
});
|
|
160
|
+
await appendFile(record.log, "", { mode: 0o600 });
|
|
61
161
|
invocationSignal.throwIfAborted();
|
|
62
|
-
|
|
63
|
-
|
|
162
|
+
let contribution = attempt.contribution;
|
|
163
|
+
if (readOnly)
|
|
164
|
+
await workspace.verify(project, expected);
|
|
165
|
+
else
|
|
166
|
+
contribution = await saveImplementationSnapshot(run.id, expected, profile.provider, dependencies);
|
|
64
167
|
if (task.evidence)
|
|
65
168
|
await verifyEvidence(task.evidence);
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
sessionState: "pending",
|
|
79
|
-
requested: profile,
|
|
80
|
-
effective,
|
|
81
|
-
prompt: redact(prompt),
|
|
82
|
-
taskPrompt: { ...resolved, content: redact(resolved.content) },
|
|
83
|
-
outputContract: outputSchema
|
|
84
|
-
? sha256(JSON.stringify(outputSchema))
|
|
85
|
-
: undefined,
|
|
86
|
-
evidence: task.evidence,
|
|
87
|
-
outcome: "running",
|
|
88
|
-
startedAt: new Date().toISOString(),
|
|
89
|
-
log: join(directory, `${id}.jsonl`),
|
|
169
|
+
invocationSignal.throwIfAborted();
|
|
170
|
+
const response = validateResponse(output, task.outputContract);
|
|
171
|
+
if (response.kind === "invalid") {
|
|
172
|
+
const correction = await recordInvalidResponse(record, output, response.error, redact);
|
|
173
|
+
return { kind: "correction", correction, contribution };
|
|
174
|
+
}
|
|
175
|
+
if (contribution)
|
|
176
|
+
await acceptContribution(run.id, contribution, dependencies);
|
|
177
|
+
record.outcome = "completed";
|
|
178
|
+
return {
|
|
179
|
+
kind: "completed",
|
|
180
|
+
output: redact(task.outputContract ? JSON.stringify(response.parsed) : output),
|
|
90
181
|
};
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (record.outcome === "running")
|
|
185
|
+
record.outcome = "failed";
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
record.finishedAt = new Date().toISOString();
|
|
190
|
+
if (!record.sessionId)
|
|
191
|
+
record.sessionState = "unavailable";
|
|
91
192
|
await store.saveInvocation(record);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function invokeProvider(execution, prepared, attempt) {
|
|
196
|
+
const { run, name, dependencies: { project, store, redact }, } = execution;
|
|
197
|
+
const { adapter, profile, outputSchema, invocationSignal, deadline } = prepared;
|
|
198
|
+
const { record, prompt, readOnly } = attempt;
|
|
199
|
+
return adapter.invoke({
|
|
200
|
+
id: record.id,
|
|
201
|
+
runId: run.id,
|
|
202
|
+
step: name,
|
|
203
|
+
cwd: project.checkout,
|
|
204
|
+
prompt,
|
|
205
|
+
profile,
|
|
206
|
+
outputSchema,
|
|
207
|
+
processFile: record.log + ".process.json",
|
|
208
|
+
readOnly,
|
|
209
|
+
signal: invocationSignal,
|
|
210
|
+
timeoutMs: Math.max(1, deadline - Date.now()),
|
|
211
|
+
session: async (sessionId) => {
|
|
212
|
+
record.sessionId = sessionId;
|
|
213
|
+
record.sessionState = "available";
|
|
214
|
+
await store.saveInvocation(record);
|
|
215
|
+
},
|
|
216
|
+
event: async (event) => {
|
|
217
|
+
await appendFile(record.log, redact(JSON.stringify(event)) + "\n", {
|
|
218
|
+
mode: 0o600,
|
|
116
219
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
parsed = task.outputContract
|
|
130
|
-
? task.outputContract.parse(JSON.parse(output))
|
|
131
|
-
: undefined;
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
if (!(error instanceof SyntaxError) && !(error instanceof z.ZodError))
|
|
135
|
-
throw error;
|
|
136
|
-
record.outcome = "invalid-output";
|
|
137
|
-
record.validationError = redact(String(error));
|
|
138
|
-
await appendFile(record.log, redact(JSON.stringify({
|
|
139
|
-
type: "invalid-output",
|
|
140
|
-
output,
|
|
141
|
-
error: String(error),
|
|
142
|
-
})) + "\n");
|
|
143
|
-
if (attempt === maxInvocationAttempts)
|
|
144
|
-
throw new Error(`Invalid output after one correction: ${String(error)}`);
|
|
145
|
-
correction = `\n\nCorrect the prior response format in this fresh inspection-only session. Do not modify files.\nPrior invalid response:\n${output}\nValidation errors:\n${String(error)}`;
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
if (pendingContribution)
|
|
149
|
-
await acceptContribution(run.id, pendingContribution);
|
|
150
|
-
record.outcome = "completed";
|
|
151
|
-
return redact(task.outputContract ? JSON.stringify(parsed) : output);
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
if (record.outcome === "running")
|
|
155
|
-
record.outcome = "failed";
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/** Only returned output validation failures qualify for correction. */
|
|
224
|
+
function validateResponse(output, contract) {
|
|
225
|
+
let parsed;
|
|
226
|
+
try {
|
|
227
|
+
parsed = contract ? contract.parse(JSON.parse(output)) : undefined;
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (!(error instanceof SyntaxError) && !(error instanceof z.ZodError))
|
|
156
231
|
throw error;
|
|
157
|
-
}
|
|
158
|
-
finally {
|
|
159
|
-
record.finishedAt = new Date().toISOString();
|
|
160
|
-
if (!record.sessionId)
|
|
161
|
-
record.sessionState = "unavailable";
|
|
162
|
-
await store.saveInvocation(record);
|
|
163
|
-
}
|
|
232
|
+
return { kind: "invalid", error };
|
|
164
233
|
}
|
|
165
|
-
|
|
234
|
+
return { kind: "valid", parsed };
|
|
235
|
+
}
|
|
236
|
+
async function recordInvalidResponse(record, output, error, redact) {
|
|
237
|
+
record.outcome = "invalid-output";
|
|
238
|
+
record.validationError = redact(String(error));
|
|
239
|
+
await appendFile(record.log, redact(JSON.stringify({ type: "invalid-output", output, error: String(error) })) + "\n");
|
|
240
|
+
if (record.attempt === maxInvocationAttempts)
|
|
241
|
+
throw new Error(`Invalid output after one correction: ${String(error)}`);
|
|
242
|
+
return `\n\nCorrect the prior response format in this fresh inspection-only session. Do not modify files.\nPrior invalid response:\n${output}\nValidation errors:\n${String(error)}`;
|
|
243
|
+
}
|
|
244
|
+
async function saveImplementationSnapshot(runId, expected, provider, dependencies) {
|
|
245
|
+
const { workspace, project, store } = dependencies;
|
|
246
|
+
const snapshot = await workspace.inspect(project);
|
|
247
|
+
if (snapshot.head !== expected.head || snapshot.branch !== expected.branch)
|
|
248
|
+
throw new BlockedError("Agent changed branch or committed unexpectedly");
|
|
249
|
+
await store.patchRun(runId, { snapshot });
|
|
250
|
+
return snapshot.fingerprint === expected.fingerprint
|
|
251
|
+
? undefined
|
|
252
|
+
: {
|
|
253
|
+
provider,
|
|
254
|
+
beforeFiles: expected.files,
|
|
255
|
+
afterFiles: snapshot.files,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async function acceptContribution(runId, candidate, dependencies) {
|
|
259
|
+
const run = await dependencies.store.run(runId);
|
|
260
|
+
await dependencies.store.patchRun(runId, {
|
|
261
|
+
contributionCandidates: [...(run.contributionCandidates ?? []), candidate],
|
|
262
|
+
});
|
|
166
263
|
}
|
package/dist/src/operations.d.ts
CHANGED
|
@@ -25,10 +25,9 @@ export declare class Operations {
|
|
|
25
25
|
eligible(): Promise<boolean>;
|
|
26
26
|
prepare(): Promise<void>;
|
|
27
27
|
invoke(name: string, stage: Stage, task: InvocationTask): Promise<string>;
|
|
28
|
-
private saveImplementationSnapshot;
|
|
29
|
-
private acceptContribution;
|
|
30
28
|
implement(): Promise<void>;
|
|
31
29
|
validate(): Promise<boolean>;
|
|
30
|
+
private recordValidation;
|
|
32
31
|
private prepareEvidence;
|
|
33
32
|
writePublication(): Promise<void>;
|
|
34
33
|
commit(): Promise<void>;
|
package/dist/src/operations.js
CHANGED
|
@@ -9,6 +9,7 @@ import { invokeStage } from "./invocation.js";
|
|
|
9
9
|
import { defaultStagePrompts } from "./prompts.js";
|
|
10
10
|
import { publicationSteps } from "./recovery.js";
|
|
11
11
|
import { command } from "./runtime/process.js";
|
|
12
|
+
import { selectValidation } from "./validation-selection.js";
|
|
12
13
|
const maxStepAttempts = 3;
|
|
13
14
|
/** Reusable durable coding operations. Call from a registered DBOS workflow. */
|
|
14
15
|
export class Operations {
|
|
@@ -76,8 +77,14 @@ export class Operations {
|
|
|
76
77
|
return this.step("eligibility", async (run) => {
|
|
77
78
|
const { hosting, project } = this.dependencies;
|
|
78
79
|
const issue = await hosting.getIssue(run.issue.number);
|
|
79
|
-
|
|
80
|
-
project.labels.every((label) => issue.labels.includes(label)))
|
|
80
|
+
if (!issue.open ||
|
|
81
|
+
!project.labels.every((label) => issue.labels.includes(label)))
|
|
82
|
+
return false;
|
|
83
|
+
const { profile } = selectValidation(run.issue.body, project);
|
|
84
|
+
await this.dependencies.store.patchRun(run.id, {
|
|
85
|
+
validationProfile: profile,
|
|
86
|
+
});
|
|
87
|
+
return true;
|
|
81
88
|
});
|
|
82
89
|
}
|
|
83
90
|
async prepare() {
|
|
@@ -104,33 +111,8 @@ export class Operations {
|
|
|
104
111
|
task,
|
|
105
112
|
stepId: DBOS.stepID,
|
|
106
113
|
dependencies: this.dependencies,
|
|
107
|
-
saveImplementationSnapshot: (runId, expected, provider) => this.saveImplementationSnapshot(runId, expected, provider),
|
|
108
|
-
acceptContribution: (runId, candidate) => this.acceptContribution(runId, candidate),
|
|
109
114
|
}));
|
|
110
115
|
}
|
|
111
|
-
async saveImplementationSnapshot(runId, expected, provider) {
|
|
112
|
-
const { workspace, project, store } = this.dependencies;
|
|
113
|
-
const snapshot = await workspace.inspect(project);
|
|
114
|
-
if (snapshot.head !== expected.head || snapshot.branch !== expected.branch)
|
|
115
|
-
throw new BlockedError("Agent changed branch or committed unexpectedly");
|
|
116
|
-
await store.patchRun(runId, { snapshot });
|
|
117
|
-
return snapshot.fingerprint === expected.fingerprint
|
|
118
|
-
? undefined
|
|
119
|
-
: {
|
|
120
|
-
provider,
|
|
121
|
-
beforeFiles: expected.files,
|
|
122
|
-
afterFiles: snapshot.files,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
async acceptContribution(runId, candidate) {
|
|
126
|
-
const run = await this.dependencies.store.run(runId);
|
|
127
|
-
await this.dependencies.store.patchRun(runId, {
|
|
128
|
-
contributionCandidates: [
|
|
129
|
-
...(run.contributionCandidates ?? []),
|
|
130
|
-
candidate,
|
|
131
|
-
],
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
116
|
async implement() {
|
|
135
117
|
await this.invoke("implementation", this.dependencies.project.stages.implementation, {
|
|
136
118
|
defaultPrompt: defaultStagePrompts.implementation,
|
|
@@ -139,7 +121,7 @@ export class Operations {
|
|
|
139
121
|
}
|
|
140
122
|
async validate() {
|
|
141
123
|
return this.step("validation", async (run) => {
|
|
142
|
-
const { project, workspace,
|
|
124
|
+
const { project, workspace, signal, redact } = this.dependencies;
|
|
143
125
|
if (!run.snapshot)
|
|
144
126
|
throw new BlockedError("Missing implementation snapshot");
|
|
145
127
|
await workspace.verify(project, run.snapshot);
|
|
@@ -148,7 +130,8 @@ export class Operations {
|
|
|
148
130
|
const validation = [];
|
|
149
131
|
const directory = join(this.dependencies.artifacts, run.id);
|
|
150
132
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
151
|
-
|
|
133
|
+
const checks = selectValidation(run.issue.body, project).commands;
|
|
134
|
+
for (const [index, check] of checks.entries()) {
|
|
152
135
|
const startedAt = new Date().toISOString();
|
|
153
136
|
const log = join(directory, `validation-${index}.log`);
|
|
154
137
|
let captured = "";
|
|
@@ -169,25 +152,23 @@ export class Operations {
|
|
|
169
152
|
await appendFile(log, redact(captured + "\n" + String(error)), {
|
|
170
153
|
mode: 0o600,
|
|
171
154
|
});
|
|
172
|
-
|
|
155
|
+
await this.recordValidation(run.id, validation, {
|
|
173
156
|
...check,
|
|
174
157
|
exitCode: -1,
|
|
175
158
|
log,
|
|
176
159
|
startedAt,
|
|
177
160
|
finishedAt: new Date().toISOString(),
|
|
178
161
|
});
|
|
179
|
-
await store.patchRun(run.id, { validation });
|
|
180
162
|
throw error;
|
|
181
163
|
}
|
|
182
164
|
await appendFile(log, redact(captured), { mode: 0o600 });
|
|
183
|
-
|
|
165
|
+
await this.recordValidation(run.id, validation, {
|
|
184
166
|
...check,
|
|
185
167
|
exitCode: result.exitCode,
|
|
186
168
|
log,
|
|
187
169
|
startedAt,
|
|
188
170
|
finishedAt: new Date().toISOString(),
|
|
189
171
|
});
|
|
190
|
-
await store.patchRun(run.id, { validation });
|
|
191
172
|
if (result.exitCode !== 0)
|
|
192
173
|
throw new Error(`Validation failed: ${check.command} (exit ${result.exitCode})`);
|
|
193
174
|
}
|
|
@@ -195,6 +176,10 @@ export class Operations {
|
|
|
195
176
|
return true;
|
|
196
177
|
});
|
|
197
178
|
}
|
|
179
|
+
async recordValidation(runId, validation, result) {
|
|
180
|
+
validation.push(result);
|
|
181
|
+
await this.dependencies.store.patchRun(runId, { validation });
|
|
182
|
+
}
|
|
198
183
|
async prepareEvidence(name, published = false) {
|
|
199
184
|
return this.step(name, async (run) => {
|
|
200
185
|
const { project, workspace, artifacts, signal } = this.dependencies;
|