zettcode 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -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"]
zettcode/cli.py ADDED
@@ -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()
zettcode/config.py ADDED
@@ -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"
zettcode/runtime.py ADDED
@@ -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()