diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cba06d..1899aa1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,3 +15,64 @@ jobs: node-version: 22 - run: npm install - run: npm run check + + sibyl-profile: + runs-on: ubuntu-latest + env: + ENGRAM_SIBYL_DB: /tmp/engram-sibyl-demo.db + ENGRAM_SIBYL_TENANT: engram-ci-demo + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: npm install + - run: python -m pip install -r packages/sibyl/requirements.txt + - run: npm run build + - run: npm run test:sibyl + - name: Seed execution memory in process A + run: npm run demo:sibyl:seed + - name: Recall and change action in fresh process B + run: npm run demo:sibyl:recall + - name: Show no-memory control + run: npm run demo:sibyl:no-memory-control + - name: Seed accumulated provider experience in process C + run: npm run demo:sibyl:provider:seed + - name: Fresh urgent provider decision uses relationship memory + run: npm run demo:sibyl:provider:urgent + - name: Fresh routine provider decision emits Base settlement intent + env: + ENGRAM_BASE_ATLAS_ADDRESS: '0x1111111111111111111111111111111111111111' + ENGRAM_BASE_BEACON_ADDRESS: '0x2222222222222222222222222222222222222222' + ENGRAM_BASE_INTENT_OUT: /tmp/engram-base-routine-intent.json + run: npm run demo:sibyl:provider:routine + - name: Verify fresh decision Base intent artifact + run: >- + npx tsx -e "import { readFileSync } from 'node:fs'; + import { parseSerializedBaseSettlementIntent } from './packages/base-settlement/src/index.ts'; + const intent = parseSerializedBaseSettlementIntent(JSON.parse(readFileSync('/tmp/engram-base-routine-intent.json','utf8'))); + if (intent.providerId !== 'atlas') throw new Error('unexpected provider'); + if (intent.terms.authorizedPrepayAtomic !== 800000n) throw new Error('unexpected prepay'); + if (!intent.terms.requireMilestoneVerification) throw new Error('milestone verification not required'); + if (!intent.provenance.retrievalId) throw new Error('missing retrieval provenance');" + - name: Remove Sibyl runtime and prove fail-closed degradation + run: npm run test:sibyl:deletion + - name: Smoke exact evidence capture path + env: + ENGRAM_EVIDENCE_DIR: /tmp/engram-sibyl-evidence-smoke + run: npm run evidence:sibyl:capture + - name: Verify evidence manifest retains Base authority artifact + run: >- + npx tsx -e "import { readFileSync, existsSync } from 'node:fs'; + const p='/tmp/engram-sibyl-evidence-smoke/manifest.json'; + if (!existsSync(p)) throw new Error('manifest missing'); + const m=JSON.parse(readFileSync(p,'utf8')); + if (m.schema !== 'engram.sibyl-evidence-capture/v3') throw new Error('unexpected manifest schema'); + if (m.status !== 'LOCAL_PASS') throw new Error('capture did not pass'); + if (!m.baseSettlementIntent?.sha256) throw new Error('base intent digest missing'); + if (m.baseSettlementIntent?.terms?.authorizedPrepayAtomic !== '800000') throw new Error('base intent amount mismatch'); + if (!m.baseSettlementIntent?.provenance?.retrievalId) throw new Error('base retrieval provenance missing'); + if (!existsSync('/tmp/engram-sibyl-evidence-smoke/base-routine-intent.json')) throw new Error('base intent artifact missing');" diff --git a/.github/workflows/sibyl-evidence-capture.yml b/.github/workflows/sibyl-evidence-capture.yml new file mode 100644 index 0000000..11bc998 --- /dev/null +++ b/.github/workflows/sibyl-evidence-capture.yml @@ -0,0 +1,43 @@ +name: Sibyl Evidence Capture + +on: + workflow_dispatch: + +jobs: + capture: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: npm install + - run: python -m pip install -r packages/sibyl/requirements.txt + - run: npm run build + - name: Capture fresh Sibyl + Base-authority evidence bundle + run: npm run evidence:sibyl:capture + - name: Validate captured manifest before upload + run: >- + npx tsx -e "import { readdirSync, readFileSync, existsSync } from 'node:fs'; + const root='artifacts/sibyl-evidence'; + const dirs=readdirSync(root,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name); + if (dirs.length !== 1) throw new Error('expected exactly one evidence directory'); + const dir=root+'/'+dirs[0]; + const m=JSON.parse(readFileSync(dir+'/manifest.json','utf8')); + if (m.schema !== 'engram.sibyl-evidence-capture/v3') throw new Error('unexpected manifest schema'); + if (m.status !== 'LOCAL_PASS') throw new Error('capture failed'); + if (!m.baseSettlementIntent?.sha256) throw new Error('base intent digest missing'); + if (!m.baseSettlementIntent?.provenance?.retrievalId) throw new Error('base retrieval provenance missing'); + if (!existsSync(dir+'/'+m.baseSettlementIntent.file)) throw new Error('base intent artifact missing');" + - name: Upload evidence bundle + uses: actions/upload-artifact@v4 + with: + name: sibyl-evidence-${{ github.sha }} + path: artifacts/sibyl-evidence/ + if-no-files-found: error + retention-days: 30 diff --git a/.gitignore b/.gitignore index f6d427f..9344b66 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ coverage/ # Generated external evidence (uploaded as workflow artifacts, not committed) evidence/live/ evidence/aws/ +artifacts/sibyl-evidence/ # Logs and editor/OS noise *.log diff --git a/README.md b/README.md index 7392ecd..05714a4 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,120 @@ Engram is execution-memory infrastructure for autonomous systems. It preserves p The application or agent remains the decision authority. Engram does **not** choose the business action. +## Sibyl Labs Hackathon evaluated profile + +The `hackathon/sibyl-ebi` branch contains a dedicated evaluated profile for the Sibyl Labs Hackathon. Canonical Engram remains CockroachDB-backed; in this judged profile **Sibyl is the load-bearing execution-memory store and there is no Cockroach fallback for the decision-critical path**. + +The hackathon profile is designed around the deletion test: remove Sibyl and the cross-session memory required to reproduce the claimed behavior disappears. + +### Judge call map — write → read → influence → conflict → consequence → deletion + +| Judge question | Critical path | +|---|---| +| Where is Sibyl wired into Engram? | `packages/sibyl/src/runtime-store.ts` implements `EngramRuntimeStore`; `packages/sibyl/bridge.py` calls the public Sibyl Python SDK. | +| Where is decision-critical memory written? | `SibylRuntimeStore.persistMemory(...)` → bridge `put` → Sibyl WARM entity `operational_memory`. | +| Where is it read in a later session? | `SibylRuntimeStore.searchMemory(...)` → bridge `search_memories`; Engram Runtime then applies recall eligibility before exposure. | +| How is recall distinguished from influence? | `packages/runtime/src/runtime.ts`: `recall(...)` creates a memory-state digest; `recordDecision(...)` validates retrieval provenance and influence policy before recording `CHANGED_ACTION` / `CONSTRAINED_ACTION`. | +| What prevents search rank from silently resolving contradictory memories? | `tests/integration/sibyl-competing-memories.test.ts`: both contradictory memories remain recall-visible through Sibyl; unresolved influence is rejected until explicit `SUPERSEDES` evidence resolves the relevant side. | +| Where is the flagship relationship-memory policy? | `packages/scenarios/provider-continuity/src/index.ts`. | +| Where is the Sibyl integration pressure suite? | `tests/integration/sibyl-memory-loop.test.ts`, `tests/integration/sibyl-provider-continuity.test.ts`, and `tests/integration/sibyl-competing-memories.test.ts`. | +| How do I see process-boundary recall? | `npm run demo:sibyl:provider:seed`, terminate that process, then run `npm run demo:sibyl:provider:urgent` or `npm run demo:sibyl:provider:routine` against the same Sibyl DB/tenant. | +| How does remembered experience become economic authority on Base? | `packages/base-settlement/src/index.ts` converts the Engram provider decision into `engram.base-settlement-intent/v1`; `tests/integration/base-settlement-authority.test.ts` proves urgent recipient and routine prepayment deltas. | +| How is a claimed Base settlement checked against the decision? | `packages/base-settlement/src/evidence.ts` verifies RPC chain, optional expected payer, Circle USDC token, recipient, exact amount and success; `npm run base:settlement:verify` performs live receipt verification. | +| Where is the Virtuals evidence boundary? | `packages/virtuals-acp/` normalizes ACP job history into Engram observations; `npm run virtuals:acp:ingest` is the live-ingest path. No live ACP job is claimed yet. | +| How is load-bearing deletion tested? | `npm run test:sibyl:deletion`; it removes the configured Sibyl runtime and must report degradation with no fallback. | +| How do I reproduce the complete evidence bundle? | After installing the pinned Sibyl dependency, run `npm run evidence:sibyl:capture`. It uses a fresh Sibyl DB/tenant, separate processes for the behavioral phases, and emits a commit/timestamp/version-stamped `manifest.json` with SHA-256 integrity metadata. | + +### One-command evidence capture + +Install the evaluated Sibyl dependency, then capture the full pre-submission proof: + +```bash +python -m pip install -r packages/sibyl/requirements.txt +npm run evidence:sibyl:capture +``` + +The capture fails on a dirty source tree by default so the manifest's git SHA actually identifies the tested source. For non-submission diagnostics only, `ENGRAM_EVIDENCE_ALLOW_DIRTY=1` may override that guard. + +The generated evidence bundle binds: + +- exact UTC capture time and git SHA; +- repository URL and clean-tree state; +- Node, npm, Python and `sibyl-memory-client` versions; +- dependency-manifest SHA-256 digests; +- separate-process route and provider outputs; +- contradiction, expiry, tamper and deletion pressure results; +- the final Sibyl SQLite database digest; +- SHA-256 digests for retained stdout/stderr files. + +The manually dispatched `.github/workflows/sibyl-evidence-capture.yml` runs the same command on a clean GitHub-hosted runner and uploads the bundle as an artifact. A successful CI or local capture is still `LOCAL_PASS` unless the surrounding run qualifies for a stronger evidence state. + +### How memory made this possible + +The flagship profile models experiential continuity between a requester agent and service providers. + +Two prior requester-owned executions observe Atlas breaching an urgent `data_fetch` SLA. Engram admits a provenance-linked multi-execution `REPEATED_PATTERN` relationship memory and Sibyl persists it. In a fresh session: + +- without memory, the cheapest eligible provider is Atlas; +- for **urgent** work, recalled relationship memory changes delegation from Atlas to Beacon and records `CHANGED_ACTION`; +- for **routine** work, Atlas is not globally blacklisted: it remains selected, but prepayment falls from 50% to 10% and milestone verification is required, recorded as `CONSTRAINED_ACTION`. + +The claim is therefore not “Atlas has a low reputation.” It is: **this agent's attributable experience with Atlas changes the authority Atlas receives in the matching future context.** + +### Causal partner path — Virtuals → Sibyl → Base + +The evaluated partner architecture is deliberately relational rather than additive: + +```text +Virtuals/provider execution evidence + ↓ +Engram observation + ↓ +Sibyl relationship memory + ↓ +fresh Engram recall + decision + ↓ +Base settlement authority +``` + +Virtuals supplies real external economic-execution evidence when a live ACP job is eventually exercised. Sibyl is the load-bearing memory substrate. Base is the economic consequence layer downstream of the remembered decision. + +For Base local conformance: + +- urgent memory changes the settlement recipient from Atlas to Beacon; +- routine memory keeps Atlas but changes initial authorized prepayment from `4.000000 USDC` to `0.800000 USDC` and requires milestone verification; +- serialized settlement intents are schema-validated and internally amount-consistent; +- receipt verification fails on the wrong RPC chain, expected payer, token, recipient, amount, or reverted transaction. + +Run: + +```bash +npm run test:base +``` + +For an already executed Base Sepolia transaction, verification is non-custodial and read-only: + +```bash +ENGRAM_BASE_RPC_URL='' \ + npm run base:settlement:verify -- \ + --intent \ + --tx-hash <0x...> \ + --payer +``` + +**Evidence boundary:** Base and Virtuals are currently `LOCAL_CONFORMANCE_PASS / LIVE_UNVERIFIED`. No real Base Sepolia settlement and no authenticated ACP job are claimed by this pre-window branch. Local tests do not earn either partner multiplier. + +### Prior Work declaration + +Engram's protocol, runtime, execution-memory semantics, CockroachDB production profile, SDK/API/MCP surfaces, causal influence model, and earlier scenarios existed before the Sibyl Labs Hackathon build window. + +The Sibyl EBI adapter, pressure harness, provider-continuity work, Virtuals adapter and Base settlement-conformance path currently present on this branch were also implemented before the official **September 1–10, 2026** build window. If retained for submission, they must be declared as prior work rather than represented as hackathon-window implementation. Final submission evidence must identify the work performed during the official window and regenerate the required fresh-session/deletion/partner proof there. + +Current pre-window evidence is recorded under [`hackathon/sibyl/`](hackathon/sibyl/) and must not be confused with final submission evidence. + ## Governing invariant -Engram is complete only when a prior execution persisted in CockroachDB is retrieved under comparable future context, explicitly influences a later application/agent decision, causes an observable change from the memory-free baseline, and leaves enough provenance to reconstruct that relationship. +Engram is complete only when a prior execution persisted in its configured operational-memory substrate is retrieved under comparable future context, explicitly influences a later application/agent decision, causes an observable change from the memory-free baseline, and leaves enough provenance to reconstruct that relationship. ```text source execution @@ -66,6 +177,7 @@ The external multi-venue executor is **SIMULATED**. That boundary is intentional Engram is also tested outside the initial venue workload: +- **experiential provider continuity** — repeated requester-owned provider executions become bounded relationship posture that can change urgent delegation or constrain routine payment/verification authority; - **software deployment recovery** — prior migration failure/recovery changes a later comparable deployment strategy with a real memory-free control execution; - **autonomous coding regression** — prior reverted regression changes a later comparable coding methodology from patch-first to regression-test-first; - **incident recovery** — prior recovery that restored the primary service but caused a harmful secondary consequence changes the later mitigation sequence; @@ -138,170 +250,3 @@ DATABASE_URL='postgresql://...' npm run migrate ``` Run the credential-gated integration suite: - -```bash -DATABASE_URL='postgresql://...' npm run test:integration -``` - -CockroachDB is Engram's canonical operational-memory substrate. Application hot-path reads/writes use the PostgreSQL-compatible connection. CockroachDB Cloud Managed MCP is a separate read/introspection/provenance plane. - -Production retrieval is agent-scoped and cosine based. Successful vector ordering does **not** prove C-SPANN index use; the live verifier records the natural `EXPLAIN` plan and promotes that claim only when the optimizer actually selects the expected vector index. - -## TypeScript SDK - -Inside this monorepo, the canonical SDK barrel is `packages/sdk/src/index.ts` and exposes `Engram`, `httpTransport`, `runtimeTransport`, and the transport types: - -```ts -import { Engram, httpTransport } from "./packages/sdk/src/index.js"; - -const engram = new Engram(httpTransport({ - baseUrl: process.env.ENGRAM_API_URL!, - apiToken: process.env.ENGRAM_API_TOKEN, -})); - -const execution = await engram.startExecution({ - agentId: "deployment-agent", - workflowType: "deployment", - intent: "Deploy safely", - context: { service: "api" }, - constraints: {}, -}); - -const recall = await execution.recall({ query: "comparable prior failures" }); - -// The application/agent decides. Engram does not. -const decision = await application.decide({ memories: recall.candidates }); - -await execution.recordDecision({ - decisionType: "DEPLOYMENT_STRATEGY", - selectedAction: decision.action, - reasoningSummary: decision.summary, - influences: decision.memoryInfluences, -}); -``` - -The SDK is currently a monorepo package surface; publication as an installable `@engram/sdk` registry package is not claimed yet. - -## Python SDK - -```python -from engram import Engram - -client = Engram( - "https://YOUR_API", - api_token="SERVER_SIDE_TOKEN", -) - -execution = client.start_execution( - agentId="deployment-agent", - workflowType="deployment", - intent="Deploy safely", - context={"service": "api"}, - constraints={}, -) - -recall = execution.recall("comparable prior failures") -``` - -## HTTP API - -Core runtime routes: - -```text -POST /v1/executions -POST /v1/executions/{id}/recall -POST /v1/executions/{id}/decisions -POST /v1/executions/{id}/observations -POST /v1/executions/{id}/complete -GET /v1/executions/{id}/trace -``` - -Read-focused control-plane routes live under `/v1/control-plane/*`. - -`GET /health` and `POST /v1/demo/run` are intentionally public in the MVP. Every other `/v1/*` route requires `Authorization: Bearer $ENGRAM_API_TOKEN` and fails closed if the server token is not configured. - -The shared bearer token is an initial deployment guard, **not** production multi-tenant RBAC. Do not place it in a public frontend bundle. - -## Engram MCP vs CockroachDB Managed MCP - -These are intentionally different surfaces. - -**Engram MCP** exposes execution-memory semantics such as execution inspection, memory inspection, recall and influence explanation. - -**CockroachDB Cloud Managed MCP** is used as a read/introspection/provenance interface to the underlying CockroachDB data plane. - -Managed MCP is not Engram's transactional application database driver. - -## AWS deployment - -The SAM application packages successfully in CI. Local package proof: - -```bash -npm install -export PATH="$PWD/node_modules/.bin:$PATH" -sam build -``` - -The repository contains two manual external-proof workflows: - -- `.github/workflows/live-verification.yml` — CockroachDB + Bedrock + Managed MCP + natural C-SPANN plan evidence; -- `.github/workflows/aws-deploy-verification.yml` — SAM deploy plus deployed public/authenticated API exercise. - -Neither external workflow should be treated as successful merely because its code exists. See [`docs/deployment.md`](docs/deployment.md) for credentials, promotion rules and evidence artifacts. - -## Evidence status - -Engram keeps implementation/test evidence separate from live-cloud evidence. - -Current high-level boundary: - -| Area | Status | -|---|---| -| Protocol/runtime causal invariants | TESTED | -| SDK/API/MCP/adapters | TESTED | -| Memory policy/evaluation | TESTED | -| Stronger execution-memory scenarios | TESTED | -| SAM packaging | TESTED | -| External demo workload | SIMULATED | -| CockroachDB Cloud live persistence | UNVERIFIED until credentialed live proof | -| Bedrock Titan live invocation | UNVERIFIED until credentialed live proof | -| Managed MCP live provenance | UNVERIFIED until credentialed live proof | -| Natural C-SPANN index selection | UNVERIFIED until EXPLAIN proves it | -| AWS Lambda public deployment | UNVERIFIED until deploy-verification succeeds | - -The authoritative machine-readable claim ledger is [`evidence/claims.yaml`](evidence/claims.yaml). - -## Repository map - -```text -apps/web/ Control-plane UI -services/api/ Lambda/HTTP API -services/runtime/ Runtime composition -services/demo/ Deterministic causal demo -services/verification/ Credentialed external verifier -packages/core/ Protocol primitives -packages/episode/ ExecutionEpisode schema -packages/runtime/ Memory runtime -packages/policy/ Policy contracts/registry -packages/evaluation/ Memory evaluation semantics -packages/sdk/ TypeScript SDK -packages/python/ Python SDK -packages/mcp-server/ Engram semantic MCP -packages/adapters/ Framework adapters -packages/cockroach/ Cockroach persistence/vector runtime -packages/cockroach-mcp/ Managed MCP client -packages/bedrock/ Titan embeddings -packages/scenarios/ Domain acceptance workloads -db/migrations/ Ordered schema migrations -experiments/ Governed acceptance evidence -evidence/ Claim ledger and adversarial reviews -tests/ Unit/integration/conformance/E2E proofs -``` - -## What Engram is not - -Engram is not generic chat memory, a generic RAG wrapper, an autonomous adjudicator, or a replacement for an agent framework. It does not infer that retrieval equals influence and does not turn a later successful outcome into proof that a memory was beneficial. - -## License - -MIT. See [`LICENSE`](LICENSE). diff --git a/docs/frontend-modules/server-only.json b/docs/frontend-modules/server-only.json index 38249b7..8a5ae36 100644 --- a/docs/frontend-modules/server-only.json +++ b/docs/frontend-modules/server-only.json @@ -1,8 +1,9 @@ { "schemaVersion": "engram.server-only-module-registry/v1", - "updatedAt": "2026-08-18", + "updatedAt": "2026-08-29", "modules": [ { "modulePath": "packages/adapters", "reason": "Framework telemetry integration; frontend consumes resulting execution evidence through API surfaces." }, + { "modulePath": "packages/base-settlement", "reason": "Hackathon Base settlement-authority adapter derives decision-linked USDC intents and will own credentialed/testnet settlement verification; signer material must never enter browser code." }, { "modulePath": "packages/bedrock", "reason": "AWS Bedrock embedding client requires server-side credentials." }, { "modulePath": "packages/cockroach-mcp", "reason": "Cockroach Managed MCP credentials and database inspection remain server-side." }, { "modulePath": "packages/cockroach", "reason": "Canonical storage/database implementation; browsers must never connect directly to CockroachDB." }, @@ -14,7 +15,9 @@ { "modulePath": "packages/python", "reason": "Python client SDK; non-browser execution surface." }, { "modulePath": "packages/runtime", "reason": "Server-side semantic authority and runtime policy enforcement." }, { "modulePath": "packages/scenarios", "reason": "Internal experiment workloads and application fixtures, not product UI APIs." }, + { "modulePath": "packages/sibyl", "reason": "Hackathon runtime-store adapter launches the server-side Sibyl Python SDK and owns local durable memory access; browsers consume resulting traces through Engram API surfaces." }, { "modulePath": "packages/vertex", "reason": "Google Vertex AI embedding client uses Application Default Credentials and must remain server-side." }, + { "modulePath": "packages/virtuals-acp", "reason": "Virtuals ACP adapter handles agent-commerce job evidence and eventually credentialed CLI/testnet interactions; frontend must consume normalized evidence through Engram API surfaces." }, { "modulePath": "services/runtime", "reason": "Server runtime construction and infrastructure wiring." }, { "modulePath": "services/verification", "reason": "Credentialed live-verification and evidence generation service." } ] diff --git a/hackathon/sibyl/ARCHITECTURE.md b/hackathon/sibyl/ARCHITECTURE.md new file mode 100644 index 0000000..00b131c --- /dev/null +++ b/hackathon/sibyl/ARCHITECTURE.md @@ -0,0 +1,111 @@ +# Architecture — Engram × Sibyl Hackathon Profile + +## Architectural decision +Do **not** rewrite canonical Engram around Sibyl. Introduce a hackathon storage profile where Sibyl is the sole decision-critical persistence/recall substrate and Engram keeps its execution-memory semantics. + +```text +Application / Agent + | + v +Engram Runtime +- admission +- eligibility +- influence +- provenance +- invalidation/expiry +- counterfactual evaluation + | + v +SibylRuntimeStore : EngramRuntimeStore + | + +--> WARM entities + | executions + | outcomes + | operational memories + | recalls + | decisions + | evaluation records + | + +--> COLD journal + adapter write/audit events + | + v +Sibyl MemoryClient.local(...) + | + v +fresh-process recall + | + v +application decision + observable changed action +``` + +## Why the full runtime-store boundary +The earlier narrow `ExecutionMemoryStore` proposal was rejected after auditing Engram's runtime. Engram already has a clean `EngramRuntimeStore` interface. Recall provenance, exposed-memory digests, decisions, evaluation records and source-lineage checks all depend on that store surface. + +Using Sibyl only for `persistMemory/searchMemory/getMemory` while retaining CockroachDB recall bookkeeping would create a split authority surface and weaken the deletion test. The evaluated profile therefore implements the existing full runtime-store contract as `SibylRuntimeStore`. + +## Boundary + +### Engram owns +- what qualifies as an execution episode; +- when an observation may become operational memory; +- whether recalled memory is eligible to influence action; +- explicit influence recording; +- provenance from source execution to later decision; +- recalled-memory state digests; +- counterfactual comparison; +- expiry/invalidation/conflict semantics. + +### Sibyl owns in the judged profile +- durable storage required to reconstruct the execution-memory trace; +- decision-critical operational memories; +- recall records and exposed-memory bindings; +- execution/outcome/decision/evaluation records needed by the evaluated profile; +- cross-process retrieval. + +### Application owns +- final business/action decision; +- external action execution. + +## Public-SDK rule +The adapter uses `sibyl_memory_client.MemoryClient` only. Direct access to Sibyl's SQLite schema is forbidden in the evaluated path because it would bypass the integration surface being judged. + +## Forbidden architecture + +```text +Engram -> CockroachDB equivalent decision memory + \-> Sibyl mirror +``` + +If CockroachDB can independently reconstruct the claimed fresh-session behavior in the evaluated profile, Sibyl is decorative and the gate is at risk. + +## Current implementation + +```text +packages/sibyl/bridge.py + MemoryClient.local(...) + set_entity / get_entity / list_entities + search_entities + write_event + +packages/sibyl/src/runtime-store.ts + class SibylRuntimeStore implements EngramRuntimeStore +``` + +The Python bridge exists because the current first-party Sibyl SDK is Python while Engram's runtime is TypeScript. The bridge is deliberately thin and JSON-only; memory semantics remain in Engram and persistence semantics remain in Sibyl. + +## Minimum causal acceptance +1. Run A executes with no relevant memory. +2. Failure/recovery evidence is observed. +3. Engram admits an operational memory and persists it through `SibylRuntimeStore`. +4. The first runtime/store instance is discarded. +5. Fresh Run B starts against the same Sibyl database. +6. Sibyl recalls the memory. +7. Engram eligibility permits influence. +8. Application records memory influence and chooses a different action. +9. Outcome and trace are reconstructable from Sibyl-backed state. +10. A no-memory control demonstrates the action difference. +11. Removing the Sibyl runtime makes the evaluated profile fail closed; there is no equivalent memory fallback. + +## Production boundary +This profile proves storage portability and load-bearing cross-session behavior. It does not replace the canonical Engram production persistence decision unless separately adopted after the hackathon. diff --git a/hackathon/sibyl/BASE_LIVE_RUN.md b/hackathon/sibyl/BASE_LIVE_RUN.md new file mode 100644 index 0000000..7ff275a --- /dev/null +++ b/hackathon/sibyl/BASE_LIVE_RUN.md @@ -0,0 +1,142 @@ +# Base Live Settlement Proof + +Status: `NOT_EXECUTED` + +This runbook promotes Base only when an Engram memory-conditioned decision causes and matches a real Base Sepolia USDC action. + +## Frozen evaluated network + +- network: Base Sepolia +- chain ID: `84532` +- token: Circle USDC +- token address: `0x036CbD53842c5426634e7929541eC2318f3dCF7e` +- decimals: `6` + +Reverify these values from first-party Base/Circle documentation immediately before the official live run. + +## Required causal chain + +```text +execution evidence + -> Sibyl memory + -> fresh Engram recall + -> Engram provider decision + -> engram.base-settlement-intent/v1 + -> explicit wallet execution on Base Sepolia + -> receipt + -> chain + payer + USDC recipient + amount match + -> engram.base-live-settlement-evidence/v1 +``` + +A transaction hash without the preceding memory/decision linkage does not qualify. + +## Build-window capture first + +Run the canonical evidence capture before the live Base action: + +```bash +npm run evidence:sibyl:capture +``` + +Capture v3 retains `base-routine-intent.json` inside the same artifact as the fresh Sibyl memory proof and binds it by SHA-256 in `manifest.json`. + +The deterministic addresses used by local/CI capture are evidence fixtures only. For a partner-qualifying live action, rerun the fresh routine decision with the reviewed Base Sepolia provider addresses and retain that live intent alongside the build-window capture. Never sign a transaction against the deterministic fixture addresses. + +## Live sequence + +1. Start from a hackathon-window commit with green canonical CI. +2. Produce and retain the build-window Sibyl evidence bundle. +3. Run/ingest real provider execution evidence if the claim depends on an external provider/Virtuals interaction. +4. Form the relationship memory through normal Engram admission. +5. Terminate the originating process. +6. In a fresh process, recall the relationship memory and record the provider decision. +7. Emit the Base settlement intent directly from that fresh decision using reviewed provider addresses. The artifact must contain: + - execution ID; + - retrieval ID; + - decision ID when available; + - memory refs; + - provider ID; + - provider recipient address; + - prepay basis points; + - exact USDC atomic amount; + - no-memory counterfactual when present. +8. Review the serialized intent before signing anything. +9. Record the requester/payer wallet address expected to execute the settlement. +10. Execute exactly that authorized USDC transfer with the selected testnet wallet/signer. +11. Retain the transaction hash. Never retain/export the private key in Engram evidence. +12. Verify independently and persist the verifier result: + +```bash +ENGRAM_BASE_RPC_URL='' \ + npm run base:settlement:verify -- \ + --intent artifacts/base/.json \ + --tx-hash <0x...> \ + --payer \ + --out artifacts/base/base-live-settlement-evidence.json +``` + +13. The verifier independently calls `eth_chainId` and must observe `84532` before accepting the receipt. +14. Verification must fail unless the receipt: + - was fetched from Base Sepolia; + - succeeded; + - contains a Circle Base Sepolia USDC `Transfer`; + - originates from the expected requester/payer when payer binding is supplied; + - transfers to the exact decision-derived recipient; + - transfers the exact decision-derived amount. +15. The persisted verifier evidence must bind: + - intent-file SHA-256; + - execution/retrieval/decision provenance; + - memory refs; + - observed chain ID; + - expected payer; + - transaction hash and block number; + - verified recipient and atomic amount. +16. Save the explorer link and raw receipt beside the persisted intent and `engram.base-live-settlement-evidence/v1` file. + +## Flagship expected deltas + +### Urgent +Control: +- provider: Atlas +- Base recipient: Atlas address + +Memory-conditioned: +- provider: Beacon +- Base recipient: Beacon address + +### Routine +Control: +- provider: Atlas +- max spend: $8 +- prepay: 50% +- authorized initial transfer: `4.000000 USDC` + +Memory-conditioned: +- provider: Atlas +- max spend: $8 +- prepay: 10% +- authorized initial transfer: `0.800000 USDC` +- milestone verification: required + +The routine case is the stronger Base authority proof because memory changes how much capital the same counterparty is permitted to receive, not merely which address is selected. + +## Negative pressure + +A live claim is invalid if any of these are true: +- serialized intent is malformed or internally inconsistent; +- RPC reports a chain other than `84532`; +- payer differs from the expected requester wallet when payer binding is claimed; +- recipient differs from the settlement intent; +- amount differs from the settlement intent; +- wrong token contract; +- reverted transaction; +- no retrieval/decision provenance; +- the live intent uses deterministic fixture addresses; +- transfer was executed independently of the Engram decision; +- only a screenshot is retained and raw transaction/evidence files are unavailable. + +## Promotion + +`BASE-001` remains `UNVERIFIED` until this sequence is executed during the eligible build window. + +Local intent/receipt tests and capture-v3 artifacts support internal conformance only. They cannot promote the published Base partner gate. diff --git a/hackathon/sibyl/BUILD_ORDER.md b/hackathon/sibyl/BUILD_ORDER.md new file mode 100644 index 0000000..5ed9639 --- /dev/null +++ b/hackathon/sibyl/BUILD_ORDER.md @@ -0,0 +1,38 @@ +# Build Order + +Build order follows evaluator-critical dependency, not architectural elegance. + +## P0 — Gate survival +1. Probe and lock current Sibyl API/package behavior. +2. Implement `ExecutionMemoryStore` abstraction only where required. +3. Implement `SibylMemoryStore`. +4. Route hackathon profile's decision-critical persist/recall exclusively through Sibyl. +5. Prove process-boundary fresh-session recall. +6. Implement deletion-test mode that disables/removes Sibyl and demonstrates core degradation/failure. +7. Preserve Engram source-execution -> memory -> recall -> influence provenance. + +## P1 — Score maximization +8. Add paired no-memory control versus Sibyl-memory execution. +9. Add stale/incompatible/conflicting memory negative tests. +10. Add one compelling real-world scenario beyond route failure if time allows: agent-provider relationship continuity is preferred. +11. Make trace/evidence visible to a cold evaluator. + +## P2 — Partner multiplier +12. Evaluate Base integration only if it produces a real economic/onchain consequence from recalled memory. +13. Evaluate Virtuals only if an ACP/native interaction creates or consumes decision-relevant experience. +14. Prefer one native partner that strengthens the product over two decorative integrations. + +## P3 — Submission +15. README Prior Work declaration and critical memory call map. +16. 2–5 minute continuous fresh-session demo choreography. +17. Build-log post + demo post. +18. Cold evaluator run. +19. Re-run truth freeze. +20. Freeze submission. + +## Explicitly deferred +- replacing canonical CockroachDB architecture; +- registry publication work unrelated to judging; +- broad framework integrations; +- production multi-tenant auth upgrades unless necessary for public demo safety; +- partner integrations without a causal role in the core scenario. diff --git a/hackathon/sibyl/DEVELOPMENT_ENVIRONMENT.md b/hackathon/sibyl/DEVELOPMENT_ENVIRONMENT.md new file mode 100644 index 0000000..59aa26b --- /dev/null +++ b/hackathon/sibyl/DEVELOPMENT_ENVIRONMENT.md @@ -0,0 +1,76 @@ +# Development Environment + +Status: `PROBED_BASE_GOLDEN_PASS_PRESSURE_CI_PENDING` for Sibyl integration. + +## Existing Engram baseline +- Node.js 22 +- npm +- TypeScript monorepo +- canonical checks: `npm install` then `npm run check` + +## Sibyl toolchain lock +Verified from first-party Sibyl documentation/repository on 2026-08-24: + +- direct SDK language: Python; +- package: `sibyl-memory-client`; +- public client: `MemoryClient.local(path, tenant_id=...)`; +- durable tiers used by this profile: WARM entities plus COLD journal audit events; +- retrieval API used by this profile: `search_entities(...)`; +- Python requirement: >=3.10 in the upstream package metadata; +- currently observed PyPI release: `0.6.1`; +- upstream GitHub `main` currently declares `0.7.0`, so release status must be revalidated before the Sep 1 build window and again before submission freeze. + +Evaluated dependency pin: + +```text +packages/sibyl/requirements.txt +sibyl-memory-client==0.6.1 +``` + +## Runtime configuration + +```bash +ENGRAM_SIBYL_DB=/absolute/path/to/memory.db +ENGRAM_SIBYL_TENANT=engram-hackathon +ENGRAM_SIBYL_PYTHON=python3 +# optional if repository layout differs +ENGRAM_SIBYL_BRIDGE=/absolute/path/to/packages/sibyl/bridge.py +``` + +There is deliberately no Cockroach fallback flag in `SibylRuntimeStore`. + +## Current commands + +```bash +npm install +python -m pip install -r packages/sibyl/requirements.txt +npm run build +npm run test:sibyl +npm run demo:sibyl:seed +# process exits +npm run demo:sibyl:recall +npm run demo:sibyl:no-memory-control +npm run test:sibyl:deletion +``` + +`test:sibyl` is an explicit optional-integration command. The ordinary `test:all` suite skips this external-SDK test unless `ENGRAM_SIBYL_TEST_REQUIRED=1`, so canonical Engram development does not silently acquire a Python dependency. The Sibyl CI job always runs it in required mode. + +## CI evidence and topology +The first clean Sibyl profile proof passed in GitHub Actions run `32749722101` at head `630e59d`. + +A later generic CI run exposed two integration-hygiene issues (missing optional Python SDK in generic CI and an undeclared server-only package boundary). Both were fixed. Generic Engram CI then passed at head `5908305` in run `32750255631`. + +The strengthened Sibyl proof is now a second job inside the canonical `.github/workflows/ci.yml`, rather than a standalone workflow. It provisions Node 22 + Python 3.12, installs the pinned SDK, runs the Sibyl pressure tests, then executes seed and recall as distinct Node processes, a no-memory control, and a deletion mutation. + +## Toolchain probe gate +- [x] current Sibyl SDK/package/API verified from first-party docs; +- [x] local-first configuration path understood; +- [x] public write/read API preserves Engram IDs/structured metadata in the observed baseline test; +- [x] deletion/unavailable-Sibyl mutation is implementable and baseline-tested; +- [x] no Sibyl secret is required for the local-first core path; +- [x] baseline golden Sibyl integration passed in clean CI; +- [x] canonical Engram suite passed after integration-hygiene fixes; +- [ ] strengthened stale/tamper + separate-Node-process pressure job passes on the current head; +- [ ] SDK/version truth revalidated at Sep 1. + +Do not promote the EBI state beyond the evidence actually recorded for the current head. diff --git a/hackathon/sibyl/EVALUATION_TRUTH.md b/hackathon/sibyl/EVALUATION_TRUTH.md new file mode 100644 index 0000000..a63d509 --- /dev/null +++ b/hackathon/sibyl/EVALUATION_TRUTH.md @@ -0,0 +1,58 @@ +# Evaluation Truth — Sibyl Labs Hackathon + +Verified: 2026-08-24 +Canonical sources: https://hack.sibyllabs.org/ ; /rules ; /submissions ; Sibyl docs. + +## Program +- Registration: Aug 16–31, 2026, closes 23:59 UTC. +- Build window: Sep 1–10, 2026. +- Judging: Sep 11–12. +- Winners: Sep 13–15. +- Prize pool: $10,000 USDC across top five. + +## Eligibility gate — PUBLISHED_HARD +Sibyl Memory must be load-bearing. Judges apply the deletion test: remove Sibyl Memory; if the product still does what it claims, it fails. + +Required proof: +1. persist context that matters; +2. recall it in a genuinely fresh session; +3. use it to change a decision, action, or result; +4. cold-start recall shown in one continuous unedited demo segment with on-screen timestamp or commit hash; +5. README points judges to critical write/read calls discoverable in under two minutes. + +## Rubric — PUBLISHED_SCORING +- Memory load-bearing / sophistication: 40 +- Innovation & originality: 25 +- Technical execution: 20 +- Pitch & presentation: 15 +- PMF bonus: up to +10, only with publicly verifiable evidence. + +Top memory band favors coordination and dynamic-storage patterns over trivial notepad recall. + +## Partner multiplier — PUBLISHED_BONUS +- 0 verified stacks: ×1.00 +- 1 verified stack: ×1.15 +- 2 verified stacks: ×1.25 cap + +Base qualifies when product deployment exists and the demo exercises a real onchain action such as wallet operation, x402 payment, B20 read or contract interaction. + +Virtuals qualifies when the demo exercises an ACP job, registered/transacting agent or another Virtuals-native integration. + +Decorative/import-only integrations do not count. + +## Submission — PUBLISHED_HARD +- Public GitHub repo under MIT or Apache-2.0 with real commit history. +- 2–5 minute demo. +- README with setup/run instructions, load-bearing memory explanation, partner stack locations, 'how memory made this possible', and Prior Work declaration. +- Two public posts: demo video + at least one build log, tagging Sibyl and claimed partners. +- Submission through registered team's private build page before Sep 10, 23:59 UTC. + +## Engram-specific conservative rule +The judged hackathon profile must not contain an equivalent CockroachDB decision-memory fallback. CockroachDB may remain part of canonical Engram outside this profile, but the submitted Sibyl path must fail the load-bearing function when Sibyl is removed. + +## Unknowns +- Exact Sibyl package/API version to lock at implementation start. +- Exact partner workshop guidance that may change preferred Base/Virtuals integration shape. +- Whether an existing repo with substantial prior work is advantaged/disadvantaged beyond mandatory Prior Work declaration; no penalty is published. + +Re-run this truth freeze before `READY_FOR_SUBMISSION`. diff --git a/hackathon/sibyl/FLAGSHIP_SCENARIO.md b/hackathon/sibyl/FLAGSHIP_SCENARIO.md new file mode 100644 index 0000000..861db89 --- /dev/null +++ b/hackathon/sibyl/FLAGSHIP_SCENARIO.md @@ -0,0 +1,94 @@ +# Flagship Scenario — Experiential Provider Continuity + +Status: `PRE_WINDOW_IMPLEMENTATION / AWAITING_CI` + +## Thesis +An autonomous agent should not treat every provider interaction as if it were the first one, nor should one bad interaction become a permanent global blacklist. + +Engram turns repeated, attributable execution experience into a bounded **relationship posture** that can change future provider selection and contract authority in the matching context. + +This is not a generic reputation score. + +```text +provider interaction + -> execution evidence + -> repeated attributable pattern + -> Engram admission + -> Sibyl persistent relationship memory + -> fresh-session recall + -> context-specific provider/terms decision + -> changed economic/coordination behavior +``` + +## Actors +- `requester-agent` — owns the relationship memory and future decision. +- `atlas` — cheapest provider; has two observed urgent `data_fetch` SLA breaches in requester-owned executions. +- `beacon` — slightly more expensive provider; next eligible option for urgent work. + +## Historical experience +Two independent urgent data-fetch executions with Atlas breach the latency SLA. + +A third execution explicitly evaluates those observations and admits a multi-source `REPEATED_PATTERN` memory with provenance to all three executions. + +The memory carries: +- provider: `atlas`; +- task type: `data_fetch`; +- failure class: `SLA_BREACH`; +- breach count: 2; +- relationship posture: `CONTEXT_GUARDED`; +- source execution IDs; +- evidence state and confidence. + +## Fresh-session consequences + +### Urgent task +Without memory: +- Atlas is cheapest eligible provider; +- requester selects Atlas; +- deterministic fixture breaches SLA. + +With recalled relationship memory: +- Atlas becomes ineligible for this urgent task; +- requester selects Beacon; +- decision records `CHANGED_ACTION` with Atlas as counterfactual. + +### Routine task +The same memory does **not** blacklist Atlas globally. + +Atlas remains selectable because price still matters, but Engram changes authority terms: +- prepayment falls from 50% to 10%; +- milestone verification becomes required. + +This demonstrates that memory changes the **shape of the relationship**, not merely a ranking. + +## Why this is stronger than reputation +A reputation system asks: `How good is Atlas?` + +This scenario asks: + +> `Given what this agent itself has experienced with Atlas, for this task class and consequence level, what authority should Atlas receive now?` + +The answer is contextual and provenance-bound. + +## Non-negotiable controls +1. One failure alone must not create relationship posture. +2. Experience from another task type must not transfer solely because retrieval score is high. +3. Expired memory must not be exposed. +4. Memory changed after recall must fail influence via state-digest mismatch. +5. Removing Sibyl must remove the cross-session relationship state; no fallback may preserve equivalent behavior. + +## Partner path, not yet implemented +If later justified: +- Virtuals ACP can instantiate Atlas/Beacon as actual agent-service providers and produce the provider-job events. +- Base can make the changed terms economically consequential, e.g. different staged payment/prepayment behavior. + +Neither partner is required for the core proof and neither should be added decoratively. + +## Submission evidence requirement +All current implementation before Sep 1, 2026 is prior work. The final submission must regenerate: +- historical interactions; +- relationship-memory admission; +- fresh-session recall; +- provider/terms behavioral delta; +- deletion mutation; +inside the official build window and preserve the exact commit/run/video evidence. diff --git a/hackathon/sibyl/JUDGE_CALL_MAP.md b/hackathon/sibyl/JUDGE_CALL_MAP.md new file mode 100644 index 0000000..2dfa751 --- /dev/null +++ b/hackathon/sibyl/JUDGE_CALL_MAP.md @@ -0,0 +1,120 @@ +# Judge Call Map — Engram × Sibyl + +This file is intentionally short. It answers: **where is Sibyl load-bearing, and where does recalled memory change behavior?** + +## 1. Sibyl adapter + +`packages/sibyl/src/runtime-store.ts` + +This implements Engram's `EngramRuntimeStore` contract for the evaluated profile. + +Critical methods: +- `persistMemory(...)` — writes Engram operational memory to Sibyl; +- `searchMemory(...)` — retrieves decision-critical memory from Sibyl; +- `getMemory(...)` — resolves the current authority-relevant memory state; +- `getRecalls(...)` / `updateRecallExposure(...)` — preserve recall provenance and exposed memory-state digests; +- `recordRuntimeDecision(...)` — persists the decision that references memory; +- `getTrace(...)` — reconstructs execution -> recall -> influence evidence. + +There is no Cockroach fallback in `SibylRuntimeStore`. + +## 2. Public Sibyl SDK bridge + +`packages/sibyl/bridge.py` + +The TypeScript runtime reaches Sibyl through the public Python `MemoryClient` API. The bridge does not query Sibyl's SQLite tables directly. + +Dependency lock: + +`packages/sibyl/requirements.txt` + +## 3. Engram semantic authority + +`packages/runtime/src/runtime.ts` + +Critical flow: +- `complete(...)` evaluates admission and calls `store.persistMemory(...)`; +- `recall(...)` calls `store.searchMemory(...)`, then applies Engram eligibility policy; +- `recordDecision(...)` verifies that claimed influential memory was actually recalled and still matches its exposed state digest; +- `validateMemorySourceLineage(...)` checks source execution provenance; +- `validateCounterfactualComparison(...)` and influence policy prevent unsupported causal claims. + +Sibyl owns persistence/recall. Engram owns whether memory may legitimately influence behavior. + +## 4. Deterministic deletion/control proof + +`tests/integration/sibyl-memory-loop.test.ts` + +Proves: +- persistent memory crosses fresh runtime/store boundaries; +- expired memory is not exposed; +- memory changed after recall cannot influence the decision; +- unavailable Sibyl fails closed. + +Demo: + +`scripts/sibyl-demo.ts` + +Commands: + +```bash +npm run demo:sibyl:seed +npm run demo:sibyl:recall +npm run demo:sibyl:no-memory-control +npm run test:sibyl:deletion +``` + +## 5. Headline experiential-continuity scenario + +Policy primitive: + +`packages/scenarios/provider-continuity/src/index.ts` + +Sibyl-backed integration: + +`tests/integration/sibyl-provider-continuity.test.ts` + +Evaluator demo: + +`scripts/sibyl-provider-demo.ts` + +Commands: + +```bash +npm run demo:sibyl:provider:seed +npm run demo:sibyl:provider:urgent +npm run demo:sibyl:provider:routine +``` + +Expected causal behavior: + +```text +historical Atlas breach #1 +historical Atlas breach #2 + | + v +Engram multi-source REPEATED_PATTERN admission + | + v +Sibyl relationship memory + | + +--> fresh urgent task + | control: Atlas + | memory: Beacon + | + +--> fresh routine task + control: Atlas / 50% prepay / no checkpoint + memory: Atlas / 10% prepay / milestone verification +``` + +The same memory therefore changes behavior contextually rather than becoming a blanket reputation label. + +## 6. Mandatory CI + +`.github/workflows/ci.yml` + +The `sibyl-profile` job provisions Python + the pinned Sibyl SDK, builds Engram, runs both Sibyl integration suites, executes fresh-process deterministic demos, executes the provider-continuity demos, and then runs the deletion mutation. + +## 7. Prior work boundary + +Everything on this branch created before the official Sep 1 build window is pre-window work and must be declared as Prior Work where the submission rules require it. Hackathon-window evidence must be regenerated from the then-current commit; this document is not itself submission proof. diff --git a/hackathon/sibyl/JUDGE_MODEL.md b/hackathon/sibyl/JUDGE_MODEL.md new file mode 100644 index 0000000..8f1e213 --- /dev/null +++ b/hackathon/sibyl/JUDGE_MODEL.md @@ -0,0 +1,34 @@ +# Judge Model + +## Judge questions Engram must answer fast + +1. What is Engram? +2. Why is Sibyl necessary rather than decorative? +3. What exact memory was persisted? +4. Is the second run genuinely fresh? +5. What changed because of recalled memory? +6. Can I delete Sibyl and make the claimed behavior fail? +7. Is this more than generic RAG/chat memory? +8. Can I trace source execution -> memory -> recall -> influence -> changed action? +9. Does it survive a second comparable run? +10. Are Base/Virtuals doing product work or sponsor theater? + +## Evaluator personas + +### Cold product evaluator +Needs a 20-second explanation: Engram turns prior execution experience into operational memory that changes future autonomous behavior. + +### Memory skeptic +Attempts deletion test; compares fresh-session run with and without Sibyl; rejects retrieval without behavioral influence. + +### Evidence skeptic +Checks provenance, commit hash/timestamp, decision record, memory write/read paths and counterfactual baseline. + +### Adversarial operator +Injects stale, incompatible, contradictory or wrong-provenance memory and expects Engram policy to block or expose it. + +### Partner specialist +Requires protocol-native evidence for each claimed partner and checks it is exercised during the demo. + +## Winning evaluator moment +A fresh process starts with no in-process state, recalls a prior failed execution through Sibyl, Engram marks the memory eligible, the application selects a different action than the memory-free control, and the trace shows exactly why. diff --git a/hackathon/sibyl/LIVE_PRESSURE_PLAN.md b/hackathon/sibyl/LIVE_PRESSURE_PLAN.md new file mode 100644 index 0000000..4272c63 --- /dev/null +++ b/hackathon/sibyl/LIVE_PRESSURE_PLAN.md @@ -0,0 +1,47 @@ +# Live Pressure Plan + +## P0 mutations — must detect failure + +1. **Delete/disable Sibyl write** + - Expected: no future cross-session operational memory. + - Gate: core flagship behavior cannot reproduce. + +2. **Delete/disable Sibyl recall** + - Expected: fresh session behaves like no-memory control. + - Gate: changed-action proof disappears. + +3. **Kill process between runs** + - Expected: Run B still recalls through Sibyl. + - Detects accidental in-process/session caching. + +4. **Inject stale/incompatible memory** + - Expected: Engram eligibility blocks influence. + +5. **Inject wrong-provenance memory** + - Expected: rejected or explicitly surfaced as invalid provenance. + +6. **Inject contradictory memories** + - Expected: conflict remains visible; no silent overwrite/adjudication. + +7. **Repeat flagship scenario twice** + - Expected: deterministic invariants remain stable; no one-shot demo behavior. + +## Evaluator runs + +### Cold evaluator +Clone, install, run, locate write/read paths, execute flagship scenario without guidance. + +### Memory skeptic +Run memory-enabled, no-memory control, and deletion mutation; compare decisions and outputs. + +### Evidence skeptic +Reconstruct source execution -> memory ID -> recalled candidate -> influence record -> changed action. + +### Dependency degradation +Make Sibyl unavailable after prior persistence; product must fail legibly rather than silently switch to equivalent hidden persistence. + +### Partner specialist +If Base/Virtuals claimed, verify protocol-native transaction/job evidence and confirm it serves the actual remembered decision. + +## Promotion rule +`LOCAL_PASS` cannot become `LIVE_PASS` from screenshots or simulated fixtures. Public/evaluator claims require observed public evidence. diff --git a/hackathon/sibyl/README.md b/hackathon/sibyl/README.md new file mode 100644 index 0000000..56ca7b6 --- /dev/null +++ b/hackathon/sibyl/README.md @@ -0,0 +1,133 @@ +# Engram × Sibyl — EBI Instance + +Status: `READY_FOR_LIVE_PRESSURE` for the memory + experiential-continuity path. + +Submission status remains **not ready**. Hackathon-window evidence, live partner execution, public demo/video/posts and final truth revalidation are still outstanding. + +This directory is the Evaluated Build Instantiation (EBI) wrapper for adapting Engram to the Sibyl Labs Hackathon without changing Engram's canonical product semantics. + +## Frozen candidate thesis + +**Engram is execution-memory infrastructure: prior execution experience becomes bounded operational memory that can legitimately change later autonomous behavior.** + +For this hackathon deployment, **Sibyl is the load-bearing persistence and recall substrate**. Engram retains the semantics for admission, eligibility, influence, provenance, invalidation, competing memory and counterfactual evaluation. + +```text +execution event + -> Engram episode/evidence semantics + -> Sibyl persistence + -> fresh-session Sibyl recall + -> Engram eligibility/influence policy + -> application decision + -> changed action / changed authority + -> outcome/provenance +``` + +## Observed flagship proof — 2026-08-24 +GitHub Actions run `32753747711`, Sibyl job `97516430322`, executed the expanded profile on code head `d3d6e983...` in a clean Ubuntu runner. + +### Pressure harness +- installed `sibyl-memory-client==0.6.1`; +- built Engram successfully; +- passed **6/6** Sibyl integration tests; +- retained the original process-boundary Route C → Route D proof; +- rejected expired Sibyl memory before exposure; +- rejected influence after persisted memory changed post-recall; +- passed two Sibyl-backed experiential provider-continuity tests; +- canonical Engram `npm run check` also passed in the parallel CI job. + +### Experiential provider continuity +Two separate requester-owned Atlas executions observed urgent `data_fetch` SLA breaches. A third admitting execution consolidated that evidence into a provenance-linked `REPEATED_PATTERN` memory: + +- relationship memory: `f269d755-f642-4495-8f68-da735618d74b`; +- relationship posture: `CONTEXT_GUARDED`; +- memory source executions: two historical provider interactions plus the explicit admitting execution. + +A new process then recalled that exact Sibyl memory. + +**Urgent context** + +```text +no memory: +Atlas, 50% prepay -> SLA_BREACH + +with relationship memory: +Beacon, 50% prepay -> SUCCESS +``` + +The trace records `CHANGED_ACTION`, the recalled memory ID, retrieval ID, memory-state digest and Atlas counterfactual. + +**Routine context** + +```text +no memory: +Atlas, 50% prepay, no milestone verification + +with same relationship memory: +Atlas, 10% prepay, milestone verification required +``` + +The provider remains Atlas. The trace records `CONSTRAINED_ACTION` rather than pretending the relationship is a global blacklist. + +The stronger claim is therefore: + +> Multiple attributable executions become an agent-specific, task-specific relationship posture that changes how much authority a provider receives in a future context. + +This is not a universal provider reputation score. + +### Deletion proof +The same current-head CI run removed the configured Sibyl runtime and observed: + +- `degraded: true`; +- `fallbackAvailable: false`; +- no Cockroach path preserved equivalent cross-session behavior. + +This is `LOCAL_PASS` evidence generated before the official hackathon build window. It is not the final submission proof and must remain declared as prior work. + +## Evaluator navigation +The root [`README.md`](../../README.md) now includes a judge call map for: +- Sibyl memory write; +- Sibyl memory read; +- recall-state binding; +- influence validation; +- provider-continuity policy; +- fresh-process demos; +- deletion mutation; +- Prior Work declaration. + +## Non-negotiable deletion test + +If Sibyl persistence/recall is removed, the submitted build must lose the decision-critical cross-session execution memory required to produce the claimed changed behavior. + +A CockroachDB fallback that preserves equivalent decision-critical memory in the judged path is therefore **not allowed** for the hackathon profile. + +## Partner decision +Virtuals ACP is a natural next integration candidate because an ACP provider job can become the real provider interaction whose outcome Engram observes and later remembers. The intended causal boundary is: + +```text +Virtuals ACP job + -> job history / outcome evidence + -> Engram execution observation + -> Engram relationship-memory admission + -> Sibyl persistence + -> fresh-session recall + -> different future ACP provider / terms +``` + +No Virtuals multiplier is claimed until an actual testnet ACP job is exercised and evidenced. + +Base remains optional. It should be added only if the remembered relationship posture changes a real onchain payment/escrow action rather than serving as a decorative transaction. + +## Current next gates +1. Revalidate official hackathon and Sibyl SDK truth at the start of the Sep 1 build window. +2. Re-run the full pressure harness as hackathon-window evidence. +3. Implement the Virtuals ACP evidence adapter and exercise a real testnet provider job if the authentication/funding path is practical. +4. Add a contradictory-Sibyl-memory mutation so competing relationship evidence cannot be silently collapsed. +5. Build the continuous 2–5 minute evaluator proof and public evidence surfaces. +6. Consider Base only after the ACP path is live and causally useful. + +## EBI state machine + +`READY_TO_RESEARCH -> READY_TO_BUILD -> READY_FOR_LIVE_PRESSURE -> READY_FOR_SUBMISSION -> SUBMISSION_FROZEN` + +No optimistic promotion. Evidence state must justify every transition. diff --git a/hackathon/sibyl/TOP_BAND_SCENARIO.md b/hackathon/sibyl/TOP_BAND_SCENARIO.md new file mode 100644 index 0000000..f9182dc --- /dev/null +++ b/hackathon/sibyl/TOP_BAND_SCENARIO.md @@ -0,0 +1,141 @@ +# Top-Band Scenario — Experiential Provider Continuity + +## Thesis +An autonomous requester should not merely remember that a provider was good or bad. It should preserve bounded evidence from its own prior interactions, derive a relationship posture only when provenance is sufficient, and let that posture change future delegation or authority contextually. + +This is not a global reputation score. + +```text +reputation + = a claim about a provider + +experiential continuity + = prior requester-owned executions + -> evidence + -> bounded relationship memory + -> context-specific future policy + -> changed provider / terms / authority +``` + +## Flagship story +Provider `atlas` is the cheapest eligible data-fetch provider. Provider `beacon` is slightly more expensive. + +### Historical execution 1 +- requester selects Atlas for an urgent data fetch; +- Atlas returns useful work but breaches the latency SLA; +- requester records observed execution evidence; +- no relationship conclusion is admitted yet. + +### Historical execution 2 +- a later comparable urgent task again uses Atlas; +- Atlas again breaches the SLA; +- second independent execution is observed. + +### Relationship-state admission +A third admitting execution evaluates the repeated pattern. Engram admits one `REPEATED_PATTERN` memory only with multi-source provenance linking both prior breach executions plus the admitting execution. + +Derived bounded state: + +```text +providerId: atlas +memoryPrimitive: EXPERIENTIAL_RELATIONSHIP +relationshipPosture: CONTEXT_GUARDED +taskType: data_fetch +failureType: SLA_BREACH +breachCount: 2 +``` + +The memory does **not** say "Atlas is bad." It says that this requester has enough observed history to guard a particular class of future interaction. + +## Fresh-session consequence A — urgent task +Without memory: + +```text +cheapest eligible provider -> Atlas +standard terms +``` + +With recalled relationship memory: + +```text +Atlas is excluded for this urgent task +next eligible provider -> Beacon +``` + +Required proof: +- same offers; +- same task constraints; +- only memory condition changes; +- control selects Atlas; +- treatment selects Beacon; +- decision records `CHANGED_ACTION` and counterfactual Atlas selection. + +## Fresh-session consequence B — routine task +The same memory must not become a permanent blacklist. + +Without memory: + +```text +Atlas +50% prepay +no milestone verification +``` + +With relationship memory: + +```text +Atlas remains selected +10% prepay +milestone verification required +``` + +This demonstrates contextual authority narrowing rather than blanket exclusion. + +## Why multiple executions matter +One bad interaction must not establish relationship state. + +The scenario therefore requires: +- at least two independent source execution IDs; +- same requester/agent authority; +- compatible workflow/task class; +- observed evidence; +- repeated pattern admission; +- reconstructable source lineage. + +## Negative controls +The build should fail or remain baseline when: +1. only one breach exists; +2. memory belongs to another requester; +3. memory concerns another task type; +4. memory is expired; +5. memory changes after recall; +6. source lineage is missing or contradictory; +7. Sibyl is unavailable; +8. no applicable provider alternative exists — in this case terms may narrow, but the system must not invent a provider. + +## Sibyl deletion test +Sibyl is the only persistence/recall substrate in the evaluated profile. Delete or disable Sibyl and the fresh process cannot reconstruct the relationship memory; provider selection therefore reverts to the memory-free baseline. + +## Product meaning +The agent is not learning a universal truth about Atlas. It is maintaining a bounded private history of what Atlas has meant **to this agent under specific conditions**, and carrying that experience into future economic behavior. + +That is the intended Experiential Continuity primitive: + +```text +Event +-> Evidence +-> Memory +-> Experience +-> Legitimate State Delta +-> Relationship Posture +-> Decision +-> Action +-> New Event +``` + +## Partner attachment point +Only after this core is green: +- Virtuals ACP can provide the real agent/provider job relationship; +- Base can provide the economically consequential payment/escrow/prepayment action. + +Partner integrations should exercise the remembered relationship decision. They must not replace or decorate it. diff --git a/hackathon/sibyl/VIRTUALS_LIVE_RUN.md b/hackathon/sibyl/VIRTUALS_LIVE_RUN.md new file mode 100644 index 0000000..7676cdd --- /dev/null +++ b/hackathon/sibyl/VIRTUALS_LIVE_RUN.md @@ -0,0 +1,194 @@ +# Virtuals ACP Live Proof Runbook + +Status: `IMPLEMENTED_ADAPTER / AUTHENTICATED_JOB_NOT_RUN` + +Goal: replace the provider-continuity fixture's external interaction boundary with a real Virtuals ACP testnet job while keeping Engram/Sibyl as the memory and influence authority. + +## Safety / truth boundary +- Use the maintained `@virtuals-protocol/acp-cli`. +- Use `IS_TESTNET=true` until the entire proof is stable. +- Every scripted command uses `--json`. +- Authentication and signer state remain in the ACP config / OS keychain. Never commit them. +- Do not claim the Virtuals partner multiplier until a real ACP job/agent interaction is captured in evidence. + +## 1. Install / select testnet + +```bash +npm i -g @virtuals-protocol/acp-cli +export IS_TESTNET=true +``` + +Optional isolated config for the experiment: + +```bash +export ACP_CONFIG_DIR="$HOME/.config/acp-engram-sibyl" +``` + +## 2. Authenticate — split flow + +For agent/tool-driven terminals, do not use the blocking bare configure command. + +```bash +acp configure start --json +``` + +This returns a sign-in URL and request ID. Open the URL, authenticate, then: + +```bash +acp configure complete --request-id --wait --timeout 300 --json +``` + +Expected terminal state: `authenticated` plus wallet address. + +## 3. Create/select requester agent and signer + +Inspect current state first: + +```bash +acp agent list --json +acp agent whoami --json +``` + +If no suitable testnet requester exists: + +```bash +acp agent create \ + --name "Engram Requester" \ + --description "Requester agent for Sibyl experiential-continuity evaluation" \ + --signer \ + --policy restricted \ + --json +``` + +If an agent already exists, select it and ensure a signer is configured using the current ACP CLI guidance before any job action that signs onchain. + +## 4. Discover a real provider/offering + +Start with a low-cost, deterministic-enough service category: + +```bash +acp browse "data analysis" --top-k 5 --json +``` + +Record: +- provider wallet/address; +- provider/agent name; +- offering name; +- price; +- advertised SLA if available; +- supported testnet chain ID. + +Do not invent `Atlas`/`Beacon` identities for the live proof. The deterministic fixture names are test-only. Live evidence uses the actual ACP provider identity. + +## 5. Start an Engram execution before the ACP job + +The live harness needs an Engram execution ID that will receive the ACP observation. Start it through the existing SDK/API/runtime surface with: +- `agentId`: requester identity; +- `workflowType`: `agent_provider_selection`; +- `taskType`: matching the chosen offering; +- provider identity; +- urgency / SLA constraint; +- environment version including ACP testnet and chain. + +Record the Engram execution ID and start timestamp. + +## 6. Create and execute the ACP job + +Recommended offering flow: + +```bash +acp client create-job \ + --provider \ + --offering-name "" \ + --requirements '' \ + --chain-id \ + --json +``` + +Follow the current ACP state machine rather than assuming the next action: + +```text +open -> budget_set -> funded -> submitted -> completed/rejected +``` + +Inspect/watch the job: + +```bash +acp job watch --job-id --timeout 300 --json +acp job history --job-id --chain-id --json +``` + +Funding/release only when the current job state requires it: + +```bash +acp client fund --job-id --amount --chain-id --json +acp client complete --job-id --chain-id --reason "accepted" --json +``` + +If the deliverable is invalid, use the real rejection path rather than forcing a success: + +```bash +acp client reject --job-id --chain-id --reason "" --json +``` + +## 7. Ingest authoritative ACP history into Engram + +Once terminal/meaningful evidence exists, record completion timestamp and run: + +```bash +npm run virtuals:acp:ingest -- \ + --execution-id \ + --job-id \ + --chain-id \ + --provider-id \ + --task-type \ + --urgency URGENT \ + --expected-latency-seconds \ + --started-at \ + --completed-at \ + --testnet +``` + +The command: +1. calls `acp job history ... --json`; +2. validates the current v2 history shape; +3. derives bounded observed evidence; +4. appends `VIRTUALS_ACP_JOB_OBSERVED` to the specified Sibyl-backed Engram execution; +5. emits a receipt that still labels the partner claim unverified until evidence is retained/audited. + +## 8. Admit only what the evidence supports + +One ACP interaction is an execution episode, not automatically a relationship memory. + +For the flagship `CONTEXT_GUARDED` relationship posture, collect multiple attributable interactions or another justified evidence basis, then use Engram's multi-source admission path. + +The existing acceptance rule intentionally rejects: +- a one-failure relationship posture; +- unrelated task-type transfer; +- expired memory; +- post-recall memory mutation. + +## 9. Fresh-session consequence + +After relationship memory is admitted to Sibyl: +1. terminate the requester process/session; +2. start a fresh comparable provider-selection execution; +3. recall the provider relationship memory; +4. show the memory-free candidate decision; +5. show the memory-conditioned ACP provider/terms decision; +6. execute the changed ACP interaction if practical; +7. retain Engram trace + ACP job IDs/history. + +## 10. Promotion evidence + +`VIRTUALS-001` can move above `UNVERIFIED` only when we have: +- authenticated testnet agent identity; +- real ACP provider/offering; +- real ACP job ID + chain ID; +- job history / terminal state; +- Engram execution observation sourced from that history; +- Sibyl memory derived from attributable ACP experience; +- fresh-session decision trace showing the memory's causal role; +- demo evidence that clearly shows the Virtuals-native interaction. + +Until then, adapter/test code is implementation evidence only — not partner-stack proof. diff --git a/hackathon/sibyl/audits.md b/hackathon/sibyl/audits.md new file mode 100644 index 0000000..ef75bfe --- /dev/null +++ b/hackathon/sibyl/audits.md @@ -0,0 +1,43 @@ +# Engram × Sibyl Audit Gates + +## Release vetoes +- [ ] Sibyl decision-critical persistence is implemented. +- [ ] Fresh process/session recalls earlier state through Sibyl. +- [ ] Recalled memory changes a decision/action/result. +- [ ] No-Sibyl mutation breaks or materially degrades the claimed core behavior. +- [ ] No equivalent CockroachDB fallback preserves the judged memory behavior. +- [ ] Source execution -> admitted memory -> recall -> influence -> changed action is reconstructable. +- [ ] Continuous demo segment proves cold start with timestamp or commit hash. +- [ ] README identifies write/read calls and Prior Work. + +## Engram semantic integrity +- [ ] Engram remains decision-support infrastructure; application retains final action authority. +- [ ] Retrieval is not labeled influence unless a decision explicitly references memory. +- [ ] Later success is not automatically labeled memory benefit. +- [ ] Wrong-provenance recall is rejected or exposed. +- [ ] Stale/incompatible memory is blocked before influence. +- [ ] Contradictory memories are not silently overwritten. +- [ ] Counterfactual/no-memory control exists for flagship scenario. + +## Technical execution +- [ ] Build passes from clean checkout. +- [ ] Test suite passes twice. +- [ ] Fresh-session test uses no hidden in-process state. +- [ ] Sibyl failure is legible; no fake fallback. +- [ ] Secrets are absent from client bundles/repo/logs. +- [ ] Public demo surface exposes evidence close to claims. + +## Partner gates +- [ ] Base is claimed only if a real Base action is exercised. +- [ ] Virtuals is claimed only if a native Virtuals interaction is exercised. +- [ ] Partner integration changes/serves actual product behavior. +- [ ] Protocol-native evidence exists independently of Engram UI. + +## Submission +- [ ] Public MIT/Apache repo. +- [ ] 2–5 minute demo. +- [ ] Two public posts. +- [ ] Private build page marked ready before Sep 10 23:59 UTC. +- [ ] Truth freeze revalidated immediately before submission. + +Current release status: `READY_TO_RESEARCH`. diff --git a/hackathon/sibyl/foundry/FOUNDRY_MATRIX.md b/hackathon/sibyl/foundry/FOUNDRY_MATRIX.md new file mode 100644 index 0000000..0b63c99 --- /dev/null +++ b/hackathon/sibyl/foundry/FOUNDRY_MATRIX.md @@ -0,0 +1,27 @@ +# Foundry Matrix + +| Foundry | Activation | Required output for Engram × Sibyl | +|---|---|---| +| Research | REQUIRED | live rules/docs truth, API/package lock, contradictions, unknowns | +| Opportunity | REQUIRED | score-maximizing work vs decorative work; partner ROI | +| Product | REQUIRED | exact user/job, product promise, MVP boundary, PMF evidence path | +| Systems | REQUIRED | storage adapter, critical path, state machine, failure/recovery, provenance | +| Interface | REQUIRED | cold evaluator IA, proof discoverability, demo comprehension | +| Experiment | REQUIRED | paired no-memory/memory baseline, deletion mutation, stale/conflict tests | +| QA / Verification | REQUIRED | evidence promotion, release vetoes, repeatability | +| Demo | REQUIRED | 2–5 minute evidence choreography; continuous cold-start beat | +| Learning | REQUIRED POST-RUN | preserve evaluator misses/failures as regression corpus | +| Sound | OPTIONAL | narration only if it improves clarity without hiding evidence | +| Onchain Systems Engineering | CONDITIONAL | activate before Base/x402/wallet/contract execution | + +## Orchestration order + +`Research -> Opportunity -> Product -> Systems -> Interface -> Experiment -> QA -> Demo -> observed evaluator run -> Learning` + +Parallelization is allowed only when dependencies are explicit. + +## Current routing decisions +- Sibyl API/tooling verification blocks implementation architecture details. +- Product and Systems may refine the flagship scenario together after truth/toolchain lock. +- Partner work is downstream of passing the load-bearing deletion test. +- Demo work may stage only evidence already promoted by QA. diff --git a/hackathon/sibyl/partners/base.md b/hackathon/sibyl/partners/base.md new file mode 100644 index 0000000..16bb8e9 --- /dev/null +++ b/hackathon/sibyl/partners/base.md @@ -0,0 +1,84 @@ +# Base Partner Contract + +Class: `PUBLISHED_BONUS` + +## Published qualification +A claimed Base stack counts only when judges can see it doing real work in the product. Deployment is the eligibility floor; an executed onchain action shown in the demo earns the partner bonus, including wallet operation, x402 payment, B20 read, or contract interaction. + +## Engram fit +Base is the economic consequence layer for remembered provider experience: + +```text +Virtuals/provider execution evidence + -> Engram observation + -> Sibyl relationship memory + -> fresh Engram recall + -> provider/authority decision changes + -> Base settlement action changes +``` + +The evaluated Base profile is Base Sepolia (`84532`) using Circle USDC at `0x036CbD53842c5426634e7929541eC2318f3dCF7e`. + +## Flagship causal deltas + +### Urgent +No-memory control selects Atlas. Applicable relationship memory selects Beacon. + +The Base effect is a different settlement recipient. + +### Routine +No-memory control selects Atlas with 50% prepayment on an $8 offer: `4.000000 USDC`. + +Applicable relationship memory still selects Atlas but reduces prepayment to 10%: `0.800000 USDC`, while requiring milestone verification. + +The Base effect is reduced economic authority, not a global provider blacklist. + +## Local conformance +The current adapter and verifier are `LOCAL_CONFORMANCE_PASS` on tested code head `b6ffe6fcc824c20e8f41f2e64532ada3867c858d`: + +- Engram CI `33264345953`: SUCCESS; +- canonical check job `99131636110`: SUCCESS; +- complete Sibyl profile job `99131636029`: SUCCESS; +- SAM Build `33264345950`: SUCCESS. + +Local pressure proves: +- recipient and prepayment amount are derived from the Engram decision; +- serialized intent is schema-validated and amount-consistent; +- RPC chain must resolve to Base Sepolia `84532`; +- optional payer binding checks the USDC `Transfer.from` address; +- wrong recipient, amount, token, payer, chain, or reverted receipt fails closed. + +This is not a live Base transaction and does not earn the partner bonus. + +## Veto +Do not claim Base merely because a transaction receipt exists. The transaction must match the settlement intent derived from the remembered Engram decision. + +## Required evidence +- Engram execution ID; +- recall/retrieval ID when memory influenced the decision; +- decision-linked `engram.base-settlement-intent/v1` serialized intent; +- expected requester/payer wallet when payer binding is claimed; +- RPC-observed chain ID `84532`; +- protocol-native Base transaction hash and successful receipt; +- observed Circle USDC transfer payer/recipient/amount matching the intent; +- action visible in the demo; +- counterfactual showing what recipient/amount would have been authorized without memory. + +## Acceptance test +`BASE-001` may promote only when a real Base Sepolia action proves: + +`remembered decision == authorized settlement intent == observed onchain settlement`. + +A transaction hash alone is insufficient. + +## Negative tests +- remove memory: recipient/amount must revert to the control settlement; +- malformed or internally inconsistent serialized intent: reject; +- wrong RPC chain: reject; +- wrong payer when bound: reject; +- alter provider address: reject wrong recipient; +- alter prepayment amount: reject wrong amount; +- wrong token or reverted transaction: reject; +- remove Base action: no Base partner claim remains. + +Status: `IMPLEMENTED / LOCAL_CONFORMANCE_PASS / LIVE_BASE_UNVERIFIED`. diff --git a/hackathon/sibyl/partners/virtuals.md b/hackathon/sibyl/partners/virtuals.md new file mode 100644 index 0000000..ae9d093 --- /dev/null +++ b/hackathon/sibyl/partners/virtuals.md @@ -0,0 +1,22 @@ +# Virtuals Partner Contract + +Class: `PUBLISHED_BONUS` + +## Published qualification +A claimed Virtuals stack counts only when judges can see a Virtuals-native integration exercised in the submitted build, such as an ACP job or a registered/transacting agent. + +## Engram fit +Best use: a Virtuals agent/provider interaction generates execution experience that Engram persists through Sibyl and later uses to alter provider selection, contracting terms, verification requirements, or another autonomous decision. + +## Veto +Do not use Virtuals as a decorative runtime label. The native interaction must create or consume decision-relevant experience. + +## Required evidence +- protocol-native ACP/agent/job evidence; +- exercised in demo; +- trace linking interaction -> persisted memory -> fresh-session changed behavior. + +## Negative test +Remove the Virtuals interaction. If the flagship scenario and product meaning remain unchanged, do not claim the partner stack. + +Status: `UNVERIFIED / OPTIONAL`. diff --git a/hackathon/sibyl/requirements/requirements.json b/hackathon/sibyl/requirements/requirements.json new file mode 100644 index 0000000..81773c4 --- /dev/null +++ b/hackathon/sibyl/requirements/requirements.json @@ -0,0 +1,23 @@ +{ + "verified_at": "2026-08-29", + "status": "READY_FOR_LIVE_PRESSURE", + "evidence_note": "Core Sibyl, experiential-provider, evidence-capture and hardened Base local-conformance gates are LOCAL_PASS on strongest tested pre-window code head bd02738a57ebb4bde1bdc68bc5ff475b1bbdad64: Engram CI 33264580330 and SAM build 33264580331 both succeeded. The Sibyl profile now directly emits and validates a serialized Base settlement artifact from the fresh memory-conditioned routine decision, closing the manual translation gap. BASE-001 remains UNVERIFIED until a real Base Sepolia action is executed. Virtuals ACP remains LOCAL_CONFORMANCE_PASS / LIVE_ACP_UNVERIFIED. Submission/live-public gates must be re-proven during the official build window.", + "requirements": [ + {"id":"SIBYL-GATE-001","track":"main","class":"PUBLISHED_HARD","severity":"VETO","description":"Sibyl Memory is load-bearing; deleting it breaks or materially degrades the core function.","check":"run deletion mutation and compare claimed behavior","evidence_required":"failing/degraded no-Sibyl run plus passing Sibyl run","status":"LOCAL_PASS","evidence":"GitHub Actions run 33264580330 job 99132270544: deletion mutation degraded with no fallback and exact evidence-capture path passed"}, + {"id":"SIBYL-GATE-002","track":"main","class":"PUBLISHED_HARD","severity":"VETO","description":"Fresh session recalls earlier persisted state.","check":"terminate process/session, restart clean, recall prior memory","evidence_required":"continuous demo segment with timestamp or commit hash","status":"LOCAL_PASS","evidence":"GitHub Actions run 33264580330: separate route/provider processes recalled persisted Sibyl memory; conflict test retrieves contradictory memories through a fresh SibylRuntimeStore"}, + {"id":"SIBYL-GATE-003","track":"main","class":"PUBLISHED_HARD","severity":"VETO","description":"Recalled memory changes a decision, action, or result.","check":"paired memory vs no-memory decision comparison","evidence_required":"decision trace and observable action delta","status":"LOCAL_PASS","evidence":"Run 33264580330: route A-B-C -> A-B-D; urgent provider Atlas -> Beacon with CHANGED_ACTION; routine Atlas terms constrained with CONSTRAINED_ACTION"}, + {"id":"SIBYL-GATE-004","track":"main","class":"PUBLISHED_HARD","severity":"HIGH","description":"README exposes critical memory write/read paths in under two minutes.","check":"cold reviewer repo navigation test","evidence_required":"README call map","status":"LOCAL_PASS","evidence":"Root README exposes Sibyl store/write/read/influence/conflict/deletion/evidence paths plus Base intent/verifier and Virtuals evidence boundary. Human cold-review timing remains pending."}, + {"id":"SCORE-MEM-001","track":"main","class":"PUBLISHED_SCORING","severity":"HIGH","description":"Memory use is sophisticated enough for top band; coordination/dynamic-state behavior preferred.","check":"judge-model scorecard","evidence_required":"non-trivial stateful scenario plus adversarial memory handling","status":"LOCAL_PASS","evidence":"Multi-execution experiential relationship memory changes urgent delegation and routine authority; expiry, post-recall tamper and unresolved-contradiction controls pass. Final evaluator score remains future."}, + {"id":"SCORE-TECH-001","track":"main","class":"PUBLISHED_SCORING","severity":"HIGH","description":"Build is clean, robust and survives a second run/curious judge.","check":"repeatability and negative test suite","evidence_required":"test report and second-run evidence","status":"LOCAL_PASS","evidence":"Head bd02738a: Engram CI 33264580330 and SAM Build 33264580331 succeeded, including exact evidence-capture smoke, hardened Base tests and fresh-decision Base artifact check"}, + {"id":"EXP-CONT-001","track":"main","class":"INTERNAL_ACCEPTANCE","severity":"HIGH","description":"Multiple attributable executions may form bounded relationship state only with sufficient lineage.","check":"multi-source provider history test","evidence_required":"two historical executions + admitting execution + relationship memory lineage","status":"LOCAL_PASS","evidence":"Two Atlas breach executions plus admitting execution produce CONTEXT_GUARDED relationship memory; one-breach and cross-task controls remain baseline"}, + {"id":"EXP-CONT-002","track":"main","class":"INTERNAL_ACCEPTANCE","severity":"HIGH","description":"Relationship state changes behavior contextually rather than becoming a global provider blacklist.","check":"urgent vs routine paired decisions","evidence_required":"urgent provider change plus routine authority narrowing using same memory","status":"LOCAL_PASS","evidence":"Urgent Atlas -> Beacon; routine stays Atlas while prepay 5000 -> 1000 bps and milestone verification false -> true"}, + {"id":"EXP-CONT-003","track":"main","class":"INTERNAL_ACCEPTANCE","severity":"HIGH","description":"Retrieval ranking must not silently adjudicate explicit contradictory memories.","check":"persist contradictory memories through Sibyl, fresh-recall both, attempt influence before and after explicit resolution","evidence_required":"both memories recall-visible; unresolved influence rejected; explicit supersession required before resolved influence","status":"LOCAL_PASS","evidence":"sibyl-competing-memories.test.ts rejects unresolved influence with UNRESOLVED_MEMORY_CONTRADICTION and accepts the resolved side only after SUPERSEDES evidence"}, + {"id":"BASE-CONF-001","track":"base","class":"INTERNAL_ACCEPTANCE","severity":"HIGH","description":"Engram memory-conditioned provider decisions deterministically change Base settlement authority, fresh Sibyl decisions emit the serialized settlement artifact, and receipt verification fails closed on divergent onchain evidence.","check":"run test:base, canonical check and Sibyl profile fresh-decision intent gate","evidence_required":"recipient/amount delta, direct fresh-decision artifact, retrieval provenance, serialized-intent integrity, wrong-chain/wrong-payer/wrong-recipient/wrong-amount/wrong-token/revert mutations","status":"LOCAL_PASS","evidence":"CI 33264580330: check job 99132270601 passed Base pressure; Sibyl job 99132270544 emitted and parsed the routine Base intent directly from fresh recall, requiring Atlas, 800000 atomic USDC, milestone verification and retrieval provenance."}, + {"id":"SUBMIT-001","track":"submission","class":"PUBLISHED_HARD","severity":"VETO","description":"Public MIT/Apache-2.0 repo with real commit history.","check":"repository inspection","evidence_required":"public repo/license/commits","status":"LOCAL_PASS"}, + {"id":"SUBMIT-002","track":"submission","class":"PUBLISHED_HARD","severity":"VETO","description":"2–5 minute demo includes fresh-session recall.","check":"video review","evidence_required":"public video","status":"UNVERIFIED"}, + {"id":"SUBMIT-003","track":"submission","class":"PUBLISHED_HARD","severity":"VETO","description":"README includes Prior Work declaration and required memory/partner notes.","check":"README audit","evidence_required":"merged README","status":"UNVERIFIED","evidence":"Branch README includes Prior Work, memory paths and Base/Virtuals local-vs-live boundaries; final submission-window truth freeze remains pending."}, + {"id":"SUBMIT-004","track":"submission","class":"PUBLISHED_HARD","severity":"VETO","description":"Two public posts: demo and build log, tagging required accounts.","check":"public URL verification","evidence_required":"two public post URLs","status":"UNVERIFIED"}, + {"id":"BASE-001","track":"base","class":"PUBLISHED_BONUS","severity":"OPTIONAL","description":"Executed Base action shown doing real product work downstream of remembered decision authority.","check":"decision-linked Base Sepolia receipt/explorer evidence","evidence_required":"Engram execution/retrieval/decision provenance + serialized settlement intent + expected payer + independently verified Base chain + successful matching Circle USDC action shown in demo","status":"UNVERIFIED","evidence":"Fresh Sibyl decision -> serialized Base intent and hardened verification are locally conformant. No real Base Sepolia settlement has been executed for the partner claim yet."}, + {"id":"VIRTUALS-001","track":"virtuals","class":"PUBLISHED_BONUS","severity":"OPTIONAL","description":"Virtuals-native integration is exercised in the demo.","check":"ACP/agent native evidence","evidence_required":"job/transaction/native receipt","status":"UNVERIFIED","evidence":"Server-only adapter normalizes current acp-cli v2 job-history JSON into Engram observations and virtuals:acp:ingest can fetch/ingest a live job. No authenticated ACP job has been executed yet; no multiplier claimed."} + ] +} diff --git a/hackathon/sibyl/templates/EVIDENCE_LEDGER.md b/hackathon/sibyl/templates/EVIDENCE_LEDGER.md new file mode 100644 index 0000000..73c4559 --- /dev/null +++ b/hackathon/sibyl/templates/EVIDENCE_LEDGER.md @@ -0,0 +1,77 @@ +# Evidence Ledger + +Allowed states: +`UNVERIFIED | SIMULATED_PASS | LOCAL_PASS | FORK_PASS | TESTNET_PASS | LIVE_PASS | PUBLIC_EVALUATOR_PASS | PRODUCTION_PASS | FAILED | BLOCKED` + +## Current claim ledger + +| Claim / gate | State | Evidence | Negative mutation | Notes | +|---|---|---|---|---| +| Sibyl persists decision-critical memory | LOCAL_PASS | GitHub Actions run `33264580330`, job `99132270544`, branch head `bd02738a57ebb4bde1bdc68bc5ff475b1bbdad64`: SDK 0.6.1 installed; route/provider/conflict memories persisted through Sibyl | missing Sibyl runtime | Pre-build-window evidence; must be re-run during official window | +| Fresh session/process recalls prior memory | LOCAL_PASS | run `33264580330`: separate seed and recall processes; route and provider relationship memory recalled in later processes; conflict test uses a fresh `SibylRuntimeStore` | terminate source process / unavailable Sibyl | Stronger than fresh-object-only proof | +| Recalled memory changes action | LOCAL_PASS | run `33264580330`: route `A-B-C -> A-B-D`; urgent provider `atlas -> beacon`; explicit `CHANGED_ACTION` traces | no-memory control | Paired behavioral deltas, not retrieval-only | +| Recalled memory constrains authority without global blacklist | LOCAL_PASS | run `33264580330`: fresh routine provider process keeps Atlas while prepay falls `5000 -> 1000` bps and milestone verification becomes required | routine-context control | Demonstrates contextual relationship posture rather than global reputation | +| Multi-execution experience becomes bounded relationship memory | LOCAL_PASS | run `33264580330`: two historical Atlas breach executions plus admitting execution produced `CONTEXT_GUARDED` relationship memory | single-failure fixture / cross-task fixture | Requires attributable lineage, not one bad event | +| Urgent provider experience changes outcome | LOCAL_PASS | run `33264580330`: no-memory Atlas path returns deterministic `SLA_BREACH`; memory-conditioned Beacon path returns `SUCCESS` | no-memory provider control | Application fixture, not a live provider-network claim | +| Deleting/unavailable Sibyl breaks/degrades core function | LOCAL_PASS | run `33264580330`: deletion CLI emitted degradation with no fallback | remove/disable Sibyl runtime | VETO | +| Engram provenance remains reconstructable | LOCAL_PASS | run `33264580330`: recall IDs, memory IDs, memory-state digests, counterfactuals, accepted influence and evaluation events remain in traces | post-recall memory tamper | Preserves recall-to-action lineage | +| Expired Sibyl memory is retrieved but blocked before exposure | LOCAL_PASS | run `33264580330`, `npm run test:sibyl` | expired memory fixture | Retrieval != legitimate influence | +| Post-recall memory tamper is rejected | LOCAL_PASS | run `33264580330`, `npm run test:sibyl` | overwrite memory after digest exposure | Protects recall-to-decision state integrity | +| Conflicting memory remains non-silently adjudicated | LOCAL_PASS | run `33264580330`, job `99132270544`: contradictory memories remain recall-visible, unresolved influence is rejected, explicit `SUPERSEDES` resolves the relevant side | unresolved `CONTRADICTS` pair | Ranking cannot silently become authority | +| Expanded Sibyl pressure suite | LOCAL_PASS | run `33264580330`, job `99132270544`: 3 test files / 7 tests passed, followed by route/provider/deletion process-boundary proof | expiry, tamper, contradiction, deletion, single-failure/cross-task controls | Repeatable on hosted runner | +| Self-verifying evidence capture path | LOCAL_PASS | run `33264580330`, job `99132270544`: exact evidence capture and manifest check passed | dirty source / unexpected untracked file; failed precursor run `33232299952` | Capture records git/source state, runtime/dependency digests, output hashes and final Sibyl DB digest | +| Base settlement authority follows Engram memory-conditioned decision | LOCAL_PASS | run `33264580330`, check job `99132270601`: urgent memory changes recipient Atlas -> Beacon; routine changes authorized prepay `4.000000 -> 0.800000 USDC` | no-memory decision / malformed address | Local causal-model evidence, not a live Base transaction | +| Fresh Sibyl decision emits exact Base authority artifact | LOCAL_PASS | run `33264580330`, Sibyl job `99132270544`: fresh routine recall wrote serialized Base intent; follow-up gate parsed it as Atlas + `800000` atomic USDC + milestone verification + retrieval provenance | omit provider addresses / malformed artifact | Closes manual translation gap between memory-conditioned decision and settlement artifact | +| Base serialized intent is internally bound | LOCAL_PASS | run `33264580330`, check job `99132270601`: strict schema fixes network/token/provider/address/provenance and rejects inconsistent atomic/decimal values | malformed schema, wrong chain, inconsistent amount | Removes unsafe JSON-cast ambiguity | +| Base receipt verification fails closed | LOCAL_PASS | run `33264580330`, check job `99132270601`: rejects wrong RPC chain, payer, recipient, amount, token and reverted transaction | chain/payer/recipient/amount/token/status mutations | Live verifier independently calls `eth_chainId` | +| Canonical Engram suite remains green with Sibyl + Base evaluated profiles | LOCAL_PASS | run `33264580330`, check job `99132270601`; SAM run `33264580331` | canonical test suite | Strongest tested code head `bd02738a57ebb4bde1bdc68bc5ff475b1bbdad64` | +| Root README exposes judged memory and partner call map | LOCAL_PASS | branch README exposes Sibyl write/read/influence/conflict/deletion/evidence, Base intent/verifier and Virtuals evidence boundary | cold reviewer navigation | Human under-two-minute timing remains pending; not `PUBLIC_EVALUATOR_PASS` | +| Hackathon-window rerun of core proof | UNVERIFIED | — | clean clone + fresh DB | Pre-window proof cannot substitute for final event-window evidence | +| Base integration does real product work onchain | UNVERIFIED | decision-derived intent + hardened verifier + live runbook exist; no real Base Sepolia settlement yet | remove Base action / mismatch settlement | `BASE-001` remains unearned | +| Virtuals integration does real product work | UNVERIFIED | adapter/conformance/live-ingest path implemented; no authenticated ACP job yet | remove Virtuals interaction | partner multiplier remains unclaimed | + +## Current flagship interpretation +The strongest current memory claim is not `the agent remembers Atlas is bad`. + +It is: + +> Multiple attributable executions become an agent-specific, task-specific relationship posture. In a fresh session that posture can change delegation entirely for urgent work, or narrow payment/verification authority for routine work, while preserving the same provider relationship. + +This is evidence-bounded experiential continuity, not a universal reputation score. + +The adversarial corollary is equally important: + +> Retrieval rank is not adjudication. If two persisted memories explicitly contradict each other, Engram may expose both but refuses to let either silently become authority until relationship evidence resolves the conflict. + +The economic consequence is now directly connected at local conformance level: + +> The fresh Sibyl-backed routine decision itself can emit the Base settlement artifact authorizing only `0.800000 USDC` upfront to Atlas, with retrieval provenance retained. There is no manual re-entry of the remembered decision between memory and settlement intent. + +A claimed Base receipt is accepted only when serialized intent, RPC chain, payer when bound, token, recipient, amount and success state agree. + +## Evidence-history note +Run `32753747711` remains an earlier pre-window baseline for the original six-test profile. + +Head `8f616545...` / run `33221823993` added the Sibyl-backed contradiction gate. + +The first exact capture-smoke run on head `45806957...`, CI `33232299952`, surfaced an evidence-harness failure after product/Sibyl phases passed: generated `package-lock.json` was treated as unexpected dirtiness. This is retained as harness-debug evidence, not hidden. + +Head `afb3ac37...` / run `33232373531` corrected and proved the self-verifying evidence-capture path. + +Head `266d3c38...` / run `33264157362` added first Base settlement-authority/receipt local conformance while preserving Sibyl. + +Head `b6ffe6fc...` / run `33264345953` hardened Base serialized-intent, chain and payer verification. + +The current strongest tested pre-window code baseline is `bd02738a57ebb4bde1bdc68bc5ff475b1bbdad64`: +- Engram CI `33264580330`: SUCCESS +- canonical check job `99132270601`: SUCCESS +- Sibyl profile job `99132270544`: SUCCESS +- fresh routine decision -> Base serialized-intent gate: SUCCESS +- deletion + exact evidence-capture smoke + manifest check: SUCCESS +- Engram SAM Build `33264580331`: SUCCESS + +Later documentation commits record this evidence but do not strengthen it. + +Every promotion above `LOCAL_PASS` must point to the exact hackathon-window artifact/run that supports it. + +No claim may be promoted above the smallest state actually supported by evidence. diff --git a/package.json b/package.json index 7441ad5..d98365b 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,19 @@ "mcp:stdio": "tsx packages/mcp-server/src/stdio.ts", "test": "vitest run tests/e2e/memory-caused-behavior.test.ts", "test:integration": "vitest run tests/integration/cockroach-memory-loop.test.ts", + "test:sibyl": "ENGRAM_SIBYL_TEST_REQUIRED=1 vitest run tests/integration/sibyl-memory-loop.test.ts tests/integration/sibyl-provider-continuity.test.ts tests/integration/sibyl-competing-memories.test.ts", + "test:virtuals": "vitest run tests/integration/virtuals-acp-evidence.test.ts", + "test:base": "vitest run tests/integration/base-settlement-authority.test.ts tests/integration/base-settlement-receipt.test.ts", + "demo:sibyl:seed": "tsx scripts/sibyl-demo.ts seed", + "demo:sibyl:recall": "tsx scripts/sibyl-demo.ts recall", + "demo:sibyl:no-memory-control": "tsx scripts/sibyl-demo.ts control", + "test:sibyl:deletion": "tsx scripts/sibyl-demo.ts deletion", + "demo:sibyl:provider:seed": "tsx scripts/sibyl-provider-demo.ts seed", + "demo:sibyl:provider:urgent": "tsx scripts/sibyl-provider-demo.ts urgent", + "demo:sibyl:provider:routine": "tsx scripts/sibyl-provider-demo.ts routine", + "evidence:sibyl:capture": "tsx scripts/sibyl-evidence-capture.ts", + "virtuals:acp:ingest": "tsx scripts/virtuals-acp-ingest.ts", + "base:settlement:verify": "tsx scripts/base-settlement-verify.ts", "test:all": "vitest run", "test:watch": "vitest", "check": "npm run build && npm run test:all" diff --git a/packages/base-settlement/README.md b/packages/base-settlement/README.md new file mode 100644 index 0000000..6c97f80 --- /dev/null +++ b/packages/base-settlement/README.md @@ -0,0 +1,125 @@ +# Base settlement authority adapter + +Status: `IMPLEMENTED / LOCAL_CONFORMANCE_PASS / LIVE_BASE_UNVERIFIED` + +This server-only package converts an Engram provider decision into a bounded Base Sepolia USDC settlement intent and independently verifies the resulting onchain receipt. + +It exists to make remembered experience economically consequential rather than merely observable. + +```text +prior execution evidence + -> Engram relationship memory + -> Sibyl persistence + -> fresh Engram recall + -> changed provider/terms + -> serialized Base settlement authority + -> explicit wallet execution + -> Base receipt verification +``` + +## Current evaluated network + +- network: Base Sepolia +- chain ID: `84532` +- asset: Circle USDC +- USDC contract: `0x036CbD53842c5426634e7929541eC2318f3dCF7e` +- decimals: `6` + +The contract address is pinned only for the evaluated testnet profile and must be reverified before the live hackathon run. + +## Causal product role + +### Urgent work +Without applicable memory, the application selects Atlas. With repeated Atlas SLA-breach memory, the fresh decision selects Beacon. + +Base therefore receives a different settlement recipient downstream of memory. + +### Routine work +Without memory, Atlas receives 50% prepayment authority on an $8 offer: `4.000000 USDC`. + +With applicable relationship memory, Atlas remains selected but prepayment authority falls to 10%: `0.800000 USDC`, with milestone verification required. + +Base therefore receives a smaller authorized economic action downstream of memory rather than a decorative transfer. + +## Local conformance + +Run: + +```bash +npm run test:base +``` + +The local suite proves: + +- urgent relationship memory changes settlement recipient Atlas -> Beacon; +- routine relationship memory changes authorized prepay `4.000000 -> 0.800000 USDC`; +- malformed provider addresses fail closed; +- serialized intents are strict-schema validated; +- serialized atomic/decimal amounts must recompute from max spend and prepay basis points; +- RPC chain must be Base Sepolia `84532` when chain evidence is supplied; +- expected payer may be bound to ERC-20 `Transfer.from`; +- wrong recipient, amount, token, payer, chain, or reverted transaction fails. + +Strongest tested code head: `bd02738a57ebb4bde1bdc68bc5ff475b1bbdad64`. + +- Engram CI `33264580330`: SUCCESS; +- canonical check `99132270601`: SUCCESS; +- full Sibyl profile `99132270544`: SUCCESS; +- SAM Build `33264580331`: SUCCESS. + +Critically, the Sibyl profile now proves the cross-layer boundary directly. A fresh routine provider decision, after persisted relationship-memory recall, emitted a serialized Base settlement intent. CI parsed that artifact and required: + +- provider: Atlas; +- authorized prepayment: `800000` atomic USDC; +- milestone verification: `true`; +- retrieval provenance: present. + +So Base authority is not only tested in isolation; the judged fresh-memory path produces the exact artifact consumed by the live settlement verifier. + +## Emitting an intent from the fresh provider decision + +Provide provider addresses and an output path when running the fresh decision: + +```bash +ENGRAM_BASE_ATLAS_ADDRESS='' \ +ENGRAM_BASE_BEACON_ADDRESS='' \ +ENGRAM_BASE_INTENT_OUT='artifacts/base/routine-intent.json' \ +npm run demo:sibyl:provider:routine +``` + +The demo writes `engram.base-settlement-intent/v1` directly from the memory-conditioned decision. No manual reconstruction of provider, amount, or retrieval provenance is required. + +## Live verification + +Once an explicit wallet step has executed the reviewed settlement intent: + +```bash +ENGRAM_BASE_RPC_URL='' \ + npm run base:settlement:verify -- \ + --intent \ + --tx-hash <0x...> \ + --payer +``` + +The verifier: + +1. strict-parses the serialized intent and recomputes its authorized amount; +2. independently calls `eth_chainId` and requires `84532`; +3. fetches `eth_getTransactionReceipt`; +4. emits Base evidence only if the successful Circle USDC `Transfer` matches expected payer when bound, exact recipient, and exact atomic amount. + +## Evidence boundary + +`LOCAL_CONFORMANCE_PASS` proves the causal settlement model, fresh-decision intent boundary, and fail-closed receipt verifier. It does **not** prove an executed Base transaction and does not earn the partner multiplier. + +`BASE-001` remains `UNVERIFIED` until a real Base Sepolia action is executed and retained with: + +- serialized decision-derived intent; +- transaction hash; +- independently observed chain ID; +- successful receipt; +- token transfer evidence matching payer when claimed, recipient and amount; +- Engram execution/retrieval/decision provenance; +- demonstration that removing or changing the remembered decision changes the Base action. + +No private key, wallet secret, or signer material belongs in this package or repository. diff --git a/packages/base-settlement/src/evidence.ts b/packages/base-settlement/src/evidence.ts new file mode 100644 index 0000000..032292a --- /dev/null +++ b/packages/base-settlement/src/evidence.ts @@ -0,0 +1,149 @@ +import { + BASE_SEPOLIA_CHAIN_ID, + BASE_SEPOLIA_USDC, + type BaseSettlementIntent, +} from "./index.js"; + +export const ERC20_TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; + +const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/; + +export type RpcLog = { + address: string; + topics: string[]; + data: string; + transactionHash?: string; + logIndex?: string; +}; + +export type RpcReceipt = { + transactionHash: string; + status: string; + blockNumber: string; + logs: RpcLog[]; +}; + +export type BaseSettlementEvidence = { + sourceSystem: "BASE"; + chainId: typeof BASE_SEPOLIA_CHAIN_ID; + transactionHash: string; + blockNumber: string; + tokenAddress: typeof BASE_SEPOLIA_USDC; + payer?: string; + recipient: string; + amountAtomic: string; + executionId: string; + retrievalId?: string; + decisionId?: string; + memoryRefs: string[]; + evidenceState: "OBSERVED"; +}; + +function normalizedAddress(value: string): string { + return value.toLowerCase(); +} + +function topicAddress(address: string): string { + if (!ADDRESS_RE.test(address)) throw new Error("INVALID_BASE_ADDRESS"); + return `0x${address.slice(2).toLowerCase().padStart(64, "0")}`; +} + +function addressFromTopic(topic: string): string { + if (!/^0x[0-9a-fA-F]{64}$/.test(topic)) throw new Error("INVALID_BASE_ADDRESS_TOPIC"); + return `0x${topic.slice(-40)}`.toLowerCase(); +} + +function parseHexUint(value: string): bigint { + if (!/^0x[0-9a-fA-F]+$/.test(value)) throw new Error("INVALID_BASE_UINT_HEX"); + return BigInt(value); +} + +export function verifyBaseSettlementReceipt(input: { + intent: BaseSettlementIntent; + receipt: RpcReceipt; + observedChainId?: number; + expectedPayer?: string; +}): BaseSettlementEvidence { + if (input.intent.chainId !== BASE_SEPOLIA_CHAIN_ID) throw new Error("BASE_CHAIN_MISMATCH"); + if (input.observedChainId !== undefined && input.observedChainId !== BASE_SEPOLIA_CHAIN_ID) { + throw new Error("BASE_RPC_CHAIN_MISMATCH"); + } + if (normalizedAddress(input.intent.tokenAddress) !== normalizedAddress(BASE_SEPOLIA_USDC)) { + throw new Error("BASE_TOKEN_MISMATCH"); + } + if (input.receipt.status.toLowerCase() !== "0x1") throw new Error("BASE_TRANSACTION_NOT_SUCCESSFUL"); + if (!/^0x[0-9a-fA-F]{64}$/.test(input.receipt.transactionHash)) throw new Error("INVALID_BASE_TRANSACTION_HASH"); + + const expectedRecipientTopic = topicAddress(input.intent.recipient); + const expectedPayerTopic = input.expectedPayer ? topicAddress(input.expectedPayer) : undefined; + const expectedAmount = input.intent.terms.authorizedPrepayAtomic; + const matching = input.receipt.logs.find((log) => + normalizedAddress(log.address) === normalizedAddress(BASE_SEPOLIA_USDC) + && log.topics[0]?.toLowerCase() === ERC20_TRANSFER_TOPIC + && log.topics[2]?.toLowerCase() === expectedRecipientTopic + && (expectedPayerTopic === undefined || log.topics[1]?.toLowerCase() === expectedPayerTopic) + && parseHexUint(log.data) === expectedAmount + ); + + if (!matching) throw new Error("BASE_USDC_TRANSFER_DOES_NOT_MATCH_INTENT"); + const observedPayer = matching.topics[1] ? addressFromTopic(matching.topics[1]) : undefined; + + return { + sourceSystem: "BASE", + chainId: BASE_SEPOLIA_CHAIN_ID, + transactionHash: input.receipt.transactionHash, + blockNumber: input.receipt.blockNumber, + tokenAddress: BASE_SEPOLIA_USDC, + payer: observedPayer, + recipient: input.intent.recipient, + amountAtomic: expectedAmount.toString(), + executionId: input.intent.provenance.executionId, + retrievalId: input.intent.provenance.retrievalId, + decisionId: input.intent.provenance.decisionId, + memoryRefs: [...input.intent.memoryRefs], + evidenceState: "OBSERVED", + }; +} + +async function rpcCall(input: { + rpcUrl: string; + method: string; + params: unknown[]; + fetchImpl?: typeof fetch; +}): Promise { + const fetchImpl = input.fetchImpl ?? fetch; + const response = await fetchImpl(input.rpcUrl, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: input.method, params: input.params }), + }); + if (!response.ok) throw new Error(`BASE_RPC_HTTP_${response.status}`); + const payload = await response.json() as { result?: T | null; error?: unknown }; + if (payload.error) throw new Error(`BASE_RPC_ERROR: ${JSON.stringify(payload.error)}`); + if (payload.result === null || payload.result === undefined) { + throw new Error(`BASE_RPC_RESULT_MISSING: ${input.method}`); + } + return payload.result; +} + +export async function fetchBaseChainId(input: { + rpcUrl: string; + fetchImpl?: typeof fetch; +}): Promise { + const result = await rpcCall({ ...input, method: "eth_chainId", params: [] }); + const chainId = Number(parseHexUint(result)); + if (!Number.isSafeInteger(chainId)) throw new Error("INVALID_BASE_CHAIN_ID"); + return chainId; +} + +export async function fetchBaseTransactionReceipt(input: { + rpcUrl: string; + transactionHash: string; + fetchImpl?: typeof fetch; +}): Promise { + return rpcCall({ + ...input, + method: "eth_getTransactionReceipt", + params: [input.transactionHash], + }); +} diff --git a/packages/base-settlement/src/index.ts b/packages/base-settlement/src/index.ts new file mode 100644 index 0000000..e6f58d7 --- /dev/null +++ b/packages/base-settlement/src/index.ts @@ -0,0 +1,204 @@ +import { z } from "zod"; +import type { + ProviderDecision, + ProviderId, + ProviderTerms, +} from "../../scenarios/provider-continuity/src/index.js"; + +export const BASE_SEPOLIA_CHAIN_ID = 84_532; +export const BASE_SEPOLIA_NETWORK = "base-sepolia" as const; +export const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" as const; +export const USDC_DECIMALS = 6; + +const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/; +const AddressSchema = z.string().regex(ADDRESS_RE); +const DecimalAtomicSchema = z.string().regex(/^\d+$/); +const UsdcDecimalSchema = z.string().regex(/^\d+\.\d{6}$/); +const ProviderIdSchema = z.enum(["atlas", "beacon"]); + +export type ProviderAddressBook = Record; + +export type SettlementProvenance = { + executionId: string; + retrievalId?: string; + decisionId?: string; +}; + +export type BaseSettlementTerms = { + maxSpendUsd: number; + prepayBps: number; + authorizedPrepayAtomic: bigint; + authorizedPrepayUsd: string; + requireMilestoneVerification: boolean; +}; + +export type BaseSettlementCounterfactual = { + providerId: ProviderId; + recipient: string; + terms: BaseSettlementTerms; +}; + +export type BaseSettlementIntent = { + schema: "engram.base-settlement-intent/v1"; + chainId: typeof BASE_SEPOLIA_CHAIN_ID; + network: typeof BASE_SEPOLIA_NETWORK; + token: "USDC"; + tokenAddress: typeof BASE_SEPOLIA_USDC; + providerId: ProviderId; + recipient: string; + terms: BaseSettlementTerms; + memoryRefs: string[]; + provenance: SettlementProvenance; + counterfactual?: BaseSettlementCounterfactual; +}; + +export type SerializedBaseSettlementIntent = Omit & { + terms: Omit & { authorizedPrepayAtomic: string }; + counterfactual?: Omit & { + terms: Omit & { authorizedPrepayAtomic: string }; + }; +}; + +const SerializedTermsSchema = z.object({ + maxSpendUsd: z.number().finite().nonnegative(), + prepayBps: z.number().int().min(0).max(10_000), + authorizedPrepayAtomic: DecimalAtomicSchema, + authorizedPrepayUsd: UsdcDecimalSchema, + requireMilestoneVerification: z.boolean(), +}).strict(); + +const SerializedIntentSchema = z.object({ + schema: z.literal("engram.base-settlement-intent/v1"), + chainId: z.literal(BASE_SEPOLIA_CHAIN_ID), + network: z.literal(BASE_SEPOLIA_NETWORK), + token: z.literal("USDC"), + tokenAddress: z.literal(BASE_SEPOLIA_USDC), + providerId: ProviderIdSchema, + recipient: AddressSchema, + terms: SerializedTermsSchema, + memoryRefs: z.array(z.string().min(1)), + provenance: z.object({ + executionId: z.string().min(1), + retrievalId: z.string().min(1).optional(), + decisionId: z.string().min(1).optional(), + }).strict(), + counterfactual: z.object({ + providerId: ProviderIdSchema, + recipient: AddressSchema, + terms: SerializedTermsSchema, + }).strict().optional(), +}).strict(); + +function assertAddress(address: string, label: string): string { + if (!ADDRESS_RE.test(address)) throw new Error(`INVALID_${label.toUpperCase()}_ADDRESS`); + return address; +} + +function usdToUsdcAtomic(value: number): bigint { + if (!Number.isFinite(value) || value < 0) throw new Error("INVALID_USD_AMOUNT"); + const scaled = Math.round(value * 1_000_000); + if (Math.abs((scaled / 1_000_000) - value) > 1e-9) { + throw new Error("USD_AMOUNT_EXCEEDS_USDC_PRECISION"); + } + return BigInt(scaled); +} + +export function usdcAtomicToDecimal(value: bigint): string { + if (value < 0n) throw new Error("NEGATIVE_USDC_AMOUNT"); + const whole = value / 1_000_000n; + const fraction = (value % 1_000_000n).toString().padStart(6, "0"); + return `${whole}.${fraction}`; +} + +function settlementTerms(terms: ProviderTerms): BaseSettlementTerms { + if (!Number.isInteger(terms.prepayBps) || terms.prepayBps < 0 || terms.prepayBps > 10_000) { + throw new Error("INVALID_PREPAY_BPS"); + } + const maxSpendAtomic = usdToUsdcAtomic(terms.maxSpendUsd); + const authorizedPrepayAtomic = (maxSpendAtomic * BigInt(terms.prepayBps)) / 10_000n; + return { + maxSpendUsd: terms.maxSpendUsd, + prepayBps: terms.prepayBps, + authorizedPrepayAtomic, + authorizedPrepayUsd: usdcAtomicToDecimal(authorizedPrepayAtomic), + requireMilestoneVerification: terms.requireMilestoneVerification, + }; +} + +function assertSerializedTermsConsistent(terms: SerializedBaseSettlementIntent["terms"]): void { + const expectedMaxSpendAtomic = usdToUsdcAtomic(terms.maxSpendUsd); + const expectedPrepayAtomic = (expectedMaxSpendAtomic * BigInt(terms.prepayBps)) / 10_000n; + if (BigInt(terms.authorizedPrepayAtomic) !== expectedPrepayAtomic) { + throw new Error("BASE_SETTLEMENT_INTENT_AMOUNT_INCONSISTENT"); + } + if (terms.authorizedPrepayUsd !== usdcAtomicToDecimal(expectedPrepayAtomic)) { + throw new Error("BASE_SETTLEMENT_INTENT_DECIMAL_INCONSISTENT"); + } +} + +export function serializeBaseSettlementIntent(intent: BaseSettlementIntent): SerializedBaseSettlementIntent { + return { + ...intent, + terms: { + ...intent.terms, + authorizedPrepayAtomic: intent.terms.authorizedPrepayAtomic.toString(), + }, + counterfactual: intent.counterfactual ? { + ...intent.counterfactual, + terms: { + ...intent.counterfactual.terms, + authorizedPrepayAtomic: intent.counterfactual.terms.authorizedPrepayAtomic.toString(), + }, + } : undefined, + }; +} + +export function parseSerializedBaseSettlementIntent(input: unknown): BaseSettlementIntent { + const parsed = SerializedIntentSchema.parse(input) as SerializedBaseSettlementIntent; + assertSerializedTermsConsistent(parsed.terms); + if (parsed.counterfactual) assertSerializedTermsConsistent(parsed.counterfactual.terms); + + return { + ...parsed, + terms: { + ...parsed.terms, + authorizedPrepayAtomic: BigInt(parsed.terms.authorizedPrepayAtomic), + }, + counterfactual: parsed.counterfactual ? { + ...parsed.counterfactual, + terms: { + ...parsed.counterfactual.terms, + authorizedPrepayAtomic: BigInt(parsed.counterfactual.terms.authorizedPrepayAtomic), + }, + } : undefined, + }; +} + +export function deriveBaseSettlementIntent(input: { + decision: ProviderDecision; + addresses: ProviderAddressBook; + provenance: SettlementProvenance; +}): BaseSettlementIntent { + const recipient = assertAddress(input.addresses[input.decision.providerId], "provider"); + const counterfactual = input.decision.counterfactual + ? { + providerId: input.decision.counterfactual.providerId, + recipient: assertAddress(input.addresses[input.decision.counterfactual.providerId], "counterfactual_provider"), + terms: settlementTerms(input.decision.counterfactual.terms), + } + : undefined; + + return { + schema: "engram.base-settlement-intent/v1", + chainId: BASE_SEPOLIA_CHAIN_ID, + network: BASE_SEPOLIA_NETWORK, + token: "USDC", + tokenAddress: BASE_SEPOLIA_USDC, + providerId: input.decision.providerId, + recipient, + terms: settlementTerms(input.decision.terms), + memoryRefs: [...input.decision.memoryRefs], + provenance: input.provenance, + counterfactual, + }; +} diff --git a/packages/scenarios/provider-continuity/src/index.ts b/packages/scenarios/provider-continuity/src/index.ts new file mode 100644 index 0000000..b0d7852 --- /dev/null +++ b/packages/scenarios/provider-continuity/src/index.ts @@ -0,0 +1,160 @@ +import type { OperationalMemory } from "../../../memory-core/src/domain.js"; + +export type ProviderId = "atlas" | "beacon"; +export type ProviderUrgency = "URGENT" | "ROUTINE"; + +export type ProviderContinuityContext = { + workflowType: "agent_provider_selection"; + taskType: "data_fetch"; + urgency: ProviderUrgency; + budgetUsd: number; + maxLatencySeconds: number; + environmentVersion: string; +}; + +export type ProviderOffer = { + providerId: ProviderId; + priceUsd: number; + expectedLatencySeconds: number; +}; + +export type ProviderTerms = { + prepayBps: number; + requireMilestoneVerification: boolean; + maxSpendUsd: number; +}; + +export type ProviderDecision = { + providerId: ProviderId; + terms: ProviderTerms; + memoryRefs: string[]; + reason: string; + counterfactual?: { + providerId: ProviderId; + terms: ProviderTerms; + }; +}; + +export type ProviderExecutionResult = { + status: "SUCCESS" | "FAILURE"; + providerId: ProviderId; + latencySeconds: number; + failureType?: "SLA_BREACH"; +}; + +export type RecalledProviderMemory = { + memory: OperationalMemory; + finalScore: number; +}; + +const BASELINE_PREPAY_BPS = 5_000; +const GUARDED_PREPAY_BPS = 1_000; + +function terms(maxSpendUsd: number, guarded = false): ProviderTerms { + return { + prepayBps: guarded ? GUARDED_PREPAY_BPS : BASELINE_PREPAY_BPS, + requireMilestoneVerification: guarded, + maxSpendUsd, + }; +} + +function eligibleOffers(context: ProviderContinuityContext, offers: ProviderOffer[]): ProviderOffer[] { + return offers + .filter((offer) => offer.priceUsd <= context.budgetUsd) + .filter((offer) => offer.expectedLatencySeconds <= context.maxLatencySeconds) + .sort((a, b) => a.priceUsd - b.priceUsd || a.expectedLatencySeconds - b.expectedLatencySeconds); +} + +function applicableRelationshipMemory( + context: ProviderContinuityContext, + memories: RecalledProviderMemory[], +): RecalledProviderMemory | undefined { + return memories.find(({ memory, finalScore }) => { + const c = memory.structuredContext; + const sourceIds = Array.isArray(c.sourceExecutionIds) ? c.sourceExecutionIds : []; + return finalScore >= 0.65 + && memory.confidence >= 0.8 + && c.workflowType === context.workflowType + && c.memoryPrimitive === "EXPERIENTIAL_RELATIONSHIP" + && c.taskType === context.taskType + && c.providerId === "atlas" + && c.relationshipPosture === "CONTEXT_GUARDED" + && c.failureType === "SLA_BREACH" + && Number(c.breachCount ?? 0) >= 2 + && sourceIds.length >= 2; + }); +} + +export function decideProviderEngagement(input: { + context: ProviderContinuityContext; + offers: ProviderOffer[]; + memories: RecalledProviderMemory[]; +}): ProviderDecision { + const eligible = eligibleOffers(input.context, input.offers); + if (!eligible.length) throw new Error("NO_ELIGIBLE_PROVIDER_OFFER"); + + const baseline = eligible[0]!; + const baselineTerms = terms(baseline.priceUsd); + const relationship = applicableRelationshipMemory(input.context, input.memories); + + if (!relationship) { + return { + providerId: baseline.providerId, + terms: baselineTerms, + memoryRefs: [], + reason: "No applicable experiential relationship memory changes the cheapest eligible provider offer.", + }; + } + + const guardedProviderId = relationship.memory.structuredContext.providerId as ProviderId; + + if (input.context.urgency === "URGENT" && guardedProviderId === baseline.providerId) { + const alternative = eligible.find((offer) => offer.providerId !== guardedProviderId); + if (alternative) { + return { + providerId: alternative.providerId, + terms: terms(alternative.priceUsd), + memoryRefs: [relationship.memory.id], + reason: "Repeated agent-specific SLA breaches make Atlas ineligible for this urgent task; select the next eligible provider.", + counterfactual: { providerId: baseline.providerId, terms: baselineTerms }, + }; + } + } + + if (baseline.providerId === guardedProviderId) { + return { + providerId: baseline.providerId, + terms: terms(baseline.priceUsd, true), + memoryRefs: [relationship.memory.id], + reason: "Atlas remains economical for a routine task, but prior relationship experience reduces prepayment authority and requires milestone verification.", + counterfactual: { providerId: baseline.providerId, terms: baselineTerms }, + }; + } + + return { + providerId: baseline.providerId, + terms: baselineTerms, + memoryRefs: [], + reason: "The remembered provider is not on the selected path, so it does not affect this engagement.", + }; +} + +export function executeProviderEngagement( + decision: ProviderDecision, + context: ProviderContinuityContext, +): ProviderExecutionResult { + if (decision.providerId === "atlas" && context.urgency === "URGENT") { + return { + status: "FAILURE", + providerId: decision.providerId, + latencySeconds: context.maxLatencySeconds + 25, + failureType: "SLA_BREACH", + }; + } + + return { + status: "SUCCESS", + providerId: decision.providerId, + latencySeconds: decision.providerId === "beacon" ? 18 : 24, + }; +} diff --git a/packages/sibyl/bridge.py b/packages/sibyl/bridge.py new file mode 100644 index 0000000..c4daf98 --- /dev/null +++ b/packages/sibyl/bridge.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Thin JSON bridge from Engram's TypeScript runtime to Sibyl MemoryClient. + +This intentionally uses only the public sibyl_memory_client.MemoryClient API. +The evaluated profile must not bypass Sibyl by reaching into its SQLite schema. +""" +from __future__ import annotations + +import json +import os +import sys +from pathlib import Path +from typing import Any + +from sibyl_memory_client import MemoryClient + + +def _client() -> MemoryClient: + path = Path(os.environ.get("ENGRAM_SIBYL_DB", "~/.sibyl-memory/engram-hackathon.db")).expanduser() + tenant = os.environ.get("ENGRAM_SIBYL_TENANT", "engram-hackathon") + return MemoryClient.local(path, tenant_id=tenant) + + +def _safe_get(memory: MemoryClient, category: str, name: str) -> dict[str, Any] | None: + try: + return memory.get_entity(category, name) + except Exception as exc: + if exc.__class__.__name__ == "NotFoundError": + return None + raise + + +def _entities(memory: MemoryClient, category: str, limit: int = 10000) -> list[dict[str, Any]]: + return memory.list_entities(category=category, limit=limit) + + +def _body(row: dict[str, Any] | None) -> Any: + return None if row is None else row.get("body") + + +def _put(memory: MemoryClient, category: str, name: str, body: dict[str, Any]) -> dict[str, Any]: + row = memory.set_entity(category, name, body) + memory.write_event( + acted=[f"engram:{category}:write"], + extra={"category": category, "name": name, "executionId": body.get("executionId") or body.get("id")}, + ) + return row + + +def handle(req: dict[str, Any]) -> Any: + memory = _client() + op = req["op"] + args = req.get("args", {}) + + if op == "put": + return _put(memory, args["category"], args["name"], args["body"]) + if op == "get": + return _body(_safe_get(memory, args["category"], args["name"])) + if op == "list": + rows = _entities(memory, args["category"], int(args.get("limit", 10000))) + return [row["body"] for row in rows] + if op == "search_memories": + query = args["query"] + limit = min(max(int(args.get("limit", 8)), 1), 50) + agent_id = args["agentId"] + # Ask Sibyl's documented WARM-tier FTS5 search for a larger pool, then + # enforce Engram's agent boundary and result limit in the adapter. + rows = memory.search_entities(query, category="operational_memory", limit=min(limit * 8, 400)) + out = [] + for row in rows: + body = row.get("body") or {} + if body.get("agentId") != agent_id: + continue + out.append({"memory": body, "sibyl": {"entityId": row.get("id"), "updatedAt": row.get("updated_at")}}) + if len(out) >= limit: + break + return out + if op == "delete_memory": + return memory.delete_entity("operational_memory", args["memoryId"]) + if op == "ping": + return {"tenant": memory.get_tenant(), "schemaVersion": memory.schema_version()} + raise ValueError(f"unsupported operation: {op}") + + +def main() -> None: + try: + req = json.load(sys.stdin) + result = handle(req) + json.dump({"ok": True, "result": result}, sys.stdout, default=str) + except Exception as exc: + json.dump({"ok": False, "error": {"type": exc.__class__.__name__, "message": str(exc)}}, sys.stdout) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/packages/sibyl/requirements.txt b/packages/sibyl/requirements.txt new file mode 100644 index 0000000..11b3121 --- /dev/null +++ b/packages/sibyl/requirements.txt @@ -0,0 +1,2 @@ +# Re-verify the latest published version at the start of the Sep 1 build window. +sibyl-memory-client==0.6.1 diff --git a/packages/sibyl/src/runtime-store.ts b/packages/sibyl/src/runtime-store.ts new file mode 100644 index 0000000..548f41a --- /dev/null +++ b/packages/sibyl/src/runtime-store.ts @@ -0,0 +1,329 @@ +import { randomUUID } from "node:crypto"; +import { spawn } from "node:child_process"; +import { resolve } from "node:path"; +import type { EvidenceState, MemoryRecall } from "../../core/src/protocol.js"; +import type { + ExecutionContext, + ExecutionEvent, + MemorySearchInput, + MemorySearchResult, + OperationalMemory, + Outcome, +} from "../../memory-core/src/domain.js"; +import { scoreMemory } from "../../memory-core/src/policy.js"; +import type { EngramRuntimeStore } from "../../runtime/src/store.js"; +import type { + RecallExposureUpdate, + RuntimeDecisionRecord, + RuntimeEvaluationEvent, + RuntimeExecutionRecord, +} from "../../runtime/src/types.js"; + +function resolveBridgePath(): string { + return process.env.ENGRAM_SIBYL_BRIDGE ?? resolve(process.cwd(), "packages/sibyl/bridge.py"); +} + +type BridgeResponse = { ok: true; result: T } | { ok: false; error: { type: string; message: string } }; + +async function bridge(op: string, args: Record = {}): Promise { + const python = process.env.ENGRAM_SIBYL_PYTHON ?? "python3"; + return new Promise((resolveResult, reject) => { + const child = spawn(python, [resolveBridgePath()], { + env: process.env, + stdio: ["pipe", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (chunk) => { stdout += chunk; }); + child.stderr.on("data", (chunk) => { stderr += chunk; }); + child.on("error", reject); + child.on("close", (code) => { + let parsed: BridgeResponse | undefined; + try { + parsed = JSON.parse(stdout) as BridgeResponse; + } catch { + reject(new Error(`SIBYL_BRIDGE_INVALID_RESPONSE: exit=${code}; stderr=${stderr}; stdout=${stdout}`)); + return; + } + if (!parsed.ok) { + reject(new Error(`SIBYL_${parsed.error.type}: ${parsed.error.message}`)); + return; + } + resolveResult(parsed.result); + }); + child.stdin.end(JSON.stringify({ op, args })); + }); +} + +function iso(value: Date | undefined): string | undefined { + return value?.toISOString(); +} + +function asDate(value: unknown): Date | undefined { + return typeof value === "string" ? new Date(value) : undefined; +} + +function serializeMemory(memory: OperationalMemory): Record { + return { + ...memory, + validFrom: iso(memory.validFrom), + validUntil: iso(memory.validUntil), + }; +} + +function deserializeMemory(raw: Record): OperationalMemory { + return { + id: String(raw.id), + agentId: String(raw.agentId), + memoryType: String(raw.memoryType), + summary: String(raw.summary), + structuredContext: (raw.structuredContext ?? {}) as Record, + confidence: Number(raw.confidence), + evidenceState: raw.evidenceState as OperationalMemory["evidenceState"], + validFrom: asDate(raw.validFrom), + validUntil: asDate(raw.validUntil), + environmentVersion: typeof raw.environmentVersion === "string" ? raw.environmentVersion : undefined, + toolVersion: typeof raw.toolVersion === "string" ? raw.toolVersion : undefined, + policyVersion: typeof raw.policyVersion === "string" ? raw.policyVersion : undefined, + }; +} + +async function put(category: string, name: string, body: Record): Promise { + await bridge("put", { category, name, body }); +} + +async function get(category: string, name: string): Promise { + return bridge("get", { category, name }); +} + +async function list(category: string): Promise { + return bridge("list", { category }); +} + +export class SibylRuntimeStore implements EngramRuntimeStore { + async ping(): Promise<{ tenant: string; schemaVersion: number | null }> { + return bridge("ping"); + } + + async startExecution(input: ExecutionContext): Promise<{ executionId: string }> { + const executionId = randomUUID(); + const record: Record = { + id: executionId, + ...input, + status: "RUNNING", + startedAt: new Date().toISOString(), + }; + await put("execution", executionId, record); + return { executionId }; + } + + async getExecution(executionId: string): Promise { + const raw = await get>("execution", executionId); + if (!raw) return null; + return { + id: String(raw.id), + agentId: String(raw.agentId), + agentVersion: typeof raw.agentVersion === "string" ? raw.agentVersion : undefined, + workflowType: String(raw.workflowType), + intent: String(raw.intent), + context: (raw.context ?? {}) as Record, + constraints: (raw.constraints ?? {}) as Record, + environmentVersion: typeof raw.environmentVersion === "string" ? raw.environmentVersion : undefined, + toolVersion: typeof raw.toolVersion === "string" ? raw.toolVersion : undefined, + policyVersion: typeof raw.policyVersion === "string" ? raw.policyVersion : undefined, + memoryPolicyBundleVersion: typeof raw.memoryPolicyBundleVersion === "string" ? raw.memoryPolicyBundleVersion : undefined, + status: raw.status as RuntimeExecutionRecord["status"], + startedAt: new Date(String(raw.startedAt)), + completedAt: typeof raw.completedAt === "string" ? new Date(raw.completedAt) : undefined, + }; + } + + async setExecutionMemoryPolicy(executionId: string, bundleVersion: string): Promise { + const execution = await get>("execution", executionId); + if (!execution) throw new Error(`Execution ${executionId} does not exist`); + const current = execution.memoryPolicyBundleVersion; + if (current && current !== bundleVersion) { + throw new Error(`EXECUTION_POLICY_FROZEN: execution ${executionId} already uses ${String(current)}`); + } + await put("execution", executionId, { ...execution, memoryPolicyBundleVersion: bundleVersion }); + } + + async nextEventSequence(executionId: string): Promise { + const events = await list>("execution_event"); + return events.filter((event) => event.executionId === executionId).length; + } + + async appendEvent(event: ExecutionEvent): Promise { + const key = `${event.executionId}-${event.sequenceNo}`; + const existing = await get>("execution_event", key); + const body = { ...event, occurredAt: event.occurredAt.toISOString() }; + if (existing) { + if (JSON.stringify(existing) === JSON.stringify(body)) return; + throw new Error(`EVENT_IDEMPOTENCY_CONFLICT:${event.executionId}:${event.sequenceNo}`); + } + await put("execution_event", key, body); + } + + async recordOutcome(outcome: Outcome): Promise { + const existing = await get>("outcome", outcome.executionId); + const body = { ...outcome } as Record; + if (existing && JSON.stringify(existing) !== JSON.stringify(body)) { + throw new Error(`OUTCOME_IDEMPOTENCY_CONFLICT:${outcome.executionId}`); + } + await put("outcome", outcome.executionId, body); + const execution = await get>("execution", outcome.executionId); + if (!execution) throw new Error(`Execution ${outcome.executionId} does not exist`); + await put("execution", outcome.executionId, { + ...execution, + status: outcome.status, + completedAt: new Date().toISOString(), + }); + } + + async getOutcomeEvidenceState(executionId: string): Promise { + const outcome = await get>("outcome", executionId); + return (outcome?.evidenceState as EvidenceState | undefined) ?? null; + } + + async persistMemory(memory: OperationalMemory, sourceExecutionIds: string[]): Promise { + if (!sourceExecutionIds.length) throw new Error("Operational memory requires at least one source execution"); + await put("operational_memory", memory.id, { + ...serializeMemory(memory), + sourceExecutionIds, + searchText: `${memory.summary} ${JSON.stringify(memory.structuredContext)}`, + }); + } + + async getMemory(memoryId: string): Promise { + const raw = await get>("operational_memory", memoryId); + return raw ? deserializeMemory(raw) : null; + } + + async searchMemory(input: MemorySearchInput): Promise { + const retrievalId = randomUUID(); + const limit = Math.min(Math.max(input.limit ?? 8, 1), 50); + const rows = await bridge; sibyl: Record }>>( + "search_memories", + { query: input.query, agentId: input.agentId, limit }, + ); + const now = Date.now(); + const candidates = rows + .map((row, index) => { + const memory = deserializeMemory(row.memory); + const context = memory.structuredContext; + if (input.workflowType && context.workflowType !== input.workflowType) return null; + if (input.environmentVersion && memory.environmentVersion !== input.environmentVersion) return null; + if (input.status?.length && !input.status.includes(context.outcome as (typeof input.status)[number])) return null; + const semanticScore = Math.max(0.55, 1 - index * 0.08); + const contextScore = input.workflowType && context.workflowType === input.workflowType ? 1 : 0.75; + const outcomeScore = ["FAILURE", "COMPENSATED", "PARTIAL", "ABORTED", "UNKNOWN"].includes(String(context.outcome ?? "")) ? 1 : 0.5; + const ageDays = memory.validFrom ? Math.max(0, (now - memory.validFrom.getTime()) / 86_400_000) : 0; + const recencyScore = Math.max(0, 1 - ageDays / 30); + const finalScore = scoreMemory({ memory, semanticScore, contextScore, outcomeScore, recencyScore }); + return { + memoryId: memory.id, + memory, + semanticScore, + contextScore, + outcomeScore, + confidenceScore: memory.confidence, + recencyScore, + finalScore, + rank: index + 1, + }; + }) + .filter((value): value is NonNullable => value !== null) + .sort((a, b) => b.finalScore - a.finalScore) + .map((value, index) => ({ ...value, rank: index + 1 })) + .slice(0, limit); + + await put("recall", retrievalId, { + id: retrievalId, + executionId: input.executionId, + query: input.query, + policyVersion: input.retrievalPolicyVersion ?? "engram-sibyl-v1", + recalledAt: new Date().toISOString(), + candidates: candidates.map((candidate) => ({ + memoryId: candidate.memoryId, + rank: candidate.rank, + score: candidate.finalScore, + })), + rejected: [], + }); + return { retrievalId, candidates }; + } + + async getRecalls(executionId: string): Promise { + const rows = await list>("recall"); + return rows + .filter((row) => row.executionId === executionId) + .map((row) => ({ + id: String(row.id), + executionId, + query: String(row.query), + policyVersion: String(row.policyVersion ?? "engram-sibyl-v1"), + recalledAt: new Date(String(row.recalledAt)), + candidates: ((row.candidates ?? []) as Array>).map((candidate) => ({ + retrievalId: String(row.id), + memoryId: String(candidate.memoryId), + memoryStateDigest: typeof candidate.memoryStateDigest === "string" ? candidate.memoryStateDigest : undefined, + rank: Number(candidate.rank), + score: candidate.score === undefined ? undefined : Number(candidate.score), + })), + })); + } + + async updateRecallExposure(update: RecallExposureUpdate): Promise { + const recall = await get>("recall", update.retrievalId); + if (!recall) throw new Error(`Recall ${update.retrievalId} does not exist`); + const scoreByMemory = new Map( + ((recall.candidates ?? []) as Array>).map((candidate) => [String(candidate.memoryId), candidate.score]), + ); + await put("recall", update.retrievalId, { + ...recall, + candidates: update.exposedMemoryStates.map((state, index) => ({ + memoryId: state.memoryId, + memoryStateDigest: state.memoryStateDigest, + rank: index + 1, + score: scoreByMemory.get(state.memoryId), + })), + rejected: update.rejected, + }); + } + + async recordRuntimeDecision(decision: RuntimeDecisionRecord): Promise { + await put("decision", decision.id, { + ...decision, + decidedAt: decision.decidedAt.toISOString(), + } as unknown as Record); + } + + async appendRuntimeEvaluationEvent(event: RuntimeEvaluationEvent): Promise { + await put("runtime_evaluation", event.id, { + ...event, + createdAt: event.createdAt.toISOString(), + }); + } + + async getTrace(executionId: string): Promise { + const [execution, events, outcome, recalls, decisions, runtimeEvaluations] = await Promise.all([ + this.getExecution(executionId), + list>("execution_event"), + get>("outcome", executionId), + this.getRecalls(executionId), + list>("decision"), + list>("runtime_evaluation"), + ]); + return { + execution, + events: events.filter((row) => row.executionId === executionId), + outcome, + retrievals: recalls, + decisions: decisions.filter((row) => row.executionId === executionId), + runtimeEvaluations: runtimeEvaluations.filter((row) => row.executionId === executionId), + memoryBackend: "sibyl-memory-client", + }; + } +} diff --git a/packages/virtuals-acp/README.md b/packages/virtuals-acp/README.md new file mode 100644 index 0000000..224769d --- /dev/null +++ b/packages/virtuals-acp/README.md @@ -0,0 +1,96 @@ +# Virtuals ACP evidence adapter + +Status: `IMPLEMENTED / LOCAL_CONFORMANCE_PASS / LIVE_ACP_UNVERIFIED` + +This server-only package normalizes machine-readable output from the current Virtuals `@virtuals-protocol/acp-cli` into Engram execution evidence. + +It does **not** let Virtuals decide Engram memory semantics. The boundary is: + +```text +Virtuals ACP job/history + -> normalize observed job evidence + -> Engram observation + -> Engram admission / eligibility / influence + -> Sibyl persistent memory +``` + +## Current first-party CLI contract +The adapter targets the maintained `@virtuals-protocol/acp-cli` v2 history command: + +```bash +acp job history --job-id --chain-id --json +``` + +The CLI currently emits a JSON object containing `jobId`, `chainId`, `protocol`, `status`, `entryCount`, and `entries` for v2 jobs. Every ACP CLI command supports `--json`. + +Testnet is selected globally: + +```bash +export IS_TESTNET=true +``` + +The deterministic conformance fixture uses Base Sepolia chain ID `84532`, which is exposed by the current Virtuals CLI chain configuration. The fixture is synthetic and can never promote the partner evidence state above local conformance. + +## Example + +```ts +import { + acpHistoryToExecutionEvidence, + acpEvidenceToEngramObservation, +} from "./src/evidence.js"; + +const evidence = acpHistoryToExecutionEvidence(jobHistoryJson, { + providerId: "0xProvider", + taskType: "data_fetch", + urgency: "URGENT", + expectedLatencySeconds: 30, + startedAt, + completedAt, +}); + +await runtime.observe({ + executionId, + ...acpEvidenceToEngramObservation(evidence), +}); +``` + +## Local conformance pressure + +Run: + +```bash +npm run test:virtuals +``` + +The suite checks: + +- current v2 history shape (`jobId`, `chainId`, `protocol`, `status`, `entryCount`, `entries`); +- no invented failure on an on-time completed job; +- `SLA_BREACH` only when explicit expected latency and observed timestamps justify it; +- `rejected` and `expired` remain distinct failure classes; +- malformed or invalid history fails closed. + +Fixture: + +```text +tests/fixtures/virtuals-acp/completed-job-history.json +``` + +Test: + +```text +tests/integration/virtuals-acp-evidence.test.ts +``` + +Current conformance state is `LOCAL_CONFORMANCE_PASS`: the adapter suite is included by the repository's canonical `npm run test:all` / `npm run check` path and has passed in CI. This is code-contract evidence only; it is not evidence of a real ACP interaction. + +## Evidence boundary +- ACP job history is treated as `OBSERVED` external-system evidence only when it comes from the live ACP command. A repository fixture is `SIMULATED_PASS`/local conformance evidence, never live partner evidence. +- SLA breach classification requires caller-supplied expected latency plus observed start/completion timestamps; the adapter does not invent SLA expectations from provider reputation. +- `rejected` and `expired` jobs are preserved as distinct failure classes and take causal precedence over a derived latency classification. +- Raw parsed history is retained in the normalized evidence object for audit/debugging, but the Engram observation records only bounded fields plus a provenance command reference. + +## Live-proof gate +This package is not evidence that the Virtuals partner multiplier has been earned. + +Promotion requires an actual authenticated ACP testnet flow with a real job/agent interaction, retained job ID/chain ID/history, and a later Engram/Sibyl decision whose behavior changes because of that prior ACP experience. diff --git a/packages/virtuals-acp/src/cli.ts b/packages/virtuals-acp/src/cli.ts new file mode 100644 index 0000000..3d9e4d5 --- /dev/null +++ b/packages/virtuals-acp/src/cli.ts @@ -0,0 +1,72 @@ +import { spawn } from "node:child_process"; + +export type AcpCliResult = { + stdout: string; + stderr: string; + json: unknown; +}; + +function commandPrefix(): { command: string; args: string[] } { + const configured = process.env.ENGRAM_ACP_BIN?.trim(); + if (configured) return { command: configured, args: [] }; + return { command: "npx", args: ["--yes", "@virtuals-protocol/acp-cli"] }; +} + +export async function runAcpJson(args: string[], options: { testnet?: boolean } = {}): Promise { + const prefix = commandPrefix(); + const commandArgs = [...prefix.args, ...args, "--json"]; + + return new Promise((resolve, reject) => { + const child = spawn(prefix.command, commandArgs, { + env: { + ...process.env, + ...(options.testnet === undefined ? {} : { IS_TESTNET: options.testnet ? "true" : "false" }), + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (chunk) => { stdout += chunk; }); + child.stderr.on("data", (chunk) => { stderr += chunk; }); + child.on("error", reject); + child.on("close", (code) => { + const trimmed = stdout.trim(); + if (code !== 0) { + reject(new Error(`ACP_CLI_FAILED: exit=${code}; stderr=${stderr.trim()}; stdout=${trimmed}`)); + return; + } + if (!trimmed) { + reject(new Error(`ACP_CLI_EMPTY_RESPONSE: stderr=${stderr.trim()}`)); + return; + } + try { + const json = JSON.parse(trimmed); + if (json && typeof json === "object" && "error" in json) { + reject(new Error(`ACP_CLI_ERROR_RESPONSE: ${JSON.stringify(json)}`)); + return; + } + resolve({ stdout, stderr, json }); + } catch (error) { + reject(new Error(`ACP_CLI_INVALID_JSON: ${error instanceof Error ? error.message : String(error)}; stdout=${trimmed}`)); + } + }); + }); +} + +export async function fetchAcpJobHistory(input: { + jobId: string; + chainId: number; + testnet?: boolean; +}): Promise { + return (await runAcpJson([ + "job", + "history", + "--job-id", + input.jobId, + "--chain-id", + String(input.chainId), + ], { testnet: input.testnet })).json; +} diff --git a/packages/virtuals-acp/src/evidence.ts b/packages/virtuals-acp/src/evidence.ts new file mode 100644 index 0000000..fb08e08 --- /dev/null +++ b/packages/virtuals-acp/src/evidence.ts @@ -0,0 +1,124 @@ +import { z } from "zod"; +import type { EvidenceState } from "../../core/src/protocol.js"; + +const AcpHistorySchema = z.object({ + jobId: z.union([z.string(), z.number()]).transform(String), + chainId: z.coerce.number().int().positive(), + protocol: z.string().optional(), + legacy: z.boolean().optional(), + status: z.string().min(1), + entryCount: z.coerce.number().int().nonnegative().optional(), + entries: z.array(z.unknown()).default([]), +}).passthrough(); + +export type AcpJobHistory = z.infer; + +export type AcpProviderExecutionEvidence = { + sourceSystem: "VIRTUALS_ACP"; + jobId: string; + chainId: number; + protocol: string; + status: string; + providerId: string; + taskType: string; + urgency: "URGENT" | "ROUTINE"; + expectedLatencySeconds?: number; + observedLatencySeconds?: number; + failureType?: "SLA_BREACH" | "JOB_REJECTED" | "JOB_EXPIRED" | "UNKNOWN_FAILURE"; + evidenceState: EvidenceState; + historyEntryCount: number; + rawHistory: AcpJobHistory; +}; + +export type AcpEvidenceContext = { + providerId: string; + taskType: string; + urgency: "URGENT" | "ROUTINE"; + expectedLatencySeconds?: number; + startedAt?: Date; + completedAt?: Date; +}; + +export function parseAcpJobHistory(input: unknown): AcpJobHistory { + return AcpHistorySchema.parse(input); +} + +export function acpHistoryToExecutionEvidence( + input: unknown, + context: AcpEvidenceContext, +): AcpProviderExecutionEvidence { + const history = parseAcpJobHistory(input); + const status = history.status.toLowerCase(); + const observedLatencySeconds = context.startedAt && context.completedAt + ? Math.max(0, (context.completedAt.getTime() - context.startedAt.getTime()) / 1000) + : undefined; + + let failureType: AcpProviderExecutionEvidence["failureType"]; + if (status === "rejected") failureType = "JOB_REJECTED"; + else if (status === "expired") failureType = "JOB_EXPIRED"; + else if (!["completed", "submitted", "funded", "budget_set", "open"].includes(status)) { + failureType = "UNKNOWN_FAILURE"; + } + + // Latency is a secondary classification. It may classify an otherwise + // successful/in-flight job as an SLA breach, but must not erase a stronger + // terminal cause such as ACP rejection or expiry. + if ( + failureType === undefined + && context.expectedLatencySeconds !== undefined + && observedLatencySeconds !== undefined + && observedLatencySeconds > context.expectedLatencySeconds + ) { + failureType = "SLA_BREACH"; + } + + return { + sourceSystem: "VIRTUALS_ACP", + jobId: history.jobId, + chainId: history.chainId, + protocol: history.protocol ?? (history.legacy ? "legacy" : "v2"), + status: history.status, + providerId: context.providerId, + taskType: context.taskType, + urgency: context.urgency, + expectedLatencySeconds: context.expectedLatencySeconds, + observedLatencySeconds, + failureType, + evidenceState: "OBSERVED", + historyEntryCount: history.entryCount ?? history.entries.length, + rawHistory: history, + }; +} + +export function acpEvidenceToEngramObservation(evidence: AcpProviderExecutionEvidence): { + type: "VIRTUALS_ACP_JOB_OBSERVED"; + payload: Record; + evidenceState: EvidenceState; + provenance: Array>; +} { + return { + type: "VIRTUALS_ACP_JOB_OBSERVED", + payload: { + sourceSystem: evidence.sourceSystem, + providerId: evidence.providerId, + taskType: evidence.taskType, + urgency: evidence.urgency, + jobId: evidence.jobId, + chainId: evidence.chainId, + protocol: evidence.protocol, + status: evidence.status, + expectedLatencySeconds: evidence.expectedLatencySeconds, + observedLatencySeconds: evidence.observedLatencySeconds, + failureType: evidence.failureType, + historyEntryCount: evidence.historyEntryCount, + }, + evidenceState: evidence.evidenceState, + provenance: [{ + source: "Virtuals ACP", + command: `acp job history --job-id ${evidence.jobId} --chain-id ${evidence.chainId} --json`, + jobId: evidence.jobId, + chainId: evidence.chainId, + protocol: evidence.protocol, + }], + }; +} diff --git a/scripts/base-settlement-verify.ts b/scripts/base-settlement-verify.ts new file mode 100644 index 0000000..63cd8b8 --- /dev/null +++ b/scripts/base-settlement-verify.ts @@ -0,0 +1,73 @@ +import { createHash } from "node:crypto"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { + fetchBaseChainId, + fetchBaseTransactionReceipt, + verifyBaseSettlementReceipt, +} from "../packages/base-settlement/src/evidence.js"; +import { parseSerializedBaseSettlementIntent } from "../packages/base-settlement/src/index.js"; + +function arg(name: string): string | undefined { + const index = process.argv.indexOf(name); + return index >= 0 ? process.argv[index + 1] : undefined; +} + +function sha256(value: Buffer | string): string { + return createHash("sha256").update(value).digest("hex"); +} + +const intentPathArg = arg("--intent"); +const transactionHash = arg("--tx-hash"); +const rpcUrl = arg("--rpc-url") ?? process.env.ENGRAM_BASE_RPC_URL; +const expectedPayer = arg("--payer") ?? process.env.ENGRAM_BASE_EXPECTED_PAYER; +const outPathArg = arg("--out") ?? process.env.ENGRAM_BASE_EVIDENCE_OUT; + +if (!intentPathArg || !transactionHash || !rpcUrl) { + throw new Error( + "Usage: npm run base:settlement:verify -- --intent --tx-hash <0x...> [--rpc-url ] [--payer <0x...>] [--out ] " + + "or set ENGRAM_BASE_RPC_URL / ENGRAM_BASE_EXPECTED_PAYER / ENGRAM_BASE_EVIDENCE_OUT", + ); +} + +const intentPath = resolve(intentPathArg); +const intentBytes = await readFile(intentPath); +const raw = JSON.parse(intentBytes.toString("utf8")) as unknown; +const intent = parseSerializedBaseSettlementIntent(raw); +const observedChainId = await fetchBaseChainId({ rpcUrl }); +const receipt = await fetchBaseTransactionReceipt({ rpcUrl, transactionHash }); +const settlement = verifyBaseSettlementReceipt({ + intent, + receipt, + observedChainId, + expectedPayer, +}); + +const evidence = { + schema: "engram.base-live-settlement-evidence/v1", + capturedAt: new Date().toISOString(), + intent: { + file: intentPath, + sha256: sha256(intentBytes), + executionId: intent.provenance.executionId, + retrievalId: intent.provenance.retrievalId, + decisionId: intent.provenance.decisionId, + memoryRefs: intent.memoryRefs, + }, + verification: { + observedChainId, + expectedPayer: expectedPayer ?? null, + transactionHash, + receiptBlockNumber: receipt.blockNumber, + }, + settlement, + evidenceBoundary: "This file records independently verified Base settlement evidence. It is only partner-qualifying when the transaction itself occurred in the eligible build window and the referenced intent came from the retained fresh-memory decision path.", +}; + +if (outPathArg) { + const outPath = resolve(outPathArg); + await mkdir(dirname(outPath), { recursive: true }); + await writeFile(outPath, `${JSON.stringify(evidence, null, 2)}\n`, "utf8"); +} + +process.stdout.write(`${JSON.stringify(evidence, null, 2)}\n`); diff --git a/scripts/sibyl-demo.ts b/scripts/sibyl-demo.ts new file mode 100644 index 0000000..e998e23 --- /dev/null +++ b/scripts/sibyl-demo.ts @@ -0,0 +1,175 @@ +import { SibylRuntimeStore } from "../packages/sibyl/src/runtime-store.js"; +import { EngramRuntime } from "../packages/runtime/src/runtime.js"; +import { DEFAULT_RUNTIME_POLICIES } from "../packages/runtime/src/defaults.js"; +import { decideRoute } from "../packages/memory-core/src/policy.js"; + +const command = process.argv[2]; + +function emit(value: unknown): void { + process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); +} + +async function seed(): Promise { + const store = new SibylRuntimeStore(); + const runtime = new EngramRuntime(store, DEFAULT_RUNTIME_POLICIES); + const backend = await store.ping(); + + const run = await runtime.startExecution({ + agentId: "agent-demo", + workflowType: "multi_venue_execution", + intent: "route value through available venues", + context: { liquidityClass: "thin", demoPhase: "seed" }, + constraints: {}, + }); + + const completed = await runtime.complete({ + executionId: run.executionId, + status: "COMPENSATED", + summary: "Venue C lacked required liquidity; recovery through D succeeded.", + result: { baselineRoute: ["A", "B", "C"], failedVenue: "C", recoveryVenue: "D" }, + evidenceState: "OBSERVED", + admissionSignals: [{ + kind: "UNEXPECTED_FAILURE", + summary: "Venue C failed with LIQUIDITY_UNAVAILABLE under thin liquidity; prefer D under comparable conditions.", + evidenceState: "OBSERVED", + confidence: 0.91, + details: { + failureType: "LIQUIDITY_UNAVAILABLE", + failedVenue: "C", + recoveryVenue: "D", + liquidityClass: "thin", + }, + }], + }); + + emit({ + phase: "seed", + backend: "sibyl-memory-client", + sibyl: backend, + executionId: run.executionId, + admittedMemoryIds: completed.admittedMemories.map((memory) => memory.id), + instruction: "Terminate this process. Run `npm run demo:sibyl:recall` in a new process against the same ENGRAM_SIBYL_DB and tenant.", + }); +} + +async function recall(): Promise { + const store = new SibylRuntimeStore(); + const runtime = new EngramRuntime(store, DEFAULT_RUNTIME_POLICIES); + const backend = await store.ping(); + + const run = await runtime.startExecution({ + agentId: "agent-demo", + workflowType: "multi_venue_execution", + intent: "route value through available venues", + context: { liquidityClass: "thin", demoPhase: "fresh-recall" }, + constraints: {}, + }); + + const recalled = await runtime.recall({ + executionId: run.executionId, + query: "Venue C failed LIQUIDITY_UNAVAILABLE", + }); + + const control = decideRoute({ memories: [], memoryAvailable: true }); + const treatment = decideRoute({ + memoryAvailable: true, + memories: recalled.candidates.map((candidate) => ({ + memory: candidate.memory, + semanticScore: candidate.semanticScore, + contextScore: candidate.contextScore, + outcomeScore: candidate.outcomeScore, + recencyScore: candidate.recencyScore, + })), + }); + + if (recalled.candidates[0] && treatment.memoryRefs.length) { + await runtime.recordDecision({ + executionId: run.executionId, + decisionType: "ROUTE_SELECTION", + selectedAction: { route: treatment.route }, + alternatives: [{ route: control.route }], + reasoningSummary: treatment.reason, + influences: [{ + memoryId: recalled.candidates[0].memory.id, + retrievalId: recalled.recall.id, + influenceType: "CHANGED_ACTION", + summary: "A persisted prior execution changed route selection from C to D.", + counterfactual: { + action: { route: control.route }, + source: "APPLICATION_DECLARED", + evidenceState: "OBSERVED", + explanation: "The same route policy without recalled memory selects Route C.", + }, + }], + }); + } + + emit({ + phase: "fresh-recall", + backend: "sibyl-memory-client", + sibyl: backend, + executionId: run.executionId, + retrievalId: recalled.recall.id, + recalledMemoryIds: recalled.candidates.map((candidate) => candidate.memory.id), + controlRoute: control.route, + memoryConditionedRoute: treatment.route, + changedAction: JSON.stringify(control.route) !== JSON.stringify(treatment.route), + trace: await runtime.trace(run.executionId), + }); +} + +async function noMemoryControl(): Promise { + const decision = decideRoute({ memories: [], memoryAvailable: false }); + emit({ + phase: "no-memory-control", + memoryAvailable: false, + route: decision.route, + memoryRefs: decision.memoryRefs, + reason: decision.reason, + }); +} + +async function deletionMutation(): Promise { + const previous = process.env.ENGRAM_SIBYL_PYTHON; + process.env.ENGRAM_SIBYL_PYTHON = process.env.ENGRAM_SIBYL_MISSING_PYTHON ?? "/definitely-missing-sibyl-python"; + let deletionObserved = false; + let observedError = ""; + try { + const store = new SibylRuntimeStore(); + await store.ping(); + } catch (error) { + deletionObserved = true; + observedError = error instanceof Error ? error.message : String(error); + } finally { + if (previous === undefined) delete process.env.ENGRAM_SIBYL_PYTHON; + else process.env.ENGRAM_SIBYL_PYTHON = previous; + } + + if (!deletionObserved) { + throw new Error("DELETION_MUTATION_FAILED: Sibyl unexpectedly remained available"); + } + + emit({ + phase: "sibyl-deletion-mutation", + degraded: true, + fallbackAvailable: false, + error: observedError, + }); +} + +switch (command) { + case "seed": + await seed(); + break; + case "recall": + await recall(); + break; + case "control": + await noMemoryControl(); + break; + case "deletion": + await deletionMutation(); + break; + default: + throw new Error("Usage: tsx scripts/sibyl-demo.ts "); +} diff --git a/scripts/sibyl-evidence-capture.ts b/scripts/sibyl-evidence-capture.ts new file mode 100644 index 0000000..b29ffac --- /dev/null +++ b/scripts/sibyl-evidence-capture.ts @@ -0,0 +1,246 @@ +import { createHash } from "node:crypto"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { spawnSync } from "node:child_process"; +import { resolve } from "node:path"; + +function safeStamp(date = new Date()): string { + return date.toISOString().replace(/[:.]/g, "-"); +} + +function commandOutput(command: string, args: string[]): string { + const result = spawnSync(command, args, { encoding: "utf8", cwd: process.cwd() }); + if (result.status !== 0) return "unknown"; + return result.stdout.trim() || result.stderr.trim() || "unknown"; +} + +function commandStdoutAllowEmpty(command: string, args: string[]): string | null { + const result = spawnSync(command, args, { encoding: "utf8", cwd: process.cwd() }); + if (result.status !== 0) return null; + return result.stdout.trim(); +} + +function gitHead(): string { + return commandOutput("git", ["rev-parse", "HEAD"]); +} + +async function sha256File(path: string): Promise { + try { + const body = await readFile(path); + return createHash("sha256").update(body).digest("hex"); + } catch { + return null; + } +} + +async function readJsonObject(path: string): Promise | null> { + try { + const parsed = JSON.parse(await readFile(path, "utf8")) as unknown; + return parsed && typeof parsed === "object" && !Array.isArray(parsed) + ? parsed as Record + : null; + } catch { + return null; + } +} + +type Step = { + id: string; + args: string[]; + env?: Record; +}; + +type StepReceipt = { + id: string; + command: string; + exitCode: number | null; + stdoutFile: string; + stdoutSha256: string | null; + stderrFile: string; + stderrSha256: string | null; + startedAt: string; + completedAt: string; +}; + +const capturedAt = new Date(); +const head = gitHead(); +const stamp = safeStamp(capturedAt); +const sourceTreeStatusResult = commandStdoutAllowEmpty("git", ["status", "--porcelain"]); +const sourceTreeStatus = sourceTreeStatusResult ?? "unknown"; +const sourceTreeStatusLines = sourceTreeStatusResult === null || sourceTreeStatusResult.length === 0 + ? [] + : sourceTreeStatusResult.split("\n").map((line) => line.trimEnd()).filter(Boolean); + +// `npm install` currently creates this untracked file because the repository +// does not commit a package lock. It is setup residue, not source drift. It is +// still hashed into the evidence manifest if present. +const allowedGeneratedStatusLines = new Set(["?? package-lock.json"]); +const allowedGeneratedStatus = sourceTreeStatusLines.filter((line) => allowedGeneratedStatusLines.has(line)); +const unexpectedSourceTreeStatus = sourceTreeStatusLines.filter((line) => !allowedGeneratedStatusLines.has(line)); +const submissionSourceClean = sourceTreeStatusResult !== null && unexpectedSourceTreeStatus.length === 0; +const literallyClean = sourceTreeStatusResult !== null && sourceTreeStatusLines.length === 0; +const allowDirty = process.env.ENGRAM_EVIDENCE_ALLOW_DIRTY === "1"; + +if (!submissionSourceClean && !allowDirty) { + throw new Error( + `EVIDENCE_SOURCE_TREE_DIRTY: commit-stamped evidence requires no tracked source drift or unexpected untracked files. ` + + `Set ENGRAM_EVIDENCE_ALLOW_DIRTY=1 only for non-submission diagnostics. ` + + `unexpected=${JSON.stringify(unexpectedSourceTreeStatus)}`, + ); +} + +const outputDir = resolve( + process.env.ENGRAM_EVIDENCE_DIR ?? `artifacts/sibyl-evidence/${stamp}-${head.slice(0, 12)}`, +); +const dbPath = resolve(outputDir, "sibyl-evidence.db"); +const baseRoutineIntentPath = resolve(outputDir, "base-routine-intent.json"); +const tenant = process.env.ENGRAM_SIBYL_TENANT ?? `engram-evidence-${stamp}`; +const python = process.env.ENGRAM_SIBYL_PYTHON ?? "python3"; + +// Deterministic non-secret addresses used only to bind the local evidence +// artifact. Official-window live execution must replace these with reviewed +// Base Sepolia requester/provider addresses before any wallet action. +const evidenceAtlasAddress = "0x1111111111111111111111111111111111111111"; +const evidenceBeaconAddress = "0x2222222222222222222222222222222222222222"; + +const steps: Step[] = [ + { id: "sibyl-pressure-suite", args: ["run", "test:sibyl"] }, + { id: "base-conformance-suite", args: ["run", "test:base"] }, + { id: "route-seed", args: ["run", "demo:sibyl:seed"] }, + { id: "route-fresh-recall", args: ["run", "demo:sibyl:recall"] }, + { id: "route-no-memory-control", args: ["run", "demo:sibyl:no-memory-control"] }, + { id: "provider-history", args: ["run", "demo:sibyl:provider:seed"] }, + { id: "provider-fresh-urgent", args: ["run", "demo:sibyl:provider:urgent"] }, + { + id: "provider-fresh-routine-base-intent", + args: ["run", "demo:sibyl:provider:routine"], + env: { + ENGRAM_BASE_INTENT_OUT: baseRoutineIntentPath, + ENGRAM_BASE_ATLAS_ADDRESS: evidenceAtlasAddress, + ENGRAM_BASE_BEACON_ADDRESS: evidenceBeaconAddress, + }, + }, + { id: "sibyl-deletion-mutation", args: ["run", "test:sibyl:deletion"] }, +]; + +await mkdir(outputDir, { recursive: true }); + +const baseEnv = { + ...process.env, + ENGRAM_SIBYL_DB: dbPath, + ENGRAM_SIBYL_TENANT: tenant, +}; + +const receipts: StepReceipt[] = []; +let failed = false; + +for (const step of steps) { + const startedAt = new Date(); + const result = spawnSync("npm", step.args, { + encoding: "utf8", + env: { ...baseEnv, ...step.env }, + cwd: process.cwd(), + }); + const completedAt = new Date(); + const stdoutFile = `${step.id}.stdout.txt`; + const stderrFile = `${step.id}.stderr.txt`; + const stdoutPath = resolve(outputDir, stdoutFile); + const stderrPath = resolve(outputDir, stderrFile); + await writeFile(stdoutPath, result.stdout ?? "", "utf8"); + await writeFile(stderrPath, result.stderr ?? "", "utf8"); + + receipts.push({ + id: step.id, + command: `npm ${step.args.join(" ")}`, + exitCode: result.status, + stdoutFile, + stdoutSha256: await sha256File(stdoutPath), + stderrFile, + stderrSha256: await sha256File(stderrPath), + startedAt: startedAt.toISOString(), + completedAt: completedAt.toISOString(), + }); + + if (result.status !== 0) { + failed = true; + break; + } +} + +const baseRoutineIntent = await readJsonObject(baseRoutineIntentPath); +if (!failed) { + const terms = baseRoutineIntent?.terms as Record | undefined; + const provenance = baseRoutineIntent?.provenance as Record | undefined; + const memoryRefs = baseRoutineIntent?.memoryRefs; + const validBaseIntent = baseRoutineIntent?.schema === "engram.base-settlement-intent/v1" + && baseRoutineIntent?.chainId === 84532 + && baseRoutineIntent?.providerId === "atlas" + && baseRoutineIntent?.recipient === evidenceAtlasAddress + && terms?.authorizedPrepayAtomic === "800000" + && terms?.requireMilestoneVerification === true + && typeof provenance?.executionId === "string" + && typeof provenance?.retrievalId === "string" + && Array.isArray(memoryRefs) + && memoryRefs.length > 0; + if (!validBaseIntent) failed = true; +} + +const packageJsonPath = resolve(process.cwd(), "package.json"); +const packageLockPath = resolve(process.cwd(), "package-lock.json"); +const sibylRequirementsPath = resolve(process.cwd(), "packages/sibyl/requirements.txt"); +const dbSha256 = await sha256File(dbPath); +const baseRoutineIntentSha256 = await sha256File(baseRoutineIntentPath); + +const manifest = { + schema: "engram.sibyl-evidence-capture/v3", + capturedAt: capturedAt.toISOString(), + repository: commandOutput("git", ["remote", "get-url", "origin"]), + gitHead: head, + sourceTree: { + submissionSourceClean, + literallyClean, + rawStatus: sourceTreeStatus, + allowedGeneratedStatus, + unexpectedStatus: unexpectedSourceTreeStatus, + dirtyOverrideUsed: allowDirty && !submissionSourceClean, + }, + tenant, + environment: { + platform: process.platform, + arch: process.arch, + node: process.version, + npm: commandOutput("npm", ["--version"]), + python: commandOutput(python, ["--version"]), + sibylMemoryClient: commandOutput(python, [ + "-c", + "import importlib.metadata as m; print(m.version('sibyl-memory-client'))", + ]), + }, + dependencyDigests: { + packageJsonSha256: await sha256File(packageJsonPath), + generatedPackageLockSha256: await sha256File(packageLockPath), + sibylRequirementsSha256: await sha256File(sibylRequirementsPath), + }, + dbFile: "sibyl-evidence.db", + dbSha256, + baseSettlementIntent: baseRoutineIntent ? { + file: "base-routine-intent.json", + sha256: baseRoutineIntentSha256, + schema: baseRoutineIntent.schema, + chainId: baseRoutineIntent.chainId, + providerId: baseRoutineIntent.providerId, + recipient: baseRoutineIntent.recipient, + terms: baseRoutineIntent.terms, + provenance: baseRoutineIntent.provenance, + memoryRefs: baseRoutineIntent.memoryRefs, + liveExecutionBoundary: "This artifact binds memory-conditioned authority only. The deterministic addresses are not live partner addresses and no transaction is executed by evidence capture.", + } : null, + status: failed ? "FAILED" : "LOCAL_PASS", + evidenceBoundary: "This capture is local/CI evidence unless the surrounding run is itself an eligible public evaluator or live partner execution. The retained Base intent is decision-authority evidence only; it is not a Base transaction receipt.", + integrityNote: "SHA-256 values bind retained stdout/stderr, dependency manifests/setup lockfile when present, the final Sibyl database, and the memory-conditioned Base settlement intent to this manifest. The manifest itself is the root receipt and should be retained with the artifact.", + steps: receipts, +}; + +await writeFile(resolve(outputDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8"); + +process.stdout.write(`${JSON.stringify({ outputDir, ...manifest }, null, 2)}\n`); +if (failed) process.exit(1); diff --git a/scripts/sibyl-provider-demo.ts b/scripts/sibyl-provider-demo.ts new file mode 100644 index 0000000..ceeb09e --- /dev/null +++ b/scripts/sibyl-provider-demo.ts @@ -0,0 +1,222 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { EngramRuntime } from "../packages/runtime/src/runtime.js"; +import { DEFAULT_RUNTIME_POLICIES } from "../packages/runtime/src/defaults.js"; +import { SibylRuntimeStore } from "../packages/sibyl/src/runtime-store.js"; +import { + deriveBaseSettlementIntent, + serializeBaseSettlementIntent, +} from "../packages/base-settlement/src/index.js"; +import { + decideProviderEngagement, + executeProviderEngagement, + type ProviderContinuityContext, + type ProviderOffer, +} from "../packages/scenarios/provider-continuity/src/index.js"; + +const command = process.argv[2]; +const offers: ProviderOffer[] = [ + { providerId: "atlas", priceUsd: 8, expectedLatencySeconds: 20 }, + { providerId: "beacon", priceUsd: 11, expectedLatencySeconds: 18 }, +]; + +const baseContext: ProviderContinuityContext = { + workflowType: "agent_provider_selection", + taskType: "data_fetch", + urgency: "URGENT", + budgetUsd: 20, + maxLatencySeconds: 30, + environmentVersion: "provider-market-v1", +}; + +function emit(value: unknown): void { + process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); +} + +async function recordBreach(runtime: EngramRuntime, attempt: number): Promise { + const run = await runtime.startExecution({ + agentId: "requester-agent", + workflowType: "agent_provider_selection", + intent: "obtain urgent data from an eligible provider", + context: { taskType: "data_fetch", urgency: "URGENT", providerId: "atlas", attempt }, + constraints: { maxLatencySeconds: 30 }, + environmentVersion: "provider-market-v1", + }); + await runtime.observe({ + executionId: run.executionId, + type: "PROVIDER_SLA_BREACH", + payload: { providerId: "atlas", taskType: "data_fetch", latencySeconds: 55 + attempt }, + evidenceState: "OBSERVED", + provenance: [{ source: "provider-execution", attempt }], + }); + await runtime.complete({ + executionId: run.executionId, + status: "PARTIAL", + failureType: "SLA_BREACH", + summary: `Atlas breached the urgent SLA on attempt ${attempt}.`, + result: { providerId: "atlas", failureType: "SLA_BREACH" }, + evidenceState: "OBSERVED", + }); + return run.executionId; +} + +async function seed(): Promise { + const store = new SibylRuntimeStore(); + const runtime = new EngramRuntime(store, DEFAULT_RUNTIME_POLICIES); + const backend = await store.ping(); + const first = await recordBreach(runtime, 1); + const second = await recordBreach(runtime, 2); + const admitting = await runtime.startExecution({ + agentId: "requester-agent", + workflowType: "agent_provider_selection", + intent: "consolidate repeated provider execution experience", + context: { taskType: "data_fetch", providerId: "atlas", purpose: "relationship-state-update" }, + constraints: {}, + environmentVersion: "provider-market-v1", + }); + const completed = await runtime.complete({ + executionId: admitting.executionId, + status: "PARTIAL", + summary: "Two requester-owned Atlas executions breached the urgent data-fetch SLA.", + result: { providerId: "atlas", repeatedPattern: true }, + evidenceState: "OBSERVED", + admissionSignals: [{ + kind: "REPEATED_PATTERN", + summary: "Across two requester-owned executions, Atlas repeatedly breached urgent data-fetch SLAs. Guard urgent delegation and reduce prepayment authority on routine work.", + evidenceState: "OBSERVED", + confidence: 0.92, + sourceExecutionIds: [first, second, admitting.executionId], + details: { + memoryPrimitive: "EXPERIENTIAL_RELATIONSHIP", + taskType: "data_fetch", + providerId: "atlas", + relationshipPosture: "CONTEXT_GUARDED", + failureType: "SLA_BREACH", + breachCount: 2, + }, + }], + }); + emit({ + phase: "provider-history", + backend: "sibyl-memory-client", + sibyl: backend, + historicalExecutionIds: [first, second], + admittingExecutionId: admitting.executionId, + admittedMemoryIds: completed.admittedMemories.map((memory) => memory.id), + relationshipPosture: "CONTEXT_GUARDED", + instruction: "Terminate this process, then run urgent/routine commands against the same Sibyl DB and tenant.", + }); +} + +async function maybeWriteBaseIntent(input: { + executionId: string; + retrievalId: string; + treatment: ReturnType; +}): Promise<{ intentPath: string; intent: ReturnType } | undefined> { + const out = process.env.ENGRAM_BASE_INTENT_OUT; + if (!out) return undefined; + + const atlas = process.env.ENGRAM_BASE_ATLAS_ADDRESS; + const beacon = process.env.ENGRAM_BASE_BEACON_ADDRESS; + if (!atlas || !beacon) { + throw new Error("BASE_INTENT_REQUIRES_ENGRAM_BASE_ATLAS_ADDRESS_AND_ENGRAM_BASE_BEACON_ADDRESS"); + } + + const serialized = serializeBaseSettlementIntent(deriveBaseSettlementIntent({ + decision: input.treatment, + addresses: { atlas, beacon }, + provenance: { + executionId: input.executionId, + retrievalId: input.retrievalId, + }, + })); + const intentPath = resolve(out); + await mkdir(dirname(intentPath), { recursive: true }); + await writeFile(intentPath, `${JSON.stringify(serialized, null, 2)}\n`, "utf8"); + return { intentPath, intent: serialized }; +} + +async function decide(urgency: "URGENT" | "ROUTINE"): Promise { + const store = new SibylRuntimeStore(); + const runtime = new EngramRuntime(store, DEFAULT_RUNTIME_POLICIES); + const backend = await store.ping(); + const run = await runtime.startExecution({ + agentId: "requester-agent", + workflowType: "agent_provider_selection", + intent: `obtain ${urgency.toLowerCase()} data from an eligible provider`, + context: { taskType: "data_fetch", urgency }, + constraints: { budgetUsd: 20, maxLatencySeconds: 30 }, + environmentVersion: "provider-market-v1", + }); + const recalled = await runtime.recall({ + executionId: run.executionId, + query: "Atlas repeated data fetch SLA breaches experiential relationship", + }); + const context: ProviderContinuityContext = { ...baseContext, urgency }; + const control = decideProviderEngagement({ context, offers, memories: [] }); + const treatment = decideProviderEngagement({ + context, + offers, + memories: recalled.candidates.map((candidate) => ({ memory: candidate.memory, finalScore: candidate.score })), + }); + + if (treatment.memoryRefs.length) { + await runtime.recordDecision({ + executionId: run.executionId, + decisionType: "PROVIDER_ENGAGEMENT", + selectedAction: { providerId: treatment.providerId, terms: treatment.terms }, + alternatives: [{ providerId: control.providerId, terms: control.terms }], + reasoningSummary: treatment.reason, + influences: [{ + memoryId: treatment.memoryRefs[0]!, + retrievalId: recalled.recall.id, + influenceType: treatment.providerId !== control.providerId ? "CHANGED_ACTION" : "CONSTRAINED_ACTION", + summary: urgency === "URGENT" + ? "Accumulated provider experience changed urgent delegation from Atlas to Beacon." + : "Accumulated provider experience constrained Atlas prepayment and verification authority.", + counterfactual: treatment.providerId !== control.providerId ? { + action: { providerId: control.providerId, terms: control.terms }, + source: "APPLICATION_DECLARED", + evidenceState: "OBSERVED", + explanation: "Without relationship memory, the cheapest eligible provider is Atlas.", + } : undefined, + }], + }); + } + + const baseSettlement = await maybeWriteBaseIntent({ + executionId: run.executionId, + retrievalId: recalled.recall.id, + treatment, + }); + + emit({ + phase: urgency === "URGENT" ? "provider-fresh-urgent" : "provider-fresh-routine", + backend: "sibyl-memory-client", + sibyl: backend, + executionId: run.executionId, + retrievalId: recalled.recall.id, + recalledMemoryIds: recalled.candidates.map((candidate) => candidate.memory.id), + baseline: { + providerId: control.providerId, + terms: control.terms, + result: executeProviderEngagement(control, context), + }, + memoryConditioned: { + providerId: treatment.providerId, + terms: treatment.terms, + result: executeProviderEngagement(treatment, context), + }, + providerChanged: control.providerId !== treatment.providerId, + authorityChanged: JSON.stringify(control.terms) !== JSON.stringify(treatment.terms), + baseSettlement, + trace: await runtime.trace(run.executionId), + }); +} + +switch (command) { + case "seed": await seed(); break; + case "urgent": await decide("URGENT"); break; + case "routine": await decide("ROUTINE"); break; + default: throw new Error("Usage: tsx scripts/sibyl-provider-demo.ts "); +} diff --git a/scripts/virtuals-acp-ingest.ts b/scripts/virtuals-acp-ingest.ts new file mode 100644 index 0000000..e77228f --- /dev/null +++ b/scripts/virtuals-acp-ingest.ts @@ -0,0 +1,72 @@ +import { EngramRuntime } from "../packages/runtime/src/runtime.js"; +import { DEFAULT_RUNTIME_POLICIES } from "../packages/runtime/src/defaults.js"; +import { SibylRuntimeStore } from "../packages/sibyl/src/runtime-store.js"; +import { fetchAcpJobHistory } from "../packages/virtuals-acp/src/cli.js"; +import { + acpEvidenceToEngramObservation, + acpHistoryToExecutionEvidence, +} from "../packages/virtuals-acp/src/evidence.js"; + +function flag(name: string): string | undefined { + const index = process.argv.indexOf(`--${name}`); + return index >= 0 ? process.argv[index + 1] : undefined; +} + +function required(name: string): string { + const value = flag(name); + if (!value) throw new Error(`Missing --${name}`); + return value; +} + +function parseDate(name: string): Date | undefined { + const value = flag(name); + if (!value) return undefined; + const date = new Date(value); + if (Number.isNaN(date.getTime())) throw new Error(`Invalid --${name}: ${value}`); + return date; +} + +const executionId = required("execution-id"); +const jobId = required("job-id"); +const chainId = Number(required("chain-id")); +const providerId = required("provider-id"); +const taskType = required("task-type"); +const urgency = required("urgency").toUpperCase(); +const expectedLatency = flag("expected-latency-seconds"); +const testnet = process.argv.includes("--testnet"); + +if (!Number.isInteger(chainId) || chainId <= 0) throw new Error("--chain-id must be a positive integer"); +if (urgency !== "URGENT" && urgency !== "ROUTINE") throw new Error("--urgency must be URGENT or ROUTINE"); + +const history = await fetchAcpJobHistory({ jobId, chainId, testnet }); +const evidence = acpHistoryToExecutionEvidence(history, { + providerId, + taskType, + urgency, + expectedLatencySeconds: expectedLatency === undefined ? undefined : Number(expectedLatency), + startedAt: parseDate("started-at"), + completedAt: parseDate("completed-at"), +}); + +const runtime = new EngramRuntime(new SibylRuntimeStore(), DEFAULT_RUNTIME_POLICIES); +await runtime.observe({ + executionId, + ...acpEvidenceToEngramObservation(evidence), +}); + +process.stdout.write(`${JSON.stringify({ + phase: "virtuals-acp-ingest", + executionId, + jobId: evidence.jobId, + chainId: evidence.chainId, + protocol: evidence.protocol, + status: evidence.status, + providerId: evidence.providerId, + taskType: evidence.taskType, + urgency: evidence.urgency, + failureType: evidence.failureType ?? null, + observedLatencySeconds: evidence.observedLatencySeconds ?? null, + evidenceState: evidence.evidenceState, + memoryBackend: "sibyl-memory-client", + partnerClaim: "UNVERIFIED_UNTIL_REAL_ACP_JOB_EVIDENCE_IS_RETAINED", +}, null, 2)}\n`); diff --git a/tests/adapters/virtuals-acp-evidence.test.ts b/tests/adapters/virtuals-acp-evidence.test.ts new file mode 100644 index 0000000..d358b0b --- /dev/null +++ b/tests/adapters/virtuals-acp-evidence.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it } from "vitest"; +import { + acpHistoryToExecutionEvidence, + acpEvidenceToEngramObservation, + parseAcpJobHistory, +} from "../../packages/virtuals-acp/src/evidence.js"; + +describe("Virtuals ACP evidence adapter", () => { + it("parses the maintained ACP v2 job-history JSON shape", () => { + const parsed = parseAcpJobHistory({ + jobId: "42", + chainId: 84532, + protocol: "v2", + status: "completed", + entryCount: 3, + entries: [ + { kind: "system", event: { type: "job.created" } }, + { kind: "message", contentType: "requirement", content: "{}" }, + { kind: "system", event: { type: "job.completed" } }, + ], + }); + + expect(parsed).toMatchObject({ jobId: "42", chainId: 84532, protocol: "v2", status: "completed", entryCount: 3 }); + }); + + it("classifies an over-SLA completed job as observed SLA-breach evidence", () => { + const startedAt = new Date("2026-08-24T10:00:00Z"); + const completedAt = new Date("2026-08-24T10:00:55Z"); + const evidence = acpHistoryToExecutionEvidence({ + jobId: "42", + chainId: 84532, + protocol: "v2", + status: "completed", + entryCount: 4, + entries: [], + }, { + providerId: "0xAtlas", + taskType: "data_fetch", + urgency: "URGENT", + expectedLatencySeconds: 30, + startedAt, + completedAt, + }); + + expect(evidence).toMatchObject({ + sourceSystem: "VIRTUALS_ACP", + failureType: "SLA_BREACH", + observedLatencySeconds: 55, + evidenceState: "OBSERVED", + }); + + const observation = acpEvidenceToEngramObservation(evidence); + expect(observation.type).toBe("VIRTUALS_ACP_JOB_OBSERVED"); + expect(observation.payload).toMatchObject({ jobId: "42", chainId: 84532, failureType: "SLA_BREACH" }); + expect(observation.provenance[0]).toMatchObject({ source: "Virtuals ACP", jobId: "42", chainId: 84532 }); + }); + + it("preserves rejected and expired terminal states without pretending they are SLA failures", () => { + const rejected = acpHistoryToExecutionEvidence({ + jobId: 7, + chainId: 84532, + protocol: "v2", + status: "rejected", + entries: [], + }, { + providerId: "0xAtlas", + taskType: "data_fetch", + urgency: "URGENT", + }); + const expired = acpHistoryToExecutionEvidence({ + jobId: 8, + chainId: 84532, + protocol: "v2", + status: "expired", + entries: [], + }, { + providerId: "0xAtlas", + taskType: "data_fetch", + urgency: "URGENT", + }); + + expect(rejected.failureType).toBe("JOB_REJECTED"); + expect(expired.failureType).toBe("JOB_EXPIRED"); + }); + + it("fails closed on malformed ACP history rather than fabricating evidence", () => { + expect(() => parseAcpJobHistory({ status: "completed", entries: [] })).toThrow(); + }); +}); diff --git a/tests/fixtures/virtuals-acp/completed-job-history.json b/tests/fixtures/virtuals-acp/completed-job-history.json new file mode 100644 index 0000000..32fb189 --- /dev/null +++ b/tests/fixtures/virtuals-acp/completed-job-history.json @@ -0,0 +1,31 @@ +{ + "jobId": "engram-acp-fixture-001", + "chainId": 84532, + "protocol": "v2", + "status": "completed", + "entryCount": 5, + "entries": [ + { + "kind": "system", + "event": { "type": "job_opened" } + }, + { + "kind": "message", + "from": "0xRequesterFixture", + "content": "Fetch the requested dataset within the agreed SLA." + }, + { + "kind": "system", + "event": { "type": "funded" } + }, + { + "kind": "message", + "from": "0xProviderFixture", + "content": "Deliverable submitted." + }, + { + "kind": "system", + "event": { "type": "completed" } + } + ] +} diff --git a/tests/integration/base-settlement-authority.test.ts b/tests/integration/base-settlement-authority.test.ts new file mode 100644 index 0000000..68940eb --- /dev/null +++ b/tests/integration/base-settlement-authority.test.ts @@ -0,0 +1,136 @@ +import { describe, expect, it } from "vitest"; +import { + BASE_SEPOLIA_CHAIN_ID, + BASE_SEPOLIA_USDC, + deriveBaseSettlementIntent, +} from "../../packages/base-settlement/src/index.js"; +import { + decideProviderEngagement, + type ProviderContinuityContext, + type ProviderOffer, + type RecalledProviderMemory, +} from "../../packages/scenarios/provider-continuity/src/index.js"; +import type { OperationalMemory } from "../../packages/memory-core/src/domain.js"; + +const addresses = { + atlas: "0x1111111111111111111111111111111111111111", + beacon: "0x2222222222222222222222222222222222222222", +}; + +const offers: ProviderOffer[] = [ + { providerId: "atlas", priceUsd: 8, expectedLatencySeconds: 20 }, + { providerId: "beacon", priceUsd: 11, expectedLatencySeconds: 18 }, +]; + +function relationshipMemory(): RecalledProviderMemory { + return { + finalScore: 0.99, + memory: { + id: "memory-atlas-guarded", + agentId: "requester-agent", + workflowType: "agent_provider_selection", + memoryType: "REPEATED_PATTERN", + summary: "Repeated Atlas SLA breaches.", + structuredContext: { + workflowType: "agent_provider_selection", + memoryPrimitive: "EXPERIENTIAL_RELATIONSHIP", + taskType: "data_fetch", + providerId: "atlas", + relationshipPosture: "CONTEXT_GUARDED", + failureType: "SLA_BREACH", + breachCount: 2, + sourceExecutionIds: ["exec-1", "exec-2"], + }, + sourceExecutionIds: ["exec-1", "exec-2"], + confidence: 0.92, + evidenceState: "OBSERVED", + status: "ACTIVE", + createdAt: new Date("2026-08-29T00:00:00.000Z"), + updatedAt: new Date("2026-08-29T00:00:00.000Z"), + } as OperationalMemory, + }; +} + +function context(urgency: "URGENT" | "ROUTINE"): ProviderContinuityContext { + return { + workflowType: "agent_provider_selection", + taskType: "data_fetch", + urgency, + budgetUsd: 20, + maxLatencySeconds: 30, + environmentVersion: "provider-market-v1", + }; +} + +describe("Base settlement authority", () => { + it("turns remembered urgent provider experience into a different Base recipient", () => { + const baseline = decideProviderEngagement({ context: context("URGENT"), offers, memories: [] }); + const conditioned = decideProviderEngagement({ + context: context("URGENT"), + offers, + memories: [relationshipMemory()], + }); + + const baselineIntent = deriveBaseSettlementIntent({ + decision: baseline, + addresses, + provenance: { executionId: "exec-new" }, + }); + const conditionedIntent = deriveBaseSettlementIntent({ + decision: conditioned, + addresses, + provenance: { executionId: "exec-new", retrievalId: "retrieval-1" }, + }); + + expect(baselineIntent).toMatchObject({ + chainId: BASE_SEPOLIA_CHAIN_ID, + tokenAddress: BASE_SEPOLIA_USDC, + providerId: "atlas", + recipient: addresses.atlas, + }); + expect(conditionedIntent).toMatchObject({ + providerId: "beacon", + recipient: addresses.beacon, + memoryRefs: ["memory-atlas-guarded"], + }); + expect(conditionedIntent.recipient).not.toBe(baselineIntent.recipient); + expect(conditionedIntent.counterfactual?.recipient).toBe(addresses.atlas); + }); + + it("turns remembered routine experience into a smaller authorized Base prepayment", () => { + const baseline = decideProviderEngagement({ context: context("ROUTINE"), offers, memories: [] }); + const conditioned = decideProviderEngagement({ + context: context("ROUTINE"), + offers, + memories: [relationshipMemory()], + }); + + const baselineIntent = deriveBaseSettlementIntent({ + decision: baseline, + addresses, + provenance: { executionId: "exec-routine" }, + }); + const conditionedIntent = deriveBaseSettlementIntent({ + decision: conditioned, + addresses, + provenance: { executionId: "exec-routine", retrievalId: "retrieval-routine" }, + }); + + expect(baselineIntent.providerId).toBe("atlas"); + expect(conditionedIntent.providerId).toBe("atlas"); + expect(baselineIntent.terms.authorizedPrepayUsd).toBe("4.000000"); + expect(conditionedIntent.terms.authorizedPrepayUsd).toBe("0.800000"); + expect(conditionedIntent.terms.authorizedPrepayAtomic).toBe(800_000n); + expect(conditionedIntent.terms.requireMilestoneVerification).toBe(true); + expect(conditionedIntent.counterfactual?.terms.authorizedPrepayUsd).toBe("4.000000"); + }); + + it("fails closed on malformed provider addresses", () => { + const decision = decideProviderEngagement({ context: context("URGENT"), offers, memories: [] }); + expect(() => deriveBaseSettlementIntent({ + decision, + addresses: { ...addresses, atlas: "not-an-address" }, + provenance: { executionId: "exec-invalid" }, + })).toThrow("INVALID_PROVIDER_ADDRESS"); + }); +}); diff --git a/tests/integration/base-settlement-receipt.test.ts b/tests/integration/base-settlement-receipt.test.ts new file mode 100644 index 0000000..49bdabb --- /dev/null +++ b/tests/integration/base-settlement-receipt.test.ts @@ -0,0 +1,162 @@ +import { describe, expect, it } from "vitest"; +import { + deriveBaseSettlementIntent, + parseSerializedBaseSettlementIntent, + serializeBaseSettlementIntent, +} from "../../packages/base-settlement/src/index.js"; +import { + ERC20_TRANSFER_TOPIC, + verifyBaseSettlementReceipt, + type RpcReceipt, +} from "../../packages/base-settlement/src/evidence.js"; +import type { ProviderDecision } from "../../packages/scenarios/provider-continuity/src/index.js"; + +const atlas = "0x1111111111111111111111111111111111111111"; +const beacon = "0x2222222222222222222222222222222222222222"; +const sender = "0x3333333333333333333333333333333333333333"; +const wrongSender = "0x5555555555555555555555555555555555555555"; +const txHash = `0x${"ab".repeat(32)}`; + +function topic(address: string): string { + return `0x${address.slice(2).toLowerCase().padStart(64, "0")}`; +} + +function receipt(input: { + recipient: string; + amount: bigint; + status?: string; + token?: string; + payer?: string; +}): RpcReceipt { + return { + transactionHash: txHash, + status: input.status ?? "0x1", + blockNumber: "0x1234", + logs: [{ + address: input.token ?? "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + topics: [ERC20_TRANSFER_TOPIC, topic(input.payer ?? sender), topic(input.recipient)], + data: `0x${input.amount.toString(16).padStart(64, "0")}`, + transactionHash: txHash, + logIndex: "0x0", + }], + }; +} + +function routineDecision(): ProviderDecision { + return { + providerId: "atlas", + terms: { + prepayBps: 1000, + requireMilestoneVerification: true, + maxSpendUsd: 8, + }, + memoryRefs: ["memory-atlas-guarded"], + reason: "Prior experience constrains authority.", + counterfactual: { + providerId: "atlas", + terms: { + prepayBps: 5000, + requireMilestoneVerification: false, + maxSpendUsd: 8, + }, + }, + }; +} + +function intent() { + return deriveBaseSettlementIntent({ + decision: routineDecision(), + addresses: { atlas, beacon }, + provenance: { + executionId: "exec-routine", + retrievalId: "retrieval-routine", + decisionId: "decision-routine", + }, + }); +} + +describe("Base settlement receipt evidence", () => { + it("accepts only a successful USDC Transfer matching chain, payer, recipient and amount", () => { + const evidence = verifyBaseSettlementReceipt({ + intent: intent(), + receipt: receipt({ recipient: atlas, amount: 800_000n }), + observedChainId: 84532, + expectedPayer: sender, + }); + + expect(evidence).toMatchObject({ + sourceSystem: "BASE", + chainId: 84532, + transactionHash: txHash, + payer: sender, + recipient: atlas, + amountAtomic: "800000", + executionId: "exec-routine", + retrievalId: "retrieval-routine", + decisionId: "decision-routine", + memoryRefs: ["memory-atlas-guarded"], + evidenceState: "OBSERVED", + }); + }); + + it("round-trips a serialized intent and rejects internally inconsistent amounts", () => { + const serialized = serializeBaseSettlementIntent(intent()); + expect(parseSerializedBaseSettlementIntent(serialized).terms.authorizedPrepayAtomic).toBe(800_000n); + + expect(() => parseSerializedBaseSettlementIntent({ + ...serialized, + terms: { ...serialized.terms, authorizedPrepayAtomic: "4000000" }, + })).toThrow("BASE_SETTLEMENT_INTENT_AMOUNT_INCONSISTENT"); + expect(() => parseSerializedBaseSettlementIntent({ ...serialized, chainId: 1 })).toThrow(); + }); + + it("rejects a successful transfer to the wrong provider", () => { + expect(() => verifyBaseSettlementReceipt({ + intent: intent(), + receipt: receipt({ recipient: beacon, amount: 800_000n }), + observedChainId: 84532, + })).toThrow("BASE_USDC_TRANSFER_DOES_NOT_MATCH_INTENT"); + }); + + it("rejects the no-memory prepayment amount when memory authorized only the guarded amount", () => { + expect(() => verifyBaseSettlementReceipt({ + intent: intent(), + receipt: receipt({ recipient: atlas, amount: 4_000_000n }), + observedChainId: 84532, + })).toThrow("BASE_USDC_TRANSFER_DOES_NOT_MATCH_INTENT"); + }); + + it("rejects a receipt fetched from the wrong chain", () => { + expect(() => verifyBaseSettlementReceipt({ + intent: intent(), + receipt: receipt({ recipient: atlas, amount: 800_000n }), + observedChainId: 1, + })).toThrow("BASE_RPC_CHAIN_MISMATCH"); + }); + + it("rejects a transfer whose payer is not the expected requester wallet", () => { + expect(() => verifyBaseSettlementReceipt({ + intent: intent(), + receipt: receipt({ recipient: atlas, amount: 800_000n, payer: wrongSender }), + observedChainId: 84532, + expectedPayer: sender, + })).toThrow("BASE_USDC_TRANSFER_DOES_NOT_MATCH_INTENT"); + }); + + it("rejects reverted transactions and transfers from the wrong token contract", () => { + expect(() => verifyBaseSettlementReceipt({ + intent: intent(), + receipt: receipt({ recipient: atlas, amount: 800_000n, status: "0x0" }), + observedChainId: 84532, + })).toThrow("BASE_TRANSACTION_NOT_SUCCESSFUL"); + expect(() => verifyBaseSettlementReceipt({ + intent: intent(), + receipt: receipt({ + recipient: atlas, + amount: 800_000n, + token: "0x4444444444444444444444444444444444444444", + }), + observedChainId: 84532, + })).toThrow("BASE_USDC_TRANSFER_DOES_NOT_MATCH_INTENT"); + }); +}); diff --git a/tests/integration/sibyl-competing-memories.test.ts b/tests/integration/sibyl-competing-memories.test.ts new file mode 100644 index 0000000..6cb26a6 --- /dev/null +++ b/tests/integration/sibyl-competing-memories.test.ts @@ -0,0 +1,178 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { randomUUID } from "node:crypto"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { MemoryRelationship } from "../../packages/evaluation/src/domain.js"; +import { RelationshipMemoryEligibilityAdvisor } from "../../packages/evaluation/src/eligibility-advisor.js"; +import type { OperationalMemory } from "../../packages/memory-core/src/domain.js"; +import { DEFAULT_RUNTIME_POLICIES } from "../../packages/runtime/src/defaults.js"; +import { EngramRuntime } from "../../packages/runtime/src/runtime.js"; +import { SibylRuntimeStore } from "../../packages/sibyl/src/runtime-store.js"; + +const describeSibyl = process.env.ENGRAM_SIBYL_TEST_REQUIRED === "1" ? describe : describe.skip; + +let dir: string; +let previousDb: string | undefined; +let previousTenant: string | undefined; + +beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "engram-sibyl-conflict-")); + previousDb = process.env.ENGRAM_SIBYL_DB; + previousTenant = process.env.ENGRAM_SIBYL_TENANT; + process.env.ENGRAM_SIBYL_DB = join(dir, "memory.db"); + process.env.ENGRAM_SIBYL_TENANT = "engram-conflict-test"; +}); + +afterEach(async () => { + if (previousDb === undefined) delete process.env.ENGRAM_SIBYL_DB; + else process.env.ENGRAM_SIBYL_DB = previousDb; + if (previousTenant === undefined) delete process.env.ENGRAM_SIBYL_TENANT; + else process.env.ENGRAM_SIBYL_TENANT = previousTenant; + await rm(dir, { recursive: true, force: true }); +}); + +class RelationshipStore { + relationships: MemoryRelationship[] = []; + + async listRelationships(memoryId: string): Promise { + return this.relationships.filter((relationship) => + relationship.leftMemoryId === memoryId || relationship.rightMemoryId === memoryId, + ); + } +} + +function relationship( + leftMemoryId: string, + rightMemoryId: string, + relation: MemoryRelationship["relation"], +): MemoryRelationship { + return { + id: randomUUID(), + leftMemoryId, + rightMemoryId, + relation, + rationale: `${relation} is explicit evaluation evidence`, + evidenceState: "OBSERVED", + method: "HUMAN_ASSESSMENT", + assessedAt: new Date(), + }; +} + +function memory(id: string, sourceExecutionId: string, summary: string): OperationalMemory { + return { + id, + agentId: "agent-conflict", + memoryType: "OPERATIONAL_LESSON", + summary, + structuredContext: { + workflowType: "deployment", + sourceExecutionId, + sourceExecutionIds: [sourceExecutionId], + outcome: "COMPENSATED", + }, + confidence: 0.93, + evidenceState: "OBSERVED", + validFrom: new Date(Date.now() - 1_000), + environmentVersion: "prod-v1", + toolVersion: "1.4.0", + }; +} + +async function sourceExecution(store: SibylRuntimeStore) { + const runtime = new EngramRuntime(store, DEFAULT_RUNTIME_POLICIES); + const run = await runtime.startExecution({ + agentId: "agent-conflict", + workflowType: "deployment", + intent: "produce conflict evidence", + context: {}, + constraints: {}, + environmentVersion: "prod-v1", + toolVersion: "1.4.0", + }); + await runtime.complete({ + executionId: run.executionId, + status: "COMPENSATED", + summary: "Source execution retained for provenance.", + evidenceState: "OBSERVED", + }); + return run.executionId; +} + +async function start(runtime: EngramRuntime) { + return runtime.startExecution({ + agentId: "agent-conflict", + workflowType: "deployment", + intent: "Deploy service", + context: {}, + constraints: {}, + environmentVersion: "prod-v1", + toolVersion: "1.4.0", + }); +} + +describeSibyl("Sibyl competing memory semantics", () => { + it("keeps contradictory memories recall-visible but rejects influence until explicit supersession", async () => { + const seedStore = new SibylRuntimeStore(); + const sourceA = await sourceExecution(seedStore); + const sourceB = await sourceExecution(seedStore); + const memoryA = memory(randomUUID(), sourceA, "Use dependency alpha during production deployment."); + const memoryB = memory(randomUUID(), sourceB, "Avoid dependency alpha during production deployment."); + await seedStore.persistMemory(memoryA, [sourceA]); + await seedStore.persistMemory(memoryB, [sourceB]); + + const relationships = new RelationshipStore(); + relationships.relationships = [relationship(memoryA.id, memoryB.id, "CONTRADICTS")]; + const advisor = new RelationshipMemoryEligibilityAdvisor(relationships, { + unresolvedContradictionStages: ["INFLUENCE"], + }); + + // Fresh store/runtime proves the memories themselves are coming back through Sibyl. + const runtime = new EngramRuntime(new SibylRuntimeStore(), DEFAULT_RUNTIME_POLICIES, undefined, advisor); + const run = await start(runtime); + const recalled = await runtime.recall({ + executionId: run.executionId, + query: "dependency alpha production deployment", + }); + + expect(recalled.candidates.map((candidate) => candidate.memory.id).sort()).toEqual( + [memoryA.id, memoryB.id].sort(), + ); + + await expect(runtime.recordDecision({ + executionId: run.executionId, + decisionType: "DEPENDENCY_SELECTION", + selectedAction: { dependency: "alpha" }, + reasoningSummary: "Attempt to act on one side of unresolved contradictory Sibyl memory.", + influences: [{ + memoryId: memoryA.id, + retrievalId: recalled.recall.id, + influenceType: "SUPPORTED_ACTION", + summary: "Memory A supports dependency alpha.", + }], + })).rejects.toThrow("UNRESOLVED_MEMORY_CONTRADICTION"); + + const rejectedTrace = await runtime.trace(run.executionId) as Record; + expect((rejectedTrace.decisions as unknown[])).toHaveLength(0); + + relationships.relationships.push(relationship(memoryB.id, memoryA.id, "SUPERSEDES")); + const resolvedRun = await start(runtime); + const resolvedRecall = await runtime.recall({ + executionId: resolvedRun.executionId, + query: "dependency alpha production deployment", + }); + + await expect(runtime.recordDecision({ + executionId: resolvedRun.executionId, + decisionType: "DEPENDENCY_SELECTION", + selectedAction: { dependency: "beta" }, + reasoningSummary: "Explicit supersession resolves the prior contradiction for Memory B.", + influences: [{ + memoryId: memoryB.id, + retrievalId: resolvedRecall.recall.id, + influenceType: "SUPPORTED_ACTION", + summary: "Memory B is eligible after explicit supersession evidence.", + }], + })).resolves.toMatchObject({ executionId: resolvedRun.executionId }); + }, 20_000); +}); diff --git a/tests/integration/sibyl-memory-loop.test.ts b/tests/integration/sibyl-memory-loop.test.ts new file mode 100644 index 0000000..95b74d6 --- /dev/null +++ b/tests/integration/sibyl-memory-loop.test.ts @@ -0,0 +1,241 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { randomUUID } from "node:crypto"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { EngramRuntime } from "../../packages/runtime/src/runtime.js"; +import { DEFAULT_RUNTIME_POLICIES } from "../../packages/runtime/src/defaults.js"; +import { SibylRuntimeStore } from "../../packages/sibyl/src/runtime-store.js"; +import { decideRoute, ROUTE_C, ROUTE_D } from "../../packages/memory-core/src/policy.js"; +import type { OperationalMemory } from "../../packages/memory-core/src/domain.js"; + +const describeSibyl = process.env.ENGRAM_SIBYL_TEST_REQUIRED === "1" ? describe : describe.skip; + +let dir: string; +let previousDb: string | undefined; +let previousTenant: string | undefined; +let previousPython: string | undefined; + +beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "engram-sibyl-")); + previousDb = process.env.ENGRAM_SIBYL_DB; + previousTenant = process.env.ENGRAM_SIBYL_TENANT; + previousPython = process.env.ENGRAM_SIBYL_PYTHON; + process.env.ENGRAM_SIBYL_DB = join(dir, "memory.db"); + process.env.ENGRAM_SIBYL_TENANT = "engram-integration-test"; + delete process.env.ENGRAM_SIBYL_PYTHON; +}); + +afterEach(async () => { + if (previousDb === undefined) delete process.env.ENGRAM_SIBYL_DB; + else process.env.ENGRAM_SIBYL_DB = previousDb; + if (previousTenant === undefined) delete process.env.ENGRAM_SIBYL_TENANT; + else process.env.ENGRAM_SIBYL_TENANT = previousTenant; + if (previousPython === undefined) delete process.env.ENGRAM_SIBYL_PYTHON; + else process.env.ENGRAM_SIBYL_PYTHON = previousPython; + await rm(dir, { recursive: true, force: true }); +}); + +async function seedFailureMemory(runtime: EngramRuntime) { + const run = await runtime.startExecution({ + agentId: "agent-demo", + workflowType: "multi_venue_execution", + intent: "route value through available venues", + context: { liquidityClass: "thin" }, + constraints: {}, + }); + + const completed = await runtime.complete({ + executionId: run.executionId, + status: "COMPENSATED", + summary: "Venue C lacked required liquidity; recovery through D succeeded.", + result: { failedVenue: "C", recoveryVenue: "D" }, + evidenceState: "OBSERVED", + admissionSignals: [{ + kind: "UNEXPECTED_FAILURE", + summary: "Venue C failed with LIQUIDITY_UNAVAILABLE under thin liquidity; prefer D under comparable conditions.", + evidenceState: "OBSERVED", + confidence: 0.91, + details: { + failureType: "LIQUIDITY_UNAVAILABLE", + failedVenue: "C", + recoveryVenue: "D", + liquidityClass: "thin", + }, + }], + }); + return { run, completed }; +} + +async function startComparableRun(runtime: EngramRuntime, suffix = "treatment") { + return runtime.startExecution({ + agentId: "agent-demo", + workflowType: "multi_venue_execution", + intent: "route value through available venues", + context: { liquidityClass: "thin", suffix }, + constraints: {}, + }); +} + +describeSibyl("Engram × Sibyl evaluated profile", () => { + it("persists execution-derived memory and changes a later fresh-store decision", async () => { + const firstStore = new SibylRuntimeStore(); + const firstRuntime = new EngramRuntime(firstStore, DEFAULT_RUNTIME_POLICIES); + await expect(firstStore.ping()).resolves.toMatchObject({ tenant: "engram-integration-test" }); + + const { completed: completedA } = await seedFailureMemory(firstRuntime); + expect(completedA.admittedMemories).toHaveLength(1); + + // Fresh store + runtime instance: no JS object from Run A is reused. + const secondStore = new SibylRuntimeStore(); + const secondRuntime = new EngramRuntime(secondStore, DEFAULT_RUNTIME_POLICIES); + const runB = await startComparableRun(secondRuntime); + + const recalled = await secondRuntime.recall({ + executionId: runB.executionId, + query: "Venue C failed LIQUIDITY_UNAVAILABLE", + }); + expect(recalled.candidates).toHaveLength(1); + expect(recalled.candidates[0]?.memory.id).toBe(completedA.admittedMemories[0]?.id); + + const control = decideRoute({ memories: [], memoryAvailable: true }); + expect(control.route).toEqual(ROUTE_C); + + const treatment = decideRoute({ + memoryAvailable: true, + memories: recalled.candidates.map((candidate) => ({ + memory: candidate.memory, + semanticScore: candidate.semanticScore, + contextScore: candidate.contextScore, + outcomeScore: candidate.outcomeScore, + recencyScore: candidate.recencyScore, + })), + }); + expect(treatment.route).toEqual(ROUTE_D); + + await secondRuntime.recordDecision({ + executionId: runB.executionId, + decisionType: "ROUTE_SELECTION", + selectedAction: { route: treatment.route }, + alternatives: [{ route: control.route }], + reasoningSummary: treatment.reason, + influences: [{ + memoryId: recalled.candidates[0]!.memory.id, + retrievalId: recalled.recall.id, + influenceType: "CHANGED_ACTION", + summary: "Prior venue failure changed the selected route from C to D.", + counterfactual: { + action: { route: control.route }, + source: "APPLICATION_DECLARED", + evidenceState: "OBSERVED", + explanation: "The same route policy without recalled memory selects Route C.", + }, + }], + }); + + const trace = await secondRuntime.trace(runB.executionId) as Record; + expect(trace.memoryBackend).toBe("sibyl-memory-client"); + expect((trace.decisions as unknown[])).toHaveLength(1); + expect((trace.retrievals as unknown[])).toHaveLength(1); + }); + + it("retrieves but refuses to expose expired Sibyl memory", async () => { + const store = new SibylRuntimeStore(); + const sourceRuntime = new EngramRuntime(store, DEFAULT_RUNTIME_POLICIES); + const source = await sourceRuntime.startExecution({ + agentId: "agent-demo", + workflowType: "multi_venue_execution", + intent: "source execution", + context: {}, + constraints: {}, + }); + await sourceRuntime.complete({ + executionId: source.executionId, + status: "COMPENSATED", + summary: "Historical source completed.", + evidenceState: "OBSERVED", + }); + + const expired: OperationalMemory = { + id: randomUUID(), + agentId: "agent-demo", + memoryType: "OPERATIONAL_LESSON", + summary: "Venue C failed with LIQUIDITY_UNAVAILABLE and should be avoided.", + structuredContext: { + sourceExecutionId: source.executionId, + sourceExecutionIds: [source.executionId], + workflowType: "multi_venue_execution", + failureType: "LIQUIDITY_UNAVAILABLE", + failedVenue: "C", + outcome: "COMPENSATED", + }, + confidence: 0.95, + evidenceState: "OBSERVED", + validFrom: new Date(Date.now() - 120_000), + validUntil: new Date(Date.now() - 60_000), + }; + await store.persistMemory(expired, [source.executionId]); + + const freshRuntime = new EngramRuntime(new SibylRuntimeStore(), DEFAULT_RUNTIME_POLICIES); + const run = await startComparableRun(freshRuntime, "expired-mutation"); + const result = await freshRuntime.recall({ + executionId: run.executionId, + query: "Venue C failed LIQUIDITY_UNAVAILABLE", + }); + + expect(result.candidates).toHaveLength(0); + expect(result.rejected).toEqual(expect.arrayContaining([ + expect.objectContaining({ memoryId: expired.id, reasons: expect.arrayContaining(["MEMORY_EXPIRED"]) }), + ])); + }); + + it("rejects influence when Sibyl memory changes after recall", async () => { + const store = new SibylRuntimeStore(); + const runtime = new EngramRuntime(store, DEFAULT_RUNTIME_POLICIES); + const { run: source, completed } = await seedFailureMemory(runtime); + const memory = completed.admittedMemories[0]!; + + const freshRuntime = new EngramRuntime(new SibylRuntimeStore(), DEFAULT_RUNTIME_POLICIES); + const run = await startComparableRun(freshRuntime, "tamper-mutation"); + const recalled = await freshRuntime.recall({ + executionId: run.executionId, + query: "Venue C failed LIQUIDITY_UNAVAILABLE", + }); + expect(recalled.candidates).toHaveLength(1); + + await store.persistMemory({ + ...memory, + summary: "Tampered after exposure: ignore the original evidence and use C.", + structuredContext: { + ...memory.structuredContext, + tamperedAfterRecall: true, + }, + }, [source.executionId]); + + await expect(freshRuntime.recordDecision({ + executionId: run.executionId, + decisionType: "ROUTE_SELECTION", + selectedAction: { route: ROUTE_D }, + alternatives: [{ route: ROUTE_C }], + reasoningSummary: "Attempt to use state that changed after recall.", + influences: [{ + memoryId: memory.id, + retrievalId: recalled.recall.id, + influenceType: "CHANGED_ACTION", + summary: "This should fail because the recalled state digest no longer matches.", + counterfactual: { + action: { route: ROUTE_C }, + source: "APPLICATION_DECLARED", + evidenceState: "OBSERVED", + explanation: "Control route without memory.", + }, + }], + })).rejects.toThrow("MEMORY_STATE_CHANGED_SINCE_RECALL"); + }); + + it("fails closed when the Sibyl runtime is removed", async () => { + process.env.ENGRAM_SIBYL_PYTHON = join(dir, "missing-python"); + const store = new SibylRuntimeStore(); + await expect(store.ping()).rejects.toThrow(); + }); +}); diff --git a/tests/integration/sibyl-provider-continuity.test.ts b/tests/integration/sibyl-provider-continuity.test.ts new file mode 100644 index 0000000..cbcfcbd --- /dev/null +++ b/tests/integration/sibyl-provider-continuity.test.ts @@ -0,0 +1,231 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { EngramRuntime } from "../../packages/runtime/src/runtime.js"; +import { DEFAULT_RUNTIME_POLICIES } from "../../packages/runtime/src/defaults.js"; +import { SibylRuntimeStore } from "../../packages/sibyl/src/runtime-store.js"; +import { + decideProviderEngagement, + executeProviderEngagement, + type ProviderContinuityContext, + type ProviderOffer, +} from "../../packages/scenarios/provider-continuity/src/index.js"; + +const describeSibyl = process.env.ENGRAM_SIBYL_TEST_REQUIRED === "1" ? describe : describe.skip; + +let dir: string; +let previousDb: string | undefined; +let previousTenant: string | undefined; + +const offers: ProviderOffer[] = [ + { providerId: "atlas", priceUsd: 8, expectedLatencySeconds: 20 }, + { providerId: "beacon", priceUsd: 11, expectedLatencySeconds: 18 }, +]; + +const urgent: ProviderContinuityContext = { + workflowType: "agent_provider_selection", + taskType: "data_fetch", + urgency: "URGENT", + budgetUsd: 20, + maxLatencySeconds: 30, + environmentVersion: "provider-market-v1", +}; + +beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "engram-sibyl-provider-")); + previousDb = process.env.ENGRAM_SIBYL_DB; + previousTenant = process.env.ENGRAM_SIBYL_TENANT; + process.env.ENGRAM_SIBYL_DB = join(dir, "memory.db"); + process.env.ENGRAM_SIBYL_TENANT = "engram-provider-test"; +}); + +afterEach(async () => { + if (previousDb === undefined) delete process.env.ENGRAM_SIBYL_DB; + else process.env.ENGRAM_SIBYL_DB = previousDb; + if (previousTenant === undefined) delete process.env.ENGRAM_SIBYL_TENANT; + else process.env.ENGRAM_SIBYL_TENANT = previousTenant; + await rm(dir, { recursive: true, force: true }); +}); + +async function recordAtlasBreach(runtime: EngramRuntime, attempt: number) { + const run = await runtime.startExecution({ + agentId: "requester-agent", + workflowType: "agent_provider_selection", + intent: "obtain urgent data from an eligible provider", + context: { taskType: "data_fetch", urgency: "URGENT", providerId: "atlas", attempt }, + constraints: { maxLatencySeconds: 30 }, + environmentVersion: "provider-market-v1", + }); + await runtime.observe({ + executionId: run.executionId, + type: "PROVIDER_SLA_BREACH", + payload: { providerId: "atlas", taskType: "data_fetch", latencySeconds: 55 + attempt }, + evidenceState: "OBSERVED", + provenance: [{ source: "provider-execution", attempt }], + }); + await runtime.complete({ + executionId: run.executionId, + status: "PARTIAL", + summary: `Atlas breached the urgent data-fetch SLA on attempt ${attempt}.`, + result: { providerId: "atlas", failureType: "SLA_BREACH", urgency: "URGENT" }, + evidenceState: "OBSERVED", + }); + return run.executionId; +} + +describeSibyl("Sibyl-backed experiential provider continuity", () => { + it("accumulates two prior executions into relationship memory that changes a fresh-session provider decision", async () => { + const sourceRuntime = new EngramRuntime(new SibylRuntimeStore(), DEFAULT_RUNTIME_POLICIES); + const first = await recordAtlasBreach(sourceRuntime, 1); + const second = await recordAtlasBreach(sourceRuntime, 2); + + const admitting = await sourceRuntime.startExecution({ + agentId: "requester-agent", + workflowType: "agent_provider_selection", + intent: "consolidate repeated provider execution experience", + context: { taskType: "data_fetch", providerId: "atlas", purpose: "relationship-state-update" }, + constraints: {}, + environmentVersion: "provider-market-v1", + }); + + const admitted = await sourceRuntime.complete({ + executionId: admitting.executionId, + status: "PARTIAL", + summary: "Two independent Atlas executions breached the urgent data-fetch SLA.", + result: { providerId: "atlas", repeatedPattern: true }, + evidenceState: "OBSERVED", + admissionSignals: [{ + kind: "REPEATED_PATTERN", + summary: "Across two requester-owned executions, Atlas repeatedly breached urgent data-fetch SLAs. Guard urgent delegation and reduce prepayment authority on routine work.", + evidenceState: "OBSERVED", + confidence: 0.92, + sourceExecutionIds: [first, second, admitting.executionId], + details: { + memoryPrimitive: "EXPERIENTIAL_RELATIONSHIP", + taskType: "data_fetch", + providerId: "atlas", + relationshipPosture: "CONTEXT_GUARDED", + failureType: "SLA_BREACH", + breachCount: 2, + }, + }], + }); + expect(admitted.admittedMemories).toHaveLength(1); + const memory = admitted.admittedMemories[0]!; + + const freshRuntime = new EngramRuntime(new SibylRuntimeStore(), DEFAULT_RUNTIME_POLICIES); + const next = await freshRuntime.startExecution({ + agentId: "requester-agent", + workflowType: "agent_provider_selection", + intent: "obtain urgent data from an eligible provider", + context: { taskType: "data_fetch", urgency: "URGENT" }, + constraints: { budgetUsd: 20, maxLatencySeconds: 30 }, + environmentVersion: "provider-market-v1", + }); + + const recalled = await freshRuntime.recall({ + executionId: next.executionId, + query: "Atlas repeated urgent data fetch SLA breaches relationship", + }); + expect(recalled.candidates.map((candidate) => candidate.memory.id)).toContain(memory.id); + + const baseline = decideProviderEngagement({ context: urgent, offers, memories: [] }); + expect(baseline.providerId).toBe("atlas"); + expect(executeProviderEngagement(baseline, urgent).status).toBe("FAILURE"); + + const treatment = decideProviderEngagement({ + context: urgent, + offers, + memories: recalled.candidates.map((candidate) => ({ memory: candidate.memory, finalScore: candidate.score })), + }); + expect(treatment.providerId).toBe("beacon"); + expect(treatment.memoryRefs).toContain(memory.id); + expect(executeProviderEngagement(treatment, urgent).status).toBe("SUCCESS"); + + await freshRuntime.recordDecision({ + executionId: next.executionId, + decisionType: "PROVIDER_ENGAGEMENT", + selectedAction: { providerId: treatment.providerId, terms: treatment.terms }, + alternatives: [{ providerId: baseline.providerId, terms: baseline.terms }], + reasoningSummary: treatment.reason, + influences: [{ + memoryId: memory.id, + retrievalId: recalled.recall.id, + influenceType: "CHANGED_ACTION", + summary: "Accumulated provider experience changed urgent delegation from Atlas to Beacon.", + counterfactual: { + action: { providerId: baseline.providerId, terms: baseline.terms }, + source: "APPLICATION_DECLARED", + evidenceState: "OBSERVED", + explanation: "Without experiential relationship memory, the cheapest eligible provider is Atlas.", + }, + }], + }); + + const trace = await freshRuntime.trace(next.executionId) as Record; + expect(trace.memoryBackend).toBe("sibyl-memory-client"); + expect(JSON.stringify(trace)).toContain("PROVIDER_ENGAGEMENT"); + expect(JSON.stringify(trace)).toContain("CHANGED_ACTION"); + }, 15_000); + + it("uses the same relationship memory to constrain terms rather than blacklist Atlas for routine work", async () => { + const sourceRuntime = new EngramRuntime(new SibylRuntimeStore(), DEFAULT_RUNTIME_POLICIES); + const first = await recordAtlasBreach(sourceRuntime, 1); + const second = await recordAtlasBreach(sourceRuntime, 2); + const admitting = await sourceRuntime.startExecution({ + agentId: "requester-agent", + workflowType: "agent_provider_selection", + intent: "consolidate repeated provider execution experience", + context: { taskType: "data_fetch", providerId: "atlas" }, + constraints: {}, + environmentVersion: "provider-market-v1", + }); + const admitted = await sourceRuntime.complete({ + executionId: admitting.executionId, + status: "PARTIAL", + summary: "Repeated Atlas SLA pattern established.", + evidenceState: "OBSERVED", + admissionSignals: [{ + kind: "REPEATED_PATTERN", + summary: "Atlas repeatedly breached urgent data-fetch SLAs.", + evidenceState: "OBSERVED", + confidence: 0.92, + sourceExecutionIds: [first, second, admitting.executionId], + details: { + memoryPrimitive: "EXPERIENTIAL_RELATIONSHIP", + taskType: "data_fetch", + providerId: "atlas", + relationshipPosture: "CONTEXT_GUARDED", + failureType: "SLA_BREACH", + breachCount: 2, + }, + }], + }); + const memory = admitted.admittedMemories[0]!; + + const freshRuntime = new EngramRuntime(new SibylRuntimeStore(), DEFAULT_RUNTIME_POLICIES); + const next = await freshRuntime.startExecution({ + agentId: "requester-agent", + workflowType: "agent_provider_selection", + intent: "obtain routine data", + context: { taskType: "data_fetch", urgency: "ROUTINE" }, + constraints: { budgetUsd: 20, maxLatencySeconds: 30 }, + environmentVersion: "provider-market-v1", + }); + const recalled = await freshRuntime.recall({ + executionId: next.executionId, + query: "Atlas repeated data fetch SLA breaches relationship", + }); + const routine: ProviderContinuityContext = { ...urgent, urgency: "ROUTINE" }; + const decision = decideProviderEngagement({ + context: routine, + offers, + memories: recalled.candidates.map((candidate) => ({ memory: candidate.memory, finalScore: candidate.score })), + }); + + expect(decision.providerId).toBe("atlas"); + expect(decision.memoryRefs).toContain(memory.id); + expect(decision.terms).toMatchObject({ prepayBps: 1_000, requireMilestoneVerification: true }); + }, 15_000); +}); diff --git a/tests/integration/virtuals-acp-evidence.test.ts b/tests/integration/virtuals-acp-evidence.test.ts new file mode 100644 index 0000000..7fa6ad0 --- /dev/null +++ b/tests/integration/virtuals-acp-evidence.test.ts @@ -0,0 +1,116 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; +import { + acpEvidenceToEngramObservation, + acpHistoryToExecutionEvidence, + parseAcpJobHistory, +} from "../../packages/virtuals-acp/src/evidence.js"; + +async function fixture() { + const raw = await readFile(resolve(process.cwd(), "tests/fixtures/virtuals-acp/completed-job-history.json"), "utf8"); + return JSON.parse(raw) as unknown; +} + +describe("Virtuals ACP evidence adapter", () => { + it("normalizes a current v2 history shape without inventing failure", async () => { + const history = await fixture(); + const parsed = parseAcpJobHistory(history); + expect(parsed).toMatchObject({ + jobId: "engram-acp-fixture-001", + chainId: 84532, + protocol: "v2", + status: "completed", + entryCount: 5, + }); + + const evidence = acpHistoryToExecutionEvidence(history, { + providerId: "0xProviderFixture", + taskType: "data_fetch", + urgency: "ROUTINE", + expectedLatencySeconds: 60, + startedAt: new Date("2026-08-28T12:00:00.000Z"), + completedAt: new Date("2026-08-28T12:00:40.000Z"), + }); + + expect(evidence).toMatchObject({ + sourceSystem: "VIRTUALS_ACP", + chainId: 84532, + protocol: "v2", + status: "completed", + providerId: "0xProviderFixture", + observedLatencySeconds: 40, + evidenceState: "OBSERVED", + historyEntryCount: 5, + }); + expect(evidence.failureType).toBeUndefined(); + + const observation = acpEvidenceToEngramObservation(evidence); + expect(observation).toMatchObject({ + type: "VIRTUALS_ACP_JOB_OBSERVED", + evidenceState: "OBSERVED", + payload: { + jobId: "engram-acp-fixture-001", + chainId: 84532, + providerId: "0xProviderFixture", + }, + }); + expect(observation.provenance[0]).toMatchObject({ + source: "Virtuals ACP", + jobId: "engram-acp-fixture-001", + chainId: 84532, + }); + }); + + it("classifies an SLA breach only from explicit expected and observed timing", async () => { + const evidence = acpHistoryToExecutionEvidence(await fixture(), { + providerId: "0xProviderFixture", + taskType: "data_fetch", + urgency: "URGENT", + expectedLatencySeconds: 30, + startedAt: new Date("2026-08-28T12:00:00.000Z"), + completedAt: new Date("2026-08-28T12:00:55.000Z"), + }); + expect(evidence.failureType).toBe("SLA_BREACH"); + expect(evidence.observedLatencySeconds).toBe(55); + }); + + it("preserves rejected and expired jobs as distinct failure classes", () => { + const rejected = acpHistoryToExecutionEvidence({ + jobId: "job-rejected", + chainId: 84532, + protocol: "v2", + status: "rejected", + entries: [], + }, { + providerId: "0xProviderFixture", + taskType: "data_fetch", + urgency: "ROUTINE", + expectedLatencySeconds: 30, + startedAt: new Date("2026-08-28T12:00:00.000Z"), + completedAt: new Date("2026-08-28T12:02:00.000Z"), + }); + const expired = acpHistoryToExecutionEvidence({ + jobId: "job-expired", + chainId: 84532, + protocol: "v2", + status: "expired", + entries: [], + }, { + providerId: "0xProviderFixture", + taskType: "data_fetch", + urgency: "ROUTINE", + expectedLatencySeconds: 30, + startedAt: new Date("2026-08-28T12:00:00.000Z"), + completedAt: new Date("2026-08-28T12:02:00.000Z"), + }); + + expect(rejected.failureType).toBe("JOB_REJECTED"); + expect(expired.failureType).toBe("JOB_EXPIRED"); + }); + + it("fails closed on malformed ACP history", () => { + expect(() => parseAcpJobHistory({ status: "completed", entries: [] })).toThrow(); + expect(() => parseAcpJobHistory({ jobId: "x", chainId: -1, status: "completed", entries: [] })).toThrow(); + }); +}); diff --git a/tests/scenarios/provider-continuity-memory.test.ts b/tests/scenarios/provider-continuity-memory.test.ts new file mode 100644 index 0000000..8b85832 --- /dev/null +++ b/tests/scenarios/provider-continuity-memory.test.ts @@ -0,0 +1,123 @@ +import { randomUUID } from "node:crypto"; +import { describe, expect, it } from "vitest"; +import type { OperationalMemory } from "../../packages/memory-core/src/domain.js"; +import { + decideProviderEngagement, + executeProviderEngagement, + type ProviderContinuityContext, + type ProviderOffer, +} from "../../packages/scenarios/provider-continuity/src/index.js"; + +const offers: ProviderOffer[] = [ + { providerId: "atlas", priceUsd: 8, expectedLatencySeconds: 20 }, + { providerId: "beacon", priceUsd: 11, expectedLatencySeconds: 18 }, +]; + +const urgent: ProviderContinuityContext = { + workflowType: "agent_provider_selection", + taskType: "data_fetch", + urgency: "URGENT", + budgetUsd: 20, + maxLatencySeconds: 30, + environmentVersion: "provider-market-v1", +}; + +function relationshipMemory(overrides: Partial = {}): OperationalMemory { + const sources = [randomUUID(), randomUUID()]; + return { + id: randomUUID(), + agentId: "requester-agent", + memoryType: "REPEATED_PATTERN", + summary: "Across two requester-owned executions, Atlas repeatedly breached urgent data-fetch SLAs. Guard urgent delegation and reduce prepayment authority on routine work.", + structuredContext: { + workflowType: "agent_provider_selection", + memoryPrimitive: "EXPERIENTIAL_RELATIONSHIP", + taskType: "data_fetch", + providerId: "atlas", + relationshipPosture: "CONTEXT_GUARDED", + failureType: "SLA_BREACH", + breachCount: 2, + sourceExecutionId: sources[1], + sourceExecutionIds: sources, + outcome: "PARTIAL", + }, + confidence: 0.92, + evidenceState: "OBSERVED", + validFrom: new Date(), + ...overrides, + }; +} + +describe("experiential provider continuity", () => { + it("changes provider for an urgent task after repeated relationship-specific SLA breaches", () => { + const baseline = decideProviderEngagement({ context: urgent, offers, memories: [] }); + expect(baseline.providerId).toBe("atlas"); + expect(executeProviderEngagement(baseline, urgent)).toMatchObject({ status: "FAILURE", failureType: "SLA_BREACH" }); + + const memory = relationshipMemory(); + const treatment = decideProviderEngagement({ + context: urgent, + offers, + memories: [{ memory, finalScore: 0.98 }], + }); + expect(treatment.providerId).toBe("beacon"); + expect(treatment.memoryRefs).toEqual([memory.id]); + expect(treatment.counterfactual?.providerId).toBe("atlas"); + expect(executeProviderEngagement(treatment, urgent).status).toBe("SUCCESS"); + }); + + it("does not globally blacklist the provider; routine work changes terms instead", () => { + const memory = relationshipMemory(); + const routine: ProviderContinuityContext = { ...urgent, urgency: "ROUTINE" }; + const decision = decideProviderEngagement({ + context: routine, + offers, + memories: [{ memory, finalScore: 0.98 }], + }); + + expect(decision.providerId).toBe("atlas"); + expect(decision.terms.prepayBps).toBe(1_000); + expect(decision.terms.requireMilestoneVerification).toBe(true); + expect(decision.counterfactual?.terms.prepayBps).toBe(5_000); + }); + + it("requires multi-execution provenance rather than treating one bad interaction as relationship state", () => { + const memory = relationshipMemory({ + structuredContext: { + workflowType: "agent_provider_selection", + memoryPrimitive: "EXPERIENTIAL_RELATIONSHIP", + taskType: "data_fetch", + providerId: "atlas", + relationshipPosture: "CONTEXT_GUARDED", + failureType: "SLA_BREACH", + breachCount: 1, + sourceExecutionId: randomUUID(), + sourceExecutionIds: [randomUUID()], + outcome: "PARTIAL", + }, + }); + const decision = decideProviderEngagement({ + context: urgent, + offers, + memories: [{ memory, finalScore: 0.99 }], + }); + expect(decision.providerId).toBe("atlas"); + expect(decision.memoryRefs).toEqual([]); + }); + + it("does not transfer experience from a different task type solely because retrieval score is high", () => { + const memory = relationshipMemory({ + structuredContext: { + ...relationshipMemory().structuredContext, + taskType: "code_review", + }, + }); + const decision = decideProviderEngagement({ + context: urgent, + offers, + memories: [{ memory, finalScore: 0.99 }], + }); + expect(decision.providerId).toBe("atlas"); + expect(decision.memoryRefs).toEqual([]); + }); +});