forgeos 0.1.0-alpha.70 → 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/skills/forge-agent-fabric/SKILL.md +10 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +12 -0
- package/docs/agent-fabric-programs.md +183 -168
- package/docs/agent-fabric.md +4 -6
- 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/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_ADR.md +81 -41
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS.md +39 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS_EVIDENCE.md +53 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_EVIDENCE.md +49 -109
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_PLAN.md +463 -423
- package/examples/agent-fabric-v2/README.md +16 -0
- package/examples/agent-fabric-v2/item-child.workflow.ts +11 -0
- package/examples/agent-fabric-v2/map-child.workflow.ts +10 -0
- package/examples/agent-fabric-v2/migrate.workflow.ts +1 -1
- package/examples/agent-fabric-v2/run-ui-fixture.ts +48 -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/examples/agent-fabric-v2/ui-audit.workflow.ts +28 -0
- package/examples/agent-fabric-v2/ui-fixture-worker.cjs +36 -0
- package/examples/agent-fabric-v2/visual-population.json +13 -0
- package/package.json +7 -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 +12 -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 +37 -0
- 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 +106 -36
- package/src/forge/agent-fabric/program-dsl.ts +45 -31
- package/src/forge/agent-fabric/program-evidence.ts +22 -0
- package/src/forge/agent-fabric/program-image.ts +15 -0
- package/src/forge/agent-fabric/program-lock.ts +50 -0
- package/src/forge/agent-fabric/program-observation.ts +63 -0
- package/src/forge/agent-fabric/program-owner.ts +16 -0
- package/src/forge/agent-fabric/program-runtime-options.ts +23 -0
- package/src/forge/agent-fabric/program-scheduler.ts +57 -0
- package/src/forge/agent-fabric/program-service.ts +423 -112
- package/src/forge/agent-fabric/program-store.ts +88 -55
- package/src/forge/agent-fabric/program-structure.ts +65 -0
- package/src/forge/agent-fabric/program-templates.ts +96 -0
- package/src/forge/agent-fabric/program-types.ts +21 -9
- package/src/forge/agent-fabric/program-view.ts +26 -0
- package/src/forge/agent-fabric/program-worker.ts +93 -23
- package/src/forge/agent-memory/mcp.ts +9 -3
- package/src/forge/cli/fabric.ts +4 -2
- package/src/forge/cli/main.ts +4 -0
- package/src/forge/cli/parse.ts +3 -1
- package/src/forge/version.ts +1 -1
|
@@ -52,3 +52,19 @@ The owner registry is project configuration, not workflow input. `cache: none` i
|
|
|
52
52
|
default. Only commands declared to depend entirely on their captured workspace inputs
|
|
53
53
|
may opt into `cache: workspace`; the owner must account for external inputs. SDK output
|
|
54
54
|
reuse is disabled until its effective instruction closure can be attested.
|
|
55
|
+
|
|
56
|
+
## Current workflow runtime and UI example
|
|
57
|
+
|
|
58
|
+
`ui-audit.workflow.ts` uses the current operatorVersion 2 architecture directly. `run-ui-fixture.ts` is a disposable local integration example: twenty HTML pages, three known defects, command-only agents, item and final checks, capture receipts, compose and gate. Run `node bin/forge-bun.mjs run examples/agent-fabric-v2/run-ui-fixture.ts` from the Forge checkout. It sets the no-LLM fuse and never applies to a user project.
|
|
59
|
+
|
|
60
|
+
`runUiFixture(count, defects, browserPath?)` also accepts an explicit Chromium/Edge executable for real local headless screenshots; the default uses valid PNG fixtures. Both reviewers are deterministic checks of fixture attributes, not visual AI evaluation. The worker's cooperative process capability must be owner-authorized.
|
|
61
|
+
|
|
62
|
+
The static lowerer and TS author API share the same example including `as const`, `satisfies`, typed refs and scoped checks. Arbitrary TS execution is unsupported. Legacy file-conversion example remains a separate usage example, not a compatibility interpreter. See docs/agent-fabric-programs.md for all fifty author constructors and fifteen controls.
|
|
63
|
+
|
|
64
|
+
## Typed reusable subworkflow and visual cases
|
|
65
|
+
|
|
66
|
+
`item-child.workflow.ts` is a finite typed inspection program. Lower it and register its IR as `inspect-item@v2`; the root `map-child.workflow.ts` calls it with explicit input and a map quota of one. Supply owner schemas `path@v2` (object path:string), `inspection@v2` (object ok:boolean), `any@v2`, policy `local@v2`, acceptance `data@v2`, and executor `inspect@v2` with inputSchema path@v2/output schema inspection@v2. These are templates, not automatically installed executors. Child quotas/cancellation inherit from the root; keep the same policy/acceptance.
|
|
67
|
+
|
|
68
|
+
`visual-population.json` demonstrates separate desktop/ready and mobile/error cases for one route. Replace the baseline placeholder with the owner-captured digest and enumerate all required scenarios. Matching itemKey alone cannot close a visual case when route, viewport, state or dimensions differ. Image header validation is not proof of visual correctness.
|
|
69
|
+
|
|
70
|
+
Use program-explain to retrieve the template graph, observed expansions and a Mermaid diagram. Format 3 runs live in program-runs-v3; older checkpoint directories are preserved and not loaded.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineWorkflow, schemaRef, policyRef, acceptanceRef, executorRef, workflowInput, agent, output } from "forgeos/agent-fabric/workflows";
|
|
2
|
+
|
|
3
|
+
// Register this IR as inspect-item@v2. Owner schemas/executor remain authoritative.
|
|
4
|
+
const inspect = agent("inspect", {
|
|
5
|
+
executor: executorRef<{ path: string }, { ok: boolean }>("inspect", "v2"),
|
|
6
|
+
input: workflowInput<{ path: string }>()
|
|
7
|
+
});
|
|
8
|
+
export default defineWorkflow({
|
|
9
|
+
id: "inspect-item", version: 2, mode: "data", inputSchema: schemaRef("path", "v2"), outputSchema: schemaRef("inspection", "v2"),
|
|
10
|
+
policy: policyRef("local", "v2"), acceptance: acceptanceRef("data", "v2"), steps: [inspect], result: output(inspect)
|
|
11
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineWorkflow, schemaRef, policyRef, acceptanceRef, programRef, map, subworkflow, item, field, output } from "forgeos/agent-fabric/workflows";
|
|
2
|
+
|
|
3
|
+
export default defineWorkflow({
|
|
4
|
+
id: "inspect-files", version: 2, mode: "data", inputSchema: schemaRef("any", "v2"), outputSchema: schemaRef("any", "v2"),
|
|
5
|
+
policy: policyRef("local", "v2"), acceptance: acceptanceRef("data", "v2"),
|
|
6
|
+
steps: [map("files", {
|
|
7
|
+
items: [{ path: "src/a.ts" }, { path: "src/b.ts" }], key: field(item(), "path"), concurrency: 1, completion: "all-required",
|
|
8
|
+
body: subworkflow("inspect", { program: programRef<{ path: string }, { ok: boolean }>("inspect-item", "v2"), input: item<{ path: string }>() })
|
|
9
|
+
})], result: output("files")
|
|
10
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineWorkflow, schemaRef, policyRef, acceptanceRef, populationRef, executorRef, recipeRef, agent, map, field, item, output, population, coverageFor, repair, candidateFromBaseline, compose, acceptedCandidates, outputCandidate, acceptedCandidate, gate, coverageReceipt } from "forgeos/agent-fabric/workflows";
|
|
2
2
|
|
|
3
3
|
const recipe = {
|
|
4
|
-
recipe: recipeRef("repair", "
|
|
4
|
+
recipe: recipeRef("repair", "v2"),
|
|
5
5
|
implement: executorRef("migrate", "v1"), review: executorRef("review", "v1"),
|
|
6
6
|
checks: ["check@v1"], maxRepairRounds: 3, maxAssessmentAttempts: 6,
|
|
7
7
|
maxInfrastructureAttempts: 2,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { ProgramRunService } from "../../src/forge/agent-fabric/program-service.ts";
|
|
7
|
+
import { programDigest, type ProgramRegistry, type ProgramRunV2 } from "../../src/forge/agent-fabric/program-contract.ts";
|
|
8
|
+
import { lowerWorkflowSource, schemaRef } from "../../src/forge/agent-fabric/program-dsl.ts";
|
|
9
|
+
|
|
10
|
+
export async function runUiFixture(count = 20, defects = [2, 9, 17], browserPath?: string) {
|
|
11
|
+
const root = await mkdtemp(join(tmpdir(), "forge-ui-fixture-"));
|
|
12
|
+
let service: ProgramRunService | undefined;
|
|
13
|
+
const startedAt = performance.now();
|
|
14
|
+
try {
|
|
15
|
+
await mkdir(join(root, "src")); await mkdir(join(root, ".forge"));
|
|
16
|
+
for (let index = 0; index < count; index++) await writeFile(join(root, "src", `ui-${String(index).padStart(2, "0")}.html`), `<!doctype html><html><title>UI ${index}</title><body data-fixture="${defects.includes(index) ? "bad" : "good"}"><h1>Local fixture ${index}</h1></body></html>`);
|
|
17
|
+
for (const args of [["init", "--quiet"], ["add", "src"], ["-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", "commit", "--quiet", "-m", "UI fixture baseline"]]) {
|
|
18
|
+
const result = spawnSync("git", args, { cwd: root, encoding: "utf8", windowsHide: true }); if (result.status) throw new Error(result.stderr);
|
|
19
|
+
}
|
|
20
|
+
const registry: ProgramRegistry = { schemas: { "any@v2": {} }, executors: {}, policies: {}, acceptance: {}, populations: {} };
|
|
21
|
+
const worker = fileURLToPath(new URL("./ui-fixture-worker.cjs", import.meta.url));
|
|
22
|
+
for (const id of ["discover", "capture", "implement", "review", "local-check", "global-check"]) registry.executors[`${id}@v2`] = {
|
|
23
|
+
id, version: "v2", kind: "command", effect: id === "implement" ? "isolated-write" : "read", role: id === "implement" ? "implementer" : id === "review" ? "reviewer" : "investigator",
|
|
24
|
+
argv: [process.execPath, worker, id, ...(id === "capture" && browserPath ? [browserPath] : [])], timeoutMs: id === "capture" && browserPath ? 120000 : 10000, writeScope: id === "implement" ? ["src"] : [], network: "host", isolation: "cooperative", schema: schemaRef("any", "v2")
|
|
25
|
+
};
|
|
26
|
+
registry.policies["local@v2"] = { id: "local", version: "v2", concurrency: 4, maxItems: 100, maxAttempts: 500, maxOperations: 2000, maxDepth: 12, deadlineMs: 240000, maxOutputBytes: 4 * 1024 * 1024, writeScope: ["src"], executors: Object.keys(registry.executors), allowNetwork: true, allowCooperativeCommands: true };
|
|
27
|
+
registry.acceptance["ui@v2"] = { id: "ui", version: "v2", criteria: ["Known fixture attributes are correct"], requireReview: true, allowNoWork: false, writeScope: ["src"], requiredChecks: [],
|
|
28
|
+
requiredChecksByScope: { item: ["local-check@v2"], final: ["global-check@v2"] }, assessmentBindings: { page: "item", final: "final" },
|
|
29
|
+
obligations: [{ id: "layout", scope: "item", criteria: ["known fixture constraint"], requiredEvidence: ["capture"] }, { id: "integration", scope: "final", criteria: ["all pages correct together"] }] };
|
|
30
|
+
registry.populations["pages@v2"] = { id: "pages", version: "v2", members: [], baselineDigest: "owner-capture", inventoryRoots: ["src"], extensions: [".html"], exclusions: [], allowNoWork: false, evidence: "owner filesystem inventory" };
|
|
31
|
+
await writeFile(join(root, ".forge/fabric-programs.json"), JSON.stringify(registry));
|
|
32
|
+
const source = await readFile(new URL("./ui-audit.workflow.ts", import.meta.url), "utf8"), program = lowerWorkflowSource(source);
|
|
33
|
+
service = await ProgramRunService.open(root);
|
|
34
|
+
const initial = await service.execute("program-start", { requestId: "ui-fixture", program, input: {} }) as ProgramRunV2;
|
|
35
|
+
let run: ProgramRunV2;
|
|
36
|
+
do { await new Promise(resolve => setTimeout(resolve, 20)); run = (await service.store.read(initial.runId))!; } while (run.status === "executing");
|
|
37
|
+
if (run.status !== "acceptance-ready") throw new Error(`UI fixture failed: ${run.status}: ${run.reason}`);
|
|
38
|
+
const assessment = Object.values(run.assessments).find(receipt => receipt.phase === "final")!;
|
|
39
|
+
const implementations = Object.values(run.attempts).filter(attempt => attempt.operationId.endsWith("/implement"));
|
|
40
|
+
const beforeApply = await readFile(join(root, "src", `ui-${String(defects[0] ?? 0).padStart(2, "0")}.html`), "utf8");
|
|
41
|
+
const report = { datasetDigest: programDigest({ count, defects }), pages: count, knownDefects: defects.length, implementations: implementations.length, attempts: run.totalAttempts,
|
|
42
|
+
captures: Object.keys(run.artifactRecords ?? {}).length, finalObligations: assessment.satisfiedObligationIds?.length, finalCaptures: assessment.evidenceRefs?.length,
|
|
43
|
+
destinationPreserved: beforeApply.includes(`data-fixture="${defects.length ? "bad" : "good"}"`), llmCalls: 0, elapsedMs: Math.round(performance.now() - startedAt), storage: service.store.metrics,
|
|
44
|
+
captureMode: browserPath ? "local-headless-browser" : "png-fixture", limitations: [browserPath ? "Local browser rendering; no visual model evaluation" : "PNG fixtures; no browser rendering or visual model evaluation", "Cooperative command isolation", "No automatic apply; no Claude benchmark"] };
|
|
45
|
+
return report;
|
|
46
|
+
} finally { await service?.close(); await rm(root, { recursive: true, force: true }); }
|
|
47
|
+
}
|
|
48
|
+
if (import.meta.main) { process.env.FORGE_FABRIC_TEST_MODE = "1"; console.log(JSON.stringify(await runUiFixture(), null, 2)); }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Owner-bound authoring templates
|
|
2
|
+
|
|
3
|
+
These JSON requests select the review, bugfix and migration templates. `program-author`
|
|
4
|
+
validates references against the owner's registry and returns a proposal without running
|
|
5
|
+
workers, replacing policies, applying changes or spending model tokens. Start is a
|
|
6
|
+
separate action. The supplied registry's workers and migration criteria are text-file
|
|
7
|
+
fixtures; replace them with real project contracts and verification commands for coding.
|
|
8
|
+
|
|
9
|
+
Review begins with assessment and requests an empty write scope. A negative assessment
|
|
10
|
+
cannot change files; bounded follow-up may end exhausted or stalled. Bugfix assesses
|
|
11
|
+
first, avoiding edits to an already accepted baseline. Migration consumes
|
|
12
|
+
`input.items: [{id, allowedPaths}]`, seals coverage against the owner population, repairs
|
|
13
|
+
each item only after a negative assessment, composes accepted candidates and assesses the integrated candidate before gate. An optional owner-authorized resolver can be selected in migration options; its candidate requires fresh final population evaluation.
|
|
14
|
+
|
|
15
|
+
Each repair preserves mandatory checks. Scoped acceptance must bind `component` to
|
|
16
|
+
`item` and `assessment` to `final`; templates do not mutate these owner bindings.
|
|
17
|
+
Human approval, when required, remains outstanding at the gate. Apply remains separate.
|
|
18
|
+
`program-author-types` exports owner schema/executor declarations for author assistance;
|
|
19
|
+
runtime validation is still authoritative.
|
|
20
|
+
|
|
21
|
+
## Independent benchmark reports
|
|
22
|
+
|
|
23
|
+
`program-benchmark.ts` defines a versioned observation report. Record identity of runtime,
|
|
24
|
+
model, tools, context, contracts and budget for each task. Queue/preparation/execution/
|
|
25
|
+
capture/persistence/integration timings are optional observations; omitted means absent,
|
|
26
|
+
not zero. Consumption is observed, estimated or unknown. Independent evaluation requires
|
|
27
|
+
an assessor and criteria digest fixed independently of the implementer. Owner gate
|
|
28
|
+
approval alone does not qualify as external success.
|
|
29
|
+
|
|
30
|
+
Read and summarize an already collected report with:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
node --import tsx scripts/summarize-fabric-benchmark.mjs report.json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This command only validates data. It does not run workflows, providers or benchmarks.
|
|
37
|
+
The existing deterministic benchmark scripts remain runtime references and do not prove
|
|
38
|
+
superiority over Claude or coding quality. Real evaluations are a separate explicit run.
|
|
39
|
+
|
|
40
|
+
## CI scope
|
|
41
|
+
|
|
42
|
+
PRs changing only explicitly covered authoring, observation, benchmark or adapter modules use a small critical suite. Missing critical files fail closed.
|
|
43
|
+
Core owner/store/workspace/services, shared or unknown paths and main runs retain the full Agent Fabric suite. Workflow
|
|
44
|
+
contracts, typecheck, compiler checks, lint and the separate security gate remain.
|
|
45
|
+
Ubuntu/Node 22 CLI smoke runs once in verify; the matrix adds Windows 22 for PRs and
|
|
46
|
+
Ubuntu 24 on main. Manual CI includes macOS.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "bugfix",
|
|
3
|
+
"options": {
|
|
4
|
+
"id": "bugfix-workflow",
|
|
5
|
+
"inputSchema": { "id": "input", "version": "v1" },
|
|
6
|
+
"outputSchema": { "id": "gate", "version": "v1" },
|
|
7
|
+
"policy": { "id": "local", "version": "v1" },
|
|
8
|
+
"acceptance": { "id": "migration", "version": "v1" },
|
|
9
|
+
"implement": { "id": "migrate", "version": "v1" },
|
|
10
|
+
"review": { "id": "review", "version": "v1" }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "migration",
|
|
3
|
+
"options": {
|
|
4
|
+
"id": "migration-workflow",
|
|
5
|
+
"inputSchema": { "id": "discovery", "version": "v1" },
|
|
6
|
+
"outputSchema": { "id": "gate", "version": "v1" },
|
|
7
|
+
"policy": { "id": "local", "version": "v1" },
|
|
8
|
+
"acceptance": { "id": "migration", "version": "v1" },
|
|
9
|
+
"implement": { "id": "migrate", "version": "v1" },
|
|
10
|
+
"review": { "id": "review", "version": "v1" },
|
|
11
|
+
"population": { "id": "files", "version": "v1" }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "review",
|
|
3
|
+
"options": {
|
|
4
|
+
"id": "review-workflow",
|
|
5
|
+
"inputSchema": { "id": "input", "version": "v1" },
|
|
6
|
+
"outputSchema": { "id": "gate", "version": "v1" },
|
|
7
|
+
"policy": { "id": "local", "version": "v1" },
|
|
8
|
+
"acceptance": { "id": "migration", "version": "v1" },
|
|
9
|
+
"implement": { "id": "migrate", "version": "v1" },
|
|
10
|
+
"review": { "id": "review", "version": "v1" }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineWorkflow, schemaRef, executorRef, policyRef, acceptanceRef, populationRef, recipeRef, agent, map, repair, compose, gate, field, item, output, population, coverageFor, candidateFromBaseline, acceptedCandidates, outputCandidate, acceptedCandidate, coverageReceipt, type WorkflowOptions } from "forgeos/agent-fabric/workflows";
|
|
2
|
+
|
|
3
|
+
const repairPolicy = {
|
|
4
|
+
recipe: recipeRef("repair", "v2"), implement: executorRef("implement", "v2"), review: executorRef("review", "v2"), evidence: [executorRef("capture", "v2")],
|
|
5
|
+
entryMode: "assess-first", maxRepairRounds: 3, maxAssessmentAttempts: 6, maxInfrastructureAttempts: 2,
|
|
6
|
+
progressPolicy: { unchangedCandidateRounds: 2, repeatedFindingsRounds: 2 }
|
|
7
|
+
} as const;
|
|
8
|
+
const itemRepair = {
|
|
9
|
+
...repairPolicy, evidence: [executorRef("capture", "v2")], assessmentScope: "item", checks: ["local-check@v2"], initialCandidate: candidateFromBaseline(item()), writeScope: field(item(), "allowedPaths")
|
|
10
|
+
} satisfies WorkflowOptions["repair"];
|
|
11
|
+
|
|
12
|
+
export default defineWorkflow({
|
|
13
|
+
id: "ui-audit", version: 1, operatorVersion: 2, mode: "candidate", inputSchema: schemaRef("any", "v2"), outputSchema: schemaRef("any", "v2"),
|
|
14
|
+
policy: policyRef("local", "v2"), acceptance: acceptanceRef("ui", "v2"), population: populationRef("pages", "v2"),
|
|
15
|
+
steps: [
|
|
16
|
+
agent("discover", { executor: executorRef("discover", "v2"), input: {} }),
|
|
17
|
+
map("pages", {
|
|
18
|
+
items: field(output("discover"), "items"), key: field(item(), "id"), coverage: coverageFor(population(), output("discover")), completion: "all-required", concurrency: 4,
|
|
19
|
+
body: repair("page", itemRepair)
|
|
20
|
+
}),
|
|
21
|
+
compose("integrated", { candidates: acceptedCandidates("pages"), onConflict: "needs-resolution" }),
|
|
22
|
+
repair("final", {
|
|
23
|
+
...repairPolicy, evidence: [executorRef("capture", "v2")], assessmentScope: "final", checks: ["global-check@v2"],
|
|
24
|
+
initialCandidate: outputCandidate("integrated"), writeScope: ["src"]
|
|
25
|
+
}),
|
|
26
|
+
gate("ready", { candidate: acceptedCandidate("final"), coverage: coverageReceipt("pages") })
|
|
27
|
+
], result: output("ready")
|
|
28
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Deterministic fixture worker. Does not call an LLM or make network requests.
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { spawnSync } = require('node:child_process');
|
|
5
|
+
const { pathToFileURL } = require('node:url');
|
|
6
|
+
const role = process.argv[2];
|
|
7
|
+
const data = JSON.parse(fs.readFileSync(process.env.FORGE_PROGRAM_INPUT_PATH, 'utf8'));
|
|
8
|
+
const context = data.assessmentContext;
|
|
9
|
+
const inventory = () => fs.readdirSync('src').filter(name => name.endsWith('.html')).sort().map(name => ({ id: 'src/' + name, allowedPaths: ['src/' + name] }));
|
|
10
|
+
const items = data.workItem ? [data.workItem] : inventory();
|
|
11
|
+
const invalid = () => items.filter(item => !fs.readFileSync(item.id, 'utf8').includes('data-fixture="good"'));
|
|
12
|
+
let result;
|
|
13
|
+
if (role === 'discover') result = { items: inventory() };
|
|
14
|
+
else if (role === 'implement') {
|
|
15
|
+
for (const item of invalid()) fs.writeFileSync(item.id, fs.readFileSync(item.id, 'utf8').replace('data-fixture="bad"', 'data-fixture="good"'));
|
|
16
|
+
result = { summary: 'Repaired known fixture attributes' };
|
|
17
|
+
} else if (role === 'review') {
|
|
18
|
+
const findings = invalid().map(item => ({ itemKey: item.id, reason: 'Known deterministic fixture defect' }));
|
|
19
|
+
result = { verdict: findings.length ? 'changes_requested' : 'approved', findings, coveredObligationIds: context.obligationIds };
|
|
20
|
+
} else if (role === 'local-check' || role === 'global-check') result = { passed: invalid().length === 0 };
|
|
21
|
+
else if (role === 'capture') {
|
|
22
|
+
// A valid PNG fixture proves artifact transport/binding, not browser rendering.
|
|
23
|
+
const bytes = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aGfoAAAAASUVORK5CYII=', 'base64');
|
|
24
|
+
result = { evidenceArtifacts: items.map((item, index) => {
|
|
25
|
+
const name = 'capture-' + index + '.png', file = path.join(process.env.FORGE_PROGRAM_SCRATCH_DIRECTORY, name);
|
|
26
|
+
const browser = process.argv[3];
|
|
27
|
+
if (browser) {
|
|
28
|
+
const profile = path.join(process.env.FORGE_PROGRAM_SCRATCH_DIRECTORY, 'profile-' + index);
|
|
29
|
+
const rendered = spawnSync(browser, ['--headless', '--disable-gpu', '--disable-extensions', '--disable-background-networking', '--no-first-run', '--no-default-browser-check', '--hide-scrollbars', '--window-size=800,600', '--user-data-dir=' + profile, '--screenshot=' + file, pathToFileURL(path.resolve(item.id)).href], { windowsHide: true, timeout: 15000, stdio: 'pipe' });
|
|
30
|
+
if (rendered.error || rendered.status !== 0 || !fs.existsSync(file)) throw new Error('Headless browser capture failed');
|
|
31
|
+
} else fs.writeFileSync(file, bytes);
|
|
32
|
+
const image = fs.readFileSync(file), width = image.readUInt32BE(16), height = image.readUInt32BE(20);
|
|
33
|
+
return { path: name, itemKey: item.id, buildDigest: data.candidateDigest, environmentRef: context.environmentRef, mime: 'image/png', width, height, route: '/' + item.id, viewport: width + 'x' + height, state: browser ? 'local-browser-render' : 'deterministic-fixture' };
|
|
34
|
+
}) };
|
|
35
|
+
} else throw new Error('Unknown fixture role');
|
|
36
|
+
console.log(JSON.stringify(result));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "checkout-cases",
|
|
3
|
+
"version": "v2",
|
|
4
|
+
"members": ["checkout-desktop-ready", "checkout-mobile-error"],
|
|
5
|
+
"exclusions": [],
|
|
6
|
+
"baselineDigest": "OWNER_MUST_SUPPLY_CAPTURED_BASELINE_DIGEST",
|
|
7
|
+
"evidence": "Owner catalog of required checkout scenarios; extend explicitly for every route, viewport and state",
|
|
8
|
+
"allowNoWork": false,
|
|
9
|
+
"visualCases": [
|
|
10
|
+
{ "itemKey": "checkout-desktop-ready", "route": "/checkout", "viewport": "desktop", "state": "ready", "width": 1440, "height": 900 },
|
|
11
|
+
{ "itemKey": "checkout-mobile-error", "route": "/checkout", "viewport": "mobile", "state": "error", "width": 390, "height": 844 }
|
|
12
|
+
]
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgeos",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.72",
|
|
4
4
|
"description": "Agent-native application framework and compiler for building Forge apps without a mandatory dashboard.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"bin/",
|
|
13
13
|
"scripts/field-test-forgeos.mjs",
|
|
14
14
|
"scripts/install-agent-fabric-skill.mjs",
|
|
15
|
+
"scripts/summarize-fabric-benchmark.mjs",
|
|
15
16
|
".agents/skills/forge-agent-fabric/",
|
|
16
17
|
"docs/cair-protocol.md",
|
|
17
18
|
"docs/forge-protocol.md",
|
|
@@ -26,6 +27,11 @@
|
|
|
26
27
|
"docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_PLAN.md",
|
|
27
28
|
"docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_ADR.md",
|
|
28
29
|
"docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_EVIDENCE.md",
|
|
30
|
+
"docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS.md",
|
|
31
|
+
"docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS_EVIDENCE.md",
|
|
32
|
+
"docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_PLAN.md",
|
|
33
|
+
"docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_EVIDENCE.md",
|
|
34
|
+
"docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_REVIEW.md",
|
|
29
35
|
"examples/go-billing/",
|
|
30
36
|
"examples/agent-fabric-v2/",
|
|
31
37
|
"examples/java-billing/",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { summarizeBenchmarkReport, compareBenchmarkReports } from "../src/forge/agent-fabric/program-benchmark.ts";
|
|
3
|
+
const path = process.argv[2];
|
|
4
|
+
if (!path) throw new Error("Usage: node --import tsx scripts/summarize-fabric-benchmark.mjs REPORT.json [COUNTERPART.json]");
|
|
5
|
+
const report = JSON.parse(await readFile(path, "utf8"));
|
|
6
|
+
console.log(JSON.stringify(process.argv[3] ? compareBenchmarkReports(report, JSON.parse(await readFile(process.argv[3], "utf8"))) : summarizeBenchmarkReport(report), null, 2));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.
|
|
1
|
+
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.72","releaseId":"forgeos@0.1.0-alpha.72+unknown","schemaVersion":"0.1.0"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.72 input=4096e19589f57b9568accde0ae34aa0782c1598f54261e161e1ff5ac7cc857b9 content=d749e9d32ae5d270f7ba002ac58ee67da6d8e1a0dd704ad5e9a05a4decf4d59f
|
|
2
2
|
export const releaseManifest = {
|
|
3
3
|
"defaultProvider": "local",
|
|
4
4
|
"diagnostics": [],
|
|
@@ -19,7 +19,7 @@ export const releaseManifest = {
|
|
|
19
19
|
"custom"
|
|
20
20
|
],
|
|
21
21
|
"packageName": "forgeos",
|
|
22
|
-
"packageVersion": "0.1.0-alpha.
|
|
23
|
-
"releaseId": "forgeos@0.1.0-alpha.
|
|
22
|
+
"packageVersion": "0.1.0-alpha.72",
|
|
23
|
+
"releaseId": "forgeos@0.1.0-alpha.72+unknown",
|
|
24
24
|
"schemaVersion": "0.1.0"
|
|
25
25
|
} as const;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawn, execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import type { ProgramWorkerObservation } from "./program-observation.ts";
|
|
4
|
+
|
|
5
|
+
/** The CLI is cooperative: neither network denial nor process-tree termination is attested. */
|
|
6
|
+
export const claudeProgramCapabilities = Object.freeze({ adapter: "claude", protocol: "stream-json", minimumVersion: "2.1.259", isolation: "cooperative", network: "host", structuredOutput: true, threadResume: true, nativeImages: false, hardTokenLimit: false, processTreeTermination: false });
|
|
7
|
+
export function validateClaudeProgramVersion(output: string): string {
|
|
8
|
+
const match = /^\s*(\d+)\.(\d+)\.(\d+)(?:\s|$)/.exec(output);
|
|
9
|
+
if (!match) throw new Error("Claude CLI version identity unavailable");
|
|
10
|
+
const [major, minor, patch] = match.slice(1).map(Number);
|
|
11
|
+
if (![major, minor, patch].every(Number.isSafeInteger) || major! < 2 || major === 2 && (minor! < 1 || minor === 1 && patch! < 259)) throw new Error("Claude CLI requires version >=2.1.259 for restricted unattended protocol");
|
|
12
|
+
return `${major}.${minor}.${patch}`;
|
|
13
|
+
}
|
|
14
|
+
export interface ClaudeProgramInput {
|
|
15
|
+
binary: string; cwd: string; prompt: string; schema: unknown; model?: string; threadId?: string;
|
|
16
|
+
writable: boolean; signal: AbortSignal; environment: NodeJS.ProcessEnv;
|
|
17
|
+
onThread: (id: string) => Promise<void>;
|
|
18
|
+
onEvent: (event: Omit<ProgramWorkerObservation, "id" | "at">) => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export function claudeProgramArguments(input: Pick<ClaudeProgramInput, "schema" | "model" | "threadId" | "writable">): string[] {
|
|
21
|
+
const tools = input.writable ? "Read,Glob,Grep,Edit,Write" : "Read,Glob,Grep";
|
|
22
|
+
return ["--print", "--output-format", "stream-json", "--verbose", "--json-schema", JSON.stringify(input.schema),
|
|
23
|
+
"--restricted", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--disable-slash-commands",
|
|
24
|
+
"--settings", '{"disableAllHooks":true}', "--setting-sources", "", "--permission-mode", "dontAsk", "--permission-prompts", "none",
|
|
25
|
+
"--tools", tools, "--allowedTools", tools, ...(input.model ? ["--model", input.model] : []), ...(input.threadId ? ["--resume", input.threadId] : [])];
|
|
26
|
+
}
|
|
27
|
+
/** Parse only the documented operational envelope; never persist assistant/tool text. */
|
|
28
|
+
export function claudeProgramResult(event: Record<string, unknown>): { data: unknown; usage?: ProgramWorkerObservation["usage"] } {
|
|
29
|
+
if (event.type !== "result" || event.subtype !== "success" || event.is_error === true || !("structured_output" in event)) throw new Error("Claude structured completion not observed");
|
|
30
|
+
return { data: event.structured_output, usage: claudeProgramUsage(event) };
|
|
31
|
+
}
|
|
32
|
+
export function claudeProgramUsage(event: Record<string, unknown>): ProgramWorkerObservation["usage"] | undefined {
|
|
33
|
+
const raw = event.usage as Record<string, unknown> | undefined;
|
|
34
|
+
if (!raw) return undefined;
|
|
35
|
+
for (const key of ["input_tokens", "output_tokens"]) if (!Number.isSafeInteger(raw[key]) || (raw[key] as number) < 0) throw new Error("Invalid Claude usage counters");
|
|
36
|
+
for (const key of ["cache_read_input_tokens", "cache_creation_input_tokens"]) if (raw[key] !== undefined && (!Number.isSafeInteger(raw[key]) || (raw[key] as number) < 0)) throw new Error("Invalid Claude cache usage");
|
|
37
|
+
const cached = (raw.cache_read_input_tokens as number | undefined) ?? 0;
|
|
38
|
+
const inputTokens = (raw.input_tokens as number) + cached + ((raw.cache_creation_input_tokens as number | undefined) ?? 0);
|
|
39
|
+
if (!Number.isSafeInteger(inputTokens)) throw new Error("Claude aggregate usage exceeds safe counter bound");
|
|
40
|
+
return { input_tokens: inputTokens, cached_input_tokens: cached, output_tokens: raw.output_tokens as number };
|
|
41
|
+
}
|
|
42
|
+
export async function runClaudeProgramWorker(input: ClaudeProgramInput): Promise<{ data: unknown; usage?: ProgramWorkerObservation["usage"] }> {
|
|
43
|
+
if (process.env.FORGE_FABRIC_TEST_MODE === "1") throw new Error("Real Claude workers forbidden in deterministic test mode");
|
|
44
|
+
if (input.signal.aborted) throw new Error("Claude canceled before dispatch");
|
|
45
|
+
const identity = await promisify(execFile)(input.binary, ["--version"], { cwd: input.cwd, env: input.environment, windowsHide: true, timeout: 10000, maxBuffer: 4096, signal: input.signal }).catch(() => { throw new Error("Claude CLI version inspection failed; model dispatch refused"); });
|
|
46
|
+
const version = validateClaudeProgramVersion(identity.stdout);
|
|
47
|
+
await input.onEvent({ type: "adapter.version", source: "claude", metadata: { version, minimumVersion: claudeProgramCapabilities.minimumVersion } });
|
|
48
|
+
if (input.signal.aborted) throw new Error("Claude canceled before model dispatch");
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const child = spawn(input.binary, claudeProgramArguments(input), { cwd: input.cwd, env: input.environment, shell: false, windowsHide: true });
|
|
51
|
+
let pending = "", bytes = 0, count = 0, resultObserved = false, sessionId = input.threadId, final: ReturnType<typeof claudeProgramResult> | undefined;
|
|
52
|
+
let queue = Promise.resolve(), failure: unknown, settled = false, grace: ReturnType<typeof setTimeout> | undefined;
|
|
53
|
+
const finish = (error?: unknown) => { if (settled) return; settled = true; if (grace) clearTimeout(grace); input.signal.removeEventListener("abort", abort); if (error) reject(error); else resolve(final!); };
|
|
54
|
+
const abort = () => { child.kill(); grace ??= setTimeout(() => { child.stdout.destroy(); child.stderr.destroy(); child.unref(); finish(new Error("Claude termination/effects unknown; reconcile before retry")); }, 1000); };
|
|
55
|
+
input.signal.addEventListener("abort", abort, { once: true });
|
|
56
|
+
const consume = (line: string) => {
|
|
57
|
+
queue = queue.then(async () => {
|
|
58
|
+
if (settled || failure) return;
|
|
59
|
+
if (++count > 10000) throw new Error("Claude event budget exceeded");
|
|
60
|
+
const event = JSON.parse(line) as Record<string, unknown>;
|
|
61
|
+
if (typeof event.session_id === "string") {
|
|
62
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/.test(event.session_id) || sessionId && event.session_id !== sessionId) throw new Error("Claude session identity mismatch");
|
|
63
|
+
if (!sessionId) { sessionId = event.session_id; await input.onThread(sessionId); }
|
|
64
|
+
}
|
|
65
|
+
if (event.type === "result") {
|
|
66
|
+
if (settled) return;
|
|
67
|
+
if (resultObserved) throw new Error("Duplicate Claude result");
|
|
68
|
+
resultObserved = true;
|
|
69
|
+
const usage = claudeProgramUsage(event);
|
|
70
|
+
if (usage) await input.onEvent({ type: "usage", source: "claude", usage, semantics: "incremental" });
|
|
71
|
+
final = claudeProgramResult(event);
|
|
72
|
+
}
|
|
73
|
+
}).catch(error => { failure = error; abort(); });
|
|
74
|
+
};
|
|
75
|
+
child.stdout.setEncoding("utf8");
|
|
76
|
+
child.stdout.on("data", (chunk: string) => {
|
|
77
|
+
bytes += Buffer.byteLength(chunk);
|
|
78
|
+
if (bytes > 4 * 1024 * 1024) { failure = new Error("Claude output budget exceeded"); abort(); return; }
|
|
79
|
+
pending += chunk; let index: number;
|
|
80
|
+
while ((index = pending.indexOf("\n")) >= 0) { const line = pending.slice(0, index).trim(); pending = pending.slice(index + 1); if (line) consume(line); }
|
|
81
|
+
});
|
|
82
|
+
child.stderr.on("data", () => {});
|
|
83
|
+
child.once("error", () => finish(new Error("Claude executable unavailable or launch failed")));
|
|
84
|
+
child.once("close", async code => {
|
|
85
|
+
if (pending.trim()) consume(pending.trim()); await queue;
|
|
86
|
+
try {
|
|
87
|
+
await input.onEvent({ type: "process.closed", source: "claude", metadata: { exitCode: code, rootTerminationObserved: true, processTreeTermination: false } });
|
|
88
|
+
finish(failure || input.signal.aborted || code !== 0 || !final ? new Error("Claude completion/effects require reconciliation") : undefined);
|
|
89
|
+
} catch (error) { finish(error); }
|
|
90
|
+
});
|
|
91
|
+
child.stdin.on("error", () => { failure = new Error("Claude input delivery failed"); abort(); });
|
|
92
|
+
child.stdin.end(input.prompt);
|
|
93
|
+
if (input.signal.aborted) abort();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -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;
|
|
@@ -165,6 +166,7 @@ async function runCodexWorkerInternal(input: CodexWorkerInput, factory: CodexWor
|
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
export async function runCodexWorker(input: CodexWorkerInput, factory?: CodexWorkerFactory): Promise<CodexWorkerOutput> {
|
|
169
|
+
if (process.env.FORGE_FABRIC_TEST_MODE === "1" && !factory) fail("AF_CODEX_TEST_MODE", "Real LLM calls forbidden; provide a simulated SDK factory");
|
|
168
170
|
try { return await runCodexWorkerInternal(input, factory ?? ((options) => isolatedCodexClient(options, input.cwd, input.signal))); }
|
|
169
171
|
catch (error) {
|
|
170
172
|
if (error instanceof CodexWorkerError) throw error;
|
|
@@ -182,20 +184,26 @@ async function runTypedCodexWorkerInternal(input: CodexWorkerInput & { outputSch
|
|
|
182
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 } : {}) };
|
|
183
185
|
const thread = input.threadId ? client.resumeThread(input.threadId, threadOptions) : client.startThread(threadOptions);
|
|
184
186
|
let threadId = input.threadId ?? thread.id ?? "", finalMessage: string | undefined, completed = false, count = 0, observedUsage: CodexWorkerUsage | undefined;
|
|
185
|
-
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` });
|
|
186
192
|
for await (const event of stream.events) {
|
|
187
193
|
checkAbort(input.signal); if (++count > 10000) fail("AF_CODEX_EVENT_LIMIT", "Typed worker event budget exceeded");
|
|
188
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");
|
|
189
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 }); }
|
|
190
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; }
|
|
191
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 }); }
|
|
192
|
-
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" });
|
|
193
200
|
}
|
|
194
201
|
checkAbort(input.signal); if (!completed || !threadId || !finalMessage) fail("AF_CODEX_INCOMPLETE", "Typed worker completion not observed");
|
|
195
202
|
let data: unknown; try { data = JSON.parse(finalMessage); input.validateOutput(data); } catch { fail("AF_CODEX_REPORT", "Typed worker output schema invalid"); }
|
|
196
203
|
return { threadId, data, ...(observedUsage ? { usage: observedUsage } : {}) };
|
|
197
204
|
}
|
|
198
205
|
export async function runTypedCodexWorker(input: CodexWorkerInput & { outputSchema: unknown; validateOutput: (data: unknown) => void }, factory?: CodexWorkerFactory): Promise<{ threadId: string; data: unknown; usage?: CodexWorkerUsage }> {
|
|
206
|
+
if (process.env.FORGE_FABRIC_TEST_MODE === "1" && !factory) fail("AF_CODEX_TEST_MODE", "Real LLM calls forbidden; provide a simulated SDK factory");
|
|
199
207
|
try { return await runTypedCodexWorkerInternal(input, factory); }
|
|
200
208
|
catch (error) { if (error instanceof CodexWorkerError) throw error; fail("AF_CODEX_EXECUTION", "Typed SDK or persistence failed; reconcile before retry"); }
|
|
201
209
|
}
|
|
@@ -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; }
|