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,555 @@
1
+ """The shapes the HTTP API promises, so a client has a schema and not a guess.
2
+
3
+ These are the server's own types. The SDK's values are converted into them at the edge (see
4
+ :mod:`zettcode_server.serialize`), which keeps the wire format a decision of this package: the SDK may gain a field
5
+ without changing what a client sees, and a field is only published when it is meant.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from datetime import date, datetime
11
+ from typing import Literal
12
+
13
+ from pydantic import BaseModel, ConfigDict, Field
14
+
15
+ #: The shell permission levels, as the SDK names them.
16
+ PermissionName = Literal["deny", "ask", "safe", "auto"]
17
+
18
+ #: How a run answers the shell commands the model asks to run.
19
+ ApprovalMode = Literal["deny", "auto", "commands", "ask"]
20
+
21
+ #: Whether a question the model asks waits for the client.
22
+ QuestionMode = Literal["ask", "decline"]
23
+
24
+
25
+ class ApprovalPolicy(BaseModel):
26
+ """The rule a run answers shell approvals with.
27
+
28
+ A run over HTTP is unattended unless the client stays in the conversation: the agent is suspended inside the shell
29
+ tool until the request is answered. ``deny`` refuses every command (the default), ``auto`` approves every one, and
30
+ ``commands`` approves only the commands named and refuses the rest. ``ask`` leaves each one to the client: it
31
+ arrives in the stream as ``approval_requested`` and is answered with ``POST /runs/approvals``, or denied once
32
+ ``answer_timeout`` runs out.
33
+
34
+ Attributes:
35
+ mode: Which rule applies.
36
+ commands: The commands ``commands`` approves, such as ``"git status"``. A command matches when it is one of
37
+ them or continues one with arguments; one holding a shell character (``;``, ``|``, ``&&``) never matches.
38
+ """
39
+
40
+ mode: ApprovalMode = "deny"
41
+ commands: list[str] = Field(default_factory=list)
42
+
43
+
44
+ class RunOptions(BaseModel):
45
+ """What every run answers with, whether it starts a prompt or continues an interrupted one.
46
+
47
+ Attributes:
48
+ approvals: How shell commands the model asks to run are answered.
49
+ questions: ``ask`` sends a question the model asks down the stream and waits for
50
+ ``POST /runs/questions``; ``decline`` tells the model the reader is not there.
51
+ timeout: Seconds the whole run may take before it is stopped, or ``None`` for no deadline.
52
+ answer_timeout: Seconds an ``ask`` approval or question waits for the client before the safe default is
53
+ applied (deny, decline), so a client that went away cannot hang the run.
54
+ keep_events: Include every event in the final result as well, as the SDK's ``keep_events`` does.
55
+ """
56
+
57
+ approvals: ApprovalPolicy = Field(default_factory=ApprovalPolicy)
58
+ questions: QuestionMode = "decline"
59
+ timeout: float | None = Field(default=None, gt=0)
60
+ answer_timeout: float = Field(default=300.0, gt=0, le=3600)
61
+ keep_events: bool = False
62
+
63
+
64
+ class ImageIn(BaseModel):
65
+ """An image sent with a prompt, base64 encoded.
66
+
67
+ Attributes:
68
+ data: The file's bytes, base64 encoded (no data-URL prefix).
69
+ media_type: ``image/png``, ``image/jpeg``, ``image/gif`` or ``image/webp``.
70
+ """
71
+
72
+ data: str = Field(min_length=1)
73
+ media_type: str = Field(min_length=1)
74
+
75
+
76
+ class PromptRequest(RunOptions):
77
+ """One prompt, streamed as it runs.
78
+
79
+ Attributes:
80
+ text: What to ask the agent; ``@path`` references are resolved when ``mentions`` is on.
81
+ images: Pictures for a model that takes them.
82
+ mentions: Resolve ``@`` references in the text, as the terminal does.
83
+ """
84
+
85
+ text: str = Field(min_length=1)
86
+ images: list[ImageIn] = Field(default_factory=list)
87
+ mentions: bool = True
88
+
89
+
90
+ class ResumeRequest(RunOptions):
91
+ """Continue the request that was interrupted; nothing is asked, so only the answers are named."""
92
+
93
+
94
+ class HealthOut(BaseModel):
95
+ """What ``/health`` answers: the process is up, and what it has to work with."""
96
+
97
+ status: Literal["ok"] = "ok"
98
+ version: str
99
+ workspace: str
100
+ #: Whether a model is configured, so a prompt can run. A fresh machine is ``false`` until ``POST /models``.
101
+ ready: bool = False
102
+ session: str | None = None
103
+ models: int = 0
104
+ busy: bool
105
+
106
+
107
+ class UsageOut(BaseModel):
108
+ """Token counts, for the session or for one run."""
109
+
110
+ input_tokens: int = 0
111
+ output_tokens: int = 0
112
+ cache_read_tokens: int = 0
113
+ cache_write_tokens: int = 0
114
+ reasoning_tokens: int = 0
115
+ seconds: float = 0.0
116
+ requests: int = 0
117
+ context_tokens: int = 0
118
+
119
+ @property
120
+ def total_tokens(self) -> int:
121
+ """Return input plus output, as the SDK's ``Usage.total_tokens``."""
122
+ return self.input_tokens + self.output_tokens
123
+
124
+
125
+ class ModelOut(BaseModel):
126
+ """One configured model."""
127
+
128
+ name: str
129
+ model: str
130
+ context_window: int
131
+ multimodal: bool
132
+ efforts: list[str]
133
+ active: bool = False
134
+ compact_percent: float = 80.0
135
+
136
+
137
+ class ModelsOut(BaseModel):
138
+ """Every configured model, and which one a run would use."""
139
+
140
+ models: list[ModelOut]
141
+ current: str | None = None
142
+
143
+
144
+ class SettingsOut(BaseModel):
145
+ """The model, reasoning effort and permission a run starts with."""
146
+
147
+ model: ModelOut
148
+ effort: str
149
+ efforts: list[str]
150
+ permission: PermissionName
151
+ permissions: list[str]
152
+ unsaved: str | None = None
153
+
154
+
155
+ class SettingsUpdate(BaseModel):
156
+ """A change to what a run starts with; every field left out stays as it is."""
157
+
158
+ model: str | None = None
159
+ effort: str | None = None
160
+ permission: PermissionName | None = None
161
+ compact_percent: float | None = Field(default=None, gt=0, le=100)
162
+
163
+
164
+ class ModelCreate(BaseModel):
165
+ """One model to write into the configuration file.
166
+
167
+ The keys are the file's own, so what is added here is what ``/config add`` writes and what the terminal reads.
168
+ A token may be left out when the environment names one (``OPENAI_API_KEY``, ``ANTHROPIC_API_KEY``).
169
+ """
170
+
171
+ model_config = ConfigDict(extra="forbid")
172
+
173
+ model: str = Field(min_length=1)
174
+ token: str | None = None
175
+ display_model: str | None = None
176
+ base_url: str | None = None
177
+ anthropic: bool = False
178
+ responses_api: bool = False
179
+ multimodal: bool = False
180
+ context_window: int | None = Field(default=None, ge=1)
181
+ compact_percent: float | None = Field(default=None, gt=0, le=100)
182
+ reasoning_efforts: list[str] = Field(default_factory=list)
183
+
184
+ def entry(self) -> dict[str, object]:
185
+ """Return the file keys to write, leaving out what was not given."""
186
+ data = self.model_dump(exclude_none=True)
187
+ for flag in ("anthropic", "responses_api", "multimodal"):
188
+ if not data.get(flag):
189
+ data.pop(flag, None)
190
+ if not data.get("reasoning_efforts"):
191
+ data.pop("reasoning_efforts", None)
192
+ return data
193
+
194
+
195
+ class ModelUpdate(BaseModel):
196
+ """Changes to one configured model; a key left out stays as it is, and the token is kept."""
197
+
198
+ model_config = ConfigDict(extra="forbid")
199
+
200
+ model: str | None = Field(default=None, min_length=1)
201
+ token: str | None = None
202
+ display_model: str | None = None
203
+ base_url: str | None = None
204
+ anthropic: bool | None = None
205
+ responses_api: bool | None = None
206
+ multimodal: bool | None = None
207
+ context_window: int | None = Field(default=None, ge=1)
208
+ compact_percent: float | None = Field(default=None, gt=0, le=100)
209
+ reasoning_efforts: list[str] | None = None
210
+
211
+ def changes(self) -> dict[str, object]:
212
+ """Return only the keys the request set, as the file writes them."""
213
+ return self.model_dump(exclude_unset=True, exclude_none=True)
214
+
215
+
216
+ class SessionOut(BaseModel):
217
+ """One stored session, as a list shows it."""
218
+
219
+ id: str
220
+ title: str | None = None
221
+ created_at: datetime | None = None
222
+ updated_at: datetime | None = None
223
+ current: bool = False
224
+
225
+
226
+ class SessionsOut(BaseModel):
227
+ """The stored sessions, most recently active first."""
228
+
229
+ sessions: list[SessionOut]
230
+
231
+
232
+ class CurrentSessionOut(BaseModel):
233
+ """The conversation the next prompt continues."""
234
+
235
+ id: str
236
+ title: str | None = None
237
+ locked: bool = False
238
+ usage: UsageOut
239
+
240
+
241
+ class SessionCreate(BaseModel):
242
+ """The body of a new session: a name for it, or none until one is written."""
243
+
244
+ title: str | None = Field(default=None, max_length=500)
245
+
246
+
247
+ class RenameRequest(BaseModel):
248
+ """A new name for a session."""
249
+
250
+ title: str = Field(min_length=1, max_length=500)
251
+
252
+
253
+ class ToolOut(BaseModel):
254
+ """One tool call and how it ended."""
255
+
256
+ call_id: str
257
+ name: str
258
+ arguments: dict[str, object] = Field(default_factory=dict)
259
+ output: str = ""
260
+ ok: bool = True
261
+ skipped: bool = False
262
+
263
+
264
+ class MessageOut(BaseModel):
265
+ """One message of a stored conversation."""
266
+
267
+ role: str
268
+ text: str
269
+ tool_calls: list[ToolOut] = Field(default_factory=list)
270
+ tool_call_id: str | None = None
271
+ tool_name: str | None = None
272
+ ok: bool = True
273
+ images: int = 0
274
+ usage: UsageOut | None = None
275
+ seconds: float = 0.0
276
+
277
+
278
+ class HistoryOut(BaseModel):
279
+ """A stored conversation, oldest message first."""
280
+
281
+ messages: list[MessageOut]
282
+
283
+
284
+ class CompactionOut(BaseModel):
285
+ """A compaction: how much was folded into the summary, and how much was kept."""
286
+
287
+ applied: bool
288
+ summary: str = ""
289
+ compressed: int = 0
290
+ kept: int = 0
291
+
292
+
293
+ class ContextShareOut(BaseModel):
294
+ """One source filling the context window."""
295
+
296
+ name: str
297
+ tokens: int
298
+ items: int = 0
299
+
300
+
301
+ class ContextOut(BaseModel):
302
+ """How full the context window is, and what is filling it."""
303
+
304
+ window: int
305
+ used: int
306
+ measured_by: str
307
+ shares: list[ContextShareOut] = Field(default_factory=list)
308
+ partial: bool = False
309
+ from_provider: bool = False
310
+
311
+
312
+ class ResultOut(BaseModel):
313
+ """What a finished run came to."""
314
+
315
+ text: str
316
+ session_id: str
317
+ usage: UsageOut
318
+ tools: list[ToolOut] = Field(default_factory=list)
319
+ stop: str | None = None
320
+ stop_message: str | None = None
321
+ questions: list[str] = Field(default_factory=list)
322
+
323
+
324
+ class CancelledOut(BaseModel):
325
+ """Whether there was a run to stop."""
326
+
327
+ cancelled: bool
328
+
329
+
330
+ class ApprovalAnswer(BaseModel):
331
+ """The client's answer to one shell approval the run is waiting on.
332
+
333
+ Attributes:
334
+ call_id: The ``call_id`` of the ``approval_requested`` frame.
335
+ approve: Run the command, or refuse it.
336
+ remember: Let the same kind of command run unasked for the rest of the run.
337
+ """
338
+
339
+ call_id: str = Field(min_length=1)
340
+ approve: bool = Field(strict=True)
341
+ remember: bool = Field(strict=True, default=False)
342
+
343
+
344
+ class QuestionAnswer(BaseModel):
345
+ """The client's answer to one question the model asked.
346
+
347
+ Attributes:
348
+ call_id: The ``call_id`` of the ``question_asked`` frame.
349
+ answer: The text to send the model; ``null`` declines the question.
350
+ """
351
+
352
+ call_id: str = Field(min_length=1)
353
+ answer: str | None = None
354
+
355
+
356
+ class SteerIn(BaseModel):
357
+ """A message for the run in progress."""
358
+
359
+ text: str = Field(min_length=1)
360
+
361
+
362
+ class SteeredOut(BaseModel):
363
+ """Whether a running request took the message; ``false`` when nothing was running."""
364
+
365
+ steered: bool
366
+
367
+
368
+ class PendingOut(BaseModel):
369
+ """The approval or question a run is waiting on, so a reconnecting client can answer it."""
370
+
371
+ kind: Literal["approval", "question"]
372
+ call_id: str
373
+ command: str | None = None
374
+ timeout_seconds: int | None = None
375
+ remember_supported: bool = False
376
+ question: str | None = None
377
+ options: list[str] = Field(default_factory=list)
378
+ allow_multiple: bool = False
379
+
380
+
381
+ class RunStateOut(BaseModel):
382
+ """Whether a run is going, and what it waits on if it is."""
383
+
384
+ running: bool
385
+ pending: PendingOut | None = None
386
+
387
+
388
+ class ResolvedOut(BaseModel):
389
+ """Whether an answer reached the request it named."""
390
+
391
+ resolved: bool
392
+
393
+
394
+ class ResumableOut(BaseModel):
395
+ """Where the conversation was interrupted, and what continuing would do."""
396
+
397
+ resumable: bool
398
+ kind: str | None = None
399
+ description: str | None = None
400
+
401
+
402
+ class SideQuestionRequest(BaseModel):
403
+ """A question answered on the side: it never joins the conversation.
404
+
405
+ Attributes:
406
+ question: What to ask.
407
+ timeout: Seconds to wait for the answer, or ``None`` for no deadline.
408
+ """
409
+
410
+ question: str = Field(min_length=1)
411
+ timeout: float | None = Field(default=None, gt=0)
412
+
413
+
414
+ class AnswerOut(BaseModel):
415
+ """The answer to a side question."""
416
+
417
+ answer: str
418
+
419
+
420
+ class PluginsOut(BaseModel):
421
+ """The plugins this server loaded, and the ones that refused to load."""
422
+
423
+ loaded: list[str]
424
+ failures: list[str]
425
+
426
+
427
+ class MentionOut(BaseModel):
428
+ """One ``@`` resource a prompt can carry, as a completion menu lists it."""
429
+
430
+ token: str
431
+ kind: str
432
+ name: str
433
+ description: str
434
+
435
+
436
+ class MentionsOut(BaseModel):
437
+ """The ``@`` resources matching what was typed."""
438
+
439
+ mentions: list[MentionOut]
440
+
441
+
442
+ class TotalsOut(BaseModel):
443
+ """Token counters added up over some replies, as the usage ledger records them."""
444
+
445
+ requests: int = 0
446
+ input_tokens: int = 0
447
+ output_tokens: int = 0
448
+ cache_read_tokens: int = 0
449
+ reasoning_tokens: int = 0
450
+ total_tokens: int = 0
451
+ cache_share: float | None = None
452
+
453
+
454
+ class SessionTotalsOut(BaseModel):
455
+ """One session in the workspace's usage."""
456
+
457
+ id: str
458
+ title: str | None = None
459
+ started: datetime
460
+ updated: datetime
461
+ tokens: TotalsOut
462
+ models: list[str] = Field(default_factory=list)
463
+ entries: int = 0
464
+
465
+
466
+ class ModelTotalsOut(BaseModel):
467
+ """What one model was charged across the workspace."""
468
+
469
+ model: str
470
+ tokens: TotalsOut
471
+
472
+
473
+ class DayTotalsOut(BaseModel):
474
+ """What one day spent across the workspace."""
475
+
476
+ day: date
477
+ tokens: TotalsOut
478
+
479
+
480
+ class WorkspaceUsageOut(BaseModel):
481
+ """What the workspace's stored sessions have spent, as the terminal's ``/usage`` page shows it."""
482
+
483
+ sessions: list[SessionTotalsOut]
484
+ models: list[ModelTotalsOut]
485
+ days: list[DayTotalsOut]
486
+ total: TotalsOut
487
+
488
+
489
+ class SessionStatsOut(BaseModel):
490
+ """The totals one row of the terminal's ``/resume`` panel shows."""
491
+
492
+ id: str
493
+ entries: int = 0
494
+ requests: int = 0
495
+ input_tokens: int = 0
496
+ output_tokens: int = 0
497
+ cache_read_tokens: int = 0
498
+ reasoning_tokens: int = 0
499
+ total_tokens: int = 0
500
+ cache_share: float | None = None
501
+
502
+
503
+ class SessionStatsListOut(BaseModel):
504
+ """The stored totals of the sessions asked for."""
505
+
506
+ stats: list[SessionStatsOut]
507
+
508
+
509
+ class SelfCompactionOut(BaseModel):
510
+ """Whether the model may compact this session itself, and the session's own choice."""
511
+
512
+ choice: bool | None = None
513
+ effective: bool
514
+
515
+
516
+ class SelfCompactionUpdate(BaseModel):
517
+ """A choice about self-compaction: ``true``, ``false``, or ``null`` to follow the configuration."""
518
+
519
+ choice: bool | None = Field(default=None, strict=True)
520
+
521
+
522
+ class ModelCheckOut(BaseModel):
523
+ """One model asked one short question by ``/doctor``."""
524
+
525
+ model: str
526
+ model_id: str
527
+ ok: bool
528
+ seconds: float
529
+ detail: str = ""
530
+ input_tokens: int | None = None
531
+ output_tokens: int | None = None
532
+
533
+
534
+ class DoctorOut(BaseModel):
535
+ """Every configured model, and whether it answers."""
536
+
537
+ checks: list[ModelCheckOut]
538
+
539
+
540
+ class DoctorRequest(BaseModel):
541
+ """How long each model may take to answer."""
542
+
543
+ timeout: float = Field(default=30.0, gt=0, le=300)
544
+
545
+
546
+ class ErrorOut(BaseModel):
547
+ """A failure, in a shape a client can branch on.
548
+
549
+ Attributes:
550
+ error: A stable code: ``not_found``, ``busy``, ``locked``, ``invalid``, ``timeout``, ``run``, ``internal``.
551
+ message: The sentence the SDK gave, which is the one the terminal would show.
552
+ """
553
+
554
+ error: str
555
+ message: str