From 59afd295accc700b99ab2a723a588828c842c66d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 17:22:35 +0000 Subject: [PATCH 1/7] feat: add four agent packages + knowing-state-sdk + AtomDirective schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Packages added: - @factory/knowing-state-sdk — domain-agnostic Knowing-State Prosthesis SDK Provisional name; will move to @koales/knowing-state-sdk (DECISIONS N+3) - @factory/mediation-agent — per-repo Knowing-State Prosthesis DO Append-only event log, Bead graph flush, Verification-Process hosting SPEC-MEDIATION-AGENT-DO-001 v2.0 - @factory/commissioning-agent — per-repo spec-execution governance Worker Commission flow, polling, Hypothesis formation, Amendment proposal SPEC-COMMISSIONING-AGENT-001 - @factory/conducting-agent — execution substrate interface Gas City session dispatch, AtomDirective execution, trace reporting SPEC-CONDUCTING-AGENT-001 - @factory/architect-agent — Factory-wide governance singleton DO D1 patch governance, D2 CRP resolution, D3 vertical slicing, D4 pipeline config SPEC-ARCHITECT-AGENT-DO-001 Schema addition: - packages/schemas/src/atom-directive.ts — canonical AtomDirective schema Closes shared blocker across all four agent specs SPEC-CONDUCTING-AGENT-001 §1.2 All implementations are stubs with // TODO: markers aligned to spec sections. Two shared blockers closed: AtomDirective schema, Divergence severity policy (DECISIONS N+2). --- packages/architect-agent/package.json | 21 + .../architect-agent/src/architect-agent-do.ts | 301 ++++++++++++ packages/architect-agent/src/index.ts | 3 + packages/architect-agent/src/types.ts | 75 +++ packages/architect-agent/tsconfig.json | 9 + packages/commissioning-agent/package.json | 21 + .../src/commissioning-agent.ts | 169 +++++++ packages/commissioning-agent/src/index.ts | 3 + packages/commissioning-agent/src/types.ts | 69 +++ packages/commissioning-agent/tsconfig.json | 9 + packages/conducting-agent/package.json | 21 + .../conducting-agent/src/conducting-agent.ts | 198 ++++++++ packages/conducting-agent/src/index.ts | 3 + packages/conducting-agent/src/types.ts | 36 ++ packages/conducting-agent/tsconfig.json | 9 + packages/knowing-state-sdk/package.json | 18 + packages/knowing-state-sdk/src/index.ts | 2 + packages/knowing-state-sdk/src/sdk.ts | 58 +++ packages/knowing-state-sdk/src/types.ts | 30 ++ packages/knowing-state-sdk/tsconfig.json | 8 + packages/mediation-agent/package.json | 21 + packages/mediation-agent/src/index.ts | 3 + .../mediation-agent/src/mediation-agent-do.ts | 435 ++++++++++++++++++ packages/mediation-agent/src/types.ts | 137 ++++++ packages/mediation-agent/tsconfig.json | 9 + packages/schemas/src/atom-directive.ts | 62 +++ packages/schemas/src/index.ts | 1 + 27 files changed, 1731 insertions(+) create mode 100644 packages/architect-agent/package.json create mode 100644 packages/architect-agent/src/architect-agent-do.ts create mode 100644 packages/architect-agent/src/index.ts create mode 100644 packages/architect-agent/src/types.ts create mode 100644 packages/architect-agent/tsconfig.json create mode 100644 packages/commissioning-agent/package.json create mode 100644 packages/commissioning-agent/src/commissioning-agent.ts create mode 100644 packages/commissioning-agent/src/index.ts create mode 100644 packages/commissioning-agent/src/types.ts create mode 100644 packages/commissioning-agent/tsconfig.json create mode 100644 packages/conducting-agent/package.json create mode 100644 packages/conducting-agent/src/conducting-agent.ts create mode 100644 packages/conducting-agent/src/index.ts create mode 100644 packages/conducting-agent/src/types.ts create mode 100644 packages/conducting-agent/tsconfig.json create mode 100644 packages/knowing-state-sdk/package.json create mode 100644 packages/knowing-state-sdk/src/index.ts create mode 100644 packages/knowing-state-sdk/src/sdk.ts create mode 100644 packages/knowing-state-sdk/src/types.ts create mode 100644 packages/knowing-state-sdk/tsconfig.json create mode 100644 packages/mediation-agent/package.json create mode 100644 packages/mediation-agent/src/index.ts create mode 100644 packages/mediation-agent/src/mediation-agent-do.ts create mode 100644 packages/mediation-agent/src/types.ts create mode 100644 packages/mediation-agent/tsconfig.json create mode 100644 packages/schemas/src/atom-directive.ts diff --git a/packages/architect-agent/package.json b/packages/architect-agent/package.json new file mode 100644 index 00000000..e4e71f5f --- /dev/null +++ b/packages/architect-agent/package.json @@ -0,0 +1,21 @@ +{ + "name": "@factory/architect-agent", + "version": "0.1.0", + "description": "Architect Agent Durable Object — Factory-wide governance singleton. SPEC-ARCHITECT-AGENT-DO-001", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@factory/knowing-state-sdk": "workspace:*", + "@factory/schemas": "workspace:*", + "zod": "^3.23.0" + }, + "devDependencies": { + "typescript": "^5.4.0", + "@cloudflare/workers-types": "^4.0.0" + } +} diff --git a/packages/architect-agent/src/architect-agent-do.ts b/packages/architect-agent/src/architect-agent-do.ts new file mode 100644 index 00000000..809249ce --- /dev/null +++ b/packages/architect-agent/src/architect-agent-do.ts @@ -0,0 +1,301 @@ +// Architect Agent Durable Object +// SPEC-ARCHITECT-AGENT-DO-001 +// Singleton — do-key: architect-agent:factory + +import type { + FactoryState, + CRPItem, + PatchRecord, + PipelineConfig, + RepoSummary, + CRPFailureClass, +} from './types.js' + +export type Env = { + ARANGO_URL: string + ARANGO_DB: string + ARANGO_TOKEN: string + WEOPS_GATEWAY_URL: string + HARNESS_BRIDGE_URL: string + COMPILER_URL: string + ANOMALY_SCAN_INTERVAL_MS: string + PATCH_PROPAGATION_TIMEOUT_MS: string + CRP_RESOLUTION_TIMEOUT_MS: string +} + +const DEFAULT_ANOMALY_INTERVAL = 15 * 60 * 1000 // 15 min +const DEFAULT_PATCH_TIMEOUT = 30 * 60 * 1000 // 30 min +const DEFAULT_CRP_TIMEOUT = 10 * 60 * 1000 // 10 min + +export class ArchitectAgentDO implements DurableObject { + constructor( + private readonly state: DurableObjectState, + private readonly env: Env + ) {} + + async fetch(request: Request): Promise { + const url = new URL(request.url) + const method = request.method + + if (method === 'POST' && url.pathname === '/crp') return this.handleCRP(request) + if (method === 'POST' && url.pathname === '/patch') return this.handlePatch(request) + if (method === 'POST' && url.pathname === '/register-repo') return this.handleRegisterRepo(request) + if (method === 'POST' && url.pathname === '/deregister-repo') return this.handleDeregisterRepo(request) + if (method === 'POST' && url.pathname === '/pipeline-config-auth') return this.handlePipelineConfigAuth(request) + if (method === 'POST' && url.pathname === '/override') return this.handleOverride(request) + if (method === 'GET' && url.pathname === '/health') return this.handleHealth() + if (method === 'GET' && url.pathname === '/pipeline-config') return this.handleGetPipelineConfig() + + return new Response('Not found', { status: 404 }) + } + + async alarm(): Promise { + await this.runAnomalyScan() + await this.checkPatchPropagation() + // Re-arm + await this.state.storage.setAlarm( + Date.now() + Number(this.env.ANOMALY_SCAN_INTERVAL_MS ?? DEFAULT_ANOMALY_INTERVAL) + ) + } + + // ── D2: CRP Resolution ────────────────────────────────────────────────── + + private async handleCRP(request: Request): Promise { + const body = await request.json() as { + repoId: string + amendmentId: string + coherenceVerdictDetail: string + hypothesisId: string + divergenceIds: string[] + } + + const crpId = `CRP-${body.repoId}-${Date.now()}` + const item: CRPItem = { + crpId, + repoId: body.repoId, + amendmentId: body.amendmentId, + coherenceVerdict: body.coherenceVerdictDetail, + status: 'pending', + receivedAt: new Date().toISOString(), + } + + // Add to CRP queue + const queue = (await this.state.storage.get('crp:queue')) ?? [] + queue.push(item) + await this.state.storage.put('crp:queue', queue) + + // Attempt synchronous resolution + const failureClass = await this.classifyCRPFailure(body.coherenceVerdictDetail) + const resolved = await this.resolveCRP(crpId, failureClass, body) + + return Response.json({ crpId, status: resolved ? 'resolved' : 'in-resolution' }) + } + + private async classifyCRPFailure(verdictDetail: string): Promise { + if (verdictDetail.includes('schema') || verdictDetail.includes('unknown type')) return 'SCHEMA_VIOLATION' + if (verdictDetail.includes('invariant') || verdictDetail.includes('conflict')) return 'INVARIANT_CONFLICT' + if (verdictDetail.includes('coverage') || verdictDetail.includes('detector')) return 'COVERAGE_GAP' + if (verdictDetail.includes('lineage') || verdictDetail.includes('edge')) return 'LINEAGE_BREAK' + return 'UNKNOWN' + } + + private async resolveCRP( + crpId: string, + failureClass: CRPFailureClass, + context: { repoId: string; amendmentId: string; coherenceVerdictDetail: string } + ): Promise { + // TODO: implement resolution paths per SPEC §2.2 + // SCHEMA_VIOLATION → emit corrected AMD-* diff + // INVARIANT_CONFLICT → check cross-repo; open Patch if so + // COVERAGE_GAP → emit missing DetectorSpec + // LINEAGE_BREAK → reconstruct missing edge + // UNKNOWN → escalate to We-layer + if (failureClass === 'UNKNOWN') { + await this.escalateToWeLayer('CRPFail', { crpId, ...context }) + return false + } + return false // TODO: implement + } + + // ── D1: Patch Governance ──────────────────────────────────────────────── + + private async handlePatch(request: Request): Promise { + const body = await request.json() as { + changedArtifactId: string + changeDescription: string + authorizedBy: string + urgency: 'normal' | 'emergency' + } + + const patchId = `PATCH-${Date.now()}` + + // TODO: AQL traversal to find affected repos + // TODO: Coherence Verification per repo + // TODO: propagate to Commissioning Agents + + const patch: PatchRecord = { + patchId, + trigger: body.changedArtifactId, + affectedRepoIds: [], // populated after AQL traversal + appliedToRepoIds: [], + pendingRepoIds: [], + status: 'propagating', + issuedAt: new Date().toISOString(), + } + + const registry = (await this.state.storage.get('patches:active')) ?? [] + registry.push(patch) + await this.state.storage.put('patches:active', registry) + + return Response.json({ patchId, affectedRepoCount: 0, status: 'propagating' }) + } + + // ── D4: Pipeline Config Auth ──────────────────────────────────────────── + + private async handlePipelineConfigAuth(request: Request): Promise { + const body = await request.json() as { + proposedConfigId: string + affectedLiveRepoIds: string[] + authorizedBy: string + dispositionEventId: string + } + + // TODO: fetch PIPELINE-CONFIG-* from ArangoDB + // TODO: apply to DO hot storage + // TODO: notify harness-bridge of updated routing + + return Response.json({ status: 'applied', configId: body.proposedConfigId }) + } + + // ── Override ─────────────────────────────────────────────────────────── + + private async handleOverride(request: Request): Promise { + const body = await request.json() as { action: string; authorizedBy: string } + + // TODO: validate elevated token scope (we-layer:override) + + const factoryState = await this.factoryState() + factoryState.lifecycleState = body.action === 'force-suspend' ? 'EMERGENCY_SUSPEND' : 'ACTIVE' + await this.state.storage.put('factory:state', factoryState) + + return Response.json({ status: 'override-applied' }) + } + + // ── Register / Deregister ────────────────────────────────────────────── + + private async handleRegisterRepo(request: Request): Promise { + const body = await request.json() as { repoId: string; commissioningAgentUrl: string; mediationAgentDoKey: string } + + const state = await this.factoryState() + const exists = state.activeRepos.find(r => r.repoId === body.repoId) + if (!exists) { + state.activeRepos.push({ + repoId: body.repoId, + commissioningAgentUrl: body.commissioningAgentUrl, + mediationAgentDoKey: body.mediationAgentDoKey, + lastHealthPollAt: new Date().toISOString(), + healthStatus: 'unknown', + activeBlockingDivergences: 0, + pendingCrpCount: 0, + }) + await this.state.storage.put('factory:state', state) + } + + return Response.json({ status: 'registered' }) + } + + private async handleDeregisterRepo(request: Request): Promise { + const body = await request.json() as { repoId: string } + const state = await this.factoryState() + state.activeRepos = state.activeRepos.filter(r => r.repoId !== body.repoId) + await this.state.storage.put('factory:state', state) + return Response.json({ status: 'deregistered' }) + } + + // ── Health / Pipeline config ─────────────────────────────────────────── + + private async handleHealth(): Promise { + const state = await this.factoryState() + const crpQueue = (await this.state.storage.get<{ items: { status: string }[] }>('crp:queue')) ?? { items: [] } + const patches = (await this.state.storage.get('patches:active')) ?? [] + + return Response.json({ + factoryLifecycleState: state.lifecycleState, + activeRepoCount: state.activeRepos.length, + repoHealthBreakdown: { + healthy: state.activeRepos.filter(r => r.healthStatus === 'healthy').length, + degraded: state.activeRepos.filter(r => r.healthStatus === 'degraded').length, + suspended: state.activeRepos.filter(r => r.healthStatus === 'suspended').length, + unknown: state.activeRepos.filter(r => r.healthStatus === 'unknown').length, + }, + openEscalationCount: 0, // TODO: read from ArangoDB + activePatchCount: patches.filter(p => p.status === 'propagating').length, + pendingCrpCount: crpQueue.items?.filter(i => i.status === 'pending').length ?? 0, + pipelineConfigId: state.pipelineConfig?.configId ?? 'default', + }) + } + + private async handleGetPipelineConfig(): Promise { + const state = await this.factoryState() + return Response.json(state.pipelineConfig) + } + + // ── Cross-repo anomaly scan (alarm) ──────────────────────────────────── + + private async runAnomalyScan(): Promise { + // TODO: AQL query for cross-repo failure patterns (SPEC §4) + // Pattern thresholds: + // pass failure rate > 15% across 3+ repos in 1h → D4 trigger + // Amendment Coherence failures > 5 in 1h → D2 triage + // Patch stalled > 30 min → D1 escalation check + } + + private async checkPatchPropagation(): Promise { + // TODO: check active patches for timeout; escalate if stalled + } + + // ── Helpers ───────────────────────────────────────────────────────────── + + private async factoryState(): Promise { + return (await this.state.storage.get('factory:state')) ?? { + activeRepos: [], + pipelineConfig: defaultPipelineConfig(), + lifecycleState: 'ACTIVE', + } + } + + private async escalateToWeLayer(type: string, evidence: unknown): Promise { + await fetch(`${this.env.WEOPS_GATEWAY_URL}/escalations`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + signalType: 'EscalationEvent', + sourceAgent: 'ArchitectAgentDO', + sourceId: 'factory', + escalationType: type, + evidence, + issuedAt: new Date().toISOString(), + }), + }) + } +} + +function defaultPipelineConfig(): PipelineConfig { + return { + configId: 'default', + passRouting: [], + gateThresholds: { + coherenceMinCoverage: 1.0, + fidelityMaxOpenBlockingDivergences: 0, + assuranceMaxDetectorStalenessHours: 24, + }, + verticalSlicePolicy: { + atomRetryIsolation: true, + maxAtomRetries: 3, + parallelSliceThreshold: 4, + dagDispatchEnabled: false, + }, + effectiveFrom: new Date().toISOString(), + reason: 'default', + } +} diff --git a/packages/architect-agent/src/index.ts b/packages/architect-agent/src/index.ts new file mode 100644 index 00000000..282ffe25 --- /dev/null +++ b/packages/architect-agent/src/index.ts @@ -0,0 +1,3 @@ +export { ArchitectAgentDO } from './architect-agent-do.js' +export type { Env } from './architect-agent-do.js' +export type { FactoryState, CRPItem, PatchRecord, PipelineConfig, VerticalSlicePolicy, RepoSummary, CRPFailureClass } from './types.js' diff --git a/packages/architect-agent/src/types.ts b/packages/architect-agent/src/types.ts new file mode 100644 index 00000000..77aa204e --- /dev/null +++ b/packages/architect-agent/src/types.ts @@ -0,0 +1,75 @@ +// Architect Agent DO — factory state and decision domain types +// SPEC-ARCHITECT-AGENT-DO-001 + +export type RepoSummary = { + repoId: string + commissioningAgentUrl: string + mediationAgentDoKey: string + lastHealthPollAt: string + healthStatus: 'healthy' | 'degraded' | 'suspended' | 'unknown' + activeBlockingDivergences: number + pendingCrpCount: number +} + +export type VerticalSlicePolicy = { + atomRetryIsolation: boolean + maxAtomRetries: number + parallelSliceThreshold: number + dagDispatchEnabled: boolean +} + +export type PassRoutingConfig = { + passId: string + model: 'gpt-5-5' | 'deepseek-flash' | 'claude-opus' | 'local' + fallback: 'gpt-5-5' | 'claude-opus' + maxRetries: number +} + +export type GateThresholdConfig = { + coherenceMinCoverage: number + fidelityMaxOpenBlockingDivergences: number + assuranceMaxDetectorStalenessHours: number +} + +export type PipelineConfig = { + configId: string + passRouting: PassRoutingConfig[] + gateThresholds: GateThresholdConfig + verticalSlicePolicy: VerticalSlicePolicy + effectiveFrom: string + reason: string +} + +export type FactoryState = { + activeRepos: RepoSummary[] + pipelineConfig: PipelineConfig + lifecycleState: 'ACTIVE' | 'EMERGENCY_SUSPEND' | 'MAINTENANCE' +} + +export type CRPItem = { + crpId: string + repoId: string + amendmentId: string + coherenceVerdict: string + status: 'pending' | 'in-resolution' | 'resolved' | 'escalated-to-we-layer' + receivedAt: string + resolvedAt?: string +} + +export type PatchRecord = { + patchId: string + trigger: string + affectedRepoIds: string[] + appliedToRepoIds: string[] + pendingRepoIds: string[] + status: 'propagating' | 'complete' | 'partial-failure' + issuedAt: string + completedAt?: string +} + +export type CRPFailureClass = + | 'SCHEMA_VIOLATION' + | 'INVARIANT_CONFLICT' + | 'COVERAGE_GAP' + | 'LINEAGE_BREAK' + | 'UNKNOWN' // fallback — escalates to We-layer by default diff --git a/packages/architect-agent/tsconfig.json b/packages/architect-agent/tsconfig.json new file mode 100644 index 00000000..911bc16c --- /dev/null +++ b/packages/architect-agent/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["@cloudflare/workers-types"] + }, + "include": ["src"] +} diff --git a/packages/commissioning-agent/package.json b/packages/commissioning-agent/package.json new file mode 100644 index 00000000..33821f12 --- /dev/null +++ b/packages/commissioning-agent/package.json @@ -0,0 +1,21 @@ +{ + "name": "@factory/commissioning-agent", + "version": "0.1.0", + "description": "Commissioning Agent — per-repo spec-execution governance loop. SPEC-COMMISSIONING-AGENT-001", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@factory/knowing-state-sdk": "workspace:*", + "@factory/schemas": "workspace:*", + "zod": "^3.23.0" + }, + "devDependencies": { + "typescript": "^5.4.0", + "@cloudflare/workers-types": "^4.0.0" + } +} diff --git a/packages/commissioning-agent/src/commissioning-agent.ts b/packages/commissioning-agent/src/commissioning-agent.ts new file mode 100644 index 00000000..c552d1a9 --- /dev/null +++ b/packages/commissioning-agent/src/commissioning-agent.ts @@ -0,0 +1,169 @@ +// Commissioning Agent — stateless CF Worker +// SPEC-COMMISSIONING-AGENT-001 +// One instance per repo. All governance state in ArangoDB. + +import type { CommissioningSignal, EscalationEvent } from './types.js' + +export type Env = { + ARANGO_URL: string + ARANGO_DB: string + ARANGO_TOKEN: string + MEDIATION_AGENT: DurableObjectNamespace + WEOPS_GATEWAY_URL: string + HARNESS_BRIDGE_URL: string + AUTO_SUSPEND_THRESHOLD_CYCLES: string + POLL_INTERVAL_ACTIVE_MS: string + POLL_INTERVAL_IDLE_MS: string +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url) + const method = request.method + + if (method === 'POST' && url.pathname === '/commission') return handleCommission(request, env) + if (method === 'POST' && url.pathname === '/resume') return handleResume(request, env) + if (method === 'POST' && url.pathname === '/override') return handleOverride(request, env) + if (method === 'GET' && url.pathname.startsWith('/health/')) return handleHealth(request, env) + + return new Response('Not found', { status: 404 }) + }, +} + +// ── R1: Commission flow ───────────────────────────────────────────────────── + +async function handleCommission(request: Request, env: Env): Promise { + const signal = await request.json() as CommissioningSignal + + // TODO: validate WeOpsDispositionToken (SPEC-WEOPS-GATEWAY-BOUNDARY-001 §4) + // TODO: verify Coherence Verdict on WG-* in ArangoDB + // TODO: produce ElucidationArtifact (A9 — Axiom A9) + // TODO: write CommissionRecord to ArangoDB + + // Commission the Mediation Agent DO + const doId = env.MEDIATION_AGENT.idFromName(`mediation-agent:${signal.repoId}`) + const doStub = env.MEDIATION_AGENT.get(doId) + + const doResponse = await doStub.fetch('https://do/commission', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + workGraphId: signal.workGraphId, + workGraphVersion: signal.workGraphVersion, + arangoLineageRefs: [signal.workGraphId], + }), + }) + + const result = await doResponse.json() as { status: string; policyBeadId?: string } + + if (result.status !== 'commissioned') { + // TODO: escalate to We-layer (CommissionFail) + return Response.json({ status: 'error', reason: result.status }, { status: 502 }) + } + + // TODO: write VCR to ArangoDB + // TODO: start polling loop (CF Workflow or Cron Trigger) + + return Response.json({ status: 'commissioned', commissionRecordId: `CMR-${signal.repoId}-${Date.now()}` }) +} + +// ── R5: Resume ────────────────────────────────────────────────────────────── + +async function handleResume(request: Request, env: Env): Promise { + const body = await request.json() as { repoId: string; newWorkGraphId?: string; newWorkGraphVersion?: string; authorizedBy: string; dispositionArtifactId: string } + + // Resume always triggers a new Commission flow + const commissionSignal: CommissioningSignal = { + signalType: 'CommissioningSignal', + repoId: body.repoId, + workGraphId: body.newWorkGraphId ?? '', + workGraphVersion: body.newWorkGraphVersion ?? '', + commissionedBy: body.authorizedBy, + dispositionEventId: body.dispositionArtifactId, + elucidationArtifactId: body.dispositionArtifactId, + issuedAt: new Date().toISOString(), + } + + const fakeRequest = new Request('https://internal/commission', { + method: 'POST', + body: JSON.stringify(commissionSignal), + }) + return handleCommission(fakeRequest, env) +} + +// ── Override (Architect Agent emergency) ─────────────────────────────────── + +async function handleOverride(request: Request, env: Env): Promise { + const body = await request.json() as { repoId: string; action: string; authorizedBy: string } + + // TODO: validate elevated WeOpsDispositionToken scope (we-layer:override) + + const doId = env.MEDIATION_AGENT.idFromName(`mediation-agent:${body.repoId}`) + const doStub = env.MEDIATION_AGENT.get(doId) + + if (body.action === 'force-suspend') { + await doStub.fetch('https://do/suspend', { method: 'POST', body: JSON.stringify(body) }) + } + + return Response.json({ status: 'override-applied' }) +} + +// ── Health ────────────────────────────────────────────────────────────────── + +async function handleHealth(request: Request, env: Env): Promise { + const repoId = new URL(request.url).pathname.split('/health/')[1] + + const doId = env.MEDIATION_AGENT.idFromName(`mediation-agent:${repoId}`) + const doStub = env.MEDIATION_AGENT.get(doId) + + const stateResponse = await doStub.fetch('https://do/state') + const state = await stateResponse.json() + + return Response.json(state) +} + +// ── R3: Hypothesis formation ──────────────────────────────────────────────── +// Invoked from polling loop (CF Workflow — not in this Worker) +// Model: Claude Opus via @factory/harness-bridge + +export async function formHypothesis( + divergenceId: string, + repoId: string, + env: Env +): Promise { + // TODO: read Divergence from ArangoDB + // TODO: call harness-bridge with Claude Opus for structured Hypothesis output + // TODO: write HYP-* artifact to ArangoDB with lineage edges + // TODO: if blocking: trigger proposeAmendment() + return `HYP-${repoId}-${Date.now()}` +} + +// ── R4: Amendment proposal ───────────────────────────────────────────────── +// Model: GPT-5.5 via @factory/harness-bridge + +export async function proposeAmendment( + hypothesisId: string, + repoId: string, + env: Env +): Promise { + // TODO: translate Hypothesis corrective response into WorkGraph diff + // TODO: write AMD-* artifact to ArangoDB + // TODO: submit to compiler for Coherence Verification + // TODO: on favorable: trigger commission flow with successor WG-* + // TODO: on unfavorable: escalate to We-layer + return `AMD-${repoId}-${Date.now()}` +} + +// ── R5: Escalation ───────────────────────────────────────────────────────── + +export async function escalateToWeLayer( + event: EscalationEvent, + env: Env +): Promise { + // TODO: write ESC-* to ArangoDB before send + await fetch(`${env.WEOPS_GATEWAY_URL}/escalations`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(event), + }) +} diff --git a/packages/commissioning-agent/src/index.ts b/packages/commissioning-agent/src/index.ts new file mode 100644 index 00000000..65dcedf7 --- /dev/null +++ b/packages/commissioning-agent/src/index.ts @@ -0,0 +1,3 @@ +export { default, formHypothesis, proposeAmendment, escalateToWeLayer } from './commissioning-agent.js' +export type { Env } from './commissioning-agent.js' +export type { CommissionRecord, ElucidationArtifact, VCR, CommissioningSignal, EscalationEvent } from './types.js' diff --git a/packages/commissioning-agent/src/types.ts b/packages/commissioning-agent/src/types.ts new file mode 100644 index 00000000..4fe180fd --- /dev/null +++ b/packages/commissioning-agent/src/types.ts @@ -0,0 +1,69 @@ +// Commissioning Agent — artifact and signal types +// SPEC-COMMISSIONING-AGENT-001 + +export type CommissionRecord = { + _key: string // CMR-{repoId}-{timestamp} + repoId: string + workGraphId: string + workGraphVersion: string + commissionedAt: string + mediationAgentDoKey: string + elucidationArtifactId: string + status: 'success' | 'error' + errorReason?: string + source: 'commissioning-agent' + explicitness: 'stated' +} + +export type ElucidationArtifact = { + _key: string // ELC-CMR-{repoId}-{timestamp} + dispositionEventType: 'commission' | 'amendment-adoption' | 'suspension' | 'resumption' + candidateSet: { workGraphVersions: string[] } + selectedOption: string + rejectedOptions: Array<{ workGraphVersion: string; rejectionReason: string }> + constraintsApplied: string[] + producedAt: string + producedBy: string + source: 'commissioning-agent' + explicitness: 'stated' +} + +export type VCR = { + _key: string + dispositionEventType: string + repoId: string + workGraphVersion?: string + verdict: 'favorable' | 'unfavorable' + verdictSource: 'coherence-verification' | 'fidelity-verification' | 'we-layer-override' + producedAt: string + linkedArtifacts: string[] + source: 'commissioning-agent' + explicitness: 'stated' +} + +export type CommissioningSignal = { + signalType: 'CommissioningSignal' + repoId: string + workGraphId: string + workGraphVersion: string + commissionedBy: string + dispositionEventId: string + elucidationArtifactId: string + issuedAt: string +} + +export type EscalationEvent = { + signalType: 'EscalationEvent' + escalationId: string + sourceAgent: 'CommissioningAgent' + sourceId: string + escalationType: 'AutoSuspend' | 'AmendmentCoherenceFail' | 'CommissionFail' + evidence: { + divergenceIds?: string[] + hypothesisId?: string + amendmentId?: string + coherenceVerdictDetail?: string + } + requestedAction: string + issuedAt: string +} diff --git a/packages/commissioning-agent/tsconfig.json b/packages/commissioning-agent/tsconfig.json new file mode 100644 index 00000000..911bc16c --- /dev/null +++ b/packages/commissioning-agent/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["@cloudflare/workers-types"] + }, + "include": ["src"] +} diff --git a/packages/conducting-agent/package.json b/packages/conducting-agent/package.json new file mode 100644 index 00000000..472ae7f5 --- /dev/null +++ b/packages/conducting-agent/package.json @@ -0,0 +1,21 @@ +{ + "name": "@factory/conducting-agent", + "version": "0.1.0", + "description": "Conducting Agent — execution substrate interface (Gas City + CF Sandboxes). SPEC-CONDUCTING-AGENT-001", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@factory/knowing-state-sdk": "workspace:*", + "@factory/schemas": "workspace:*", + "zod": "^3.23.0" + }, + "devDependencies": { + "typescript": "^5.4.0", + "@cloudflare/workers-types": "^4.0.0" + } +} diff --git a/packages/conducting-agent/src/conducting-agent.ts b/packages/conducting-agent/src/conducting-agent.ts new file mode 100644 index 00000000..51e6fec8 --- /dev/null +++ b/packages/conducting-agent/src/conducting-agent.ts @@ -0,0 +1,198 @@ +// Conducting Agent — stateless CF Worker +// SPEC-CONDUCTING-AGENT-001 +// No Factory ontology awareness. Receives AtomDirective, runs session, reports trace. + +import { AtomDirective } from '@factory/schemas' +import { SessionNotInitialized } from '@factory/knowing-state-sdk' +import type { GasCitySessionRequest, GasCitySessionResponse, ConductingAgentTraceFragment } from './types.js' + +export type Env = { + GAS_CITY_SUPERVISOR_URL: string + MEDIATION_AGENT: DurableObjectNamespace + SANDBOX_OUTPUT_BUCKET: R2Bucket + VERTICAL_SLICE_MAX_PARALLEL: string +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url) + + if (request.method === 'POST' && url.pathname === '/execute') { + return handleExecute(request, env) + } + + return new Response('Not found', { status: 404 }) + }, +} + +// ── Main execution handler ────────────────────────────────────────────────── + +async function handleExecute(request: Request, env: Env): Promise { + const body = await request.json() as { repoId: string; executionId: string; sessionId: string } + + // I2 — Retrieval enforcement: pull AtomDirective from Mediation Agent + const doId = env.MEDIATION_AGENT.idFromName(`mediation-agent:${body.repoId}`) + const doStub = env.MEDIATION_AGENT.get(doId) + + const dispatchResponse = await doStub.fetch('https://do/dispatch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ executionId: body.executionId, sessionId: body.sessionId }), + }) + + const dispatched = await dispatchResponse.json() as { status: string; atom?: unknown } + + if (dispatched.status === 'blocked') { + return Response.json({ status: 'blocked', reason: 'mediation-agent-not-active' }) + } + if (dispatched.status === 'complete') { + return Response.json({ status: 'complete' }) + } + + // Validate AtomDirective schema + const parseResult = AtomDirective.safeParse(dispatched.atom) + if (!parseResult.success) { + await reportTrace(doStub, { + executionId: body.executionId, + directiveId: 'unknown', + atomRef: 'unknown', + workGraphVersion: 'unknown', + repoId: body.repoId, + outcome: 'failure', + rawOutput: JSON.stringify(parseResult.error.issues), + durationMs: 0, + attemptNumber: 1, + producedAt: new Date().toISOString(), + }) + return Response.json({ status: 'error', reason: 'invalid-directive' }) + } + + const directive = parseResult.data + + // Execute with retry loop + const trace = await executeWithRetry(directive, body.executionId, body.repoId, env) + + // Report to Mediation Agent + await reportTrace(doStub, trace) + + return Response.json({ status: 'executed', outcome: trace.outcome }) +} + +// ── Execution loop with vertical slice retry ──────────────────────────────── + +async function executeWithRetry( + directive: AtomDirective, + executionId: string, + repoId: string, + env: Env +): Promise { + const { maxAttempts, backoffMs, isolatedRetry } = directive.retryPolicy + let lastTrace: ConductingAgentTraceFragment | null = null + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + if (attempt > 1) await sleep(backoffMs) + + const sessionResult = await runGasCitySession(directive, env) + + const rawOutput = sessionResult.stdout.slice(0, 4096) // post_execution.ts hook + const sandboxOutputRef = sessionResult.stdout.length > 4096 + ? await storeFullOutput(sessionResult.stdout, directive.directiveId, env) + : undefined + + const success = evaluateSuccessCondition(directive.successCondition, sessionResult) + const outcome = sessionResult.outcome === 'timeout' ? 'timeout' + : success ? 'success' + : 'failure' + + lastTrace = { + executionId, + directiveId: directive.directiveId, + atomRef: directive.atomRef, + workGraphVersion: directive.workGraphVersion, + repoId, + outcome, + rawOutput, + sandboxOutputRef, + durationMs: sessionResult.durationMs, + attemptNumber: attempt, + producedAt: new Date().toISOString(), + } + + if (outcome === 'success') return lastTrace + + // on_failure.ts hook: capture sandbox state + // TODO: capture filesystem diff, stderr, exit code + + if (!isolatedRetry || attempt >= maxAttempts) break + } + + return lastTrace! +} + +// ── Gas City session ──────────────────────────────────────────────────────── + +async function runGasCitySession( + directive: AtomDirective, + env: Env +): Promise { + const sessionReq: GasCitySessionRequest = { + repoId: directive.repoId, + workingDir: directive.workingDir, + memoryMb: directive.sandboxConfig.memoryMb, + envVars: directive.envVars, + permittedTools: directive.permittedTools, + instruction: directive.instruction, + timeoutMs: directive.timeoutMs, + } + + const response = await fetch(`${env.GAS_CITY_SUPERVISOR_URL}/session`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(sessionReq), + }) + + return response.json() as Promise +} + +// ── SuccessCondition evaluation ───────────────────────────────────────────── +// Deterministic — no LLM involvement (SPEC-CONDUCTING-AGENT-001 §2.1 step 5) + +function evaluateSuccessCondition( + condition: AtomDirective['successCondition'], + result: GasCitySessionResponse +): boolean { + switch (condition.type) { + case 'exit-code': + return result.exitCode === condition.expectedCode + case 'output-contains': + return result.stdout.includes(condition.substring) + case 'output-matches': + return new RegExp(condition.pattern).test(result.stdout) + case 'file-exists': + // TODO: query sandbox filesystem + return false + case 'composite': + return condition.all.every(c => evaluateSuccessCondition(c, result)) + } +} + +// ── Helpers ───────────────────────────────────────────────────────────────── + +async function reportTrace(doStub: DurableObjectStub, trace: ConductingAgentTraceFragment): Promise { + await doStub.fetch('https://do/trace', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(trace), + }) +} + +async function storeFullOutput(output: string, directiveId: string, env: Env): Promise { + const key = `sandbox-output/${directiveId}/${Date.now()}.txt` + await env.SANDBOX_OUTPUT_BUCKET.put(key, output) + // TODO: generate presigned URL with 24h TTL + return `r2://${key}` +} + +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) +} diff --git a/packages/conducting-agent/src/index.ts b/packages/conducting-agent/src/index.ts new file mode 100644 index 00000000..c4c44525 --- /dev/null +++ b/packages/conducting-agent/src/index.ts @@ -0,0 +1,3 @@ +export { default } from './conducting-agent.js' +export type { Env } from './conducting-agent.js' +export type { GasCitySessionRequest, GasCitySessionResponse, ConductingAgentTraceFragment } from './types.js' diff --git a/packages/conducting-agent/src/types.ts b/packages/conducting-agent/src/types.ts new file mode 100644 index 00000000..74f90a5c --- /dev/null +++ b/packages/conducting-agent/src/types.ts @@ -0,0 +1,36 @@ +// Conducting Agent — Gas City interface types +// SPEC-CONDUCTING-AGENT-001 §3, §4 + +export type GasCitySessionRequest = { + repoId: string + workingDir: string + memoryMb: number + envVars: Record + permittedTools: string[] + instruction: string + timeoutMs: number +} + +export type GasCitySessionResponse = { + sessionId: string + outcome: 'success' | 'failure' | 'timeout' + stdout: string + stderr: string + exitCode: number + durationMs: number +} + +export type ConductingAgentTraceFragment = { + executionId: string + directiveId: string + atomRef: string + workGraphVersion: string + repoId: string + outcome: 'success' | 'failure' | 'timeout' | 'cancelled' + rawOutput: string // truncated to 4KB + sandboxOutputRef?: string // presigned R2 URL, 24h TTL + durationMs: number + attemptNumber: number + cancelReason?: string + producedAt: string +} diff --git a/packages/conducting-agent/tsconfig.json b/packages/conducting-agent/tsconfig.json new file mode 100644 index 00000000..911bc16c --- /dev/null +++ b/packages/conducting-agent/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["@cloudflare/workers-types"] + }, + "include": ["src"] +} diff --git a/packages/knowing-state-sdk/package.json b/packages/knowing-state-sdk/package.json new file mode 100644 index 00000000..1e7533e1 --- /dev/null +++ b/packages/knowing-state-sdk/package.json @@ -0,0 +1,18 @@ +{ + "name": "@factory/knowing-state-sdk", + "version": "0.1.0", + "description": "Domain-agnostic Knowing-State Prosthesis SDK. Provisional name — will move to @koales/knowing-state-sdk when cross-product infrastructure decision is made.", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@factory/schemas": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.4.0" + } +} diff --git a/packages/knowing-state-sdk/src/index.ts b/packages/knowing-state-sdk/src/index.ts new file mode 100644 index 00000000..74c26765 --- /dev/null +++ b/packages/knowing-state-sdk/src/index.ts @@ -0,0 +1,2 @@ +export type { BeadId, BeadEnvelope, KnowingState, AmendmentBead } from './types.js' +export { KnowingStateSDK, SessionNotInitialized } from './sdk.js' diff --git a/packages/knowing-state-sdk/src/sdk.ts b/packages/knowing-state-sdk/src/sdk.ts new file mode 100644 index 00000000..cb74ff2d --- /dev/null +++ b/packages/knowing-state-sdk/src/sdk.ts @@ -0,0 +1,58 @@ +// Knowing-State Prosthesis SDK — generic interface +// SPEC-MEDIATION-AGENT-DO-001 §8, DECISIONS N+3 +// +// Type parameters: +// PolicyContent — domain-specific policy/specification content +// TrustContent — domain-specific trust/verdict state +// ExecutionContent — domain-specific execution record payload +// OutcomeContent — domain-specific outcome record payload +// +// Instantiations: +// Factory repo DO: Policy=ArchitectureDecisionBead, Trust=PatternTrustBead, +// Execution=CommitBead, Outcome=BuildOutcomeBead +// ComeFlow commerce: Policy=OrgPreferenceBead, Trust=VendorTrustBead, +// Execution=PurchaseBead, Outcome=OutcomeBead + +import type { BeadId, KnowingState, AmendmentBead } from './types.js' + +export class SessionNotInitialized extends Error { + constructor(message = 'retrieveKnowingState() must be called before execution') { + super(message) + this.name = 'SessionNotInitialized' + } +} + +export interface KnowingStateSDK< + PolicyContent, + TrustContent, + ExecutionContent, + OutcomeContent, +> { + /** + * I2 — Retrieval enforcement. + * Must be called before any execution write. + * Throws SessionNotInitialized if called without valid session context. + */ + retrieveKnowingState(category: string): Promise> + + /** + * Write an execution record. The executing agent is the primary writer. + * Returns the BeadId of the written ExecutionBead. + */ + writeExecutionBead(payload: ExecutionContent): Promise + + /** + * Write an outcome record. Closes the execution loop. + * Returns the BeadId of the written OutcomeBead. + */ + writeOutcomeBead( + executionBeadId: BeadId, + outcome: OutcomeContent + ): Promise + + /** + * Retrieve open amendments (proposed modifications to the governing policy). + * Consumed by the Commissioning Agent / governance layer. + */ + getOpenAmendments(): Promise[]> +} diff --git a/packages/knowing-state-sdk/src/types.ts b/packages/knowing-state-sdk/src/types.ts new file mode 100644 index 00000000..126356f1 --- /dev/null +++ b/packages/knowing-state-sdk/src/types.ts @@ -0,0 +1,30 @@ +// Knowing-State Prosthesis SDK — domain-agnostic types +// SPEC-MEDIATION-AGENT-DO-001 §8, DECISIONS N+3 + +export type BeadId = string + +export type BeadEnvelope = { + _key: string + beadType: string + createdAt: string + immutable: true + source: string + explicitness: 'stated' | 'inferred' +} + +export type KnowingState = { + policyBeadId: BeadId + trustContent: TrustContent + lifecycleState: string + coherenceVerdict: { + value: 'favorable' | 'unfavorable' | 'pending' + reason?: string + } +} + +export type AmendmentBead = BeadEnvelope & { + beadType: 'AmendmentBead' + proposedModification: PolicyContent + motivatedBy: BeadId + status: 'open' | 'adopted' | 'rejected' +} diff --git a/packages/knowing-state-sdk/tsconfig.json b/packages/knowing-state-sdk/tsconfig.json new file mode 100644 index 00000000..792172fb --- /dev/null +++ b/packages/knowing-state-sdk/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"] +} diff --git a/packages/mediation-agent/package.json b/packages/mediation-agent/package.json new file mode 100644 index 00000000..3a1e9b94 --- /dev/null +++ b/packages/mediation-agent/package.json @@ -0,0 +1,21 @@ +{ + "name": "@factory/mediation-agent", + "version": "0.1.0", + "description": "Mediation Agent Durable Object — per-repo Knowing-State Prosthesis. SPEC-MEDIATION-AGENT-DO-001 v2.0", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@factory/knowing-state-sdk": "workspace:*", + "@factory/schemas": "workspace:*", + "zod": "^3.23.0" + }, + "devDependencies": { + "typescript": "^5.4.0", + "@cloudflare/workers-types": "^4.0.0" + } +} diff --git a/packages/mediation-agent/src/index.ts b/packages/mediation-agent/src/index.ts new file mode 100644 index 00000000..7307be5f --- /dev/null +++ b/packages/mediation-agent/src/index.ts @@ -0,0 +1,3 @@ +export { MediationAgentDO } from './mediation-agent-do.js' +export type { Env } from './mediation-agent-do.js' +export type { RepoState, DOEvent, AgentLifecycleState, DetectorSpec, DetectorResult, Verdict } from './types.js' diff --git a/packages/mediation-agent/src/mediation-agent-do.ts b/packages/mediation-agent/src/mediation-agent-do.ts new file mode 100644 index 00000000..1939f628 --- /dev/null +++ b/packages/mediation-agent/src/mediation-agent-do.ts @@ -0,0 +1,435 @@ +// Mediation Agent Durable Object +// SPEC-MEDIATION-AGENT-DO-001 v2.0 +// One instance per repo — do-key: mediation-agent:{repoId} + +import type { + DOEvent, + RepoState, + LogMeta, + CommissionPayload, + TracePayload, + DivergencePayload, + LifecyclePayload, + VerdictPayload, + FlushPayload, + SnapshotPayload, + AgentLifecycleState, + DetectorResult, +} from './types.js' + +// Env bindings — see SPEC §10 +export type Env = { + ARANGO_URL: string + ARANGO_DB: string + ARANGO_TOKEN: string + COMMISSIONING_AGENT_URL: string + SANDBOX_OUTPUT_BUCKET: R2Bucket + STALENESS_THRESHOLD_HOURS: string + SNAPSHOT_INTERVAL_EVENTS: string + FLUSH_INTERVAL_MS: string + FLUSH_BUFFER_THRESHOLD: string +} + +const SHARD_SIZE = 100 +const DEFAULT_STALENESS_HOURS = 24 +const DEFAULT_SNAPSHOT_INTERVAL = 500 +const DEFAULT_FLUSH_THRESHOLD = 50 + +export class MediationAgentDO implements DurableObject { + constructor( + private readonly state: DurableObjectState, + private readonly env: Env + ) {} + + async fetch(request: Request): Promise { + const url = new URL(request.url) + const method = request.method + + if (method === 'POST' && url.pathname === '/commission') return this.handleCommission(request) + if (method === 'POST' && url.pathname === '/dispatch') return this.handleDispatch(request) + if (method === 'POST' && url.pathname === '/trace') return this.handleTrace(request) + if (method === 'GET' && url.pathname === '/state') return this.handleState() + if (method === 'POST' && url.pathname === '/suspend') return this.handleSuspend(request) + if (method === 'POST' && url.pathname === '/resume') return this.handleResume(request) + if (method === 'POST' && url.pathname === '/close-divergence') return this.handleCloseDivergence(request) + + return new Response('Not found', { status: 404 }) + } + + async alarm(): Promise { + const repoState = await this.reconstructState() + + // Staleness check — I3 enforcement + const staleness = Number(this.env.STALENESS_THRESHOLD_HOURS ?? DEFAULT_STALENESS_HOURS) + const lastCommission = new Date(repoState.lastCommissionAt).getTime() + const staleAt = lastCommission + staleness * 60 * 60 * 1000 + + if (Date.now() > staleAt && repoState.lifecycleState === 'ACTIVE') { + await this.appendEvent({ + type: 'LifecycleEvent', + payload: { + state: 'DEGRADED', + reason: 'staleness-alarm', + triggeredBy: 'alarm', + } satisfies LifecyclePayload, + }) + } + + // Trace flush + await this.flushToArango(repoState) + + // Re-arm alarm + await this.state.storage.setAlarm( + Date.now() + Number(this.env.FLUSH_INTERVAL_MS ?? 60_000) + ) + } + + // ── Handlers ───────────────────────────────────────────────────────────── + + private async handleCommission(request: Request): Promise { + const body = await request.json() as { + workGraphId: string + workGraphVersion: string + arangoLineageRefs: string[] + stalenessThresholdHours?: number + } + + // TODO: fetch WorkGraph from ArangoDB using body.arangoLineageRefs + // TODO: compile AtomDirective[] (SPEC-CONDUCTING-AGENT-001 §1.4) + // TODO: run Coherence Verification + + // Stub: emit LifecycleEvent + CommissionEvent + await this.appendEvent({ + type: 'VerdictEvent', + payload: { + verdictType: 'coherence', + value: 'favorable', + workGraphVersion: body.workGraphVersion, + } satisfies VerdictPayload, + }) + + await this.appendEvent({ + type: 'CommissionEvent' as const, + payload: { + workGraphId: body.workGraphId, + workGraphVersion: body.workGraphVersion, + policyBeadId: `BEAD-ARCH-${body.workGraphVersion}`, + atoms: [], // populated after compile step + detectorSpecs: [], // populated after compile step + agentsMd: '', // populated after compile step + sourceRefs: body.arangoLineageRefs, + committedBy: 'commissioning-agent', + stalenessThresholdHours: body.stalenessThresholdHours ?? DEFAULT_STALENESS_HOURS, + } satisfies CommissionPayload, + }) + + await this.appendEvent({ + type: 'LifecycleEvent', + payload: { + state: 'ACTIVE', + reason: 'commission-success', + triggeredBy: 'commission', + } satisfies LifecyclePayload, + }) + + // Arm staleness alarm + const threshold = (body.stalenessThresholdHours ?? DEFAULT_STALENESS_HOURS) * 60 * 60 * 1000 + await this.state.storage.setAlarm(Date.now() + threshold) + + return Response.json({ + status: 'commissioned', + policyBeadId: `BEAD-ARCH-${body.workGraphVersion}`, + workGraphVersion: body.workGraphVersion, + }) + } + + private async handleDispatch(request: Request): Promise { + const repoState = await this.reconstructState() + + // I2 — Retrieval enforcement + I4 — Fail-closed + if (repoState.lifecycleState !== 'ACTIVE') { + return Response.json({ status: 'blocked', lifecycleState: repoState.lifecycleState }) + } + if (repoState.coherenceVerdict.value !== 'favorable') { + return Response.json({ status: 'blocked', reason: 'coherence-unfavorable' }) + } + + const body = await request.json() as { executionId: string; sessionId: string; requestedAtomRef?: string } + + // TODO: write EngineerRoleBead for this session to ArangoDB + // TODO: DAG-aware atom selection from repoState.atoms + + const atom = repoState.atoms.find(a => !body.requestedAtomRef || a.atomRef === body.requestedAtomRef) + ?? repoState.atoms[0] + + if (!atom) { + return Response.json({ status: 'complete' }) + } + + return Response.json({ + status: 'dispatched', + atom, + policyBeadId: repoState.policyBeadId, + }) + } + + private async handleTrace(request: Request): Promise { + const body = await request.json() as TracePayload + + await this.appendEvent({ type: 'TraceEvent', payload: body }) + + // Run detectors synchronously + const repoState = await this.reconstructState() + const divergences: DivergencePayload[] = [] + + for (const spec of repoState.detectorSpecs) { + let fired = false + if (spec.pattern && new RegExp(spec.pattern).test(body.rawOutput)) fired = true + if (spec.exitCode !== undefined) { + const dr = body.detectorResults.find(r => r.detectorId === spec.detectorId) + if (dr?.fired) fired = true + } + + if (fired) { + const severity = spec.severity === 'critical' ? 'blocking' + : spec.severity === 'warning' ? 'advisory' + : 'informational' + + const div: DivergencePayload = { + divergenceId: `DIV-${body.atomRef}-${Date.now()}`, + atomRef: body.atomRef, + detectorId: spec.detectorId, + severity, + evidence: String(await this.currentSeq()), + } + + divergences.push(div) + await this.appendEvent({ type: 'DivergenceEvent', payload: div }) + + if (severity === 'blocking') { + await this.appendEvent({ + type: 'VerdictEvent', + payload: { + verdictType: 'fidelity', + value: 'unfavorable', + reason: `Blocking divergence: ${spec.detectorId}`, + workGraphVersion: repoState.workGraphVersion, + } satisfies VerdictPayload, + }) + } + } + } + + // Auto-flush check + const threshold = Number(this.env.FLUSH_BUFFER_THRESHOLD ?? DEFAULT_FLUSH_THRESHOLD) + const pendingCount = await this.pendingTraceCount() + if (pendingCount >= threshold) await this.flushToArango(repoState) + + return Response.json({ + status: 'recorded', + divergencesDetected: divergences.length, + blockingCount: divergences.filter(d => d.severity === 'blocking').length, + }) + } + + private async handleState(): Promise { + const s = await this.reconstructState() + const meta = await this.logMeta() + + return Response.json({ + lifecycleState: s.lifecycleState, + workGraphVersion: s.workGraphVersion, + policyBeadId: s.policyBeadId, + coherenceVerdict: s.coherenceVerdict, + fidelityVerdict: s.fidelityVerdict, + openDivergences: { + // TODO: classify from s.openDivergenceIds + blocking: 0, + advisory: 0, + informational: 0, + }, + eventLogDepth: meta.totalEvents, + lastFlushAt: '', // TODO: derive from FlushEvent + }) + } + + private async handleSuspend(request: Request): Promise { + await this.appendEvent({ + type: 'LifecycleEvent', + payload: { state: 'SUSPENDED', reason: 'explicit-suspend', triggeredBy: 'suspend' } satisfies LifecyclePayload, + }) + return Response.json({ status: 'suspended' }) + } + + private async handleResume(request: Request): Promise { + // Resume requires re-commission — handled by Commissioning Agent + return Response.json({ status: 'awaiting-commission' }) + } + + private async handleCloseDivergence(request: Request): Promise { + const body = await request.json() as { divergenceId: string; closedBy: string } + await this.appendEvent({ + type: 'DivergenceClosedEvent', + payload: { divergenceId: body.divergenceId, closedBy: body.closedBy, closedAt: new Date().toISOString() }, + }) + return Response.json({ status: 'closed' }) + } + + // ── Event log ───────────────────────────────────────────────────────────── + + private async appendEvent(event: Omit): Promise { + await this.state.storage.transaction(async txn => { + const meta = (await txn.get('log:meta')) ?? { currentShard: 0, totalEvents: 0, lastSnapshotSeq: 0 } + const seq = meta.totalEvents + const full: DOEvent = { seq, producedAt: new Date().toISOString(), ...event } + + const shardKey = `log:shard:${meta.currentShard}` + const shard = (await txn.get(shardKey)) ?? [] + shard.push(full) + + const newShard = shard.length >= SHARD_SIZE ? meta.currentShard + 1 : meta.currentShard + await txn.put(shardKey, shard) + await txn.put('log:meta', { ...meta, totalEvents: seq + 1, currentShard: newShard }) + }) + + // Write snapshot periodically + const meta = await this.logMeta() + const snapshotInterval = Number(this.env.SNAPSHOT_INTERVAL_EVENTS ?? DEFAULT_SNAPSHOT_INTERVAL) + if (meta.totalEvents % snapshotInterval === 0) await this.writeSnapshot() + } + + private async reconstructState(): Promise { + const meta = await this.logMeta() + + // Check for recent snapshot first + const snapshot = await this.state.storage.get('snapshot:latest') + + // Read last 200 events (configurable) + const events = await this.readRecentEvents(200) + + // Start from snapshot if available and recent + let state: RepoState = snapshot + ? this.stateFromSnapshot(snapshot) + : this.emptyState() + + // Apply events after snapshot seq + for (const event of events) { + if (snapshot && event.seq <= snapshot.atSeq) continue + state = this.applyEvent(state, event) + } + + return state + } + + private applyEvent(state: RepoState, event: DOEvent): RepoState { + switch (event.type) { + case 'CommissionEvent': { + const p = event.payload as CommissionPayload + return { ...state, workGraphVersion: p.workGraphVersion, policyBeadId: p.policyBeadId, atoms: p.atoms, detectorSpecs: p.detectorSpecs, agentsMd: p.agentsMd, lastCommissionAt: event.producedAt, stalenessThresholdHours: p.stalenessThresholdHours } + } + case 'LifecycleEvent': { + const p = event.payload as LifecyclePayload + return { ...state, lifecycleState: p.state } + } + case 'VerdictEvent': { + const p = event.payload as VerdictPayload + if (p.verdictType === 'coherence') return { ...state, coherenceVerdict: { value: p.value, reason: p.reason } } + return { ...state, fidelityVerdict: { value: p.value, reason: p.reason } } + } + case 'DivergenceEvent': { + const p = event.payload as DivergencePayload + return { ...state, openDivergenceIds: [...state.openDivergenceIds, p.divergenceId] } + } + case 'DivergenceClosedEvent': { + const p = event.payload as { divergenceId: string } + return { ...state, openDivergenceIds: state.openDivergenceIds.filter(id => id !== p.divergenceId) } + } + case 'FlushEvent': { + const p = event.payload as FlushPayload + return { ...state, lastFlushSeq: p.flushedSeqRange[1] } + } + default: return state + } + } + + private emptyState(): RepoState { + return { + lifecycleState: 'UNINITIALIZED', + workGraphVersion: '', + policyBeadId: '', + atoms: [], + detectorSpecs: [], + agentsMd: '', + coherenceVerdict: { value: 'pending' }, + fidelityVerdict: { value: 'pending' }, + openDivergenceIds: [], + lastCommissionAt: new Date(0).toISOString(), + stalenessThresholdHours: DEFAULT_STALENESS_HOURS, + lastFlushSeq: -1, + } + } + + private stateFromSnapshot(s: SnapshotPayload): RepoState { + return { + ...this.emptyState(), + lifecycleState: s.lifecycleState as AgentLifecycleState, + workGraphVersion: s.workGraphVersion, + openDivergenceIds: s.openDivergenceIds, + coherenceVerdict: { value: s.coherenceVerdict as 'favorable' | 'unfavorable' | 'pending' }, + fidelityVerdict: { value: s.fidelityVerdict as 'favorable' | 'unfavorable' | 'pending' }, + } + } + + private async writeSnapshot(): Promise { + const state = await this.reconstructState() + const meta = await this.logMeta() + const snapshot: SnapshotPayload = { + atSeq: meta.totalEvents - 1, + lifecycleState: state.lifecycleState, + workGraphVersion: state.workGraphVersion, + openDivergenceIds: state.openDivergenceIds, + coherenceVerdict: state.coherenceVerdict.value, + fidelityVerdict: state.fidelityVerdict.value, + } + await this.state.storage.put('snapshot:latest', snapshot) + await this.appendEvent({ type: 'SnapshotEvent', payload: snapshot }) + } + + private async readRecentEvents(n: number): Promise { + const meta = await this.logMeta() + const events: DOEvent[] = [] + let shard = meta.currentShard + + while (events.length < n && shard >= 0) { + const chunk = (await this.state.storage.get(`log:shard:${shard}`)) ?? [] + events.unshift(...chunk) + shard-- + } + + return events.slice(-n) + } + + private async logMeta(): Promise { + return (await this.state.storage.get('log:meta')) ?? { currentShard: 0, totalEvents: 0, lastSnapshotSeq: 0 } + } + + private async currentSeq(): Promise { + const meta = await this.logMeta() + return meta.totalEvents - 1 + } + + private async pendingTraceCount(): Promise { + const state = await this.reconstructState() + const meta = await this.logMeta() + return meta.totalEvents - state.lastFlushSeq - 1 + } + + // ── ArangoDB flush ──────────────────────────────────────────────────────── + + private async flushToArango(_state: RepoState): Promise { + // TODO: implement Bead flush (SPEC-MEDIATION-AGENT-DO-001 §5.2) + // CommitBead + BuildOutcomeBead + PatternTrustBead + AuditBead per pending TraceEvent + // Single ArangoDB multi-document transaction + // Append FlushEvent on success + } +} diff --git a/packages/mediation-agent/src/types.ts b/packages/mediation-agent/src/types.ts new file mode 100644 index 00000000..ea4edb7e --- /dev/null +++ b/packages/mediation-agent/src/types.ts @@ -0,0 +1,137 @@ +// Mediation Agent DO — event log types +// SPEC-MEDIATION-AGENT-DO-001 v2.0 §2.1 + +import type { AtomDirective } from '@factory/schemas' + +export type AgentLifecycleState = + | 'UNINITIALIZED' + | 'ACTIVE' + | 'SUSPENDED' + | 'DEGRADED' + | 'TERMINATED' + +export type DetectorSpec = { + detectorId: string // INV-* ref + pattern?: string // regex; type: 'pattern' + exitCode?: number // type: 'exit-code' + severity: 'critical' | 'warning' | 'info' +} + +export type DetectorResult = { + detectorId: string + fired: boolean + severity?: 'critical' | 'warning' | 'info' + matchedContent?: string +} + +export type Verdict = { + value: 'favorable' | 'unfavorable' | 'pending' + reason?: string +} + +// ── Event types ─────────────────────────────────────────────────────────── + +export type EventType = + | 'CommissionEvent' + | 'TraceEvent' + | 'DivergenceEvent' + | 'DivergenceClosedEvent' + | 'VerdictEvent' + | 'LifecycleEvent' + | 'FlushEvent' + | 'SnapshotEvent' + +export type DOEvent = { + seq: number + type: EventType + producedAt: string + payload: unknown +} + +export type CommissionPayload = { + workGraphId: string + workGraphVersion: string + policyBeadId: string + atoms: AtomDirective[] + detectorSpecs: DetectorSpec[] + agentsMd: string + sourceRefs: string[] + committedBy: string + stalenessThresholdHours: number +} + +export type TracePayload = { + executionId: string + directiveId: string + atomRef: string + outcome: 'success' | 'failure' | 'timeout' | 'cancelled' + rawOutput: string // truncated to 4KB + sandboxOutputRef?: string // presigned R2 URL + durationMs: number + attemptNumber: number + detectorResults: DetectorResult[] +} + +export type DivergencePayload = { + divergenceId: string // DIV-* ID + atomRef: string + detectorId: string + severity: 'blocking' | 'advisory' | 'informational' + evidence: string // TraceEvent seq ref +} + +export type DivergenceClosedPayload = { + divergenceId: string + closedBy: string + closedAt: string +} + +export type VerdictPayload = { + verdictType: 'coherence' | 'fidelity' + value: 'favorable' | 'unfavorable' | 'pending' + reason?: string + workGraphVersion: string +} + +export type LifecyclePayload = { + state: AgentLifecycleState + reason: string + triggeredBy: 'commission' | 'suspend' | 'resume' | 'alarm' | 'terminate' +} + +export type FlushPayload = { + flushedSeqRange: [number, number] + beadIdsWritten: string[] +} + +export type SnapshotPayload = { + atSeq: number + lifecycleState: AgentLifecycleState + workGraphVersion: string + openDivergenceIds: string[] + coherenceVerdict: string + fidelityVerdict: string +} + +// ── Reconstructed state (derived from event log tail) ───────────────────── + +export type RepoState = { + lifecycleState: AgentLifecycleState + workGraphVersion: string + policyBeadId: string + atoms: AtomDirective[] + detectorSpecs: DetectorSpec[] + agentsMd: string + coherenceVerdict: Verdict + fidelityVerdict: Verdict + openDivergenceIds: string[] + lastCommissionAt: string + stalenessThresholdHours: number + lastFlushSeq: number +} + +export type LogMeta = { + currentShard: number + totalEvents: number + lastSnapshotSeq: number +} diff --git a/packages/mediation-agent/tsconfig.json b/packages/mediation-agent/tsconfig.json new file mode 100644 index 00000000..911bc16c --- /dev/null +++ b/packages/mediation-agent/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "types": ["@cloudflare/workers-types"] + }, + "include": ["src"] +} diff --git a/packages/schemas/src/atom-directive.ts b/packages/schemas/src/atom-directive.ts new file mode 100644 index 00000000..ee2b60d5 --- /dev/null +++ b/packages/schemas/src/atom-directive.ts @@ -0,0 +1,62 @@ +// AtomDirective — canonical schema for the I-layer / execution-substrate boundary +// SPEC-CONDUCTING-AGENT-001 §1.2 — authoritative definition +// Referenced by: SPEC-MEDIATION-AGENT-DO-001, SPEC-COMMISSIONING-AGENT-001, +// SPEC-ARCHITECT-AGENT-DO-001 + +import { z } from 'zod' + +export const ToolPermission = z.enum([ + 'shell', // shell.schema.json tool set + 'git', // git.schema.json tool set + 'compiler', // compiler.schema.json tool set + 'read-only', // subset of shell: cat, ls, find, grep, head, tail, wc only +]) + +export const SuccessCondition: z.ZodType = z.lazy(() => + z.discriminatedUnion('type', [ + z.object({ type: z.literal('exit-code'), expectedCode: z.number().int().default(0) }), + z.object({ type: z.literal('output-contains'), substring: z.string().min(1) }), + z.object({ type: z.literal('output-matches'), pattern: z.string().min(1) }), + z.object({ type: z.literal('file-exists'), path: z.string().min(1) }), + z.object({ + type: z.literal('composite'), + all: z.array(SuccessCondition).min(2).max(8), // max depth 3 + }), + ]) +) + +export type SuccessConditionType = + | { type: 'exit-code'; expectedCode: number } + | { type: 'output-contains'; substring: string } + | { type: 'output-matches'; pattern: string } + | { type: 'file-exists'; path: string } + | { type: 'composite'; all: SuccessConditionType[] } + +export const RetryPolicy = z.object({ + maxAttempts: z.number().int().min(1).max(5).default(3), + backoffMs: z.number().int().min(0).default(1000), + isolatedRetry: z.boolean().default(true), +}) + +export const AtomDirective = z.object({ + directiveId: z.string().regex(/^DIR-[A-Z0-9]+-\d+$/), + atomRef: z.string().min(1), + workGraphVersion: z.string().min(1), + repoId: z.string().min(1), + instruction: z.string().min(10).max(2000), + workingDir: z.string().default('.'), + permittedTools: z.array(ToolPermission).min(1), + timeoutMs: z.number().int().min(1000).max(300000).default(60000), + successCondition: SuccessCondition, + retryPolicy: RetryPolicy, + dependsOn: z.array(z.string()).default([]), + envVars: z.record(z.string(), z.string()).default({}), + sandboxConfig: z.object({ + memoryMb: z.number().int().min(128).max(4096).default(512), + persistFilesystem: z.boolean().default(false), + }).default({}), +}) + +export type AtomDirective = z.infer +export type RetryPolicy = z.infer +export type ToolPermission = z.infer diff --git a/packages/schemas/src/index.ts b/packages/schemas/src/index.ts index bf154967..bd25d52d 100644 --- a/packages/schemas/src/index.ts +++ b/packages/schemas/src/index.ts @@ -19,3 +19,4 @@ export * from "./trust.js" export * from "./decision-state.js" export * from "./commit-triage.js" export * from "./sdlc.js" +export * from "./atom-directive.js" From a3be10bdda763b1a456771b72cf22ca06b3c2472 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 17:43:07 +0000 Subject: [PATCH 2/7] chore: update pnpm-lock.yaml for new agent packages --- pnpm-lock.yaml | 96 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8d03485..8a7923bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,6 +46,25 @@ importers: specifier: ^1.4.0 version: 1.6.1(@types/node@24.12.2) + packages/architect-agent: + dependencies: + '@factory/knowing-state-sdk': + specifier: workspace:* + version: link:../knowing-state-sdk + '@factory/schemas': + specifier: workspace:* + version: link:../schemas + zod: + specifier: ^3.23.0 + version: 3.25.76 + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.0.0 + version: 4.20260425.1 + typescript: + specifier: ^5.4.0 + version: 5.9.3 + packages/architecture-candidates: dependencies: '@factory/schemas': @@ -128,6 +147,25 @@ importers: specifier: ^1.4.0 version: 1.6.1(@types/node@24.12.2) + packages/commissioning-agent: + dependencies: + '@factory/knowing-state-sdk': + specifier: workspace:* + version: link:../knowing-state-sdk + '@factory/schemas': + specifier: workspace:* + version: link:../schemas + zod: + specifier: ^3.23.0 + version: 3.25.76 + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.0.0 + version: 4.20260425.1 + typescript: + specifier: ^5.4.0 + version: 5.9.3 + packages/compiler: dependencies: '@factory/coverage-gates': @@ -156,6 +194,25 @@ importers: specifier: ^1.4.0 version: 1.6.1(@types/node@20.19.39) + packages/conducting-agent: + dependencies: + '@factory/knowing-state-sdk': + specifier: workspace:* + version: link:../knowing-state-sdk + '@factory/schemas': + specifier: workspace:* + version: link:../schemas + zod: + specifier: ^3.23.0 + version: 3.25.76 + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.0.0 + version: 4.20260425.1 + typescript: + specifier: ^5.4.0 + version: 5.9.3 + packages/controlled-effectors: dependencies: '@factory/schemas': @@ -354,6 +411,16 @@ importers: specifier: ^5 version: 5.9.3 + packages/knowing-state-sdk: + dependencies: + '@factory/schemas': + specifier: workspace:* + version: link:../schemas + devDependencies: + typescript: + specifier: ^5.4.0 + version: 5.9.3 + packages/literate-tools: dependencies: '@factory/schemas': @@ -370,6 +437,25 @@ importers: specifier: ^1.6.1 version: 1.6.1(@types/node@24.12.2) + packages/mediation-agent: + dependencies: + '@factory/knowing-state-sdk': + specifier: workspace:* + version: link:../knowing-state-sdk + '@factory/schemas': + specifier: workspace:* + version: link:../schemas + zod: + specifier: ^3.23.0 + version: 3.25.76 + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.0.0 + version: 4.20260425.1 + typescript: + specifier: ^5.4.0 + version: 5.9.3 + packages/meta-governance: dependencies: '@factory/schemas': @@ -618,7 +704,7 @@ importers: version: link:../../packages/gdk-ai agents: specifier: 0.11.6 - version: 0.11.6(@babel/core@7.29.0)(@babel/runtime@7.29.2)(@cloudflare/codemode@0.3.4(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(ai@6.0.168(zod@3.25.76))(zod@3.25.76))(@cloudflare/workers-types@4.20260425.1)(ai@6.0.168(zod@3.25.76))(react@19.2.5)(rolldown@1.0.0-rc.18)(vite@5.4.21(@types/node@24.12.2))(zod@3.25.76) + version: 0.11.6(@babel/core@7.29.0)(@babel/runtime@7.29.2)(@cloudflare/codemode@0.3.4(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(ai@6.0.168(zod@3.25.76))(zod@3.25.76))(@cloudflare/workers-types@4.20260425.1)(ai@6.0.168(zod@3.25.76))(react@19.2.5)(rolldown@1.0.0-rc.18)(zod@3.25.76) devDependencies: '@cloudflare/workers-types': specifier: ^4.20260101.0 @@ -5015,14 +5101,13 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': optional: true - '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.2)(rolldown@1.0.0-rc.18)(vite@5.4.21(@types/node@24.12.2))': + '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.2)(rolldown@1.0.0-rc.18)': dependencies: '@babel/core': 7.29.0 picomatch: 4.0.4 rolldown: 1.0.0-rc.18 optionalDependencies: '@babel/runtime': 7.29.2 - vite: 5.4.21(@types/node@24.12.2) '@rolldown/pluginutils@1.0.0-rc.18': {} @@ -5573,12 +5658,12 @@ snapshots: agent-base@7.1.4: {} - agents@0.11.6(@babel/core@7.29.0)(@babel/runtime@7.29.2)(@cloudflare/codemode@0.3.4(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(ai@6.0.168(zod@3.25.76))(zod@3.25.76))(@cloudflare/workers-types@4.20260425.1)(ai@6.0.168(zod@3.25.76))(react@19.2.5)(rolldown@1.0.0-rc.18)(vite@5.4.21(@types/node@24.12.2))(zod@3.25.76): + agents@0.11.6(@babel/core@7.29.0)(@babel/runtime@7.29.2)(@cloudflare/codemode@0.3.4(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(ai@6.0.168(zod@3.25.76))(zod@3.25.76))(@cloudflare/workers-types@4.20260425.1)(ai@6.0.168(zod@3.25.76))(react@19.2.5)(rolldown@1.0.0-rc.18)(zod@3.25.76): dependencies: '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) '@cfworker/json-schema': 4.1.1 '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) - '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.2)(rolldown@1.0.0-rc.18)(vite@5.4.21(@types/node@24.12.2)) + '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.2)(rolldown@1.0.0-rc.18) ai: 6.0.168(zod@3.25.76) cron-schedule: 6.0.0 mimetext: 3.0.28 @@ -5590,7 +5675,6 @@ snapshots: zod: 3.25.76 optionalDependencies: '@cloudflare/codemode': 0.3.4(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(ai@6.0.168(zod@3.25.76))(zod@3.25.76) - vite: 5.4.21(@types/node@24.12.2) transitivePeerDependencies: - '@babel/core' - '@babel/plugin-transform-runtime' From 0eeb9559849c4ef3da017c9ad83ddca72219a1a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 17:45:25 +0000 Subject: [PATCH 3/7] fix: resolve SuccessCondition recursive Zod type error in atom-directive.ts Replace manual SuccessConditionType declaration with z.infer-based pattern. The ZodLazy wrapper's _input type was incompatible with the manually declared union because z.default() makes fields optional in input types. Using a base discriminated union + lazy recursive extension fixes the TS2322 error. --- packages/schemas/src/atom-directive.ts | 51 ++++++++++++++++++-------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/schemas/src/atom-directive.ts b/packages/schemas/src/atom-directive.ts index ee2b60d5..29444e89 100644 --- a/packages/schemas/src/atom-directive.ts +++ b/packages/schemas/src/atom-directive.ts @@ -12,26 +12,25 @@ export const ToolPermission = z.enum([ 'read-only', // subset of shell: cat, ls, find, grep, head, tail, wc only ]) +// Recursive type — use z.infer only; no manual type declaration +const BaseSuccessCondition = z.discriminatedUnion('type', [ + z.object({ type: z.literal('exit-code'), expectedCode: z.number().int().default(0) }), + z.object({ type: z.literal('output-contains'), substring: z.string().min(1) }), + z.object({ type: z.literal('output-matches'), pattern: z.string().min(1) }), + z.object({ type: z.literal('file-exists'), path: z.string().min(1) }), +]) + +type BaseSuccessCondition = z.infer + +export type SuccessConditionType = BaseSuccessCondition | { type: 'composite'; all: SuccessConditionType[] } + export const SuccessCondition: z.ZodType = z.lazy(() => - z.discriminatedUnion('type', [ - z.object({ type: z.literal('exit-code'), expectedCode: z.number().int().default(0) }), - z.object({ type: z.literal('output-contains'), substring: z.string().min(1) }), - z.object({ type: z.literal('output-matches'), pattern: z.string().min(1) }), - z.object({ type: z.literal('file-exists'), path: z.string().min(1) }), - z.object({ - type: z.literal('composite'), - all: z.array(SuccessCondition).min(2).max(8), // max depth 3 - }), + z.union([ + BaseSuccessCondition, + z.object({ type: z.literal('composite'), all: z.array(SuccessCondition).min(2).max(8) }), ]) ) -export type SuccessConditionType = - | { type: 'exit-code'; expectedCode: number } - | { type: 'output-contains'; substring: string } - | { type: 'output-matches'; pattern: string } - | { type: 'file-exists'; path: string } - | { type: 'composite'; all: SuccessConditionType[] } - export const RetryPolicy = z.object({ maxAttempts: z.number().int().min(1).max(5).default(3), backoffMs: z.number().int().min(0).default(1000), @@ -39,18 +38,38 @@ export const RetryPolicy = z.object({ }) export const AtomDirective = z.object({ + // ── Identity ──────────────────────────────────────────────────────────── directiveId: z.string().regex(/^DIR-[A-Z0-9]+-\d+$/), atomRef: z.string().min(1), workGraphVersion: z.string().min(1), repoId: z.string().min(1), + + // ── Execution instruction ──────────────────────────────────────────────── + // Must be procedural (sequence of steps), never conceptual. instruction: z.string().min(10).max(2000), workingDir: z.string().default('.'), + + // ── Tool permissions ───────────────────────────────────────────────────── + // Explicit allowlist. Any tool call outside this set is rejected. permittedTools: z.array(ToolPermission).min(1), + + // ── Timing ────────────────────────────────────────────────────────────── timeoutMs: z.number().int().min(1000).max(300000).default(60000), + + // ── Success definition ─────────────────────────────────────────────────── successCondition: SuccessCondition, + + // ── Retry policy ──────────────────────────────────────────────────────── retryPolicy: RetryPolicy, + + // ── DAG ordering ──────────────────────────────────────────────────────── + // directiveIds that must complete successfully before this one starts. dependsOn: z.array(z.string()).default([]), + + // ── Context injection ──────────────────────────────────────────────────── envVars: z.record(z.string(), z.string()).default({}), + + // ── Sandbox configuration ──────────────────────────────────────────────── sandboxConfig: z.object({ memoryMb: z.number().int().min(128).max(4096).default(512), persistFilesystem: z.boolean().default(false), From 33c5b32ae34b0af4da75c59f85564ccac96b2300 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 17:47:29 +0000 Subject: [PATCH 4/7] fix: remove z.default() from SuccessCondition variants to satisfy ZodType constraint z.default() makes fields optional in Zod input types (number | undefined) but the manual SuccessConditionType declaration requires them to be required. Removing z.default() from variant fields makes input and output types identical, satisfying the recursive ZodType constraint. Callers should provide explicit values (e.g. expectedCode: 0) rather than relying on defaults within SuccessCondition. --- packages/schemas/src/atom-directive.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/schemas/src/atom-directive.ts b/packages/schemas/src/atom-directive.ts index 29444e89..5d0b16ee 100644 --- a/packages/schemas/src/atom-directive.ts +++ b/packages/schemas/src/atom-directive.ts @@ -12,21 +12,21 @@ export const ToolPermission = z.enum([ 'read-only', // subset of shell: cat, ls, find, grep, head, tail, wc only ]) -// Recursive type — use z.infer only; no manual type declaration -const BaseSuccessCondition = z.discriminatedUnion('type', [ - z.object({ type: z.literal('exit-code'), expectedCode: z.number().int().default(0) }), - z.object({ type: z.literal('output-contains'), substring: z.string().min(1) }), - z.object({ type: z.literal('output-matches'), pattern: z.string().min(1) }), - z.object({ type: z.literal('file-exists'), path: z.string().min(1) }), -]) - -type BaseSuccessCondition = z.infer - -export type SuccessConditionType = BaseSuccessCondition | { type: 'composite'; all: SuccessConditionType[] } +// No z.default() inside SuccessCondition variants — keeps input/output types identical +// which allows the recursive ZodType constraint to be satisfied. +export type SuccessConditionType = + | { type: 'exit-code'; expectedCode: number } + | { type: 'output-contains'; substring: string } + | { type: 'output-matches'; pattern: string } + | { type: 'file-exists'; path: string } + | { type: 'composite'; all: SuccessConditionType[] } // max depth 3 export const SuccessCondition: z.ZodType = z.lazy(() => z.union([ - BaseSuccessCondition, + z.object({ type: z.literal('exit-code'), expectedCode: z.number().int() }), + z.object({ type: z.literal('output-contains'), substring: z.string().min(1) }), + z.object({ type: z.literal('output-matches'), pattern: z.string().min(1) }), + z.object({ type: z.literal('file-exists'), path: z.string().min(1) }), z.object({ type: z.literal('composite'), all: z.array(SuccessCondition).min(2).max(8) }), ]) ) From e769652ef66697ea72c1ac0878fee5c38747106b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 17:49:44 +0000 Subject: [PATCH 5/7] fix: use 'export type' for type re-exports (verbatimModuleSyntax) Repo tsconfig has verbatimModuleSyntax enabled. All type-only re-exports across the five new packages updated to use 'export type' syntax. --- packages/knowing-state-sdk/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/knowing-state-sdk/src/index.ts b/packages/knowing-state-sdk/src/index.ts index 74c26765..bfa27c6a 100644 --- a/packages/knowing-state-sdk/src/index.ts +++ b/packages/knowing-state-sdk/src/index.ts @@ -1,2 +1,3 @@ export type { BeadId, BeadEnvelope, KnowingState, AmendmentBead } from './types.js' -export { KnowingStateSDK, SessionNotInitialized } from './sdk.js' +export type { KnowingStateSDK } from './sdk.js' +export { SessionNotInitialized } from './sdk.js' From 274fa60d1b9e91afce110170a81ae424a7cf6d79 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 17:51:47 +0000 Subject: [PATCH 6/7] fix: exactOptionalPropertyTypes and null assertion in conducting-agent - Spread sandboxOutputRef conditionally to satisfy exactOptionalPropertyTypes - Replace lastTrace! non-null assertion with explicit guard --- packages/conducting-agent/src/conducting-agent.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/conducting-agent/src/conducting-agent.ts b/packages/conducting-agent/src/conducting-agent.ts index 51e6fec8..eb7144b5 100644 --- a/packages/conducting-agent/src/conducting-agent.ts +++ b/packages/conducting-agent/src/conducting-agent.ts @@ -95,7 +95,7 @@ async function executeWithRetry( const sessionResult = await runGasCitySession(directive, env) const rawOutput = sessionResult.stdout.slice(0, 4096) // post_execution.ts hook - const sandboxOutputRef = sessionResult.stdout.length > 4096 + const fullOutput = sessionResult.stdout.length > 4096 ? await storeFullOutput(sessionResult.stdout, directive.directiveId, env) : undefined @@ -112,7 +112,7 @@ async function executeWithRetry( repoId, outcome, rawOutput, - sandboxOutputRef, + ...(fullOutput !== undefined ? { sandboxOutputRef: fullOutput } : {}), durationMs: sessionResult.durationMs, attemptNumber: attempt, producedAt: new Date().toISOString(), @@ -126,7 +126,8 @@ async function executeWithRetry( if (!isolatedRetry || attempt >= maxAttempts) break } - return lastTrace! + if (!lastTrace) throw new Error('executeWithRetry: no trace produced — maxAttempts must be >= 1') + return lastTrace } // ── Gas City session ──────────────────────────────────────────────────────── From 31ef9239665325232562cacb95d67c3777b3774e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 17:53:43 +0000 Subject: [PATCH 7/7] fix: conditional reason spread in Verdict construction (exactOptionalPropertyTypes) --- packages/mediation-agent/src/mediation-agent-do.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/mediation-agent/src/mediation-agent-do.ts b/packages/mediation-agent/src/mediation-agent-do.ts index 1939f628..81d19a09 100644 --- a/packages/mediation-agent/src/mediation-agent-do.ts +++ b/packages/mediation-agent/src/mediation-agent-do.ts @@ -333,8 +333,9 @@ export class MediationAgentDO implements DurableObject { } case 'VerdictEvent': { const p = event.payload as VerdictPayload - if (p.verdictType === 'coherence') return { ...state, coherenceVerdict: { value: p.value, reason: p.reason } } - return { ...state, fidelityVerdict: { value: p.value, reason: p.reason } } + const verdict = p.reason !== undefined ? { value: p.value, reason: p.reason } : { value: p.value } + if (p.verdictType === 'coherence') return { ...state, coherenceVerdict: verdict } + return { ...state, fidelityVerdict: verdict } } case 'DivergenceEvent': { const p = event.payload as DivergencePayload