# Project export: Baba Babooshka

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: Your agent for everything!
- Devpost: https://devpost.com/software/baba-babooshka
- GitHub: https://github.com/slowloris-98/baba_babooshka
- Video: https://www.youtube.com/embed/paZyqcuqrOY?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Udayan Atreya (6 commits), Claude Sonnet 4.6 (1 commits)

## Devpost submission (written by the team)

### Inspiration

Babushka means two things: the grandmother who never forgets and the nesting doll with a world tucked inside. Both nail the problem we kept hitting — coding assistants forget you between sessions. We wanted an agent you could text like a teammate, one that remembers your projects and gets sharper the more you talk to it.

### What it does

Baba Babooshka is a Fetch.ai uAgent that can trigger Claude Code as a network-addressable AI workers for your discussions and project work. Reach her as a human (ASI:One / Agentverse chat) or as another agent (ClaudeRequest → ClaudeResponse). Long-term memory — searches a Redis Agent Memory Server for prior context per sender, prepends it to the prompt, and persists each exchange. Conversations compound. Sentry observability — every timeout, crash, and memory hiccup is captured so the agent never dies silently.

### How we built it

Four components, no more: a Fetch.ai uAgent orchestrator, Claude Code as the engine, a Redis Agent Memory Server, and Sentry. The core spawns Claude headlessly as a non-blocking asyncio subprocess and parses its JSON result — and never raises, returning a readable error on any failure so the agent stays up.

### Challenges we ran into

The Windows claude .cmd/.exe shim wouldn't resolve under asyncio — fixed with a cross-platform _resolve_claude(). Port collision: the memory server and agent both wanted :8000 (moved memory to :8001). Killing runaway Claude subprocesses cleanly on timeout without leaking processes. Holding scope discipline to four components under a 1.5-day clock.

### Accomplishments we're proud of

A genuinely stateful agent — conversations carry across sessions instead of starting cold every time. A core that never crashes: every failure path returns a readable error, so the agent stays alive in the wild. A clean, minimal architecture — four components doing real work, nothing bolted on for show.

### What we learned

Memory is a product feature, not a database — retrieving the right top-$k$ context matters more than storing it. "Never raise" is a design philosophy — an agent that returns an honest error beats one that crashes. Observability earns its keep instantly — Sentry turned "why did it hang?" into annotated events.

### What's next

Working memory per project, so she tracks the state of each repo separately, not just per sender. Poke / iMessage as a human-facing layer so you can text her about project progress and errors directly. Multi-session orchestration — planning, scaffolding, and coding sessions running in parallel. Smarter memory — summarization and decay so context stays sharp instead of piling up.

## README (from the GitHub repository)

# Baba Babooshka

<p align="center">
  <img src="data/icon.png" alt="Baba Babooshka" width="320">
</p>

A [Fetch.ai uAgent](https://uagents.fetch.ai/) for all your discussions and project needs. Like the *babushka* — the wise grandmother who never forgets and the nesting doll with a whole world tucked inside — Baba Babooshka remembers every conversation and keeps the layers that matter close at hand.

Powered by **Claude Code**, she carries her own long-term **agent memory** so each chat picks up where the last one left off, and **Sentry** keeps watch over every session — so the conversations get sharper the more you talk, and nothing slips through the cracks.

## What it does

The agent accepts prompts from two sources:

| Source | Protocol | Use case |
|--------|----------|----------|
| ASI:One / Agentverse chat | Standard `chat_protocol` | Human users chatting via the Fetch.ai ecosystem |
| Another uAgent | `ClaudeRequest` → `ClaudeResponse` | Programmatic agent-to-agent calls |

Each request is enriched with **long-term memory**: before spawning Claude, the agent searches a [Redis Agent Memory Server](https://github.com/redis/agent-memory-server) for prior context from the same sender and prepends it to the prompt; after the run it persists the prompt + result back to memory. Memory is keyed per sender and can be disabled with `MEMORY_ENABLED=false`.

> **Safety:** the spawned Claude runs with `--dangerously-skip-permissions`, so it can read/edit files and run commands inside `CLAUDE_WORKDIR` autonomously. Point `CLAUDE_WORKDIR` at a dedicated/sandboxed folder, not your whole machine.

## Prerequisites

- Python 3.10+
- [Claude Code CLI](https://claude.ai/code) installed and authenticated (`claude --version` should work)
- `uagents >= 0.22.0` (see [requirements.txt](requirements.txt))
- A running [Redis Agent Memory Server](https://github.com/redis/agent-memory-server) (optional — set `MEMORY_ENABLED=false` to skip)

## Setup

```bash
python -m venv .venv
.venv\Scripts\activate          # Windows
# source .venv/bin/activate     # macOS / Linux
pip install -r requirements.txt
```

### Memory server

The agent stores long-term memory in a [Redis Agent Memory Server](https://github.com/redis/agent-memory-server). Run it in a separate terminal before starting the agent:

```bash
docker compose up api redis
```

The memory server's API defaults to port `8000`, which collides with `AGENT_PORT`. Run it on `8001` and point `MEMORY_SERVER_URL` at it (see the [Configuration](#configuration) table). To run the agent without memory, set `MEMORY_ENABLED=false`.

## Configuration

All settings are controlled by environment variables:

| Variable | Default | Description |
|----------|---------|-------------|
| `AGENT_SEED` | `baba-babooshka-seed-change-me` | Deterministic seed for the agent's identity. **Change this before deploying.** |
| `AGENT_NAME` | `baba_babooshka` | Human-readable name shown on Agentverse |
| `AGENT_PORT` | `8000` | Local HTTP port |
| `AGENTVERSE_API_KEY` | _(unset)_ | API key for the Agentverse mailbox (get one at [agentverse.ai](https://agentverse.ai)) |
| `CLAUDE_WORKDIR` | current working directory | Directory Claude Code runs in. All file edits happen here. |
| `CLAUDE_MODEL` | `claude-opus-4-8` | Model passed to `claude --model`. Empty string = Claude's default. |
| `CLAUDE_TIMEOUT` | `600` | Seconds before a spawned Claude run is killed |
| `CLAUDE_MAX_TURNS` | `40` | Maximum agentic turns per Claude run |
| `MEMORY_ENABLED` | `true` | Set to `false` to disable long-term memory entirely |
| `MEMORY_SERVER_URL` | `http://localhost:8001` | Base URL of the Redis Agent Memory Server |

## Running

```bash
export AGENT_SEED="my-unique-secret-seed"
export AGENTVERSE_API_KEY="<your key>"
export CLAUDE_WORKDIR="/path/to/workspace"

python agent.py
```

On startup the agent logs its address:

```
INFO: Agent 'baba_babooshka' address: agent1q...
INFO: Claude binary: /usr/local/bin/claude
INFO: Claude workdir: /path/to/workspace
```

Copy that address to chat with it from ASI:One / Agentverse, or to message it from another agent.

## Agent-to-agent usage

```python
from uagents import Agent, Context, Model

class ClaudeRequest(Model):
    prompt: str

class ClaudeResponse(Model):
    result: str

WORKER = "agent1q..."  # address printed on worker startup

@agent.on_event("startup")
async def ask(ctx: Context):
    await ctx.send(WORKER, ClaudeRequest(prompt="List files in the workspace."))

@agent.on_message(model=ClaudeResponse)
async def got(ctx: Context, sender: str, msg: ClaudeResponse):
    print(msg.result)
```

## Running the test suite

[test_local.py](test_local.py) runs a full end-to-end test in a single process — it spins up the real worker alongside a throwaway client in a uAgents Bureau, sends a prompt, and checks the reply:

```bash
python test_local.py
```

Expected output on success:

```
TEST RESULT: worker replied -> 'PONG'
TEST PASSED
```

Times out after 90 seconds and exits with code 1 on failure.

## How it works

[`run_claude_code()`](agent.py) in [agent.py](agent.py) launches:

```
claude -p "<prompt>" --output-format json \
  --dangerously-skip-permissions --max-turns 40 --model claude-opus-4-8
```

via a non-blocking `asyncio` subprocess, parses the JSON `result` field, and returns the text. On any failure (missing binary, non-zero exit, timeout) it returns a human-readable error string so the agent stays alive.

When `MEMORY_ENABLED` is set, each incoming request is wrapped by two helpers in [agent.py](agent.py):

- [`get_memory_context()`](agent.py) searches long-term memory for the sender's prior interactions and prepends the top matches to the prompt.
- [`save_interaction()`](agent.py) persists a summary of the prompt + result as a semantic memory after the run.

Both fail soft — any memory-server error is reported to Sentry and the run continues without memory.

## Project layout

```
agent.py        — the worker agent
test_local.py   — local end-to-end test
requirements.txt
```

## License

MIT


## Detected evidence (automated analysis)

Indexed codebase: 4 recognized source files, 28 KB.
- Python (language) — detected in the code
- Docker (technology) — claimed on Devpost, not found in the code
- LangChain (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (7 of 7)

```
.env.example
.gitignore
agent.py
baba-babooshka-build-plan.md
README.md
requirements.txt
test_local.py
```

### Dependencies

- requirements.txt: agent-memory-client@>=0.4.0, python-dotenv@>=1.0.0, sentry-sdk@>=2.0.0, uagents@>=0.22.0

### Recent commits (newest first)

- update: all the docs with latest changes
- bug fixed: change workspace
- update: icon, docs, name
- Rename project to baba_babooshka
- Merge branch 'main' of https://github.com/slowloris-98/baba_babooshka
- initial commit
- Initial commit: agent test

## Key source files (fetched from GitHub, selected and truncated for size)

### baba-babooshka-build-plan.md

```markdown
# Baba Babooshka — build plan

**Context:** AI Hackathon @ UC Berkeley, June 20–21, 2026. Time budget: ~1.5 days. This plan is scoped to ship a working demo, not a finished product. Follow the phases in order — do not start Phase 2 work before Phase 1's acceptance criteria pass.

**Architecture is locked.** Four components only. Do not introduce a different orchestration framework, a different memory store, a different error tracker, or a different chat surface. If something in this plan seems to require a fifth component, stop and flag it instead of adding one.

---

## 1. Components and responsibilities

| Component | Role | Exposed via | Docs |
|---|---|---|---|
| **uAgent** (Fetch.ai `uagents` framework) | Main orchestrator. Starts and runs work sessions: Claude Code sessions, planning sessions, project-scaffolding sessions. | Must expose an MCP server interface so Poke can trigger it. | [Getting started](https://uagents.fetch.ai/docs/getting-started/create) |
| **Redis Agent Memory Server** (`redis/agent-memory-server`) | Memory layer. Stores working memory (per-session) and long-term memory (persistent, searchable) for every task/project the agent works on. | Ships its own MCP server (`agent-memory mcp`) — use as-is, do not reimplement. | [GitHub repo](https://github.com/redis/agent-memory-server) |
| **Sentry** | Error tracking for the uAgent's processes. | Sentry's hosted MCP server — use as-is, do not reimplement. | [Education plan](https://sentry.io/for/education) · [Quickstarts](https://docs.sentry.io) |
| **Poke** | Human-facing communication layer (text/iMessage). Connects to the above three as MCP integrations and lets the user ask about project progress (via the memory server MCP) and errors (via the Sentry MCP). | N/A — Poke is the MCP *client*, not something we build. | [Managing integrations](https://poke.com/docs/managing-integrations) · [Custom MCP servers](https://interaction.co/mcp) |

**Data flow (do not deviate from this shape):**

```
User (iMessage/SMS)
   ↕
Poke  ──MCP──>  Redis memory server   (read: project/task status)
   ├──MCP──>  Sentry                (read: error data)
   └──MCP──>  uAgent                (trigger: start a session)

uAgent
   ├──spawns──>  Code session / Planning session / Project session
   ├──writes──>  Redis memory server  (session state, progress, results)
   └──reports──>  Sentry              (errors raised during a session)
```

Poke never talks to the uAgent's internals directly — only through whatever MCP tools the uAgent exposes. The uAgent never talks to the user directly — only through what it writes to Redis memory / Sentry, which Poke then surfaces.

---

## 2. Repo structure

```
baba-babooshka/
├── docker-compose.yml          # redis + agent-memory api + agent-memory mcp
├── .env.example
├── uagent/
│   ├── orchestrator.py         # uAgent definition, message handlers
│   ├── mcp_server.py           # exposes uAgent as an MCP server for Poke
│   ├── sessions/
│   │   ├── code_sessio
[truncated — 5945 more characters]
```

### requirements.txt

```
uagents>=0.22.0
sentry-sdk>=2.0.0
python-dotenv>=1.0.0
agent-memory-client>=0.4.0

```

### test_local.py

```python
"""Local end-to-end test of agent.py's real handlers via a uAgents Bureau.

Runs the actual worker `agent` from agent.py plus a throwaway client in one
process. The client sends a ClaudeRequest; the worker spawns Claude Code and
replies with a ClaudeResponse. Exits 0 on success, 1 on timeout/failure.
"""

import asyncio
import importlib.util
import os
import sys

# Keep the spawned Claude run short for the test.
os.environ.setdefault("CLAUDE_MAX_TURNS", "3")
os.environ.setdefault("AGENT_SEED", "test-worker-seed")
os.environ.setdefault(
    "CLAUDE_WORKDIR", os.path.join(os.path.dirname(__file__), "workspace")
)
os.makedirs(os.environ["CLAUDE_WORKDIR"], exist_ok=True)

from uagents import Agent, Bureau, Context  # noqa: E402

# Import the real agent module (registers the worker's handlers).
spec = importlib.util.spec_from_file_location("agent", "agent.py")
agent_mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(agent_mod)

worker = agent_mod.agent
ClaudeRequest = agent_mod.ClaudeRequest
ClaudeResponse = agent_mod.ClaudeResponse

client = Agent(name="test_client", seed="test-client-seed")

PROMPT = "Reply with exactly the word PONG and nothing else."
got_reply = {"ok": False}


@client.on_event("startup")
async def send_request(ctx: Context):
    ctx.logger.info(f"Sending ClaudeRequest to worker {worker.address}")
    await ctx.send(worker.address, ClaudeRequest(prompt=PROMPT))


@client.on_message(model=ClaudeResponse)
async def on_response(ctx: Context, sender: str, msg: ClaudeResponse):
    ctx.logger.info(f"Got ClaudeResponse: {msg.result!r}")
    got_reply["ok"] = True
    print(f"\nTEST RESULT: worker replied -> {msg.result!r}")
    print("TEST PASSED" if "PONG" in msg.result else "TEST FAILED (unexpected text)")
    os._exit(0 if "PONG" in msg.result else 1)


async def watchdog():
    await asyncio.sleep(90)
    if not got_reply["ok"]:
        print("\nTEST FAILED: timed out waiting for ClaudeResponse")
        os._exit(1)


bureau = Bureau(port=8100, endpoint=["http://127.0.0.1:8100/submit"])
bureau.add(worker)
bureau.add(client)

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.create_task(watchdog())
    bureau.run()

```

### agent.py

```python
"""A Fetch.ai uAgent that spawns Claude Code (the `claude` CLI) on a user's request.

When the agent receives a message (via the standard chat protocol from ASI:One /
Agentverse, or via a direct ClaudeRequest from another agent), it runs Claude Code
in headless mode (`claude -p ...`) inside CLAUDE_WORKDIR and returns the result.

See: https://uagents.fetch.ai/docs/getting-started/create
"""

import asyncio
import json
import os
import shutil
from datetime import datetime, timezone
from uuid import uuid4

from dotenv import load_dotenv

load_dotenv()

import sentry_sdk
from agent_memory_client import MemoryAPIClient, MemoryClientConfig
from agent_memory_client.filters import UserId
from agent_memory_client.models import ClientMemoryRecord

from uagents import Agent, Context, Model, Protocol
from uagents_core.contrib.protocols.chat import (
    ChatAcknowledgement,
    ChatMessage,
    EndSessionContent,
    StartSessionContent,
    TextContent,
    chat_protocol_spec,
)

# ---------------------------------------------------------------------------
# Configuration (all overridable via environment / .env)
# ---------------------------------------------------------------------------
AGENT_SEED = os.environ.get("AGENT_SEED", "baba-babooshka-seed-change-me")
AGENT_NAME = os.environ.get("AGENT_NAME", "Baba Babooshka")
AGENT_PORT = int(os.environ.get("AGENT_PORT", "8000"))

# Directory Claude Code runs in. Defaults to the current working directory.
CLAUDE_WORKDIR = os.environ.get("CLAUDE_WORKDIR", os.getcwd())
# Model passed to `claude --model`. Empty string -> let claude use its default.
CLAUDE_MODEL = os.environ.get("CLAUDE_MODEL", "claude-opus-4-8")
# Hard cap on how long a single spawned Claude run may take (seconds).
CLAUDE_TIMEOUT = int(os.environ.get("CLAUDE_TIMEOUT", "600"))
# Bound on autonomous agentic turns for a single run.
CLAUDE_MAX_TURNS = int(os.environ.get("CLAUDE_MAX_TURNS", "40"))

MEMORY_SERVER_URL = os.environ.get("MEMORY_SERVER_URL", "http://localhost:8001")
MEMORY_ENABLED = os.environ.get("MEMORY_ENABLED", "true").lower() == "true"
_memory_client: MemoryAPIClient | None = None


def _resolve_claude() -> str | None:
    """Locate the claude executable, handling the Windows .cmd/.exe shims."""
    for candidate in ("claude", "claude.cmd", "claude.exe"):
        path = shutil.which(candidate)
        if path:
            return path
    return None


CLAUDE_BIN = _resolve_claude()

# ---------------------------------------------------------------------------
# Sentry — initialize before any agent/framework code
# ---------------------------------------------------------------------------
_sentry_dsn = os.environ.get("SENTRY_DSN", "")
if _sentry_dsn:
    sentry_sdk.init(
        dsn=_sentry_dsn,
        environment=os.environ.get("SENTRY_ENVIRONMENT", "production"),
        traces_sample_rate=1.0,
        enable_logs=True,
    )


async def run_claude_code(prompt: str, logger=None) -> str:
    """Spawn Claude Code headlessly to handle `prompt`; return its text result.

    Never raises: on any failure it returns a human-readable error string so the
    agent stays alive and the caller gets useful feedback.
    """
    if CLAUDE_BIN is None:
        return (
            "Error: the `claude` CLI was not found on PATH. Install Claude Code "
            "and make sure `claude` is runnable, then restart the agent."
        )

    args = [
        CLAUDE_BIN,
        "-p",
        prompt,
        "--output-format",
        "json",
        "--max-turns",
        str(CLAUDE_MAX_TURNS),
    ]
    if CLAUDE_MODEL:
        args += ["--model", CLAUDE_MODEL]

    if logger:
        logger.info(f"Spawning Claude Code in {CLAUDE_WORKDIR}: {prompt[:120]!r}")

    try:
        proc = await asyncio.create_subprocess_exec(
            *args,
            cwd=CLAUDE_WORKDIR,
            env=os.environ.copy(),
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
    except OSError as exc:
        sentry_sdk.capture_exception(exc)
        return f"Error: failed to start Claude Code: {exc}"

    try:
        stdout_b, stderr_b = await asyncio.wait_for(
            proc.communicate(), timeout=CLAUDE_TIMEOUT
        )
    except asyncio.TimeoutError:
        proc.kill()
        await proc.communicate()
        with sentry_sdk.new_scope() as scope:
            scope.set_extra("prompt_preview", prompt[:300])
            scope.set_extra("timeout_seconds", CLAUDE_TIMEOUT)
            scope.set_extra("workdir", CLAUDE_WORKDIR)
            sentry_sdk.capture_message(
                f"Claude Code timed out after {CLAUDE_TIMEOUT}s",
                level="error",
            )
        return (
            f"Error: Claude Code timed out after {CLAUDE_TIMEOUT}s and was stopped. "
            "Try a smaller task or raise CLAUDE_TIMEOUT."
        )

    stdout = stdout_b.decode("utf-8", errors="replace").strip()
    stderr = stderr_b.decode("utf-8", errors="replace").strip()

    if proc.returncode != 0:
        detail = stderr or stdout or "(no output)"
        with sentry_sdk.new_scope() as scope:
            scope.set_extra("exit_code", proc.returncode)
            scope.set_extra("stderr", stderr[:2000])
            scope.set_extra("stdout", stdout[:2000])
            scope.set_extra("prompt_preview", prompt[:300])
            scope.set_extra("workdir", CLAUDE_WORKDIR)
            sentry_sdk.capture_message(
                f"Claude Code exited with code {proc.returncode}",
                level="error",
            )
        return f"Error: Claude Code exited with code {proc.returncode}.\n{detail}"

    # `--output-format json` prints a single JSON object with a `result` field.
    try:
        payload = json.loads(stdout)
        if isinstance(payload, dict) and "result" in payload:
            return str(payload["result"]).strip() or "(Claude returned an empty result.)"
    except (json.JSONDecodeError, ValueError):
        pass

    # Fall back to raw output if
[truncated — 4882 more characters]
```