zettcode 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ **/.venv/
2
+ **/__pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .coverage
8
+ **/.pytest_cache/
9
+ **/.ruff_cache/
10
+ .zett-tool-output/
11
+ .env
12
+ .DS_Store
zettcode-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chang-LeHung
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.5
2
+ Name: zettcode
3
+ Version: 0.1.0
4
+ Summary: A focused terminal coding agent powered by zett-agent
5
+ Project-URL: Repository, https://github.com/Chang-LeHung/zettcode
6
+ Project-URL: Issues, https://github.com/Chang-LeHung/zettcode/issues
7
+ Author: Chang-LeHung
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agent,coding-agent,llm,terminal,tui
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.14
16
+ Requires-Dist: pyperclip<2,>=1.9
17
+ Requires-Dist: wcwidth<1,>=0.2
18
+ Requires-Dist: zett-agent==0.1.0
19
+ Description-Content-Type: text/markdown
20
+
21
+ # ZettCode
22
+
23
+ ZettCode is a focused terminal coding agent built on `zett-agent`. Its TUI owns
24
+ raw terminal input, component layout, mouse interaction, and cell-level
25
+ differential rendering directly; it does not use prompt-toolkit or another TUI
26
+ framework.
27
+
28
+ The agent runtime is published separately as
29
+ [`zett-agent`](https://github.com/Chang-LeHung/zett-agent); this repository owns
30
+ only the terminal client.
31
+
32
+ ## Install
33
+
34
+ ZettCode needs Python 3.14 or newer and is published on PyPI as `zettcode`:
35
+
36
+ ```bash
37
+ uv tool install zettcode
38
+ # or
39
+ pip install zettcode
40
+ ```
41
+
42
+ ## Run
43
+
44
+ DeepSeek:
45
+
46
+ ```bash
47
+ export DEEPSEEK_API_KEY=...
48
+ zettcode --provider deepseek --model deepseek-chat
49
+ ```
50
+
51
+ OpenAI or an OpenAI-compatible endpoint:
52
+
53
+ ```bash
54
+ export OPENAI_API_KEY=...
55
+ zettcode --provider openai --model gpt-5-mini
56
+ zettcode --provider openai --model my-model --base-url https://example.com/v1
57
+ ```
58
+
59
+ Pass a workspace path as the final argument. It defaults to the current directory.
60
+ Session data is stored in `~/.zettcode/sessions.sqlite3` by default.
61
+
62
+ From a source checkout, run the same commands through `uv`:
63
+
64
+ ```bash
65
+ uv sync
66
+ uv run zettcode --provider deepseek --model deepseek-chat
67
+ ```
68
+
69
+ ## Keys
70
+
71
+ - `Enter`: send
72
+ - `Esc`, then `Enter`: insert a newline
73
+ - `Ctrl-C`: stop the active request, or clear the input
74
+ - `Ctrl-D`: delete the next character; exit when the input is empty and the agent is idle
75
+ - `Ctrl-A` / `Ctrl-E`, `Home` / `End`: move to the start or end of the current line
76
+ - `Ctrl-B` / `Ctrl-F`, `Left` / `Right`: move by one character
77
+ - `Alt-B` / `Alt-F`, `Ctrl-Left` / `Ctrl-Right`: move by one word
78
+ - `Ctrl-Home` / `Ctrl-End`, `Alt-<` / `Alt->`: move to the start or end of the whole input
79
+ - `Ctrl-U` / `Ctrl-K`: delete to the start or end of the current line
80
+ - `Ctrl-W` / `Alt-Backspace`, `Alt-D`: delete the previous or next word
81
+ - `Ctrl-Y`: paste the last text removed with a line or word deletion
82
+ - `Ctrl-Z` / `Ctrl-_`: undo the latest input edit
83
+ - `Up` / `Down`: move between input lines, then browse submitted input history
84
+ - `Ctrl-P` / `Ctrl-N`: browse submitted input history directly
85
+ - `Ctrl-R`: search backward through submitted input history
86
+ - `Ctrl-L`: redraw the terminal
87
+ - `Ctrl-T`: expand or collapse the latest thinking block
88
+ - `Page Up` / `Page Down`: scroll history while keeping the composer focused
89
+ - Click the transcript and use `Up` / `Down`, `Home` / `End` to browse it
90
+ - Drag in the transcript to select text; dragging past an edge automatically scrolls
91
+ - Double-click a transcript row to select the complete visible line
92
+ - `Shift`-click extends the existing selection
93
+ - Completed mouse selections are copied to the system clipboard automatically
94
+ - `Ctrl-C` also copies a transcript selection and returns focus to the input
95
+ - `Tab` / `Shift-Tab`: complete slash commands
96
+
97
+ Thinking is collapsed by default. Click a Thinking row to inspect it; moving the
98
+ pointer outside that block collapses it again. Tool calls keep their output and
99
+ completion status visible in the transcript. Active thinking and tool rows use a
100
+ low-frequency moving highlight wave with an animated activity icon. Tool results show a five-row preview by default;
101
+ click a completed tool row to expand it. Expanded output is still bounded so a
102
+ large command result cannot take over the terminal or exhaust renderer memory.
103
+
104
+ Assistant text renders common terminal-friendly Markdown: bold and emphasis,
105
+ headings, inline and fenced code, links, quotes, unordered bullet lists, and
106
+ GFM-style tables. Tables preserve left, center, and right alignment, account for
107
+ wide CJK characters, and fit or truncate columns to the current terminal width.
108
+
109
+ Use `/help` inside the application to see session commands.
@@ -0,0 +1,89 @@
1
+ # ZettCode
2
+
3
+ ZettCode is a focused terminal coding agent built on `zett-agent`. Its TUI owns
4
+ raw terminal input, component layout, mouse interaction, and cell-level
5
+ differential rendering directly; it does not use prompt-toolkit or another TUI
6
+ framework.
7
+
8
+ The agent runtime is published separately as
9
+ [`zett-agent`](https://github.com/Chang-LeHung/zett-agent); this repository owns
10
+ only the terminal client.
11
+
12
+ ## Install
13
+
14
+ ZettCode needs Python 3.14 or newer and is published on PyPI as `zettcode`:
15
+
16
+ ```bash
17
+ uv tool install zettcode
18
+ # or
19
+ pip install zettcode
20
+ ```
21
+
22
+ ## Run
23
+
24
+ DeepSeek:
25
+
26
+ ```bash
27
+ export DEEPSEEK_API_KEY=...
28
+ zettcode --provider deepseek --model deepseek-chat
29
+ ```
30
+
31
+ OpenAI or an OpenAI-compatible endpoint:
32
+
33
+ ```bash
34
+ export OPENAI_API_KEY=...
35
+ zettcode --provider openai --model gpt-5-mini
36
+ zettcode --provider openai --model my-model --base-url https://example.com/v1
37
+ ```
38
+
39
+ Pass a workspace path as the final argument. It defaults to the current directory.
40
+ Session data is stored in `~/.zettcode/sessions.sqlite3` by default.
41
+
42
+ From a source checkout, run the same commands through `uv`:
43
+
44
+ ```bash
45
+ uv sync
46
+ uv run zettcode --provider deepseek --model deepseek-chat
47
+ ```
48
+
49
+ ## Keys
50
+
51
+ - `Enter`: send
52
+ - `Esc`, then `Enter`: insert a newline
53
+ - `Ctrl-C`: stop the active request, or clear the input
54
+ - `Ctrl-D`: delete the next character; exit when the input is empty and the agent is idle
55
+ - `Ctrl-A` / `Ctrl-E`, `Home` / `End`: move to the start or end of the current line
56
+ - `Ctrl-B` / `Ctrl-F`, `Left` / `Right`: move by one character
57
+ - `Alt-B` / `Alt-F`, `Ctrl-Left` / `Ctrl-Right`: move by one word
58
+ - `Ctrl-Home` / `Ctrl-End`, `Alt-<` / `Alt->`: move to the start or end of the whole input
59
+ - `Ctrl-U` / `Ctrl-K`: delete to the start or end of the current line
60
+ - `Ctrl-W` / `Alt-Backspace`, `Alt-D`: delete the previous or next word
61
+ - `Ctrl-Y`: paste the last text removed with a line or word deletion
62
+ - `Ctrl-Z` / `Ctrl-_`: undo the latest input edit
63
+ - `Up` / `Down`: move between input lines, then browse submitted input history
64
+ - `Ctrl-P` / `Ctrl-N`: browse submitted input history directly
65
+ - `Ctrl-R`: search backward through submitted input history
66
+ - `Ctrl-L`: redraw the terminal
67
+ - `Ctrl-T`: expand or collapse the latest thinking block
68
+ - `Page Up` / `Page Down`: scroll history while keeping the composer focused
69
+ - Click the transcript and use `Up` / `Down`, `Home` / `End` to browse it
70
+ - Drag in the transcript to select text; dragging past an edge automatically scrolls
71
+ - Double-click a transcript row to select the complete visible line
72
+ - `Shift`-click extends the existing selection
73
+ - Completed mouse selections are copied to the system clipboard automatically
74
+ - `Ctrl-C` also copies a transcript selection and returns focus to the input
75
+ - `Tab` / `Shift-Tab`: complete slash commands
76
+
77
+ Thinking is collapsed by default. Click a Thinking row to inspect it; moving the
78
+ pointer outside that block collapses it again. Tool calls keep their output and
79
+ completion status visible in the transcript. Active thinking and tool rows use a
80
+ low-frequency moving highlight wave with an animated activity icon. Tool results show a five-row preview by default;
81
+ click a completed tool row to expand it. Expanded output is still bounded so a
82
+ large command result cannot take over the terminal or exhaust renderer memory.
83
+
84
+ Assistant text renders common terminal-friendly Markdown: bold and emphasis,
85
+ headings, inline and fenced code, links, quotes, unordered bullet lists, and
86
+ GFM-style tables. Tables preserve left, center, and right alignment, account for
87
+ wide CJK characters, and fit or truncate columns to the current terminal width.
88
+
89
+ Use `/help` inside the application to see session commands.
@@ -0,0 +1,66 @@
1
+ [project]
2
+ name = "zettcode"
3
+ version = "0.1.0"
4
+ description = "A focused terminal coding agent powered by zett-agent"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{name = "Chang-LeHung"}]
10
+ keywords = ["agent", "llm", "tui", "terminal", "coding-agent"]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Environment :: Console",
14
+ "Programming Language :: Python :: 3.14",
15
+ "Typing :: Typed",
16
+ ]
17
+ dependencies = [
18
+ "pyperclip>=1.9,<2",
19
+ "wcwidth>=0.2,<1",
20
+ "zett-agent==0.1.0",
21
+ ]
22
+
23
+ [project.scripts]
24
+ zettcode = "zettcode.cli:main"
25
+
26
+ [project.urls]
27
+ Repository = "https://github.com/Chang-LeHung/zettcode"
28
+ Issues = "https://github.com/Chang-LeHung/zettcode/issues"
29
+
30
+ [build-system]
31
+ requires = ["hatchling"]
32
+ build-backend = "hatchling.build"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/zettcode"]
36
+
37
+ [tool.hatch.build.targets.sdist]
38
+ include = ["/src", "/README.md", "/LICENSE", "/pyproject.toml"]
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "pytest>=8,<9",
43
+ "pytest-asyncio>=0.26,<2",
44
+ "ruff>=0.11,<1",
45
+ ]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
49
+ asyncio_mode = "auto"
50
+
51
+ [tool.ruff]
52
+ line-length = 120
53
+ target-version = "py314"
54
+ src = ["src", "tests"]
55
+
56
+ [tool.ruff.lint]
57
+ select = ["E", "F", "I", "UP", "B"]
58
+ ignore = ["E501"]
59
+
60
+ [tool.ruff.lint.isort]
61
+ known-first-party = ["zettcode"]
62
+
63
+ [tool.ruff.format]
64
+ quote-style = "double"
65
+ indent-style = "space"
66
+ line-ending = "lf"
@@ -0,0 +1,6 @@
1
+ """Terminal coding agent built on zett-agent."""
2
+
3
+ from .config import ProviderName, ZettCodeConfig
4
+ from .runtime import ZettCodeRuntime
5
+
6
+ __all__ = ["ProviderName", "ZettCodeConfig", "ZettCodeRuntime"]
@@ -0,0 +1,78 @@
1
+ """Command-line entry point for the ZettCode TUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ import os
8
+ from pathlib import Path
9
+
10
+ from zett_agent import ReasoningEffort
11
+
12
+ from .config import ProviderName, ZettCodeConfig, default_model, provider_api_key
13
+ from .runtime import ZettCodeRuntime
14
+ from .tui import ZettCodeTUI
15
+
16
+
17
+ def parse_args(argv: list[str] | None = None) -> ZettCodeConfig:
18
+ """Parse CLI arguments into a validated immutable configuration."""
19
+ default_provider = os.getenv("ZETTCODE_PROVIDER") or ProviderName.DEEPSEEK.value
20
+ parser = argparse.ArgumentParser(description="Run the ZettCode terminal coding agent")
21
+ parser.add_argument("workspace", nargs="?", type=Path, default=Path.cwd())
22
+ parser.add_argument("--provider", choices=[item.value for item in ProviderName], default=default_provider)
23
+ parser.add_argument("--model")
24
+ parser.add_argument("--api-key", help="Prefer provider environment variables to avoid shell history exposure")
25
+ parser.add_argument("--base-url")
26
+ parser.add_argument("--responses-api", action="store_true")
27
+ parser.add_argument("--session")
28
+ parser.add_argument("--database", type=Path, default=Path.home() / ".zettcode" / "sessions.sqlite3")
29
+ parser.add_argument(
30
+ "--reasoning-effort",
31
+ choices=[item.value for item in ReasoningEffort],
32
+ default=ReasoningEffort.MEDIUM.value,
33
+ )
34
+ parser.add_argument("--serial-tools", action="store_true")
35
+ parser.add_argument("--max-iterations", type=int, default=36)
36
+ parser.add_argument("--compaction-max-tokens", type=int, default=128_000)
37
+ parser.add_argument("--compaction-keep-tokens", type=int, default=32_000)
38
+ args = parser.parse_args(argv)
39
+ try:
40
+ provider = ProviderName(args.provider)
41
+ except ValueError:
42
+ parser.error(f"Unsupported provider: {args.provider!r} (check ZETTCODE_PROVIDER)")
43
+ try:
44
+ return ZettCodeConfig(
45
+ workspace=args.workspace,
46
+ provider=provider,
47
+ model=args.model or default_model(provider),
48
+ api_key=args.api_key or provider_api_key(provider),
49
+ database=args.database,
50
+ session_id=args.session,
51
+ base_url=args.base_url,
52
+ responses_api=args.responses_api,
53
+ reasoning_effort=ReasoningEffort(args.reasoning_effort),
54
+ parallel_tool_call=not args.serial_tools,
55
+ max_iterations=args.max_iterations,
56
+ compaction_max_tokens=args.compaction_max_tokens,
57
+ compaction_keep_tokens=args.compaction_keep_tokens,
58
+ )
59
+ except ValueError as error:
60
+ parser.error(str(error))
61
+
62
+
63
+ async def async_main(config: ZettCodeConfig) -> None:
64
+ """Own runtime lifecycle around the full-screen application."""
65
+ runtime = await ZettCodeRuntime.create(config)
66
+ try:
67
+ await ZettCodeTUI(runtime).run()
68
+ finally:
69
+ await runtime.aclose()
70
+
71
+
72
+ def main() -> None:
73
+ """Installed console-script entry point."""
74
+ asyncio.run(async_main(parse_args()))
75
+
76
+
77
+ if __name__ == "__main__":
78
+ main()
@@ -0,0 +1,72 @@
1
+ """Validated construction settings for the ZettCode runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from enum import StrEnum
8
+ from pathlib import Path
9
+
10
+ from zett_agent import ReasoningEffort
11
+
12
+
13
+ class ProviderName(StrEnum):
14
+ """Provider adapters exposed by the initial CLI."""
15
+
16
+ DEEPSEEK = "deepseek"
17
+ OPENAI = "openai"
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class ZettCodeConfig:
22
+ """All process-level settings needed to construct one coding runtime."""
23
+
24
+ workspace: Path
25
+ provider: ProviderName
26
+ model: str
27
+ api_key: str
28
+ database: Path
29
+ session_id: str | None = None
30
+ base_url: str | None = None
31
+ responses_api: bool = False
32
+ reasoning_effort: ReasoningEffort = ReasoningEffort.MEDIUM
33
+ parallel_tool_call: bool = True
34
+ max_iterations: int = 36
35
+ compaction_max_tokens: int = 128_000
36
+ compaction_keep_tokens: int = 32_000
37
+
38
+ def __post_init__(self) -> None:
39
+ workspace = self.workspace.expanduser().resolve()
40
+ database = self.database.expanduser().resolve()
41
+ if not workspace.is_dir():
42
+ raise ValueError(f"Workspace is not a directory: {workspace}")
43
+ if not self.model.strip():
44
+ raise ValueError("Model cannot be empty")
45
+ if not self.api_key.strip():
46
+ raise ValueError(f"Missing API key for {self.provider.value}")
47
+ if self.max_iterations < 1:
48
+ raise ValueError("max_iterations must be positive")
49
+ if self.compaction_keep_tokens < 1:
50
+ raise ValueError("compaction_keep_tokens must be positive")
51
+ if self.compaction_max_tokens <= self.compaction_keep_tokens:
52
+ raise ValueError("compaction_max_tokens must be greater than compaction_keep_tokens")
53
+ object.__setattr__(self, "workspace", workspace)
54
+ object.__setattr__(self, "database", database)
55
+
56
+
57
+ def provider_api_key(provider: ProviderName) -> str:
58
+ """Read the selected provider credential without logging it."""
59
+ match provider:
60
+ case ProviderName.DEEPSEEK:
61
+ return os.getenv("DEEPSEEK_API_KEY") or os.getenv("DEEPSEEK_API", "")
62
+ case ProviderName.OPENAI:
63
+ return os.getenv("OPENAI_API_KEY", "")
64
+
65
+
66
+ def default_model(provider: ProviderName) -> str:
67
+ """Return a conservative default model for one provider."""
68
+ match provider:
69
+ case ProviderName.DEEPSEEK:
70
+ return "deepseek-chat"
71
+ case ProviderName.OPENAI:
72
+ return "gpt-5-mini"
@@ -0,0 +1,121 @@
1
+ """Application runtime that composes AgentClient with coding extensions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import platform
7
+ import sys
8
+ from dataclasses import dataclass
9
+ from datetime import datetime
10
+
11
+ from zett_agent import (
12
+ AgentClient,
13
+ AgentRunConfig,
14
+ CodingExtension,
15
+ CompactionExtension,
16
+ DeepSeekProvider,
17
+ OpenAIProvider,
18
+ SQLiteSessionExtension,
19
+ ToolGuidelinesExtension,
20
+ create_agent,
21
+ new_uuid7,
22
+ )
23
+
24
+ from .config import ProviderName, ZettCodeConfig
25
+
26
+
27
+ def build_system_prompt(config: ZettCodeConfig) -> str:
28
+ """Build request instructions with an explicit workspace boundary."""
29
+ now = datetime.now().astimezone()
30
+ return f"""You are ZettCode, a focused coding agent working with the user in one local workspace.
31
+
32
+ Inspect relevant files before editing. Make focused changes, preserve unrelated work,
33
+ and run checks proportional to risk. Explain blockers concretely. Do not perform
34
+ destructive actions unless the user explicitly requests them.
35
+
36
+ Runtime environment:
37
+ - Workspace: {config.workspace}
38
+ - Local time: {now.isoformat(timespec="seconds")}
39
+ - Operating system: {platform.platform()}
40
+ - Architecture: {platform.machine() or "unknown"}
41
+ - Python: {platform.python_implementation()} {platform.python_version()}
42
+ - Executable: {sys.executable}
43
+ - Shell: {os.getenv("SHELL", "unknown")}
44
+
45
+ Treat the workspace as the current working directory. Prefer relative paths in tool
46
+ calls. Verify mutable facts with tools instead of assuming this startup snapshot is current."""
47
+
48
+
49
+ @dataclass(slots=True)
50
+ class ZettCodeRuntime:
51
+ """Own the model, persistence extension, AgentClient, and active session."""
52
+
53
+ config: ZettCodeConfig
54
+ client: AgentClient
55
+ persistence: SQLiteSessionExtension
56
+ model: OpenAIProvider | DeepSeekProvider
57
+ session_id: str
58
+
59
+ @classmethod
60
+ async def create(cls, config: ZettCodeConfig) -> ZettCodeRuntime:
61
+ """Create all owned resources after changing into the chosen workspace."""
62
+ os.chdir(config.workspace)
63
+ config.database.parent.mkdir(parents=True, exist_ok=True)
64
+ persistence = SQLiteSessionExtension(config.database)
65
+ recent = await persistence.list_sessions(limit=1)
66
+ session_id = config.session_id or (recent[0].session_id if recent else new_uuid7())
67
+ match config.provider:
68
+ case ProviderName.DEEPSEEK:
69
+ model = DeepSeekProvider(
70
+ config.model,
71
+ config.api_key,
72
+ base_url=config.base_url,
73
+ response=config.responses_api,
74
+ )
75
+ case ProviderName.OPENAI:
76
+ model = OpenAIProvider(
77
+ config.model,
78
+ config.api_key,
79
+ base_url=config.base_url,
80
+ response=config.responses_api,
81
+ )
82
+ try:
83
+ client = await create_agent(
84
+ model,
85
+ config=AgentRunConfig(session_id=session_id),
86
+ system_prompt=build_system_prompt(config),
87
+ extensions=[
88
+ CodingExtension(),
89
+ persistence,
90
+ ToolGuidelinesExtension(),
91
+ CompactionExtension(
92
+ model,
93
+ max_tokens=config.compaction_max_tokens,
94
+ keep_recent_tokens=config.compaction_keep_tokens,
95
+ ),
96
+ ],
97
+ reasoning_effort=config.reasoning_effort,
98
+ parallel_tool_call=config.parallel_tool_call,
99
+ max_iterations=config.max_iterations,
100
+ )
101
+ except BaseException:
102
+ await model.aclose()
103
+ await persistence.close()
104
+ raise
105
+ return cls(config, client, persistence, model, session_id)
106
+
107
+ def new_session(self) -> str:
108
+ """Switch future requests to a fresh session identity."""
109
+ self.session_id = new_uuid7()
110
+ return self.session_id
111
+
112
+ def use_session(self, session_id: str) -> None:
113
+ """Select a persisted or new explicit session for the next request."""
114
+ if not session_id.strip():
115
+ raise ValueError("Session ID cannot be empty")
116
+ self.session_id = session_id.strip()
117
+
118
+ async def aclose(self) -> None:
119
+ """Release all resources owned by this runtime."""
120
+ await self.model.aclose()
121
+ await self.persistence.close()