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.
- zettcode-0.1.0/.gitignore +12 -0
- zettcode-0.1.0/LICENSE +21 -0
- zettcode-0.1.0/PKG-INFO +109 -0
- zettcode-0.1.0/README.md +89 -0
- zettcode-0.1.0/pyproject.toml +66 -0
- zettcode-0.1.0/src/zettcode/__init__.py +6 -0
- zettcode-0.1.0/src/zettcode/cli.py +78 -0
- zettcode-0.1.0/src/zettcode/config.py +72 -0
- zettcode-0.1.0/src/zettcode/runtime.py +121 -0
- zettcode-0.1.0/src/zettcode/tui.py +924 -0
- zettcode-0.1.0/src/zettcode/tui_framework/__init__.py +29 -0
- zettcode-0.1.0/src/zettcode/tui_framework/application.py +91 -0
- zettcode-0.1.0/src/zettcode/tui_framework/components.py +821 -0
- zettcode-0.1.0/src/zettcode/tui_framework/events.py +37 -0
- zettcode-0.1.0/src/zettcode/tui_framework/input.py +163 -0
- zettcode-0.1.0/src/zettcode/tui_framework/screen.py +214 -0
- zettcode-0.1.0/src/zettcode/tui_framework/terminal.py +118 -0
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.
|
zettcode-0.1.0/PKG-INFO
ADDED
|
@@ -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.
|
zettcode-0.1.0/README.md
ADDED
|
@@ -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,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()
|