@abelxiaoxing/cadence 1.0.1 → 1.0.2
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/package.json +1 -1
- package/prompts/abel-implement.md +13 -2
- package/skills/abel-workflow/SKILL.md +7 -1
- package/src/candidate-preflight.ts +1163 -0
- package/src/child-session.ts +46 -5
- package/src/contracts.ts +379 -48
- package/src/file-snapshot.ts +62 -8
- package/src/index.ts +55 -10
- package/src/parent-payload-bridge.ts +442 -0
- package/src/parent-provider.ts +91 -12
- package/src/patch.ts +73 -3
- package/src/result-store.ts +40 -3
- package/src/runtime.ts +775 -66
- package/src/scheduler.ts +1 -0
- package/src/scoped-tools.ts +27 -6
- package/src/submit-tool.ts +3 -1
- package/src/worker.ts +136 -26
package/src/parent-provider.ts
CHANGED
|
@@ -7,6 +7,31 @@ import {
|
|
|
7
7
|
type ExtensionContext,
|
|
8
8
|
ModelRuntime,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import type {
|
|
11
|
+
ParentPayloadBridge,
|
|
12
|
+
ParentPayloadCallback,
|
|
13
|
+
ParentPayloadCapture,
|
|
14
|
+
} from "./parent-payload-bridge.ts";
|
|
15
|
+
|
|
16
|
+
export interface PhasePayloadBridge {
|
|
17
|
+
readonly bridge: ParentPayloadBridge;
|
|
18
|
+
readonly capture: ParentPayloadCapture;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function phasePayloadCallback(
|
|
22
|
+
payloadBridge: PhasePayloadBridge,
|
|
23
|
+
childOnPayload: ParentPayloadCallback | undefined,
|
|
24
|
+
signal: AbortSignal | undefined,
|
|
25
|
+
): ParentPayloadCallback {
|
|
26
|
+
return (payload, model) =>
|
|
27
|
+
payloadBridge.bridge.composePayload(
|
|
28
|
+
payloadBridge.capture,
|
|
29
|
+
payload,
|
|
30
|
+
model,
|
|
31
|
+
childOnPayload,
|
|
32
|
+
signal,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
10
35
|
|
|
11
36
|
export async function runtimeForProvider(
|
|
12
37
|
provider: Provider,
|
|
@@ -35,6 +60,7 @@ export function phaseProvider(
|
|
|
35
60
|
baseUrl?: string;
|
|
36
61
|
env?: Record<string, string>;
|
|
37
62
|
},
|
|
63
|
+
payloadBridge: PhasePayloadBridge,
|
|
38
64
|
): Provider {
|
|
39
65
|
return {
|
|
40
66
|
...parent,
|
|
@@ -54,47 +80,100 @@ export function phaseProvider(
|
|
|
54
80
|
},
|
|
55
81
|
},
|
|
56
82
|
stream(model, context, options) {
|
|
83
|
+
const onPayload = phasePayloadCallback(
|
|
84
|
+
payloadBridge,
|
|
85
|
+
options?.onPayload as ParentPayloadCallback | undefined,
|
|
86
|
+
options?.signal,
|
|
87
|
+
);
|
|
57
88
|
return parent.stream(model, context, {
|
|
58
89
|
...options,
|
|
59
90
|
apiKey: auth.apiKey,
|
|
60
|
-
headers: { ...
|
|
61
|
-
env: { ...
|
|
91
|
+
headers: { ...options?.headers, ...auth.headers },
|
|
92
|
+
env: { ...options?.env, ...auth.env },
|
|
62
93
|
maxRetries: 0,
|
|
94
|
+
onPayload,
|
|
63
95
|
} as never);
|
|
64
96
|
},
|
|
65
97
|
streamSimple(model, context, options) {
|
|
98
|
+
const onPayload = phasePayloadCallback(
|
|
99
|
+
payloadBridge,
|
|
100
|
+
options?.onPayload as ParentPayloadCallback | undefined,
|
|
101
|
+
options?.signal,
|
|
102
|
+
);
|
|
66
103
|
return parent.streamSimple(model, context, {
|
|
67
104
|
...options,
|
|
68
105
|
apiKey: auth.apiKey,
|
|
69
|
-
headers: { ...
|
|
70
|
-
env: { ...
|
|
106
|
+
headers: { ...options?.headers, ...auth.headers },
|
|
107
|
+
env: { ...options?.env, ...auth.env },
|
|
71
108
|
maxRetries: 0,
|
|
109
|
+
onPayload,
|
|
72
110
|
});
|
|
73
111
|
},
|
|
74
112
|
};
|
|
75
113
|
}
|
|
76
114
|
|
|
115
|
+
function modelKeyFor(model: Model<string>) {
|
|
116
|
+
return {
|
|
117
|
+
provider: model.provider,
|
|
118
|
+
id: model.id,
|
|
119
|
+
api: model.api,
|
|
120
|
+
baseUrl: model.baseUrl,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function sameSelectedModel(
|
|
125
|
+
left: Model<string>,
|
|
126
|
+
right: ReturnType<typeof modelKeyFor>,
|
|
127
|
+
): boolean {
|
|
128
|
+
return (
|
|
129
|
+
left.provider === right.provider &&
|
|
130
|
+
left.id === right.id &&
|
|
131
|
+
left.api === right.api &&
|
|
132
|
+
left.baseUrl === right.baseUrl
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
77
136
|
export async function runtimeFromContext(
|
|
78
137
|
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
138
|
+
payloadBridge: ParentPayloadBridge,
|
|
79
139
|
signal?: AbortSignal,
|
|
80
140
|
): Promise<{ modelRuntime: ModelRuntime; model: Model<string> }> {
|
|
81
141
|
signal?.throwIfAborted();
|
|
82
142
|
if (!ctx.model) throw new Error("parent model is unavailable");
|
|
83
|
-
const
|
|
84
|
-
|
|
143
|
+
const selectedModel = ctx.model;
|
|
144
|
+
const selectedModelKey = modelKeyFor(selectedModel);
|
|
85
145
|
const resolved = await abortable(
|
|
86
|
-
ctx.modelRegistry.getApiKeyAndHeaders(
|
|
146
|
+
ctx.modelRegistry.getApiKeyAndHeaders(selectedModel),
|
|
87
147
|
signal,
|
|
88
148
|
);
|
|
89
149
|
if (!resolved.ok) throw new Error(resolved.error);
|
|
90
150
|
signal?.throwIfAborted();
|
|
91
|
-
const provider = phaseProvider(parent, resolved);
|
|
92
|
-
const modelRuntime = await runtimeForProvider(provider, signal);
|
|
93
|
-
signal?.throwIfAborted();
|
|
94
151
|
const model = {
|
|
95
|
-
...
|
|
96
|
-
baseUrl: resolved.baseUrl ??
|
|
152
|
+
...selectedModel,
|
|
153
|
+
baseUrl: resolved.baseUrl ?? selectedModel.baseUrl,
|
|
97
154
|
} as Model<string>;
|
|
155
|
+
const effectiveModelKey = modelKeyFor(model);
|
|
156
|
+
const capture = payloadBridge.capture(effectiveModelKey, ctx.modelRegistry);
|
|
157
|
+
if (!capture) throw new Error("parent payload bridge is unavailable");
|
|
158
|
+
const requireReadyCapture = () => {
|
|
159
|
+
if (
|
|
160
|
+
!ctx.model ||
|
|
161
|
+
!sameSelectedModel(ctx.model, selectedModelKey) ||
|
|
162
|
+
payloadBridge.capture(effectiveModelKey, ctx.modelRegistry) !== capture
|
|
163
|
+
) {
|
|
164
|
+
throw new Error("parent payload bridge is unavailable");
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
requireReadyCapture();
|
|
168
|
+
const delegate = capture.delegate;
|
|
169
|
+
if (!delegate) throw new Error("parent Provider is unavailable");
|
|
170
|
+
const provider = phaseProvider(delegate, resolved, {
|
|
171
|
+
bridge: payloadBridge,
|
|
172
|
+
capture,
|
|
173
|
+
});
|
|
174
|
+
const modelRuntime = await runtimeForProvider(provider, signal);
|
|
175
|
+
signal?.throwIfAborted();
|
|
176
|
+
requireReadyCapture();
|
|
98
177
|
return { modelRuntime, model };
|
|
99
178
|
}
|
|
100
179
|
|
package/src/patch.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { preflightCandidate } from "./candidate-preflight.ts";
|
|
2
3
|
import { diffWritePaths } from "./contracts.ts";
|
|
3
4
|
import { isCurrent } from "./file-snapshot.ts";
|
|
4
5
|
import type { ResultStore } from "./result-store.ts";
|
|
@@ -7,48 +8,75 @@ interface CommandResult {
|
|
|
7
8
|
code: number;
|
|
8
9
|
stdout: Buffer;
|
|
9
10
|
stderr: Buffer;
|
|
11
|
+
cancelled: boolean;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
async function git(
|
|
13
15
|
root: string,
|
|
14
16
|
args: string[],
|
|
15
17
|
input: Buffer,
|
|
18
|
+
signal?: AbortSignal,
|
|
16
19
|
): Promise<CommandResult> {
|
|
17
20
|
return new Promise((resolve, reject) => {
|
|
18
21
|
const child = spawn("git", args, {
|
|
19
22
|
cwd: root,
|
|
20
23
|
shell: false,
|
|
21
24
|
stdio: ["pipe", "pipe", "pipe"],
|
|
25
|
+
...(signal ? { signal } : {}),
|
|
22
26
|
});
|
|
23
27
|
const stdout: Buffer[] = [];
|
|
24
28
|
const stderr: Buffer[] = [];
|
|
29
|
+
let settled = false;
|
|
30
|
+
const finish = (result: CommandResult) => {
|
|
31
|
+
if (settled) return;
|
|
32
|
+
settled = true;
|
|
33
|
+
resolve(result);
|
|
34
|
+
};
|
|
25
35
|
child.stdout.on("data", (b: Buffer) => stdout.push(b));
|
|
26
36
|
child.stderr.on("data", (b: Buffer) => stderr.push(b));
|
|
27
|
-
child.on("error",
|
|
37
|
+
child.on("error", (error) => {
|
|
38
|
+
if (signal?.aborted || error.name === "AbortError") {
|
|
39
|
+
finish({
|
|
40
|
+
code: 1,
|
|
41
|
+
stdout: Buffer.concat(stdout),
|
|
42
|
+
stderr: Buffer.concat(stderr),
|
|
43
|
+
cancelled: true,
|
|
44
|
+
});
|
|
45
|
+
} else {
|
|
46
|
+
reject(error);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
28
49
|
child.on("close", (code) =>
|
|
29
|
-
|
|
50
|
+
finish({
|
|
30
51
|
code: code ?? 1,
|
|
31
52
|
stdout: Buffer.concat(stdout),
|
|
32
53
|
stderr: Buffer.concat(stderr),
|
|
54
|
+
cancelled: signal?.aborted ?? false,
|
|
33
55
|
}),
|
|
34
56
|
);
|
|
35
57
|
child.stdin.end(input);
|
|
36
58
|
});
|
|
37
59
|
}
|
|
38
60
|
|
|
61
|
+
function cancellationFailure(): { ok: false; error: string } {
|
|
62
|
+
return { ok: false, error: "candidate application cancelled" };
|
|
63
|
+
}
|
|
64
|
+
|
|
39
65
|
export async function applyRetainedPatch(input: {
|
|
40
66
|
root: string;
|
|
41
67
|
id: string;
|
|
42
68
|
store: ResultStore;
|
|
69
|
+
signal?: AbortSignal;
|
|
43
70
|
}): Promise<
|
|
44
71
|
| { ok: true; targets: string[]; checkExitCode: 0; applyExitCode: 0 }
|
|
45
72
|
| { ok: false; error: string }
|
|
46
73
|
> {
|
|
74
|
+
if (input.signal?.aborted) return cancellationFailure();
|
|
47
75
|
const retained = input.store.get(input.id);
|
|
48
76
|
if (!retained) return { ok: false, error: "retained result not found" };
|
|
49
77
|
if (retained.root !== input.root)
|
|
50
78
|
return { ok: false, error: "retained result root mismatch" };
|
|
51
|
-
if (!isCurrent(input.root, retained.snapshot))
|
|
79
|
+
if (!retained.verification && !isCurrent(input.root, retained.snapshot))
|
|
52
80
|
return { ok: false, error: "stale file snapshot" };
|
|
53
81
|
|
|
54
82
|
let targets: string[];
|
|
@@ -64,11 +92,48 @@ export async function applyRetainedPatch(input: {
|
|
|
64
92
|
};
|
|
65
93
|
}
|
|
66
94
|
|
|
95
|
+
if (retained.verification) {
|
|
96
|
+
if (
|
|
97
|
+
!retained.packageManifest ||
|
|
98
|
+
!retained.lockfile ||
|
|
99
|
+
!retained.dependencyTarget
|
|
100
|
+
) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
error: "retained candidate preflight inputs are incomplete",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const preflight = await preflightCandidate({
|
|
107
|
+
root: input.root,
|
|
108
|
+
diff: retained.diff,
|
|
109
|
+
writeSet: retained.writeSet,
|
|
110
|
+
snapshot: retained.snapshot,
|
|
111
|
+
baseline: retained.baseline,
|
|
112
|
+
verification: retained.verification,
|
|
113
|
+
packageManifest: retained.packageManifest,
|
|
114
|
+
lockfile: retained.lockfile,
|
|
115
|
+
dependencyTarget: retained.dependencyTarget,
|
|
116
|
+
signal: input.signal,
|
|
117
|
+
});
|
|
118
|
+
if (!preflight.ok) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
error: `candidate preflight rejected: ${preflight.class}:${preflight.code}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (!isCurrent(input.root, retained.snapshot)) {
|
|
125
|
+
return { ok: false, error: "stale file snapshot after preflight" };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (input.signal?.aborted) return cancellationFailure();
|
|
129
|
+
|
|
67
130
|
const numstat = await git(
|
|
68
131
|
input.root,
|
|
69
132
|
["apply", "--numstat", "-z", "--recount", "-"],
|
|
70
133
|
retained.diff,
|
|
134
|
+
input.signal,
|
|
71
135
|
);
|
|
136
|
+
if (numstat.cancelled || input.signal?.aborted) return cancellationFailure();
|
|
72
137
|
if (numstat.code !== 0 || numstat.stdout.includes(Buffer.from("-\t-\t"))) {
|
|
73
138
|
return {
|
|
74
139
|
ok: false,
|
|
@@ -79,7 +144,9 @@ export async function applyRetainedPatch(input: {
|
|
|
79
144
|
input.root,
|
|
80
145
|
["apply", "--summary", "--recount", "-"],
|
|
81
146
|
retained.diff,
|
|
147
|
+
input.signal,
|
|
82
148
|
);
|
|
149
|
+
if (summary.cancelled || input.signal?.aborted) return cancellationFailure();
|
|
83
150
|
if (
|
|
84
151
|
summary.code !== 0 ||
|
|
85
152
|
/rename|copy|mode change|create mode 160000/i.test(
|
|
@@ -92,12 +159,15 @@ export async function applyRetainedPatch(input: {
|
|
|
92
159
|
input.root,
|
|
93
160
|
["apply", "--check", "--recount", "--whitespace=nowarn", "-"],
|
|
94
161
|
retained.diff,
|
|
162
|
+
input.signal,
|
|
95
163
|
);
|
|
164
|
+
if (check.cancelled || input.signal?.aborted) return cancellationFailure();
|
|
96
165
|
if (check.code !== 0)
|
|
97
166
|
return {
|
|
98
167
|
ok: false,
|
|
99
168
|
error: `git apply --check failed: ${check.stderr.toString("utf8")}`,
|
|
100
169
|
};
|
|
170
|
+
if (input.signal?.aborted) return cancellationFailure();
|
|
101
171
|
const apply = await git(
|
|
102
172
|
input.root,
|
|
103
173
|
["apply", "--recount", "--whitespace=nowarn", "-"],
|
package/src/result-store.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
BaselineEntry,
|
|
4
|
+
VerificationContract,
|
|
5
|
+
} from "./candidate-preflight.ts";
|
|
6
|
+
import type { Bound, DirBound, FileBound } from "./file-snapshot.ts";
|
|
3
7
|
import { snapshotFiles } from "./file-snapshot.ts";
|
|
4
8
|
|
|
5
9
|
export interface RetainedResult {
|
|
@@ -8,19 +12,52 @@ export interface RetainedResult {
|
|
|
8
12
|
writeSet: string[];
|
|
9
13
|
root: string;
|
|
10
14
|
snapshot: Bound;
|
|
15
|
+
baseline: BaselineEntry[];
|
|
16
|
+
verification?: VerificationContract;
|
|
17
|
+
packageManifest?: FileBound;
|
|
18
|
+
lockfile?: FileBound;
|
|
19
|
+
dependencyTarget?: FileBound | DirBound;
|
|
11
20
|
}
|
|
12
21
|
|
|
13
22
|
export class ResultStore {
|
|
14
23
|
private readonly results = new Map<string, RetainedResult>();
|
|
15
24
|
|
|
16
|
-
retain(input: {
|
|
25
|
+
retain(input: {
|
|
26
|
+
diff: string;
|
|
27
|
+
writeSet: string[];
|
|
28
|
+
root: string;
|
|
29
|
+
snapshot?: Bound;
|
|
30
|
+
baseline?: BaselineEntry[];
|
|
31
|
+
verification?: VerificationContract;
|
|
32
|
+
packageManifest?: FileBound;
|
|
33
|
+
lockfile?: FileBound;
|
|
34
|
+
dependencyTarget?: FileBound | DirBound;
|
|
35
|
+
}): string {
|
|
17
36
|
const id = randomUUID();
|
|
18
37
|
this.results.set(id, {
|
|
19
38
|
id,
|
|
20
39
|
diff: Buffer.from(input.diff, "utf8"),
|
|
21
40
|
writeSet: [...new Set(input.writeSet)],
|
|
22
41
|
root: input.root,
|
|
23
|
-
snapshot: snapshotFiles(input.root, input.writeSet),
|
|
42
|
+
snapshot: input.snapshot ?? snapshotFiles(input.root, input.writeSet),
|
|
43
|
+
baseline: (input.baseline ?? []).map((entry) => ({ ...entry })),
|
|
44
|
+
...(input.verification === undefined
|
|
45
|
+
? {}
|
|
46
|
+
: {
|
|
47
|
+
verification: {
|
|
48
|
+
...input.verification,
|
|
49
|
+
argv: [...input.verification.argv],
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
...(input.packageManifest === undefined
|
|
53
|
+
? {}
|
|
54
|
+
: { packageManifest: { ...input.packageManifest } }),
|
|
55
|
+
...(input.lockfile === undefined
|
|
56
|
+
? {}
|
|
57
|
+
: { lockfile: { ...input.lockfile } }),
|
|
58
|
+
...(input.dependencyTarget === undefined
|
|
59
|
+
? {}
|
|
60
|
+
: { dependencyTarget: { ...input.dependencyTarget } }),
|
|
24
61
|
});
|
|
25
62
|
return id;
|
|
26
63
|
}
|