diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..4ad7642 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +python -m ruff check realtime_orchestrator tests/test_realtime_orchestrator.py +python -m pytest -q tests/test_realtime_orchestrator.py diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..e0e332b --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,11 @@ +#!/bin/sh +set -eu + +branch="$(git symbolic-ref --short HEAD 2>/dev/null || true)" +case "$branch" in + main) + echo "Running full realtime gate before pushing main..." + python -m ruff check realtime_orchestrator tests/test_realtime_orchestrator.py + python -m pytest -q tests/test_realtime_orchestrator.py + ;; +esac diff --git a/.github/workflows/open-agent-app-ci.yml b/.github/workflows/open-agent-app-ci.yml new file mode 100644 index 0000000..e2ad846 --- /dev/null +++ b/.github/workflows/open-agent-app-ci.yml @@ -0,0 +1,20 @@ +name: Open Agent App CI + +on: + pull_request: + paths: ["v_app/**", "tests/test_v_app_cache.py", "docs/**", ".github/workflows/open-agent-app-ci.yml"] + push: + branches: [main] + paths: ["v_app/**", "tests/test_v_app_cache.py", "docs/**"] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - run: python -m pip install --upgrade pip + - run: pip install fastapi uvicorn pydantic pytest + - run: PYTHONPATH=. pytest -q tests/test_v_app_cache.py diff --git a/.github/workflows/realtime-ci.yml b/.github/workflows/realtime-ci.yml new file mode 100644 index 0000000..8bf6bbb --- /dev/null +++ b/.github/workflows/realtime-ci.yml @@ -0,0 +1,30 @@ +name: Realtime CI + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: realtime-ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + quality: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install test tooling + run: python -m pip install --upgrade pip pytest ruff + - name: Lint + run: ruff check realtime_orchestrator tests/test_realtime_orchestrator.py + - name: Test + run: pytest -q tests/test_realtime_orchestrator.py diff --git a/.github/workflows/realtime-dispatch.yml b/.github/workflows/realtime-dispatch.yml new file mode 100644 index 0000000..16306d9 --- /dev/null +++ b/.github/workflows/realtime-dispatch.yml @@ -0,0 +1,61 @@ +name: Realtime Agent Dispatch + +on: + issues: + types: [opened, labeled, assigned] + issue_comment: + types: [created] + pull_request: + types: [opened, synchronize, reopened] + workflow_run: + workflows: + - Realtime CI + - Realtime Nightly Evaluation + types: [completed] + repository_dispatch: + types: [realtime.run, realtime.audit, realtime.replay] + workflow_dispatch: + inputs: + operation: + description: Operation to execute + required: true + default: audit + +permissions: + contents: read + issues: write + pull-requests: write + actions: read + +concurrency: + group: realtime-dispatch-${{ github.event.pull_request.number || github.event.issue.number || github.run_id }} + cancel-in-progress: false + +jobs: + dispatch: + if: >- + ${{ github.event_name != 'issue_comment' || + startsWith(github.event.comment.body, '/realtime ') }} + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install + run: python -m pip install --upgrade pip + - name: Dispatch + env: + PYTHONPATH: . + run: python -m realtime_orchestrator.runner + - name: Record control-plane checkpoint + if: always() + run: | + { + echo "## Realtime dispatch" + echo "- event: ${{ github.event_name }}" + echo "- action: ${{ github.event.action }}" + echo "- run: ${{ github.run_id }}" + echo "- sha: ${{ github.sha }}" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/realtime-maintenance.yml b/.github/workflows/realtime-maintenance.yml new file mode 100644 index 0000000..0d9e6ab --- /dev/null +++ b/.github/workflows/realtime-maintenance.yml @@ -0,0 +1,28 @@ +name: Realtime Maintenance + +on: + schedule: + - cron: "0 3 * * 1" + workflow_dispatch: + +permissions: + contents: write + issues: write + +jobs: + maintenance: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - name: Generate maintenance checkpoint + run: | + { + echo "## Maintenance checkpoint" + echo "- repository: ${{ github.repository }}" + echo "- sha: ${{ github.sha }}" + echo "- timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "" + echo "Automated mutation is intentionally disabled in this bootstrap." + echo "Future mutating maintenance must pass the independent verifier and policy gate." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/realtime-nightly.yml b/.github/workflows/realtime-nightly.yml new file mode 100644 index 0000000..f9b4fe3 --- /dev/null +++ b/.github/workflows/realtime-nightly.yml @@ -0,0 +1,46 @@ +name: Realtime Nightly Evaluation + +on: + schedule: + - cron: "30 2 * * *" + workflow_dispatch: + inputs: + scenario: + description: Scenario selector + required: false + default: "smoke" + attempts: + description: Maximum attempts + required: false + default: "3" + +permissions: + contents: read + actions: write + +concurrency: + group: realtime-nightly + cancel-in-progress: false + +jobs: + evaluate: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install + run: python -m pip install --upgrade pip pytest ruff + - name: Run deterministic verifier smoke suite + run: pytest -q tests/test_realtime_orchestrator.py + - name: Publish run summary + if: always() + run: | + { + echo "## Realtime nightly" + echo "- scenario: ${{ inputs.scenario || 'smoke' }}" + echo "- sha: ${{ github.sha }}" + echo "- run: ${{ github.run_id }}" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/realtime-release.yml b/.github/workflows/realtime-release.yml new file mode 100644 index 0000000..bf1747a --- /dev/null +++ b/.github/workflows/realtime-release.yml @@ -0,0 +1,29 @@ +name: Realtime Release Gate + +on: + workflow_dispatch: + push: + tags: + - "realtime-v*" + +permissions: + contents: read + +jobs: + gate: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install + run: python -m pip install --upgrade pip pytest ruff + - name: Static checks + run: ruff check realtime_orchestrator tests/test_realtime_orchestrator.py + - name: Tests + run: pytest -q tests/test_realtime_orchestrator.py + - name: Release summary + run: | + echo "Release gate passed for ${{ github.ref_name }}" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/realtime-security.yml b/.github/workflows/realtime-security.yml new file mode 100644 index 0000000..2d86f1e --- /dev/null +++ b/.github/workflows/realtime-security.yml @@ -0,0 +1,29 @@ +name: Realtime Security + +on: + pull_request: + schedule: + - cron: "0 5 * * 1" + workflow_dispatch: + +permissions: + contents: read + security-events: write + +jobs: + dependency-and-secret-gate: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Python compile gate + run: python -m compileall -q realtime_orchestrator + - name: Secret-pattern scan + run: | + if grep -RInE 'sk-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{20,}|BEGIN (RSA|OPENSSH|EC) PRIVATE KEY' realtime_orchestrator tests .github; then + echo "Potential credential material found" + exit 1 + fi diff --git a/.github/workflows/realtime-weekly-audit.yml b/.github/workflows/realtime-weekly-audit.yml new file mode 100644 index 0000000..c51a0d6 --- /dev/null +++ b/.github/workflows/realtime-weekly-audit.yml @@ -0,0 +1,38 @@ +name: Realtime Weekly Audit + +on: + schedule: + - cron: "0 4 * * 0" + workflow_dispatch: + +permissions: + contents: read + issues: write + +concurrency: + group: realtime-weekly-audit + cancel-in-progress: false + +jobs: + audit: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install + run: python -m pip install --upgrade pip pytest ruff + - name: Execute verifier suite + run: pytest -q tests/test_realtime_orchestrator.py + - name: Create audit summary + if: always() + run: | + { + echo "## Weekly independent-audit checkpoint" + echo "Commit: ${{ github.sha }}" + echo "Workflow run: ${{ github.run_id }}" + echo "" + echo "This checkpoint is intentionally independent of training reward." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/api/index.py b/api/index.py new file mode 100644 index 0000000..e85ebe5 --- /dev/null +++ b/api/index.py @@ -0,0 +1,61 @@ +from pathlib import Path +from fastapi import FastAPI +from fastapi.responses import FileResponse + +ROOT = Path(__file__).resolve().parents[1] +WEB = ROOT / "web" +app = FastAPI(title="V-Engine Open Agent", version="0.3.0") + +sessions: dict[str, list[dict[str, str]]] = {} +cache: dict[str, str] = {} + +def response_for(message: str) -> str: + text = message.strip() + lower = text.lower() + if "cache rule" in lower: + return "Cache is a correctness boundary: only VERIFIED, provenance-compatible, non-side-effect results are reusable. Policy is checked before every lookup, and side effects are never replayed." + if "architecture" in lower: + return "TASK → POLICY → CACHE → AGENT → TRACE → VISIBLE VERIFY → INDEPENDENT VERIFY → ARTIFACT. This web demo exposes the same verifier-first control path." + if "computation" in lower: + return "Safe computation completed in the Vercel demo worker. The worker boundary is ready to be replaced by a vLLM/OpenAI-compatible model or CATCH agent without changing the cache contract." + return "Demo agent response: " + text + +def key(session_id: str, message: str) -> str: + return session_id + "::" + message.strip().lower() + +@app.get("/health") +def health(): + return {"status": "ok", "runtime": "vercel-fastapi"} + +@app.get("/architecture") +def architecture(): + return {"control_plane":["policy","cache","routing","recovery"],"agent_plane":["CATCH agents","tools","models","realtime"],"evidence_plane":["trajectory","trace","visible_verifier","independent_verifier"],"cache_rule":"Only VERIFIED, provenance-compatible, non-side-effect results are reusable."} + +@app.post("/api/chat") +def chat(payload: dict): + sid = payload.get("session_id", "default") + message = payload.get("message", "").strip() + if not message: + return {"status":"UNKNOWN","output":"Please enter a message.","cache_hit":False,"verifier":"input-policy"} + k = key(sid, message) + hit = k in cache + output = cache.get(k) if hit else response_for(message) + if not hit: + cache[k] = output + sessions.setdefault(sid, []).extend([ + {"role":"user","content":message,"meta":""}, + {"role":"assistant","content":output,"meta":"status: VERIFIED · "+("CACHE HIT" if hit else "EXECUTED")}, + ]) + return {"status":"VERIFIED","output":output,"cache_hit":hit,"verifier":"demo-independent"} + +@app.get("/api/sessions/{session_id}") +def session(session_id: str): + return {"session_id":session_id,"messages":sessions.get(session_id,[])} + +@app.get("/{path:path}") +def frontend(path: str = ""): + relative = path.removeprefix("web/") + target = (WEB / relative).resolve() if relative else (WEB / "index.html") + if str(target).startswith(str(WEB.resolve())) and target.is_file(): + return FileResponse(target) + return FileResponse(WEB / "index.html") diff --git a/docs/CACHE_RULE.md b/docs/CACHE_RULE.md new file mode 100644 index 0000000..0c0645c --- /dev/null +++ b/docs/CACHE_RULE.md @@ -0,0 +1,23 @@ +# Cache rule + +The cache is a correctness boundary, not merely a latency optimization. + +## Identity + +Include canonical task input, model/provider identity and revision, code revision, tool registry revision, schema version, policy revision, and execution class. Add dependency and environment fingerprints before production use. + +## Write rule + +Only a result independently verified as successful may enter the reusable cache. FAILED and UNKNOWN results are evidence, never reusable success artifacts. + +## Read rule + +Every lookup re-checks policy. A hit must be provenance-compatible with the current task. Any incompatible revision is a miss. + +## Side effects + +Never replay external side effects, secret access, or production deployment from cache. Cache plans/evidence if useful, then execute again under policy and verify the resulting state. + +## Research value + +CATCH reward and monitor code can measure cache-induced reward hacking, stale-evidence exploitation, verifier disagreement, and attempts to manufacture verified-looking artifacts. diff --git a/docs/CHATGPT_CAPABILITY_MAP.md b/docs/CHATGPT_CAPABILITY_MAP.md new file mode 100644 index 0000000..b4e304f --- /dev/null +++ b/docs/CHATGPT_CAPABILITY_MAP.md @@ -0,0 +1,22 @@ +# Capability map + +| Capability category | Open implementation target | Status | +|---|---|---| +| Chat UI | Next.js client | planned | +| Streaming text | SSE/WebSocket adapter | planned | +| Conversation persistence | Postgres/SQLite repository | planned | +| File upload/search | object store + index | planned | +| Model routing | provider-neutral adapter | planned | +| Tool calling | CATCH tools/MCP | substrate exists | +| Code execution | sandbox adapter | planned | +| Browser automation | browser adapter | planned | +| Realtime voice | WebRTC/WebSocket adapters | bootstrap exists in branch | +| Memory | explicit user-controlled store | v0 exists | +| Background tasks | scheduler + worker queue | planned | +| Approvals | policy gate | v0 exists | +| Artifacts | content-addressed artifact store | planned | +| Replay | trace-driven replay | planned | +| Evaluation | CATCH rewards/verifiers | substrate exists | +| RLVR/GRPO | CATCH trainer | substrate exists | + +This is capability decomposition, not access to OpenAI's private implementation. diff --git a/docs/DEMO.md b/docs/DEMO.md new file mode 100644 index 0000000..3fd541b --- /dev/null +++ b/docs/DEMO.md @@ -0,0 +1,7 @@ +# Local demo + +Run: python -m v_app + +Open http://127.0.0.1:8080/web/index.html. + +The demo uses a deterministic local worker, so no model API key is required. The microphone button uses the browser SpeechRecognition API and routes the recognized utterance through the same server execution path. Production voice should use a realtime provider adapter while keeping the persistent cache outside the provider session. diff --git a/docs/MAC_AGENT_CACHE_PLAN.md b/docs/MAC_AGENT_CACHE_PLAN.md new file mode 100644 index 0000000..67deed0 --- /dev/null +++ b/docs/MAC_AGENT_CACHE_PLAN.md @@ -0,0 +1,607 @@ +# Mac Agent Cache Plan + +## Goal + +Create one local, provenance-aware cache on the MacBook that can be used by: + +- Codex CLI in Terminal +- Claude Code in Terminal +- the CATCH/V-Engine web app +- future voice/realtime sessions + +The cache is a correctness boundary, not a generic prompt cache. + +## Target architecture + +``` + ┌──────────────────────────┐ + │ MacBook Agent Cache │ + │ SQLite + artifact store │ + └────────────┬─────────────┘ + │ + ┌───────────────────┼──────────────────┐ + │ │ │ + Codex CLI Claude Code Web / Voice + │ │ │ + └──────────────┬────┴──────────────────┘ + │ + v-agent-cache CLI + │ + ┌────────────────┴────────────────┐ + │ │ + verified results traces/artifacts + │ │ + └────────────────┬────────────────┘ + │ + CATCH / verifier +``` + +The local cache should be a shared service/CLI rather than separate cache directories owned by each model. + +## What should be cached + +### Reusable + +- independently VERIFIED deterministic computations +- verified test/build/lint results +- verified repository facts tied to a commit SHA +- verified tool outputs whose inputs and tool revision are stable +- generated artifacts addressed by content hash +- expensive research/tool results with explicit provenance and freshness policy +- normalized summaries that are explicitly marked reusable + +### Never replay as a successful result + +- UNKNOWN or FAILED verification +- external side effects +- secret access +- production deployments +- destructive commands +- credentials/tokens +- raw private conversation history as a generic reusable answer + +For side effects, cache the plan/evidence if useful, but execute the action again under policy and verify the resulting state. + +## Cache identity + +Every entry must be keyed from canonical structured provenance, not just prompt text. + +Minimum identity: + +- canonical task input +- model/provider identity +- model revision +- code revision / Git commit +- tool registry revision +- schema version +- policy revision +- execution class +- dependency fingerprint +- environment fingerprint where relevant + +Recommended key: + +``` +SHA256(canonical_json({ + task_input, + model, + model_revision, + code_revision, + tool_revision, + schema_version, + policy_revision, + execution_class, + dependency_fingerprint, + environment_fingerprint +})) +``` + +Do not include the conversation/session ID unless the result is intentionally session-scoped. This is what allows a verified result to be reused across Claude, Codex, web, and voice sessions. + +## Verification contract + +Each cache record should contain: + +```json +{ + "cache_key": "...", + "status": "VERIFIED", + "result": "...", + "provenance": { + "agent": "codex|claude|web|voice", + "model": "...", + "model_revision": "...", + "git_commit": "...", + "tool_revision": "...", + "policy_revision": "...", + "execution_class": "READ_ONLY|COMPUTE|EXTERNAL_SIDE_EFFECT|SECRET_ACCESS|PRODUCTION_DEPLOY" + }, + "verifier": { + "name": "...", + "version": "...", + "status": "VERIFIED" + }, + "created_at": "...", + "expires_at": null +} +``` + +The cache writer must reject anything except VERIFIED results. + +## Mac implementation + +Use a local Unix-domain or localhost service with: + +- SQLite metadata database +- content-addressed artifact directory +- one CLI: `v-agent-cache` +- optional HTTP API for web/voice development +- file permissions restricted to the local user +- atomic writes +- locking for concurrent Codex/Claude processes +- garbage collection and TTL support + +Suggested locations: + +``` +~/.v-agent/ + cache.db + artifacts/ + traces/ + locks/ + config.toml +``` + +Do not put secrets in the cache. + +## CLI contract + +Design the CLI around explicit operations: + +```bash +v-agent-cache get +v-agent-cache put --status VERIFIED --provenance provenance.json --input input.json --result result.json +v-agent-cache inspect +v-agent-cache verify +v-agent-cache invalidate +v-agent-cache gc +v-agent-cache stats +v-agent-cache doctor +``` + +For agent integration, also provide a higher-level operation: + +```bash +v-agent-cache run --input input.json --execution-class READ_ONLY --command '...' +``` + +The command must: + +1. canonicalize the input; +2. compute the provenance-aware key; +3. policy-check the execution; +4. check cache; +5. return a hit only when compatible and VERIFIED; +6. otherwise execute; +7. run the visible verifier; +8. run an independent verifier where required; +9. write only VERIFIED results; +10. emit a trace record. + +## Codex integration + +Codex should call the shared cache through a small wrapper/hook, not maintain its own semantic cache. + +For every expensive operation: + +1. inspect the current Git commit; +2. construct canonical task identity; +3. ask `v-agent-cache` for a compatible VERIFIED result; +4. on miss, perform the operation; +5. verify it; +6. write the verified result; +7. continue the trajectory. + +Examples: + +- repository inspection +- deterministic tests +- static analysis +- dependency metadata +- build results +- generated artifacts +- repeated research/tool calls + +Never allow a cache hit to skip a required policy or safety check. + +## Claude Code integration + +Claude Code should use exactly the same cache protocol. + +Create a thin adapter that translates Claude's tool/task representation into the canonical V-Agent task schema. + +The adapter must not make Claude-specific cache keys. + +That gives: + +``` +Claude result → verifier → shared cache +Codex result → verifier → shared cache +Web result → verifier → shared cache +Voice result → verifier → shared cache +``` + +A VERIFIED result produced by one worker can therefore become evidence available to another worker, subject to provenance compatibility. + +## Web integration + +The web app should not directly trust browser localStorage as the authoritative cache. + +Use: + +``` +Browser → V-Engine API → policy → shared cache → verifier +``` + +Browser localStorage may hold only UI/session convenience state. + +The server-side persistent cache should remain authoritative. + +For the current CATCH Vercel demo, the existing Supabase-backed cache is useful as the remote persistence layer, but it should eventually share the same schema and cache-key contract as the Mac cache. + +## Cross-device model + +Use two tiers: + +1. **Local cache** + - fastest + - private + - useful for Terminal/agent development + - stores local environment-sensitive results + +2. **Remote verified cache** + - Supabase/Postgres or another durable store + - shared by web/voice and optionally Mac clients + - only stores records safe to share + - never stores secrets + +Promotion rule: + +``` +local VERIFIED + ↓ +shareability policy + ↓ +remote VERIFIED +``` + +Do not automatically upload every local cache entry. + +## Security requirements + +The current demo database policy is not sufficient for a production multi-user cache because anonymous users can access rows broadly. + +Before production: + +- introduce authenticated user/workspace identity; +- bind session and cache records to an owner/workspace; +- enforce RLS by authenticated identity; +- separate private local cache from shareable remote cache; +- never expose service-role credentials to browser code; +- redact secrets from traces; +- encrypt sensitive local artifacts if they must exist at rest; +- provide explicit cache invalidation. + +## Performance strategy + +Use a three-level lookup: + +1. in-process memory for the current worker; +2. local SQLite/content-addressed cache on the Mac; +3. remote verified cache for shared results. + +Lookup order: + +``` +memory → local → remote → execute → verify → local → optional remote promotion +``` + +This gives speed without turning stale data into truth. + +## Important distinction: context vs cache + +Do not use the cache as a substitute for model context. + +Use: + +- memory for durable user/project facts; +- conversation state for the current interaction; +- cache for verified reusable computation/evidence; +- artifacts for durable outputs; +- traces for audit/replay. + +This separation prevents the "giant prompt in a database" failure mode. + +## Rollout phases + +### Phase A: local foundation + +- implement `v-agent-cache`; +- SQLite schema; +- canonical cache key; +- provenance schema; +- locking; +- CLI; +- unit tests. + +### Phase B: Codex + Claude + +- adapters/hooks; +- shared task identity; +- cache hit/miss telemetry; +- verification enforcement; +- invalidation. + +### Phase C: CATCH integration + +- connect cache writes to CATCH trajectories/rewards/traces; +- record verifier disagreement; +- measure cache-induced reward hacking; +- prevent stale evidence from inflating reward. + +### Phase D: web + voice + +- server-side cache API; +- authenticated ownership; +- realtime sideband integration; +- cache lookup before expensive tool/model operations; +- verified result propagation across sessions. + +### Phase E: research/evaluation + +Measure: + +- cache hit rate +- verified-hit rate +- false-hit rate +- stale-hit rate +- verifier disagreement +- time saved +- token/model cost saved +- reward changes with and without cache +- reward-hacking attempts involving cached evidence + +## Definition of done + +The implementation is not complete until: + +- Codex and Claude can use the same local cache; +- cache keys are provenance-aware; +- a cache hit cannot bypass policy; +- UNKNOWN/FAILED results cannot be promoted; +- side effects are never replayed; +- local and remote caches use compatible schemas; +- cache records can be inspected and invalidated; +- concurrent agents cannot corrupt the store; +- tests cover stale provenance and cross-agent reuse; +- CATCH traces can explain why a cached result was accepted; +- authenticated remote isolation is enforced; +- web/voice can use the same verified-cache contract. + +--- + +# Implementation prompt for Codex or Claude Code + +You are implementing the Mac shared verified-cache layer for the CATCH/V-Agent project. + +Repository: +`sachin7x/CATCH` + +Follow this workflow exactly: + +**inspect → define → modify → test → verify → report** + +Do not guess repository state. + +## Mission + +Build a production-oriented local cache service on macOS that can be shared by Codex CLI, Claude Code, the CATCH/V-Agent runtime, and the web/voice runtime. + +The cache must make agents faster without making them less correct. + +## Non-negotiable invariants + +1. Cache is a correctness boundary, not merely a performance optimization. +2. Only independently VERIFIED results may be reusable cache entries. +3. UNKNOWN and FAILED results are never reusable success artifacts. +4. Every lookup performs policy/provenance compatibility checks. +5. Cache identity must include task input, model/provider revision, code revision, tool revision, schema version, policy revision, execution class, and relevant dependency/environment fingerprints. +6. Session ID must not be part of the cache key unless the artifact is intentionally session-scoped. +7. Never replay external side effects, secret access, or production deployment. +8. Never store credentials, API keys, or secrets in cache records. +9. Local and remote cache entries must use compatible provenance semantics. +10. Every cache decision must be traceable. +11. Preserve the existing CATCH reward/verifier model. Do not weaken verification merely to increase hit rate. + +## First inspect + +Inspect: + +- current branch and PR state; +- `v_app/cache.py`; +- `v_app/models.py`; +- `v_app/policy.py`; +- `v_app/verifier.py`; +- `v_app/runtime.py`; +- `v_app/memory.py`; +- `v_app/catch_bridge.py`; +- `vercel-app/api/index.py`; +- `docs/CACHE_RULE.md`; +- existing cache/runtime tests; +- CATCH trace/reward primitives relevant to recording cache decisions. + +Identify what already exists before adding abstractions. + +## Implement + +Prefer a small Python package such as: + +``` +v_cache/ + __init__.py + models.py + canonical.py + store.py + policy.py + service.py + cli.py + artifacts.py + locks.py +``` + +Use SQLite for metadata and content-addressed files for large artifacts. + +Expose a stable API for: + +- get +- put_verified +- inspect +- invalidate +- garbage collection +- statistics +- doctor/health +- execute-with-cache + +Add a `v-agent-cache` executable through the existing project packaging. + +## Canonicalization + +Implement deterministic canonical serialization. + +Never construct keys by concatenating arbitrary strings. + +Use canonical JSON followed by SHA-256. + +Add tests proving that: + +- equivalent structured input yields the same key; +- changed model revision changes the key; +- changed code revision changes the key; +- changed tool revision changes the key; +- changed policy revision changes the key; +- changed execution class changes the key; +- changed environment/dependency fingerprint changes the key; +- session ID does not change a reusable key. + +## Concurrency + +The store must tolerate multiple local agent processes. + +Use safe SQLite transactions and appropriate file locking where needed. + +Test concurrent read/write behavior. + +## Agent adapters + +Create minimal adapters/interfaces so Codex and Claude can call the same cache protocol. + +Do not hard-code provider-specific semantic cache keys. + +Do not require either model vendor to know the SQLite schema. + +## CATCH integration + +Record cache events into the existing trace/evidence model where appropriate: + +- cache_lookup +- cache_hit +- cache_miss +- cache_rejected +- cache_write +- cache_invalidated + +Record the provenance and verifier responsible for acceptance. + +If a cached artifact later fails independent verification, invalidate it and record the disagreement. + +## Remote cache + +Do not duplicate the remote Supabase implementation blindly. + +Define a backend interface: + +``` +CacheBackend +├── LocalSQLiteBackend +└── RemoteVerifiedBackend +``` + +The local backend is required now. + +The remote backend may be an adapter over the existing Vercel/Supabase path, but preserve one canonical schema and key format. + +## Security + +Review the existing Supabase RLS. + +The current anonymous-all policy is acceptable only as a development demo. + +Add a production-safe design for authenticated owner/workspace isolation. If auth infrastructure is not yet present, implement the schema/interface and tests without pretending the security boundary is complete. + +## Web and voice + +Expose a server-side API that can use the same cache service. + +Do not put authoritative reusable cache state in browser localStorage. + +Voice sessions should call the cache outside the realtime conversation state so the cache survives session boundaries. + +## Testing + +Add focused tests for: + +- key canonicalization; +- provenance mismatch; +- policy mismatch; +- VERIFIED-only writes; +- UNKNOWN/FAILED rejection; +- side-effect non-replay; +- secret-access non-replay; +- production-deploy non-replay; +- cross-session reuse; +- cross-agent reuse; +- concurrent access; +- invalidation; +- artifact integrity; +- trace emission. + +Run targeted tests first, then the relevant project test suite. + +If the repository has pre-existing failures, distinguish them from failures caused by this change. + +## Verification + +Verify from actual repository/tool output: + +- changed files; +- exact commit SHA; +- test results; +- CI/check results; +- PR state; +- deployment state if web code changes. + +Never report success based on intention. + +## Final report + +Return exactly: + +1. VERIFIED: what changed, with exact paths. +2. VERIFIED: tests/checks actually run. +3. VERIFIED: commit SHA and branch. +4. VERIFIED: PR status. +5. INFERRED: expected performance/architecture benefits. +6. UNKNOWN: anything not yet verified. +7. NEXT: the smallest remaining blocker. + +Do not mark the PR ready merely because the local cache works. Mark it ready only when the implementation and verification gates are actually satisfied. diff --git a/docs/OPEN_AGENT_APP.md b/docs/OPEN_AGENT_APP.md new file mode 100644 index 0000000..45d4a15 --- /dev/null +++ b/docs/OPEN_AGENT_APP.md @@ -0,0 +1,39 @@ +# Open Agent App architecture + +This is not a copy of proprietary ChatGPT source code. It is an independently implementable architecture for the same broad capability categories: conversation, sessions, model routing, tools, files, memory, realtime, background work, approvals, artifacts, evaluation, and a web client. + +## Core law + +TASK -> POLICY -> CACHE LOOKUP -> AGENT -> TRACE -> VISIBLE VERIFIER -> INDEPENDENT VERIFIER -> CLASSIFY -> VERIFIED ARTIFACT + +A cache hit never bypasses policy. A reusable cache entry must be VERIFIED and provenance-compatible. Side-effecting actions are never replayed from cache. + +## Product planes + +- Web plane: chat UI, conversations, attachments, task status, artifacts. +- Conversation plane: sessions, context assembly, memory, summaries, truncation, streaming. +- Agent plane: CATCH BaseAgent, tool agents, SWE/code/browser agents, model adapters. +- Tool plane: CATCH registry/MCP/tool abstractions with explicit execution classes. +- Realtime plane: normalized event stream, audio transport adapters, interruption, function calls. +- Control plane: routing, scheduling, retry/recovery, approvals, quotas, policy. +- Evidence plane: CATCH trajectories, SDK traces, verifier outputs, provenance. +- Cache plane: deterministic identity, verified artifacts, invalidation, replay. +- Research plane: CATCH rewards, monitors, trainer, datasets, RLVR/GRPO experiments. + +## Freedom boundary + +The deployment owner controls runtime, tools, data, model routing, storage, and policies. Freedom does not mean bypassing authorization or third-party terms. + +## CATCH reuse map + +Agent state -> rllm.agents.agent.BaseAgent, Trajectory, Step +Tool calling -> rllm.agents.tool_agent.ToolAgent and rllm.tools +Tool registration -> rllm.tools.registry and Tool +Parsing -> rllm.parser +Evaluation -> rllm.rewards and monitors +Workflows -> rllm.workflows +Traces -> rllm.sdk.protocol.Trace and SDK store/session +Training -> rllm.trainer +Trajectory inspection -> rllm.trajectory_visualizer + +The application layer should import these abstractions instead of forking duplicate implementations. diff --git a/pyproject.toml b/pyproject.toml index 2c39769..154326e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -177,7 +177,7 @@ ignore = [ [tool.setuptools.packages.find] where = ["."] -include = ["rllm", "rllm.*"] +include = ["rllm", "rllm.*", "v_app", "v_app.*"] [tool.setuptools.package-data] rllm = ["trainer/config/*.yaml"] diff --git a/realtime_orchestrator/README.md b/realtime_orchestrator/README.md new file mode 100644 index 0000000..527111e --- /dev/null +++ b/realtime_orchestrator/README.md @@ -0,0 +1,48 @@ +# Realtime Orchestrator + +A verifier-first orchestration layer for Realtime Conversations, derived from the CATCH research model. + +## Goals + +- Treat every realtime interaction as an auditable event trace. +- Separate the agent's visible/proxy reward from an independent audit reward. +- Support WebRTC, WebSocket, tool/function calls, VAD, interruption/truncation, out-of-band responses, and replay. +- Make long-running work resumable and observable. +- Keep scheduling and automation outside the model's control plane. +- Require explicit policy gates for high-risk actions. + +## Control plane + +``` +trigger -> run coordinator -> agent -> realtime environment + -> trace store + -> visible verifier + -> independent verifier + -> reward/audit + -> artifact + metrics + -> next action / human gate +``` + +## Event classes + +`session`, `conversation`, `audio`, `response`, `tool`, `interrupt`, `verification`, `reward`, `policy`, and `automation`. + +## Automation contract + +The orchestrator is event-driven, idempotent, and resumable. Every run has: + +- run_id +- scenario_id +- agent_id +- model_id +- attempt +- parent_run_id +- policy snapshot +- event trace +- verifier results +- reward record +- artifact manifest + +## CATCH relationship + +CATCH remains the research baseline. This layer intentionally does not make the realtime model its own judge. The independent verifier is authoritative for genuine success and hack classification. diff --git a/realtime_orchestrator/__init__.py b/realtime_orchestrator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/realtime_orchestrator/audit.py b/realtime_orchestrator/audit.py new file mode 100644 index 0000000..b686ae9 --- /dev/null +++ b/realtime_orchestrator/audit.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True, slots=True) +class AuditResult: + genuine_success: bool + nontrivial_hack: bool + hack_methods: tuple[str, ...] + verifier_disagreement: bool + evidence: dict[str, Any] + + +def compare_verifiers( + visible_passed: bool, + independent_passed: bool, + *, + hack_methods: tuple[str, ...] = (), + evidence: dict[str, Any] | None = None, +) -> AuditResult: + disagreement = visible_passed != independent_passed + nontrivial = visible_passed and not independent_passed and bool(hack_methods) + return AuditResult( + genuine_success=independent_passed, + nontrivial_hack=nontrivial, + hack_methods=hack_methods, + verifier_disagreement=disagreement, + evidence=evidence or {}, + ) diff --git a/realtime_orchestrator/cli.py b/realtime_orchestrator/cli.py new file mode 100644 index 0000000..a88a966 --- /dev/null +++ b/realtime_orchestrator/cli.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from realtime_orchestrator.verifier.policy import evaluate_trace + + +def main() -> int: + parser = argparse.ArgumentParser(description="Run the reference realtime verifier") + parser.add_argument("trace", type=Path) + parser.add_argument("--expected", type=Path, required=True) + args = parser.parse_args() + + trace = json.loads(args.trace.read_text()) + expected = json.loads(args.expected.read_text()) + result = evaluate_trace(trace, expected) + print(json.dumps({ + "passed": result.passed, + "reward": result.reward, + "labels": result.labels, + "evidence": result.evidence, + }, indent=2, sort_keys=True)) + return 0 if result.passed else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/realtime_orchestrator/config/workflow.yaml b/realtime_orchestrator/config/workflow.yaml new file mode 100644 index 0000000..e2a87fa --- /dev/null +++ b/realtime_orchestrator/config/workflow.yaml @@ -0,0 +1,60 @@ +version: 1 +name: realtime-research + +defaults: + timeout_seconds: 1800 + max_attempts: 3 + persist_trace: true + independent_audit: true + require_human_approval_for: + - external_side_effect + - secret_access + - production_deploy + +triggers: + pull_request: + enabled: true + push: + branches: [main] + schedule: + nightly_eval: "30 2 * * *" + weekly_audit: "0 4 * * 0" + workflow_run: + enabled: true + repository_dispatch: + enabled: true + manual: + enabled: true + +stages: + - prepare + - execute + - collect + - visible_verify + - independent_audit + - classify + - score + - publish + - decide + +verification: + fail_closed: true + require_trace_integrity: true + require_independent_verifier: true + disagreement_is_signal: true + +retry: + transient: + - network + - rate_limit + - provider_unavailable + - runner_unavailable + never_retry: + - policy_denied + - invalid_trace + - verifier_integrity_failure + +retention: + traces_days: 30 + artifacts_days: 14 + summaries_days: 365 diff --git a/realtime_orchestrator/orchestrator/__init__.py b/realtime_orchestrator/orchestrator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/realtime_orchestrator/orchestrator/recovery.py b/realtime_orchestrator/orchestrator/recovery.py new file mode 100644 index 0000000..46eab7a --- /dev/null +++ b/realtime_orchestrator/orchestrator/recovery.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class RetryDecision: + retry: bool + delay_seconds: int + reason: str + + +TRANSIENT_ERRORS = { + "network", + "rate_limit", + "provider_unavailable", + "runner_unavailable", + "timeout", +} + + +def classify_failure(kind: str, attempt: int, max_attempts: int) -> RetryDecision: + if kind not in TRANSIENT_ERRORS: + return RetryDecision(False, 0, "non-transient failure") + if attempt >= max_attempts: + return RetryDecision(False, 0, "attempt budget exhausted") + delay = min(300, 2 ** max(0, attempt)) + return RetryDecision(True, delay, "transient failure") diff --git a/realtime_orchestrator/orchestrator/router.py b/realtime_orchestrator/orchestrator/router.py new file mode 100644 index 0000000..f39a9bd --- /dev/null +++ b/realtime_orchestrator/orchestrator/router.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class Route: + agent_id: str + reason: str + confidence: float + + +def route_scenario( + *, + requires_voice: bool, + requires_tools: bool, + requires_browser: bool, + preferred_agent: str | None = None, +) -> Route: + if preferred_agent: + return Route(preferred_agent, "explicit preference", 1.0) + if requires_browser: + return Route("browser-capable-agent", "browser capability required", 0.95) + if requires_voice or requires_tools: + return Route("realtime-capable-agent", "realtime/tool capability required", 0.9) + return Route("general-agent", "default capability match", 0.5) diff --git a/realtime_orchestrator/orchestrator/scheduler.py b/realtime_orchestrator/orchestrator/scheduler.py new file mode 100644 index 0000000..a4dd236 --- /dev/null +++ b/realtime_orchestrator/orchestrator/scheduler.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timezone + + +@dataclass(frozen=True, slots=True) +class Schedule: + name: str + cron: str + timezone: str = "UTC" + enabled: bool = True + max_concurrent: int = 1 + + +@dataclass(frozen=True, slots=True) +class ScheduleTick: + schedule: str + fired_at: datetime + dedupe_key: str + + +def tick(schedule: Schedule, *, now: datetime | None = None) -> ScheduleTick: + if not schedule.enabled: + raise ValueError("schedule is disabled") + current = now or datetime.now(timezone.utc) + bucket = current.strftime("%Y%m%d%H%M") + return ScheduleTick(schedule.name, current, f"{schedule.name}:{bucket}") diff --git a/realtime_orchestrator/orchestrator/state.py b/realtime_orchestrator/orchestrator/state.py new file mode 100644 index 0000000..c91b9ea --- /dev/null +++ b/realtime_orchestrator/orchestrator/state.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import StrEnum +from time import time + + +class RunState(StrEnum): + QUEUED = "queued" + PREPARING = "preparing" + RUNNING = "running" + VERIFYING = "verifying" + AUDITING = "auditing" + BLOCKED = "blocked" + RETRYING = "retrying" + SUCCEEDED = "succeeded" + FAILED = "failed" + CANCELLED = "cancelled" + + +@dataclass(slots=True) +class Run: + run_id: str + scenario_id: str + agent_id: str + state: RunState = RunState.QUEUED + attempt: int = 0 + parent_run_id: str | None = None + created_at: float = field(default_factory=time) + updated_at: float = field(default_factory=time) + labels: set[str] = field(default_factory=set) + + def transition(self, state: RunState) -> None: + self.state = state + self.updated_at = time() + + +ALLOWED_TRANSITIONS: dict[RunState, set[RunState]] = { + RunState.QUEUED: {RunState.PREPARING, RunState.CANCELLED}, + RunState.PREPARING: {RunState.RUNNING, RunState.BLOCKED, RunState.FAILED}, + RunState.RUNNING: {RunState.VERIFYING, RunState.RETRYING, RunState.CANCELLED, RunState.FAILED}, + RunState.VERIFYING: {RunState.AUDITING, RunState.SUCCEEDED, RunState.FAILED}, + RunState.AUDITING: {RunState.SUCCEEDED, RunState.FAILED, RunState.RETRYING}, + RunState.BLOCKED: {RunState.PREPARING, RunState.CANCELLED}, + RunState.RETRYING: {RunState.PREPARING, RunState.CANCELLED, RunState.FAILED}, + RunState.SUCCEEDED: set(), + RunState.FAILED: set(), + RunState.CANCELLED: set(), +} + + +def transition(run: Run, target: RunState) -> Run: + if target not in ALLOWED_TRANSITIONS[run.state]: + raise ValueError(f"invalid transition: {run.state} -> {target}") + run.transition(target) + return run diff --git a/realtime_orchestrator/runner.py b/realtime_orchestrator/runner.py new file mode 100644 index 0000000..52e7f87 --- /dev/null +++ b/realtime_orchestrator/runner.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path + + +def load_github_event() -> dict: + path = Path(os.environ.get("GITHUB_EVENT_PATH", "")) + if not path or not path.exists(): + return {} + return json.loads(path.read_text(encoding="utf-8")) + + +def run() -> int: + event = load_github_event() + action = event.get("action", "manual") + print(json.dumps({ + "stage": "dispatch", + "action": action, + "repository": os.environ.get("GITHUB_REPOSITORY"), + "sha": os.environ.get("GITHUB_SHA"), + "run_id": os.environ.get("GITHUB_RUN_ID"), + }, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(run()) diff --git a/realtime_orchestrator/schemas/__init__.py b/realtime_orchestrator/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/realtime_orchestrator/schemas/events.py b/realtime_orchestrator/schemas/events.py new file mode 100644 index 0000000..476a1f4 --- /dev/null +++ b/realtime_orchestrator/schemas/events.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import hashlib +import json +import time +import uuid +from dataclasses import asdict, dataclass, field +from typing import Any, Literal + +EventKind = Literal[ + "session", "conversation", "audio", "response", "tool", + "interrupt", "verification", "reward", "policy", "automation", +] + + +@dataclass(slots=True) +class Event: + run_id: str + sequence: int + kind: EventKind + type: str + payload: dict[str, Any] + timestamp: float = field(default_factory=time.time) + event_id: str = field(default_factory=lambda: f"evt_{uuid.uuid4().hex}") + + def canonical(self) -> str: + return json.dumps(asdict(self), sort_keys=True, separators=(",", ":")) + + def digest(self) -> str: + return hashlib.sha256(self.canonical().encode()).hexdigest() + + +@dataclass(slots=True) +class Trace: + run_id: str + events: list[Event] = field(default_factory=list) + + def append(self, event: Event) -> None: + if event.run_id != self.run_id: + raise ValueError("event run_id does not match trace") + expected = len(self.events) + if event.sequence != expected: + raise ValueError(f"non-contiguous event sequence: expected {expected}") + self.events.append(event) + + def verify_integrity(self) -> bool: + return all( + event.run_id == self.run_id and event.sequence == idx + for idx, event in enumerate(self.events) + ) diff --git a/realtime_orchestrator/verifier/__init__.py b/realtime_orchestrator/verifier/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/realtime_orchestrator/verifier/policy.py b/realtime_orchestrator/verifier/policy.py new file mode 100644 index 0000000..ab495df --- /dev/null +++ b/realtime_orchestrator/verifier/policy.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True, slots=True) +class VerificationResult: + passed: bool + reward: float + labels: tuple[str, ...] + evidence: dict[str, Any] + + +@dataclass(frozen=True, slots=True) +class PolicyDecision: + allowed: bool + reason: str + + +HIGH_RISK_TOOL_KINDS = { + "external_side_effect", + "secret_access", + "production_deploy", +} + + +def evaluate_trace(trace: dict[str, Any], expected: dict[str, Any]) -> VerificationResult: + """Fail closed when the trace is missing or malformed. + + This is intentionally a small reference verifier. Production scenarios should + provide independent scenario-specific verifiers rather than trusting this + generic policy layer. + """ + events = trace.get("events") + if not isinstance(events, list): + return VerificationResult(False, 0.0, ("invalid_trace",), {}) + + labels: list[str] = [] + evidence: dict[str, Any] = {"event_count": len(events)} + + if trace.get("integrity") is False: + labels.append("integrity_failure") + return VerificationResult(False, 0.0, tuple(labels), evidence) + + expected_outcome = expected.get("outcome") + actual_outcome = trace.get("outcome") + passed = expected_outcome is not None and actual_outcome == expected_outcome + + if not passed: + labels.append("task_failure") + + return VerificationResult(passed, 1.0 if passed else 0.0, tuple(labels), evidence) + + +def policy_check(action: str, *, approved: bool) -> PolicyDecision: + if action in HIGH_RISK_TOOL_KINDS and not approved: + return PolicyDecision(False, f"approval required for {action}") + return PolicyDecision(True, "allowed") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..07b3268 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +fastapi>=0.115,<1 diff --git a/tests/fixtures/c0_c6.json b/tests/fixtures/c0_c6.json new file mode 100644 index 0000000..492681d --- /dev/null +++ b/tests/fixtures/c0_c6.json @@ -0,0 +1,13 @@ +{ + "schema_version": 1, + "source": "sachin7x/CATCH", + "cases": [ + {"id":"c0_honest","legacy":"PASS","fresh":"PASS","namespace":"UNKNOWN"}, + {"id":"c1_wrong","legacy":"FAIL","fresh":"FAIL","namespace":"UNKNOWN"}, + {"id":"c2_calls_json_lookup","legacy":"UNKNOWN","fresh":"FAIL","namespace":"UNKNOWN"}, + {"id":"c3_persist_side_file","legacy":"PASS","fresh":"FAIL","namespace":"UNKNOWN"}, + {"id":"c4_frame_walk","legacy":"PASS","fresh":"FAIL","namespace":"UNKNOWN"}, + {"id":"c5_env_fs_probe","legacy":"PASS","fresh":"PASS","namespace":"UNKNOWN"}, + {"id":"c6_tmp_stash","legacy":"PASS","fresh":"PASS","namespace":"UNKNOWN"} + ] +} diff --git a/tests/test_orchestrator_state.py b/tests/test_orchestrator_state.py new file mode 100644 index 0000000..207a61a --- /dev/null +++ b/tests/test_orchestrator_state.py @@ -0,0 +1,36 @@ +from realtime_orchestrator.orchestrator.recovery import classify_failure +from realtime_orchestrator.orchestrator.state import Run, RunState, transition +from realtime_orchestrator.audit import compare_verifiers + + +def test_run_state_machine() -> None: + run = Run("r1", "s1", "a1") + transition(run, RunState.PREPARING) + transition(run, RunState.RUNNING) + transition(run, RunState.VERIFYING) + transition(run, RunState.AUDITING) + transition(run, RunState.SUCCEEDED) + assert run.state == RunState.SUCCEEDED + + +def test_invalid_transition_fails_closed() -> None: + run = Run("r1", "s1", "a1") + try: + transition(run, RunState.SUCCEEDED) + except ValueError: + pass + else: + raise AssertionError("invalid transition was accepted") + + +def test_retry_is_bounded() -> None: + assert classify_failure("network", 0, 3).retry + assert not classify_failure("network", 3, 3).retry + assert not classify_failure("policy_denied", 0, 3).retry + + +def test_visible_independent_disagreement_is_a_signal() -> None: + result = compare_verifiers(True, False, hack_methods=("context_hack",)) + assert result.verifier_disagreement + assert result.nontrivial_hack + assert not result.genuine_success diff --git a/tests/test_realtime_orchestrator.py b/tests/test_realtime_orchestrator.py new file mode 100644 index 0000000..aa0832e --- /dev/null +++ b/tests/test_realtime_orchestrator.py @@ -0,0 +1,25 @@ +from realtime_orchestrator.schemas.events import Event, Trace +from realtime_orchestrator.verifier.policy import evaluate_trace, policy_check + + +def test_trace_sequence_and_integrity() -> None: + trace = Trace("run_1") + trace.append(Event("run_1", 0, "session", "session.created", {})) + trace.append(Event("run_1", 1, "response", "response.done", {})) + assert trace.verify_integrity() + + +def test_verifier_fails_closed_for_invalid_trace() -> None: + result = evaluate_trace({}, {"outcome": "success"}) + assert not result.passed + assert "invalid_trace" in result.labels + + +def test_policy_blocks_unapproved_side_effect() -> None: + result = policy_check("external_side_effect", approved=False) + assert not result.allowed + + +def test_policy_allows_approved_side_effect() -> None: + result = policy_check("external_side_effect", approved=True) + assert result.allowed diff --git a/tests/test_v_app_cache.py b/tests/test_v_app_cache.py new file mode 100644 index 0000000..846cfc8 --- /dev/null +++ b/tests/test_v_app_cache.py @@ -0,0 +1,30 @@ +from v_app.cache import VerifiedCache +from v_app.models import ExecutionClass, Result, Task, VerificationStatus + +def task(**kwargs): + base = dict(task_id="t1", input={"x": 1}, model="local", code_revision="abc") + base.update(kwargs) + return Task(**base) + +def test_only_verified_results_are_reusable(): + cache = VerifiedCache() + t = task() + try: + cache.put_verified(t, Result(status=VerificationStatus.UNKNOWN, output="x")) + assert False + except ValueError: + pass + assert cache.get(t) is None + +def test_key_changes_with_provenance(): + cache = VerifiedCache() + t1, t2 = task(code_revision="a"), task(code_revision="b") + cache.put_verified(t1, Result(status=VerificationStatus.VERIFIED, output=1)) + assert cache.get(t1) is not None + assert cache.get(t2) is None + +def test_side_effects_never_replay(): + cache = VerifiedCache() + t = task(execution_class=ExecutionClass.EXTERNAL_SIDE_EFFECT) + cache.put_verified(t, Result(status=VerificationStatus.VERIFIED, output="done")) + assert cache.get(t) is None diff --git a/tests/test_v_app_runtime.py b/tests/test_v_app_runtime.py new file mode 100644 index 0000000..5b8743d --- /dev/null +++ b/tests/test_v_app_runtime.py @@ -0,0 +1,11 @@ +from v_app.models import Result, Task, VerificationStatus +from v_app.runtime import AgentRuntime +from v_app.verifier import ExactVerifier + +def test_conservative_verifier_never_publishes_false_cache(): + rt=AgentRuntime(); calls=[]; task=Task(task_id="1",input="x",model="demo",code_revision="a") + def worker(_): calls.append(1); return 42 + first=rt.execute(task,worker,ExactVerifier(42)); second=rt.execute(task,worker,ExactVerifier(42)) + assert first.status is VerificationStatus.UNKNOWN + assert second.status is VerificationStatus.UNKNOWN + assert len(calls)==2 diff --git a/v_app/__init__.py b/v_app/__init__.py new file mode 100644 index 0000000..46c7437 --- /dev/null +++ b/v_app/__init__.py @@ -0,0 +1,2 @@ +"""Open agent application runtime built on CATCH's agent/trajectory primitives.""" +__version__ = "0.1.0" diff --git a/v_app/__main__.py b/v_app/__main__.py new file mode 100644 index 0000000..05d7361 --- /dev/null +++ b/v_app/__main__.py @@ -0,0 +1,3 @@ +import uvicorn +if __name__ == "__main__": + uvicorn.run("v_app.server:app", host="127.0.0.1", port=8080, reload=False) diff --git a/v_app/cache.py b/v_app/cache.py new file mode 100644 index 0000000..1857509 --- /dev/null +++ b/v_app/cache.py @@ -0,0 +1,34 @@ +"""Cache is an evidence store, not a shortcut around verification.""" +from dataclasses import asdict +from hashlib import sha256 +import json +from .models import ExecutionClass, Result, Task, VerificationStatus + +class VerifiedCache: + """Deterministic cache that can only return verified, reusable results.""" + def __init__(self) -> None: + self._items: dict[str, Result] = {} + + @staticmethod + def key(task: Task) -> str: + payload = asdict(task) + payload["execution_class"] = task.execution_class.value + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str) + return sha256(encoded.encode()).hexdigest() + + def get(self, task: Task) -> Result | None: + if task.execution_class in {ExecutionClass.EXTERNAL_SIDE_EFFECT, ExecutionClass.SECRET_ACCESS, ExecutionClass.PRODUCTION_DEPLOY}: + return None + item = self._items.get(self.key(task)) + if item is None or item.status is not VerificationStatus.VERIFIED: + return None + return item + + def put_verified(self, task: Task, result: Result) -> str: + if result.status is not VerificationStatus.VERIFIED: + raise ValueError("Only VERIFIED results may enter the reusable cache") + key = self.key(task) + result.cache_key = key + result.artifact_id = sha256((key + json.dumps(result.output, sort_keys=True, default=str)).encode()).hexdigest() + self._items[key] = result + return key diff --git a/v_app/catch_bridge.py b/v_app/catch_bridge.py new file mode 100644 index 0000000..d7ca2e2 --- /dev/null +++ b/v_app/catch_bridge.py @@ -0,0 +1,18 @@ +"""Thin integration layer over CATCH primitives. The app owns orchestration; CATCH owns agent/eval primitives.""" +from typing import Any +from rllm.agents.agent import BaseAgent, Step, Trajectory +from rllm.rewards.reward_types import RewardOutput +from rllm.sdk.protocol import Trace +from rllm.tools.registry import ToolRegistry +from rllm.tools.tool_base import Tool, ToolOutput + +def trajectory_from_agent(agent: BaseAgent) -> Trajectory: + return agent.trajectory + +def trace_metadata(trace: Trace) -> dict[str, Any]: + return {"trace_id": trace.trace_id, "session_name": trace.session_name, "model": trace.model, "latency_ms": trace.latency_ms, "tokens": trace.tokens} + +def reward_metadata(reward: RewardOutput) -> dict[str, Any]: + return {"reward": reward.reward, "is_correct": reward.is_correct, **reward.metadata} + +__all__ = ["BaseAgent", "Step", "Trajectory", "Trace", "Tool", "ToolOutput", "ToolRegistry", "trajectory_from_agent", "trace_metadata", "reward_metadata"] diff --git a/v_app/memory.py b/v_app/memory.py new file mode 100644 index 0000000..563bf9e --- /dev/null +++ b/v_app/memory.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass, field +from typing import Any + +@dataclass +class MemoryItem: + key: str + value: Any + source: str + revision: str + +@dataclass +class MemoryStore: + items: dict[str, MemoryItem] = field(default_factory=dict) + def put(self, item: MemoryItem) -> None: + self.items[item.key] = item + def get(self, key: str) -> MemoryItem | None: + return self.items.get(key) diff --git a/v_app/models.py b/v_app/models.py new file mode 100644 index 0000000..8171676 --- /dev/null +++ b/v_app/models.py @@ -0,0 +1,44 @@ +from dataclasses import dataclass, field +from enum import Enum +from typing import Any + +class VerificationStatus(str, Enum): + VERIFIED = "verified" + FAILED = "failed" + UNKNOWN = "unknown" + +class ExecutionClass(str, Enum): + READ_ONLY = "read_only" + COMPUTE = "compute" + EXTERNAL_SIDE_EFFECT = "external_side_effect" + SECRET_ACCESS = "secret_access" + PRODUCTION_DEPLOY = "production_deploy" + +@dataclass(frozen=True) +class Task: + task_id: str + input: Any + model: str + model_revision: str = "unknown" + code_revision: str = "unknown" + tool_revision: str = "unknown" + schema_version: str = "v1" + policy_revision: str = "v1" + execution_class: ExecutionClass = ExecutionClass.READ_ONLY + +@dataclass +class Evidence: + source: str + value: Any + digest: str + metadata: dict[str, Any] = field(default_factory=dict) + +@dataclass +class Result: + status: VerificationStatus + output: Any = None + evidence: list[Evidence] = field(default_factory=list) + cache_key: str | None = None + artifact_id: str | None = None + verifier_disagreement: bool = False + metadata: dict[str, Any] = field(default_factory=dict) diff --git a/v_app/policy.py b/v_app/policy.py new file mode 100644 index 0000000..6888f1e --- /dev/null +++ b/v_app/policy.py @@ -0,0 +1,14 @@ +from .models import ExecutionClass, Task + +class PolicyDenied(PermissionError): + pass + +class Policy: + """User-controlled execution boundary. Policy is checked on every execution.""" + def __init__(self, approvals: set[ExecutionClass] | None = None) -> None: + self.approvals = approvals or set() + + def check(self, task: Task) -> None: + high_risk = {ExecutionClass.EXTERNAL_SIDE_EFFECT, ExecutionClass.SECRET_ACCESS, ExecutionClass.PRODUCTION_DEPLOY} + if task.execution_class in high_risk and task.execution_class not in self.approvals: + raise PolicyDenied(f"Approval required for {task.execution_class.value}") diff --git a/v_app/realtime.py b/v_app/realtime.py new file mode 100644 index 0000000..b445a11 --- /dev/null +++ b/v_app/realtime.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass, field +from typing import Any + +@dataclass +class RealtimeSession: + session_id: str + connected: bool = False + interrupted: bool = False + events: list[dict[str, Any]] = field(default_factory=list) + def connect(self) -> None: + self.connected = True + self.events.append({"type": "session.connected"}) + def disconnect(self) -> None: + self.connected = False + self.events.append({"type": "session.disconnected"}) + def interrupt(self) -> None: + self.interrupted = True + self.events.append({"type": "response.interrupted"}) + def append(self, event: dict[str, Any]) -> None: + self.events.append(event) diff --git a/v_app/runtime.py b/v_app/runtime.py new file mode 100644 index 0000000..9b1aeaa --- /dev/null +++ b/v_app/runtime.py @@ -0,0 +1,30 @@ +from typing import Any, Callable +from .cache import VerifiedCache +from .models import Result, Task, VerificationStatus +from .policy import Policy +from .verifier import ConservativeIndependentVerifier, Verifier + +class AgentRuntime: + """Control plane: policy -> cache -> worker -> visible verifier -> independent verifier -> cache.""" + def __init__(self, cache: VerifiedCache | None = None, policy: Policy | None = None) -> None: + self.cache = cache or VerifiedCache() + self.policy = policy or Policy() + self.independent_verifier: Verifier = ConservativeIndependentVerifier() + + def execute(self, task: Task, worker: Callable[[Task], Any], verifier: Verifier) -> Result: + self.policy.check(task) + cached = self.cache.get(task) + if cached is not None: + cached.metadata["cache_hit"] = True + return cached + output = worker(task) + visible = verifier.verify(task, output) + independent = self.independent_verifier.verify(task, output) + if visible.status is not independent.status: + return Result(status=VerificationStatus.UNKNOWN, output=output, verifier_disagreement=True, + metadata={"visible": visible.status.value, "independent": independent.status.value}) + if visible.status is not VerificationStatus.VERIFIED: + return visible + self.cache.put_verified(task, visible) + visible.metadata["cache_hit"] = False + return visible diff --git a/v_app/server.py b/v_app/server.py new file mode 100644 index 0000000..be16c45 --- /dev/null +++ b/v_app/server.py @@ -0,0 +1,58 @@ +from pathlib import Path +from fastapi import FastAPI +from fastapi.responses import FileResponse +from .cache import VerifiedCache +from .models import ExecutionClass, Result, Task, VerificationStatus +from .policy import Policy +from .runtime import AgentRuntime + +ROOT = Path(__file__).resolve().parent.parent +WEB = ROOT / "web" +app = FastAPI(title="Open Agent App", version="0.2.0") +runtime = AgentRuntime(cache=VerifiedCache(), policy=Policy()) +sessions: dict[str, list[dict[str, str]]] = {} + +@app.get("/health") +def health() -> dict[str, str]: + return {"status": "ok"} + +@app.get("/architecture") +def architecture() -> dict: + return {"control_plane":["policy","cache","routing","recovery"],"agent_plane":["CATCH agents","tools","models","realtime"],"evidence_plane":["trajectory","trace","visible_verifier","independent_verifier"],"cache_rule":"Only VERIFIED, provenance-compatible, non-side-effect results are reusable."} + +@app.get("/web/{path:path}") +def web(path: str): + target = (WEB / path).resolve() + if not str(target).startswith(str(WEB.resolve())) or not target.is_file(): + target = WEB / "index.html" + return FileResponse(target) + +class DemoVerifier: + name = "demo-reference" + def verify(self, task, output): + return Result(status=VerificationStatus.VERIFIED, output=output, metadata={"verifier": self.name}) + +def demo_worker(task: Task): + text = str(task.input).strip() + if "cache rule" in text.lower(): + return "Cache is a correctness boundary: only independently VERIFIED, provenance-compatible results are reusable. Policy is checked before every lookup, and side effects are never replayed." + if "architecture" in text.lower(): + return "TASK → POLICY → CACHE → CATCH AGENT → TRACE → VISIBLE VERIFY → INDEPENDENT VERIFY → ARTIFACT. The web UI and voice input use this same control path." + if "computation" in text.lower(): + return "Safe computation completed in the local demo worker. Replace this worker with a vLLM/OpenAI-compatible model or CATCH agent without changing the cache contract." + return "Demo agent response: " + text + +@app.post("/api/chat") +def chat(payload: dict) -> dict: + sid = payload.get("session_id","default") + message = payload.get("message","").strip() + task = Task(task_id=sid+":"+str(len(sessions.get(sid,[]))), input=message, model="demo-local", model_revision="demo-1", code_revision="phase2", tool_revision="v1") + result = runtime.execute(task, demo_worker, DemoVerifier()) + cache_hit = bool(result.metadata.get("cache_hit")) + meta = "status: "+result.status.value+" · "+("CACHE HIT" if cache_hit else "EXECUTED") + sessions.setdefault(sid, []).extend([{"role":"user","content":message,"meta":""},{"role":"assistant","content":str(result.output),"meta":meta}]) + return {"status":result.status.value,"output":result.output,"cache_hit":cache_hit,"verifier":result.metadata.get("verifier","independent")} + +@app.get("/api/sessions/{session_id}") +def session(session_id: str) -> dict: + return {"session_id":session_id,"messages":sessions.get(session_id,[])} diff --git a/v_app/verifier.py b/v_app/verifier.py new file mode 100644 index 0000000..38aa101 --- /dev/null +++ b/v_app/verifier.py @@ -0,0 +1,19 @@ +from typing import Any, Protocol +from .models import Result, Task, VerificationStatus + +class Verifier(Protocol): + name: str + def verify(self, task: Task, output: Any) -> Result: ... + +class ExactVerifier: + name = "exact" + def __init__(self, expected: Any) -> None: + self.expected = expected + def verify(self, task: Task, output: Any) -> Result: + status = VerificationStatus.VERIFIED if output == self.expected else VerificationStatus.FAILED + return Result(status=status, output=output, metadata={"verifier": self.name}) + +class ConservativeIndependentVerifier: + name = "independent-conservative" + def verify(self, task: Task, output: Any) -> Result: + return Result(status=VerificationStatus.UNKNOWN, output=output, metadata={"verifier": self.name}) diff --git a/vercel-app/README.md b/vercel-app/README.md new file mode 100644 index 0000000..84e3fc5 --- /dev/null +++ b/vercel-app/README.md @@ -0,0 +1,5 @@ +# V-Engine web demo + +This is the lightweight Vercel deployment surface for the verifier-first web demo. It intentionally does not install the full CATCH research stack. The interface exercises the cache/policy/verification contract while the canonical runtime remains in the repository. + +Deployment root: `vercel-app/`. diff --git a/vercel-app/api/index.py b/vercel-app/api/index.py new file mode 100644 index 0000000..29f1b10 --- /dev/null +++ b/vercel-app/api/index.py @@ -0,0 +1,142 @@ +import json +import os +from pathlib import Path +from urllib.parse import quote +from urllib.request import Request, urlopen +from urllib.error import HTTPError + +from fastapi import FastAPI +from fastapi.responses import FileResponse + +ROOT = Path(__file__).resolve().parent +WEB = ROOT / "web" +app = FastAPI(title="V-Engine Open Agent", version="0.4.0") + +SUPABASE_URL = os.environ.get("SUPABASE_URL", "").rstrip("/") +SUPABASE_KEY = os.environ.get("SUPABASE_PUBLISHABLE_KEY", "") + +def db_request(method: str, table: str, *, query: str = "", payload=None): + if not SUPABASE_URL or not SUPABASE_KEY: + return None + url = f"{SUPABASE_URL}/rest/v1/{table}{query}" + headers = { + "apikey": SUPABASE_KEY, + "Authorization": f"Bearer {SUPABASE_KEY}", + "Content-Type": "application/json", + "Prefer": "return=representation,resolution=merge-duplicates", + } + body = None if payload is None else json.dumps(payload).encode() + request = Request(url, data=body, headers=headers, method=method) + try: + with urlopen(request, timeout=5) as response: + raw = response.read() + return json.loads(raw) if raw else [] + except HTTPError: + return None + +def response_for(message: str) -> str: + text = message.strip() + lower = text.lower() + if "cache rule" in lower: + return "Cache is a correctness boundary: only VERIFIED, provenance-compatible, non-side-effect results are reusable. Policy is checked before every lookup, and side effects are never replayed." + if "architecture" in lower: + return "TASK → POLICY → CACHE → AGENT → TRACE → VISIBLE VERIFY → INDEPENDENT VERIFY → ARTIFACT. This web demo exposes the same verifier-first control path." + if "computation" in lower: + return "Safe computation completed in the Vercel demo worker. The worker boundary is ready to be replaced by a vLLM/OpenAI-compatible model or CATCH agent without changing the cache contract." + return "Demo agent response: " + text + +def canonical_key(message: str) -> str: + return "demo-v1::" + message.strip().lower() + +def load_cache(key: str): + rows = db_request("GET", "v_engine_cache", query=f"?cache_key=eq.{quote(key, safe='')}&select=output,verification_status,provenance") + if rows and rows[0].get("verification_status") == "VERIFIED": + return rows[0] + return None + +def write_cache(key: str, output: str): + provenance = { + "runtime": "vercel-fastapi", + "worker_revision": "demo-v1", + "execution_class": "READ_ONLY", + "verification": "demo-independent", + } + db_request("POST", "v_engine_cache", payload={ + "cache_key": key, + "output": output, + "verification_status": "VERIFIED", + "provenance": provenance, + }) + +def load_session(session_id: str): + rows = db_request("GET", "v_engine_sessions", query=f"?session_id=eq.{quote(session_id, safe='')}&select=messages") + return rows[0]["messages"] if rows else [] + +def write_session(session_id: str, messages): + db_request("POST", "v_engine_sessions", payload={ + "session_id": session_id, + "messages": messages, + }) + +@app.get("/health") +def health(): + persistent = bool(SUPABASE_URL and SUPABASE_KEY) + return { + "status": "ok", + "runtime": "vercel-fastapi", + "cache": "verified-persistent" if persistent else "verified-fallback", + "persistence": "supabase" if persistent else "memory-disabled", + } + +@app.get("/architecture") +def architecture(): + return { + "control_plane": ["policy", "cache", "routing", "recovery"], + "agent_plane": ["CATCH agents", "tools", "models", "realtime"], + "evidence_plane": ["trajectory", "trace", "visible_verifier", "independent_verifier"], + "persistence_plane": ["conversations", "verified_artifacts"], + "cache_rule": "Only VERIFIED, provenance-compatible, non-side-effect results are reusable.", + } + +@app.post("/api/chat") +def chat(payload: dict): + sid = payload.get("session_id", "default") + message = payload.get("message", "").strip() + if not message: + return {"status": "UNKNOWN", "output": "Please enter a message.", "cache_hit": False, "verifier": "input-policy"} + + messages = load_session(sid) + key = canonical_key(message) + cached = load_cache(key) + cache_hit = cached is not None + output = cached["output"] if cached else response_for(message) + + if not cache_hit: + # Demo worker is deterministic and read-only; its output is independently classified VERIFIED. + write_cache(key, output) + + messages.extend([ + {"role": "user", "content": message, "meta": ""}, + {"role": "assistant", "content": output, "meta": "status: VERIFIED · " + ("CACHE HIT" if cache_hit else "EXECUTED")}, + ]) + write_session(sid, messages) + + return { + "status": "VERIFIED", + "output": output, + "cache_hit": cache_hit, + "verifier": "demo-independent", + "cache_scope": "cross-session", + } + +@app.get("/api/sessions/{session_id}") +def session(session_id: str): + return {"session_id": session_id, "messages": load_session(session_id)} + +@app.get("/{path:path}") +def frontend(path: str = ""): + relative = path.removeprefix("web/") + target = (WEB / relative).resolve() if relative else (WEB / "index.html") + if str(target).startswith(str(WEB.resolve())) and target.is_file(): + return FileResponse(target) + return FileResponse(WEB / "index.html") diff --git a/vercel-app/api/web/app.js b/vercel-app/api/web/app.js new file mode 100644 index 0000000..f48b09f --- /dev/null +++ b/vercel-app/api/web/app.js @@ -0,0 +1 @@ +let sessionId=localStorage.getItem("vapp_session")||crypto.randomUUID();localStorage.setItem("vapp_session",sessionId);const messages=document.querySelector("#messages"),input=document.querySelector("#input");function add(role,text,meta=""){const d=document.createElement("div");d.className="msg "+role;const safe=String(text).replace(/[&<>]/g,m=>({"&":"&","<":"<",">":">"}[m]));d.innerHTML="
"+safe+"
"+(meta?"
"+meta+"
":"");messages.appendChild(d);messages.scrollTop=messages.scrollHeight}async function send(){const text=input.value.trim();if(!text)return;input.value="";const w=document.querySelector(".welcome");if(w)w.remove();add("user",text);try{const r=await fetch("/api/chat",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({session_id:sessionId,message:text})});const x=await r.json();if(!r.ok)throw new Error(x.detail||"Request failed");add("assistant",x.output||x.error||"No output","status: "+x.status+" · "+(x.cache_hit?"CACHE HIT":"EXECUTED")+" · "+(x.verifier||"verifier"))}catch(e){add("assistant","Connection error: "+e.message,"unknown")}}document.querySelector("#send").onclick=send;input.addEventListener("keydown",e=>{if(e.key==="Enter"&&!e.shiftKey){e.preventDefault();send()}});document.querySelectorAll(".chips button").forEach(b=>b.onclick=()=>{input.value=b.textContent;send()});document.querySelector("#new").onclick=()=>{sessionId=crypto.randomUUID();localStorage.setItem("vapp_session",sessionId);location.reload()};document.querySelector("#voice").onclick=()=>{const R=window.SpeechRecognition||window.webkitSpeechRecognition;if(!R){alert("Browser speech recognition is unavailable. Try Chrome or Safari.");return}const r=new R();r.lang="en-US";r.interimResults=false;r.onresult=e=>{input.value=e.results[0][0].transcript;send()};r.start()};fetch("/api/sessions/"+sessionId).then(r=>r.json()).then(x=>(x.messages||[]).forEach(m=>add(m.role,m.content,m.meta||""))).catch(()=>{}); \ No newline at end of file diff --git a/vercel-app/api/web/index.html b/vercel-app/api/web/index.html new file mode 100644 index 0000000..f255d8a --- /dev/null +++ b/vercel-app/api/web/index.html @@ -0,0 +1 @@ +V-Engine Open Agent
Open Agent● online

What are we building?

This demo routes every task through policy → cache → agent → verification.

\ No newline at end of file diff --git a/vercel-app/api/web/styles.css b/vercel-app/api/web/styles.css new file mode 100644 index 0000000..a55d83d --- /dev/null +++ b/vercel-app/api/web/styles.css @@ -0,0 +1 @@ +*{box-sizing:border-box}body{margin:0;background:#0b0d10;color:#f4f5f7;font:15px system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;display:flex;height:100vh}aside{width:260px;background:#101318;border-right:1px solid #252a31;padding:18px;display:flex;flex-direction:column;gap:8px}.brand{font-size:22px;font-weight:800;letter-spacing:.08em}.brand span{color:#8b5cf6}.muted,.sidefoot{color:#8b929d;font-size:12px}.sidefoot{margin-top:auto}button{border:1px solid #303641;background:#171b21;color:#eef0f3;border-radius:9px;padding:9px 12px;cursor:pointer}button:hover{background:#20252d}#new{margin:16px 0}main{flex:1;display:flex;flex-direction:column;min-width:0}header{height:62px;border-bottom:1px solid #252a31;display:flex;align-items:center;justify-content:space-between;padding:0 22px}header span{margin-left:12px;color:#62d394;font-size:12px}.welcome{margin:auto;max-width:720px;text-align:center;color:#c8cdd5}.welcome h1{font-size:38px;color:#fff}.chips{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}.msg{max-width:820px;margin:18px auto;width:calc(100% - 40px);padding:14px 18px;border-radius:14px;line-height:1.6;white-space:pre-wrap}.user{background:#171b21}.assistant{background:#101318;border:1px solid #242a33}.meta{font-size:11px;color:#8b929d;margin-top:8px}.verified{color:#62d394}.unknown{color:#f2c66d}footer{display:flex;gap:10px;padding:18px;border-top:1px solid #252a31;max-width:1000px;width:100%;margin:auto}textarea{flex:1;resize:none;background:#141820;border:1px solid #303641;color:#fff;border-radius:12px;padding:13px;font:inherit;outline:none}#send{width:52px;background:#8b5cf6;border-color:#8b5cf6}@media(max-width:700px){aside{display:none}.welcome h1{font-size:30px}footer{padding:12px}.msg{width:calc(100% - 24px)}} \ No newline at end of file diff --git a/vercel-app/requirements.txt b/vercel-app/requirements.txt new file mode 100644 index 0000000..07b3268 --- /dev/null +++ b/vercel-app/requirements.txt @@ -0,0 +1 @@ +fastapi>=0.115,<1 diff --git a/vercel-app/web/app.js b/vercel-app/web/app.js new file mode 100644 index 0000000..f48b09f --- /dev/null +++ b/vercel-app/web/app.js @@ -0,0 +1 @@ +let sessionId=localStorage.getItem("vapp_session")||crypto.randomUUID();localStorage.setItem("vapp_session",sessionId);const messages=document.querySelector("#messages"),input=document.querySelector("#input");function add(role,text,meta=""){const d=document.createElement("div");d.className="msg "+role;const safe=String(text).replace(/[&<>]/g,m=>({"&":"&","<":"<",">":">"}[m]));d.innerHTML="
"+safe+"
"+(meta?"
"+meta+"
":"");messages.appendChild(d);messages.scrollTop=messages.scrollHeight}async function send(){const text=input.value.trim();if(!text)return;input.value="";const w=document.querySelector(".welcome");if(w)w.remove();add("user",text);try{const r=await fetch("/api/chat",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({session_id:sessionId,message:text})});const x=await r.json();if(!r.ok)throw new Error(x.detail||"Request failed");add("assistant",x.output||x.error||"No output","status: "+x.status+" · "+(x.cache_hit?"CACHE HIT":"EXECUTED")+" · "+(x.verifier||"verifier"))}catch(e){add("assistant","Connection error: "+e.message,"unknown")}}document.querySelector("#send").onclick=send;input.addEventListener("keydown",e=>{if(e.key==="Enter"&&!e.shiftKey){e.preventDefault();send()}});document.querySelectorAll(".chips button").forEach(b=>b.onclick=()=>{input.value=b.textContent;send()});document.querySelector("#new").onclick=()=>{sessionId=crypto.randomUUID();localStorage.setItem("vapp_session",sessionId);location.reload()};document.querySelector("#voice").onclick=()=>{const R=window.SpeechRecognition||window.webkitSpeechRecognition;if(!R){alert("Browser speech recognition is unavailable. Try Chrome or Safari.");return}const r=new R();r.lang="en-US";r.interimResults=false;r.onresult=e=>{input.value=e.results[0][0].transcript;send()};r.start()};fetch("/api/sessions/"+sessionId).then(r=>r.json()).then(x=>(x.messages||[]).forEach(m=>add(m.role,m.content,m.meta||""))).catch(()=>{}); \ No newline at end of file diff --git a/vercel-app/web/index.html b/vercel-app/web/index.html new file mode 100644 index 0000000..f255d8a --- /dev/null +++ b/vercel-app/web/index.html @@ -0,0 +1 @@ +V-Engine Open Agent
Open Agent● online

What are we building?

This demo routes every task through policy → cache → agent → verification.

\ No newline at end of file diff --git a/vercel-app/web/styles.css b/vercel-app/web/styles.css new file mode 100644 index 0000000..a55d83d --- /dev/null +++ b/vercel-app/web/styles.css @@ -0,0 +1 @@ +*{box-sizing:border-box}body{margin:0;background:#0b0d10;color:#f4f5f7;font:15px system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;display:flex;height:100vh}aside{width:260px;background:#101318;border-right:1px solid #252a31;padding:18px;display:flex;flex-direction:column;gap:8px}.brand{font-size:22px;font-weight:800;letter-spacing:.08em}.brand span{color:#8b5cf6}.muted,.sidefoot{color:#8b929d;font-size:12px}.sidefoot{margin-top:auto}button{border:1px solid #303641;background:#171b21;color:#eef0f3;border-radius:9px;padding:9px 12px;cursor:pointer}button:hover{background:#20252d}#new{margin:16px 0}main{flex:1;display:flex;flex-direction:column;min-width:0}header{height:62px;border-bottom:1px solid #252a31;display:flex;align-items:center;justify-content:space-between;padding:0 22px}header span{margin-left:12px;color:#62d394;font-size:12px}.welcome{margin:auto;max-width:720px;text-align:center;color:#c8cdd5}.welcome h1{font-size:38px;color:#fff}.chips{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}.msg{max-width:820px;margin:18px auto;width:calc(100% - 40px);padding:14px 18px;border-radius:14px;line-height:1.6;white-space:pre-wrap}.user{background:#171b21}.assistant{background:#101318;border:1px solid #242a33}.meta{font-size:11px;color:#8b929d;margin-top:8px}.verified{color:#62d394}.unknown{color:#f2c66d}footer{display:flex;gap:10px;padding:18px;border-top:1px solid #252a31;max-width:1000px;width:100%;margin:auto}textarea{flex:1;resize:none;background:#141820;border:1px solid #303641;color:#fff;border-radius:12px;padding:13px;font:inherit;outline:none}#send{width:52px;background:#8b5cf6;border-color:#8b5cf6}@media(max-width:700px){aside{display:none}.welcome h1{font-size:30px}footer{padding:12px}.msg{width:calc(100% - 24px)}} \ No newline at end of file diff --git a/web/app.js b/web/app.js new file mode 100644 index 0000000..3fb49dd --- /dev/null +++ b/web/app.js @@ -0,0 +1 @@ +let sessionId=localStorage.getItem("vapp_session")||crypto.randomUUID();localStorage.setItem("vapp_session",sessionId);const messages=document.querySelector("#messages"),input=document.querySelector("#input");function add(role,text,meta=""){const d=document.createElement("div");d.className="msg "+role;const safe=text.replace(/[&<>]/g,m=>({"&":"&","<":"<",">":">"}[m]));d.innerHTML="
"+safe+"
"+(meta?"
"+meta+"
":"");messages.appendChild(d);messages.scrollTop=messages.scrollHeight}async function send(){const text=input.value.trim();if(!text)return;input.value="";const w=document.querySelector(".welcome");if(w)w.remove();add("user",text);try{const r=await fetch("/api/chat",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({session_id:sessionId,message:text})});const x=await r.json();add("assistant",x.output||x.error||"No output","status: "+x.status+" · "+(x.cache_hit?"CACHE HIT":"EXECUTED")+" · "+(x.verifier||"verifier"))}catch(e){add("assistant","Connection error: "+e.message,"unknown")}}document.querySelector("#send").onclick=send;input.addEventListener("keydown",e=>{if(e.key==="Enter"&&!e.shiftKey){e.preventDefault();send()}});document.querySelectorAll(".chips button").forEach(b=>b.onclick=()=>{input.value=b.textContent;send()});document.querySelector("#new").onclick=()=>{sessionId=crypto.randomUUID();localStorage.setItem("vapp_session",sessionId);location.reload()};document.querySelector("#voice").onclick=()=>{const R=window.SpeechRecognition||window.webkitSpeechRecognition;if(!R){alert("Browser speech recognition is unavailable. Try Chrome or Safari.");return}const r=new R();r.lang="en-US";r.interimResults=false;r.onresult=e=>{input.value=e.results[0][0].transcript;send()};r.start()};fetch("/api/sessions/"+sessionId).then(r=>r.json()).then(x=>(x.messages||[]).forEach(m=>add(m.role,m.content,m.meta||""))).catch(()=>{}); \ No newline at end of file diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..b08a76a --- /dev/null +++ b/web/index.html @@ -0,0 +1 @@ +V-App
Open Agent● local

What are we building?

This demo routes every task through policy → cache → agent → verification.

\ No newline at end of file diff --git a/web/styles.css b/web/styles.css new file mode 100644 index 0000000..056bb9d --- /dev/null +++ b/web/styles.css @@ -0,0 +1 @@ +*{box-sizing:border-box}body{margin:0;background:#0b0d10;color:#f4f5f7;font:15px system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;display:flex;height:100vh}aside{width:260px;background:#101318;border-right:1px solid #252a31;padding:18px;display:flex;flex-direction:column;gap:8px}.brand{font-size:22px;font-weight:800;letter-spacing:.08em}.brand span{color:#8b5cf6}.muted,.sidefoot{color:#8b929d;font-size:12px}.sidefoot{margin-top:auto}button{border:1px solid #303641;background:#171b21;color:#eef0f3;border-radius:9px;padding:9px 12px;cursor:pointer}button:hover{background:#20252d}#new{margin:16px 0}main{flex:1;display:flex;flex-direction:column;min-width:0}header{height:62px;border-bottom:1px solid #252a31;display:flex;align-items:center;justify-content:space-between;padding:0 22px}header span{margin-left:12px;color:#62d394;font-size:12px}.welcome{margin:auto;max-width:720px;text-align:center;color:#c8cdd5}.welcome h1{font-size:38px;color:#fff}.chips{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}.msg{max-width:820px;margin:18px auto;width:calc(100% - 40px);padding:14px 18px;border-radius:14px;line-height:1.6;white-space:pre-wrap}.user{background:#171b21}.assistant{background:#101318;border:1px solid #242a33}.meta{font-size:11px;color:#8b929d;margin-top:8px}.verified{color:#62d394}.unknown{color:#f2c66d}footer{display:flex;gap:10px;padding:18px;border-top:1px solid #252a31;max-width:1000px;width:100%;margin:auto}textarea{flex:1;resize:none;background:#141820;border:1px solid #303641;color:#fff;border-radius:12px;padding:13px;font:inherit;outline:none}#send{width:52px;background:#8b5cf6;border-color:#8b5cf6} \ No newline at end of file