zettcode-server 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.
@@ -0,0 +1,292 @@
1
+ """The SDK's values as the HTTP API's own: one place that decides what crosses the wire.
2
+
3
+ A conversion lives here rather than in the endpoints so the format is written down once: an SDK field that is not
4
+ meant for a client is left out here, a field the API documents is produced here, and the event stream and the final
5
+ result are shaped by the same rules.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from .schemas import (
13
+ CompactionOut,
14
+ ContextOut,
15
+ ContextShareOut,
16
+ CurrentSessionOut,
17
+ DayTotalsOut,
18
+ MentionOut,
19
+ MessageOut,
20
+ ModelCheckOut,
21
+ ModelOut,
22
+ ModelTotalsOut,
23
+ ResultOut,
24
+ SessionOut,
25
+ SessionStatsOut,
26
+ SessionTotalsOut,
27
+ ToolOut,
28
+ TotalsOut,
29
+ UsageOut,
30
+ WorkspaceUsageOut,
31
+ )
32
+
33
+ if TYPE_CHECKING: # pragma: no cover - annotations only
34
+ from zettcode.sdk.events import AnyEvent
35
+ from zettcode.sdk.session import Session
36
+ from zettcode.sdk.types import (
37
+ Compaction,
38
+ ContextReport,
39
+ MentionInfo,
40
+ Message,
41
+ ModelCheck,
42
+ ModelInfo,
43
+ RunResult,
44
+ SessionInfo,
45
+ SessionStatsInfo,
46
+ ToolRecord,
47
+ Totals,
48
+ Usage,
49
+ WorkspaceUsage,
50
+ )
51
+
52
+
53
+ def totals_out(totals: Totals) -> TotalsOut:
54
+ """Return a tally as the API counts it, with the totals the terminal shows worked out."""
55
+ return TotalsOut(
56
+ requests=totals.requests,
57
+ input_tokens=totals.input_tokens,
58
+ output_tokens=totals.output_tokens,
59
+ cache_read_tokens=totals.cache_read_tokens,
60
+ reasoning_tokens=totals.reasoning_tokens,
61
+ total_tokens=totals.total_tokens,
62
+ cache_share=totals.cache_share,
63
+ )
64
+
65
+
66
+ def ledger_out(usage: WorkspaceUsage) -> WorkspaceUsageOut:
67
+ """Return the workspace's usage the way the ``/usage`` page lays it out."""
68
+ return WorkspaceUsageOut(
69
+ sessions=[
70
+ SessionTotalsOut(
71
+ id=session.id,
72
+ title=session.title,
73
+ started=session.started,
74
+ updated=session.updated,
75
+ tokens=totals_out(session.tokens),
76
+ models=list(session.models),
77
+ entries=session.entries,
78
+ )
79
+ for session in usage.sessions
80
+ ],
81
+ models=[ModelTotalsOut(model=item.model, tokens=totals_out(item.tokens)) for item in usage.models],
82
+ days=[DayTotalsOut(day=item.day, tokens=totals_out(item.tokens)) for item in usage.days],
83
+ total=totals_out(usage.total),
84
+ )
85
+
86
+
87
+ def stats_out(stats: SessionStatsInfo) -> SessionStatsOut:
88
+ """Return one session's stored totals, with the share the resume panel colours."""
89
+ return SessionStatsOut(
90
+ id=stats.id,
91
+ entries=stats.entries,
92
+ requests=stats.requests,
93
+ input_tokens=stats.input_tokens,
94
+ output_tokens=stats.output_tokens,
95
+ cache_read_tokens=stats.cache_read_tokens,
96
+ reasoning_tokens=stats.reasoning_tokens,
97
+ total_tokens=stats.total_tokens,
98
+ cache_share=stats.cache_share,
99
+ )
100
+
101
+
102
+ def mention_out(mention: MentionInfo) -> MentionOut:
103
+ """Return one ``@`` resource as a menu row."""
104
+ return MentionOut(token=mention.token, kind=mention.kind, name=mention.name, description=mention.description)
105
+
106
+
107
+ def check_out(check: ModelCheck) -> ModelCheckOut:
108
+ """Return one model's doctor result."""
109
+ return ModelCheckOut(
110
+ model=check.model,
111
+ model_id=check.model_id,
112
+ ok=check.ok,
113
+ seconds=check.seconds,
114
+ detail=check.detail,
115
+ input_tokens=check.input_tokens,
116
+ output_tokens=check.output_tokens,
117
+ )
118
+
119
+
120
+ def usage_out(usage: Usage) -> UsageOut:
121
+ """Return the token counts of a session or a run."""
122
+ return UsageOut(
123
+ input_tokens=usage.input_tokens,
124
+ output_tokens=usage.output_tokens,
125
+ cache_read_tokens=usage.cache_read_tokens,
126
+ cache_write_tokens=usage.cache_write_tokens,
127
+ reasoning_tokens=usage.reasoning_tokens,
128
+ seconds=usage.seconds,
129
+ requests=usage.requests,
130
+ context_tokens=usage.context_tokens,
131
+ )
132
+
133
+
134
+ def model_out(model: ModelInfo) -> ModelOut:
135
+ """Return one configured model, without its token or its address."""
136
+ return ModelOut(
137
+ name=model.name,
138
+ model=model.model,
139
+ context_window=model.context_window,
140
+ multimodal=model.multimodal,
141
+ efforts=list(model.efforts),
142
+ active=model.active,
143
+ compact_percent=model.compact_percent,
144
+ )
145
+
146
+
147
+ def session_out(info: SessionInfo, current: str | None = None) -> SessionOut:
148
+ """Return one stored session: when it changed, and whether it is the one a prompt would continue."""
149
+ return SessionOut(
150
+ id=info.id,
151
+ title=info.title,
152
+ created_at=info.created_at,
153
+ updated_at=info.updated_at,
154
+ current=info.id == current,
155
+ )
156
+
157
+
158
+ def current_session_out(session: Session) -> CurrentSessionOut:
159
+ """Return the conversation the next prompt continues."""
160
+ return CurrentSessionOut(
161
+ id=session.id,
162
+ title=session.title,
163
+ locked=session.locked,
164
+ usage=usage_out(session.usage),
165
+ )
166
+
167
+
168
+ def tool_out(tool: ToolRecord) -> ToolOut:
169
+ """Return one tool call and how it ended."""
170
+ return ToolOut(
171
+ call_id=tool.id,
172
+ name=tool.name,
173
+ arguments=dict(tool.arguments),
174
+ output=tool.output,
175
+ ok=tool.ok,
176
+ skipped=tool.skipped,
177
+ )
178
+
179
+
180
+ def message_out(message: Message) -> MessageOut:
181
+ """Return one message of a stored conversation."""
182
+ return MessageOut(
183
+ role=message.role,
184
+ text=message.text,
185
+ tool_calls=[
186
+ ToolOut(call_id=call.id, name=call.name, arguments=dict(call.arguments), output="", ok=True)
187
+ for call in message.tool_calls
188
+ ],
189
+ tool_call_id=message.tool_call_id,
190
+ tool_name=message.tool_name,
191
+ ok=message.ok,
192
+ images=message.images,
193
+ usage=None if message.usage is None else usage_out(message.usage),
194
+ seconds=message.seconds,
195
+ )
196
+
197
+
198
+ def compaction_out(compaction: Compaction | None) -> CompactionOut:
199
+ """Return what a compaction did, or that it applied nothing."""
200
+ if compaction is None:
201
+ return CompactionOut(applied=False)
202
+ return CompactionOut(
203
+ applied=True,
204
+ summary=compaction.summary,
205
+ compressed=compaction.compressed,
206
+ kept=compaction.kept,
207
+ )
208
+
209
+
210
+ def context_out(report: ContextReport) -> ContextOut:
211
+ """Return how full the context window is and what is filling it."""
212
+ return ContextOut(
213
+ window=report.window,
214
+ used=report.used,
215
+ measured_by=report.measured_by,
216
+ shares=[ContextShareOut(name=share.name, tokens=share.tokens, items=share.items) for share in report.shares],
217
+ partial=report.partial,
218
+ from_provider=report.from_provider,
219
+ )
220
+
221
+
222
+ def result_out(result: RunResult) -> ResultOut:
223
+ """Return what a run came to, with its tool calls but not its raw event log."""
224
+ return ResultOut(
225
+ text=result.text,
226
+ session_id=result.session_id,
227
+ usage=usage_out(result.usage),
228
+ tools=[tool_out(tool) for tool in result.tools],
229
+ stop=result.stop,
230
+ stop_message=result.stop_message,
231
+ questions=list(result.questions),
232
+ )
233
+
234
+
235
+ def event_payload(event: AnyEvent) -> dict[str, Any]:
236
+ """Return one event of a run as the JSON an SSE frame carries.
237
+
238
+ Every payload has a ``type``; the rest of the fields depend on it, and are documented with the stream. An event
239
+ this server does not know is sent as ``{"type": "event", "name": ...}`` rather than dropped, so a newer SDK
240
+ cannot make a stream look as if nothing happened.
241
+ """
242
+ from zettcode.sdk import events
243
+
244
+ match event:
245
+ case events.TextDelta(text=text):
246
+ return {"type": "text_delta", "text": text}
247
+ case events.ReasoningDelta(text=text):
248
+ return {"type": "reasoning_delta", "text": text}
249
+ case events.ToolStarted(call_id=call_id, name=name, arguments=arguments):
250
+ return {"type": "tool_started", "call_id": call_id, "name": name, "arguments": dict(arguments)}
251
+ case events.ToolFinished(call_id=call_id, name=name, output=output, ok=ok, skipped=skipped):
252
+ return {
253
+ "type": "tool_finished",
254
+ "call_id": call_id,
255
+ "name": name,
256
+ "output": output,
257
+ "ok": ok,
258
+ "skipped": skipped,
259
+ }
260
+ case events.ApprovalRequested(call_id=call_id, command=command):
261
+ return {"type": "approval_requested", "call_id": call_id, "command": command, "approved": event.approved}
262
+ case events.QuestionAsked(call_id=call_id, question=question, options=options, allow_multiple=allow_multiple):
263
+ return {
264
+ "type": "question_asked",
265
+ "call_id": call_id,
266
+ "question": question,
267
+ "options": list(options),
268
+ "allow_multiple": allow_multiple,
269
+ "answered": event.answered,
270
+ }
271
+ case events.UsageUpdated(usage=usage):
272
+ return {"type": "usage_updated", "usage": usage_out(usage).model_dump()}
273
+ case events.CompactionStarted():
274
+ return {"type": "compaction_started"}
275
+ case events.CompactionFinished(applied=applied, compaction=compaction):
276
+ return {
277
+ "type": "compaction_finished",
278
+ "applied": applied,
279
+ "summary": compaction.summary if compaction is not None else "",
280
+ }
281
+ case events.ModelStopped(reason=reason, message=message):
282
+ return {"type": "model_stopped", "reason": reason, "message": message}
283
+ case events.Steered(text=text):
284
+ return {"type": "steered", "text": text}
285
+ case events.Notice(text=text, level=level):
286
+ return {"type": "notice", "text": text, "level": level}
287
+ case events.Finished(text=text):
288
+ return {"type": "finished", "text": text}
289
+ case events.RawEvent():
290
+ return {"type": "raw"}
291
+ case _:
292
+ return {"type": "event", "name": type(event).__name__}
@@ -0,0 +1,58 @@
1
+ """What the server was started with: the workspace it serves and the level its runs start at.
2
+
3
+ One process serves one workspace. The SDK keeps the working directory of the process, so a second workspace in the
4
+ same process is a conflict rather than a feature; a second workspace is a second ``zettcode-server``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+
13
+ #: The permission a run starts at. Shell commands are refused, which is what an HTTP endpoint can safely assume:
14
+ #: nobody is at a terminal to answer for one, and whoever calls it names the level they want.
15
+ DEFAULT_PERMISSION = "deny"
16
+
17
+ #: The interface to bind. Loopback, so starting the server does not put a shell on the network by accident.
18
+ DEFAULT_HOST = "127.0.0.1"
19
+ DEFAULT_PORT = 3290
20
+
21
+ #: The environment variable that supplies the bearer token, when one is wanted.
22
+ TOKEN_VARIABLE = "ZETTCODE_SERVER_TOKEN"
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class Settings:
27
+ """The workspace, the socket, and how a run starts.
28
+
29
+ Attributes:
30
+ workspace: The directory the agent works in.
31
+ host: The interface to bind.
32
+ port: The port to bind.
33
+ token: The bearer token every endpoint but ``/health`` requires; ``None`` requires none. It defaults to
34
+ ``$ZETTCODE_SERVER_TOKEN``, so a token can be kept out of the command line.
35
+ permission: The shell permission level a run starts at.
36
+ model: The model to start on, by name, or ``None`` for the first configured one.
37
+ effort: The reasoning level to start on, or ``None`` for the model's own.
38
+ config_path: A configuration file to read instead of ``~/.zettcode/config.toml``.
39
+ title: The ``Server`` header, so a client can tell which server it is talking to.
40
+ log_level: What uvicorn logs; ``info`` says which requests arrived.
41
+ """
42
+
43
+ workspace: Path
44
+ host: str = DEFAULT_HOST
45
+ port: int = DEFAULT_PORT
46
+ token: str | None = field(default_factory=lambda: os.environ.get(TOKEN_VARIABLE) or None)
47
+ permission: str = DEFAULT_PERMISSION
48
+ model: str | None = None
49
+ effort: str | None = None
50
+ config_path: Path | None = None
51
+ title: str = "zettcode-server"
52
+ log_level: str = "info"
53
+
54
+ @property
55
+ def url(self) -> str:
56
+ """Return the address the server serves on, as a client writes it."""
57
+ host = f"[{self.host}]" if ":" in self.host else self.host
58
+ return f"http://{host}:{self.port}"
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.5
2
+ Name: zettcode-server
3
+ Version: 0.1.0
4
+ Summary: The ZettCode coding agent behind an HTTP API: prompts, streaming, sessions and models
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
+ Keywords: agent,api,coding-agent,http,llm
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Web Environment
12
+ Classifier: Framework :: FastAPI
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: Microsoft :: Windows :: Windows 10
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: fastapi<1,>=0.115
24
+ Requires-Dist: httpx<1,>=0.27
25
+ Requires-Dist: uvicorn<1,>=0.35
26
+ Requires-Dist: zettcode>=0.1.8
27
+ Description-Content-Type: text/markdown
28
+
29
+ # zettcode-server
30
+
31
+ The ZettCode coding agent behind an HTTP API, in the same repository as the terminal program and its SDK.
32
+
33
+ One process serves one workspace. Every endpoint drives the released `zettcode` SDK — the same tools, plugins, MCP
34
+ servers, skills, project instructions and session store as the terminal — so a conversation begun here can be reopened
35
+ in `zettcode`, and the other way round. The package holds no agent of its own.
36
+
37
+ ```bash
38
+ uv tool install zettcode-server
39
+ zettcode-server --workspace ~/projects/api
40
+ ```
41
+
42
+ ```text
43
+ zettcode-server 0.1.0 (zettcode 0.1.8) · http://127.0.0.1:3290 · workspace /home/you/projects/api · permission deny · token off
44
+ ```
45
+
46
+ It binds loopback on port **3290**, denies shell commands by default, and asks for a bearer token only when one is
47
+ configured (`--token`, `$ZETTCODE_SERVER_TOKEN`).
48
+
49
+ A machine with no model configured is still served: `/health` says `ready: false` and `POST /models` writes the
50
+ `[[models]]` entry (the same one `/config add` writes) and builds the agent, so an editor can set ZettCode up without
51
+ touching a file by hand.
52
+
53
+ ```bash
54
+ curl http://127.0.0.1:3290/health
55
+ curl -N -X POST http://127.0.0.1:3290/prompts \
56
+ -H 'content-type: application/json' \
57
+ -d '{"text": "summarize the README", "approvals": {"mode": "deny"}}'
58
+ ```
59
+
60
+ The stream carries the run's events and ends with its result. A run that needs an answer — a shell command, a
61
+ question — is suspended and answered through `POST /runs/approvals` or `POST /runs/questions`, so a client stays in
62
+ the conversation instead of guessing.
63
+
64
+ - [The guide](https://chang-lehung.github.io/zettcode/server/): install, endpoints, event stream
65
+ - [TypeScript client](../clients/typescript): `npm install zettcode-client`
66
+ - [Python SDK](https://chang-lehung.github.io/zettcode/sdk/): the same agent from Python
@@ -0,0 +1,11 @@
1
+ zettcode_server/__init__.py,sha256=LAmx23s5UvWFRobXmQIlr3c5OFGijrJqmLPrRLGKssg,1175
2
+ zettcode_server/app.py,sha256=wg7hNRzVrqRpI7J6UoShB913qE5cn8_Ybhmn_9z0ISM,28380
3
+ zettcode_server/cli.py,sha256=vfo_m6CVoa_MUu3Uji_unBMnthAqPSKFqFqtzAVPB70,4427
4
+ zettcode_server/runner.py,sha256=UkeckWB-Q-WNNhNL4hWAcc90KDcK7f9yuOJ6I4Pe8fA,20479
5
+ zettcode_server/schemas.py,sha256=hNuh1gHWBQQyBVjjlAkoPnlc14x0Hak5ESJAO1g1WIE,15753
6
+ zettcode_server/serialize.py,sha256=hHhY-qgfZWPrVdk4f0GHx8gte7vasCxIcTo6TG5DyDQ,10217
7
+ zettcode_server/settings.py,sha256=TPyytEOxq0ubyZ0bbs6xtgE8b00DYORBCVlNVUxrqL8,2493
8
+ zettcode_server-0.1.0.dist-info/METADATA,sha256=wAsORUS-EA-_pqXelh8DckbDo7qsqrYxOmkfqH_0iTM,2958
9
+ zettcode_server-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
10
+ zettcode_server-0.1.0.dist-info/entry_points.txt,sha256=zxhbq2cCa4MMP3_dQAlP_RVEWpGzjh-dse56yoIiIBo,61
11
+ zettcode_server-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ zettcode-server = zettcode_server.cli:main