pinecall-protocol 0.6.2__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.6.2 → 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.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/commands.py +31 -3
  4. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/defs.py +22 -4
  5. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/events.py +119 -23
  6. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/fixtures/__init__.py +1 -1
  7. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/fixtures/call-log-golden.state.json +1 -0
  8. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/registry.py +46 -0
  9. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/rest.py +123 -1
  10. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/state.py +13 -1
  11. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/verbs.py +4 -3
  12. pinecall_protocol-0.6.2/pinecall_protocol/_version.py +0 -4
  13. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/.gitignore +0 -0
  14. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/.python-version +0 -0
  15. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/LICENSE +0 -0
  16. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/README.md +0 -0
  17. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/__init__.py +0 -0
  18. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/_base.py +0 -0
  19. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/codec.py +0 -0
  20. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/envelope.py +0 -0
  21. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/fixtures/call-log-golden.json +0 -0
  22. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/metrics.py +0 -0
  23. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/py.typed +0 -0
  24. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pinecall_protocol/room.py +0 -0
  25. {pinecall_protocol-0.6.2 → pinecall_protocol-0.6.14}/pyproject.toml +0 -0
  26. {pinecall_protocol-0.6.2 → 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.6.2
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,6 +17,10 @@ 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."""
@@ -46,6 +50,30 @@ class AgentSay(WireModel):
46
50
  allow_interruptions: bool | None = None
47
51
 
48
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
+
49
77
  # The new call's log opens with call.dialing; call.started follows when the far end answers.
50
78
  class CallDial(WireModel):
51
79
  """Place an outbound call as this agent."""
@@ -95,12 +123,12 @@ class CallMute(WireModel):
95
123
  """Mute the agent: it keeps listening and thinking, produces no audio."""
96
124
 
97
125
 
98
- # call.transferred says whether it worked.
126
+ # call.transferred says whether it worked, and which mode it was.
99
127
  class CallTransfer(WireModel):
100
- """Send the caller to another number."""
128
+ """Send the caller to another number, or bring that number into the call."""
101
129
 
102
130
  to: str
103
- mode: TransferMode
131
+ mode: TransferMode | None = None
104
132
 
105
133
 
106
134
  class CallUnhold(WireModel):
@@ -27,13 +27,15 @@ type Env = Literal["production", "sandbox"]
27
27
  # What a console may ask of the process standing in the agent's directory, relayed by the gateway: a
28
28
  # written call to the class mounted there (chat), a simulated caller put on the class it holds, its
29
29
  # goldens and a suite of them, its knowledge folder pushed or its golden asked, its memory goldens,
30
- # a call promoted to a candidate file, the drift of the last two windows, and the reproductions a
31
- # broken run left on that disk. Everything else a console needs is a door of the gateway.
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",
38
+ "view.render",
37
39
  "simulate.start",
38
40
  "goldens.roster",
39
41
  "goldens.run",
@@ -80,8 +82,9 @@ type EndedBy = Literal["caller", "agent", "supervisor", "platform"]
80
82
  type ScoreVerdict = Literal["held", "broken", "deferred", "skipped"]
81
83
 
82
84
 
83
- # Cold: the caller is sent on and the agent leaves. Warm: the agent stays on the line until the
84
- # 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.
85
88
  type TransferMode = Literal["cold", "warm"]
86
89
 
87
90
 
@@ -234,6 +237,7 @@ class DocSource(WireModel):
234
237
  """One chunk of the knowledge base that retrieval put in front of the model for this turn."""
235
238
 
236
239
  id: str
240
+ base: str | None = None
237
241
  path: str
238
242
  heading: str | None = None
239
243
  score: float
@@ -309,6 +313,8 @@ class TurnConfig(WireModel):
309
313
 
310
314
  min_interruption_words: int | None = None
311
315
  endpointing_ms: int | None = None
316
+ eot_threshold: float | None = None
317
+ eager_eot_threshold: float | None = None
312
318
 
313
319
 
314
320
  # How the voice says one word it would otherwise get wrong: a proper name, a brand, a street.
@@ -327,6 +333,16 @@ class StateFieldSpec(WireModel):
327
333
  visibility: Visibility
328
334
 
329
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
+
330
346
  # An event nobody declared is refused before it touches the log.
331
347
  class EventSpec(WireModel):
332
348
  """One outside event the agent accepts, and from whom."""
@@ -404,7 +420,9 @@ class AgentConfig(WireModel):
404
420
  docs: DocsConfig | None = None
405
421
  memory: MemoryConfig | None = None
406
422
  hangup: HangupConfig | None = None
423
+ record: bool = True
407
424
  tools: list[ToolSpec] | None = None
408
425
  uses_knowledge: bool = False
409
426
  state_fields: list[StateFieldSpec] | None = None
427
+ view: ViewSpec | None = None
410
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,21 +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
- persona: str | None = None
163
- caller: Contact | None
164
- started_at: float
165
- env: Env | None = None
166
-
167
-
168
216
  # Written after call.ended, once memory and pricing are done; call.score follows it and seals the
169
217
  # log.
170
218
  class CallSummary(WireModel):
@@ -183,24 +231,42 @@ class CallTransferred(WireModel):
183
231
  """A transfer asked for by the agent or a supervisor finished, one way or the other."""
184
232
 
185
233
  to: str
186
- mode: TransferMode
234
+ mode: TransferMode | None = None
187
235
  ok: bool
188
236
  error: str | None = None
189
237
 
190
238
 
191
- # Somebody asked to be called back because no seat was free: a phone caller the overflow agent
192
- # answered, or a web visitor who left a number at the widget. Written into the agent's own log; the
193
- # 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.
194
242
  class CallbackRequested(WireModel):
195
- """Somebody asked to be called back because no seat was free."""
243
+ """Somebody asked to be called back."""
196
244
 
197
245
  channel: Channel
198
246
  number: str
199
- via: Literal["overflow", "widget"]
247
+ via: Literal["overflow", "widget", "agent"]
200
248
  call: str | None
249
+ when: str | None = None
250
+ note: str | None = None
201
251
  contact: Contact | None
202
252
 
203
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
+
204
270
  # The tool does not run; the model is told.
205
271
  class ConfirmDeclined(WireModel):
206
272
  """The caller did not say yes, or the request lapsed."""
@@ -236,9 +302,10 @@ class ConfirmRequest(WireModel):
236
302
  ttl_s: int
237
303
 
238
304
 
305
+ # The gateway refused a call, a written turn or a register because one of the org's quotas ran out.
239
306
  # Written into the agent's own log, which is the org's, before the door says no.
240
307
  class CreditsExhausted(WireModel):
241
- """The gateway refused a call or a register because one of the org's quotas ran out."""
308
+ """CreditsExhausted, as protocol/schema declares it."""
242
309
 
243
310
  org: str
244
311
  quota: Literal[
@@ -250,6 +317,7 @@ class CreditsExhausted(WireModel):
250
317
  "knowledge_chunks",
251
318
  "numbers",
252
319
  "seats",
320
+ "llm_tokens",
253
321
  ]
254
322
  used: float
255
323
  limit: int
@@ -281,6 +349,13 @@ class DocsSources(WireModel):
281
349
  speech_id: str | None = None
282
350
 
283
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
+
284
359
  # Inside a call it says what failed; outside a call it says which command the gateway refused.
285
360
  class ErrorEvent(WireModel):
286
361
  """Something went wrong."""
@@ -329,6 +404,27 @@ class MemoryOps(WireModel):
329
404
  speech_id: str | None = None
330
405
 
331
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
+
332
428
  # Ephemeral: it proves the socket is alive and says nothing else.
333
429
  class Pong(WireModel):
334
430
  """The answer to ping."""
@@ -409,7 +505,7 @@ class SupervisorTransferred(WireModel):
409
505
 
410
506
  by: Supervisor
411
507
  to: str
412
- mode: TransferMode
508
+ mode: TransferMode | None = None
413
509
 
414
510
 
415
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",),
@@ -22,7 +22,7 @@ from pinecall_protocol.defs import (
22
22
  TurnConfig,
23
23
  )
24
24
  from pinecall_protocol.envelope import Entry
25
- from pinecall_protocol.state import CallStatus, State
25
+ from pinecall_protocol.state import AttentionState, CallStatus, State
26
26
 
27
27
 
28
28
  class CallState(WireModel):
@@ -78,6 +78,15 @@ class SessionLine(WireModel):
78
78
  cost: Cost | None
79
79
  score: SessionScore | None = None
80
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
81
90
 
82
91
 
83
92
  # GET /v1/agents/{slug}/sessions and GET /v1/sessions: the calls that match, newest first, a page at
@@ -138,6 +147,29 @@ class Insights(WireModel):
138
147
  budget: InsightsBudget
139
148
 
140
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
+
141
173
  # GET and PUT /v1/org/judging: whether the org's calls are judged at hang-up, and what judging one
142
174
  # may spend on a model.
143
175
  class Judging(WireModel):
@@ -235,6 +267,11 @@ class AgentMemory(WireModel):
235
267
  next: str | None
236
268
 
237
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
+
238
275
  # GET and PUT /v1/agents/{slug}/widget: how the widget presents this agent, kept per org, world and
239
276
  # agent. PUT takes the whole set.
240
277
  class WidgetSettings(WireModel):
@@ -245,6 +282,7 @@ class WidgetSettings(WireModel):
245
282
  greeting: str | None
246
283
  accent: str | None
247
284
  autostart: bool
285
+ theme: WidgetTheme | None = None
248
286
 
249
287
 
250
288
  # GET and PUT /v1/org/sso: the OpenID Connect provider this org's people sign in at, and never the
@@ -738,6 +776,41 @@ class Dialled(WireModel):
738
776
  to: str
739
777
  from_: str = Field(alias="from")
740
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
741
814
 
742
815
 
743
816
  class DialGuards(WireModel):
@@ -783,6 +856,8 @@ class TuningBody(WireModel):
783
856
  hangup: HangupConfig | None = None
784
857
  turn: TurnConfig | None = None
785
858
  memory: MemoryConfig | None = None
859
+ record: bool | None = None
860
+ max_duration_s: int | None = None
786
861
  knowledge: str | None = None
787
862
  bases: list[DocsConfig] | None = None
788
863
 
@@ -982,6 +1057,11 @@ class Persona(WireModel):
982
1057
  style: str
983
1058
  facts: dict[str, str]
984
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
985
1065
  author: str
986
1066
  set_at: float
987
1067
 
@@ -1002,6 +1082,11 @@ class PersonaPut(WireModel):
1002
1082
  style: str
1003
1083
  facts: dict[str, str] | None = None
1004
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
1005
1090
  was: str | None = None
1006
1091
 
1007
1092
 
@@ -1028,3 +1113,40 @@ class PersonaRunList(WireModel):
1028
1113
  runs: list[PersonaRun]
1029
1114
  total: int
1030
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.6.2"