forgeos 0.1.0-alpha.71 → 0.1.0-alpha.72
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/AGENTS.md +1 -1
- package/CHANGELOG.md +6 -0
- package/docs/agent-fabric-programs.md +34 -2
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_EVIDENCE.md +75 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_PLAN.md +137 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_REVIEW.md +56 -0
- package/examples/agent-fabric-v2/templates/README.md +46 -0
- package/examples/agent-fabric-v2/templates/bugfix.json +12 -0
- package/examples/agent-fabric-v2/templates/migration.json +13 -0
- package/examples/agent-fabric-v2/templates/review.json +12 -0
- package/package.json +5 -1
- package/scripts/summarize-fabric-benchmark.mjs +6 -0
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-fabric/claude-program-worker.ts +95 -0
- package/src/forge/agent-fabric/codex-sdk-worker.ts +10 -4
- package/src/forge/agent-fabric/index.ts +5 -0
- package/src/forge/agent-fabric/local-task-server.ts +2 -1
- package/src/forge/agent-fabric/managed-environment.ts +14 -8
- package/src/forge/agent-fabric/managed-workspace.ts +4 -0
- package/src/forge/agent-fabric/program-api.ts +1 -1
- package/src/forge/agent-fabric/program-authoring.ts +48 -0
- package/src/forge/agent-fabric/program-benchmark.ts +76 -0
- package/src/forge/agent-fabric/program-capabilities.ts +14 -0
- package/src/forge/agent-fabric/program-contract.ts +19 -3
- package/src/forge/agent-fabric/program-observation.ts +63 -0
- package/src/forge/agent-fabric/program-runtime-options.ts +23 -0
- package/src/forge/agent-fabric/program-scheduler.ts +25 -6
- package/src/forge/agent-fabric/program-service.ts +172 -35
- package/src/forge/agent-fabric/program-store.ts +9 -1
- package/src/forge/agent-fabric/program-templates.ts +96 -0
- package/src/forge/agent-fabric/program-view.ts +15 -1
- package/src/forge/agent-fabric/program-worker.ts +73 -20
- package/src/forge/agent-memory/mcp.ts +8 -2
- package/src/forge/cli/fabric.ts +4 -2
- package/src/forge/cli/main.ts +2 -0
- package/src/forge/cli/parse.ts +3 -1
- package/src/forge/version.ts +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Codex, type CodexOptions, type ThreadOptions, type TurnOptions, type ThreadEvent } from "@openai/codex-sdk";
|
|
1
|
+
import { Codex, type CodexOptions, type ThreadOptions, type TurnOptions, type ThreadEvent, type Input } from "@openai/codex-sdk";
|
|
2
2
|
import { realpath, stat, readFile } from "node:fs/promises";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { homedir } from "node:os";
|
|
@@ -13,6 +13,7 @@ export interface CodexWorkerEvent { type: string; threadId?: string; summary?: s
|
|
|
13
13
|
export interface CodexWorkerInput {
|
|
14
14
|
cwd: string; prompt: string; role: "implementer" | "reviewer" | "investigator" | "decision";
|
|
15
15
|
model?: string; threadId?: string; signal: AbortSignal;
|
|
16
|
+
images?: { path: string }[];
|
|
16
17
|
onEvent: (event: CodexWorkerEvent) => Promise<void> | void;
|
|
17
18
|
}
|
|
18
19
|
export interface CodexWorkerOutput {
|
|
@@ -22,7 +23,7 @@ export interface CodexWorkerOutput {
|
|
|
22
23
|
}
|
|
23
24
|
export interface CodexWorkerThread {
|
|
24
25
|
readonly id: string | null;
|
|
25
|
-
runStreamed(prompt:
|
|
26
|
+
runStreamed(prompt: Input, options: TurnOptions): Promise<{ events: AsyncIterable<ThreadEvent> }>;
|
|
26
27
|
}
|
|
27
28
|
export interface CodexWorkerClient {
|
|
28
29
|
startThread(options: ThreadOptions): CodexWorkerThread;
|
|
@@ -183,14 +184,19 @@ async function runTypedCodexWorkerInternal(input: CodexWorkerInput & { outputSch
|
|
|
183
184
|
const threadOptions: ThreadOptions = { workingDirectory: cwd, sandboxMode: input.role === "implementer" ? "workspace-write" : "read-only", networkAccessEnabled: false, webSearchMode: "disabled", approvalPolicy: "never", ...(input.model ? { model: input.model } : {}) };
|
|
184
185
|
const thread = input.threadId ? client.resumeThread(input.threadId, threadOptions) : client.startThread(threadOptions);
|
|
185
186
|
let threadId = input.threadId ?? thread.id ?? "", finalMessage: string | undefined, completed = false, count = 0, observedUsage: CodexWorkerUsage | undefined;
|
|
186
|
-
const
|
|
187
|
+
const prompt = `${input.prompt}\nReturn only data matching the requested output schema. Never include credentials.`;
|
|
188
|
+
if (input.images && (input.images.length > 100 || input.images.some(image => !text(image.path, 4096)))) fail("AF_CODEX_INPUT", "Invalid image inputs");
|
|
189
|
+
const content: Input = input.images?.length ? [{ type: "text", text: prompt }, ...input.images.map(image => ({ type: "local_image" as const, path: image.path }))] : prompt;
|
|
190
|
+
const stream = await thread.runStreamed(content, { outputSchema: input.outputSchema, signal: input.signal });
|
|
191
|
+
if (input.images?.length) await input.onEvent({ type: "input.images.delivered", summary: `${input.images.length} images supplied through SDK local_image; understanding is not attested` });
|
|
187
192
|
for await (const event of stream.events) {
|
|
188
193
|
checkAbort(input.signal); if (++count > 10000) fail("AF_CODEX_EVENT_LIMIT", "Typed worker event budget exceeded");
|
|
189
194
|
if (!["thread.started", "turn.started", "item.completed", "item.started", "item.updated", "turn.completed", "turn.failed", "error"].includes(event.type)) fail("AF_CODEX_EVENT", "Unknown typed event");
|
|
190
195
|
if (event.type === "thread.started") { if (!text(event.thread_id, 128) || !/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/u.test(event.thread_id) || threadId && threadId !== event.thread_id) fail("AF_CODEX_EVENT", "Thread identity changed"); threadId = event.thread_id; await input.onEvent({ type: event.type, threadId }); }
|
|
191
196
|
else if (event.type === "item.completed" && event.item.type === "agent_message") { if (completed || !text(event.item.text, 4 * 1024 * 1024)) fail("AF_CODEX_REPORT", "Invalid typed message"); finalMessage = event.item.text; }
|
|
192
197
|
else if (event.type === "turn.completed") { if (completed) fail("AF_CODEX_EVENT", "Duplicate completion"); completed = true; observedUsage = usage(event.usage); await input.onEvent({ type: event.type, usage: observedUsage }); }
|
|
193
|
-
else if (event.type === "turn.failed" || event.type === "error") fail("AF_CODEX_TURN_FAILED", "Typed worker failed; reconcile before retry");
|
|
198
|
+
else if (event.type === "turn.failed" || event.type === "error") { await input.onEvent({ type: event.type, summary: "Typed worker failed; reconcile before retry" }); fail("AF_CODEX_TURN_FAILED", "Typed worker failed; reconcile before retry"); }
|
|
199
|
+
else await input.onEvent({ type: event.type, summary: "Typed SDK activity observed" });
|
|
194
200
|
}
|
|
195
201
|
checkAbort(input.signal); if (!completed || !threadId || !finalMessage) fail("AF_CODEX_INCOMPLETE", "Typed worker completion not observed");
|
|
196
202
|
let data: unknown; try { data = JSON.parse(finalMessage); input.validateOutput(data); } catch { fail("AF_CODEX_REPORT", "Typed worker output schema invalid"); }
|
|
@@ -44,5 +44,10 @@ export { lowerWorkflowSource, PROGRAM_DSL_VERSION } from "./program-dsl.ts";
|
|
|
44
44
|
export { ProgramRunService, PROGRAM_RUN_ACTIONS } from "./program-service.ts";
|
|
45
45
|
export type { ProgramRunAction } from "./program-service.ts";
|
|
46
46
|
export { ProgramRunStore } from "./program-store.ts";
|
|
47
|
+
export * from "./program-authoring.ts";
|
|
48
|
+
export * from "./program-templates.ts";
|
|
49
|
+
export * from "./program-runtime-options.ts";
|
|
50
|
+
export * from "./program-benchmark.ts";
|
|
51
|
+
export * from "./program-observation.ts";
|
|
47
52
|
export { validateProgramCapabilities } from "./program-worker.ts";
|
|
48
53
|
export type { ProgramWorkerAdapter, ProgramWorkerInput, ProgramWorkerResult } from "./program-worker.ts";
|
|
@@ -14,6 +14,7 @@ import { localFabricPath } from "./local-paths.ts";
|
|
|
14
14
|
import { applyFabricProfile } from "./project-profile.ts";
|
|
15
15
|
import { ProgramRunService, PROGRAM_RUN_ACTIONS, type ProgramRunAction } from "./program-service.ts";
|
|
16
16
|
import { ProgramError } from "./program-contract.ts";
|
|
17
|
+
import { readProgramRuntimeOptions } from "./program-runtime-options.ts";
|
|
17
18
|
|
|
18
19
|
const MAX_REQUEST_BYTES = 40 * 1024;
|
|
19
20
|
const ENDPOINT_FILENAME = "owner-endpoint.json";
|
|
@@ -200,7 +201,7 @@ export async function serveLocalTasks(
|
|
|
200
201
|
attachedService = await AttachedTaskService.open(root);
|
|
201
202
|
const { ManagedRunService: ManagedService } = await import("./managed-run-service.ts");
|
|
202
203
|
managedService = await ManagedService.open(root);
|
|
203
|
-
programService = await ProgramRunService.open(root);
|
|
204
|
+
programService = await ProgramRunService.open(root, undefined, undefined, readProgramRuntimeOptions(root));
|
|
204
205
|
const token = randomBytes(32).toString("hex");
|
|
205
206
|
listener = createServer((request, response: ServerResponse) => {
|
|
206
207
|
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
@@ -8,7 +8,8 @@ import { tmpdir } from "node:os";
|
|
|
8
8
|
import { stableStringify } from "./canonical.ts";
|
|
9
9
|
|
|
10
10
|
type Manager = "npm" | "pnpm" | "yarn" | "bun" | "none";
|
|
11
|
-
export interface ManagedEnvironmentOptions { mode?: "auto" | "none"; ignoreScripts?: boolean; registry?: string; timeoutMs?: number; cacheDirectory?: string; signal?: AbortSignal
|
|
11
|
+
export interface ManagedEnvironmentOptions { mode?: "auto" | "none"; ignoreScripts?: boolean; registry?: string; timeoutMs?: number; cacheDirectory?: string; signal?: AbortSignal;
|
|
12
|
+
onTiming?: (measurement: { phase: "cache.verify" | "cache.copy" | "dependencies.install"; elapsedMs: number }) => Promise<void> | void }
|
|
12
13
|
export interface ManagedEnvironment { schemaVersion: 1; directory: string; manager: Manager; lockDigest: string; dependencyDigest: string; derivedPaths: string[]; cacheHit: boolean }
|
|
13
14
|
const execute = promisify(execFile);
|
|
14
15
|
const hash = (value: string | Uint8Array) => `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
@@ -134,6 +135,11 @@ export async function verifyManagedEnvironment(environment: ManagedEnvironment):
|
|
|
134
135
|
assert(await dependencyDigest(environment.directory, environment.derivedPaths) === environment.dependencyDigest, "prepared dependencies were modified; result is invalid");
|
|
135
136
|
}
|
|
136
137
|
export async function prepareManagedEnvironment(directory: string, options: ManagedEnvironmentOptions = {}): Promise<ManagedEnvironment> {
|
|
138
|
+
const timed = async <T>(phase: "cache.verify" | "cache.copy" | "dependencies.install", operation: () => Promise<T>): Promise<T> => {
|
|
139
|
+
const start = performance.now();
|
|
140
|
+
try { return await operation(); }
|
|
141
|
+
finally { await options.onTiming?.({ phase, elapsedMs: performance.now() - start }); }
|
|
142
|
+
};
|
|
137
143
|
directory = resolve(directory); aborted(options.signal);
|
|
138
144
|
assert(!(await lstat(directory)).isSymbolicLink(), "workspace root cannot be a symlink");
|
|
139
145
|
const none = (): ManagedEnvironment => ({ schemaVersion: 1, directory, manager: "none", lockDigest: hash("none"), dependencyDigest: hash(stableStringify([])), derivedPaths: [], cacheHit: false });
|
|
@@ -160,9 +166,9 @@ export async function prepareManagedEnvironment(directory: string, options: Mana
|
|
|
160
166
|
const cache = join(cacheRoot, lockDigest.slice(7)), marker = join(cache, "environment.json");
|
|
161
167
|
if (await exists(marker)) {
|
|
162
168
|
const cached = JSON.parse(await readFile(marker, "utf8")) as ManagedEnvironment;
|
|
163
|
-
assert(cached.lockDigest === lockDigest && cached.dependencyDigest === await dependencyDigest(join(cache, "tree"), cached.derivedPaths, options.signal), "dependency cache integrity failed");
|
|
164
|
-
await copyDependencyPaths(join(cache, "tree"), directory, cached.derivedPaths, options.signal);
|
|
165
|
-
const environment = { ...cached, directory, cacheHit: true }; await verifyManagedEnvironment(environment); return environment;
|
|
169
|
+
await timed("cache.verify", async () => { assert(cached.lockDigest === lockDigest && cached.dependencyDigest === await dependencyDigest(join(cache, "tree"), cached.derivedPaths, options.signal), "dependency cache integrity failed"); });
|
|
170
|
+
await timed("cache.copy", () => copyDependencyPaths(join(cache, "tree"), directory, cached.derivedPaths, options.signal));
|
|
171
|
+
const environment = { ...cached, directory, cacheHit: true }; await timed("cache.verify", () => verifyManagedEnvironment(environment)); return environment;
|
|
166
172
|
}
|
|
167
173
|
const configBackups: { path: string; bytes?: Buffer }[] = [];
|
|
168
174
|
try {
|
|
@@ -178,7 +184,7 @@ export async function prepareManagedEnvironment(directory: string, options: Mana
|
|
|
178
184
|
: Number.parseInt(version) >= 2 ? ["install", "--immutable", ...(ignore ? ["--mode=skip-builds"] : [])] : ["install", "--frozen-lockfile", "--non-interactive", `--registry=${registry}`, ...(ignore ? ["--ignore-scripts"] : [])];
|
|
179
185
|
env.YARN_NODE_LINKER = "node-modules"; env.YARN_NM_MODE = "classic"; env.YARN_ENABLE_GLOBAL_CACHE = "false"; env.YARN_NPM_REGISTRY_SERVER = registry; env.YARN_ENABLE_SCRIPTS = ignore ? "false" : "true";
|
|
180
186
|
env.NPM_CONFIG_CACHE = join(cacheRoot, "download-cache"); env.BUN_INSTALL_CACHE_DIR = join(cacheRoot, "bun-download-cache"); env.COREPACK_HOME = join(cacheRoot, "corepack-cache");
|
|
181
|
-
await execute(command.executable, [...command.prefix, ...args], { cwd: directory, env, windowsHide: true, timeout, signal: options.signal, maxBuffer: 1024 * 1024 });
|
|
187
|
+
await timed("dependencies.install", () => execute(command.executable, [...command.prefix, ...args], { cwd: directory, env, windowsHide: true, timeout, signal: options.signal, maxBuffer: 1024 * 1024 }));
|
|
182
188
|
} catch (error) { if (options.signal?.aborted) throw new Error("managed environment: preparation canceled"); throw new Error(`managed environment: frozen ${manager} install failed: ${(error as Error).message.slice(0, 2000)}`); }
|
|
183
189
|
finally { for (const config of configBackups) await writeFile(config.path, config.bytes!); }
|
|
184
190
|
aborted(options.signal); const derivedPaths = await derivePaths(directory);
|
|
@@ -186,12 +192,12 @@ export async function prepareManagedEnvironment(directory: string, options: Mana
|
|
|
186
192
|
derivedPaths.sort(); await portableLinks(directory, derivedPaths); const dependency = await dependencyDigest(directory, derivedPaths, options.signal);
|
|
187
193
|
const environment: ManagedEnvironment = { schemaVersion: 1, directory, manager, lockDigest, dependencyDigest: dependency, derivedPaths, cacheHit: false };
|
|
188
194
|
stage = join(cacheRoot, `${lockDigest.slice(7)}-${randomUUID()}.stage`); await mkdir(join(stage, "tree"), { recursive: true });
|
|
189
|
-
await copyDependencyPaths(directory, join(stage
|
|
190
|
-
assert(await dependencyDigest(join(stage
|
|
195
|
+
await timed("cache.copy", () => copyDependencyPaths(directory, join(stage!, "tree"), derivedPaths, options.signal));
|
|
196
|
+
await timed("cache.verify", async () => { assert(await dependencyDigest(join(stage!, "tree"), derivedPaths, options.signal) === dependency, "cache copy integrity failed"); });
|
|
191
197
|
try {
|
|
192
198
|
await rename(stage, cache); uncommittedCache = cache;
|
|
193
199
|
await rebaseCacheJunctions(join(cache, "tree"), join(stage, "tree"), derivedPaths);
|
|
194
|
-
assert(await dependencyDigest(join(cache, "tree"), derivedPaths, options.signal) === dependency, "published cache integrity failed");
|
|
200
|
+
await timed("cache.verify", async () => { assert(await dependencyDigest(join(cache, "tree"), derivedPaths, options.signal) === dependency, "published cache integrity failed"); });
|
|
195
201
|
const markerTemp = join(cache, "environment.json.tmp");
|
|
196
202
|
await writeFile(markerTemp, stableStringify(environment)); await rename(markerTemp, join(cache, "environment.json")); uncommittedCache = undefined;
|
|
197
203
|
} catch (error) { if (uncommittedCache || !["EEXIST", "ENOTEMPTY", "EPERM"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error; }
|
|
@@ -192,6 +192,10 @@ export async function captureManagedArtifact(base: ManagedBase, workspaceDirecto
|
|
|
192
192
|
}
|
|
193
193
|
const artifact = { digest: artifactDigest(files), files }; validateArtifact(artifact, writeScope); return artifact;
|
|
194
194
|
}
|
|
195
|
+
/** A bounded observation of the complete managed source delta, never a termination attestation. */
|
|
196
|
+
export async function observeManagedWorkspace(base: ManagedBase, directory: string, inputDigest: string, environment?: ManagedEnvironment, generatedPaths: string[] = []): Promise<string> {
|
|
197
|
+
return (await captureManagedArtifact(base, directory, base.scope, inputDigest, environment, generatedPaths)).digest;
|
|
198
|
+
}
|
|
195
199
|
export async function publishManagedArtifacts(base: ManagedBase, artifacts: ManagedArtifact[], controls?: { beforeWrite?: () => Promise<void> }): Promise<{ digest: string; changedFiles: string[] }> {
|
|
196
200
|
await validateBase(base); const before = await inventory(base.root, base.scope);
|
|
197
201
|
assert(before.digest === base.digest && await currentContextMatches(base) && await git(base.root, ["rev-parse", "HEAD"]) === base.head, "source changed since capture (including readonly environment context)");
|
|
@@ -26,7 +26,7 @@ export interface WorkflowOptions {
|
|
|
26
26
|
branch: Common & { condition: boolean | ProgramExpr; then: Body; else: Body };
|
|
27
27
|
loop: Common & { initialState: Data; maxRounds: number; body: Body; next: Data; until: boolean | ProgramExpr };
|
|
28
28
|
repair: Common & { recipe: WorkflowRef<"recipe">; implement: WorkflowRef<"executor">; review: WorkflowRef<"executor">; checks: Data; input?: Data; evidence?: WorkflowRef<"executor">[]; assessmentScope?: "item" | "final"; entryMode: "implement-first" | "assess-first"; initialCandidate: Data; writeScope: Data; maxRepairRounds: number; maxAssessmentAttempts: number; maxInfrastructureAttempts: number; progressPolicy: { unchangedCandidateRounds: number; repeatedFindingsRounds: number } };
|
|
29
|
-
compose: Common & { candidates: Data; onConflict?: "needs-resolution" };
|
|
29
|
+
compose: Common & { candidates: Data; onConflict?: "needs-resolution"; resolver?: WorkflowRef<"executor">; resolverInput?: Data; resolverWriteScope?: Data };
|
|
30
30
|
gate: Common & { candidate: Data; coverage?: Data; authorization?: Data };
|
|
31
31
|
subworkflow: Common & { program: WorkflowRef<"program">; input: Data };
|
|
32
32
|
waitEvent: Common & { type: string; correlation: Data; schema: WorkflowRef<"schema">; subject?: Data; timeoutMs?: number };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { programDigest, programValueBytes, programWithin, refKey, registryEntry, validateWorkflowProgram, type ProgramRegistry, type ProgramSchema, type WorkflowProgramV2 } from "./program-contract.ts";
|
|
2
|
+
import { programGraph } from "./program-structure.ts";
|
|
3
|
+
import { buildWorkflowTemplate, type ProgramTemplateKind, type ProgramTemplateOptions } from "./program-templates.ts";
|
|
4
|
+
|
|
5
|
+
export interface ProgramAuthorDiagnostic { code: string; message: string; operationId?: string }
|
|
6
|
+
export interface ProgramAuthorProposal {
|
|
7
|
+
valid: boolean; program?: WorkflowProgramV2; digest?: string; diagnostics: ProgramAuthorDiagnostic[];
|
|
8
|
+
summary?: { criteria: string[]; requiredChecks: string[]; writeScope: string[]; humanApproval: boolean; operations: { id: string; kind: string; dependencies: string[] }[] };
|
|
9
|
+
}
|
|
10
|
+
export function validateProgramProposal(program: WorkflowProgramV2, registry: ProgramRegistry): ProgramAuthorProposal {
|
|
11
|
+
try {
|
|
12
|
+
validateWorkflowProgram(program, registry);
|
|
13
|
+
const acceptance = registryEntry(registry.acceptance, program.acceptance);
|
|
14
|
+
const policy = registryEntry(registry.policies, program.policy);
|
|
15
|
+
return { valid: true, program: structuredClone(program), digest: programDigest(program), diagnostics: [], summary: {
|
|
16
|
+
criteria: [...acceptance.criteria], requiredChecks: [...new Set([...acceptance.requiredChecks, ...(acceptance.requiredChecksByScope?.item ?? []), ...(acceptance.requiredChecksByScope?.final ?? [])])],
|
|
17
|
+
writeScope: [...new Set([...acceptance.writeScope, ...policy.writeScope])].filter(path => programWithin(path, acceptance.writeScope) && programWithin(path, policy.writeScope)), humanApproval: acceptance.requireHumanApproval === true,
|
|
18
|
+
operations: programGraph(program).map(entry => ({ id: entry.id, kind: entry.step.kind, dependencies: entry.dependencies })),
|
|
19
|
+
} };
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return { valid: false, diagnostics: [{ code: (error as { code?: string }).code ?? "AF_PROGRAM_INVALID", message: error instanceof Error ? error.message : String(error) }] };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function proposeWorkflowTemplate(kind: ProgramTemplateKind, options: ProgramTemplateOptions, registry: ProgramRegistry): ProgramAuthorProposal {
|
|
25
|
+
try { programValueBytes(options, 40000, 128); return validateProgramProposal(buildWorkflowTemplate(kind, options, registry), registry); }
|
|
26
|
+
catch (error) { return { valid: false, diagnostics: [{ code: "AF_PROGRAM_TEMPLATE_INVALID", message: error instanceof Error ? error.message : String(error) }] }; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Generate authoring declarations from the owner's schemas, without evaluating authored code. */
|
|
30
|
+
export function generateRegistryAuthorTypes(registry: ProgramRegistry): string {
|
|
31
|
+
programValueBytes(registry, 4 * 1024 * 1024);
|
|
32
|
+
const shape = (schema: ProgramSchema, depth = 0): string => {
|
|
33
|
+
if (depth > 32) throw new Error("Schema depth exceeded");
|
|
34
|
+
if (Object.hasOwn(schema, "const")) return JSON.stringify(schema.const);
|
|
35
|
+
if (schema.enum?.length) return schema.enum.map(value => JSON.stringify(value)).join(" | ");
|
|
36
|
+
if (schema.anyOf?.length) return schema.anyOf.map(value => shape(value, depth + 1)).join(" | ");
|
|
37
|
+
const types = schema.type ? Array.isArray(schema.type) ? schema.type : [schema.type] : [];
|
|
38
|
+
if (!types.length) return "unknown";
|
|
39
|
+
return types.map(type => {
|
|
40
|
+
if (type === "object") return `{ ${[...Object.entries(schema.properties ?? {}).map(([key, value]) => `${JSON.stringify(key)}${schema.required?.includes(key) ? "" : "?"}: ${shape(value, depth + 1)}`), ...(schema.additionalProperties === false ? [] : ["[key: string]: unknown"])].join("; ")} }`;
|
|
41
|
+
if (type === "array") return `Array<${shape(schema.items ?? {}, depth + 1)}>`;
|
|
42
|
+
return type === "integer" ? "number" : type;
|
|
43
|
+
}).join(" | ");
|
|
44
|
+
};
|
|
45
|
+
const schemas = Object.entries(registry.schemas).sort(([a], [b]) => a.localeCompare(b));
|
|
46
|
+
const refs = (entries: Record<string, unknown>) => Object.keys(entries).sort().map(key => JSON.stringify(key)).join(" | ") || "never";
|
|
47
|
+
return `// Generated owner registry authoring types; runtime owner validation remains authoritative.\nexport interface OwnerSchemas {\n${schemas.map(([key, value]) => ` ${JSON.stringify(key)}: ${shape(value)};`).join("\n")}\n}\nexport interface OwnerExecutors {\n${Object.entries(registry.executors).sort(([a], [b]) => a.localeCompare(b)).map(([key, executor]) => ` ${JSON.stringify(key)}: { input: ${executor.inputSchema ? `OwnerSchemas[${JSON.stringify(refKey(executor.inputSchema))}]` : "unknown"}; output: OwnerSchemas[${JSON.stringify(refKey(executor.schema))}] };`).join("\n")}\n}\nexport type OwnerPolicyKey = ${refs(registry.policies)};\nexport type OwnerAcceptanceKey = ${refs(registry.acceptance)};\n`;
|
|
48
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { programAssert, programDigest, programValueBytes } from "./program-contract.ts";
|
|
2
|
+
|
|
3
|
+
export interface ProgramBenchmarkObservation {
|
|
4
|
+
taskId: string; runtimeVersion: string; model: string; toolsDigest: string; contextDigest: string; contractDigest: string;
|
|
5
|
+
category: "runtime" | "coding"; budget: string;
|
|
6
|
+
timingMs: Partial<Record<"queue" | "preparation" | "execution" | "capture" | "persistence" | "integration", number>>;
|
|
7
|
+
inputTokens?: number; outputTokens?: number; usageStatus: "observed" | "estimated" | "unknown";
|
|
8
|
+
externalEvaluation?: { assessor: string; criteriaDigest: string; independent: boolean; success: boolean; escapedDefects: number; unnecessaryChanges: number };
|
|
9
|
+
humanInterventions: number; repeatedActivities: number; retainedBytes?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ProgramBenchmarkReport { schemaVersion: 1; benchmarkId: string; observations: ProgramBenchmarkObservation[]; limitations: string[] }
|
|
12
|
+
|
|
13
|
+
/** Data validation only: producing a report never executes a model or workflow. */
|
|
14
|
+
export function validateBenchmarkReport(report: ProgramBenchmarkReport): void {
|
|
15
|
+
programValueBytes(report, 4 * 1024 * 1024);
|
|
16
|
+
programAssert(report?.schemaVersion === 1 && typeof report.benchmarkId === "string" && report.benchmarkId.length > 0, "Benchmark identity required");
|
|
17
|
+
programAssert(Array.isArray(report.observations) && report.observations.length > 0 && Array.isArray(report.limitations) && report.limitations.every(value => typeof value === "string"), "Benchmark observations and limitations required");
|
|
18
|
+
const identities = new Set<string>();
|
|
19
|
+
const count = (value: unknown) => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
20
|
+
for (const entry of report.observations) {
|
|
21
|
+
programAssert(entry && typeof entry === "object", "Benchmark observation required");
|
|
22
|
+
for (const key of ["taskId", "runtimeVersion", "model", "toolsDigest", "contextDigest", "contractDigest", "budget"] as const) programAssert(typeof entry[key] === "string" && entry[key].length > 0, `Benchmark ${key} required`);
|
|
23
|
+
programAssert(!identities.has(entry.taskId), "Duplicate benchmark task identity"); identities.add(entry.taskId);
|
|
24
|
+
programAssert(["runtime", "coding"].includes(entry.category) && ["observed", "estimated", "unknown"].includes(entry.usageStatus), "Invalid benchmark category or usage status");
|
|
25
|
+
programAssert(entry.timingMs && typeof entry.timingMs === "object" && !Array.isArray(entry.timingMs), "Timing observations required");
|
|
26
|
+
for (const [key, value] of Object.entries(entry.timingMs)) programAssert(["queue", "preparation", "execution", "capture", "persistence", "integration"].includes(key) && typeof value === "number" && Number.isFinite(value) && value >= 0, "Invalid timing observation");
|
|
27
|
+
for (const value of [entry.humanInterventions, entry.repeatedActivities]) programAssert(count(value), "Invalid benchmark counter");
|
|
28
|
+
for (const value of [entry.inputTokens, entry.outputTokens, entry.retainedBytes]) if (value !== undefined) programAssert(count(value), "Invalid optional benchmark counter");
|
|
29
|
+
if (entry.usageStatus === "unknown") programAssert(entry.inputTokens === undefined && entry.outputTokens === undefined, "Unknown consumption cannot be represented as measured tokens");
|
|
30
|
+
else programAssert(entry.inputTokens !== undefined && entry.outputTokens !== undefined, "Observed or estimated consumption requires both token counters");
|
|
31
|
+
if (entry.externalEvaluation) {
|
|
32
|
+
const assessment = entry.externalEvaluation;
|
|
33
|
+
programAssert(typeof assessment.assessor === "string" && assessment.assessor.length > 0 && typeof assessment.criteriaDigest === "string" && assessment.criteriaDigest.length > 0, "External assessment provenance required");
|
|
34
|
+
programAssert(typeof assessment.independent === "boolean" && typeof assessment.success === "boolean" && count(assessment.escapedDefects) && count(assessment.unnecessaryChanges), "Invalid external assessment");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Pair by task identity, never by array position; unmatched/confounded trials fail eligibility. */
|
|
39
|
+
export function compareBenchmarkReports(left: ProgramBenchmarkReport, right: ProgramBenchmarkReport) {
|
|
40
|
+
validateBenchmarkReport(left); validateBenchmarkReport(right);
|
|
41
|
+
programAssert(left.benchmarkId !== right.benchmarkId, "Paired reports require distinct benchmark identities");
|
|
42
|
+
const leftById = new Map(left.observations.map(entry => [entry.taskId, entry]));
|
|
43
|
+
const rightById = new Map(right.observations.map(entry => [entry.taskId, entry]));
|
|
44
|
+
const differences: { taskId: string; fields: string[] }[] = [];
|
|
45
|
+
const unmatchedTasks = [...new Set([...leftById.keys(), ...rightById.keys()])].filter(id => !leftById.has(id) || !rightById.has(id)).sort();
|
|
46
|
+
const pairs = left.observations.filter(entry => rightById.has(entry.taskId)).map(entry => {
|
|
47
|
+
const counterpart = rightById.get(entry.taskId)!;
|
|
48
|
+
const fields: string[] = (["category", "model", "toolsDigest", "contextDigest", "contractDigest", "budget"] as const).filter(key => entry[key] !== counterpart[key]);
|
|
49
|
+
if (entry.externalEvaluation?.criteriaDigest !== counterpart.externalEvaluation?.criteriaDigest) fields.push("criteriaDigest");
|
|
50
|
+
if (fields.length) differences.push({ taskId: entry.taskId, fields: [...new Set(fields)] });
|
|
51
|
+
return { taskId: entry.taskId, leftVersion: entry.runtimeVersion, rightVersion: counterpart.runtimeVersion };
|
|
52
|
+
});
|
|
53
|
+
const leftSummary = summarizeBenchmarkReport(left), rightSummary = summarizeBenchmarkReport(right);
|
|
54
|
+
const metrics = (report: ProgramBenchmarkReport) => {
|
|
55
|
+
const sum = (values: number[], integral = false) => { const total = values.reduce((total, value) => total + value, 0); programAssert(Number.isFinite(total) && (!integral || Number.isSafeInteger(total)), "Benchmark aggregate exceeds numeric limits"); return total; };
|
|
56
|
+
const verified = report.observations.filter(entry => entry.category === "coding" && entry.externalEvaluation?.independent && entry.externalEvaluation.success && entry.externalEvaluation.escapedDefects === 0).length;
|
|
57
|
+
const observedTokens = report.observations.every(entry => entry.usageStatus === "observed") ? sum(report.observations.flatMap(entry => [entry.inputTokens!, entry.outputTokens!]), true) : null;
|
|
58
|
+
const stages = ["queue", "preparation", "execution", "capture", "persistence", "integration"] as const;
|
|
59
|
+
const timeMs = report.observations.every(entry => stages.every(stage => entry.timingMs[stage] !== undefined)) ? sum(report.observations.flatMap(entry => stages.map(stage => entry.timingMs[stage]!))) : null;
|
|
60
|
+
return { verifiedTasks: verified, observedTokens, timeMs, tokensPerVerifiedTask: observedTokens !== null && verified ? observedTokens / verified : null, timeMsPerVerifiedTask: timeMs !== null && verified ? timeMs / verified : null, humanInterventions: sum(report.observations.map(entry => entry.humanInterventions), true), repeatedActivities: sum(report.observations.map(entry => entry.repeatedActivities), true) };
|
|
61
|
+
};
|
|
62
|
+
return { schemaVersion: 1, pairs, unmatchedTasks, differences, left: { ...leftSummary, metrics: metrics(left) }, right: { ...rightSummary, metrics: metrics(right) },
|
|
63
|
+
comparisonEligible: unmatchedTasks.length === 0 && differences.length === 0 && leftSummary.comparisonEligible && rightSummary.comparisonEligible,
|
|
64
|
+
limitations: ["Paired report validation does not execute benchmarks or verify assessor independence. Model, tools, context, contract, budget and criteria differences prevent a controlled superiority claim.", ...left.limitations, ...right.limitations] };
|
|
65
|
+
}
|
|
66
|
+
export function summarizeBenchmarkReport(report: ProgramBenchmarkReport) {
|
|
67
|
+
validateBenchmarkReport(report);
|
|
68
|
+
const coding = report.observations.filter(entry => entry.category === "coding");
|
|
69
|
+
const independentlyAssessed = coding.filter(entry => entry.externalEvaluation?.independent);
|
|
70
|
+
const success = independentlyAssessed.filter(entry => entry.externalEvaluation?.success && entry.externalEvaluation.escapedDefects === 0).length;
|
|
71
|
+
return { digest: programDigest(report), tasks: report.observations.length, independentlyAssessed: independentlyAssessed.length,
|
|
72
|
+
verifiedSuccessRate: independentlyAssessed.length ? success / independentlyAssessed.length : null,
|
|
73
|
+
unknownUsageTasks: report.observations.filter(entry => entry.usageStatus === "unknown").length,
|
|
74
|
+
comparisonEligible: coding.length > 0 && independentlyAssessed.length === coding.length,
|
|
75
|
+
limitations: [...report.limitations, "Eligibility does not prove superiority; comparison requires matched workloads and declared model/tool/context differences."] };
|
|
76
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Shared public capability boundary; reports support, not real-provider certification. */
|
|
2
|
+
export const programWorkflowExtensions = Object.freeze({
|
|
3
|
+
authoring: { templates: ["review", "bugfix", "migration"], ownerValidated: true, typedRegistry: true, dispatch: false },
|
|
4
|
+
runtimeConfig: ".forge/fabric-runtime.json",
|
|
5
|
+
scheduler: { fairness: "round-robin", defaultOwnerCapacity: 4, maxOwnerCapacity: 32 },
|
|
6
|
+
resources: { enforcement: "admission-only", unknownUsageExplicit: true, providerHardCap: false },
|
|
7
|
+
observations: { durable: true, deduplicated: true, invalidOutputRetainsUsage: true },
|
|
8
|
+
executors: ["codex", "command", "claude"],
|
|
9
|
+
claude: { optIn: true, isolation: "cooperative", network: "host", realProviderVerified: false },
|
|
10
|
+
nativeImages: ["codex"],
|
|
11
|
+
threadRecovery: "codex-observed-compatible-original-workspace",
|
|
12
|
+
conflictResolution: "owner-registered-resolver-fresh-final-assessment",
|
|
13
|
+
externalQualityEvaluated: false,
|
|
14
|
+
});
|
|
@@ -3,6 +3,7 @@ import { stableStringify } from "./canonical.ts";
|
|
|
3
3
|
import { programGraph } from "./program-structure.ts";
|
|
4
4
|
import { validateVisualPopulation } from "./program-evidence.ts";
|
|
5
5
|
import { validateProgramTypes } from "./program-types.ts";
|
|
6
|
+
import type { ProgramWorkerObservation, ProgramUsageSummary } from "./program-observation.ts";
|
|
6
7
|
|
|
7
8
|
export type ProgramValue = null | boolean | number | string | ProgramValue[] | { [key: string]: ProgramValue };
|
|
8
9
|
export interface ProgramRef { id: string; version: string }
|
|
@@ -20,16 +21,18 @@ export type ProgramSchema = { type?: string | string[]; enum?: ProgramValue[]; c
|
|
|
20
21
|
minimum?: number; maximum?: number; anyOf?: ProgramSchema[] };
|
|
21
22
|
export interface ProgramLexicalContext { item?: ProgramSchema; state?: ProgramSchema }
|
|
22
23
|
export interface ProgramExecutor {
|
|
23
|
-
id: string; version: string; kind: "command" | "codex"; effect: "read" | "isolated-write" | "idempotent-effect" | "non-idempotent-effect";
|
|
24
|
+
id: string; version: string; kind: "command" | "codex" | "claude"; effect: "read" | "isolated-write" | "idempotent-effect" | "non-idempotent-effect";
|
|
24
25
|
argv?: string[]; prompt?: string; role?: "implementer" | "reviewer" | "investigator" | "decision";
|
|
25
26
|
model?: string; timeoutMs: number; writeScope: string[]; allowedGeneratedPaths?: string[];
|
|
26
27
|
network: "disabled" | "host"; isolation: "cooperative" | "sandbox"; schema: ProgramRef;
|
|
27
28
|
allowedExitCodes?: number[]; environment?: Record<string, string>;
|
|
29
|
+
tokenAccounting?: "none" | "provider";
|
|
28
30
|
inputSchema?: ProgramRef;
|
|
29
31
|
dependencies?: "none" | "auto";
|
|
30
32
|
cache?: "none" | "workspace";
|
|
31
33
|
}
|
|
32
34
|
export interface ProgramPolicy {
|
|
35
|
+
resources?: { maxTokens: number; reserveTokensPerAttempt: number; finalReserveTokens?: number; unknownUsage: "block" | "allow" };
|
|
33
36
|
allowFencedReplan?: boolean;
|
|
34
37
|
captureScope?: string[];
|
|
35
38
|
id: string; version: string; maxItems: number; concurrency: number; maxAttempts: number;
|
|
@@ -58,6 +61,7 @@ export interface ProgramRegistry {
|
|
|
58
61
|
populations: Record<string, ProgramPopulation>; programs?: Record<string, WorkflowProgramV2>;
|
|
59
62
|
}
|
|
60
63
|
export interface ProgramAssessment {
|
|
64
|
+
resolutionReceiptIds?: string[];
|
|
61
65
|
receiptId?: string; generation?: number; operationId?: string;
|
|
62
66
|
semanticDigest?: string; inputDigest?: string;
|
|
63
67
|
phase?: "item" | "final"; obligationIds?: string[]; satisfiedObligationIds?: string[]; unsatisfiedObligationIds?: string[];
|
|
@@ -78,6 +82,11 @@ export interface ProgramAttempt {
|
|
|
78
82
|
outcome: "running" | "completed" | "infrastructure_failed" | "invalid_output" | "uncertain" | "canceled_confirmed";
|
|
79
83
|
outputRef?: string; threadId?: string; reason?: string;
|
|
80
84
|
reservation?: { scopes: { id: string; limit: number }[]; status: "held" | "released"; ownerEpoch: string };
|
|
85
|
+
observations?: Record<string, ProgramWorkerObservation>;
|
|
86
|
+
usageApplicability?: "not-applicable" | "provider";
|
|
87
|
+
usageSummary?: ProgramUsageSummary;
|
|
88
|
+
resourceReservation?: { tokens: number; status: "held" | "released"; phase?: "work" | "final" };
|
|
89
|
+
recovery?: { directory: string; inputDigest: string; workspaceDigest: string; threadId?: string; terminationObserved?: boolean; generationCompatible?: boolean };
|
|
81
90
|
usage?: unknown;
|
|
82
91
|
}
|
|
83
92
|
export interface ProgramOperationRecord {
|
|
@@ -111,6 +120,7 @@ export interface ProgramRunV2 {
|
|
|
111
120
|
waits: Record<string, { generation: number; deadlineAt?: string; signalId?: string; outputRef?: string }>;
|
|
112
121
|
repairs: Record<string, { semanticDigest: string; inputDigest: string; candidate: ProgramCandidate; rounds: number; assessments: number; infrastructure: number; unchanged: number; repeated: number; priorFindings: string; feedback: unknown; phase: "implement" | "implementation-active" | "assess" | "assessment-active" | "decide" | "accepted" | "exhausted" | "stalled"; assessment?: ProgramAssessment }>;
|
|
113
122
|
collections: Record<string, { seal: string; coverageReceiptId: string | null; generation: number; ids: string[]; acceptedAssessmentIds: string[]; acceptedCandidateIds: string[]; obligationsSatisfied: boolean }>;
|
|
123
|
+
compositionResolutions?: Record<string, { receiptId: string; candidateId: string; candidateDigest: string; originalCandidateIds: string[]; attemptId: string; operationId: string; generation: number; semanticDigest: string; inputDigest: string; populationDigest: string; acceptanceDigest: string }>;
|
|
114
124
|
resultRef?: string; gateRef?: string; intent?: ProgramApplyIntent; receiptRef?: string;
|
|
115
125
|
acceptedCandidate?: ProgramCandidate;
|
|
116
126
|
scopeResults?: Record<string, { outcomes: { id: string; status: string; reason?: string }[]; status: "completed" | "waiting" | "failed"; resultRef?: string }>;
|
|
@@ -212,6 +222,11 @@ export function validateWorkflowProgram(program: WorkflowProgramV2, registry: Pr
|
|
|
212
222
|
const policy = registryEntry(registry.policies, program.policy), acceptance = registryEntry(registry.acceptance, program.acceptance);
|
|
213
223
|
for (const field of ["maxItems", "concurrency", "maxAttempts", "maxOperations", "maxDepth", "deadlineMs", "maxOutputBytes"] as const) programAssert(Number.isSafeInteger(policy[field]) && policy[field] > 0, `Invalid limit ${field}`);
|
|
214
224
|
programAssert(policy.concurrency <= 32 && policy.maxDepth <= 32 && policy.maxItems <= 10000 && policy.maxAttempts <= 10000 && policy.maxOperations <= 20000 && policy.maxOutputBytes <= 4 * 1024 * 1024, "Physical limits exceeded");
|
|
225
|
+
if (policy.resources) {
|
|
226
|
+
const budget = policy.resources;
|
|
227
|
+
for (const value of [budget.maxTokens, budget.reserveTokensPerAttempt, budget.finalReserveTokens ?? 0]) programAssert(Number.isSafeInteger(value) && value >= 0, "Invalid resource budget");
|
|
228
|
+
programAssert(budget.maxTokens > 0 && budget.reserveTokensPerAttempt > 0 && (budget.finalReserveTokens ?? 0) <= budget.maxTokens && ["block", "allow"].includes(budget.unknownUsage), "Invalid resource policy");
|
|
229
|
+
}
|
|
215
230
|
for (const path of [...policy.writeScope, ...acceptance.writeScope, ...(policy.captureScope ?? [])]) programPath(path);
|
|
216
231
|
programAssert(acceptance.criteria.length > 0 && new Set(acceptance.criteria).size === acceptance.criteria.length, "Acceptance criteria required and unique");
|
|
217
232
|
if (program.population) { const population = registryEntry(registry.populations, program.population); programAssert(population.members.length <= policy.maxItems && new Set(population.members).size === population.members.length && population.evidence.length > 0, "Population inventory invalid"); validateVisualPopulation(population); }
|
|
@@ -235,7 +250,7 @@ export function validateWorkflowProgram(program: WorkflowProgramV2, registry: Pr
|
|
|
235
250
|
map: ["items", "key", "coverage", "concurrency", "completion", "quorum", "body", "order"],
|
|
236
251
|
branch: ["condition", "then", "else"], loop: ["initialState", "maxRounds", "body", "next", "until"],
|
|
237
252
|
repair: ["recipe", "entryMode", "initialCandidate", "writeScope", "implement", "review", "checks", "input", "assessmentScope", "evidence", "maxRepairRounds", "maxAssessmentAttempts", "maxInfrastructureAttempts", "progressPolicy"],
|
|
238
|
-
compose: ["candidates", "onConflict"], gate: ["candidate", "coverage", "authorization"], subworkflow: ["program", "input"],
|
|
253
|
+
compose: ["candidates", "onConflict", "resolver", "resolverInput", "resolverWriteScope"], gate: ["candidate", "coverage", "authorization"], subworkflow: ["program", "input"],
|
|
239
254
|
waitEvent: ["schema", "type", "correlation", "subject", "timeoutMs"], sequence: ["steps", "result"], parallel: ["steps", "result", "onFailure"],
|
|
240
255
|
};
|
|
241
256
|
kinds.add("sequence"); kinds.add("parallel");
|
|
@@ -255,7 +270,8 @@ export function validateWorkflowProgram(program: WorkflowProgramV2, registry: Pr
|
|
|
255
270
|
if (step.options.completion === "quorum") programAssert(step.options.quorum && typeof step.options.quorum === "object" && Number.isSafeInteger((step.options.quorum as { minAccepted: number }).minAccepted) && (step.options.quorum as { minAccepted: number }).minAccepted > 0, "Quorum minAccepted required");
|
|
256
271
|
else programAssert(step.options.quorum === undefined, "Quorum only applies to quorum completion");
|
|
257
272
|
}
|
|
258
|
-
if (["agent", "command"].includes(step.kind)) { const executor = registryEntry(registry.executors, step.options.executor as ProgramRef); if (executor.inputSchema) registryEntry(registry.schemas, executor.inputSchema); }
|
|
273
|
+
if (["agent", "command"].includes(step.kind)) { const executor = registryEntry(registry.executors, step.options.executor as ProgramRef); programAssert(executor.tokenAccounting === undefined || executor.tokenAccounting === "provider" || executor.tokenAccounting === "none" && executor.kind === "command", "Token accounting exemption only applies to owner-declared nonprovider commands"); if (executor.inputSchema) registryEntry(registry.schemas, executor.inputSchema); }
|
|
274
|
+
if (step.kind === "compose" && step.options.resolver) programAssert(registryEntry(registry.executors, step.options.resolver as ProgramRef).effect === "isolated-write", "Conflict resolver must produce an isolated candidate");
|
|
259
275
|
if (step.kind === "repair") {
|
|
260
276
|
registryEntry(registry.executors, step.options.implement as ProgramRef); registryEntry(registry.executors, step.options.review as ProgramRef);
|
|
261
277
|
programAssert(["implement-first", "assess-first"].includes(String(step.options.entryMode)), "Repair entryMode required");
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { programAssert, programDigest, programValueBytes, type ProgramAttempt } from "./program-contract.ts";
|
|
2
|
+
|
|
3
|
+
/** Adapter observations are data, not completion or acceptance authority. */
|
|
4
|
+
export interface ProgramWorkerObservation {
|
|
5
|
+
id: string; type: string; source: string; at: string;
|
|
6
|
+
usage?: { input_tokens: number; cached_input_tokens?: number; output_tokens: number };
|
|
7
|
+
semantics?: "incremental" | "cumulative";
|
|
8
|
+
metadata?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface ProgramUsageSummary {
|
|
11
|
+
inputTokens: number; outputTokens: number; cachedInputTokens: number;
|
|
12
|
+
totalTokens: number; certainty: "observed" | "unknown" | "not-applicable";
|
|
13
|
+
}
|
|
14
|
+
export function recordProgramObservation(attempt: ProgramAttempt, observation: ProgramWorkerObservation): void {
|
|
15
|
+
programValueBytes(observation, 16384, 128);
|
|
16
|
+
programAssert(typeof observation.id === "string" && observation.id.length > 0 && observation.id.length <= 256 &&
|
|
17
|
+
typeof observation.type === "string" && observation.type.length <= 128 && typeof observation.source === "string" && observation.source.length <= 128 &&
|
|
18
|
+
Number.isFinite(Date.parse(observation.at)), "Invalid worker observation");
|
|
19
|
+
programAssert(observation.semantics === undefined || ["incremental", "cumulative"].includes(observation.semantics), "Invalid usage semantics");
|
|
20
|
+
if (observation.usage) {
|
|
21
|
+
const usage = observation.usage;
|
|
22
|
+
programAssert(Number.isSafeInteger(usage.input_tokens) && usage.input_tokens >= 0 && Number.isSafeInteger(usage.output_tokens) && usage.output_tokens >= 0, "Usage requires nonnegative safe input/output counters");
|
|
23
|
+
programAssert(usage.cached_input_tokens === undefined || Number.isSafeInteger(usage.cached_input_tokens) && usage.cached_input_tokens >= 0 && usage.cached_input_tokens <= usage.input_tokens, "Invalid cached usage counter");
|
|
24
|
+
}
|
|
25
|
+
const observations = attempt.observations ??= {};
|
|
26
|
+
if (observations[observation.id]) { programAssert(programDigest(observations[observation.id]) === programDigest(observation), "Observation identity changed"); return; }
|
|
27
|
+
programAssert(Object.keys(observations).length < 1024, "Attempt observation budget exceeded");
|
|
28
|
+
const next = { ...attempt, observations: { ...observations, [observation.id]: structuredClone(observation) } };
|
|
29
|
+
const summary = summarizeProgramUsage(next);
|
|
30
|
+
observations[observation.id] = next.observations[observation.id];
|
|
31
|
+
attempt.usageSummary = summary;
|
|
32
|
+
}
|
|
33
|
+
export function summarizeProgramUsage(attempt: ProgramAttempt): ProgramUsageSummary {
|
|
34
|
+
let inputTokens = 0, outputTokens = 0, cachedInputTokens = 0, observed = false;
|
|
35
|
+
const cumulative = new Map<string, NonNullable<ProgramWorkerObservation["usage"]>>();
|
|
36
|
+
const semantics = new Map<string, string>();
|
|
37
|
+
for (const observation of Object.values(attempt.observations ?? {})) {
|
|
38
|
+
if (!observation.usage) continue; observed = true;
|
|
39
|
+
const mode = observation.semantics ?? "incremental";
|
|
40
|
+
programAssert(!semantics.has(observation.source) || semantics.get(observation.source) === mode, "Usage source mixes cumulative and incremental semantics");
|
|
41
|
+
semantics.set(observation.source, mode);
|
|
42
|
+
if (observation.semantics === "cumulative") {
|
|
43
|
+
const previous = cumulative.get(observation.source);
|
|
44
|
+
cumulative.set(observation.source, { input_tokens: Math.max(previous?.input_tokens ?? 0, observation.usage.input_tokens), output_tokens: Math.max(previous?.output_tokens ?? 0, observation.usage.output_tokens), cached_input_tokens: Math.max(previous?.cached_input_tokens ?? 0, observation.usage.cached_input_tokens ?? 0) });
|
|
45
|
+
} else { inputTokens += observation.usage.input_tokens; outputTokens += observation.usage.output_tokens; cachedInputTokens += observation.usage.cached_input_tokens ?? 0; }
|
|
46
|
+
}
|
|
47
|
+
for (const usage of cumulative.values()) { inputTokens += usage.input_tokens; outputTokens += usage.output_tokens; cachedInputTokens += usage.cached_input_tokens ?? 0; }
|
|
48
|
+
if (!observed && attempt.usage && typeof attempt.usage === "object") {
|
|
49
|
+
const usage = attempt.usage as { input_tokens?: number; output_tokens?: number; cached_input_tokens?: number };
|
|
50
|
+
if (Number.isSafeInteger(usage.input_tokens) && Number.isSafeInteger(usage.output_tokens) && usage.input_tokens! >= 0 && usage.output_tokens! >= 0) {
|
|
51
|
+
inputTokens = usage.input_tokens!; outputTokens = usage.output_tokens!;
|
|
52
|
+
cachedInputTokens = Number.isSafeInteger(usage.cached_input_tokens) && usage.cached_input_tokens! >= 0 ? usage.cached_input_tokens! : 0; observed = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
programAssert([inputTokens, outputTokens, cachedInputTokens, inputTokens + outputTokens].every(value => Number.isSafeInteger(value) && value >= 0), "Usage aggregate exceeds safe integer range");
|
|
56
|
+
return { inputTokens, outputTokens, cachedInputTokens, totalTokens: inputTokens + outputTokens, certainty: observed ? "observed" : attempt.usageApplicability === "not-applicable" ? "not-applicable" : "unknown" };
|
|
57
|
+
}
|
|
58
|
+
/** Held reservations and unknown completed calls remain liabilities after owner restart. */
|
|
59
|
+
export function programResourceLiability(attempt: ProgramAttempt): number {
|
|
60
|
+
const usage = summarizeProgramUsage(attempt);
|
|
61
|
+
const reserve = attempt.resourceReservation?.tokens ?? 0;
|
|
62
|
+
return usage.totalTokens + (attempt.resourceReservation?.status === "held" ? Math.max(0, reserve - usage.totalTokens) : usage.certainty === "unknown" ? reserve : 0);
|
|
63
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { ProgramError } from "./program-contract.ts";
|
|
4
|
+
|
|
5
|
+
export interface ProgramRuntimeOptions { ownerCapacity?: number; ownerMaxTokens?: number }
|
|
6
|
+
|
|
7
|
+
/** Owner configuration is local trusted configuration, never a workflow request override. */
|
|
8
|
+
export function readProgramRuntimeOptions(root: string): ProgramRuntimeOptions {
|
|
9
|
+
const directory = join(root, ".forge"), path = join(directory, "fabric-runtime.json");
|
|
10
|
+
if (lstatSync(directory, { throwIfNoEntry: false })?.isSymbolicLink()) throw new ProgramError("AF_PROGRAM_INVALID", "Runtime configuration directory cannot be a symbolic link");
|
|
11
|
+
const stat = lstatSync(path, { throwIfNoEntry: false });
|
|
12
|
+
if (!stat) return {};
|
|
13
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024) throw new ProgramError("AF_PROGRAM_INVALID", "Runtime configuration must be a regular file up to 16 KiB");
|
|
14
|
+
const value: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
15
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new ProgramError("AF_PROGRAM_INVALID", "Runtime configuration must be an object");
|
|
16
|
+
const record = value as Record<string, unknown>;
|
|
17
|
+
if (record.schemaVersion !== 1 || Object.keys(record).some(key => !["schemaVersion", "ownerCapacity", "ownerMaxTokens"].includes(key))) throw new ProgramError("AF_PROGRAM_INVALID", "Unsupported runtime configuration");
|
|
18
|
+
for (const key of ["ownerCapacity", "ownerMaxTokens"] as const) {
|
|
19
|
+
const number = record[key];
|
|
20
|
+
if (number !== undefined && (typeof number !== "number" || !Number.isSafeInteger(number) || number < 1 || (key === "ownerCapacity" && number > 32))) throw new ProgramError("AF_PROGRAM_INVALID", `Invalid ${key}`);
|
|
21
|
+
}
|
|
22
|
+
return { ownerCapacity: record.ownerCapacity as number | undefined, ownerMaxTokens: record.ownerMaxTokens as number | undefined };
|
|
23
|
+
}
|
|
@@ -8,8 +8,15 @@ export class ProgramActivityScheduler {
|
|
|
8
8
|
private active = new Map<string, number>();
|
|
9
9
|
private scopes = new Map<string, number>();
|
|
10
10
|
private count = 0;
|
|
11
|
-
private
|
|
12
|
-
|
|
11
|
+
private runOrder: string[] = [];
|
|
12
|
+
private cursor = 0;
|
|
13
|
+
private capacity: number;
|
|
14
|
+
get ownerLimit(): number { return this.capacity; }
|
|
15
|
+
constructor(ownerLimit = 4) { programAssert(Number.isSafeInteger(ownerLimit) && ownerLimit > 0 && ownerLimit <= 32, "Invalid owner capacity"); this.capacity = ownerLimit; }
|
|
16
|
+
setOwnerCapacity(ownerLimit: number): void {
|
|
17
|
+
programAssert(Number.isSafeInteger(ownerLimit) && ownerLimit > 0 && ownerLimit <= 32, "Invalid owner capacity");
|
|
18
|
+
this.capacity = ownerLimit; this.drain();
|
|
19
|
+
}
|
|
13
20
|
restore(runId: string, scopes: ActivityScope[]): () => void {
|
|
14
21
|
this.count++; this.active.set(runId, (this.active.get(runId) ?? 0) + 1);
|
|
15
22
|
for (const scope of scopes) this.scopes.set(scope.id, (this.scopes.get(scope.id) ?? 0) + 1);
|
|
@@ -20,19 +27,31 @@ export class ProgramActivityScheduler {
|
|
|
20
27
|
return new Promise((resolve, reject) => {
|
|
21
28
|
const ticket: Ticket = { runId, runLimit, scopes, resolve, reject, signal, abort: () => { this.tickets = this.tickets.filter(entry => entry !== ticket); reject(new Error("Activity admission canceled")); this.drain(); } };
|
|
22
29
|
if (signal.aborted) { ticket.abort(); return; }
|
|
23
|
-
signal.addEventListener("abort", ticket.abort, { once: true }); this.tickets.push(ticket);
|
|
30
|
+
signal.addEventListener("abort", ticket.abort, { once: true }); this.tickets.push(ticket);
|
|
31
|
+
if (!this.runOrder.includes(runId)) this.runOrder.push(runId);
|
|
32
|
+
this.drain();
|
|
24
33
|
});
|
|
25
34
|
}
|
|
26
35
|
private drain(): void {
|
|
27
36
|
while (this.count < this.ownerLimit) {
|
|
28
37
|
const eligible = this.tickets.filter(ticket => !ticket.signal.aborted && (this.active.get(ticket.runId) ?? 0) < ticket.runLimit && ticket.scopes.every(scope => (this.scopes.get(scope.id) ?? 0) < scope.limit));
|
|
29
|
-
|
|
38
|
+
let ticket: Ticket | undefined;
|
|
39
|
+
for (let offset = 0; offset < this.runOrder.length; offset++) {
|
|
40
|
+
const index = (this.cursor + offset) % this.runOrder.length;
|
|
41
|
+
ticket = eligible.find(entry => entry.runId === this.runOrder[index]);
|
|
42
|
+
// Keep the next ordinal unwrapped: newly queued runs after a lone run
|
|
43
|
+
// must get their turn before the previously admitted run repeats.
|
|
44
|
+
if (ticket) { this.cursor = index + 1; break; }
|
|
45
|
+
}
|
|
46
|
+
if (!ticket) return;
|
|
30
47
|
this.tickets.splice(this.tickets.indexOf(ticket), 1); ticket.signal.removeEventListener("abort", ticket.abort);
|
|
31
|
-
this.
|
|
48
|
+
this.count++; this.active.set(ticket.runId, (this.active.get(ticket.runId) ?? 0) + 1);
|
|
32
49
|
for (const scope of ticket.scopes) this.scopes.set(scope.id, (this.scopes.get(scope.id) ?? 0) + 1);
|
|
33
50
|
let released = false;
|
|
34
51
|
ticket.resolve(() => { if (released) return; released = true; this.count--; this.active.set(ticket.runId, this.active.get(ticket.runId)! - 1); for (const scope of ticket.scopes) this.scopes.set(scope.id, this.scopes.get(scope.id)! - 1); this.drain(); });
|
|
35
52
|
}
|
|
36
53
|
}
|
|
37
|
-
snapshot(): { active: number; queued: number; ownerLimit: number
|
|
54
|
+
snapshot(): { active: number; queued: number; ownerLimit: number; runs: { runId: string; active: number; queued: number }[] } {
|
|
55
|
+
return { active: this.count, queued: this.tickets.length, ownerLimit: this.ownerLimit, runs: [...new Set([...this.active.keys(), ...this.tickets.map(ticket => ticket.runId)])].map(runId => ({ runId, active: this.active.get(runId) ?? 0, queued: this.tickets.filter(ticket => ticket.runId === runId).length })) };
|
|
56
|
+
}
|
|
38
57
|
}
|