-
Notifications
You must be signed in to change notification settings - Fork 1
feat(D3c): chaptered narrative (story arcs instead of FIFO trim) #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 5 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
442165d
feat(D3c-1): add Chapter model + chapters field to AgentIdentity
chiruu12 4e77dd7
feat(D3c-2): seal narrative into chapters instead of FIFO-dropping
chiruu12 0d50622
feat(D3c-3): render chapters as 'Story so far' in the preamble
chiruu12 1137502
fix(D3c): preserve full history in life summaries + strict narrative …
chiruu12 a2d19a9
feat(D3c): richer chapter summaries + unambiguous dates (Greptile rev…
chiruu12 19377e3
fix(D3c): normalize newlines in entries + defensive seal clear + trim…
chiruu12 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| """Tests for AgentIdentity chaptered narrative (D3c).""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| from hive.agents.identity import ( | ||
| MAX_CHAPTERS, | ||
| MAX_NARRATIVE, | ||
| AgentIdentity, | ||
| Chapter, | ||
| IdentityManager, | ||
| ) | ||
|
|
||
|
|
||
| class TestChapterModel: | ||
| def test_chapters_default_empty(self) -> None: | ||
| ident = AgentIdentity(agent_id="a1", display_name="Atlas") | ||
| assert ident.chapters == [] | ||
|
|
||
| def test_legacy_json_without_chapters_loads_empty(self) -> None: | ||
| """An identity serialized before chapters existed still loads.""" | ||
| legacy = '{"agent_id": "a1", "display_name": "Atlas", "narrative": "old"}' | ||
| ident = AgentIdentity.model_validate_json(legacy) | ||
| assert ident.chapters == [] | ||
| assert ident.narrative == "old" | ||
|
|
||
| def test_chapter_round_trip(self) -> None: | ||
| ident = AgentIdentity( | ||
| agent_id="a1", | ||
| display_name="Atlas", | ||
| chapters=[Chapter(index=1, summary="Ch1: 5 entries", entry_count=5)], | ||
| ) | ||
| restored = AgentIdentity.model_validate_json(ident.model_dump_json()) | ||
| assert len(restored.chapters) == 1 | ||
| assert restored.chapters[0].index == 1 | ||
| assert restored.chapters[0].entry_count == 5 | ||
|
|
||
| def test_manager_persists_chapters(self, tmp_path: Path) -> None: | ||
| idm = IdentityManager(tmp_path) | ||
| ident = AgentIdentity( | ||
| agent_id="a1", | ||
| display_name="Atlas", | ||
| chapters=[Chapter(index=1, summary="Ch1", entry_count=3)], | ||
| ) | ||
| idm.save(ident) | ||
| loaded = idm.load("a1") | ||
| assert loaded is not None | ||
| assert len(loaded.chapters) == 1 | ||
|
|
||
|
|
||
| class TestSealing: | ||
| def _idm_with_agent(self, tmp_path: Path) -> IdentityManager: | ||
| idm = IdentityManager(tmp_path) | ||
| idm.save(AgentIdentity(agent_id="a1", display_name="Atlas")) | ||
| return idm | ||
|
|
||
| def test_no_chapter_below_threshold(self, tmp_path: Path) -> None: | ||
| idm = self._idm_with_agent(tmp_path) | ||
| idm.update_narrative("a1", "small goal", "done") | ||
| ident = idm.load("a1") | ||
| assert ident is not None | ||
| assert ident.chapters == [] | ||
| assert "small goal" in ident.narrative | ||
|
|
||
| def test_overflow_seals_chapter_and_preserves_count(self, tmp_path: Path) -> None: | ||
| idm = self._idm_with_agent(tmp_path) | ||
| # Many entries; each ~40 chars, so we cross MAX_NARRATIVE (800) and seal. | ||
| for i in range(40): | ||
| idm.update_narrative("a1", f"goal number {i}", "completed ok") | ||
| ident = idm.load("a1") | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| assert ident is not None | ||
| assert len(ident.chapters) >= 1, "no chapter sealed despite overflow" | ||
| # Total entries are conserved across sealed chapters + the open narrative. | ||
| sealed = sum(c.entry_count for c in ident.chapters) | ||
| open_lines = len([ln for ln in ident.narrative.splitlines() if ln.strip()]) | ||
| assert sealed + open_lines == 40 | ||
|
|
||
| def test_open_narrative_stays_bounded(self, tmp_path: Path) -> None: | ||
| idm = self._idm_with_agent(tmp_path) | ||
| for i in range(60): | ||
| idm.update_narrative("a1", f"goal {i}", "done") | ||
| ident = idm.load("a1") | ||
| assert ident is not None | ||
| # Strict bound: the open narrative never exceeds MAX_NARRATIVE. | ||
| assert len(ident.narrative) <= MAX_NARRATIVE | ||
|
|
||
| def test_single_oversized_entry_is_bounded(self, tmp_path: Path) -> None: | ||
| """A lone entry longer than MAX_NARRATIVE must not bypass the bound.""" | ||
| idm = self._idm_with_agent(tmp_path) | ||
| idm.update_narrative("a1", "x" * (MAX_NARRATIVE * 2), "done") | ||
| ident = idm.load("a1") | ||
| assert ident is not None | ||
| assert len(ident.narrative) <= MAX_NARRATIVE | ||
|
|
||
| def test_chapter_indices_monotonic(self, tmp_path: Path) -> None: | ||
| idm = self._idm_with_agent(tmp_path) | ||
| for i in range(80): | ||
| idm.update_narrative("a1", f"goal {i}", "done") | ||
| ident = idm.load("a1") | ||
| assert ident is not None | ||
| indices = [c.index for c in ident.chapters] | ||
| assert indices == sorted(indices) | ||
| assert len(ident.chapters) <= MAX_CHAPTERS | ||
|
|
||
| def test_chapter_summary_carries_goal_text(self, tmp_path: Path) -> None: | ||
| """Summaries include goal text (theme/arc), not just a count + dates.""" | ||
| idm = self._idm_with_agent(tmp_path) | ||
| for i in range(40): | ||
| idm.update_narrative("a1", f"objective-{i}", "done") | ||
| ident = idm.load("a1") | ||
| assert ident is not None and ident.chapters | ||
| # The first sealed chapter began with objective-0. | ||
| assert "objective-0" in ident.chapters[0].summary | ||
|
|
||
|
|
||
| class TestFullNarrative: | ||
| def test_full_narrative_includes_chapters_and_open(self, tmp_path: Path) -> None: | ||
| idm = IdentityManager(tmp_path) | ||
| idm.save(AgentIdentity(agent_id="a1", display_name="Atlas")) | ||
| for i in range(40): | ||
| idm.update_narrative("a1", f"goal {i}", "done") | ||
| ident = idm.load("a1") | ||
| assert ident is not None and ident.chapters # at least one sealed chapter | ||
| full = ident.full_narrative() | ||
| # Both the sealed chapter summary and a current open line are present. | ||
| assert ident.chapters[-1].summary in full | ||
| assert ident.narrative in full | ||
|
|
||
| def test_full_narrative_empty_identity(self) -> None: | ||
| assert AgentIdentity(agent_id="a1", display_name="Atlas").full_narrative() == "" | ||
|
|
||
|
|
||
| class TestRenderPreamble: | ||
| def test_no_story_section_without_chapters(self, tmp_path: Path) -> None: | ||
| idm = IdentityManager(tmp_path) | ||
| idm.save(AgentIdentity(agent_id="a1", display_name="Atlas", narrative="[01-01] x: y")) | ||
| preamble = idm.build_preamble("a1") | ||
| assert "Story so far" not in preamble | ||
| assert "Recent history" in preamble | ||
|
|
||
| def test_chapters_render_as_story_so_far(self, tmp_path: Path) -> None: | ||
| idm = IdentityManager(tmp_path) | ||
| idm.save(AgentIdentity(agent_id="a1", display_name="Atlas")) | ||
| for i in range(40): | ||
| idm.update_narrative("a1", f"goal {i}", "done") | ||
| preamble = idm.build_preamble("a1") | ||
| assert "Story so far" in preamble | ||
| # The most recent sealed chapter's summary appears. | ||
| ident = idm.load("a1") | ||
| assert ident is not None and ident.chapters | ||
| assert ident.chapters[-1].summary in preamble | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.