feat: optional Dakera project memory — recall decisions across sessions#1187
feat: optional Dakera project memory — recall decisions across sessions#1187ferhimedamine wants to merge 2 commits into
Conversation
Pythagora builds apps across many separate conversations. Each new conversation re-loads project context from the spec and file tree, but the reasoning from earlier sessions (architectural decisions, debugging resolutions, conventions) is lost and re-derived from scratch every time. This adds an opt-in semantic memory backed by a self-hosted Dakera server: - On task breakdown, recall the decisions/bug-fixes most relevant to the task and offer them to the Developer agent as extra context. - On task completion, store a short summary of the outcome, including problem -> solution pairs from the debugging iterations. The feature is fully opt-in via a new optional 'memory' config section; when it is omitted, behaviour is unchanged (the injected prompt partial renders nothing). Every call to the memory server is best-effort and degrades to 'no memory' on any error, mirroring how ExternalDocumentation degrades when the docs API is down. No new dependency is added -- the client reuses the existing httpx dependency. Adds unit tests (tests/memory/test_dakera.py) and docs/MEMORY.md.
|
I have read the CLA Document and I hereby sign the CLA |
|
Thanks for the detailed follow-up — this is a strong implementation shape. The parts I like most are:
One thing I’d still watch closely is the boundary between “candidate memory” and “durable project truth.” Even if the recall prompt tells the agent to ignore outdated items, the memory object itself may still benefit from explicit lifecycle metadata, for example: That would make stale-memory handling less dependent on prompt behavior and more visible to the runtime/UI later. The current PR looks like a good v1 because it is opt-in and non-blocking. My main suggestion would be to keep the door open for memory lifecycle and supersession metadata, especially once users start relying on recalled project decisions across many sessions. |
Addresses review feedback (@hegu-1 on Pythagora-io#1187): make stale-memory handling less dependent on prompt behaviour by tagging each stored outcome with lightweight lifecycle metadata that is visible to the runtime/UI. - ProjectMemory.store() now accepts an optional metadata dict, persisted verbatim on the memory (the Dakera store API already supports it). - TaskCompleter classifies each outcome: bug_fix when the task needed debugging iterations, otherwise decision, and writes metadata {kind, status: candidate, source}. Outcomes are stored as candidates, never as accepted project truth, keeping the door open for explicit supersession later without touching recall. Adds tests for metadata passthrough (present/absent) and the bug_fix/decision classification, and documents the lifecycle in docs/MEMORY.md.
|
Thanks @hegu-1 — good call on the candidate-vs-durable boundary. I've implemented the lightweight version of your lifecycle metadata in
So stale handling no longer relies on prompt behaviour alone — the lifecycle is now on the memory object itself. Still opt-in, non-blocking, no new dependency; added tests for the metadata passthrough and the classification, and documented it in |
Problem
Pythagora builds applications iteratively across many separate conversations. Each
new conversation re-loads project context from the spec file and the file tree, but the
reasoning from earlier sessions is lost: architectural decisions, debugging
resolutions and conventions get re-derived from scratch every time. A concrete example
from the linked issue: "we hit
a Pydantic validation error on the user schema — resolved by adding
Optionaltypes" issolved once, then forgotten, and the same class of bug is re-encountered later.
Design
This PR adds an opt-in semantic memory layer backed by a self-hosted
Dakera server, wired into the two natural points in the agent loop:
Developer.breakdown_current_task— recalls the decisions/bug-fixes most relevantto the current task (
POST /v1/memory/recall) and offers them to the LLM as extracontext via a new prompt partial. Mirrors how
docsare injected today.TaskCompleter— on task completion, stores a compact outcome summary(
POST /v1/memory/store), including theproblem -> solutionpairs from the debuggingiterations, which are the most valuable thing to carry forward.
Memories are namespaced per project (
agent_id = "gptpilot-<project_id>"), so recallonly ever surfaces knowledge from the same project.
Design choices
memorysection isadded to the config (
core.config.DakeraConfig). When it is omitted (the default),ProjectMemory.for_project()returnsNone, both hooks become no-ops, and theinjected prompt partial renders an empty string — the breakdown prompt is
byte-identical to today.
httpxdependency — no SDK is added to
pyproject.toml/requirements.txt.httpx.HTTPErroranddegrades to "no memory" with a warning, exactly the way
ExternalDocumentationdegrades when the docs API is unreachable. A slow or downmemory server can never fail or stall a build.
Files
core/config/__init__.pyDakeraConfig; optionalConfig.memoryfieldcore/memory/__init__.pyProjectMemoryasync client (recall/store)core/agents/developer.pycore/agents/task_completer.pycore/prompts/partials/dakera_memory.promptcore/prompts/developer/breakdown.promptexample-config.jsonmemorysectiondocs/MEMORY.mdtests/memory/test_dakera.pyUsage
Run a server (compose from
dakera-ai/dakera-deploy, listens on port3000), then add toconfig.json:Testing
tests/memory/test_dakera.py— 12 tests covering: disabled/unconfigured →None,per-project namespacing, recall parsing + score ordering + skipping malformed entries,
X-API-Keyheader presence/absence, request payloads for recall & store, and gracefuldegradation on network errors (recall →
[], store →False).list is empty or undefined (proving default behaviour is unchanged).
ruff checkandruff format --checkclean; existingtests/config,tests/agentsand
tests/templatessuites pass unchanged (47 passed, 6 pre-existing skips).Checklist
ExternalDocumentationhttpx usage, prompt partials, config sub-models)rufflint + format cleandocs/MEMORY.md,example-config.json)Closes #1186.