everalgo-agent-memory 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. everalgo_agent_memory-0.1.0/.gitignore +58 -0
  2. everalgo_agent_memory-0.1.0/CHANGELOG.md +26 -0
  3. everalgo_agent_memory-0.1.0/LICENSE +21 -0
  4. everalgo_agent_memory-0.1.0/PKG-INFO +153 -0
  5. everalgo_agent_memory-0.1.0/README.md +132 -0
  6. everalgo_agent_memory-0.1.0/pyproject.toml +32 -0
  7. everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/__init__.py +16 -0
  8. everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/_text.py +52 -0
  9. everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/boundary.py +129 -0
  10. everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/case.py +595 -0
  11. everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/prompts/__init__.py +7 -0
  12. everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/prompts/case_compress.py +76 -0
  13. everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/prompts/case_filter.py +37 -0
  14. everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/prompts/skill_failure.py +87 -0
  15. everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/prompts/skill_maturity.py +52 -0
  16. everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/prompts/skill_success.py +106 -0
  17. everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/prompts/tool_pre_compress.py +42 -0
  18. everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/py.typed +0 -0
  19. everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/skill.py +246 -0
  20. everalgo_agent_memory-0.1.0/src/everalgo/agent_memory/skill_ops.py +482 -0
  21. everalgo_agent_memory-0.1.0/tests/agent_memory/__init__.py +0 -0
  22. everalgo_agent_memory-0.1.0/tests/agent_memory/test_agent_boundary.py +221 -0
  23. everalgo_agent_memory-0.1.0/tests/agent_memory/test_agent_memory_case.py +1097 -0
  24. everalgo_agent_memory-0.1.0/tests/agent_memory/test_agent_memory_public_api.py +34 -0
  25. everalgo_agent_memory-0.1.0/tests/agent_memory/test_agent_memory_skill.py +1032 -0
  26. everalgo_agent_memory-0.1.0/tests/agent_memory/test_agent_memory_text.py +118 -0
@@ -0,0 +1,58 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ *.egg
11
+ wheels/
12
+
13
+ # Environments
14
+ .env
15
+ .env.local
16
+ .venv
17
+ env/
18
+ venv/
19
+
20
+ # uv
21
+ .uv/
22
+
23
+ # Test / coverage / cache
24
+ .pytest_cache/
25
+ .cache
26
+ .coverage
27
+ .coverage.*
28
+ coverage.xml
29
+ report.xml
30
+ htmlcov/
31
+ .tox/
32
+ .nox/
33
+ .hypothesis/
34
+
35
+ # Type checkers / linters
36
+ .mypy_cache/
37
+ .dmypy.json
38
+ .pyre/
39
+ .pytype/
40
+ .ruff_cache/
41
+
42
+ # Jupyter
43
+ .ipynb_checkpoints/
44
+
45
+ # IDE / OS
46
+ .vscode/
47
+ .idea/
48
+ *.swp
49
+ .DS_Store
50
+
51
+ # Logs
52
+ *.log
53
+
54
+ # Project local artifacts (placed under local/ per project convention)
55
+ local/
56
+
57
+ # Claude Code session-local state (scheduled tasks, agent caches, locks)
58
+ .claude/
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are documented here. Format follows
4
+ [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/). Versioning
5
+ follows [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+
11
+ - `AgentBoundaryDetector`: facade class that filters a `list[ConversationItem]` to only tool-call-carrying turns, calls `everalgo.boundary.detect_boundaries` on the result, then remaps indices back to the original sequence.
12
+ - `AgentCaseExtractor`: 11-step pipeline — heuristic skip check, tool-call-round counting, message trimming / pre-compression, `AgentCase` extraction via LLM. Accepts `llm=` at construction time.
13
+ - `AgentSkillExtractor`: add / update / retire operations with `cluster_id` binding; accepts `llm=` at construction time. `SkillConfig` pydantic model exposes maturity and retire thresholds.
14
+ - English prompts for case and skill extraction under `agent_memory/prompts/en/`.
15
+
16
+ ### Changed
17
+
18
+ - `AgentCaseExtractor` and `AgentSkillExtractor` rewired to the unified `MemCell` contract (replacing the previous `AgentMemCell` type).
19
+
20
+ ### Removed
21
+
22
+ - Broad `except Exception → None` handler in `_evaluate_maturity`: the function now propagates exceptions from the LLM call.
23
+ - `asyncio.gather(return_exceptions=True)` in `_pre_compress_to_list`: individual compression errors now propagate instead of being swallowed.
24
+ - Fail-open path in `_is_worth_extracting`: the function raises on LLM error instead of returning `True` unconditionally.
25
+
26
+ [Unreleased]: https://github.com/EverMind-AI/EverAlgo/compare/main...HEAD
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EverMind
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: everalgo-agent-memory
3
+ Version: 0.1.0
4
+ Summary: EverAlgo agent memory: AgentCase / AgentSkill extractors and AgentBoundaryDetector facade.
5
+ Project-URL: Homepage, https://github.com/EverMind-AI/EverAlgo
6
+ Project-URL: Repository, https://github.com/EverMind-AI/EverAlgo
7
+ Project-URL: Issues, https://github.com/EverMind-AI/EverAlgo/issues
8
+ Project-URL: Documentation, https://github.com/EverMind-AI/EverAlgo/tree/main/packages/everalgo-agent-memory
9
+ Project-URL: Changelog, https://github.com/EverMind-AI/EverAlgo/blob/main/packages/everalgo-agent-memory/CHANGELOG.md
10
+ Author: EverMind
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.12
17
+ Requires-Dist: everalgo-boundary<2.0.0,>=0.1.0
18
+ Requires-Dist: everalgo-clustering<2.0.0,>=0.1.0
19
+ Requires-Dist: everalgo-core<2.0.0,>=0.1.0
20
+ Description-Content-Type: text/markdown
21
+
22
+ # everalgo-agent-memory
23
+
24
+ Agent-side memory products for EverAlgo — `AgentCaseExtractor` distils an agent trajectory `MemCell` into one `AgentCase`; `AgentSkillExtractor` maintains a cluster's reusable skill set from accumulated cases. `AgentBoundaryDetector` handles boundary detection over mixed `ConversationItem` trajectories (chat + tool calls).
25
+
26
+ See the umbrella project: [EverAlgo monorepo](../../README.md) and the architecture document at [`docs/concepts/architecture.md`](../../docs/concepts/architecture.md).
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install everalgo-agent-memory
32
+ # Auto-pulls: everalgo-core, everalgo-boundary, everalgo-clustering
33
+ ```
34
+
35
+ ## What this distribution provides
36
+
37
+ | Symbol | Role |
38
+ |---|---|
39
+ | `AgentBoundaryDetector` | Boundary detection on agent trajectories (filter → detect → remap for mixed `ConversationItem` lists) |
40
+ | `AgentCaseExtractor` | Distils one agent-trajectory `MemCell` into `[] \| [AgentCase]` (11-step pipeline) |
41
+ | `AgentSkillExtractor` | Aggregates one new `AgentCase` into incremental skill operations for a cluster; returns add / update / retire entries |
42
+
43
+ ## Quick start
44
+
45
+ ```python
46
+ import asyncio
47
+ import json
48
+
49
+ from everalgo.agent_memory.case import AgentCaseExtractor
50
+ from everalgo.llm.types import ChatResponse
51
+ from everalgo.testing.fake_llm import FakeLLMClient
52
+ from everalgo.types import AgentCase, ChatMessage, MemCell, ToolCall, ToolCallFunction, ToolCallRequest, ToolCallResult
53
+
54
+ _CASE_JSON = json.dumps({
55
+ "task_intent": "Search for Python async retry libraries",
56
+ "approach": "1. Search. 2. Filter. 3. Summarise.",
57
+ "quality_score": 0.82,
58
+ "key_insight": "Use tenacity AsyncRetrying for native async back-off.",
59
+ })
60
+
61
+ async def main() -> None:
62
+ fake = FakeLLMClient(responses=[ChatResponse(content=_CASE_JSON, model="fake")])
63
+ mc = MemCell(
64
+ items=[
65
+ ChatMessage(id="u1", role="user", content="Best async retry libs?", timestamp=1_700_000_000_000, sender_id="user"),
66
+ ToolCallRequest(tool_calls=[ToolCall(id="c1", function=ToolCallFunction(name="web.search", arguments='{}'))], timestamp=1_700_000_000_100, sender_id="assistant"),
67
+ ToolCallResult(tool_call_id="c1", content="Found: tenacity.", timestamp=1_700_000_000_200),
68
+ ToolCallRequest(tool_calls=[ToolCall(id="c2", function=ToolCallFunction(name="web.search", arguments='{}'))], timestamp=1_700_000_000_300, sender_id="assistant"),
69
+ ToolCallResult(tool_call_id="c2", content="tenacity supports async.", timestamp=1_700_000_000_400),
70
+ ChatMessage(id="a1", role="assistant", content="Use tenacity AsyncRetrying.", timestamp=1_700_000_000_500, sender_id="assistant"),
71
+ ],
72
+ timestamp=1_700_000_000_500,
73
+ )
74
+
75
+ cases: list[AgentCase] = await AgentCaseExtractor(llm=fake).aextract(mc)
76
+ if cases:
77
+ print(cases[0].task_intent)
78
+
79
+ asyncio.run(main())
80
+ ```
81
+
82
+ See [`examples/04_agent_memory_case.py`](../../examples/04_agent_memory_case.py) for the full runnable example.
83
+
84
+ ## API surface
85
+
86
+ ```python
87
+ class AgentBoundaryDetector:
88
+ def __init__(self, *, llm: LLMClient) -> None: ...
89
+ async def adetect(
90
+ self, items: list[ConversationItem], *, is_final: bool = False, prompt: str | None = None
91
+ ) -> DetectionResult: ...
92
+
93
+ class AgentCaseExtractor:
94
+ def __init__(self, *, llm: LLMClient) -> None: ...
95
+ async def aextract(
96
+ self, memcell: MemCell, *,
97
+ prompt_filter: str | None = None,
98
+ prompt_compress: str | None = None,
99
+ prompt_tool_pre_compress: str | None = None,
100
+ ) -> list[AgentCase]: ... # length 0 (filtered) or 1
101
+
102
+ class AgentSkillExtractor:
103
+ def __init__(self, *, llm: LLMClient) -> None: ...
104
+ async def aextract(
105
+ self,
106
+ case: AgentCase,
107
+ *,
108
+ existing_relevant_skills: Sequence[AgentSkill],
109
+ supporting_cases: Sequence[AgentCase],
110
+ prompt_success: str | None = None,
111
+ prompt_failure: str | None = None,
112
+ prompt_maturity: str | None = None,
113
+ skip_quality_threshold: float = 0.2,
114
+ skip_maturity_scoring: bool = True,
115
+ maturity_threshold: float = 0.6,
116
+ retire_confidence: float = 0.1,
117
+ failure_quality_threshold: float = 0.5,
118
+ max_case_history: int = 9,
119
+ max_description_tokens: int = 400,
120
+ max_content_tokens: int = 5000,
121
+ maturity_trivial_change_ratio: float = 0.2,
122
+ maturity_reeval_change_ratio: float = 0.4,
123
+ ) -> list[AgentSkill]: ...
124
+ ```
125
+
126
+ All class methods have a sync bridge: `extractor.extract(...)` is `async_to_sync(aextract)`.
127
+
128
+ ### AgentCaseExtractor pipeline
129
+
130
+ The extractor runs an 11-step pipeline: strip-before-first-user → structural pre-filter → heuristic trim → over-size bail → LLM filter (skipped when ≥2 tool rounds) → tool pre-compress → LLM compress → parse → validate → build `AgentCase`. Returns `[]` when the trajectory is filtered out; returns `[AgentCase]` on success.
131
+
132
+ ### AgentSkillExtractor return contract
133
+
134
+ Pass the new `AgentCase` and the pre-filtered `existing_relevant_skills` (e.g. top-K cosine from the caller's store) plus the associated `supporting_cases` (cases referenced by existing skills). The caller decodes add / update / retire by checking whether `skill.id` is already in `existing_relevant_skills` and whether `skill.confidence < retire_confidence`.
135
+
136
+ Cases with `quality_score < skip_quality_threshold` (default 0.2) short-circuit to `[]` without calling the LLM.
137
+
138
+ `AgentSkill.cluster_id` is always `""` on extraction — the caller stamps the cluster identity after persisting.
139
+
140
+ ### Customising prompts
141
+
142
+ ```python
143
+ import everalgo.agent_memory.prompts.case_filter as _cf
144
+ _cf.AGENT_CASE_FILTER_PROMPT = my_custom_filter_prompt # global override
145
+ ```
146
+
147
+ Or per-call: pass `prompt_filter=` / `prompt_compress=` / `prompt_tool_pre_compress=` to `aextract`.
148
+
149
+ ## Related distributions
150
+
151
+ - [`everalgo-boundary`](../everalgo-boundary/) — `detect_boundaries` primitive used by `AgentBoundaryDetector`
152
+ - [`everalgo-clustering`](../everalgo-clustering/) — `cluster_by_llm` for skill-cluster assignment
153
+ - [`everalgo-rank`](../everalgo-rank/) — `CaseRanker` / `SkillRanker` for read-time ranking
@@ -0,0 +1,132 @@
1
+ # everalgo-agent-memory
2
+
3
+ Agent-side memory products for EverAlgo — `AgentCaseExtractor` distils an agent trajectory `MemCell` into one `AgentCase`; `AgentSkillExtractor` maintains a cluster's reusable skill set from accumulated cases. `AgentBoundaryDetector` handles boundary detection over mixed `ConversationItem` trajectories (chat + tool calls).
4
+
5
+ See the umbrella project: [EverAlgo monorepo](../../README.md) and the architecture document at [`docs/concepts/architecture.md`](../../docs/concepts/architecture.md).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install everalgo-agent-memory
11
+ # Auto-pulls: everalgo-core, everalgo-boundary, everalgo-clustering
12
+ ```
13
+
14
+ ## What this distribution provides
15
+
16
+ | Symbol | Role |
17
+ |---|---|
18
+ | `AgentBoundaryDetector` | Boundary detection on agent trajectories (filter → detect → remap for mixed `ConversationItem` lists) |
19
+ | `AgentCaseExtractor` | Distils one agent-trajectory `MemCell` into `[] \| [AgentCase]` (11-step pipeline) |
20
+ | `AgentSkillExtractor` | Aggregates one new `AgentCase` into incremental skill operations for a cluster; returns add / update / retire entries |
21
+
22
+ ## Quick start
23
+
24
+ ```python
25
+ import asyncio
26
+ import json
27
+
28
+ from everalgo.agent_memory.case import AgentCaseExtractor
29
+ from everalgo.llm.types import ChatResponse
30
+ from everalgo.testing.fake_llm import FakeLLMClient
31
+ from everalgo.types import AgentCase, ChatMessage, MemCell, ToolCall, ToolCallFunction, ToolCallRequest, ToolCallResult
32
+
33
+ _CASE_JSON = json.dumps({
34
+ "task_intent": "Search for Python async retry libraries",
35
+ "approach": "1. Search. 2. Filter. 3. Summarise.",
36
+ "quality_score": 0.82,
37
+ "key_insight": "Use tenacity AsyncRetrying for native async back-off.",
38
+ })
39
+
40
+ async def main() -> None:
41
+ fake = FakeLLMClient(responses=[ChatResponse(content=_CASE_JSON, model="fake")])
42
+ mc = MemCell(
43
+ items=[
44
+ ChatMessage(id="u1", role="user", content="Best async retry libs?", timestamp=1_700_000_000_000, sender_id="user"),
45
+ ToolCallRequest(tool_calls=[ToolCall(id="c1", function=ToolCallFunction(name="web.search", arguments='{}'))], timestamp=1_700_000_000_100, sender_id="assistant"),
46
+ ToolCallResult(tool_call_id="c1", content="Found: tenacity.", timestamp=1_700_000_000_200),
47
+ ToolCallRequest(tool_calls=[ToolCall(id="c2", function=ToolCallFunction(name="web.search", arguments='{}'))], timestamp=1_700_000_000_300, sender_id="assistant"),
48
+ ToolCallResult(tool_call_id="c2", content="tenacity supports async.", timestamp=1_700_000_000_400),
49
+ ChatMessage(id="a1", role="assistant", content="Use tenacity AsyncRetrying.", timestamp=1_700_000_000_500, sender_id="assistant"),
50
+ ],
51
+ timestamp=1_700_000_000_500,
52
+ )
53
+
54
+ cases: list[AgentCase] = await AgentCaseExtractor(llm=fake).aextract(mc)
55
+ if cases:
56
+ print(cases[0].task_intent)
57
+
58
+ asyncio.run(main())
59
+ ```
60
+
61
+ See [`examples/04_agent_memory_case.py`](../../examples/04_agent_memory_case.py) for the full runnable example.
62
+
63
+ ## API surface
64
+
65
+ ```python
66
+ class AgentBoundaryDetector:
67
+ def __init__(self, *, llm: LLMClient) -> None: ...
68
+ async def adetect(
69
+ self, items: list[ConversationItem], *, is_final: bool = False, prompt: str | None = None
70
+ ) -> DetectionResult: ...
71
+
72
+ class AgentCaseExtractor:
73
+ def __init__(self, *, llm: LLMClient) -> None: ...
74
+ async def aextract(
75
+ self, memcell: MemCell, *,
76
+ prompt_filter: str | None = None,
77
+ prompt_compress: str | None = None,
78
+ prompt_tool_pre_compress: str | None = None,
79
+ ) -> list[AgentCase]: ... # length 0 (filtered) or 1
80
+
81
+ class AgentSkillExtractor:
82
+ def __init__(self, *, llm: LLMClient) -> None: ...
83
+ async def aextract(
84
+ self,
85
+ case: AgentCase,
86
+ *,
87
+ existing_relevant_skills: Sequence[AgentSkill],
88
+ supporting_cases: Sequence[AgentCase],
89
+ prompt_success: str | None = None,
90
+ prompt_failure: str | None = None,
91
+ prompt_maturity: str | None = None,
92
+ skip_quality_threshold: float = 0.2,
93
+ skip_maturity_scoring: bool = True,
94
+ maturity_threshold: float = 0.6,
95
+ retire_confidence: float = 0.1,
96
+ failure_quality_threshold: float = 0.5,
97
+ max_case_history: int = 9,
98
+ max_description_tokens: int = 400,
99
+ max_content_tokens: int = 5000,
100
+ maturity_trivial_change_ratio: float = 0.2,
101
+ maturity_reeval_change_ratio: float = 0.4,
102
+ ) -> list[AgentSkill]: ...
103
+ ```
104
+
105
+ All class methods have a sync bridge: `extractor.extract(...)` is `async_to_sync(aextract)`.
106
+
107
+ ### AgentCaseExtractor pipeline
108
+
109
+ The extractor runs an 11-step pipeline: strip-before-first-user → structural pre-filter → heuristic trim → over-size bail → LLM filter (skipped when ≥2 tool rounds) → tool pre-compress → LLM compress → parse → validate → build `AgentCase`. Returns `[]` when the trajectory is filtered out; returns `[AgentCase]` on success.
110
+
111
+ ### AgentSkillExtractor return contract
112
+
113
+ Pass the new `AgentCase` and the pre-filtered `existing_relevant_skills` (e.g. top-K cosine from the caller's store) plus the associated `supporting_cases` (cases referenced by existing skills). The caller decodes add / update / retire by checking whether `skill.id` is already in `existing_relevant_skills` and whether `skill.confidence < retire_confidence`.
114
+
115
+ Cases with `quality_score < skip_quality_threshold` (default 0.2) short-circuit to `[]` without calling the LLM.
116
+
117
+ `AgentSkill.cluster_id` is always `""` on extraction — the caller stamps the cluster identity after persisting.
118
+
119
+ ### Customising prompts
120
+
121
+ ```python
122
+ import everalgo.agent_memory.prompts.case_filter as _cf
123
+ _cf.AGENT_CASE_FILTER_PROMPT = my_custom_filter_prompt # global override
124
+ ```
125
+
126
+ Or per-call: pass `prompt_filter=` / `prompt_compress=` / `prompt_tool_pre_compress=` to `aextract`.
127
+
128
+ ## Related distributions
129
+
130
+ - [`everalgo-boundary`](../everalgo-boundary/) — `detect_boundaries` primitive used by `AgentBoundaryDetector`
131
+ - [`everalgo-clustering`](../everalgo-clustering/) — `cluster_by_llm` for skill-cluster assignment
132
+ - [`everalgo-rank`](../everalgo-rank/) — `CaseRanker` / `SkillRanker` for read-time ranking
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "everalgo-agent-memory"
7
+ version = "0.1.0"
8
+ description = "EverAlgo agent memory: AgentCase / AgentSkill extractors and AgentBoundaryDetector facade."
9
+ requires-python = ">=3.12"
10
+ license = "MIT"
11
+ readme = "README.md"
12
+ authors = [{ name = "EverMind" }]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3.12",
17
+ ]
18
+ dependencies = [
19
+ "everalgo-core>=0.1.0,<2.0.0",
20
+ "everalgo-boundary>=0.1.0,<2.0.0",
21
+ "everalgo-clustering>=0.1.0,<2.0.0",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/EverMind-AI/EverAlgo"
26
+ Repository = "https://github.com/EverMind-AI/EverAlgo"
27
+ Issues = "https://github.com/EverMind-AI/EverAlgo/issues"
28
+ Documentation = "https://github.com/EverMind-AI/EverAlgo/tree/main/packages/everalgo-agent-memory"
29
+ Changelog = "https://github.com/EverMind-AI/EverAlgo/blob/main/packages/everalgo-agent-memory/CHANGELOG.md"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/everalgo"]
@@ -0,0 +1,16 @@
1
+ """Agent-side memory extractors — 2 Extractors + boundary facade."""
2
+
3
+ import logging
4
+
5
+ from everalgo.agent_memory.boundary import AgentBoundaryDetector
6
+ from everalgo.agent_memory.case import AgentCaseExtractor
7
+ from everalgo.agent_memory.skill import AgentSkillExtractor
8
+
9
+ __all__ = [
10
+ "AgentBoundaryDetector",
11
+ "AgentCaseExtractor",
12
+ "AgentSkillExtractor",
13
+ ]
14
+
15
+ # Library logging setup (ADR-013): NullHandler on each subpackage logger.
16
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -0,0 +1,52 @@
1
+ """Token-aware text utilities for agent_memory extractors (head/tail truncation, json_default)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from typing import Any
7
+
8
+ from everalgo._tokenize import _get_tokenizer, count_tokens
9
+
10
+ __all__ = ["count_tokens", "json_default", "truncate_text"]
11
+
12
+
13
+ def truncate_text(
14
+ text: str,
15
+ max_tokens: int,
16
+ *,
17
+ head_ratio: float = 0.7,
18
+ suffix: str | None = None,
19
+ ) -> str:
20
+ r"""Truncate ``text`` to ``max_tokens`` tokens, preserving head + tail with a marker.
21
+
22
+ When ``suffix`` is ``None``: keep ``head_ratio`` of tokens from the start and the remainder from
23
+ the end, joined by ``"\\n[... trimmed N tokens ...]\\n"`` (or ``"..."`` when ``head_ratio == 1.0``).
24
+ When ``suffix`` is set: keep the head only and append ``suffix``.
25
+ Short / empty inputs are returned unchanged.
26
+ """
27
+ if not text:
28
+ return text
29
+ tokenizer = _get_tokenizer()
30
+ tokens = tokenizer.encode(text)
31
+ if len(tokens) <= max_tokens:
32
+ return text
33
+
34
+ if suffix is not None:
35
+ head = tokenizer.decode(tokens[:max_tokens])
36
+ return head.rstrip() + suffix
37
+
38
+ head_count = int(max_tokens * head_ratio)
39
+ tail_count = max_tokens - head_count
40
+ head_text = tokenizer.decode(tokens[:head_count])
41
+ if tail_count <= 0:
42
+ return head_text.rstrip() + "..."
43
+ tail_text = tokenizer.decode(tokens[-tail_count:])
44
+ trimmed = len(tokens) - max_tokens
45
+ return f"{head_text}\n[... trimmed {trimmed} tokens ...]\n{tail_text}"
46
+
47
+
48
+ def json_default(obj: Any) -> Any:
49
+ """``json.dumps(default=...)`` fallback: datetime → ISO string, anything else → ``str``."""
50
+ if isinstance(obj, datetime):
51
+ return obj.isoformat()
52
+ return str(obj)
@@ -0,0 +1,129 @@
1
+ """Agent trajectory boundary detection facade.
2
+
3
+ Wraps the low-level ``everalgo.boundary.detect_boundaries`` primitive with a filter → detect → remap
4
+ pipeline to handle mixed trajectories (``ChatMessage`` + ``ToolCallRequest`` + ``ToolCallResult``).
5
+
6
+ Tool items are filtered out before the LLM detection step so the LLM sees a clean conversation
7
+ view; boundary indices from the chat-only result are then remapped back to the original
8
+ ``ConversationItem`` index space so that the output ``MemCell.items`` preserves the full
9
+ trajectory (including tool calls).
10
+
11
+ Uses explicit composition rather than inheritance: filter → detect → remap layers are separate
12
+ functions rather than an extended class hierarchy.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from typing import TYPE_CHECKING
19
+
20
+ from asgiref.sync import async_to_sync
21
+
22
+ from everalgo.boundary import DetectionResult, detect_boundaries
23
+ from everalgo.types import ChatMessage, MemCell
24
+
25
+ if TYPE_CHECKING:
26
+ from everalgo.llm.protocols import LLMClient
27
+ from everalgo.types import ConversationItem
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ __all__ = ["AgentBoundaryDetector"]
32
+
33
+
34
+ class AgentBoundaryDetector:
35
+ """Boundary detection on an agent trajectory (mixed ``ConversationItem`` list).
36
+
37
+ The filter → detect → remap pipeline:
38
+
39
+ 1. **Filter** — extract only ``ChatMessage`` items and remember their original indices.
40
+ 2. **Detect** — run ``everalgo.boundary.detect_boundaries`` on the chat-only view.
41
+ 3. **Remap** — translate chat-space cell boundaries back to the original ``ConversationItem``
42
+ index space; ``ToolCallRequest`` / ``ToolCallResult`` items between adjacent chat
43
+ messages are folded into the cell that contains the preceding chat message.
44
+
45
+ Invariant: ``sum(len(c.items) for c in result.cells) + len(result.tail) == len(items)``.
46
+
47
+ The LLM client is bound to the instance at construction time.
48
+ """
49
+
50
+ def __init__(self, *, llm: LLMClient) -> None:
51
+ self._llm = llm
52
+
53
+ async def adetect(
54
+ self,
55
+ items: list[ConversationItem],
56
+ *,
57
+ is_final: bool = False,
58
+ prompt: str | None = None,
59
+ ) -> DetectionResult:
60
+ """Detect boundaries on a mixed agent trajectory.
61
+
62
+ Args:
63
+ items: Ordered ``ConversationItem`` list for one trajectory slice — may contain
64
+ ``ChatMessage``, ``ToolCallRequest``, and ``ToolCallResult`` in any mix.
65
+ is_final: When ``True``, the caller guarantees no more items will arrive; trailing tool items
66
+ after the last cell's final chat message are folded into that cell rather than the tail.
67
+ When ``False`` (default), trailing tool items go to the tail.
68
+ prompt: Per-call boundary detection prompt override; ``None`` uses the built-in default.
69
+
70
+ Returns:
71
+ ``cells`` — remapped ``MemCell`` list preserving tool calls inside each cell.
72
+ ``tail`` — remainder items (not yet committed to a cell) for the non-final case.
73
+ """
74
+ chat_items: list[ChatMessage] = []
75
+ chat_to_orig: list[int] = []
76
+ for orig_idx, item in enumerate(items):
77
+ if isinstance(item, ChatMessage):
78
+ chat_items.append(item)
79
+ chat_to_orig.append(orig_idx)
80
+
81
+ if not chat_items:
82
+ # No chat messages → no LLM signal; everything goes to tail per should_wait semantics.
83
+ logger.debug("adetect: no ChatMessage items in trajectory (n_items=%d)", len(items))
84
+ return DetectionResult(cells=[], tail=list(items)) # type: ignore[arg-type]
85
+
86
+ chat_result = await detect_boundaries(chat_items, llm=self._llm, is_final=is_final, prompt=prompt)
87
+
88
+ remapped_cells: list[MemCell] = []
89
+ chat_cursor = 0
90
+ for cell in chat_result.cells:
91
+ cell_chat_count = len(cell.items)
92
+ start_chat_idx = chat_cursor
93
+ end_chat_idx = chat_cursor + cell_chat_count - 1
94
+ chat_cursor += cell_chat_count
95
+
96
+ start_orig = chat_to_orig[start_chat_idx]
97
+
98
+ if chat_cursor < len(chat_to_orig):
99
+ # More cells follow: include everything up to (exclusive) the next cell's first chat.
100
+ end_orig_exclusive = chat_to_orig[chat_cursor]
101
+ elif is_final:
102
+ # Last cell, caller says stream is done: absorb all remaining items.
103
+ end_orig_exclusive = len(items)
104
+ else:
105
+ # Last cell, stream is ongoing: include only up to (inclusive) the last chat in
106
+ # this cell; trailing tool items go to the tail so they can be re-processed.
107
+ end_orig_exclusive = chat_to_orig[end_chat_idx] + 1
108
+
109
+ remapped_cells.append(MemCell(items=list(items[start_orig:end_orig_exclusive]), timestamp=cell.timestamp))
110
+
111
+ # Tail: items after the last consumed original index.
112
+ if remapped_cells:
113
+ if is_final:
114
+ last_consumed_exclusive = len(items)
115
+ elif chat_cursor < len(chat_to_orig):
116
+ last_consumed_exclusive = chat_to_orig[chat_cursor]
117
+ else:
118
+ # chat_cursor == len(chat_to_orig): all chats consumed; last cell boundary already
119
+ # set to chat_to_orig[end_chat_idx] + 1, so tail starts right after.
120
+ last_consumed_exclusive = chat_to_orig[-1] + 1
121
+ tail = list(items[last_consumed_exclusive:])
122
+ else:
123
+ # detect_boundaries returned no cells — everything is tail.
124
+ tail = list(items)
125
+
126
+ return DetectionResult(cells=remapped_cells, tail=tail) # type: ignore[arg-type]
127
+
128
+ detect = async_to_sync(adetect)
129
+ """Sync bridge — only callable from non-event-loop contexts."""