Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ everalgo/ # monorepo, uv virtual workspace
├── docs/
│ ├── concepts/ # high-level architecture notes
│ └── api/ # API reference (per-distribution)
├── examples/ # runnable quickstart scripts (01–07, use FakeLLMClient)
├── examples/ # runnable quickstart scripts (01–08, use FakeLLMClient)
├── packages/
│ ├── everalgo-core/ # types, llm (+ providers), prompts, testing
│ ├── everalgo-boundary/ # detect_boundaries + DetectionResult + workspace stub
│ ├── everalgo-clustering/ # cluster_by_geometry / cluster_by_llm over list[Cluster]
│ ├── everalgo-rank/ # 4 rankers + fusion / weight / rerank toolkit
│ ├── everalgo-parser/ # multimodal raw-file → ParsedContent (EXPERIMENTAL stub)
│ ├── everalgo-user-memory/ # BoundaryDetector + Episode / Foresight / AtomicFact / Profile
│ ├── everalgo-user-memory/ # BoundaryDetector + Episode / Foresight / AtomicFact / Profile / Decision
│ ├── everalgo-agent-memory/ # AgentBoundaryDetector + AgentCase / AgentSkill / AgentProfile
│ └── everalgo-knowledge/ # KnowledgeExtractor + aclassify_category (file-based knowledge extraction)
├── benchmarks/ # internal LoCoMo benchmark suite ([tool.uv] package = false, not published)
Expand Down Expand Up @@ -107,6 +107,7 @@ uv run python examples/01_boundary_chat.py # Chat → MemCell
uv run python examples/03_user_memory_episode.py # MemCell → Episode
uv run python examples/04_agent_memory_case.py # Agent trajectory → AgentCase
uv run python examples/06_full_user_memory_pipeline.py # Full pipeline
uv run python examples/08_decision_memory_pipeline.py # Decision Extract → Reflect → Principle
```

### Pre-commit hook (required)
Expand Down
23 changes: 21 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,34 @@ The table tracks the current version declared in each `packages/everalgo-*/pypro

| Distribution | Version | Changelog |
|---|---|---|
| `everalgo-core` | 0.5.0 | [packages/everalgo-core/CHANGELOG.md](packages/everalgo-core/CHANGELOG.md) |
| `everalgo-core` | 0.6.0 | [packages/everalgo-core/CHANGELOG.md](packages/everalgo-core/CHANGELOG.md) |
| `everalgo-boundary` | 0.3.0 | [packages/everalgo-boundary/CHANGELOG.md](packages/everalgo-boundary/CHANGELOG.md) |
| `everalgo-clustering` | 0.2.1 | [packages/everalgo-clustering/CHANGELOG.md](packages/everalgo-clustering/CHANGELOG.md) |
| `everalgo-rank` | 0.4.1 | [packages/everalgo-rank/CHANGELOG.md](packages/everalgo-rank/CHANGELOG.md) |
| `everalgo-user-memory` | 0.7.0 | [packages/everalgo-user-memory/CHANGELOG.md](packages/everalgo-user-memory/CHANGELOG.md) |
| `everalgo-user-memory` | 0.8.0 | [packages/everalgo-user-memory/CHANGELOG.md](packages/everalgo-user-memory/CHANGELOG.md) |
| `everalgo-agent-memory` | 0.5.0 | [packages/everalgo-agent-memory/CHANGELOG.md](packages/everalgo-agent-memory/CHANGELOG.md) |
| `everalgo-parser` | 0.2.1 | [packages/everalgo-parser/CHANGELOG.md](packages/everalgo-parser/CHANGELOG.md) |
| `everalgo-knowledge` | 0.1.1 | [packages/everalgo-knowledge/CHANGELOG.md](packages/everalgo-knowledge/CHANGELOG.md) |

## Minor release — 2026-08-26 (decision memory)

Two distributions, one product line. `everalgo-core` adds the `Decision` and `Principle` DTOs.
`everalgo-user-memory` adds `DecisionExtractor`, `DecisionReflector`, and `PrincipleExtractor`, and
raises its core floor to `>=0.6.0`. Decision is the new instance memory type; Principle is algorithm
output synthesised from a Decision cluster — not a product Memory Kind.

The extractors stay storage-free: one LLM call per MemCell with no `sender_id`, reflection still
returns a `Decision`, and `source_entry_ids` are a subset of caller-supplied ids. An offline example
(`examples/08_decision_memory_pipeline.py`) chains Extract → Reflect INIT → Principle on FakeLLM.

| Distribution | Version | Bump |
|---|---|---|
| `everalgo-core` | 0.6.0 | minor — `Decision` and `Principle` types |
| `everalgo-user-memory` | 0.8.0 | minor — Decision extract / reflect / Principle extract; core floor `>=0.6.0` |

Detail in [packages/everalgo-core/CHANGELOG.md](packages/everalgo-core/CHANGELOG.md) and
[packages/everalgo-user-memory/CHANGELOG.md](packages/everalgo-user-memory/CHANGELOG.md).

## Minor release — 2026-08-20 (user-memory)

`ProfileExtractor`'s three prompts are rewritten around one definition of what a profile is: a portrait of a
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ This repo is a **monorepo** of 8 distributions sharing the `everalgo.*` namespac
| [`everalgo-clustering`](packages/everalgo-clustering/) | `Cluster` value object + `cluster_by_geometry` / `cluster_by_llm` operators |
| [`everalgo-rank`](packages/everalgo-rank/) | 4 rankers (episodic / profile / case / skill) over fusion / weight / rerank toolkit |
| [`everalgo-parser`](packages/everalgo-parser/) | Multimodal raw-file → `ParsedContent` (image, audio, PDF, HTML, email, office, URL; video deferred) |
| [`everalgo-user-memory`](packages/everalgo-user-memory/) | `BoundaryDetector` + `Episode` / `Foresight` / `AtomicFact` / `Profile` extractors |
| [`everalgo-user-memory`](packages/everalgo-user-memory/) | `BoundaryDetector` + `Episode` / `Foresight` / `AtomicFact` / `Profile` / `Decision` extractors |
| [`everalgo-agent-memory`](packages/everalgo-agent-memory/) | `AgentBoundaryDetector` + `AgentCase` / `AgentSkill` extractors |
| [`everalgo-knowledge`](packages/everalgo-knowledge/) | `KnowledgeExtractor` pipeline + `aclassify_category` (file-based knowledge extraction) |

Expand All @@ -45,6 +45,7 @@ uv run python examples/01_boundary_chat.py # Chat → MemCell
uv run python examples/03_user_memory_episode.py # MemCell → Episode
uv run python examples/04_agent_memory_case.py # Agent trajectory → AgentCase
uv run python examples/06_full_user_memory_pipeline.py # Full pipeline
uv run python examples/08_decision_memory_pipeline.py # Decision Extract → Reflect → Principle
uv run pytest # workspace-wide test suite
```

Expand Down Expand Up @@ -135,7 +136,7 @@ All eight distributions are published on PyPI. Two operators are **unimplemented
| `WorkspaceMemCellExtractor` | `everalgo.boundary.workspace` | Jira / Email / Confluence slicing — not implemented |
| video parsing | `everalgo.parser.video` | deferred pending an ADR (Gemini Video vs Whisper + frame sampling) |

Everything else is fully implemented and tested: boundary detection, both clustering operators, all four rankers, the user-memory extractors (Episode / Foresight / AtomicFact / Profile), the agent-memory extractors (Case / Skill), and the knowledge extractor (KnowledgeExtractor + document classification).
Everything else is fully implemented and tested: boundary detection, both clustering operators, all four rankers, the user-memory extractors (Episode / Foresight / AtomicFact / Profile / Decision), the agent-memory extractors (Case / Skill), and the knowledge extractor (KnowledgeExtractor + document classification).

## Releasing

Expand Down Expand Up @@ -204,7 +205,7 @@ Read [`AGENTS.md`](AGENTS.md) — the single source of truth for assistant conte
- [`docs/api/`](docs/api/) — per-package API reference
- [`docs/version-policy.md`](docs/version-policy.md) — SemVer + supported Python versions
- [`docs/contributing.md`](docs/contributing.md) — how to contribute
- [`examples/`](examples/) — runnable quickstart scripts (01 through 07)
- [`examples/`](examples/) — runnable quickstart scripts (01 through 08)
- [`AGENTS.md`](AGENTS.md) — onboarding for AI assistants + contributors

## License
Expand Down
4 changes: 3 additions & 1 deletion docs/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Each EverAlgo distribution has its own README with a quick-start, public API sur
| `everalgo-clustering` | [`packages/everalgo-clustering/README.md`](../../packages/everalgo-clustering/README.md) | `Cluster`, `cluster_by_geometry`, `cluster_by_llm` |
| `everalgo-rank` | [`packages/everalgo-rank/README.md`](../../packages/everalgo-rank/README.md) | `rank.episodic`, `rank.profile`, `rank.case`, `rank.skill`, `rank.fusion`, `rank.weight`, `rank.rerank` |
| `everalgo-parser` | [`packages/everalgo-parser/README.md`](../../packages/everalgo-parser/README.md) | `aparse`, `ParsedContent`, image / audio / document / URL parsers (video deferred) |
| `everalgo-user-memory` | [`packages/everalgo-user-memory/README.md`](../../packages/everalgo-user-memory/README.md) | `BoundaryDetector`, `EpisodeExtractor`, `ForesightExtractor`, `AtomicFactExtractor`, `ProfileExtractor` |
| `everalgo-user-memory` | [`packages/everalgo-user-memory/README.md`](../../packages/everalgo-user-memory/README.md) | `BoundaryDetector`, `EpisodeExtractor`, `ForesightExtractor`, `AtomicFactExtractor`, `ProfileExtractor`, `DecisionExtractor`, `PrincipleExtractor` |
| `everalgo-agent-memory` | [`packages/everalgo-agent-memory/README.md`](../../packages/everalgo-agent-memory/README.md) | `AgentBoundaryDetector`, `AgentCaseExtractor`, `AgentSkillExtractor`, `AgentProfileExtractor`, `CaseSkipReason`, `SkillSkipReason` |
| `everalgo-knowledge` | [`packages/everalgo-knowledge/README.md`](../../packages/everalgo-knowledge/README.md) | `KnowledgeExtractor`, `aclassify_category`, `KnowledgeMemory`, `CategorySpec` |

Expand All @@ -31,6 +31,8 @@ from everalgo.types import (
ConversationItem, # ChatMessage | ToolCallRequest | ToolCallResult (discriminated union)
MemCell, # boundary-segmented conversation slice; items: list[ConversationItem]
Episode, # narrative memory — owner_id, episode, subject, summary, timestamp
Decision, # committed trade-off — owner_id, title, decision, reason, impact, tags, timestamp
Principle, # synthesised from a Decision cluster — owner_id, title, statement, source_entry_ids, timestamp
Foresight, # anticipated future event — owner_id, foresight, evidence, start_time, end_time, duration_days
AtomicFact, # single verifiable assertion — owner_id (str | None), fact, timestamp
Profile, # structured user profile — owner_id, summary, timestamp; extra fields via extra="allow"
Expand Down
2 changes: 1 addition & 1 deletion docs/concepts/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ everalgo/ # PEP 420 namespace package — no __init
├── everalgo-clustering/ # cluster_by_geometry / cluster_by_llm over list[Cluster]
├── everalgo-rank/ # 4 rankers + fusion / weight / rerank tools
├── everalgo-parser/ # multimodal raw-file → ParsedContent
├── everalgo-user-memory/ # Episode / Foresight / AtomicFact / Profile
├── everalgo-user-memory/ # Episode / Foresight / AtomicFact / Profile / Decision
├── everalgo-agent-memory/ # AgentCase / AgentSkill
└── everalgo-knowledge/ # KnowledgeMemory
```
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,5 +202,5 @@ Both import the same class.

- **Architecture deep-dive** → [concepts/architecture.md](concepts/architecture.md)
- **Stateless design rationale** → [concepts/stateless-design.md](concepts/stateless-design.md)
- **Full example scripts** → [`examples/`](../examples/) (01 through 07)
- **Full example scripts** → [`examples/`](../examples/) (01 through 08)
- **API index per package** → [api/README.md](api/README.md)
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Install only what your use case needs:
| `everalgo-clustering` | `Cluster` value object + `cluster_by_geometry` / `cluster_by_llm` operators |
| `everalgo-rank` | 4 retrieval strategies (hybrid / agentic / cluster / maxsim) + 4 business rankers (episodic / profile / case / skill) + fusion / weight / rerank tools |
| `everalgo-parser` | Multimodal raw-file → `ParsedContent` |
| `everalgo-user-memory` | `Episode` / `Foresight` / `AtomicFact` / `Profile` extractors |
| `everalgo-user-memory` | `Episode` / `Foresight` / `AtomicFact` / `Profile` / `Decision` extractors |
| `everalgo-agent-memory` | `AgentCase` / `AgentSkill` extractors |
| `everalgo-knowledge` | `KnowledgeMemory` extractor |

Expand Down
198 changes: 198 additions & 0 deletions examples/08_decision_memory_pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
"""Decision memory pipeline — Extract → Reflect → Principle.

Chains the three Decision-line operators on two MemCells built directly from chat
(no BoundaryDetector, no markdown, no clustering):

1. ``DecisionExtractor.aextract`` (Jan) → one Decision, ``owner_id=None``
2. ``DecisionExtractor.aextract`` (Aug) → one Decision, ``owner_id=None``
3. ``DecisionReflector.areflect`` INIT → one merged Decision (still a Decision)
4. ``PrincipleExtractor.aextract`` → list[Principle] from the two *instance*
decisions, not the merge. Entry ids are caller-supplied stand-ins for EverOS
markdown ids; this operator never invents them.

One ``FakeLLMClient`` scripts all four LLM calls in order. No API key.

Run:
uv run python examples/08_decision_memory_pipeline.py
"""

from __future__ import annotations

import asyncio
import json

from everalgo.testing.fake_llm import FakeLLMClient
from everalgo.types import ChatMessage, Decision, MemCell, Principle
from everalgo.user_memory import DecisionExtractor, DecisionReflector, PrincipleExtractor

# 2026-01-15T12:00:00Z / 2026-08-20T12:00:00Z — chronological INIT input.
_TS_JAN = 1_768_478_400_000
_TS_AUG = 1_787_227_200_000

# Caller-supplied storage ids. EverOS later passes markdown entry ids (``dc_`` prefix);
# the algorithm never generates this format.
_ENTRY_JAN = "dc_001"
_ENTRY_AUG = "dc_002"

# ---------------------------------------------------------------------------
# Scripted LLM responses — one per pipeline stage, in call order.
# ---------------------------------------------------------------------------

_JAN_DECISION_JSON = json.dumps(
{
"decisions": [
{
"title": "Agent core language",
"decision": "Use Python and LangChain for the core Agent Runtime.",
"reason": "Need to ship agent capability quickly while the surface is still moving.",
"impact": "Device capabilities stay behind a Python API.",
"tags": ["architecture", "runtime"],
}
]
}
)

_AUG_DECISION_JSON = json.dumps(
{
"decisions": [
{
"title": "In-house Agent Runtime",
"decision": "Replace LangChain with a self-developed Agent Runtime.",
"reason": "Need control over the main loop; a third-party framework hides too much.",
"impact": "Device talks through our own APIs; framework upgrades no longer dictate the loop.",
"tags": ["architecture", "runtime"],
}
]
}
)

# Structured Output schema order: decision, reason, then title / impact / tags.
_REFLECT_JSON = json.dumps(
{
"decision": "Use a self-developed Agent Runtime, keeping Python for the core.",
"reason": "Iteration speed still matters, but the main loop has to be ours.",
"title": "Agent Runtime ownership",
"impact": "Device capabilities connect through our APIs.",
"tags": ["architecture", "runtime"],
}
)

_PRINCIPLE_JSON = json.dumps(
{
"principles": [
{
"title": "Own the loop, iterate the rest",
"statement": (
"Keep ownership of the Agent main loop; prefer iteration speed over a "
"premature rewrite of everything else."
),
"source_entry_ids": [_ENTRY_JAN, _ENTRY_AUG],
}
]
}
)


def _memcell_jan() -> MemCell:
"""January slice: ship with LangChain to move fast."""
return MemCell(
items=[
ChatMessage(
id="m_jan_1",
role="user",
content=(
"We need an Agent Runtime this quarter. LangChain in Python gets us a "
"working core fast. Device-side can stay Rust."
),
timestamp=_TS_JAN,
sender_id="u_alice",
sender_name="Alice",
),
ChatMessage(
id="m_jan_2",
role="assistant",
content="Agreed — Python plus LangChain for the core, Rust on device.",
timestamp=_TS_JAN + 1_000,
sender_id="assistant",
),
],
timestamp=_TS_JAN,
)


def _memcell_aug() -> MemCell:
"""August slice: take back the main loop."""
return MemCell(
items=[
ChatMessage(
id="m_aug_1",
role="user",
content=(
"LangChain is hiding the main loop. We should replace it with our own "
"Agent Runtime and keep Python for the core."
),
timestamp=_TS_AUG,
sender_id="u_alice",
sender_name="Alice",
),
ChatMessage(
id="m_aug_2",
role="assistant",
content="Then the device talks through our APIs, not the framework's.",
timestamp=_TS_AUG + 1_000,
sender_id="assistant",
),
],
timestamp=_TS_AUG,
)


async def main() -> None:
"""Run Extract → Reflect → Principle and print each stage."""
fake = FakeLLMClient(
responses=[
_JAN_DECISION_JSON,
_AUG_DECISION_JSON,
_REFLECT_JSON,
_PRINCIPLE_JSON,
]
)
extractor = DecisionExtractor(llm=fake)
reflector = DecisionReflector(llm=fake)
principles = PrincipleExtractor(llm=fake)

# --- 1-2. Extract (no sender_id; owner_id stays None) --------------------
extracted_jan = await extractor.aextract(_memcell_jan())
extracted_aug = await extractor.aextract(_memcell_aug())
assert len(extracted_jan) == 1 and extracted_jan[0].owner_id is None
assert len(extracted_aug) == 1 and extracted_aug[0].owner_id is None
d_jan, d_aug = extracted_jan[0], extracted_aug[0]
print(f"[extract] n=1 owner_id={d_jan.owner_id!r} ts={d_jan.timestamp} title={d_jan.title!r}")
print(f"[extract] n=1 owner_id={d_aug.owner_id!r} ts={d_aug.timestamp} title={d_aug.title!r}")

# --- 3. Reflect INIT -- still a Decision, not a Principle ----------------
merged: Decision = await reflector.areflect([d_jan, d_aug])
assert isinstance(merged, Decision) and not isinstance(merged, Principle)
assert merged.owner_id is None
assert merged.timestamp == d_aug.timestamp
print(f"[reflect] owner_id={merged.owner_id!r} timestamp={merged.timestamp} decision={merged.decision!r}")

# --- 4. Principle from the instance cluster (not the merge) --------------
cluster: list[tuple[str, Decision]] = [(_ENTRY_JAN, d_jan), (_ENTRY_AUG, d_aug)]
extracted_principles = await principles.aextract(cluster, owner_id="u_alice")
assert len(extracted_principles) == 1
principle = extracted_principles[0]
assert isinstance(principle, Principle)
assert principle.owner_id == "u_alice"
assert set(principle.source_entry_ids) <= {_ENTRY_JAN, _ENTRY_AUG}
print(
f"[principle] n=1 owner={principle.owner_id!r} "
f"sources={principle.source_entry_ids!r} statement={principle.statement!r}"
)

print(f"\ntotal LLM calls: {fake.call_count} (expected 4)")
assert fake.call_count == 4


if __name__ == "__main__":
asyncio.run(main())
Loading