Skip to content

Session

pycodeloop.core.session.Session dataclass

Source code in pycodeloop/core/session.py
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
@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 a session left with a trailing tool_calls message
        that has no tool_result replies — e.g. the process was killed
        (crash, force-quit) between persisting the assistant's tool_use
        message and running the tools. Every provider rejects a tool_use
        without a matching tool_result on the next call, which would
        otherwise make the session permanently unusable."""
        if not self.messages:
            return

        last = self.messages[-1]
        if last.role != "assistant" or not last.tool_calls:
            return

        for call in last.tool_calls:
            self.add_tool_result(
                call["id"], "Cancelled — connection was lost before this tool ran."
            )

    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
28
29
30
31
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
33
34
35
36
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
25
26
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
38
39
40
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
61
62
63
64
65
66
67
68
69
70
71
72
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/core/session.py
 9
10
11
12
13
14
15
16
@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)