pinecall-protocol 0.3.0__tar.gz → 0.6.14__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.
Files changed (26) hide show
  1. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/PKG-INFO +1 -1
  2. pinecall_protocol-0.6.14/pinecall_protocol/_version.py +4 -0
  3. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/commands.py +32 -3
  4. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/defs.py +31 -10
  5. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/events.py +119 -22
  6. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/fixtures/__init__.py +1 -1
  7. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/fixtures/call-log-golden.state.json +1 -0
  8. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/registry.py +46 -0
  9. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/rest.py +440 -3
  10. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/state.py +13 -1
  11. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/verbs.py +4 -3
  12. pinecall_protocol-0.3.0/pinecall_protocol/_version.py +0 -4
  13. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/.gitignore +0 -0
  14. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/.python-version +0 -0
  15. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/LICENSE +0 -0
  16. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/README.md +0 -0
  17. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/__init__.py +0 -0
  18. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/_base.py +0 -0
  19. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/codec.py +0 -0
  20. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/envelope.py +0 -0
  21. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/fixtures/call-log-golden.json +0 -0
  22. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/metrics.py +0 -0
  23. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/py.typed +0 -0
  24. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pinecall_protocol/room.py +0 -0
  25. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/pyproject.toml +0 -0
  26. {pinecall_protocol-0.3.0 → pinecall_protocol-0.6.14}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pinecall-protocol
3
- Version: 0.3.0
3
+ Version: 0.6.14
4
4
  Summary: The Pinecall wire: pydantic models generated from the protocol schema
5
5
  Author-email: Bernardo Castro <me@bernardocastro.dev>
6
6
  License-Expression: Apache-2.0
@@ -0,0 +1,4 @@
1
+ """Package version; hatch reads it at build time."""
2
+
3
+ # 0.0.0 means "unreleased". Set by the maintainer; never bumped from code.
4
+ __version__ = "0.6.14"
@@ -17,12 +17,17 @@ class AgentConfigure(WireModel):
17
17
  config: AgentConfig
18
18
 
19
19
 
20
+ class AgentDrain(WireModel):
21
+ """This socket is leaving, and its calls go on without it."""
22
+
23
+
20
24
  # The gateway answers agent.registered, or error.
21
25
  class AgentRegister(WireModel):
22
26
  """The app's first message: this socket speaks for this agent and answers these doors."""
23
27
 
24
28
  routes: list[Route]
25
29
  sdk: str | None = None
30
+ host: str | None = None
26
31
  takes_unclaimed: bool = True
27
32
 
28
33
 
@@ -45,6 +50,30 @@ class AgentSay(WireModel):
45
50
  allow_interruptions: bool | None = None
46
51
 
47
52
 
53
+ # Ask for a person without sending the caller anywhere: the call waits on hold until a supervisor
54
+ # takes the line, or until wait_s passes with nobody taking it. attention.answered says which.
55
+ class CallAttention(WireModel):
56
+ """Ask for a person without sending the caller anywhere."""
57
+
58
+ reason: str
59
+ wait_s: float
60
+
61
+
62
+ # Lands as callback.requested with via agent; placing the call is the app's.
63
+ class CallCallback(WireModel):
64
+ """Write down that the caller wants to be called back."""
65
+
66
+ number: str
67
+ when: str | None = None
68
+ note: str | None = None
69
+
70
+
71
+ class CallClaim(WireModel):
72
+ """The code the caller said, to bind this call to the page that shows it."""
73
+
74
+ code: str = Field(pattern="^[0-9]{4}$")
75
+
76
+
48
77
  # The new call's log opens with call.dialing; call.started follows when the far end answers.
49
78
  class CallDial(WireModel):
50
79
  """Place an outbound call as this agent."""
@@ -94,12 +123,12 @@ class CallMute(WireModel):
94
123
  """Mute the agent: it keeps listening and thinking, produces no audio."""
95
124
 
96
125
 
97
- # call.transferred says whether it worked.
126
+ # call.transferred says whether it worked, and which mode it was.
98
127
  class CallTransfer(WireModel):
99
- """Send the caller to another number."""
128
+ """Send the caller to another number, or bring that number into the call."""
100
129
 
101
130
  to: str
102
- mode: TransferMode
131
+ mode: TransferMode | None = None
103
132
 
104
133
 
105
134
  class CallUnhold(WireModel):
@@ -25,16 +25,17 @@ type Env = Literal["production", "sandbox"]
25
25
 
26
26
 
27
27
  # What a console may ask of the process standing in the agent's directory, relayed by the gateway: a
28
- # written call to the class mounted there (chat), a simulated caller from its personas, its goldens
29
- # and a suite of them, its knowledge folder pushed or its golden asked, its memory goldens, a call
30
- # promoted to a candidate file, the drift of the last two windows, and the reproductions a broken
31
- # run left on that disk. Everything else a console needs is a door of the gateway.
28
+ # written call to the class mounted there (chat), a simulated caller put on the class it holds, its
29
+ # goldens and a suite of them, its knowledge folder pushed or its golden asked, its memory goldens,
30
+ # the panel it draws beside a conversation (view), a call promoted to a candidate file, the drift of
31
+ # the last two windows, and the reproductions a broken run left on that disk. Everything else a
32
+ # console needs is a door of the gateway.
32
33
  type DevVerb = Literal[
33
34
  "chat.roster",
34
35
  "chat.start",
35
36
  "chat.say",
36
37
  "chat.end",
37
- "simulate.roster",
38
+ "view.render",
38
39
  "simulate.start",
39
40
  "goldens.roster",
40
41
  "goldens.run",
@@ -81,8 +82,9 @@ type EndedBy = Literal["caller", "agent", "supervisor", "platform"]
81
82
  type ScoreVerdict = Literal["held", "broken", "deferred", "skipped"]
82
83
 
83
84
 
84
- # Cold: the caller is sent on and the agent leaves. Warm: the agent stays on the line until the
85
- # other side answers, then leaves.
85
+ # Cold: the caller is sent on with a REFER on their SIP leg and the call ends here. Warm: the number
86
+ # is dialled into the call's own room, the agent stays on the line until the other side answers and
87
+ # then falls silent; the call ends when either of them hangs up.
86
88
  type TransferMode = Literal["cold", "warm"]
87
89
 
88
90
 
@@ -235,6 +237,7 @@ class DocSource(WireModel):
235
237
  """One chunk of the knowledge base that retrieval put in front of the model for this turn."""
236
238
 
237
239
  id: str
240
+ base: str | None = None
238
241
  path: str
239
242
  heading: str | None = None
240
243
  score: float
@@ -310,6 +313,8 @@ class TurnConfig(WireModel):
310
313
 
311
314
  min_interruption_words: int | None = None
312
315
  endpointing_ms: int | None = None
316
+ eot_threshold: float | None = None
317
+ eager_eot_threshold: float | None = None
313
318
 
314
319
 
315
320
  # How the voice says one word it would otherwise get wrong: a proper name, a brand, a street.
@@ -328,6 +333,16 @@ class StateFieldSpec(WireModel):
328
333
  visibility: Visibility
329
334
 
330
335
 
336
+ # The panel the agent draws beside a conversation: it is declared here so a console knows the agent
337
+ # has one before it asks for it, and draws its own about the contact when it has not. What the panel
338
+ # CONTAINS never comes this way — it is asked for a conversation at a time, through the view.render
339
+ # dev verb.
340
+ class ViewSpec(WireModel):
341
+ """The panel the agent draws beside a conversation."""
342
+
343
+ name: str
344
+
345
+
331
346
  # An event nobody declared is refused before it touches the log.
332
347
  class EventSpec(WireModel):
333
348
  """One outside event the agent accepts, and from whom."""
@@ -337,7 +352,7 @@ class EventSpec(WireModel):
337
352
 
338
353
 
339
354
  class KnowledgeFile(WireModel):
340
- """One file of knowledge, sent whole: its path as the tenant keeps it, and its text."""
355
+ """One file of a base, as a push sends it: its path as the tenant keeps it, and its text."""
341
356
 
342
357
  path: str
343
358
  text: str
@@ -384,8 +399,11 @@ class MemoryConfig(WireModel):
384
399
  forget: list[str] = Field(default_factory=list[str])
385
400
 
386
401
 
387
- # What an app declares about its agent: the voice, the models, the language, the greeting, the
388
- # tools, and who may see and send what. Every field is optional so a configure can change one thing.
402
+ # What an app declares about its agent: the prompt's layout, the language, the tools, whether it
403
+ # searches its bases itself, and who may see and send what. Every field is optional so a configure
404
+ # can change one thing. The environment — voice, models, greeting, hangup, turn, says, hears,
405
+ # knowledge, docs, memory — is the world's, set in the agent's settings, and no longer read off this
406
+ # declaration.
389
407
  class AgentConfig(WireModel):
390
408
  """What an app declares about its agent."""
391
409
 
@@ -402,6 +420,9 @@ class AgentConfig(WireModel):
402
420
  docs: DocsConfig | None = None
403
421
  memory: MemoryConfig | None = None
404
422
  hangup: HangupConfig | None = None
423
+ record: bool = True
405
424
  tools: list[ToolSpec] | None = None
425
+ uses_knowledge: bool = False
406
426
  state_fields: list[StateFieldSpec] | None = None
427
+ view: ViewSpec | None = None
407
428
  events: list[EventSpec] | None = None
@@ -17,6 +17,7 @@ from pinecall_protocol.defs import (
17
17
  EndReason,
18
18
  Env,
19
19
  MemoryOp,
20
+ Projection,
20
21
  Route,
21
22
  ScoreVerdict,
22
23
  Supervisor,
@@ -42,6 +43,15 @@ class AgentDetached(WireModel):
42
43
  left: bool
43
44
 
44
45
 
46
+ class AgentDraining(WireModel):
47
+ """One socket let go of its calls without cutting them."""
48
+
49
+ app: str
50
+ env: Env
51
+ handed: int
52
+ parked: int
53
+
54
+
45
55
  # The gateway accepted an agent.register: this socket now speaks for the agent and answers its
46
56
  # routes. Many sockets may hold one agent at once — a new call takes the newest of them, unless the
47
57
  # caller names one by its `app` id.
@@ -74,6 +84,59 @@ class AgentTranscript(WireModel):
74
84
  end: float | None = None
75
85
 
76
86
 
87
+ # An ask for a person settled: a supervisor took the line, or the wait ran out and the agent has the
88
+ # caller back.
89
+ class AttentionAnswered(WireModel):
90
+ """An ask for a person settled."""
91
+
92
+ ok: bool
93
+ by: Supervisor | None
94
+ error: str | None = None
95
+
96
+
97
+ # The agent asked for a person: the caller is on hold and waits for a supervisor to take the line.
98
+ # What a supervisor's console and phone are notified by.
99
+ class AttentionRequested(WireModel):
100
+ """The agent asked for a person."""
101
+
102
+ reason: str
103
+ wait_s: float
104
+
105
+
106
+ # Everything the agent says and hears comes after this.
107
+ class CallStarted(WireModel):
108
+ """Media is up: the caller and the agent can hear each other, or the text session is open."""
109
+
110
+ channel: Channel
111
+ direction: Direction
112
+ from_: str = Field(alias="from")
113
+ to: str
114
+ run: str | None = None
115
+ persona: str | None = None
116
+ accepts_when: str | None = None
117
+ declines_when: str | None = None
118
+ caller: Contact | None
119
+ started_at: float
120
+ env: Env | None = None
121
+
122
+
123
+ class CallAttached(WireModel):
124
+ """A call changed hands without ending."""
125
+
126
+ app: str
127
+ started: CallStarted
128
+ state: dict[str, Any]
129
+ seq: int
130
+ claimed: str | None = None
131
+
132
+
133
+ class CallClaimed(WireModel):
134
+ """The call was bound to a page's code."""
135
+
136
+ code: str
137
+ via: Literal["keypad", "agent"]
138
+
139
+
77
140
  # The first entry of an outbound call's log.
78
141
  class CallDialing(WireModel):
79
142
  """The platform is placing an outbound call and the far end has not answered yet."""
@@ -150,20 +213,6 @@ class CallScore(WireModel):
150
213
  judge_cost_eur: float | None = None
151
214
 
152
215
 
153
- # Everything the agent says and hears comes after this.
154
- class CallStarted(WireModel):
155
- """Media is up: the caller and the agent can hear each other, or the text session is open."""
156
-
157
- channel: Channel
158
- direction: Direction
159
- from_: str = Field(alias="from")
160
- to: str
161
- run: str | None = None
162
- caller: Contact | None
163
- started_at: float
164
- env: Env | None = None
165
-
166
-
167
216
  # Written after call.ended, once memory and pricing are done; call.score follows it and seals the
168
217
  # log.
169
218
  class CallSummary(WireModel):
@@ -182,24 +231,42 @@ class CallTransferred(WireModel):
182
231
  """A transfer asked for by the agent or a supervisor finished, one way or the other."""
183
232
 
184
233
  to: str
185
- mode: TransferMode
234
+ mode: TransferMode | None = None
186
235
  ok: bool
187
236
  error: str | None = None
188
237
 
189
238
 
190
- # Somebody asked to be called back because no seat was free: a phone caller the overflow agent
191
- # answered, or a web visitor who left a number at the widget. Written into the agent's own log; the
192
- # tenant's app reads it and places the call.
239
+ # Somebody asked to be called back: a phone caller the overflow agent answered, a web visitor who
240
+ # left a number at the widget, or a caller who asked the agent for one (call.callback). Written into
241
+ # the agent's own log; the tenant's app reads it and places the call.
193
242
  class CallbackRequested(WireModel):
194
- """Somebody asked to be called back because no seat was free."""
243
+ """Somebody asked to be called back."""
195
244
 
196
245
  channel: Channel
197
246
  number: str
198
- via: Literal["overflow", "widget"]
247
+ via: Literal["overflow", "widget", "agent"]
199
248
  call: str | None
249
+ when: str | None = None
250
+ note: str | None = None
200
251
  contact: Contact | None
201
252
 
202
253
 
254
+ class CodeClaimed(WireModel):
255
+ """One code, taken or expired."""
256
+
257
+ code: str
258
+ call: str | None
259
+
260
+
261
+ class CodeIssued(WireModel):
262
+ """One code, waiting for the call that keys it."""
263
+
264
+ code: str
265
+ env: Env
266
+ expires_at: float
267
+ log: Projection
268
+
269
+
203
270
  # The tool does not run; the model is told.
204
271
  class ConfirmDeclined(WireModel):
205
272
  """The caller did not say yes, or the request lapsed."""
@@ -235,9 +302,10 @@ class ConfirmRequest(WireModel):
235
302
  ttl_s: int
236
303
 
237
304
 
305
+ # The gateway refused a call, a written turn or a register because one of the org's quotas ran out.
238
306
  # Written into the agent's own log, which is the org's, before the door says no.
239
307
  class CreditsExhausted(WireModel):
240
- """The gateway refused a call or a register because one of the org's quotas ran out."""
308
+ """CreditsExhausted, as protocol/schema declares it."""
241
309
 
242
310
  org: str
243
311
  quota: Literal[
@@ -249,6 +317,7 @@ class CreditsExhausted(WireModel):
249
317
  "knowledge_chunks",
250
318
  "numbers",
251
319
  "seats",
320
+ "llm_tokens",
252
321
  ]
253
322
  used: float
254
323
  limit: int
@@ -280,6 +349,13 @@ class DocsSources(WireModel):
280
349
  speech_id: str | None = None
281
350
 
282
351
 
352
+ class DtmfReceived(WireModel):
353
+ """One tone the caller keyed."""
354
+
355
+ digit: Literal["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"]
356
+ code: int
357
+
358
+
283
359
  # Inside a call it says what failed; outside a call it says which command the gateway refused.
284
360
  class ErrorEvent(WireModel):
285
361
  """Something went wrong."""
@@ -328,6 +404,27 @@ class MemoryOps(WireModel):
328
404
  speech_id: str | None = None
329
405
 
330
406
 
407
+ class MessageTaken(WireModel):
408
+ """One waiting message, off the queue."""
409
+
410
+ message_id: str
411
+ call: str | None
412
+
413
+
414
+ class MessageWaiting(WireModel):
415
+ """One message, kept until somebody can answer it."""
416
+
417
+ channel: Channel
418
+ env: Env
419
+ number: str
420
+ phone_number_id: str
421
+ from_: str = Field(alias="from")
422
+ name: str | None
423
+ message_id: str
424
+ text: str
425
+ received_at: float
426
+
427
+
331
428
  # Ephemeral: it proves the socket is alive and says nothing else.
332
429
  class Pong(WireModel):
333
430
  """The answer to ping."""
@@ -408,7 +505,7 @@ class SupervisorTransferred(WireModel):
408
505
 
409
506
  by: Supervisor
410
507
  to: str
411
- mode: TransferMode
508
+ mode: TransferMode | None = None
412
509
 
413
510
 
414
511
  class SupervisorWhispered(WireModel):
@@ -1,4 +1,4 @@
1
- """The golden call log and the state it reduces to: every consumer proves the same thing."""
1
+ """Paths to the golden call log and the state it reduces to, shared by every language's tests."""
2
2
 
3
3
  from importlib.resources import files
4
4
  from pathlib import Path
@@ -30,6 +30,7 @@
30
30
  "held": false,
31
31
  "muted": false,
32
32
  "transfer": null,
33
+ "attention": null,
33
34
  "usage": [{"input_audio_tokens": 0, "input_cache_creation_tokens": 562, "input_cached_audio_tokens": 0, "input_cached_image_tokens": 0, "input_cached_text_tokens": 0, "input_cached_tokens": 13145, "input_image_tokens": 0, "input_text_tokens": 0, "input_tokens": 14963, "model": "claude-haiku-4-5", "output_audio_tokens": 0, "output_reasoning_tokens": 0, "output_text_tokens": 0, "output_tokens": 281, "provider": "anthropic", "session_duration": 0.0, "type": "llm_usage"}, {"audio_duration": 25.6, "characters_count": 397, "input_tokens": 0, "model": "eleven_flash_v2_5", "output_tokens": 0, "provider": "elevenlabs", "type": "tts_usage"}, {"audio_duration": 12.1, "input_tokens": 0, "model": "stt-rt-v5", "output_tokens": 0, "provider": "soniox", "type": "stt_usage"}, {"model": "adaptive", "provider": "livekit", "total_requests": 12, "type": "interruption_usage"}, {"model": "turn-detector-v1", "provider": "livekit", "total_requests": 5, "type": "eot_usage"}],
34
35
  "cost": {"eur": 0.022937, "rate": {"as_of": "2026-08-12", "currency": "EUR", "usd_to_eur": 0.92}, "rows": [{"eur": 0.001156, "model": "claude-haiku-4-5", "provider": "anthropic", "quantity": 1256.0, "unit": "input_tokens", "unit_price_usd": 1e-06}, {"eur": 0.001209, "model": "claude-haiku-4-5", "provider": "anthropic", "quantity": 13145.0, "unit": "cached_input_tokens", "unit_price_usd": 1e-07}, {"eur": 0.000646, "model": "claude-haiku-4-5", "provider": "anthropic", "quantity": 562.0, "unit": "cache_creation_tokens", "unit_price_usd": 1.25e-06}, {"eur": 0.001293, "model": "claude-haiku-4-5", "provider": "anthropic", "quantity": 281.0, "unit": "output_tokens", "unit_price_usd": 5e-06}, {"eur": 0.018262, "model": "eleven_flash_v2_5", "provider": "elevenlabs", "quantity": 397.0, "unit": "characters", "unit_price_usd": 5e-05}, {"eur": 0.000371, "model": "stt-rt-v5", "provider": "soniox", "quantity": 12.1, "unit": "audio_seconds", "unit_price_usd": 3.33e-05}], "unpriced": [{"model": "adaptive", "provider": "livekit"}, {"model": "turn-detector-v1", "provider": "livekit"}]},
35
36
  "routes": [],
@@ -5,9 +5,13 @@ from typing import Literal
5
5
  from pinecall_protocol._base import WireModel
6
6
  from pinecall_protocol.commands import (
7
7
  AgentConfigure,
8
+ AgentDrain,
8
9
  AgentRegister,
9
10
  AgentReply,
10
11
  AgentSay,
12
+ CallAttention,
13
+ CallCallback,
14
+ CallClaim,
11
15
  CallDial,
12
16
  CallDtmf,
13
17
  CallEvent,
@@ -34,11 +38,16 @@ from pinecall_protocol.defs import ToolResult
34
38
  from pinecall_protocol.events import (
35
39
  AgentConfigured,
36
40
  AgentDetached,
41
+ AgentDraining,
37
42
  AgentRegistered,
38
43
  AgentStateChanged,
39
44
  AgentTranscript,
40
45
  AgentTurnEnded,
46
+ AttentionAnswered,
47
+ AttentionRequested,
48
+ CallAttached,
41
49
  CallbackRequested,
50
+ CallClaimed,
42
51
  CallDialing,
43
52
  CallEnded,
44
53
  CallLine,
@@ -47,6 +56,8 @@ from pinecall_protocol.events import (
47
56
  CallStarted,
48
57
  CallSummary,
49
58
  CallTransferred,
59
+ CodeClaimed,
60
+ CodeIssued,
50
61
  ConfirmDeclined,
51
62
  ConfirmGranted,
52
63
  ConfirmRequest,
@@ -54,11 +65,14 @@ from pinecall_protocol.events import (
54
65
  Custom,
55
66
  DevRequest,
56
67
  DocsSources,
68
+ DtmfReceived,
57
69
  ErrorEvent,
58
70
  FleetFull,
59
71
  LogCaughtUp,
60
72
  LogGap,
61
73
  MemoryOps,
74
+ MessageTaken,
75
+ MessageWaiting,
62
76
  Pong,
63
77
  PromptChanged,
64
78
  StateChanged,
@@ -100,9 +114,14 @@ from pinecall_protocol.room import (
100
114
  EVENTS: dict[str, type[WireModel]] = {
101
115
  "agent.configured": AgentConfigured,
102
116
  "agent.detached": AgentDetached,
117
+ "agent.draining": AgentDraining,
103
118
  "agent.registered": AgentRegistered,
104
119
  "agent.state": AgentStateChanged,
105
120
  "agent.transcript": AgentTranscript,
121
+ "attention.answered": AttentionAnswered,
122
+ "attention.requested": AttentionRequested,
123
+ "call.attached": CallAttached,
124
+ "call.claimed": CallClaimed,
106
125
  "call.dialing": CallDialing,
107
126
  "call.ended": CallEnded,
108
127
  "call.line": CallLine,
@@ -112,6 +131,8 @@ EVENTS: dict[str, type[WireModel]] = {
112
131
  "call.summary": CallSummary,
113
132
  "call.transferred": CallTransferred,
114
133
  "callback.requested": CallbackRequested,
134
+ "code.claimed": CodeClaimed,
135
+ "code.issued": CodeIssued,
115
136
  "confirm.declined": ConfirmDeclined,
116
137
  "confirm.granted": ConfirmGranted,
117
138
  "confirm.request": ConfirmRequest,
@@ -119,12 +140,15 @@ EVENTS: dict[str, type[WireModel]] = {
119
140
  "custom": Custom,
120
141
  "dev.request": DevRequest,
121
142
  "docs.sources": DocsSources,
143
+ "dtmf.received": DtmfReceived,
122
144
  "error": ErrorEvent,
123
145
  "event.received": EventReceived,
124
146
  "fleet.full": FleetFull,
125
147
  "log.caught_up": LogCaughtUp,
126
148
  "log.gap": LogGap,
127
149
  "memory.ops": MemoryOps,
150
+ "message.taken": MessageTaken,
151
+ "message.waiting": MessageWaiting,
128
152
  "metrics.avatar": AvatarMetrics,
129
153
  "metrics.eot": EOTInferenceMetrics,
130
154
  "metrics.eou": EOUMetrics,
@@ -162,9 +186,14 @@ EVENTS: dict[str, type[WireModel]] = {
162
186
  type EventType = Literal[
163
187
  "agent.configured",
164
188
  "agent.detached",
189
+ "agent.draining",
165
190
  "agent.registered",
166
191
  "agent.state",
167
192
  "agent.transcript",
193
+ "attention.answered",
194
+ "attention.requested",
195
+ "call.attached",
196
+ "call.claimed",
168
197
  "call.dialing",
169
198
  "call.ended",
170
199
  "call.line",
@@ -174,6 +203,8 @@ type EventType = Literal[
174
203
  "call.summary",
175
204
  "call.transferred",
176
205
  "callback.requested",
206
+ "code.claimed",
207
+ "code.issued",
177
208
  "confirm.declined",
178
209
  "confirm.granted",
179
210
  "confirm.request",
@@ -181,12 +212,15 @@ type EventType = Literal[
181
212
  "custom",
182
213
  "dev.request",
183
214
  "docs.sources",
215
+ "dtmf.received",
184
216
  "error",
185
217
  "event.received",
186
218
  "fleet.full",
187
219
  "log.caught_up",
188
220
  "log.gap",
189
221
  "memory.ops",
222
+ "message.taken",
223
+ "message.waiting",
190
224
  "metrics.avatar",
191
225
  "metrics.eot",
192
226
  "metrics.eou",
@@ -246,9 +280,13 @@ TERMINAL_EVENT: str = "call.score"
246
280
  # Every command by its wire type; codec looks the model up here.
247
281
  COMMANDS: dict[str, type[WireModel]] = {
248
282
  "agent.configure": AgentConfigure,
283
+ "agent.drain": AgentDrain,
249
284
  "agent.register": AgentRegister,
250
285
  "agent.reply": AgentReply,
251
286
  "agent.say": AgentSay,
287
+ "call.attention": CallAttention,
288
+ "call.callback": CallCallback,
289
+ "call.claim": CallClaim,
252
290
  "call.dial": CallDial,
253
291
  "call.dtmf": CallDtmf,
254
292
  "call.event": CallEvent,
@@ -275,9 +313,13 @@ COMMANDS: dict[str, type[WireModel]] = {
275
313
 
276
314
  type CommandType = Literal[
277
315
  "agent.configure",
316
+ "agent.drain",
278
317
  "agent.register",
279
318
  "agent.reply",
280
319
  "agent.say",
320
+ "call.attention",
321
+ "call.callback",
322
+ "call.claim",
281
323
  "call.dial",
282
324
  "call.dtmf",
283
325
  "call.event",
@@ -306,9 +348,13 @@ type CommandType = Literal[
306
348
  # Which events a command lands in the log as, so a caller knows what to wait for.
307
349
  PRODUCES: dict[str, tuple[str, ...]] = {
308
350
  "agent.configure": ("agent.configured",),
351
+ "agent.drain": ("agent.draining",),
309
352
  "agent.register": ("agent.registered",),
310
353
  "agent.reply": ("turn.agent",),
311
354
  "agent.say": ("turn.agent",),
355
+ "call.attention": ("attention.requested", "call.line", "attention.answered"),
356
+ "call.callback": ("callback.requested",),
357
+ "call.claim": ("call.claimed",),
312
358
  "call.dial": ("call.dialing",),
313
359
  "call.dtmf": (),
314
360
  "call.event": ("event.received",),
@@ -10,13 +10,19 @@ from pinecall_protocol.defs import (
10
10
  Contact,
11
11
  Cost,
12
12
  Direction,
13
+ DocsConfig,
13
14
  EndReason,
14
15
  Env,
16
+ GreetingConfig,
17
+ HangupConfig,
15
18
  KnowledgeFile,
19
+ MemoryConfig,
16
20
  PlatformTool,
21
+ Pronunciation,
22
+ TurnConfig,
17
23
  )
18
24
  from pinecall_protocol.envelope import Entry
19
- from pinecall_protocol.state import CallStatus, State
25
+ from pinecall_protocol.state import AttentionState, CallStatus, State
20
26
 
21
27
 
22
28
  class CallState(WireModel):
@@ -72,6 +78,15 @@ class SessionLine(WireModel):
72
78
  cost: Cost | None
73
79
  score: SessionScore | None = None
74
80
  flags: list[SessionFlag] | None = None
81
+ attention: AttentionState | None = None
82
+
83
+
84
+ # The operator's stream, every org of the box at once.
85
+ class BoxEvent(WireModel):
86
+ """One frame of GET /v1/ops/events: an entry of some org's floor, and whose floor it is."""
87
+
88
+ org: str
89
+ entry: Entry
75
90
 
76
91
 
77
92
  # GET /v1/agents/{slug}/sessions and GET /v1/sessions: the calls that match, newest first, a page at
@@ -132,6 +147,29 @@ class Insights(WireModel):
132
147
  budget: InsightsBudget
133
148
 
134
149
 
150
+ class Limit(WireModel):
151
+ """One quota: the limit the org was given, and how much of it is used."""
152
+
153
+ limit: int | None
154
+ used: float
155
+
156
+
157
+ # GET /v1/limits: what the key's org may use and has used on this instance, where it bought more,
158
+ # and which of the box's vendor keys it runs on. Any key of the org.
159
+ class Limits(WireModel):
160
+ """GET /v1/limits."""
161
+
162
+ minutes: Limit
163
+ messages: Limit
164
+ llm_tokens: Limit
165
+ concurrent_calls: Limit
166
+ agents: Limit
167
+ seats: Limit
168
+ numbers: Limit
169
+ lends: list[str] | None
170
+ billing_url: str | None
171
+
172
+
135
173
  # GET and PUT /v1/org/judging: whether the org's calls are judged at hang-up, and what judging one
136
174
  # may spend on a model.
137
175
  class Judging(WireModel):
@@ -229,6 +267,11 @@ class AgentMemory(WireModel):
229
267
  next: str | None
230
268
 
231
269
 
270
+ # auto: the visitor's own light or dark, as the browser says. light and dark: that one, whatever the
271
+ # browser says.
272
+ type WidgetTheme = Literal["auto", "light", "dark"]
273
+
274
+
232
275
  # GET and PUT /v1/agents/{slug}/widget: how the widget presents this agent, kept per org, world and
233
276
  # agent. PUT takes the whole set.
234
277
  class WidgetSettings(WireModel):
@@ -239,6 +282,7 @@ class WidgetSettings(WireModel):
239
282
  greeting: str | None
240
283
  accent: str | None
241
284
  autostart: bool
285
+ theme: WidgetTheme | None = None
242
286
 
243
287
 
244
288
  # GET and PUT /v1/org/sso: the OpenID Connect provider this org's people sign in at, and never the
@@ -412,6 +456,38 @@ class AgentList(WireModel):
412
456
  agents: list[HeldAgent]
413
457
 
414
458
 
459
+ # One app connected to the gateway right now: one process on one machine, holding one or more agents
460
+ # in one world.
461
+ class AppProcess(WireModel):
462
+ """One app connected to the gateway right now."""
463
+
464
+ app: str
465
+ agents: list[str]
466
+ env: Env
467
+ host: str | None
468
+ address: str | None
469
+ sdk: str | None
470
+ holder: LineHolder | None
471
+ connected_at: float
472
+
473
+
474
+ # GET /v1/apps: every app connected in the request's world that this key may see — its own corner's
475
+ # and the org's, every corner's with `team`.
476
+ class AppList(WireModel):
477
+ """GET /v1/apps."""
478
+
479
+ apps: list[AppProcess]
480
+
481
+
482
+ # POST /v1/apps/{app}/stop, the answer: the socket was closed with the stop code, and the app exits
483
+ # rather than reconnect.
484
+ class AppStopped(WireModel):
485
+ """POST /v1/apps/{app}/stop, the answer."""
486
+
487
+ app: str
488
+ stopped: bool
489
+
490
+
415
491
  # GET /v1/agents/{slug}/line: whose terminal a call that RINGS at this agent's doors lands in. An
416
492
  # org shares one sandbox number, so it rings in one place and which one is claimed.
417
493
  class TheLine(WireModel):
@@ -643,8 +719,9 @@ class ExtractionRun(WireModel):
643
719
 
644
720
 
645
721
  # POST /v1/calls/{call}/lookup, the body: which platform tool to run for this turn, and what to run
646
- # it with. Worker-only; the gateway runs it against its own stores and writes memory.ops or
647
- # docs.sources on the call's log itself.
722
+ # it with. Asked by the worker for the call it serves, and by the app for a call of its own org —
723
+ # `this.knowledge.search` in a tool; the gateway runs it against its own stores and writes
724
+ # memory.ops or docs.sources on the call's log itself.
648
725
  class LookupRequest(WireModel):
649
726
  """POST /v1/calls/{call}/lookup, the body."""
650
727
 
@@ -663,6 +740,22 @@ class LookupResult(WireModel):
663
740
  took_ms: float
664
741
 
665
742
 
743
+ class FoundChunk(WireModel):
744
+ """One chunk search found: the file it came from, the heading it sits under, and its text."""
745
+
746
+ path: str
747
+ heading: str | None
748
+ text: str
749
+
750
+
751
+ # What search answers in a lookup's output: the best chunks of every base the agent reads, the best
752
+ # first. The app's `this.knowledge.search` hands the chunks to the tool that asked.
753
+ class SearchFound(WireModel):
754
+ """What search answers in a lookup's output."""
755
+
756
+ chunks: list[FoundChunk]
757
+
758
+
666
759
  # POST /v1/calls/{call}/remember, the answer: what the call taught about the contact, counted.
667
760
  # Worker-only; the body is empty, the gateway reads the turns off its own log and writes memory.ops
668
761
  # itself.
@@ -683,6 +776,41 @@ class Dialled(WireModel):
683
776
  to: str
684
777
  from_: str = Field(alias="from")
685
778
  env: Env
779
+ log_token: str
780
+
781
+
782
+ # POST /v1/codes, the answer: the four digits a page shows beside the agent's number, when they stop
783
+ # being a code, and the token the page asks about them with.
784
+ class Code(WireModel):
785
+ """POST /v1/codes, the answer."""
786
+
787
+ code: str
788
+ number: str
789
+ expires_at: float
790
+ code_token: str
791
+
792
+
793
+ # GET /v1/codes/{code}: whether a call has claimed the code yet, and once one has, the call and the
794
+ # token that reads it.
795
+ class CodeStanding(WireModel):
796
+ """GET /v1/codes/{code}."""
797
+
798
+ code: str
799
+ status: Literal["waiting", "claimed", "expired"]
800
+ expires_at: float
801
+ call: str | None
802
+ log_token: str | None
803
+
804
+
805
+ # POST /v1/tokens, the answer: where the browser joins, as whom, the call it becomes, and a token
806
+ # that reads that call's log.
807
+ class Minted(WireModel):
808
+ """POST /v1/tokens, the answer."""
809
+
810
+ server_url: str
811
+ participant_token: str
812
+ call: str
813
+ log_token: str
686
814
 
687
815
 
688
816
  class DialGuards(WireModel):
@@ -713,3 +841,312 @@ class OutboundProvisioned(WireModel):
713
841
  ready: bool
714
842
  trunk: str | None = None
715
843
  address: str | None = None
844
+
845
+
846
+ # Every field is optional; one left out is not set, and the runtime's own default stands for it.
847
+ class TuningBody(WireModel):
848
+ """An agent's settings: what the org set, per world and per corner."""
849
+
850
+ voice: str | None = None
851
+ tts: str | None = None
852
+ tts_model: str | None = None
853
+ stt: str | None = None
854
+ llm: str | None = None
855
+ greeting: GreetingConfig | None = None
856
+ hangup: HangupConfig | None = None
857
+ turn: TurnConfig | None = None
858
+ memory: MemoryConfig | None = None
859
+ record: bool | None = None
860
+ max_duration_s: int | None = None
861
+ knowledge: str | None = None
862
+ bases: list[DocsConfig] | None = None
863
+
864
+
865
+ # One kept version of an agent's tuning: whose corner, which version, who set it and when, and what
866
+ # it says.
867
+ class TuningRow(WireModel):
868
+ """One kept version of an agent's tuning."""
869
+
870
+ holder: str
871
+ version: int
872
+ author: str
873
+ note: str | None
874
+ set_at: float
875
+ config: TuningBody
876
+
877
+
878
+ # GET /v1/agents/{slug}/settings: the agent's tuning as this key sees it — its own corner's, the
879
+ # team's and production's, each corner's own newest row, or null when that corner set nothing.
880
+ class TuningAnswer(WireModel):
881
+ """GET /v1/agents/{slug}/settings."""
882
+
883
+ world: Env
884
+ yours: TuningRow | None
885
+ team: TuningRow | None
886
+ production: TuningRow | None
887
+
888
+
889
+ # PUT /v1/agents/{slug}/settings, the body: the whole set, the version it was read at, why, and
890
+ # whose corner.
891
+ class TuningPut(WireModel):
892
+ """PUT /v1/agents/{slug}/settings, the body."""
893
+
894
+ config: TuningBody
895
+ if_version: int | None = None
896
+ note: str | None = None
897
+ team: bool = False
898
+
899
+
900
+ class TuningHistory(WireModel):
901
+ """GET /v1/agents/{slug}/settings/history: one corner's versions, newest first."""
902
+
903
+ world: Env
904
+ holder: str
905
+ rows: list[TuningRow]
906
+
907
+
908
+ # GET /v1/agents/{slug}/settings/diff: what this key's corner reads against another corner's newest,
909
+ # and which fields differ.
910
+ class TuningDiff(WireModel):
911
+ """GET /v1/agents/{slug}/settings/diff."""
912
+
913
+ ours: TuningRow | None
914
+ theirs: TuningRow | None
915
+ changed: list[str]
916
+
917
+
918
+ # It comes back as a NEW version; nothing is deleted.
919
+ class Rollback(WireModel):
920
+ """POST /v1/agents/{slug}/settings/rollback, the body: which version to bring back."""
921
+
922
+ version: int
923
+ team: bool = False
924
+
925
+
926
+ # The org's lexicon: how the voice says the words it would get wrong, and the words the ears must
927
+ # know. Shared by every agent of the org, laid over each one's own says and hears.
928
+ class LexiconBody(WireModel):
929
+ """The org's lexicon."""
930
+
931
+ said: list[Pronunciation]
932
+ heard: list[str]
933
+
934
+
935
+ class LexiconRow(WireModel):
936
+ """One kept version of the org's lexicon."""
937
+
938
+ holder: str
939
+ version: int
940
+ author: str
941
+ note: str | None
942
+ set_at: float
943
+ lexicon: LexiconBody
944
+
945
+
946
+ # GET /v1/lexicon: the org's words as this key sees them — its own corner's, the team's and
947
+ # production's, each corner's own newest, or null.
948
+ class LexiconAnswer(WireModel):
949
+ """GET /v1/lexicon."""
950
+
951
+ world: Env
952
+ yours: LexiconRow | None
953
+ team: LexiconRow | None
954
+ production: LexiconRow | None
955
+
956
+
957
+ # PUT /v1/lexicon, the body: the whole lexicon, the version it was read at, why, and whose corner.
958
+ class LexiconPut(WireModel):
959
+ """PUT /v1/lexicon, the body."""
960
+
961
+ lexicon: LexiconBody
962
+ if_version: int | None = None
963
+ note: str | None = None
964
+ team: bool = False
965
+
966
+
967
+ class LexiconHistory(WireModel):
968
+ """GET /v1/lexicon/history: one corner's versions, newest first."""
969
+
970
+ world: Env
971
+ holder: str
972
+ rows: list[LexiconRow]
973
+
974
+
975
+ # GET /v1/calls/{call}/settings: the exact tuning and lexicon a call was built on, by the versions
976
+ # its head row recorded.
977
+ class CallTuning(WireModel):
978
+ """GET /v1/calls/{call}/settings."""
979
+
980
+ config_version: int | None
981
+ lexicon_version: int | None
982
+ config: TuningRow | None
983
+ lexicon: LexiconRow | None
984
+
985
+
986
+ class KnowledgeFileRow(WireModel):
987
+ """One file of a base as the list draws it."""
988
+
989
+ path: str
990
+ chars: int
991
+ chunks: int
992
+ pushed_at: float
993
+
994
+
995
+ # GET /v1/knowledge/{base}: every file of the base, by path, with its size and what it became. Never
996
+ # the text: one file is read at a time.
997
+ class KnowledgeFiles(WireModel):
998
+ """GET /v1/knowledge/{base}."""
999
+
1000
+ base: str
1001
+ kept: bool
1002
+ files: list[KnowledgeFileRow]
1003
+
1004
+
1005
+ class KnowledgeFileRead(WireModel):
1006
+ """GET /v1/knowledge/{base}/files/{path}: one file, text and all."""
1007
+
1008
+ path: str
1009
+ text: str
1010
+ chunks: int
1011
+ pushed_at: float
1012
+
1013
+
1014
+ # New under that path, or replaced in place; the base is begun when there is none.
1015
+ class KnowledgeFilePut(WireModel):
1016
+ """PUT /v1/knowledge/{base}/files/{path}, the body: the file's whole text."""
1017
+
1018
+ text: str
1019
+
1020
+
1021
+ # PUT /v1/knowledge/{base}/files/{path}, the answer: which file, how many chunks it became, and how
1022
+ # long that took.
1023
+ class KnowledgeFilePushed(WireModel):
1024
+ """PUT /v1/knowledge/{base}/files/{path}, the answer."""
1025
+
1026
+ base: str
1027
+ path: str
1028
+ chunks: int
1029
+ took_ms: float
1030
+
1031
+
1032
+ class KnowledgeUse(WireModel):
1033
+ """One base and the agents whose settings attach it."""
1034
+
1035
+ base: str
1036
+ agents: list[str]
1037
+
1038
+
1039
+ # GET /v1/knowledge/attached: which agents read each base, off every agent's newest settings in this
1040
+ # world.
1041
+ class KnowledgeUses(WireModel):
1042
+ """GET /v1/knowledge/attached."""
1043
+
1044
+ bases: list[KnowledgeUse]
1045
+
1046
+
1047
+ # One synthetic caller of an ORG, kept by the gateway: what they want, how they talk, and what they
1048
+ # may state about themselves. A model plays them turn by turn — there is no script — for `pinecall
1049
+ # simulate` and the console's Simulations. A caller is a person on the phone, so who they are does
1050
+ # not depend on which of the org's agents answers: every agent of the org calls with the same list.
1051
+ class Persona(WireModel):
1052
+ """One synthetic caller of an ORG, kept by the gateway."""
1053
+
1054
+ name: str
1055
+ about: str
1056
+ goal: str
1057
+ style: str
1058
+ facts: dict[str, str]
1059
+ state: dict[str, Any]
1060
+ llm: str | None
1061
+ tts: str | None
1062
+ voice: str | None
1063
+ accepts_when: str
1064
+ declines_when: str
1065
+ author: str
1066
+ set_at: float
1067
+
1068
+
1069
+ # The same list for every agent of the org, and the same one in both worlds.
1070
+ class PersonaList(WireModel):
1071
+ """GET /v1/personas: every caller the org wrote, by name."""
1072
+
1073
+ personas: list[Persona]
1074
+
1075
+
1076
+ # A name that exists is replaced; `was` renames the caller it names.
1077
+ class PersonaPut(WireModel):
1078
+ """PUT /v1/personas/{name}, the body: the caller, written whole."""
1079
+
1080
+ about: str | None = None
1081
+ goal: str
1082
+ style: str
1083
+ facts: dict[str, str] | None = None
1084
+ state: dict[str, Any] | None = None
1085
+ llm: str | None = None
1086
+ tts: str | None = None
1087
+ voice: str | None = None
1088
+ accepts_when: str | None = None
1089
+ declines_when: str | None = None
1090
+ was: str | None = None
1091
+
1092
+
1093
+ # Read off the call index, never off a log — the same row the sessions list is drawn from.
1094
+ class PersonaRun(WireModel):
1095
+ """One simulation this caller has run: the call it was, and what the call came to."""
1096
+
1097
+ call: str
1098
+ agent: str
1099
+ started_at: float
1100
+ ended_at: float | None
1101
+ turns: int
1102
+ end_reason: EndReason | None
1103
+ outcome: str | None
1104
+ cost_eur: float | None
1105
+ score: SessionScore | None
1106
+
1107
+
1108
+ # GET /v1/personas/{name}/runs, in the key's own world and corner, paged exactly as the sessions
1109
+ # list is.
1110
+ class PersonaRunList(WireModel):
1111
+ """Every simulation this caller has run, newest first, a page at a time."""
1112
+
1113
+ runs: list[PersonaRun]
1114
+ total: int
1115
+ next: str | None
1116
+
1117
+
1118
+ # One voice a picker offers, as GET /v1/voices lists it: the id the `voice` setting takes, and what
1119
+ # a person chooses by. `country` and `accent` are the vendor's own words (`ES` is Spain, `MX`
1120
+ # Mexico), empty when the vendor said none.
1121
+ class ListedVoice(WireModel):
1122
+ """One voice a picker offers, as GET /v1/voices lists it."""
1123
+
1124
+ id: str
1125
+ name: str
1126
+ language: str
1127
+ description: str
1128
+ gender: str
1129
+ country: str
1130
+ accent: str
1131
+
1132
+
1133
+ # GET /v1/voices?tts=&language=: a voice vendor's own voices in one language, in the vendor's order.
1134
+ class VoicesListed(WireModel):
1135
+ """GET /v1/voices?tts=&language=."""
1136
+
1137
+ tts: str
1138
+ language: str | None
1139
+ voices: list[ListedVoice]
1140
+
1141
+
1142
+ # POST /v1/voices/sample, the body: which vendor, which voice, which model and which words to hear.
1143
+ # The answer is the WAV itself (`audio/wav`), with `Server-Timing: first-audio;dur=…, total;dur=…`
1144
+ # in milliseconds.
1145
+ class VoiceSample(WireModel):
1146
+ """POST /v1/voices/sample, the body."""
1147
+
1148
+ tts: str
1149
+ voice: str
1150
+ model: str | None = None
1151
+ language: str | None = None
1152
+ text: str | None = None
@@ -134,11 +134,22 @@ class TransferState(WireModel):
134
134
  """The transfer in flight or the one that happened."""
135
135
 
136
136
  to: str
137
- mode: TransferMode
137
+ mode: TransferMode | None = None
138
138
  status: Literal["requested", "done", "failed"]
139
139
  by: Literal["agent", "supervisor"]
140
140
 
141
141
 
142
+ # The agent's last ask for a person, from attention.requested until attention.answered settles it.
143
+ class AttentionState(WireModel):
144
+ """AttentionState, as protocol/schema declares it."""
145
+
146
+ reason: str
147
+ wait_s: float
148
+ status: Literal["open", "answered", "lapsed"]
149
+ asked_at: float
150
+ by: Supervisor | None
151
+
152
+
142
153
  class LiveTranscript(WireModel):
143
154
  """The words on screen right now: interim transcripts that a finished turn clears."""
144
155
 
@@ -233,6 +244,7 @@ class State(WireModel):
233
244
  held: bool
234
245
  muted: bool
235
246
  transfer: TransferState | None
247
+ attention: AttentionState | None = None
236
248
  usage: list[ModelUsage]
237
249
  cost: Cost | None
238
250
  routes: list[Route]
@@ -45,12 +45,13 @@ class TransferVerb(WireModel):
45
45
 
46
46
  verb: Literal["transfer"] = "transfer"
47
47
  to: str
48
- mode: TransferMode
48
+ mode: TransferMode | None = None
49
49
 
50
50
 
51
- # Logged as supervisor.ended, then call.ended with reason supervisor_ended.
51
+ # Hang up on the caller's behalf, at once: a sentence playing and a reply still being written are
52
+ # cut, not finished. Logged as supervisor.ended, then call.ended with reason supervisor_ended.
52
53
  class EndVerb(WireModel):
53
- """Hang up on the caller's behalf."""
54
+ """Hang up on the caller's behalf, at once."""
54
55
 
55
56
  verb: Literal["end"] = "end"
56
57
  reason: str | None = None
@@ -1,4 +0,0 @@
1
- """The one place the version lives; hatch reads it from here at build time."""
2
-
3
- # 0.0.0 means "unreleased". The number is the maintainer's call and is never bumped from code.
4
- __version__ = "0.3.0"