From 9275c6977adac59655309f8f60b192c005be180c Mon Sep 17 00:00:00 2001 From: ColonistOne Date: Tue, 7 Apr 2026 18:26:36 +0100 Subject: [PATCH] Replace dry-run log noise with formatted summary --dry-run now collects all planned actions during the heartbeat and prints a clean summary at the end instead of scattered log lines. Shows counts, action types, targets, and content previews. Example output: ============================================================ DRY RUN SUMMARY ============================================================ Would take 4 actions: 2 upvotes, 1 comment, 1 DM reply + UPVOTE: AI research update - DOWNVOTE: Buy tokens now # COMMENT: Distributed systems paper This is a great analysis of consensus algorithms... @ DM_REPLY: to alice Hey alice, good question about CRDTs... ============================================================ Co-Authored-By: Claude Opus 4.6 (1M context) --- colony_agent/agent.py | 68 ++++++++++++++++++++++++---- tests/test_agent.py | 100 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 9 deletions(-) diff --git a/colony_agent/agent.py b/colony_agent/agent.py index 34d9fe1..bf32199 100644 --- a/colony_agent/agent.py +++ b/colony_agent/agent.py @@ -109,6 +109,7 @@ def run(self) -> None: def heartbeat(self) -> None: """Run one heartbeat cycle: introduce, check DMs, browse, engage.""" log.info("Heartbeat starting.") + self._dry_run_actions: list[tuple[str, str, str]] = [] # First run: introduce yourself if self.config.behavior.introduce_on_first_run and not self.state.introduced: @@ -130,6 +131,10 @@ def heartbeat(self) -> None: if self.memory.needs_trim(): self._trim_memory() + # Print dry-run summary + if self.dry_run and self._dry_run_actions: + self._print_dry_run_summary() + def run_once(self) -> None: """Run a single heartbeat, then exit. Useful for cron jobs.""" try: @@ -163,7 +168,7 @@ def _introduce(self) -> None: return if self.dry_run: - log.info(f"[dry-run] Would post introduction: {title}") + self._dry_run_actions.append(("introduce", title, body[:200])) return result = retry_api_call( @@ -230,7 +235,7 @@ def _check_dms(self) -> None: continue if self.dry_run: - log.info(f"[dry-run] Would reply to DM from {other}") + self._dry_run_actions.append(("dm_reply", f"to {other}", reply[:200])) continue result = retry_api_call(self.client.send_message, other, reply) @@ -335,9 +340,7 @@ def _browse_and_engage(self) -> None: if vote_value != 0: direction = "upvote" if vote_value == 1 else "downvote" if self.dry_run: - log.info( - f"[dry-run] Would {direction}: {title[:60]}" - ) + self._dry_run_actions.append((direction, title[:60], "")) else: vote_result = retry_api_call( self.client.vote_post, post_id, vote_value @@ -352,9 +355,7 @@ def _browse_and_engage(self) -> None: comment = self._extract_comment(response) if comment: if self.dry_run: - log.info( - f"[dry-run] Would comment on: {title[:60]}" - ) + self._dry_run_actions.append(("comment", title[:60], comment[:200])) else: comment_result = retry_api_call( self.client.create_comment, post_id, comment @@ -414,7 +415,7 @@ def _check_replies_to_own_post(self, post: dict) -> None: continue if self.dry_run: - log.info(f"[dry-run] Would reply to {c_author} on '{title[:40]}'") + self._dry_run_actions.append(("reply", f"{c_author} on '{title[:40]}'", reply[:200])) continue comment_result = retry_api_call( @@ -529,6 +530,55 @@ def _trim_memory(self) -> None: self.memory._messages = self.memory._messages[-keep:] log.warning("LLM failed to summarize — kept recent messages only.") + # ── Dry-run summary ──────────────────────────────────────────── + + def _print_dry_run_summary(self) -> None: + """Print a formatted summary of what the agent would have done.""" + actions = self._dry_run_actions + counts: dict[str, int] = {} + for action_type, _, _ in actions: + counts[action_type] = counts.get(action_type, 0) + 1 + + print("\n" + "=" * 60) + print(" DRY RUN SUMMARY") + print("=" * 60) + + # Counts line + parts = [] + for label, key in [ + ("upvote", "upvote"), + ("downvote", "downvote"), + ("comment", "comment"), + ("reply", "reply"), + ("DM reply", "dm_reply"), + ("introduction", "introduce"), + ]: + count = counts.get(key, 0) + if count: + parts.append(f"{count} {label}{'s' if count != 1 else ''}") + if parts: + print(f"\n Would take {len(actions)} actions: {', '.join(parts)}") + print() + + # Detailed actions + for action_type, target, content in actions: + icon = { + "upvote": "+", + "downvote": "-", + "comment": "#", + "reply": ">", + "dm_reply": "@", + "introduce": "*", + }.get(action_type, "?") + + print(f" {icon} {action_type.upper()}: {target}") + if content: + # Indent content, wrap long lines + for line in content.split("\n")[:3]: + print(f" {line[:100]}") + + print("\n" + "=" * 60 + "\n") + # ── Helpers ────────────────────────────────────────────────────── @staticmethod diff --git a/tests/test_agent.py b/tests/test_agent.py index 5b9dc94..d89f7b2 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -390,6 +390,106 @@ def test_dry_run_no_api_calls(self, mock_chat, tmp_path): agent.client.create_comment.assert_not_called() +class TestDryRunSummary: + @patch("colony_agent.agent.chat", return_value="VOTE: UPVOTE\nCOMMENT: Great insight.") + def test_prints_summary(self, mock_chat, tmp_path, capsys): + config = make_config(tmp_path) + agent = make_agent(config) + agent.dry_run = True + agent.client.get_me.return_value = {"username": "testbot"} + agent.client.get_posts.return_value = { + "posts": [ + { + "id": "p1", "title": "AI research update", + "body": "New findings.", "author": {"username": "alice"}, + }, + { + "id": "p2", "title": "Spam post", + "body": "Buy now.", "author": {"username": "bob"}, + }, + ] + } + agent.heartbeat() + output = capsys.readouterr().out + assert "DRY RUN SUMMARY" in output + assert "upvote" in output.lower() + assert "comment" in output.lower() + + @patch("colony_agent.agent.chat", return_value="SKIP") + def test_no_summary_when_no_actions(self, mock_chat, tmp_path, capsys): + config = make_config(tmp_path) + agent = make_agent(config) + agent.dry_run = True + agent.client.get_me.return_value = {"username": "testbot"} + agent.client.get_posts.return_value = { + "posts": [ + { + "id": "p1", "title": "Meh", + "body": "Nothing.", "author": {"username": "alice"}, + } + ] + } + agent.heartbeat() + output = capsys.readouterr().out + assert "DRY RUN SUMMARY" not in output + + @patch("colony_agent.agent.chat", return_value="VOTE: UPVOTE\nCOMMENT: Interesting work.") + def test_summary_shows_content(self, mock_chat, tmp_path, capsys): + config = make_config(tmp_path) + agent = make_agent(config) + agent.dry_run = True + agent.client.get_me.return_value = {"username": "testbot"} + agent.client.get_posts.return_value = { + "posts": [ + { + "id": "p1", "title": "Distributed systems paper", + "body": "Analysis.", "author": {"username": "alice"}, + } + ] + } + agent.heartbeat() + output = capsys.readouterr().out + assert "Interesting work" in output + assert "Distributed systems" in output + + @patch("colony_agent.agent.chat", return_value="Hello, I am TestBot!") + def test_summary_includes_introduction(self, mock_chat, tmp_path, capsys): + config = make_config( + tmp_path, + behavior=BehaviorConfig(introduce_on_first_run=True, reply_to_dms=False), + ) + agent = make_agent(config) + agent.dry_run = True + agent.client.get_posts.return_value = {"posts": []} + agent.heartbeat() + output = capsys.readouterr().out + assert "INTRODUCE" in output + + @patch("colony_agent.agent.chat", return_value="Hey alice, good question!") + def test_summary_includes_dm_reply(self, mock_chat, tmp_path, capsys): + config = make_config( + tmp_path, + behavior=BehaviorConfig(reply_to_dms=True, introduce_on_first_run=False), + ) + agent = make_agent(config) + agent.dry_run = True + agent.client.get_unread_count.return_value = {"unread_count": 1} + agent.client._raw_request.return_value = [ + {"other_user": {"username": "alice"}} + ] + agent.client.get_conversation.return_value = { + "messages": [ + {"sender": {"username": "alice"}, "body": "Hello!", "is_read": False} + ] + } + agent.client.get_me.return_value = {"username": "testbot"} + agent.client.get_posts.return_value = {"posts": []} + agent.heartbeat() + output = capsys.readouterr().out + assert "DM_REPLY" in output + assert "alice" in output + + class TestRepliestoOwnPosts: @patch("colony_agent.agent.chat", return_value="Thanks for the feedback, alice!") def test_replies_to_comment_on_own_post(self, mock_chat, agent):