Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions src/acp/contrib/tool_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import uuid
from collections.abc import Callable, Sequence
from copy import deepcopy
from typing import Any, cast

from pydantic import BaseModel, ConfigDict
Expand Down Expand Up @@ -86,8 +87,8 @@ def __init__(
self.status = status
self.content = _copy_model_list(content)
self.locations = _copy_model_list(locations)
self.raw_input = raw_input
self.raw_output = raw_output
self.raw_input = deepcopy(raw_input)
self.raw_output = deepcopy(raw_output)
Comment on lines +90 to +91

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO only keeping these two lines is enough, in other occurrences they are either not harmful or rarely touched.

self._stream_buffer: str | None = None

def to_view(self) -> TrackedToolCallView:
Expand Down
40 changes: 40 additions & 0 deletions tests/contrib/test_contrib_tool_calls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import pytest

from acp.contrib.tool_calls import ToolCallTracker
from acp.schema import ContentToolCallContent, TextContentBlock, ToolCallProgress

Expand Down Expand Up @@ -33,3 +35,41 @@ def test_tool_call_tracker_streaming_text_updates_content():
assert isinstance(second_content, ContentToolCallContent)
assert isinstance(second_content.content, TextContentBlock)
assert second_content.content.text == "hello, world"


@pytest.mark.parametrize("field", ["raw_input", "raw_output"])
def test_tracker_start_snapshots_incoming_raw_payloads(field: str) -> None:
tracker = ToolCallTracker()
payload = {"nested": ["original"]}
notification = tracker.start(
"external",
title="Run",
raw_input=payload if field == "raw_input" else None,
raw_output=payload if field == "raw_output" else None,
)

payload["nested"].append("caller mutation")

assert getattr(tracker.view("external"), field) == {"nested": ["original"]}
assert getattr(notification, field) == {"nested": ["original"]}


@pytest.mark.parametrize("payload", [None, False, 0, "text", ["item"]])
def test_tracker_preserves_json_raw_payload_values(payload: object) -> None:
tracker = ToolCallTracker()
tracker.start("external", title="Run", raw_input=payload, raw_output=payload)

assert tracker.view("external").raw_input == payload
assert tracker.tool_call_model("external").raw_output == payload


def test_tracker_progress_can_clear_raw_payloads() -> None:
tracker = ToolCallTracker()
tracker.start("external", title="Run", raw_input={"old": True}, raw_output={"old": True})

progress = tracker.progress("external", raw_input=None, raw_output=None)

assert progress.raw_input is None
assert progress.raw_output is None
assert tracker.view("external").raw_input is None
assert tracker.view("external").raw_output is None