@mingchuno/agent-workflows 0.1.0 → 0.2.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 +32 -6
- package/dist/src/adapters/agents.js +6 -3
- package/dist/src/adapters/hosting.js +16 -10
- package/dist/src/adapters/sdk-protocol.d.ts +3 -3
- package/dist/src/adapters/sdk-protocol.js +9 -7
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +40 -19
- package/dist/src/config.d.ts +22 -22
- package/dist/src/config.js +31 -26
- package/dist/src/defaults.d.ts +2 -0
- package/dist/src/defaults.js +2 -0
- package/dist/src/domain.d.ts +24 -4
- package/dist/src/domain.js +10 -3
- package/dist/src/evidence.d.ts +54 -0
- package/dist/src/evidence.js +214 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/invocation.d.ts +24 -0
- package/dist/src/invocation.js +163 -0
- package/dist/src/operations.d.ts +7 -2
- package/dist/src/operations.js +76 -134
- package/dist/src/prompts.d.ts +28 -0
- package/dist/src/prompts.js +63 -0
- package/dist/src/recovery.d.ts +19 -0
- package/dist/src/recovery.js +99 -0
- package/dist/src/runner.d.ts +5 -0
- package/dist/src/runner.js +145 -18
- package/dist/src/runtime/process.d.ts +2 -0
- package/dist/src/runtime/process.js +41 -12
- package/dist/src/store.d.ts +21 -2
- package/dist/src/store.js +122 -1
- package/dist/src/tui/actions.d.ts +16 -0
- package/dist/src/tui/actions.js +23 -0
- package/dist/src/tui/constants.d.ts +6 -0
- package/dist/src/tui/constants.js +3 -0
- package/dist/src/{tui-data.d.ts → tui/data.d.ts} +8 -6
- package/dist/src/tui/data.js +141 -0
- package/dist/src/tui/dialogs.d.ts +17 -0
- package/dist/src/tui/dialogs.js +149 -0
- package/dist/src/tui/format.d.ts +7 -0
- package/dist/src/tui/format.js +62 -0
- package/dist/src/tui/index.d.ts +2 -0
- package/dist/src/tui/index.js +1 -0
- package/dist/src/tui/layout.d.ts +25 -0
- package/dist/src/tui/layout.js +36 -0
- package/dist/src/tui/log-file.d.ts +26 -0
- package/dist/src/tui/log-file.js +156 -0
- package/dist/src/tui/log.d.ts +11 -0
- package/dist/src/tui/log.js +90 -0
- package/dist/src/tui/monitor.d.ts +8 -0
- package/dist/src/tui/monitor.js +222 -0
- package/dist/src/tui/text.d.ts +3 -0
- package/dist/src/tui/text.js +10 -0
- package/dist/src/tui/use-log-controller.d.ts +27 -0
- package/dist/src/tui/use-log-controller.js +192 -0
- package/dist/src/tui/views.d.ts +17 -0
- package/dist/src/tui/views.js +97 -0
- package/docs/api.md +119 -6
- package/docs/architecture.md +21 -4
- package/docs/configuration.md +137 -5
- package/docs/database.md +7 -0
- package/docs/operations.md +117 -2
- package/docs/providers.md +58 -2
- package/docs/releases.md +34 -79
- package/examples/config.ts +2 -2
- package/package.json +4 -2
- package/dist/src/tui-data.js +0 -89
- package/dist/src/tui.d.ts +0 -5
- package/dist/src/tui.js +0 -69
- package/docs/acceptance.md +0 -35
package/dist/src/domain.js
CHANGED
|
@@ -4,13 +4,20 @@ export const publicationSchema = z.strictObject({
|
|
|
4
4
|
title: z.string().trim().min(1).max(240),
|
|
5
5
|
description: z.string().trim().min(1).max(60000),
|
|
6
6
|
});
|
|
7
|
-
export const reviewSchema = z
|
|
7
|
+
export const reviewSchema = z
|
|
8
|
+
.strictObject({
|
|
9
|
+
complete: z.boolean(),
|
|
10
|
+
limitations: z.array(z.string().trim().min(1)),
|
|
8
11
|
summary: z.string().min(1),
|
|
9
12
|
findings: z.array(z.strictObject({
|
|
10
13
|
body: z.string().min(1),
|
|
11
|
-
path: z.string().
|
|
12
|
-
line: z.number().int().positive().
|
|
14
|
+
path: z.string().nullable().default(null),
|
|
15
|
+
line: z.number().int().positive().nullable().default(null),
|
|
13
16
|
})),
|
|
17
|
+
})
|
|
18
|
+
.refine((review) => review.complete || review.limitations.length > 0, {
|
|
19
|
+
message: "Incomplete review requires at least one inspection limitation",
|
|
20
|
+
path: ["limitations"],
|
|
14
21
|
});
|
|
15
22
|
export class BlockedError extends Error {
|
|
16
23
|
constructor(message) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Project } from "./config.js";
|
|
2
|
+
import { type Snapshot } from "./domain.js";
|
|
3
|
+
export declare const evidenceLimits: {
|
|
4
|
+
readonly chunkBytes: number;
|
|
5
|
+
readonly totalBytes: number;
|
|
6
|
+
};
|
|
7
|
+
interface Artifact {
|
|
8
|
+
path: string;
|
|
9
|
+
sha256: string;
|
|
10
|
+
bytes: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ChangeEvidence {
|
|
13
|
+
index: string;
|
|
14
|
+
identity: string;
|
|
15
|
+
files: Artifact[];
|
|
16
|
+
changedPaths: number;
|
|
17
|
+
base: string;
|
|
18
|
+
head?: string;
|
|
19
|
+
snapshot?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Split even single long lines, preserving UTF-8 and exact reconstruction. */
|
|
22
|
+
export declare function chunkText(text: string): string[];
|
|
23
|
+
export declare class EvidenceWriter {
|
|
24
|
+
readonly directory: string;
|
|
25
|
+
readonly files: Artifact[];
|
|
26
|
+
private bytes;
|
|
27
|
+
constructor(directory: string);
|
|
28
|
+
write(name: string, content: string): Promise<Artifact>;
|
|
29
|
+
index(contents: string, metadata: Record<string, unknown>): Promise<Artifact>;
|
|
30
|
+
chunks(prefix: string, content: string): Promise<{
|
|
31
|
+
path: string;
|
|
32
|
+
sha256: string;
|
|
33
|
+
bytes: number;
|
|
34
|
+
ordinal: number;
|
|
35
|
+
byteOffset: number;
|
|
36
|
+
startLine: number;
|
|
37
|
+
}[]>;
|
|
38
|
+
}
|
|
39
|
+
interface CaptureOptions {
|
|
40
|
+
project: Project;
|
|
41
|
+
directory: string;
|
|
42
|
+
snapshot: Snapshot;
|
|
43
|
+
revisions?: {
|
|
44
|
+
base: string;
|
|
45
|
+
head: string;
|
|
46
|
+
};
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
}
|
|
49
|
+
export declare function captureEvidence(options: CaptureOptions): Promise<ChangeEvidence>;
|
|
50
|
+
export declare function verifyEvidence(evidence: ChangeEvidence): Promise<void>;
|
|
51
|
+
export declare function evidenceContext(evidence: ChangeEvidence): string;
|
|
52
|
+
/** Resolve through existing ancestors, rejecting source-tree writes before mkdir. */
|
|
53
|
+
export declare function assertEvidenceDirectory(checkout: string, requested: string): Promise<string>;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { mkdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { BlockedError } from "./domain.js";
|
|
4
|
+
import { sha256 } from "./prompts.js";
|
|
5
|
+
import { command, maxCapturedOutputBytes } from "./runtime/process.js";
|
|
6
|
+
export const evidenceLimits = {
|
|
7
|
+
chunkBytes: 64 * 1024,
|
|
8
|
+
totalBytes: maxCapturedOutputBytes,
|
|
9
|
+
};
|
|
10
|
+
/** Split even single long lines, preserving UTF-8 and exact reconstruction. */
|
|
11
|
+
export function chunkText(text) {
|
|
12
|
+
const buffer = Buffer.from(text);
|
|
13
|
+
const chunks = [];
|
|
14
|
+
for (let start = 0; start < buffer.length;) {
|
|
15
|
+
let end = Math.min(start + evidenceLimits.chunkBytes, buffer.length);
|
|
16
|
+
while (end < buffer.length && (buffer[end] & 0xc0) === 0x80)
|
|
17
|
+
end--;
|
|
18
|
+
chunks.push(buffer.subarray(start, end).toString("utf8"));
|
|
19
|
+
start = end;
|
|
20
|
+
}
|
|
21
|
+
return chunks;
|
|
22
|
+
}
|
|
23
|
+
export class EvidenceWriter {
|
|
24
|
+
directory;
|
|
25
|
+
files = [];
|
|
26
|
+
bytes = 0;
|
|
27
|
+
constructor(directory) {
|
|
28
|
+
this.directory = directory;
|
|
29
|
+
}
|
|
30
|
+
async write(name, content) {
|
|
31
|
+
const bytes = Buffer.byteLength(content);
|
|
32
|
+
const total = this.bytes + bytes;
|
|
33
|
+
if (total > evidenceLimits.totalBytes)
|
|
34
|
+
throw new BlockedError(`Change evidence size ${total} bytes exceeds limit ${evidenceLimits.totalBytes} bytes`);
|
|
35
|
+
if (bytes > evidenceLimits.chunkBytes)
|
|
36
|
+
throw new BlockedError(`Evidence chunk size ${bytes} exceeds limit ${evidenceLimits.chunkBytes}`);
|
|
37
|
+
const artifact = {
|
|
38
|
+
path: resolve(this.directory, name),
|
|
39
|
+
sha256: sha256(content),
|
|
40
|
+
bytes,
|
|
41
|
+
};
|
|
42
|
+
await writeFile(artifact.path, content, { mode: 0o600, flag: "wx" });
|
|
43
|
+
this.bytes = total;
|
|
44
|
+
this.files.push(artifact);
|
|
45
|
+
return artifact;
|
|
46
|
+
}
|
|
47
|
+
async index(contents, metadata) {
|
|
48
|
+
let pages = await this.chunks("index", contents);
|
|
49
|
+
let depth = 0;
|
|
50
|
+
const serialize = () => JSON.stringify({
|
|
51
|
+
version: 1,
|
|
52
|
+
...metadata,
|
|
53
|
+
indexDepth: depth,
|
|
54
|
+
instructions: "At depth 0, concatenate pages as JSONL change entries. At greater depth, concatenate pages as a JSON array of page references and descend one level. Read ordered patch chunks; hunk headers carry original line numbers. Untracked content starts at line 1. Report incomplete inspection explicitly.",
|
|
55
|
+
pages,
|
|
56
|
+
});
|
|
57
|
+
while (Buffer.byteLength(serialize()) > evidenceLimits.chunkBytes) {
|
|
58
|
+
depth++;
|
|
59
|
+
pages = await this.chunks(`catalog-${depth}`, JSON.stringify(pages));
|
|
60
|
+
}
|
|
61
|
+
return this.write("index.json", serialize());
|
|
62
|
+
}
|
|
63
|
+
async chunks(prefix, content) {
|
|
64
|
+
const result = [];
|
|
65
|
+
let byteOffset = 0, line = 1;
|
|
66
|
+
for (const [ordinal, part] of chunkText(content).entries()) {
|
|
67
|
+
result.push({
|
|
68
|
+
...(await this.write(`${prefix}-${ordinal}.txt`, part)),
|
|
69
|
+
ordinal,
|
|
70
|
+
byteOffset,
|
|
71
|
+
startLine: line,
|
|
72
|
+
});
|
|
73
|
+
byteOffset += Buffer.byteLength(part);
|
|
74
|
+
line += part.split("\n").length - 1;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export async function captureEvidence(options) {
|
|
80
|
+
const { project, snapshot, revisions, signal } = options;
|
|
81
|
+
const checkout = await realpath(project.checkout);
|
|
82
|
+
const directory = await assertEvidenceDirectory(checkout, options.directory);
|
|
83
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
84
|
+
const writer = new EvidenceWriter(directory);
|
|
85
|
+
const git = async (...args) => (await command("git", args, { cwd: checkout, signal, strictUtf8: true }))
|
|
86
|
+
.stdout;
|
|
87
|
+
const diff = (...args) => git("--literal-pathspecs", "diff", "--no-ext-diff", "--no-textconv", "--no-renames", "--full-index", ...args);
|
|
88
|
+
const split = (text) => text.split("\0").filter(Boolean);
|
|
89
|
+
const untracked = revisions
|
|
90
|
+
? []
|
|
91
|
+
: split(await git("ls-files", "--others", "--exclude-standard", "-z"));
|
|
92
|
+
const paths = revisions
|
|
93
|
+
? split(await diff("--name-only", "-z", revisions.base, revisions.head))
|
|
94
|
+
: [
|
|
95
|
+
...new Set([
|
|
96
|
+
...snapshot.paths,
|
|
97
|
+
...split(await diff("--cached", "--name-only", "-z")),
|
|
98
|
+
...split(await diff("--name-only", "-z")),
|
|
99
|
+
...untracked,
|
|
100
|
+
]),
|
|
101
|
+
].sort();
|
|
102
|
+
const entries = [];
|
|
103
|
+
for (const [number, path] of paths.entries()) {
|
|
104
|
+
signal?.throwIfAborted();
|
|
105
|
+
if (untracked.includes(path)) {
|
|
106
|
+
const content = await readFile(resolve(checkout, path));
|
|
107
|
+
let text;
|
|
108
|
+
try {
|
|
109
|
+
if (!content.includes(0))
|
|
110
|
+
text = new TextDecoder("utf-8", {
|
|
111
|
+
fatal: true,
|
|
112
|
+
ignoreBOM: true,
|
|
113
|
+
}).decode(content);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
/* Binary metadata is intentional. */
|
|
117
|
+
}
|
|
118
|
+
entries.push({
|
|
119
|
+
path,
|
|
120
|
+
kind: "untracked",
|
|
121
|
+
change: "added",
|
|
122
|
+
sha256: sha256(content),
|
|
123
|
+
bytes: content.length,
|
|
124
|
+
binary: text === undefined,
|
|
125
|
+
reason: text === undefined
|
|
126
|
+
? "Binary or non-UTF-8 content; metadata only"
|
|
127
|
+
: undefined,
|
|
128
|
+
chunks: text === undefined ? [] : await writer.chunks(`file-${number}`, text),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const ranges = revisions
|
|
132
|
+
? [{ kind: "published", args: [revisions.base, revisions.head] }]
|
|
133
|
+
: [
|
|
134
|
+
{ kind: "staged", args: ["--cached", snapshot.head] },
|
|
135
|
+
{ kind: "unstaged", args: [] },
|
|
136
|
+
];
|
|
137
|
+
for (const range of ranges) {
|
|
138
|
+
const patch = await diff(...range.args, "--", path);
|
|
139
|
+
if (!patch)
|
|
140
|
+
continue;
|
|
141
|
+
const blobs = /^index ([0-9a-f]+)\.\.([0-9a-f]+)/m.exec(patch);
|
|
142
|
+
entries.push({
|
|
143
|
+
path,
|
|
144
|
+
kind: range.kind,
|
|
145
|
+
change: /^new file mode /m.test(patch)
|
|
146
|
+
? "added"
|
|
147
|
+
: /^deleted file mode /m.test(patch)
|
|
148
|
+
? "deleted"
|
|
149
|
+
: "modified",
|
|
150
|
+
blobs: blobs ? { before: blobs[1], after: blobs[2] } : undefined,
|
|
151
|
+
sha256: sha256(patch),
|
|
152
|
+
binary: /^Binary files /m.test(patch),
|
|
153
|
+
reason: /^Binary files /m.test(patch)
|
|
154
|
+
? "Git binary change; metadata only"
|
|
155
|
+
: undefined,
|
|
156
|
+
// Patch headers/hunks preserve original old/new line numbers. Offsets support split long lines.
|
|
157
|
+
chunks: await writer.chunks(`patch-${number}-${range.kind}`, patch),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const identity = {
|
|
162
|
+
base: revisions?.base ?? snapshot.head,
|
|
163
|
+
...(revisions
|
|
164
|
+
? { head: revisions.head }
|
|
165
|
+
: { snapshot: snapshot.fingerprint }),
|
|
166
|
+
};
|
|
167
|
+
const index = await writer.index(entries.map((entry) => JSON.stringify(entry)).join("\n") + "\n", { ...identity, changedPaths: paths.length });
|
|
168
|
+
const evidence = {
|
|
169
|
+
...identity,
|
|
170
|
+
index: index.path,
|
|
171
|
+
identity: index.sha256,
|
|
172
|
+
files: writer.files,
|
|
173
|
+
changedPaths: paths.length,
|
|
174
|
+
};
|
|
175
|
+
await verifyEvidence(evidence);
|
|
176
|
+
return evidence;
|
|
177
|
+
}
|
|
178
|
+
export async function verifyEvidence(evidence) {
|
|
179
|
+
for (const file of evidence.files) {
|
|
180
|
+
const content = await readFile(file.path).catch((cause) => {
|
|
181
|
+
throw new BlockedError(`Required change evidence unavailable: ${file.path}: ${String(cause)}`);
|
|
182
|
+
});
|
|
183
|
+
if (content.length !== file.bytes || sha256(content) !== file.sha256)
|
|
184
|
+
throw new BlockedError(`Change evidence changed: ${file.path}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
export function evidenceContext(evidence) {
|
|
188
|
+
return `Change evidence index: ${evidence.index}\nIdentity: ${evidence.identity}\nBase: ${evidence.base}\nHead: ${evidence.head ?? "verified dirty snapshot"}\nChanged paths: ${evidence.changedPaths}\nRead the index and its ordered artifacts incrementally. Use read-only tools and do not edit files. Required evidence must be readable; never infer missing content. Binary content is metadata-only.`;
|
|
189
|
+
}
|
|
190
|
+
/** Resolve through existing ancestors, rejecting source-tree writes before mkdir. */
|
|
191
|
+
export async function assertEvidenceDirectory(checkout, requested) {
|
|
192
|
+
const canonicalCheckout = await realpath(checkout);
|
|
193
|
+
let ancestor = resolve(requested);
|
|
194
|
+
const suffix = [];
|
|
195
|
+
while (true) {
|
|
196
|
+
try {
|
|
197
|
+
const canonical = await realpath(ancestor);
|
|
198
|
+
const destination = resolve(canonical, ...suffix);
|
|
199
|
+
const location = relative(canonicalCheckout, destination);
|
|
200
|
+
if (!location ||
|
|
201
|
+
(!isAbsolute(location) &&
|
|
202
|
+
location !== ".." &&
|
|
203
|
+
!location.startsWith(`..${sep}`)))
|
|
204
|
+
throw new BlockedError("State directory for change evidence must be outside the managed checkout");
|
|
205
|
+
return destination;
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (error.code !== "ENOENT")
|
|
209
|
+
throw error;
|
|
210
|
+
suffix.unshift(relative(dirname(ancestor), ancestor));
|
|
211
|
+
ancestor = dirname(ancestor);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type Stage } from "./config.js";
|
|
3
|
+
import { type RunRecord, type Snapshot } from "./domain.js";
|
|
4
|
+
import { type ChangeEvidence } from "./evidence.js";
|
|
5
|
+
import type { OperationDependencies } from "./operations.js";
|
|
6
|
+
export interface InvocationTask {
|
|
7
|
+
defaultPrompt: string;
|
|
8
|
+
context?: (run: RunRecord) => string;
|
|
9
|
+
readOnly?: boolean;
|
|
10
|
+
outputContract?: z.ZodType;
|
|
11
|
+
evidence?: ChangeEvidence;
|
|
12
|
+
}
|
|
13
|
+
interface StageExecution {
|
|
14
|
+
run: RunRecord;
|
|
15
|
+
name: string;
|
|
16
|
+
stage: Stage;
|
|
17
|
+
task: InvocationTask;
|
|
18
|
+
stepId: number;
|
|
19
|
+
dependencies: OperationDependencies;
|
|
20
|
+
saveImplementationSnapshot: (runId: string, snapshot: Snapshot) => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/** One logical stage; only returned format errors admit a second response attempt. */
|
|
23
|
+
export declare function invokeStage({ run, name, stage, task, stepId, dependencies, saveImplementationSnapshot, }: StageExecution): Promise<string>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { resolveProfile } from "./config.js";
|
|
6
|
+
import { BlockedError } from "./domain.js";
|
|
7
|
+
import { verifyEvidence } from "./evidence.js";
|
|
8
|
+
import { resolveStagePrompt, sha256 } from "./prompts.js";
|
|
9
|
+
const maxInvocationAttempts = 2;
|
|
10
|
+
/** One logical stage; only returned format errors admit a second response attempt. */
|
|
11
|
+
export async function invokeStage({ run, name, stage, task, stepId, dependencies, saveImplementationSnapshot, }) {
|
|
12
|
+
const { store, project, agents, signal, workspace, redact } = dependencies;
|
|
13
|
+
const previous = (await store.invocations(run.id)).filter((item) => item.stepId === stepId);
|
|
14
|
+
if (previous.length) {
|
|
15
|
+
for (const record of previous) {
|
|
16
|
+
if (record.outcome !== "running")
|
|
17
|
+
continue;
|
|
18
|
+
record.outcome = "interrupted";
|
|
19
|
+
record.finishedAt = new Date().toISOString();
|
|
20
|
+
if (!record.sessionId)
|
|
21
|
+
record.sessionState = "unavailable";
|
|
22
|
+
await store.saveInvocation(record);
|
|
23
|
+
}
|
|
24
|
+
throw new BlockedError(`Interrupted agent stage ${name}; inspect existing sessions before explicit retry`);
|
|
25
|
+
}
|
|
26
|
+
if (!run.snapshot)
|
|
27
|
+
throw new BlockedError("Missing workspace snapshot");
|
|
28
|
+
await workspace.verify(project, run.snapshot);
|
|
29
|
+
if (task.evidence)
|
|
30
|
+
await verifyEvidence(task.evidence);
|
|
31
|
+
const resolved = resolveStagePrompt(stage, task.defaultPrompt, dependencies.promptBaseDirectory);
|
|
32
|
+
const outputSchema = task.outputContract
|
|
33
|
+
? z.toJSONSchema(task.outputContract)
|
|
34
|
+
: undefined;
|
|
35
|
+
const contract = outputSchema
|
|
36
|
+
? `Return ONLY JSON satisfying this application-owned schema:\n${JSON.stringify(outputSchema)}`
|
|
37
|
+
: "";
|
|
38
|
+
const fullPrompt = [
|
|
39
|
+
resolved.content,
|
|
40
|
+
`Run context:\n${task.context?.(run) ?? ""}`,
|
|
41
|
+
task.readOnly
|
|
42
|
+
? "Inspection only. Do not modify files, commit, push, or publish."
|
|
43
|
+
: "Do not commit, push, or publish.",
|
|
44
|
+
contract,
|
|
45
|
+
].join("\n\n");
|
|
46
|
+
const profile = resolveProfile(project.agent, stage.profile);
|
|
47
|
+
const adapter = agents[profile.provider];
|
|
48
|
+
if (!adapter)
|
|
49
|
+
throw new Error(`Missing agent adapter ${profile.provider}`);
|
|
50
|
+
const deadline = Date.now() + stage.timeoutMs;
|
|
51
|
+
const invocationSignal = AbortSignal.any([
|
|
52
|
+
signal,
|
|
53
|
+
AbortSignal.timeout(stage.timeoutMs),
|
|
54
|
+
]);
|
|
55
|
+
const effective = await adapter.validate(profile, invocationSignal);
|
|
56
|
+
const directory = join(dependencies.artifacts, run.id);
|
|
57
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
58
|
+
let correction = "";
|
|
59
|
+
for (let attempt = 1; attempt <= maxInvocationAttempts; attempt++) {
|
|
60
|
+
invocationSignal.throwIfAborted();
|
|
61
|
+
const expected = (await store.run(run.id)).snapshot;
|
|
62
|
+
await workspace.verify(project, expected);
|
|
63
|
+
if (task.evidence)
|
|
64
|
+
await verifyEvidence(task.evidence);
|
|
65
|
+
const id = randomUUID();
|
|
66
|
+
const prompt = fullPrompt + correction;
|
|
67
|
+
const readOnly = task.readOnly === true || attempt === maxInvocationAttempts;
|
|
68
|
+
const record = {
|
|
69
|
+
id,
|
|
70
|
+
runId: run.id,
|
|
71
|
+
projectId: project.id,
|
|
72
|
+
step: name,
|
|
73
|
+
stepId,
|
|
74
|
+
attempt,
|
|
75
|
+
provider: profile.provider,
|
|
76
|
+
sessionId: null,
|
|
77
|
+
sessionState: "pending",
|
|
78
|
+
requested: profile,
|
|
79
|
+
effective,
|
|
80
|
+
prompt: redact(prompt),
|
|
81
|
+
taskPrompt: { ...resolved, content: redact(resolved.content) },
|
|
82
|
+
outputContract: outputSchema
|
|
83
|
+
? sha256(JSON.stringify(outputSchema))
|
|
84
|
+
: undefined,
|
|
85
|
+
evidence: task.evidence,
|
|
86
|
+
outcome: "running",
|
|
87
|
+
startedAt: new Date().toISOString(),
|
|
88
|
+
log: join(directory, `${id}.jsonl`),
|
|
89
|
+
};
|
|
90
|
+
await store.saveInvocation(record);
|
|
91
|
+
try {
|
|
92
|
+
invocationSignal.throwIfAborted();
|
|
93
|
+
const output = await adapter.invoke({
|
|
94
|
+
id,
|
|
95
|
+
runId: run.id,
|
|
96
|
+
step: name,
|
|
97
|
+
cwd: project.checkout,
|
|
98
|
+
prompt,
|
|
99
|
+
profile,
|
|
100
|
+
outputSchema,
|
|
101
|
+
processFile: record.log + ".process.json",
|
|
102
|
+
readOnly,
|
|
103
|
+
signal: invocationSignal,
|
|
104
|
+
timeoutMs: Math.max(1, deadline - Date.now()),
|
|
105
|
+
session: async (sessionId) => {
|
|
106
|
+
record.sessionId = sessionId;
|
|
107
|
+
record.sessionState = "available";
|
|
108
|
+
await store.saveInvocation(record);
|
|
109
|
+
},
|
|
110
|
+
event: async (event) => {
|
|
111
|
+
await appendFile(record.log, redact(JSON.stringify(event)) + "\n", {
|
|
112
|
+
mode: 0o600,
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
await appendFile(record.log, "", { mode: 0o600 });
|
|
117
|
+
invocationSignal.throwIfAborted();
|
|
118
|
+
if (readOnly)
|
|
119
|
+
await workspace.verify(project, expected);
|
|
120
|
+
else
|
|
121
|
+
await saveImplementationSnapshot(run.id, expected);
|
|
122
|
+
if (task.evidence)
|
|
123
|
+
await verifyEvidence(task.evidence);
|
|
124
|
+
invocationSignal.throwIfAborted();
|
|
125
|
+
// Only returned output validation failures qualify for correction.
|
|
126
|
+
let parsed;
|
|
127
|
+
try {
|
|
128
|
+
parsed = task.outputContract
|
|
129
|
+
? task.outputContract.parse(JSON.parse(output))
|
|
130
|
+
: undefined;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (!(error instanceof SyntaxError) && !(error instanceof z.ZodError))
|
|
134
|
+
throw error;
|
|
135
|
+
record.outcome = "invalid-output";
|
|
136
|
+
record.validationError = redact(String(error));
|
|
137
|
+
await appendFile(record.log, redact(JSON.stringify({
|
|
138
|
+
type: "invalid-output",
|
|
139
|
+
output,
|
|
140
|
+
error: String(error),
|
|
141
|
+
})) + "\n");
|
|
142
|
+
if (attempt === maxInvocationAttempts)
|
|
143
|
+
throw new Error(`Invalid output after one correction: ${String(error)}`);
|
|
144
|
+
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)}`;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
record.outcome = "completed";
|
|
148
|
+
return redact(task.outputContract ? JSON.stringify(parsed) : output);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (record.outcome === "running")
|
|
152
|
+
record.outcome = "failed";
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
record.finishedAt = new Date().toISOString();
|
|
157
|
+
if (!record.sessionId)
|
|
158
|
+
record.sessionState = "unavailable";
|
|
159
|
+
await store.saveInvocation(record);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
throw new Error("Format correction exhausted");
|
|
163
|
+
}
|
package/dist/src/operations.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { Project, Stage } from "./config.js";
|
|
2
2
|
import { type AgentAdapter, type HostingAdapter, type RunRecord, type Workspace } from "./domain.js";
|
|
3
|
+
import { type InvocationTask } from "./invocation.js";
|
|
3
4
|
import type { Store } from "./store.js";
|
|
5
|
+
export type { InvocationTask } from "./invocation.js";
|
|
4
6
|
export interface OperationDependencies {
|
|
7
|
+
promptBaseDirectory?: string;
|
|
5
8
|
store: Store;
|
|
6
9
|
project: Project;
|
|
7
10
|
workspace: Workspace;
|
|
@@ -10,6 +13,8 @@ export interface OperationDependencies {
|
|
|
10
13
|
artifacts: string;
|
|
11
14
|
signal: AbortSignal;
|
|
12
15
|
redact: (text: string) => string;
|
|
16
|
+
beforeStep?: () => Promise<void>;
|
|
17
|
+
executionFingerprint?: () => Promise<string>;
|
|
13
18
|
}
|
|
14
19
|
/** Reusable durable coding operations. Call from a registered DBOS workflow. */
|
|
15
20
|
export declare class Operations {
|
|
@@ -19,11 +24,11 @@ export declare class Operations {
|
|
|
19
24
|
step<T>(name: string, operation: (run: RunRecord) => Promise<T>): Promise<T>;
|
|
20
25
|
eligible(): Promise<boolean>;
|
|
21
26
|
prepare(): Promise<void>;
|
|
22
|
-
invoke(name: string, stage: Stage,
|
|
23
|
-
private prepareInvocationPrompt;
|
|
27
|
+
invoke(name: string, stage: Stage, task: InvocationTask): Promise<string>;
|
|
24
28
|
private saveImplementationSnapshot;
|
|
25
29
|
implement(): Promise<void>;
|
|
26
30
|
validate(): Promise<boolean>;
|
|
31
|
+
private prepareEvidence;
|
|
27
32
|
writePublication(): Promise<void>;
|
|
28
33
|
commit(): Promise<void>;
|
|
29
34
|
push(): Promise<void>;
|