@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.
@@ -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: { ...auth.headers, ...options?.headers },
61
- env: { ...auth.env, ...options?.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: { ...auth.headers, ...options?.headers },
70
- env: { ...auth.env, ...options?.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 parent = ctx.modelRegistry.getProvider(ctx.model.provider);
84
- if (!parent) throw new Error("parent Provider is unavailable");
143
+ const selectedModel = ctx.model;
144
+ const selectedModelKey = modelKeyFor(selectedModel);
85
145
  const resolved = await abortable(
86
- ctx.modelRegistry.getApiKeyAndHeaders(ctx.model),
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
- ...ctx.model,
96
- baseUrl: resolved.baseUrl ?? ctx.model.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", reject);
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
- resolve({
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", "-"],
@@ -1,5 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import type { Bound } from "./file-snapshot.ts";
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: { diff: string; writeSet: string[]; root: string }): string {
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
  }