From aefa27e5c11a0d98ac181d8a15c3db9a8a8b7f77 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Tue, 23 Jun 2026 07:09:46 +0800 Subject: [PATCH] {"schema":"decodex/commit/1","summary":"Add local agent loop setup task and recipes","authority":"XY-1076"} --- Makefile.toml | 8 + README.md | 3 + ...2026-06-23-local-agent-loop-drift-audit.md | 100 ++++ docs/evidence/index.md | 2 + docs/index.md | 3 + docs/log.md | 5 + docs/runbook/agent-setup.md | 224 ++++++- docs/runbook/agent_skills_cookbook.md | 56 +- docs/runbook/getting_started.md | 15 +- docs/runbook/index.md | 3 +- scripts/local-agent-loop.sh | 554 ++++++++++++++++++ 11 files changed, 961 insertions(+), 12 deletions(-) create mode 100644 docs/evidence/2026-06-23-local-agent-loop-drift-audit.md create mode 100755 scripts/local-agent-loop.sh diff --git a/Makefile.toml b/Makefile.toml index 91f95951..bccf5b03 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -12,6 +12,7 @@ # | baseline-production-private-addendum | command | | # | baseline-production-synthetic | command | | # | baseline-soak-docker | command | | +# | local-agent-loop | command | | # | openmemory-ui-export-readback | command | | # | parity-docker | command | | # | real-world-first-generation-oss | composite | | @@ -153,6 +154,13 @@ args = [ "soak", ] +[tasks.local-agent-loop] +workspace = false +command = "bash" +args = [ + "scripts/local-agent-loop.sh", +] + [tasks.openmemory-ui-export-readback] workspace = false command = "bash" diff --git a/README.md b/README.md index e6ed9cf7..3bb37719 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,9 @@ ELF is a memory service for LLM agents that stores short, evidence-linked facts Use the canonical setup runbook: - `docs/runbook/getting_started.md` +- For coding/research agents that want a deterministic local first-value loop, use + [docs/runbook/agent-setup.md](docs/runbook/agent-setup.md) or run + `cargo make local-agent-loop`. - For single-user production operation, backup, restore, and Qdrant rebuild, use [docs/runbook/single_user_production.md](docs/runbook/single_user_production.md). diff --git a/docs/evidence/2026-06-23-local-agent-loop-drift-audit.md b/docs/evidence/2026-06-23-local-agent-loop-drift-audit.md new file mode 100644 index 00000000..9def741f --- /dev/null +++ b/docs/evidence/2026-06-23-local-agent-loop-drift-audit.md @@ -0,0 +1,100 @@ +--- +type: Drift Audit +title: "Local Agent Loop Drift Audit" +description: "Drift audit for the one-command local setup and agent integration recipes." +resource: docs/evidence/2026-06-23-local-agent-loop-drift-audit.md +status: active +authority: current_state +owner: docs +last_verified: 2026-06-23 +tags: + - docs + - drift-audit + - agent-setup +source_refs: [] +code_refs: + - Makefile.toml + - scripts/local-agent-loop.sh + - config/local/elf.docker.toml + - apps/elf-api/src/routes.rs + - apps/elf-mcp/src/server.rs +related: + - docs/runbook/agent-setup.md + - docs/runbook/agent_skills_cookbook.md + - docs/runbook/getting_started.md +drift_watch: + - Makefile.toml + - scripts/local-agent-loop.sh + - config/local/elf.docker.toml + - apps/elf-api/src/routes.rs + - apps/elf-mcp/src/server.rs +--- +# Local Agent Loop Drift Audit + +Purpose: Anchor the one-command local setup and agent integration recipe to the +current task, config, HTTP, and MCP surfaces. +Read this when: You need evidence behind `cargo make local-agent-loop`, the local +agent memory+knowledge demo, or the Codex/Claude/Cursor/MCP/CLI recipes. +Not this document: Production deployment, benchmark interpretation, or hosted +provider quality evidence. + +## Watched Claims + +- `cargo make local-agent-loop` is a repository-native entrypoint for the local + agent memory+knowledge loop. +- The deterministic local recipe uses `config/local/elf.docker.toml`, local Docker + Postgres/Qdrant, and local deterministic embedding/rerank providers. +- The local recipe imports source evidence, writes a source-linked memory candidate, + creates and applies a reviewable proposal, recalls approved memory, inspects recall + debug, and exercises correction/rollback. +- The deterministic path does not call `elf_events_ingest`, LLM query expansion, or a + hosted extractor. +- MCP client recipes use the current `elf-mcp` tool surface for agent-facing docs, + note ingest, search, and recall-debug calls. + +## Evidence Anchors + +- `Makefile.toml` defines `local-agent-loop`. +- `scripts/local-agent-loop.sh` owns the runnable local loop and writes artifacts + under `tmp/local-agent-loop/`. +- `config/local/elf.docker.toml` binds local HTTP/admin/MCP services to loopback and + configures local deterministic embedding/rerank providers with query expansion off. +- `apps/elf-api/src/routes.rs` exposes the demo HTTP routes: + - `POST /v2/docs` + - `POST /v2/notes/ingest` + - `POST /v2/searches` + - `POST /v2/recall-debug/panel` + - `POST /v2/admin/consolidation/runs` + - `POST /v2/admin/consolidation/proposals/{proposal_id}/review` + - `POST /v2/admin/notes/{note_id}/corrections` +- `apps/elf-mcp/src/server.rs` exposes the agent-facing MCP tools: + - `elf_docs_put` + - `elf_notes_ingest` + - `elf_searches_create` + - `elf_recall_debug_panel` + +## Reverse Checks + +- Run `bash -n scripts/local-agent-loop.sh` after script changes. +- Run `cargo make check-docs` after docs or task-name changes. +- Run the registered repository gate before handoff. + +## Verdict + +pass + +## Required Updates + +- Update `docs/runbook/agent-setup.md`, this drift audit, and + `docs/runbook/agent_skills_cookbook.md` if the local config, MCP tool names, or + demo route sequence changes. +- Do not convert the deterministic local recipe into a provider-backed quality claim + unless provider credentials, corpus ownership, and scored evidence are supplied. + +## Citations + +- `Makefile.toml` +- `scripts/local-agent-loop.sh` +- `config/local/elf.docker.toml` +- `apps/elf-api/src/routes.rs` +- `apps/elf-mcp/src/server.rs` diff --git a/docs/evidence/index.md b/docs/evidence/index.md index 4cdac5d8..b87855de 100644 --- a/docs/evidence/index.md +++ b/docs/evidence/index.md @@ -20,5 +20,7 @@ Routes to: Drift audits and evidence concepts under `docs/evidence/`. - `2026-06-22-knowledge-watch-rebuild-drift-audit.md`: Drift audit for the changed-source Knowledge Workspace rebuild and reviewable memory-candidate proposal contract. +- `2026-06-23-local-agent-loop-drift-audit.md`: Drift audit for the one-command + local setup and agent integration recipes. - `external_memory_pattern_radar_latest.md`: Latest weekly external memory pattern radar summary. diff --git a/docs/index.md b/docs/index.md index d5850e2b..58168024 100644 --- a/docs/index.md +++ b/docs/index.md @@ -33,6 +33,9 @@ The split below is by question type, not by human-versus-agent audience. `docs/spec/agent_memory_knowledge_system_v1.md` - Need runbooks, migrations, validation steps, troubleshooting, or operational sequences -> `docs/runbook/` +- Need one-command local setup, the minimal memory+knowledge demo loop, or + Codex/Claude/Cursor/MCP/CLI agent integration recipes -> + `docs/runbook/agent-setup.md` - Need the single-user production backup, restore, and Qdrant rebuild path -> `docs/runbook/single_user_production.md` - Need benchmark commands or interpretation steps -> `docs/runbook/benchmarking/` diff --git a/docs/log.md b/docs/log.md index 58190725..48cacaf1 100644 --- a/docs/log.md +++ b/docs/log.md @@ -112,3 +112,8 @@ logs. checked-in snapshot that reruns adversarial, source-library, knowledge, and production-readiness slices while preserving private/provider blockers and keeping P5 queue labels unapplied pending main-thread acceptance. +- Added the one-command local agent loop for XY-1076, plus agent setup recipes for + Codex, Claude/Cursor-style MCP clients, generic MCP clients, and CLI workflows. + The new drift audit anchors the deterministic source import -> proposal approval + -> recall/debug -> correction/rollback route to current HTTP, MCP, config, and + task surfaces. diff --git a/docs/runbook/agent-setup.md b/docs/runbook/agent-setup.md index 13210b1c..b250ef1b 100644 --- a/docs/runbook/agent-setup.md +++ b/docs/runbook/agent-setup.md @@ -6,10 +6,24 @@ resource: docs/runbook/agent-setup.md status: active authority: procedural owner: runbook -last_verified: 2026-06-18 +last_verified: 2026-06-23 tags: - docs - runbook +code_refs: + - Makefile.toml + - scripts/local-agent-loop.sh + - config/local/elf.docker.toml +related: + - docs/runbook/getting_started.md + - docs/runbook/agent_skills_cookbook.md + - docs/evidence/2026-06-23-local-agent-loop-drift-audit.md +drift_watch: + - Makefile.toml + - scripts/local-agent-loop.sh + - config/local/elf.docker.toml + - apps/elf-api/src/routes.rs + - apps/elf-mcp/src/server.rs --- # Agent Setup Runbook @@ -38,6 +52,50 @@ ELF requires: Important: The ELF config has no implicit defaults. All required config fields must be explicitly present in your TOML. +## One-Command Local Agent Loop + +Use this path when the operator wants a deterministic install-to-first-value loop +without external provider credentials: + +```sh +cargo make local-agent-loop +``` + +The command runs `scripts/local-agent-loop.sh`, which: + +- Starts the checked-in Docker Compose Postgres and Qdrant services. +- Builds and starts `elf-api`, `elf-worker`, and `elf-mcp` with + `config/local/elf.docker.toml`. +- Imports a Source Library document through `POST /v2/docs`. +- Writes a deterministic source note through `POST /v2/notes/ingest`. +- Creates a reviewable consolidation proposal through + `POST /v2/admin/consolidation/runs`. +- Applies reviewer approval through + `POST /v2/admin/consolidation/proposals/{proposal_id}/review`. +- Recalls the approved memory through `POST /v2/searches`. +- Inspects the recall/debug panel through `POST /v2/recall-debug/panel`. +- Supersedes and restores the promoted memory through + `POST /v2/admin/notes/{note_id}/corrections`. + +The script writes request and response artifacts to `tmp/local-agent-loop/`. +It stops the background ELF service processes when the demo exits, but it leaves the +Docker dependency containers running for normal local reuse. Stop dependencies with: + +```sh +docker compose -f docker-compose.yml down +``` + +To run only the lifecycle demo against an already running local API, worker, and admin +API: + +```sh +scripts/local-agent-loop.sh demo +``` + +This local loop is deterministic. It uses `elf_notes_ingest` and a manually supplied +review proposal; it does not call `elf_events_ingest`, query expansion, or a hosted +LLM extractor. + ## Minimal Owner Inputs For the checked-in Docker local stack, no owner inputs are required. Use `docker-compose.yml` @@ -78,6 +136,11 @@ For the repository harness scripts: - `jq` or `jaq` - `taplo` +The one-command local agent loop additionally uses: + +- `cargo` +- `docker` + ## Create The Config For the checked-in Docker local stack, use the strict-valid local config directly: @@ -154,6 +217,165 @@ curl -fsS http://127.0.0.1:51892/health Adjust the port to match `service.http_bind`. +## Agent Integration Recipes + +Use the same local config for each agent client unless the operator has created a +provider-backed `elf.toml`. + +### Codex + +Run the local stack first: + +```sh +cargo make local-agent-loop +``` + +For an interactive Codex session, register an MCP server that starts ELF MCP from this +repository checkout: + +```json +{ + "mcpServers": { + "elf-local": { + "command": "cargo", + "args": ["run", "-p", "elf-mcp", "--", "-c", "config/local/elf.docker.toml"], + "cwd": "" + } + } +} +``` + +Use the configured MCP tools for the agent-facing loop: + +- `elf_docs_put` to store long-form source evidence. +- `elf_notes_ingest` to store a compact deterministic memory candidate. +- `elf_searches_create` to recall approved memory. +- `elf_recall_debug_panel` to inspect selected, dropped, stale, blocked, and + not-requested context. + +Review and correction routes are admin HTTP operations in the current local recipe: + +- `POST /v2/admin/consolidation/runs` +- `POST /v2/admin/consolidation/proposals/{proposal_id}/review` +- `POST /v2/admin/notes/{note_id}/corrections` + +### Claude, Cursor, And MCP-Style Coding Agents + +For clients that accept a Claude/Cursor-style MCP server JSON block, use the same +server command: + +```json +{ + "mcpServers": { + "elf-local": { + "command": "cargo", + "args": ["run", "-p", "elf-mcp", "--", "-c", "config/local/elf.docker.toml"], + "cwd": "" + } + } +} +``` + +If the client does not support `cwd`, run it from the repository root or replace the +config argument with the absolute path to the local config. Keep the MCP server +loopback-only for this runbook. + +### Generic MCP Clients + +Start the MCP bridge directly: + +```sh +cargo run -p elf-mcp -- -c config/local/elf.docker.toml +``` + +The local MCP config supplies tenant, project, agent, and read-profile headers: + +- `tenant_id = "local-tenant"` +- `project_id = "local-project"` +- `agent_id = "local-agent"` +- `read_profile = "private_plus_project"` + +Do not let clients override `read_profile` for search, document search, or recall +debug. The MCP adapter strips client-supplied read-profile parameters for those +agent-facing tools. + +### CLI Workflow + +Use HTTP when you need full admin review or correction operations from a shell: + +```sh +export ELF_HTTP=http://127.0.0.1:51892 +export ELF_ADMIN=http://127.0.0.1:51891 +export ELF_TENANT=local-tenant +export ELF_PROJECT=local-project +export ELF_AGENT=local-agent +export ELF_READ_PROFILE=private_plus_project +``` + +All local requests use the same context headers: + +```sh +-H "X-ELF-Tenant-Id: ${ELF_TENANT}" \ +-H "X-ELF-Project-Id: ${ELF_PROJECT}" \ +-H "X-ELF-Agent-Id: ${ELF_AGENT}" \ +-H "X-ELF-Read-Profile: ${ELF_READ_PROFILE}" +``` + +For exact request bodies, inspect the artifacts written by: + +```sh +cargo make local-agent-loop +``` + +## Minimal Memory And Knowledge Loop + +The first-value loop has six checkpoints: + +1. Import source evidence into Doc Extension v1 with `elf_docs_put` or + `POST /v2/docs`. +2. Propose memory as a reviewable, source-linked candidate. The deterministic local + path uses `elf_notes_ingest` plus a manual consolidation proposal; a provider-backed + path may use `elf_events_ingest` after the extractor provider is configured. +3. Approve and apply the proposal through the admin consolidation review route. +4. Recall the approved memory through `elf_searches_create` or `POST /v2/searches`. +5. Inspect recall/debug through `elf_recall_debug_panel` or + `POST /v2/recall-debug/panel`. +6. Correct and rollback through `POST /v2/admin/notes/{note_id}/corrections` with + `action = "supersede"` followed by `action = "restore"`. + +Source records and reviewable proposals are not mutated by recall or correction. The +correction route changes the memory note lifecycle and writes version history so the +memory can be restored. + +## Requirements And Unsupported Paths + +Required for the deterministic local loop: + +- Rust toolchain from `rust-toolchain.toml`. +- Docker Compose services from `docker-compose.yml`: Postgres with `pgvector` and + Qdrant. +- `curl`. +- `jq` or `jaq`. +- Loopback binds from `config/local/elf.docker.toml`. + +Optional provider-backed paths: + +- Replace `config/local/elf.docker.toml` with a complete `elf.toml` when using hosted + or local model providers. +- Configure `[providers.llm_extractor]` before using `elf_events_ingest`. +- Set `search.expansion.mode` to `always` or `dynamic` only after the LLM provider is + configured and cost/latency is acceptable. + +Unsupported by this local-first runbook: + +- Public internet exposure of `elf-api`, `elf-admin`, or `elf-mcp`. +- Hosted managed-memory parity claims. +- Private-corpus or provider-backed quality claims from the checked-in local config. +- Treating local SDK-style exports from other products as OpenMemory UI/export or + hosted-platform evidence. +- Using `add_event` with `config/local/elf.docker.toml`; the extractor block is a + placeholder and the deterministic local loop intentionally avoids it. + ## Run E2E Harness (Optional) The context misranking harness creates and drops a dedicated database and Qdrant collection. It requires: diff --git a/docs/runbook/agent_skills_cookbook.md b/docs/runbook/agent_skills_cookbook.md index de16ef9e..73c41136 100644 --- a/docs/runbook/agent_skills_cookbook.md +++ b/docs/runbook/agent_skills_cookbook.md @@ -6,7 +6,7 @@ resource: docs/runbook/agent_skills_cookbook.md status: active authority: procedural owner: runbook -last_verified: 2026-06-18 +last_verified: 2026-06-23 tags: - docs - runbook @@ -120,7 +120,12 @@ Minimal example: `elf_docs_put` "source_ref": { "schema": "doc_source_ref/v1", "doc_type": "knowledge", - "ts": "2026-02-28T00:00:00Z" + "ts": "2026-02-28T00:00:00Z", + "source_kind": "text_export", + "canonical_uri": "urn:example:decision-record:search-routing", + "captured_at": "2026-02-28T00:00:00Z", + "trust_label": "user_captured", + "notes": "Operator-captured decision record." }, "content": "Long-form English evidence text..." } @@ -157,7 +162,40 @@ Operational guidance: - If the fact is expected to evolve, provide a stable `key` so updates are possible. - If the doc is sensitive, choose `agent_private` scope and only publish explicitly later. -## 4) Workflow: hydrate_context (note hit -> bounded excerpt) +## 4) Workflow: local_memory_authority_loop + +Goal: Exercise the minimal local memory+knowledge authority chain before wiring an +agent into daily work. + +Run the deterministic local loop: + +```sh +cargo make local-agent-loop +``` + +The command writes request/response artifacts under `tmp/local-agent-loop/` and covers +these agent-facing steps: + +1. `elf_docs_put`: import source evidence into Doc Extension v1. +2. `elf_notes_ingest`: store a compact source-linked fact without invoking an LLM. +3. `elf_searches_create`: recall the reviewed memory after approval. +4. `elf_recall_debug_panel`: inspect memory, document, knowledge, graph, and Dreaming + layer states. + +It also uses admin HTTP review and correction routes that are not exposed as ordinary +agent write tools in the current local recipe: + +1. `POST /v2/admin/consolidation/runs`: register a reviewable memory proposal. +2. `POST /v2/admin/consolidation/proposals/{proposal_id}/review`: approve and apply + the proposal. +3. `POST /v2/admin/notes/{note_id}/corrections`: supersede and restore the approved + memory. + +Use this loop as a smoke path for Codex, Claude/Cursor-style MCP clients, generic MCP +clients, and shell workflows. It proves local wiring only; it does not prove hosted +provider quality, private-corpus quality, or `elf_events_ingest` extraction quality. + +## 5) Workflow: hydrate_context (note hit -> bounded excerpt) Goal: Given a retrieved note, hydrate supporting evidence only when needed and only in bounded windows. @@ -225,7 +263,7 @@ Verification guidance: - Prefer `verified=true` excerpts as evidence. - Treat `verified=false` as best-effort context and avoid using it as hard proof without revalidation. -## 5) Workflow: memory_write_policy (when to write and how) +## 6) Workflow: memory_write_policy (when to write and how) Goal: Keep writes minimal, consistent, and update-friendly. @@ -258,7 +296,7 @@ Goal: Keep writes minimal, consistent, and update-friendly. - `project_shared`: shared team memory inside a project. - `org_shared`: shared memory across projects inside a tenant (publish explicitly). -## 6) Workflow: share_workflow (publish + grants) +## 7) Workflow: share_workflow (publish + grants) Goal: Make shared memory explicit and reversible. @@ -304,7 +342,7 @@ Revoke that grant: } ``` -## 7) Workflow: reflect_consolidate (episodic -> stable facts) +## 8) Workflow: reflect_consolidate (episodic -> stable facts) Goal: Periodically reduce memory noise and keep stable truths current. @@ -329,7 +367,7 @@ Minimal example: `elf_notes_list` (pull candidates) } ``` -## 8) Failure modes and safety checklist +## 9) Failure modes and safety checklist - Prompt injection: assume an attacker can influence skill reasoning. Tool-side authz and input gates must still protect you. - Over-writing: do not introduce stable keys unless you are willing to overwrite. @@ -337,7 +375,7 @@ Minimal example: `elf_notes_list` (pull candidates) - Hydration blowups: start at L1; upgrade to L2 only on demand. - Drift: keep workflows centralized and versioned. When tool contracts change, update the cookbook first. -## 9) Prompt templates (agent-side) +## 10) Prompt templates (agent-side) These templates are optional. They are provided to reduce drift across agents. Do not treat them as server contracts. @@ -403,7 +441,7 @@ Given these notes (JSON), produce a plan (English bullets) that includes: Notes: -## 10) Pinned references (internal) +## 11) Pinned references (internal) - Core contract: `docs/spec/system_elf_memory_service_v2.md` - Doc Extension v1 design: `docs/reference/plans/2026-02-24-doc-ext-v1-design.md` diff --git a/docs/runbook/getting_started.md b/docs/runbook/getting_started.md index 06fe67af..052f8fb2 100644 --- a/docs/runbook/getting_started.md +++ b/docs/runbook/getting_started.md @@ -6,7 +6,7 @@ resource: docs/runbook/getting_started.md status: active authority: procedural owner: runbook -last_verified: 2026-06-18 +last_verified: 2026-06-23 tags: - docs - runbook @@ -19,6 +19,19 @@ Inputs: This repository checkout, Docker Compose for local dependencies, and opt Depends on: `Makefile.toml`, `docker-compose.yml`, `config/local/elf.docker.toml`, `elf.example.toml`, and the relevant service binaries. Verification: Configuration is in place and the local ELF stack can start successfully. +## Agent Fast Path + +For a one-command local memory+knowledge loop aimed at coding/research agents, run: + +```sh +cargo make local-agent-loop +``` + +That command starts the local Docker dependencies, runs the ELF services with the +checked-in local config, imports a source, creates and applies a reviewable memory +proposal, recalls it, inspects recall debug, and exercises correction/rollback. See +`docs/runbook/agent-setup.md` for agent integration recipes and unsupported paths. + ## Prerequisites - Docker Compose for the local dependency stack, or separately managed Postgres with `pgvector` and Qdrant. diff --git a/docs/runbook/index.md b/docs/runbook/index.md index dd8dc72a..63b8dd85 100644 --- a/docs/runbook/index.md +++ b/docs/runbook/index.md @@ -12,7 +12,8 @@ Routes to: Runbook concepts under `docs/runbook/`. - `getting_started.md`: canonical setup and local run flow. - `single_user_production.md`: single-user production operation, backup, restore, Qdrant rebuild, rollback, and cleanup. -- `agent-setup.md`: agent-oriented local installation flow. +- `agent-setup.md`: agent-oriented local installation flow, one-command local + memory+knowledge loop, and Codex/Claude/Cursor/MCP/CLI recipes. - `evaluation.md`: retrieval evaluation commands and interpretation flow. - `integration-testing.md`: integration and E2E test workflow. - `testing.md`: test names, scopes, and matching commands. diff --git a/scripts/local-agent-loop.sh b/scripts/local-agent-loop.sh new file mode 100755 index 00000000..71d33dad --- /dev/null +++ b/scripts/local-agent-loop.sh @@ -0,0 +1,554 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +MODE="${1:-run}" + +CONFIG_PATH="${ELF_LOCAL_AGENT_CONFIG:-config/local/elf.docker.toml}" +HTTP_BASE="${ELF_LOCAL_AGENT_HTTP:-http://127.0.0.1:51892}" +ADMIN_BASE="${ELF_LOCAL_AGENT_ADMIN:-http://127.0.0.1:51891}" +TENANT_ID="${ELF_LOCAL_AGENT_TENANT:-local-tenant}" +PROJECT_ID="${ELF_LOCAL_AGENT_PROJECT:-local-project}" +AGENT_ID="${ELF_LOCAL_AGENT_AGENT:-local-agent}" +READ_PROFILE="${ELF_LOCAL_AGENT_READ_PROFILE:-private_plus_project}" +OUT_DIR="${ELF_LOCAL_AGENT_OUT_DIR:-tmp/local-agent-loop}" + +SERVICE_PIDS=() +JSON_BIN="" + +usage() { + cat <<'USAGE' +Usage: + scripts/local-agent-loop.sh [run|demo] + +Modes: + run Start local Docker dependencies, build/start ELF services, and run the demo. + demo Run the demo against an already running local ELF API, worker, and admin API. + +Environment overrides: + ELF_LOCAL_AGENT_CONFIG Config path. Default: config/local/elf.docker.toml + ELF_LOCAL_AGENT_HTTP Public HTTP base URL. Default: http://127.0.0.1:51892 + ELF_LOCAL_AGENT_ADMIN Admin HTTP base URL. Default: http://127.0.0.1:51891 + ELF_LOCAL_AGENT_TENANT Tenant header. Default: local-tenant + ELF_LOCAL_AGENT_PROJECT Project header. Default: local-project + ELF_LOCAL_AGENT_AGENT Agent header. Default: local-agent + ELF_LOCAL_AGENT_READ_PROFILE Read profile header. Default: private_plus_project + ELF_LOCAL_AGENT_OUT_DIR Artifact directory. Default: tmp/local-agent-loop +USAGE +} + +log() { + printf '[local-agent-loop] %s\n' "$*" +} + +die() { + printf '[local-agent-loop] ERROR: %s\n' "$*" >&2 + exit 1 +} + +require_bin() { + command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" +} + +detect_json_tool() { + if command -v jq >/dev/null 2>&1; then + JSON_BIN="jq" + elif command -v jaq >/dev/null 2>&1; then + JSON_BIN="jaq" + else + die "Missing required command: jq or jaq" + fi +} + +json_get() { + "${JSON_BIN}" -r "$1" "$2" +} + +json_passes() { + "${JSON_BIN}" -e "$1" "$2" >/dev/null +} + +curl_elf() { + local method="$1" + local url="$2" + local body="${3:-}" + local args=( + -fsS + -X "$method" + "$url" + -H 'content-type: application/json' + -H "X-ELF-Tenant-Id: ${TENANT_ID}" + -H "X-ELF-Project-Id: ${PROJECT_ID}" + -H "X-ELF-Agent-Id: ${AGENT_ID}" + -H "X-ELF-Read-Profile: ${READ_PROFILE}" + ) + + if [[ -n "$body" ]]; then + args+=(--data-binary "@${body}") + fi + + curl "${args[@]}" +} + +cleanup_services() { + if ((${#SERVICE_PIDS[@]} == 0)); then + return + fi + + log "Stopping background ELF services." + for pid in "${SERVICE_PIDS[@]}"; do + if kill -0 "$pid" >/dev/null 2>&1; then + kill "$pid" >/dev/null 2>&1 || true + fi + done +} + +start_dependencies() { + require_bin docker + log "Starting Postgres and Qdrant through docker compose." + docker compose -f docker-compose.yml config >/dev/null + docker compose -f docker-compose.yml up -d postgres qdrant +} + +build_services() { + require_bin cargo + log "Building elf-api, elf-worker, and elf-mcp." + cargo build -p elf-api -p elf-worker -p elf-mcp +} + +target_dir() { + cargo metadata --format-version 1 --no-deps | "${JSON_BIN}" -r '.target_directory' +} + +start_services() { + local target + target="$(target_dir)" + mkdir -p "$OUT_DIR/logs" + + log "Starting elf-api, elf-worker, and elf-mcp with ${CONFIG_PATH}." + "${target}/debug/elf-api" -c "$CONFIG_PATH" >"${OUT_DIR}/logs/elf-api.log" 2>&1 & + SERVICE_PIDS+=("$!") + "${target}/debug/elf-worker" -c "$CONFIG_PATH" >"${OUT_DIR}/logs/elf-worker.log" 2>&1 & + SERVICE_PIDS+=("$!") + "${target}/debug/elf-mcp" -c "$CONFIG_PATH" >"${OUT_DIR}/logs/elf-mcp.log" 2>&1 & + SERVICE_PIDS+=("$!") +} + +assert_services_alive() { + local pid + for pid in "${SERVICE_PIDS[@]}"; do + if ! kill -0 "$pid" >/dev/null 2>&1; then + tail -n 80 "${OUT_DIR}"/logs/*.log >&2 || true + die "A background ELF service exited before the demo completed." + fi + done +} + +wait_for_health() { + log "Waiting for ${HTTP_BASE}/health." + for _ in $(seq 1 120); do + if curl -fsS "${HTTP_BASE}/health" >"${OUT_DIR}/00-health.json" 2>/dev/null; then + log "API health check passed." + return + fi + assert_services_alive + sleep 1 + done + + tail -n 80 "${OUT_DIR}"/logs/*.log >&2 || true + die "Timed out waiting for API health." +} + +write_demo_inputs() { + local demo_id="$1" + local source_note_id="${2:-}" + local doc_id="${3:-}" + local promoted_text="Fact: ELF local agent setup supports a source-linked memory review loop." + + cat >"${OUT_DIR}/01-docs-put.request.json" <"${OUT_DIR}/02-notes-ingest.request.json" <"${OUT_DIR}/03-consolidation-run.request.json" <"${OUT_DIR}/04-proposals-list.json" 2>/dev/null && + json_passes '.proposals | length > 0' "${OUT_DIR}/04-proposals-list.json"; then + json_get '.proposals[0].proposal_id' "${OUT_DIR}/04-proposals-list.json" + return + fi + assert_services_alive + sleep 1 + done + + die "Timed out waiting for consolidation proposal materialization." +} + +wait_for_search_hit() { + local note_id="$1" + + cat >"${OUT_DIR}/07-search.request.json" <"${OUT_DIR}/07-search.response.json" 2>/dev/null && + json_get '.items[]?.note_id' "${OUT_DIR}/07-search.response.json" | grep -qx "$note_id"; then + json_get '.trace_id' "${OUT_DIR}/07-search.response.json" + return + fi + assert_services_alive + sleep 1 + done + + die "Timed out waiting for promoted memory to appear in search." +} + +run_demo() { + local demo_id doc_id source_note_id run_id proposal_id promoted_note_id trace_id supersede_version + + demo_id="$(date -u '+%Y%m%d%H%M%S')" + mkdir -p "$OUT_DIR" + rm -f "${OUT_DIR}"/*.json + + write_demo_inputs "$demo_id" + + log "Importing a Source Library document." + curl_elf POST "${HTTP_BASE}/v2/docs" "${OUT_DIR}/01-docs-put.request.json" \ + >"${OUT_DIR}/01-docs-put.response.json" + doc_id="$(json_get '.doc_id' "${OUT_DIR}/01-docs-put.response.json")" + + write_demo_inputs "$demo_id" "" "$doc_id" + + log "Writing a deterministic source note linked to the imported document." + curl_elf POST "${HTTP_BASE}/v2/notes/ingest" "${OUT_DIR}/02-notes-ingest.request.json" \ + >"${OUT_DIR}/02-notes-ingest.response.json" + source_note_id="$(json_get '.results[0].note_id' "${OUT_DIR}/02-notes-ingest.response.json")" + + write_demo_inputs "$demo_id" "$source_note_id" "$doc_id" + + log "Creating a reviewable memory proposal." + curl_elf POST "${ADMIN_BASE}/v2/admin/consolidation/runs" \ + "${OUT_DIR}/03-consolidation-run.request.json" \ + >"${OUT_DIR}/03-consolidation-run.response.json" + run_id="$(json_get '.run.run_id' "${OUT_DIR}/03-consolidation-run.response.json")" + proposal_id="$(wait_for_proposal "$run_id")" + + cat >"${OUT_DIR}/05-proposal-review.request.json" <"${OUT_DIR}/05-proposal-review.response.json" + promoted_note_id="$(json_get '.target_ref.id' "${OUT_DIR}/05-proposal-review.response.json")" + + log "Searching through the agent-facing recall API." + trace_id="$(wait_for_search_hit "$promoted_note_id")" + + cat >"${OUT_DIR}/08-recall-debug.request.json" <"${OUT_DIR}/08-recall-debug.response.json" + + cat >"${OUT_DIR}/09-correction-supersede.request.json" <"${OUT_DIR}/09-correction-supersede.response.json" + supersede_version="$(json_get '.version_id' "${OUT_DIR}/09-correction-supersede.response.json")" + + cat >"${OUT_DIR}/10-correction-restore.request.json" <"${OUT_DIR}/10-correction-restore.response.json" + + log "Demo complete." + cat <&2 + die "Unknown mode: ${MODE}" + ;; + esac + + cd "$ROOT_DIR" + detect_json_tool + require_bin curl + + if [[ "$MODE" == "run" ]]; then + trap cleanup_services EXIT + mkdir -p "$OUT_DIR" + start_dependencies + build_services + start_services + wait_for_health + fi + + run_demo +} + +main "$@"