diff --git a/argus/harness.py b/argus/harness.py index 7af7685..36f08da 100644 --- a/argus/harness.py +++ b/argus/harness.py @@ -122,7 +122,8 @@ def before_turn(messages): if compacted is not messages: messages[:] = compacted if self.persist: - session.append(self.session_path, messages[0]) + for message in messages: + session.append(self.session_path, message) recorded_index = len(messages) record() return messages diff --git a/argus/loop.py b/argus/loop.py index 4371e93..6c65d97 100644 --- a/argus/loop.py +++ b/argus/loop.py @@ -1,6 +1,7 @@ """The agent turn loop with explicit tool-error containment.""" from .providers import Provider, ensure_call_ids, get_provider +from .tools import redact def run( @@ -63,6 +64,7 @@ def run( result = str(tool.run(**args)) except Exception as error: result = f"ERROR: {type(error).__name__}: {error}" + result = redact(result) messages.append( { "role": "tool", diff --git a/tests/test_harness.py b/tests/test_harness.py index ce7cc63..ce7ec6f 100644 --- a/tests/test_harness.py +++ b/tests/test_harness.py @@ -106,6 +106,58 @@ def test_compaction_is_persisted_and_reloadable(self): self.assertTrue((resumed.messages[0].get("meta") or {}).get("compaction")) self.assertEqual(harness.usage, {"input": 2, "output": 2, "total": 4}) + def test_compaction_restores_retained_turns_on_resume(self): + provider = ScriptedProvider([ + reply("", [tool_call("read_file", {"path": "note.txt"}, "c1")]), + reply("", [tool_call("read_file", {"path": "note.txt"}, "c2")]), + reply("compressed facts"), + reply("finished"), + ]) + harness = Harness( + self.root, provider=provider, policy=Policy("yolo"), + budget_tokens=1, + ) + with mock.patch.object(context, "KEEP_RECENT", 3): + self.assertEqual(harness.run("long task"), "finished") + + resumed = Harness(self.root, provider=ScriptedProvider(), persist=False) + self.assertTrue(resumed.resume(harness.session_path)) + self.assertTrue((resumed.messages[0].get("meta") or {}).get("compaction")) + assistant_calls = [ + call + for message in resumed.messages + if message.get("role") == "assistant" + for call in message.get("tool_calls") or [] + ] + tool_rows = [ + message for message in resumed.messages if message.get("role") == "tool" + ] + self.assertEqual([call["id"] for call in assistant_calls], ["c2"]) + self.assertEqual([message["call_id"] for message in tool_rows], ["c2"]) + self.assertEqual(resumed.messages[-1]["text"], "finished") + + def test_use_skill_output_is_redacted_before_persistence(self): + secret = "sk-abcdefghijklmnop1234" + skill_dir = os.path.join(self.root, "skills", "demo") + os.makedirs(skill_dir) + with open(os.path.join(skill_dir, "SKILL.md"), "w", encoding="utf-8") as stream: + stream.write(f"---\ndescription: Demo skill\n---\nUse {secret}.\n") + provider = ScriptedProvider([ + reply("", [tool_call("use_skill", {"name": "demo"}, "c1")]), + reply("done"), + ]) + harness = Harness(self.root, provider=provider, policy=Policy("yolo")) + harness.run("load the demo skill") + + tool_message = next( + message for message in harness.messages if message.get("role") == "tool" + ) + self.assertIn("[REDACTED_OPENAI_KEY]", tool_message["text"]) + with open(harness.session_path, encoding="utf-8") as stream: + persisted = stream.read() + self.assertNotIn(secret, persisted) + self.assertIn("[REDACTED_OPENAI_KEY]", persisted) + def test_remember_tool_writes_and_dedupes(self): harness = Harness( self.root, provider=ScriptedProvider(), persist=False diff --git a/tests/test_loop.py b/tests/test_loop.py index ae903e1..1c4d42f 100644 --- a/tests/test_loop.py +++ b/tests/test_loop.py @@ -53,6 +53,23 @@ def test_unknown_tool_is_reported_to_model(self): loop.run("system", messages, self.tools, provider=provider) self.assertTrue(messages[2]["text"].startswith("ERROR: unknown tool")) + def test_tool_results_are_redacted_in_history_and_events(self): + secret = "sk-abcdefghijklmnop1234" + provider = ScriptedProvider([ + reply("", [tool_call("echo", {"text": secret}, "c1")]), + reply("done"), + ]) + messages = [{"role": "user", "text": "go"}] + events = [] + loop.run( + "system", messages, self.tools, provider=provider, + on_event=lambda kind, payload: events.append((kind, payload)), + ) + self.assertNotIn(secret, messages[2]["text"]) + self.assertIn("[REDACTED_OPENAI_KEY]", messages[2]["text"]) + tool_end = next(payload for kind, payload in events if kind == "tool_end") + self.assertNotIn(secret, tool_end["result"]) + def test_tool_exception_is_contained(self): provider = ScriptedProvider([ reply("", [tool_call("boom", {}, "c1")]),