@abelxiaoxing/cadence 1.8.2 → 1.8.4

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.
@@ -22,24 +22,18 @@ interface DurableRunResources extends DurableExecutionResources {
22
22
 
23
23
  import { lstatSync, mkdirSync, readFileSync } from "node:fs";
24
24
  import path from "node:path";
25
- import { DatabaseSync } from "node:sqlite";
26
25
  import { ApplyTransaction } from "./apply-transaction.ts";
27
26
  import { ArtifactStore } from "./artifact-store.ts";
28
27
  import { compareCanonicalStrings } from "./canonical.ts";
29
28
  import type { ImplementPlan, PlanTaskDraft } from "./delivery-compiler.ts";
30
- import type { RoutePolicy } from "./route-policy.ts";
31
29
  import { observeSafePath } from "./safe-path.ts";
32
- import { configureSqlite, ensureSqliteSchema } from "./sqlite-schema.ts";
33
- import { ROUTE_HEALTH_SCHEMA } from "./storage-schema.ts";
34
30
  import { TaskLedger } from "./task-ledger.ts";
35
- import { type RouteHealthStore, RunWorkerBroker } from "./worker-broker.ts";
36
31
  import {
37
32
  deliveryBoundPaths,
38
33
  deliveryTrackingPath,
39
34
  hash,
40
35
  hasVerificationLifecycle,
41
36
  IDENTIFIER,
42
- implementationRouteRequirements,
43
37
  isRecord,
44
38
  SHA256,
45
39
  type WorkflowApplication,
@@ -52,125 +46,6 @@ import {
52
46
  import { WorkflowEngine } from "./workflow-state-machine.ts";
53
47
  import { type WorkspaceRevision, WorkspaceStore } from "./workspace-store.ts";
54
48
 
55
- type DurableRouteHealth = Parameters<RouteHealthStore["set"]>[1];
56
-
57
- class DurableRouteHealthStore implements RouteHealthStore {
58
- readonly #databasePath: string;
59
- #database?: DatabaseSync;
60
- #closed = false;
61
-
62
- constructor(databasePath: string) {
63
- this.#databasePath = databasePath;
64
- }
65
-
66
- #open(): DatabaseSync {
67
- if (this.#closed) throw new Error("route-health-store-closed");
68
- if (this.#database) return this.#database;
69
- const database = new DatabaseSync(this.#databasePath);
70
- try {
71
- configureSqlite(database);
72
- ensureSqliteSchema(database, ROUTE_HEALTH_SCHEMA);
73
- } catch (error) {
74
- database.close();
75
- throw error;
76
- }
77
-
78
- this.#database = database;
79
- return database;
80
- }
81
-
82
- get(fingerprint: string): DurableRouteHealth | undefined {
83
- if (!SHA256.test(fingerprint)) return undefined;
84
- const row = this.#open()
85
- .prepare(
86
- `SELECT state, next_half_open_at, projection_json
87
- FROM route_health WHERE route_fingerprint = ?`,
88
- )
89
- .get(fingerprint) as
90
- | {
91
- state: string;
92
- next_half_open_at: number | null;
93
- projection_json: string;
94
- }
95
- | undefined;
96
- if (!row || !["healthy", "open", "half-open"].includes(row.state)) {
97
- return undefined;
98
- }
99
- let projection: unknown;
100
- try {
101
- projection = JSON.parse(row.projection_json);
102
- } catch {
103
- return undefined;
104
- }
105
- if (!isRecord(projection) || projection.state !== row.state) {
106
- return undefined;
107
- }
108
- const retryAt = row.next_half_open_at ?? undefined;
109
- const lastCode =
110
- typeof projection.lastCode === "string" &&
111
- projection.lastCode.length > 0 &&
112
- projection.lastCode.length <= 128
113
- ? projection.lastCode
114
- : undefined;
115
- const probeExpiresAt =
116
- Number.isSafeInteger(projection.probeExpiresAt) &&
117
- (projection.probeExpiresAt as number) >= 0
118
- ? (projection.probeExpiresAt as number)
119
- : undefined;
120
- return {
121
- state: row.state as DurableRouteHealth["state"],
122
- ...(retryAt === undefined ? {} : { retryAt }),
123
- ...(projection.probeInFlight === true ? { probeInFlight: true } : {}),
124
- ...(probeExpiresAt === undefined ? {} : { probeExpiresAt }),
125
- ...(lastCode === undefined ? {} : { lastCode }),
126
- };
127
- }
128
-
129
- set(fingerprint: string, health: DurableRouteHealth): void {
130
- if (
131
- !SHA256.test(fingerprint) ||
132
- !["healthy", "open", "half-open"].includes(health.state) ||
133
- (health.retryAt !== undefined &&
134
- (!Number.isSafeInteger(health.retryAt) || health.retryAt < 0)) ||
135
- (health.probeExpiresAt !== undefined &&
136
- (!Number.isSafeInteger(health.probeExpiresAt) ||
137
- health.probeExpiresAt < 0))
138
- ) {
139
- throw new Error("route-health-fact-invalid");
140
- }
141
- const projection = {
142
- state: health.state,
143
- ...(health.probeInFlight ? { probeInFlight: true } : {}),
144
- ...(health.probeExpiresAt === undefined
145
- ? {}
146
- : { probeExpiresAt: health.probeExpiresAt }),
147
- ...(health.lastCode ? { lastCode: health.lastCode.slice(0, 128) } : {}),
148
- };
149
- this.#open()
150
- .prepare(
151
- `INSERT INTO route_health(
152
- route_fingerprint, state, next_half_open_at, projection_json
153
- ) VALUES (?, ?, ?, ?)
154
- ON CONFLICT(route_fingerprint) DO UPDATE SET
155
- state = excluded.state,
156
- next_half_open_at = excluded.next_half_open_at,
157
- projection_json = excluded.projection_json`,
158
- )
159
- .run(
160
- fingerprint,
161
- health.state,
162
- health.retryAt ?? null,
163
- JSON.stringify(projection),
164
- );
165
- }
166
-
167
- close(): void {
168
- if (this.#closed) return;
169
- this.#database?.close();
170
- this.#closed = true;
171
- }
172
- }
173
-
174
49
  function ensurePrivateRunDirectory(directory: string): void {
175
50
  const existing = lstatSync(directory, { throwIfNoEntry: false });
176
51
  if (existing) {
@@ -196,19 +71,10 @@ class DurableWorkflowComposition
196
71
  readonly #phase: PhaseExecution;
197
72
  readonly #verification: ChangeVerification;
198
73
  readonly #options: DurableWorkflowEngineOptions;
199
- readonly #routeHealth: DurableRouteHealthStore;
200
- #broker: RunWorkerBroker;
201
74
  readonly #runs = new Map<string, DurableRunResources>();
202
75
 
203
76
  constructor(options: DurableWorkflowEngineOptions) {
204
77
  this.#options = options;
205
- this.#routeHealth = new DurableRouteHealthStore(
206
- options.stateRoot.databasePath,
207
- );
208
- this.#broker = new RunWorkerBroker(options.routePolicy, {
209
- ...(options.now ? { now: options.now } : {}),
210
- healthStore: this.#routeHealth,
211
- });
212
78
  this.#verification = new ChangeVerification(options, {
213
79
  prepareResources: (input, signal) =>
214
80
  this.#prepareResources(input, signal),
@@ -217,7 +83,7 @@ class DurableWorkflowComposition
217
83
  revalidatePhasePolicy: (...args) =>
218
84
  this.#phase.revalidatePhasePolicy(...args),
219
85
  });
220
- this.#phase = new PhaseExecution(options, this.#broker, {
86
+ this.#phase = new PhaseExecution(options, {
221
87
  prepareResources: (input, signal) =>
222
88
  this.#prepareResources(input, signal),
223
89
  ledger: (resources, revision) => this.#ledger(resources, revision),
@@ -229,14 +95,6 @@ class DurableWorkflowComposition
229
95
  });
230
96
  }
231
97
 
232
- updateRoutePolicy(policy: RoutePolicy): void {
233
- this.#broker.updatePolicy(policy);
234
- }
235
-
236
- routePolicyStatus(): Record<string, unknown> {
237
- return this.#broker.status();
238
- }
239
-
240
98
  #runRoot(runId: string): string {
241
99
  if (!IDENTIFIER.test(runId)) throw new Error("workflow-run-id-invalid");
242
100
  const dataRoot = path.join(this.#options.stateRoot.rootDir, "run-data");
@@ -770,21 +628,8 @@ class DurableWorkflowComposition
770
628
  return revision.entries[input.path]?.kind === "file";
771
629
  }
772
630
 
773
- rebind(input: Parameters<WorkflowWorker["rebind"]>[0]) {
774
- const rebound = this.#broker.rebind({
775
- runId: input.runId,
776
- taskId: input.taskId,
777
- role: input.role,
778
- routeId: input.routeId,
779
- requirements: implementationRouteRequirements({ task: input.task }),
780
- });
781
- return rebound.ok
782
- ? {
783
- ok: true as const,
784
- routeId: rebound.route.id,
785
- routeFingerprint: rebound.route.fingerprint,
786
- }
787
- : { ok: false as const, code: rebound.code };
631
+ rebind(_input: Parameters<WorkflowWorker["rebind"]>[0]) {
632
+ return { ok: false as const, code: "route-configuration-removed" };
788
633
  }
789
634
 
790
635
  async prepare(
@@ -912,7 +757,6 @@ class DurableWorkflowComposition
912
757
  this.#closeResources(resources);
913
758
  }
914
759
  this.#runs.clear();
915
- this.#routeHealth.close();
916
760
  }
917
761
  runAttempt(input: Parameters<WorkflowWorker["runAttempt"]>[0]) {
918
762
  return this.#phase.runAttempt(input);
@@ -21,7 +21,7 @@ export function evidenceDraftSchema(requestId: string, role: string) {
21
21
  id: Type.Optional(
22
22
  Type.Literal(requestId, {
23
23
  description:
24
- "Omit: bound by the submit tool. If supplied, must match this request.",
24
+ "Omit: bound by the parent result adapter. If supplied, must match this request.",
25
25
  }),
26
26
  ),
27
27
  role: Type.Optional(Type.Literal(role)),
package/src/index.ts CHANGED
@@ -59,7 +59,7 @@ import { canonicalJson } from "./implement-graph.ts";
59
59
  import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
60
60
 
61
61
  import { RunStoreFormatError, RunStoreMigrationError } from "./run-store.ts";
62
- import { STARTUP_CARD_KEY, startupCardLines } from "./startup-card.ts";
62
+ import { startupCardLines } from "./startup-card.ts";
63
63
  import { StateRootError } from "./state-root.ts";
64
64
  import {
65
65
  ACTIVITY_DETAILS_KEY,
@@ -1792,23 +1792,19 @@ export function registerWorkflowControl(
1792
1792
  }
1793
1793
 
1794
1794
  export default function register(pi: ExtensionAPI): void {
1795
+ // Process children are deliberately started without extensions as a second
1796
+ // defense. Keep this guard for hosts that inject the extension explicitly.
1797
+ if (process.env.CADENCE_SUBAGENT_CHILD === "1") return;
1795
1798
  registerWorkflowControl(pi);
1796
- // Presentation only: never persist a dismissal or inject a model message.
1799
+ // Presentation only: use the host's subdued one-shot notification rather
1800
+ // than a persistent widget; never persist a dismissal or inject a model message.
1797
1801
  pi.on("session_start", (_event, ctx) => {
1798
1802
  if (!ctx.hasUI) return;
1799
- ctx.ui.setWidget(
1800
- STARTUP_CARD_KEY,
1801
- startupCardLines({
1802
- cwd: ctx.cwd,
1803
- home: homedir(),
1804
- projectTrusted: ctx.isProjectTrusted(),
1805
- }),
1806
- );
1807
- });
1808
- pi.on("agent_start", (_event, ctx) => {
1809
- if (ctx.hasUI) ctx.ui.setWidget(STARTUP_CARD_KEY, undefined);
1810
- });
1811
- pi.on("session_shutdown", (_event, ctx) => {
1812
- if (ctx.hasUI) ctx.ui.setWidget(STARTUP_CARD_KEY, undefined);
1803
+ const lines = startupCardLines({
1804
+ cwd: ctx.cwd,
1805
+ home: homedir(),
1806
+ projectTrusted: ctx.isProjectTrusted(),
1807
+ });
1808
+ ctx.ui.notify(lines.at(-1) ?? "Cadence研究配置已加载。", "info");
1813
1809
  });
1814
1810
  }
@@ -0,0 +1,206 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import type { VerificationFailureSummary } from "./contracts.ts";
6
+ import { isValidRelativePath } from "./contracts.ts";
7
+ import type { DurableWorkflowEngineOptions } from "./durable-contracts.ts";
8
+ import type { ParentModelSource } from "./model-source.ts";
9
+ import { isSafeRegularFile } from "./safe-path.ts";
10
+ import {
11
+ buildSubagentPrompt,
12
+ runSubagentProcess,
13
+ SUBAGENT_LIMITS,
14
+ } from "./subagent-process.ts";
15
+ import type { DurableCandidateProposal } from "./workflow-policy.ts";
16
+
17
+ function permitsContextRead(
18
+ relative: string,
19
+ roots: readonly string[],
20
+ ): boolean {
21
+ return (
22
+ isValidRelativePath(relative) &&
23
+ relative.split("/").every((part) => !part.startsWith(".")) &&
24
+ !/\.(?:pem|key|p12|pfx)$/iu.test(relative) &&
25
+ roots.some((root) => relative === root || relative.startsWith(`${root}/`))
26
+ );
27
+ }
28
+
29
+ function restoreWorkspace(root: string, baseline: string): void {
30
+ rmSync(root, { recursive: true, force: true });
31
+ mkdirSync(root, { recursive: true });
32
+ cpSync(baseline, root, { recursive: true, force: true, dereference: false });
33
+ }
34
+
35
+ /** Produce a Git patch from two disposable trees without invoking a shell. */
36
+ function workspaceDiff(baseline: string, proposal: string): Uint8Array {
37
+ const parent = path.dirname(baseline);
38
+ const proposalSnapshot = mkdtempSync(
39
+ path.join(parent, "cadence-subagent-proposal-"),
40
+ );
41
+ cpSync(proposal, proposalSnapshot, {
42
+ recursive: true,
43
+ force: true,
44
+ dereference: false,
45
+ });
46
+ try {
47
+ const baseName = path.basename(baseline);
48
+ const proposalName = path.basename(proposalSnapshot);
49
+ try {
50
+ execFileSync(
51
+ "git",
52
+ [
53
+ "-c",
54
+ "core.autocrlf=false",
55
+ "-c",
56
+ "core.eol=lf",
57
+ "diff",
58
+ "--no-index",
59
+ "--binary",
60
+ "--no-color",
61
+ "--",
62
+ baseName,
63
+ proposalName,
64
+ ],
65
+ {
66
+ cwd: parent,
67
+ encoding: "buffer",
68
+ maxBuffer: 8 * 1024 * 1024,
69
+ stdio: ["ignore", "pipe", "ignore"],
70
+ },
71
+ );
72
+ return Buffer.alloc(0);
73
+ } catch (error) {
74
+ if (!error || typeof error !== "object") return Buffer.alloc(0);
75
+ const output = (error as { stdout?: Buffer }).stdout;
76
+ if (!output) return Buffer.alloc(0);
77
+ const pathPrefixes = [
78
+ `a/${baseName}/`,
79
+ `a/${proposalName}/`,
80
+ `b/${baseName}/`,
81
+ `b/${proposalName}/`,
82
+ ];
83
+ const normalizeHeader = (line: string): string => {
84
+ let normalized = line;
85
+ for (const prefix of pathPrefixes) {
86
+ const replacement = prefix.startsWith("a/") ? "a/" : "b/";
87
+ normalized = normalized.replaceAll(prefix, replacement);
88
+ }
89
+ return normalized;
90
+ };
91
+ const normalized = output
92
+ .toString("utf8")
93
+ .split("\n")
94
+ .map((line) =>
95
+ line.startsWith("diff --git ") ||
96
+ line.startsWith("--- ") ||
97
+ line.startsWith("+++ ")
98
+ ? normalizeHeader(line)
99
+ : line,
100
+ )
101
+ .join("\n");
102
+ return Buffer.from(normalized, "utf8");
103
+ }
104
+ } finally {
105
+ rmSync(proposalSnapshot, { recursive: true, force: true });
106
+ }
107
+ }
108
+
109
+ export async function proposeProcessPackageCandidate(
110
+ input: Parameters<DurableWorkflowEngineOptions["proposeCandidate"]>[0],
111
+ context: ParentModelSource | undefined,
112
+ implementationAgent: { content: string },
113
+ verificationDiagnostics: VerificationFailureSummary[] = [],
114
+ ): Promise<DurableCandidateProposal> {
115
+ if (!context) return { kind: "paused", code: "parent-context-unavailable" };
116
+ if (input.signal.aborted)
117
+ return { kind: "operation-cancelled", code: "cancelled" };
118
+ const phase = input.task.phases[input.phase];
119
+ if (!phase) return { kind: "paused", code: "task-phase-unavailable" };
120
+ const taskPhases = Object.values(input.task.phases);
121
+ const read = [
122
+ ...new Set(
123
+ taskPhases.flatMap((boundary) => [
124
+ ...boundary.read,
125
+ ...boundary.write,
126
+ ...boundary.delete,
127
+ ]),
128
+ ),
129
+ ].sort();
130
+ const write = input.artifactCorrection
131
+ ? [...new Set(taskPhases.flatMap((boundary) => boundary.write))].sort()
132
+ : [...phase.write];
133
+ const deletePaths = input.artifactCorrection
134
+ ? [...new Set(taskPhases.flatMap((boundary) => boundary.delete))].sort()
135
+ : [...phase.delete];
136
+ const allowedContext = (input.contextReadPaths ?? []).filter(
137
+ (relative) =>
138
+ permitsContextRead(relative, input.task.roots) &&
139
+ isSafeRegularFile(input.workspaceRoot, relative),
140
+ );
141
+ const allowedPaths = [...new Set([...read, ...allowedContext])].sort();
142
+ const baseline = mkdtempSync(
143
+ path.join(os.tmpdir(), "cadence-subagent-baseline-"),
144
+ );
145
+ try {
146
+ cpSync(input.workspaceRoot, baseline, {
147
+ recursive: true,
148
+ force: true,
149
+ dereference: false,
150
+ });
151
+ const prompt = buildSubagentPrompt({
152
+ role: "implementation-worker",
153
+ agentContent: implementationAgent.content,
154
+ objective: input.task.objective,
155
+ context: [
156
+ input.task.context.agents,
157
+ input.task.context.contract,
158
+ JSON.stringify({
159
+ taskId: input.taskId,
160
+ phase: input.phase,
161
+ readSet: allowedPaths,
162
+ writeSet: write,
163
+ deleteSet: deletePaths,
164
+ verificationDiagnostics,
165
+ recoveryFeedback: input.recoveryFeedback,
166
+ }),
167
+ ].join("\n\n"),
168
+ finalConvention:
169
+ "Edit the disposable workspace directly. Finish with a plain-text summary; the parent will derive and validate the candidate diff.",
170
+ });
171
+ const child = await runSubagentProcess({
172
+ role: "implementation-worker",
173
+ cwd: input.workspaceRoot,
174
+ prompt,
175
+ model: context.model
176
+ ? { provider: context.model.provider, id: context.model.id }
177
+ : undefined,
178
+ signal: input.signal,
179
+ timeoutMs: SUBAGENT_LIMITS.timeoutMs,
180
+ onEvent: (event) => {
181
+ if (event.type === "agent_start") input.onRequestStart?.();
182
+ if (event.type === "tool_execution_start") input.onProgress?.();
183
+ },
184
+ });
185
+ if (child.status === "cancelled")
186
+ return { kind: "operation-cancelled", code: "cancelled" };
187
+ if (child.status === "timed-out")
188
+ return { kind: "paused", code: "child-timeout" };
189
+ if (child.status === "output-limit")
190
+ return { kind: "retryable", code: "needs-task-split" };
191
+ if (child.status !== "completed")
192
+ return { kind: "retryable", code: "transport-failure" };
193
+ const bytes = workspaceDiff(baseline, input.workspaceRoot);
194
+ // Existing parent code applies the candidate patch to this root. Restore the
195
+ // baseline first so child edits cannot be applied twice.
196
+ restoreWorkspace(input.workspaceRoot, baseline);
197
+ if (bytes.length === 0)
198
+ return { kind: "retryable", code: "candidate-diff-invalid" };
199
+ return { kind: "candidate", bytes };
200
+ } finally {
201
+ // A child is never allowed to leave edits in the proposal root: the
202
+ // parent-side apply/verification path consumes the generated bytes.
203
+ if (existsSync(baseline)) restoreWorkspace(input.workspaceRoot, baseline);
204
+ rmSync(baseline, { recursive: true, force: true });
205
+ }
206
+ }