Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Domain `Decision` + `Decision.from_algo`.** Copies `Episode.from_algo`: the caller supplies `owner_id` / `session_id` / `parent_id` (source memcell), and any algo-side `owner_id` or smuggled `parent_id` is dropped so one generic extract can fan out per user sender. No domain `Principle`.
- **`DecisionDailyFrontmatter`.** User-readable daily-log at `users/<id>/decisions/decision-<date>.md` (`ENTRY_ID_PREFIX="dc"`), with `deprecated_entries` from day one. Not a dot-prefixed internal directory.
- **LanceDB `decision` table + `decision_repo`.** Daily-log chassis with dual BM25 (`decision_tokens` / `reason_tokens`), nullable vector, and `deprecated_by` from day one. Wired into `_BUSINESS_SCHEMAS`, `BUSINESS_SCHEMAS_WITH_VECTOR`, and cascade `_TABLE_SPECS` (embed text = `r["decision"]`).
- **`DecisionWriter` / `DecisionReader`.** Append-only daily-log at `users/<id>/decisions/decision-<date>.md` with `dc_` entry ids. Markdown is the SoT; cascade projects into Lance.
- **`extract_decision` OME strategy (`enabled=True`) + `DecisionExtracted` (`source=pipeline`).** One `DecisionExtractor` call per memcell (no `sender_id`); EverOS fans out one daily-log copy per user sender and emits `DecisionExtracted` after each write. An empty list is success.
- **`DecisionHandler` + `KIND_REGISTRY` kind `decision`.** Markdown → Lance projection; embed only the Decision body (soft dependency); dual BM25 `decision_tokens` / `reason_tokens`. `deprecated_by` still from chassis `deprecated_entries`.
- **Search `data.decisions` + Get `memory_type=decision`.** User-partition Decision recall (`DecisionRecaller`: dual BM25 + cosine) fused with `rrf` (never `arank`). `kinds` filters the episode / decision lanes (`principle` is not a kind). Get lists Decision rows with `deprecated_by IS NULL`; there is no `GetMemoryType.PRINCIPLE`.
- **`trigger_decision_clustering` OME strategy.** On `DecisionExtracted(source=pipeline)` embeds `decision_text` and geometry-merges into sqlite clusters with `kind=decision` / `member_type=decision` (not the `user_memory` episode track). Emits `DecisionClusterUpdated` with a row snapshot. Cascade Phase 2 backfill synthesizes the same events for existing Lance decision rows (`parent_type=memcell`). Embed missing → debug no-op.
- **`principles.md` + Lance `principle` + `PrincipleHandler`.** Single-file rewrite via existing `ProfileWriter` (`users/<id>/principles.md`, `type=principle`). Cascade explodes the frontmatter list into N KV rows (`id=<owner_id>_<pr_…>`). No vector, no BM25, not in `_TABLE_SPECS` / `BUSINESS_SCHEMAS_WITH_VECTOR`. `KIND_REGISTRY` name `principle` is the cascade projection only — not a product Kind.
- **`extract_principles` + Search `include_principles`.** On `DecisionClusterUpdated`, unions every sqlite `kind=decision` cluster (one `PrincipleExtractor` call per cluster) into one `principles.md`. Search attaches the Lance KV rows when `include_principles=true` (`data.principles` always present). Not a kind: `kinds: ["principle"]` stays 422; agent owners ignore the flag. No Get `memory_type=principle`.
- **`DecisionReflectionOrchestrator` + `reflect_decisions` Cron (`enabled=false`).** Select → Merge (`DecisionReflector.areflect` → Decision DTO) → markdown `parent_type=cluster` → `DecisionExtracted(source="reflection")` (no wait; clustering is pipeline-only) → deprecate md `deprecated_entries` + Lance `deprecated_by`. No atomic-fact path. Sibling of episode reflection — does not edit `ReflectionOrchestrator`.
- **Decision closed-loop integration (FakeLLM).** `extract_decision` → `users/<id>/decisions/decision-*.md` → Cascade `decision` row → keyword Search recalls 「设备 Runtime 为什么使用 Rust?」 in `data.decisions`. No real model. `kinds: ["principle"]` / Get `memory_type=principle` stay 422.

### Changed

- **`everalgo-user-memory` 0.4.0 → 0.8.0** (path-pin to the sibling EverAlgo checkout until 0.8.0 is on PyPI) and **`everalgo-agent-memory` 0.4.0 → 0.5.0**. 0.8.0 is the first user-memory release that exports `Decision` / `Principle` types and extractors. `extract_decision` calls `DecisionExtractor` on each `UserPipelineStarted`. `AlgoEpisode` construction now passes the required `summary` field introduced in everalgo-core 0.5.0.

## [1.2.3] - 2026-08-07

**Background maintenance that fails loudly instead of quietly.** A soak run on
Expand Down
79 changes: 68 additions & 11 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,14 +476,16 @@ should be reserved for offline or background workflows.
| Value | Track | Returned in `data.<plural>` |
|---|---|---|
| `"episode"` | user | `data.episodes` — [GetEpisodeItem](#getepisodeitem) |
| `"decision"` | user | `data.decisions` — [GetDecisionItem](#getdecisionitem) |
| `"profile"` | user | `data.profiles` — [GetProfileItem](#getprofileitem) |
| `"agent_case"` | agent | `data.agent_cases` — [GetAgentCaseItem](#getagentcaseitem) |
| `"agent_skill"` | agent | `data.agent_skills` — [GetAgentSkillItem](#getagentskillitem) |

`memory_type` must match the requested owner kind: `"episode"` /
`"profile"` require `user_id`; `"agent_case"` / `"agent_skill"`
require `agent_id`. The mismatching combinations are rejected with
`422`.
`"decision"` / `"profile"` require `user_id`; `"agent_case"` /
`"agent_skill"` require `agent_id`. The mismatching combinations are
rejected with `422`. Principle is Meta Memory — it is not a
`memory_type` value.

## Endpoints

Expand Down Expand Up @@ -657,12 +659,13 @@ optional final LLM rerank. Returns ranked items grouped by kind.
| `radius` | `number \| null` | no | `null` | `0.0 ≤ x ≤ 1.0` if set |
| `min_score` | `number \| null` | no | `null` | `0.0 ≤ x ≤ 1.0` if set |
| `include_profile` | `boolean` | no | `false` | — |
| `kinds` | `array<"episode" \| "decision"> \| null` | no | `null` | user-partition only; empty list is `422` |
| `enable_llm_rerank` | `boolean` | no | `false` | — |
| `filters` | [FilterNode](#filternode-filter-dsl) `\| null` | no | `null` | — |

**`user_id` / `agent_id`** — **Exactly one** must be set. Determines
which track is searched: `user_id` → user-memory (episodes /
profiles); `agent_id` → agent-memory (cases / skills).
decisions / profiles); `agent_id` → agent-memory (cases / skills).

**`app_id` / `project_id`** — Scope identifiers; results never cross
scopes.
Expand Down Expand Up @@ -702,11 +705,20 @@ independent of `radius` (which is a per-recall cosine threshold).
**`include_profile`** — When `user_id` is set, also fetch the user's
profile and include it in `data.profiles`. The profile is not
ranked; `score` is `null`. Ignored when `agent_id` is set.
Independent of `kinds`.

**`kinds`** — Optional user-partition kind filter. `null` (default)
searches episode and decision in parallel. `["decision"]` /
`["episode"]` restrict the lanes; `["episode", "decision"]` is the
same as omitting the field. Rejected when `agent_id` is set, when
the list is empty, or when a value other than `"episode"` /
`"decision"` is supplied (`"principle"` is not a kind).

**`enable_llm_rerank`** — Opt-in LLM rerank pass for
`method: "hybrid"`. Applies to `agent_case` and `agent_skill` fusion
only; the episode hybrid path has built-in fact eviction and
ignores this flag. Adds one LLM call per request. Ignored by
ignores this flag. Decision HYBRID fuses with RRF and also ignores
it. Adds one LLM call per request. Ignored by
`keyword` / `vector` (no fusion to rerank) and `agentic` (uses its
own cross-encoder loop).

Expand All @@ -716,14 +728,15 @@ it does not perturb the ranker.

#### Response body

`200 OK` returns a SuccessEnvelope wrapping `SearchData`. All five
`200 OK` returns a SuccessEnvelope wrapping `SearchData`. All kind
arrays are always present so client code can iterate without
branching on owner type; arrays that do not apply to the requested
owner kind stay as `[]`.

| Field | Type | Notes |
|---|---|---|
| `episodes` | `array<SearchEpisodeItem>` | Populated when `user_id` is set |
| `episodes` | `array<SearchEpisodeItem>` | Populated when `user_id` is set (unless `kinds` excludes `"episode"`) |
| `decisions` | `array<SearchDecisionItem>` | Populated when `user_id` is set (unless `kinds` excludes `"decision"`). HYBRID fuses BM25 + vector with RRF (not `arank`) |
| `profiles` | `array<SearchProfileItem>` | Populated when `user_id` is set **and** `include_profile=true` |
| `agent_cases` | `array<SearchAgentCaseItem>` | Populated when `agent_id` is set |
| `agent_skills` | `array<SearchAgentSkillItem>` | Populated when `agent_id` is set |
Expand Down Expand Up @@ -751,6 +764,27 @@ query within this episode (already nested, no separate call needed).
| `score` | `number` | Fused retrieval score for this episode |
| `atomic_facts` | `array<SearchAtomicFactItem>` | Sub-facts extracted from the same episode that matched the query |

#### SearchDecisionItem

User-track decision hit. `score` is the fused retrieval score (RRF
on hybrid; raw BM25 / cosine on keyword / vector). There is no
`sender_ids` field — tags are labels, not conversation participants.

| Field | Type | Notes |
|---|---|---|
| `id` | `string` | `<user_id>_dc_<YYYYMMDD>_<NNN>` |
| `user_id` | `string \| null` | Owner of this decision |
| `app_id` | `string` | Scope where the decision lives |
| `project_id` | `string` | Scope where the decision lives |
| `session_id` | `string \| null` | Originating session; `null` when the extract did not bind one |
| `timestamp` | `string` | ISO-8601 with timezone offset — see [Conventions](#conventions) |
| `title` | `string` | Short title of the trade-off |
| `decision` | `string` | Decision body (the retrieval / embed anchor) |
| `reason` | `string` | Why the trade-off was made |
| `impact` | `string \| null` | Optional downstream effect |
| `tags` | `array<string>` | Caller-supplied labels |
| `score` | `number` | Fused retrieval score for this decision |

#### SearchAtomicFactItem

A single-sentence fact pulled out of an episode during extraction.
Expand Down Expand Up @@ -880,6 +914,7 @@ Response (real capture):
]
}
],
"decisions": [],
"profiles": [],
"agent_cases": [],
"agent_skills": [],
Expand Down Expand Up @@ -910,15 +945,16 @@ for UI browsing, exports, or filtered scans.
| `filters` | [FilterNode](#filternode-filter-dsl) `\| null` | no | `null` | — |

**`user_id` / `agent_id`** — **Exactly one** must be set, and it must
match the track implied by `memory_type` (`"episode"` / `"profile"`
require `user_id`; `"agent_case"` / `"agent_skill"` require
match the track implied by `memory_type` (`"episode"` / `"decision"` /
`"profile"` require `user_id`; `"agent_case"` / `"agent_skill"` require
`agent_id`).

**`app_id` / `project_id`** — Scope identifiers.

**`memory_type`** — Which item kind to list; see
[GetMemoryType](#getmemorytype). The route populates exactly one of
the four arrays in `data` based on this value.
the kind arrays in `data` based on this value. `"principle"` is not
a valid value.

**`page`** — 1-indexed page number. Together with `page_size`
determines the window. The response's `total_count` reports how many
Expand All @@ -938,13 +974,14 @@ predicate-based filtering before pagination.

#### Response body

`200 OK` returns a SuccessEnvelope wrapping `GetData`. The four
`200 OK` returns a SuccessEnvelope wrapping `GetData`. The kind
arrays are always present so client code can iterate without
branching on `memory_type`; exactly one is populated.

| Field | Type | Notes |
|---|---|---|
| `episodes` | `array<GetEpisodeItem>` | Populated when `memory_type="episode"` |
| `decisions` | `array<GetDecisionItem>` | Populated when `memory_type="decision"` |
| `profiles` | `array<GetProfileItem>` | Populated when `memory_type="profile"` |
| `agent_cases` | `array<GetAgentCaseItem>` | Populated when `memory_type="agent_case"` |
| `agent_skills` | `array<GetAgentSkillItem>` | Populated when `memory_type="agent_skill"` |
Expand All @@ -971,6 +1008,25 @@ sub-facts).
| `episode` | `string` | Full extracted narrative |
| `type` | `"Conversation"` | — |

#### GetDecisionItem

Same shape as [SearchDecisionItem](#searchdecisionitem) **minus**
`score` (listing is unranked).

| Field | Type | Notes |
|---|---|---|
| `id` | `string` | `<user_id>_dc_<YYYYMMDD>_<NNN>` |
| `user_id` | `string \| null` | Owner |
| `app_id` | `string` | Scope |
| `project_id` | `string` | Scope |
| `session_id` | `string \| null` | Originating session; `null` when unbound |
| `timestamp` | `string` | ISO-8601 with timezone offset — see [Conventions](#conventions) |
| `title` | `string` | Short title of the trade-off |
| `decision` | `string` | Decision body |
| `reason` | `string` | Why the trade-off was made |
| `impact` | `string \| null` | Optional downstream effect |
| `tags` | `array<string>` | Caller-supplied labels |

#### GetProfileItem

| Field | Type | Notes |
Expand Down Expand Up @@ -1051,6 +1107,7 @@ Response (real capture):
"type": "Conversation"
}
],
"decisions": [],
"profiles": [],
"agent_cases": [],
"agent_skills": [],
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ optional `everalgo-parser` extra), imported under the `everalgo` namespace,
holding **only memory extraction algorithms**:

- `everalgo.parser` — multi-modal parsing (optional `[multimodal]` extra)
- `everalgo.user_memory` — ConvMemCell / Episode / Foresight / AtomicFact / Profile extractors
- `everalgo.user_memory` — ConvMemCell / Episode / Foresight / AtomicFact / Profile / Decision extractors
- `everalgo.agent_memory` — AgentMemCell / Case / Skill extractors
- `everalgo.rank` — boundary detection / fusion + rerank
- `everalgo.knowledge` — KnowledgeExtractor (document parse + topic extraction)
Expand Down
Loading