Skip to content

Session

pycodeloop.core.session.Session dataclass

Source code in pycodeloop/core/session.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@dataclass
class Session:
    system_prompt: str = ""
    messages: list[Message] = field(default_factory=list)
    cwd: str = "."

    def add_user(self, text: str, images: list[str] | None = None) -> None:
        self.messages.append(Message(role="user", content=text, images=images))

    def add_assistant(
        self, text: str, tool_calls: list[dict] | None = None
    ) -> None:
        self.messages.append(
            Message(role="assistant", content=text, tool_calls=tool_calls)
        )

    def add_tool_result(self, tool_call_id: str, content: str) -> None:
        self.messages.append(
            Message(role="tool", content=content, tool_call_id=tool_call_id)
        )

    def history(self) -> list[Message]:
        self._repair_dangling_tool_calls()
        return self.messages

    def _repair_dangling_tool_calls(self) -> None:
        """Self-heals any assistant tool_calls message left without a
        matching tool_result immediately after it, anywhere in history."""
        i = 0
        while i < len(self.messages):
            msg = self.messages[i]
            if msg.role != "assistant" or not msg.tool_calls:
                i += 1
                continue

            j = i + 1
            satisfied_ids = set()
            while j < len(self.messages) and self.messages[j].role == "tool":
                satisfied_ids.add(self.messages[j].tool_call_id)
                j += 1

            missing = [
                call
                for call in msg.tool_calls
                if call["id"] not in satisfied_ids
            ]
            for offset, call in enumerate(missing):
                self.messages.insert(
                    j + offset,
                    Message(
                        role="tool",
                        content="Cancelled — connection was lost before this tool ran.",
                        tool_call_id=call["id"],
                    ),
                )
            i = j + len(missing)

    def trim(self, max_turns: int) -> None:
        """Keep only the most recent `max_turns` user-initiated turns,
        dropping older ones as a whole unit — trimming mid-turn would
        split an assistant tool_calls message from its tool_result
        replies, which every provider rejects."""
        turn_starts = [
            i for i, m in enumerate(self.messages) if m.role == "user"
        ]

        if len(turn_starts) <= max_turns:
            return

        cutoff = turn_starts[-max_turns]
        self.messages = self.messages[cutoff:]

cwd = '.' class-attribute instance-attribute

messages = field(default_factory=list) class-attribute instance-attribute

system_prompt = '' class-attribute instance-attribute

__init__(system_prompt='', messages=list(), cwd='.')

add_assistant(text, tool_calls=None)

Source code in pycodeloop/core/session.py
21
22
23
24
25
26
def add_assistant(
    self, text: str, tool_calls: list[dict] | None = None
) -> None:
    self.messages.append(
        Message(role="assistant", content=text, tool_calls=tool_calls)
    )

add_tool_result(tool_call_id, content)

Source code in pycodeloop/core/session.py
28
29
30
31
def add_tool_result(self, tool_call_id: str, content: str) -> None:
    self.messages.append(
        Message(role="tool", content=content, tool_call_id=tool_call_id)
    )

add_user(text, images=None)

Source code in pycodeloop/core/session.py
18
19
def add_user(self, text: str, images: list[str] | None = None) -> None:
    self.messages.append(Message(role="user", content=text, images=images))

history()

Source code in pycodeloop/core/session.py
33
34
35
def history(self) -> list[Message]:
    self._repair_dangling_tool_calls()
    return self.messages

trim(max_turns)

Keep only the most recent max_turns user-initiated turns, dropping older ones as a whole unit — trimming mid-turn would split an assistant tool_calls message from its tool_result replies, which every provider rejects.

Source code in pycodeloop/core/session.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def trim(self, max_turns: int) -> None:
    """Keep only the most recent `max_turns` user-initiated turns,
    dropping older ones as a whole unit — trimming mid-turn would
    split an assistant tool_calls message from its tool_result
    replies, which every provider rejects."""
    turn_starts = [
        i for i, m in enumerate(self.messages) if m.role == "user"
    ]

    if len(turn_starts) <= max_turns:
        return

    cutoff = turn_starts[-max_turns]
    self.messages = self.messages[cutoff:]

pycodeloop.core.session.Message dataclass

Source code in pycodeloop/protocol/messages.py
 7
 8
 9
10
11
12
13
14
@dataclass
class Message:
    role: str
    content: Any
    tool_call_id: str | None = None
    tool_calls: list[dict] | None = None
    images: list[str] | None = None
    """Base64-encoded PNG data, one entry per attached image."""

content instance-attribute

images = None class-attribute instance-attribute

Base64-encoded PNG data, one entry per attached image.

role instance-attribute

tool_call_id = None class-attribute instance-attribute

tool_calls = None class-attribute instance-attribute

__init__(role, content, tool_call_id=None, tool_calls=None, images=None)