pinecall-runtime 0.1.6__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.
Files changed (328) hide show
  1. pinecall/__init__.py +0 -0
  2. pinecall/channels/__init__.py +0 -0
  3. pinecall/channels/_chooser.py +45 -0
  4. pinecall/channels/offers.py +219 -0
  5. pinecall/channels/rooms.py +176 -0
  6. pinecall/channels/routes.py +275 -0
  7. pinecall/channels/telephony/__init__.py +0 -0
  8. pinecall/channels/telephony/_twilio.py +349 -0
  9. pinecall/channels/telephony/carrier.py +229 -0
  10. pinecall/channels/telephony/carrier_catalog.py +92 -0
  11. pinecall/channels/telephony/carriers.csv +41 -0
  12. pinecall/channels/telephony/dialing.py +419 -0
  13. pinecall/channels/telephony/firewall.py +58 -0
  14. pinecall/channels/telephony/hand_over.py +78 -0
  15. pinecall/channels/telephony/number_path.py +176 -0
  16. pinecall/channels/telephony/numbers.py +530 -0
  17. pinecall/channels/telephony/origination.py +77 -0
  18. pinecall/channels/telephony/sip.py +448 -0
  19. pinecall/channels/whatsapp.py +413 -0
  20. pinecall/cli/__init__.py +0 -0
  21. pinecall/cli/_archive.py +85 -0
  22. pinecall/cli/_drift.py +38 -0
  23. pinecall/cli/_facts.py +67 -0
  24. pinecall/cli/_load.py +433 -0
  25. pinecall/cli/_operator.py +626 -0
  26. pinecall/cli/_sessions.py +164 -0
  27. pinecall/cli/_traceback.py +95 -0
  28. pinecall/cli/_usage.py +34 -0
  29. pinecall/cli/main.py +521 -0
  30. pinecall/domain/__init__.py +0 -0
  31. pinecall/domain/agent.py +446 -0
  32. pinecall/domain/call.py +152 -0
  33. pinecall/domain/country_codes.py +32 -0
  34. pinecall/domain/errors.py +151 -0
  35. pinecall/domain/names.py +115 -0
  36. pinecall/domain/org.py +120 -0
  37. pinecall/domain/person.py +208 -0
  38. pinecall/domain/scope.py +21 -0
  39. pinecall/evals/__init__.py +0 -0
  40. pinecall/evals/_evidence.py +158 -0
  41. pinecall/evals/calibration.py +112 -0
  42. pinecall/evals/callers.py +160 -0
  43. pinecall/evals/case.py +327 -0
  44. pinecall/evals/checks.py +437 -0
  45. pinecall/evals/compliance.py +197 -0
  46. pinecall/evals/dataset.py +451 -0
  47. pinecall/evals/goldens.py +331 -0
  48. pinecall/evals/judges.py +525 -0
  49. pinecall/evals/runs.py +252 -0
  50. pinecall/evals/spoken.py +356 -0
  51. pinecall/fleet/__init__.py +0 -0
  52. pinecall/fleet/client.py +569 -0
  53. pinecall/fleet/demand.py +51 -0
  54. pinecall/fleet/heartbeat.py +194 -0
  55. pinecall/fleet/measures.py +157 -0
  56. pinecall/fleet/roster.py +232 -0
  57. pinecall/fleet/worlds.py +44 -0
  58. pinecall/gateway/__init__.py +0 -0
  59. pinecall/gateway/_call_setup.py +53 -0
  60. pinecall/gateway/_deps.py +556 -0
  61. pinecall/gateway/_gateway.py +75 -0
  62. pinecall/gateway/_served.py +396 -0
  63. pinecall/gateway/_sockets.py +444 -0
  64. pinecall/gateway/_streams.py +83 -0
  65. pinecall/gateway/_text_calls.py +182 -0
  66. pinecall/gateway/api/__init__.py +0 -0
  67. pinecall/gateway/api/accounts.py +324 -0
  68. pinecall/gateway/api/agents.py +150 -0
  69. pinecall/gateway/api/apps.py +478 -0
  70. pinecall/gateway/api/box.py +658 -0
  71. pinecall/gateway/api/calibration.py +58 -0
  72. pinecall/gateway/api/callbacks.py +105 -0
  73. pinecall/gateway/api/calls.py +655 -0
  74. pinecall/gateway/api/chat.py +223 -0
  75. pinecall/gateway/api/dataset.py +163 -0
  76. pinecall/gateway/api/desk.py +113 -0
  77. pinecall/gateway/api/entries.py +77 -0
  78. pinecall/gateway/api/evals.py +647 -0
  79. pinecall/gateway/api/fleet.py +73 -0
  80. pinecall/gateway/api/hosting.py +241 -0
  81. pinecall/gateway/api/judges.py +97 -0
  82. pinecall/gateway/api/keys.py +110 -0
  83. pinecall/gateway/api/line.py +142 -0
  84. pinecall/gateway/api/members.py +197 -0
  85. pinecall/gateway/api/metrics.py +226 -0
  86. pinecall/gateway/api/numbers.py +476 -0
  87. pinecall/gateway/api/ops.py +428 -0
  88. pinecall/gateway/api/org.py +283 -0
  89. pinecall/gateway/api/personas.py +157 -0
  90. pinecall/gateway/api/pipeline.py +206 -0
  91. pinecall/gateway/api/prompts.py +50 -0
  92. pinecall/gateway/api/providers.py +240 -0
  93. pinecall/gateway/api/recordings.py +89 -0
  94. pinecall/gateway/api/relay.py +169 -0
  95. pinecall/gateway/api/retrieval.py +403 -0
  96. pinecall/gateway/api/runner.py +109 -0
  97. pinecall/gateway/api/sessions.py +102 -0
  98. pinecall/gateway/api/settings.py +463 -0
  99. pinecall/gateway/api/signup.py +164 -0
  100. pinecall/gateway/api/sso_login.py +168 -0
  101. pinecall/gateway/api/threads.py +187 -0
  102. pinecall/gateway/api/usage.py +303 -0
  103. pinecall/gateway/api/visitors.py +222 -0
  104. pinecall/gateway/api/whatsapp.py +48 -0
  105. pinecall/gateway/api/widget.py +49 -0
  106. pinecall/gateway/app.py +504 -0
  107. pinecall/gateway/calls/__init__.py +0 -0
  108. pinecall/gateway/calls/binding.py +63 -0
  109. pinecall/gateway/calls/commands.py +18 -0
  110. pinecall/gateway/calls/inbox.py +100 -0
  111. pinecall/gateway/calls/known.py +23 -0
  112. pinecall/gateway/calls/owners.py +87 -0
  113. pinecall/gateway/calls/pump.py +33 -0
  114. pinecall/gateway/calls/serving.py +118 -0
  115. pinecall/gateway/calls/threads.py +367 -0
  116. pinecall/gateway/dispatching/__init__.py +0 -0
  117. pinecall/gateway/dispatching/arrivals.py +58 -0
  118. pinecall/gateway/dispatching/sweep.py +43 -0
  119. pinecall/gateway/ending/__init__.py +0 -0
  120. pinecall/gateway/ending/reaper.py +160 -0
  121. pinecall/gateway/ending/seal.py +312 -0
  122. pinecall/gateway/ending/stranded.py +117 -0
  123. pinecall/log/__init__.py +0 -0
  124. pinecall/log/_histogram.py +74 -0
  125. pinecall/log/_relay.py +259 -0
  126. pinecall/log/_writer.py +454 -0
  127. pinecall/log/days.py +154 -0
  128. pinecall/log/drift.py +550 -0
  129. pinecall/log/facts.py +358 -0
  130. pinecall/log/inbox.py +169 -0
  131. pinecall/log/lists.py +155 -0
  132. pinecall/log/logs.py +581 -0
  133. pinecall/log/openings.py +71 -0
  134. pinecall/log/private.py +135 -0
  135. pinecall/log/queries.py +229 -0
  136. pinecall/log/readers.py +207 -0
  137. pinecall/log/reduce.py +659 -0
  138. pinecall/log/refold.py +217 -0
  139. pinecall/log/store.py +370 -0
  140. pinecall/log/usage.py +242 -0
  141. pinecall/postgres/__init__.py +0 -0
  142. pinecall/postgres/migrate.py +202 -0
  143. pinecall/postgres/migrations/0001_schema.sql +591 -0
  144. pinecall/postgres/migrations/0002_worlds.sql +15 -0
  145. pinecall/postgres/migrations/0003_lent.sql +4 -0
  146. pinecall/postgres/migrations/0004_carriers.sql +23 -0
  147. pinecall/postgres/migrations/0005_retrieved_only.sql +11 -0
  148. pinecall/postgres/migrations/0006_persona_agents.sql +9 -0
  149. pinecall/postgres/migrations/0007_money_in_dollars.sql +27 -0
  150. pinecall/postgres/migrations/0008_lexicon_per_agent.sql +27 -0
  151. pinecall/postgres/migrations/0009_personas_per_agent.sql +31 -0
  152. pinecall/postgres/migrations/0010_agent_judges.sql +12 -0
  153. pinecall/postgres/migrations/0011_erasure.sql +32 -0
  154. pinecall/postgres/migrations/0012_org_policy.sql +10 -0
  155. pinecall/postgres/migrations/0013_calling_rules.sql +12 -0
  156. pinecall/postgres/migrations/0014_consents.sql +24 -0
  157. pinecall/postgres/migrations/0015_disclosure.sql +7 -0
  158. pinecall/postgres/migrations/0016_call_records.sql +21 -0
  159. pinecall/postgres/migrations/0017_reads.sql +17 -0
  160. pinecall/postgres/migrations/0018_hosting.sql +41 -0
  161. pinecall/postgres/migrations/0019_hosted_state.sql +10 -0
  162. pinecall/postgres/migrations/0020_written_once.sql +7 -0
  163. pinecall/postgres/migrations/0021_hosted_running.sql +21 -0
  164. pinecall/postgres/migrations/0022_sealed.sql +39 -0
  165. pinecall/postgres/migrations/0023_usage_totals.sql +158 -0
  166. pinecall/postgres/migrations/0024_log_key_by_day.sql +8 -0
  167. pinecall/postgres/migrations/0025_log_before_the_days.sql +15 -0
  168. pinecall/postgres/migrations/0026_log_before_the_days_validated.sql +4 -0
  169. pinecall/postgres/migrations/0027_log_by_day.sql +96 -0
  170. pinecall/postgres/migrations/0030_whatsapp_seen.sql +17 -0
  171. pinecall/postgres/migrations/0040_reads_widened.sql +15 -0
  172. pinecall/postgres/migrations/0060_drift.sql +62 -0
  173. pinecall/postgres/migrations/0061_canaries.sql +24 -0
  174. pinecall/postgres/migrations/0062_eval_cases.sql +23 -0
  175. pinecall/postgres/migrations/0063_prompts.sql +16 -0
  176. pinecall/postgres/migrations/0075_call_private.sql +14 -0
  177. pinecall/postgres/migrations/0076_recording_keys.sql +13 -0
  178. pinecall/postgres/migrations/0077_tokens_world.sql +7 -0
  179. pinecall/postgres/migrations/0078_facts_cost_by_stage.sql +12 -0
  180. pinecall/postgres/migrations/0079_judge_labels.sql +20 -0
  181. pinecall/postgres/migrations/0080_call_openings.sql +20 -0
  182. pinecall/postgres/migrations/0081_seal_lease.sql +7 -0
  183. pinecall/postgres/migrations/0082_caller_codes.sql +14 -0
  184. pinecall/postgres/migrations/0083_one_use_words.sql +15 -0
  185. pinecall/postgres/migrations/0084_knocks_and_run_leases.sql +23 -0
  186. pinecall/postgres/migrations/0085_seal_lease_holder.sql +8 -0
  187. pinecall/postgres/migrations/0086_route_origin.sql +17 -0
  188. pinecall/postgres/migrations/0087_carrier_networks.sql +20 -0
  189. pinecall/postgres/migrations/0088_route_last_call.sql +3 -0
  190. pinecall/postgres/migrations/0089_offers.sql +21 -0
  191. pinecall/postgres/migrations/0090_invitation_purpose.sql +5 -0
  192. pinecall/postgres/migrations/0091_head_opened_by.sql +6 -0
  193. pinecall/postgres/migrations/0092_key_parent.sql +6 -0
  194. pinecall/postgres/migrations/0093_code_tries.sql +10 -0
  195. pinecall/postgres/migrations/0094_route_approval.sql +12 -0
  196. pinecall/postgres/migrations/0095_sso_domain_proofs.sql +17 -0
  197. pinecall/postgres/migrations/0096_row_level_security.sql +38 -0
  198. pinecall/postgres/migrations/0097_case_status.sql +19 -0
  199. pinecall/postgres/pool.py +190 -0
  200. pinecall/process/__init__.py +0 -0
  201. pinecall/process/_objects.py +97 -0
  202. pinecall/process/box_settings.py +23 -0
  203. pinecall/process/connections.py +124 -0
  204. pinecall/process/metrics.py +127 -0
  205. pinecall/process/recordings.py +323 -0
  206. pinecall/process/resolver.py +41 -0
  207. pinecall/process/sealed_audio.py +173 -0
  208. pinecall/process/settings.py +534 -0
  209. pinecall/process/shared.py +158 -0
  210. pinecall/process/signal.py +416 -0
  211. pinecall/providers/__init__.py +0 -0
  212. pinecall/providers/build.py +570 -0
  213. pinecall/providers/catalog.py +266 -0
  214. pinecall/providers/credentials.py +230 -0
  215. pinecall/providers/declared.py +69 -0
  216. pinecall/providers/prices.py +240 -0
  217. pinecall/providers/tried.py +104 -0
  218. pinecall/providers/voices.py +174 -0
  219. pinecall/public/console/assets/index-BIfKPSgn.js +164 -0
  220. pinecall/public/console/assets/index-ClOTU-xX.css +1 -0
  221. pinecall/public/console/favicon.png +0 -0
  222. pinecall/public/console/index.html +20 -0
  223. pinecall/public/console/logo-mark.png +0 -0
  224. pinecall/public/console/pinecall-logo-dark.png +0 -0
  225. pinecall/public/console/pinecall-logo.png +0 -0
  226. pinecall/public/console/pinecall-mark.png +0 -0
  227. pinecall/public/console/sw.js +37 -0
  228. pinecall/public/widget/pinecall-widget.js +853 -0
  229. pinecall/py.typed +0 -0
  230. pinecall/retrieval/__init__.py +0 -0
  231. pinecall/retrieval/_search.py +186 -0
  232. pinecall/retrieval/_sections.py +70 -0
  233. pinecall/retrieval/embed.py +340 -0
  234. pinecall/retrieval/extraction.py +403 -0
  235. pinecall/retrieval/knowledge.py +659 -0
  236. pinecall/retrieval/lookups.py +192 -0
  237. pinecall/retrieval/memory.py +603 -0
  238. pinecall/runner/__init__.py +0 -0
  239. pinecall/runner/_kube.py +459 -0
  240. pinecall/runner/_plan.py +157 -0
  241. pinecall/runner/main.py +306 -0
  242. pinecall/session/__init__.py +0 -0
  243. pinecall/session/_agent.py +141 -0
  244. pinecall/session/_hearing.py +101 -0
  245. pinecall/session/_livekit.py +195 -0
  246. pinecall/session/_prompt.py +147 -0
  247. pinecall/session/_recording.py +237 -0
  248. pinecall/session/call.py +367 -0
  249. pinecall/session/clock.py +40 -0
  250. pinecall/session/hold.py +239 -0
  251. pinecall/session/room.py +626 -0
  252. pinecall/session/session.py +690 -0
  253. pinecall/session/text.py +144 -0
  254. pinecall/session/tools.py +483 -0
  255. pinecall/session/voice.py +118 -0
  256. pinecall/session/widget.py +201 -0
  257. pinecall/tenancy/__init__.py +0 -0
  258. pinecall/tenancy/admission.py +204 -0
  259. pinecall/tenancy/agents.py +164 -0
  260. pinecall/tenancy/canary.py +126 -0
  261. pinecall/tenancy/carrier_networks.py +156 -0
  262. pinecall/tenancy/carriers.py +265 -0
  263. pinecall/tenancy/codes.py +196 -0
  264. pinecall/tenancy/consents.py +167 -0
  265. pinecall/tenancy/dial_policy.py +405 -0
  266. pinecall/tenancy/disclosure.py +41 -0
  267. pinecall/tenancy/erasure.py +204 -0
  268. pinecall/tenancy/export.py +167 -0
  269. pinecall/tenancy/hosted_running.py +166 -0
  270. pinecall/tenancy/hosting.py +475 -0
  271. pinecall/tenancy/judges.py +126 -0
  272. pinecall/tenancy/keys.py +467 -0
  273. pinecall/tenancy/knocks.py +40 -0
  274. pinecall/tenancy/letters.py +378 -0
  275. pinecall/tenancy/mail.py +458 -0
  276. pinecall/tenancy/org_secrets.py +123 -0
  277. pinecall/tenancy/orgs.py +78 -0
  278. pinecall/tenancy/people.py +543 -0
  279. pinecall/tenancy/personas.py +145 -0
  280. pinecall/tenancy/policy.py +65 -0
  281. pinecall/tenancy/prompts.py +48 -0
  282. pinecall/tenancy/reads.py +73 -0
  283. pinecall/tenancy/recording_keys.py +41 -0
  284. pinecall/tenancy/remembered.py +148 -0
  285. pinecall/tenancy/retention.py +86 -0
  286. pinecall/tenancy/scopes.py +406 -0
  287. pinecall/tenancy/signin.py +622 -0
  288. pinecall/tenancy/spend.py +69 -0
  289. pinecall/tenancy/sso.py +431 -0
  290. pinecall/tenancy/sso_domains.py +132 -0
  291. pinecall/tenancy/throttle.py +88 -0
  292. pinecall/tenancy/tokens.py +400 -0
  293. pinecall/tenancy/traceback.py +70 -0
  294. pinecall/tenancy/usage.py +136 -0
  295. pinecall/tenancy/vault.py +204 -0
  296. pinecall/tenancy/words.py +144 -0
  297. pinecall/wire/__init__.py +0 -0
  298. pinecall/wire/commands.py +350 -0
  299. pinecall/wire/events.py +655 -0
  300. pinecall/wire/frames.py +65 -0
  301. pinecall/wire/metrics.py +287 -0
  302. pinecall/wire/parts.py +320 -0
  303. pinecall/wire/rest/__init__.py +0 -0
  304. pinecall/wire/rest/accounts.py +512 -0
  305. pinecall/wire/rest/agents.py +172 -0
  306. pinecall/wire/rest/calls.py +397 -0
  307. pinecall/wire/rest/evals.py +453 -0
  308. pinecall/wire/rest/fleet.py +69 -0
  309. pinecall/wire/rest/hosting.py +174 -0
  310. pinecall/wire/rest/numbers.py +280 -0
  311. pinecall/wire/rest/ops.py +342 -0
  312. pinecall/wire/rest/providers.py +83 -0
  313. pinecall/wire/rest/retrieval.py +301 -0
  314. pinecall/wire/rest/settings.py +184 -0
  315. pinecall/wire/rest/usage.py +245 -0
  316. pinecall/wire/room.py +61 -0
  317. pinecall/wire/scores.py +49 -0
  318. pinecall/wire/state.py +243 -0
  319. pinecall/worker/__init__.py +0 -0
  320. pinecall/worker/_job.py +655 -0
  321. pinecall/worker/_recorder.py +82 -0
  322. pinecall/worker/_traces.py +48 -0
  323. pinecall/worker/main.py +307 -0
  324. pinecall_runtime-0.1.6.dist-info/METADATA +212 -0
  325. pinecall_runtime-0.1.6.dist-info/RECORD +328 -0
  326. pinecall_runtime-0.1.6.dist-info/WHEEL +4 -0
  327. pinecall_runtime-0.1.6.dist-info/entry_points.txt +2 -0
  328. pinecall_runtime-0.1.6.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,655 @@
1
+ """The call doors a worker writes through and a reader reads: open, append, seal, the log."""
2
+
3
+ import asyncio
4
+ import base64
5
+ import logging
6
+ import time
7
+ from collections.abc import AsyncIterator
8
+ from typing import Annotated
9
+
10
+ from fastapi import APIRouter, Header, Query, Response
11
+ from fastapi.responses import StreamingResponse
12
+ from pydantic import BaseModel, Field
13
+
14
+ from pinecall.channels import offers, routes
15
+ from pinecall.channels.telephony import carrier
16
+ from pinecall.domain.agent import AgentConfig, Versions
17
+ from pinecall.domain.call import CallContext, Opener
18
+ from pinecall.domain.errors import (
19
+ Conflict,
20
+ DeclarationRefused,
21
+ NotAllowed,
22
+ NotFound,
23
+ )
24
+ from pinecall.domain.names import JsonObject
25
+ from pinecall.domain.person import THE_FLEET
26
+ from pinecall.domain.scope import Scope
27
+ from pinecall.gateway import _deps, _streams
28
+ from pinecall.gateway._call_setup import tuned
29
+ from pinecall.gateway._deps import (
30
+ Acting,
31
+ CallReaderDep,
32
+ GatewayDep,
33
+ Reader,
34
+ ReaderDep,
35
+ ScopeDep,
36
+ TeamKey,
37
+ WorkerKey,
38
+ asked_by,
39
+ )
40
+ from pinecall.gateway._gateway import Gateway
41
+ from pinecall.gateway._served import Served
42
+ from pinecall.gateway._sockets import NO_AGENT, NO_UNCLAIMED, NOT_THAT_APP, Registration
43
+ from pinecall.gateway._streams import frame, paced, streamed, wants_sse
44
+ from pinecall.gateway.calls.binding import attach
45
+ from pinecall.gateway.calls.serving import (
46
+ claim_code,
47
+ first_seen,
48
+ looked_up,
49
+ opened,
50
+ served_call,
51
+ serving_agent,
52
+ )
53
+ from pinecall.gateway.ending.seal import remembered, sealed
54
+ from pinecall.log import openings, queries
55
+ from pinecall.log.readers import Filter, parse_filter, project_entry, project_state
56
+ from pinecall.log.store import DEFAULT_LIMIT, Claim
57
+ from pinecall.process.recordings import (
58
+ recordings_of,
59
+ )
60
+ from pinecall.providers import catalog
61
+ from pinecall.providers.build import vendor_named_in
62
+ from pinecall.providers.catalog import judge_ceiling
63
+ from pinecall.session.call import ToolUse
64
+ from pinecall.tenancy import (
65
+ disclosure,
66
+ erasure,
67
+ keys,
68
+ orgs,
69
+ policy,
70
+ recording_keys,
71
+ tokens,
72
+ )
73
+ from pinecall.tenancy.scopes import Picked
74
+ from pinecall.wire.commands import CallClaim
75
+ from pinecall.wire.events import (
76
+ EVENTS,
77
+ GATEWAY_WRITES,
78
+ TERMINAL_EVENT,
79
+ ErrorEvent,
80
+ ToolCall,
81
+ )
82
+ from pinecall.wire.frames import Command, Entry
83
+ from pinecall.wire.parts import ToolResult
84
+ from pinecall.wire.rest.agents import JudgingSettings
85
+ from pinecall.wire.rest.calls import (
86
+ AppendEntriesRequest,
87
+ AppendEntriesResponse,
88
+ AppendEntryRequest,
89
+ Erasure,
90
+ LogPage,
91
+ LookupRequest,
92
+ LookupResponse,
93
+ OpenCallRequest,
94
+ OpenCallResponse,
95
+ RecordingKeyResponse,
96
+ RememberResponse,
97
+ SealCallRequest,
98
+ )
99
+
100
+ router = APIRouter()
101
+
102
+
103
+ logger = logging.getLogger(__name__)
104
+
105
+
106
+ UNKNOWN_EVENT = "no event is called {kind!r}: the log takes the protocol's own words"
107
+
108
+
109
+ THE_GATEWAYS = "{kind!r} is written by the gateway itself, never sent to it"
110
+
111
+
112
+ NOT_OPEN = "this gateway is not writing call {call!r}: open it with POST /v1/calls first"
113
+
114
+
115
+ SEALED = "call {call!r} is over: nothing more can be written to it"
116
+
117
+
118
+ NOT_ITS_ACCOUNT = (
119
+ "{number} lives in another Twilio account than the one this call came from: refused"
120
+ )
121
+
122
+
123
+ NOT_APPROVED = (
124
+ "{number} was hooked by the org and waits for the box's operator to approve it: no call to it "
125
+ "opens until then"
126
+ )
127
+
128
+
129
+ SPENT = "token_spent"
130
+
131
+
132
+ ERROR = "error"
133
+
134
+
135
+ SWITCHED = "vendor.switched"
136
+
137
+
138
+ ALREADY_SPENT = "call {call} was opened by its token already: a token opens one call, once"
139
+
140
+
141
+ ELSEWHERE = "call %s was opened in org %s, %s, agent %s: its token was minted for another"
142
+
143
+
144
+ UNOPENED_KEY = "the key of call {call}'s recording is sealed under a key the vault no longer lists"
145
+
146
+
147
+ STILL_LIVE = "call {call} is still running: it can be erased once it has ended"
148
+
149
+
150
+ class LogQuery(BaseModel):
151
+ """What a log's reader asks for: the cursor, the types, only what is kept, a page's size."""
152
+
153
+ after: int = Field(0, ge=0)
154
+ types: str | None = None
155
+ durable: bool = False
156
+ limit: int = Field(DEFAULT_LIMIT, ge=1, le=DEFAULT_LIMIT)
157
+
158
+
159
+ class StreamOptions(BaseModel):
160
+ """The headers that choose a stream over a page, and where a reconnecting stream resumes."""
161
+
162
+ accept: str | None = None
163
+ last_event_id: str | None = None
164
+
165
+
166
+ # The log opens before the media, so a console sees the call ring; call.started is the worker's.
167
+ @router.post("/v1/calls")
168
+ async def open_call(body: OpenCallRequest, key: WorkerKey, gateway: GatewayDep) -> OpenCallResponse:
169
+ """Open a call's log, serve it to its agent's socket, say its minutes and its first words."""
170
+ context = body.context
171
+ keys.check_agent(key.bearer, body.agent)
172
+ scope = _call_corner(key, context)
173
+ if context.direction == "inbound" and context.route.number is not None:
174
+ await _arrived(gateway, scope, context.route.number, body.carrier_account)
175
+ await _unclaimed_or_in(gateway, context.call, scope)
176
+ await _spent(gateway, context, scope, body.agent)
177
+ ceiling = await _deps.admit_call(gateway, scope, body.agent)
178
+ found = serving_agent(gateway.sockets, scope, body.agent, body.app, context)
179
+ _refuse_unserved(gateway, scope, body, found)
180
+ config, versions = await _tuned(gateway, scope, body.agent, found, context.call)
181
+ # Asked again by a worker whose gateway died with the answer: the same call, one ringing.
182
+ again = await openings.opening_of(gateway.connections.pool, context.call) is not None
183
+ opener = _opener_of(key)
184
+ claim = Claim(scope, versions, opened_by=opener)
185
+ await gateway.logs.store.claim(context.call, body.agent, scope.org, claim)
186
+ # A worker opened the room's call: nothing is left to offer it to another.
187
+ await offers.forgotten(gateway.connections.pool, context.call)
188
+ await openings.kept(gateway.connections.pool, scope.org, context, config)
189
+ await gateway.prompts.keep(gateway.connections.pool, scope.org, config.knowledge or "")
190
+ owner = None if found is None else found.owner
191
+ served = served_call(gateway.serving, owner, context, config, scope)
192
+ gateway.live.opened_by(context.call, opener)
193
+ await gateway.live.commands_heard(context.call)
194
+ if not again:
195
+ await opened(served.log, context, body.agent)
196
+ first, notice = await _opening(gateway, scope, config.language)
197
+ return OpenCallResponse(
198
+ seconds_left=None if ceiling is None else ceiling.seconds,
199
+ minutes=None if ceiling is None else ceiling.minutes,
200
+ disclosure=first if context.direction == "outbound" else None,
201
+ recording_notice=notice,
202
+ )
203
+
204
+
205
+ # A gateway that restarted forgot the call: served again from the worker's word, no quota, no
206
+ # token, no second call.ringing; the socket holding its agent hears call.attached.
207
+ @router.post("/v1/calls/{call}/reopened", status_code=204)
208
+ async def reopen_call(
209
+ call: str, body: OpenCallRequest, key: WorkerKey, gateway: GatewayDep
210
+ ) -> None:
211
+ """Serve again a call the gateway forgot."""
212
+ if call != body.context.call:
213
+ raise DeclarationRefused(f"the context is call {body.context.call!r}")
214
+ if gateway.live.calls.get(call) is not None:
215
+ return
216
+ scope = _call_corner(key, body.context)
217
+ kept = await queries.scope_of_call(gateway.connections.pool, call)
218
+ if kept is None:
219
+ raise NotFound(NOT_OPEN.format(call=call))
220
+ if kept.scope != scope:
221
+ raise NotFound(_deps.NO_SUCH_CALL.format(call=call))
222
+ if kept.sealed:
223
+ raise Conflict(SEALED.format(call=call))
224
+ registration = gateway.sockets.serving(scope, body.agent, None)
225
+ config, _ = await _tuned(gateway, scope, body.agent, registration, call)
226
+ await openings.kept(gateway.connections.pool, scope.org, body.context, config)
227
+ served_call(gateway.serving, None, body.context, config, scope)
228
+ gateway.live.opened_by(call, kept.opened_by)
229
+ if registration is not None:
230
+ await attach(gateway.live, call, registration.owner)
231
+
232
+
233
+ # The entry comes back with its seq: only the gateway numbers a log.
234
+ @router.post("/v1/calls/{call}/events")
235
+ async def append_entry(
236
+ call: str, body: AppendEntryRequest, key: WorkerKey, gateway: GatewayDep
237
+ ) -> Entry:
238
+ """Write one entry of a call this gateway serves."""
239
+ check_kind(body.type)
240
+ served = await known(gateway, key, call)
241
+ began = time.perf_counter()
242
+ entry = await served.log.append(body.type, body.data, ephemeral=body.ephemeral)
243
+ counted(gateway, time.perf_counter() - began, [entry])
244
+ return entry
245
+
246
+
247
+ # Taken whole or refused whole; the answer to a retry of the last batch is the seqs it was given.
248
+ @router.post("/v1/calls/{call}/entries")
249
+ async def append_entries(
250
+ call: str, body: AppendEntriesRequest, key: WorkerKey, gateway: GatewayDep
251
+ ) -> AppendEntriesResponse:
252
+ """Write a worker's batch of a call this gateway serves, once and in order."""
253
+ for item in body.entries:
254
+ check_kind(item.type)
255
+ served = await known(gateway, key, call)
256
+ began = time.perf_counter()
257
+ entries = await served.log.append_many(body.entries, after=body.after)
258
+ counted(gateway, time.perf_counter() - began, entries)
259
+ return AppendEntriesResponse(entries=entries)
260
+
261
+
262
+ @router.post("/v1/calls/{call}/sealed", status_code=204)
263
+ async def seal_call(call: str, body: SealCallRequest, key: WorkerKey, gateway: GatewayDep) -> None:
264
+ """Price the call, write its summary and score, and seal its log."""
265
+ await sealed(gateway.serving, await known(gateway, key, call), body, lent=body.lent)
266
+
267
+
268
+ # The gateway writes tool.call and tool.result: appending the call is what reaches the app.
269
+ @router.post("/v1/calls/{call}/tools")
270
+ async def run_tool(
271
+ call: str,
272
+ agent: Annotated[str, Query()],
273
+ tool_call: ToolCall,
274
+ key: WorkerKey,
275
+ gateway: GatewayDep,
276
+ ) -> ToolResult:
277
+ """Run a worker's tool call through the app that holds its agent, and answer its result."""
278
+ served = await known(gateway, key, call)
279
+ if agent != served.agent:
280
+ raise NotFound(NOT_OPEN.format(call=call))
281
+ # Parked because nobody holds the agent: a refusal now, not a wait until the tool's deadline.
282
+ if served.app is None and gateway.sockets.of(served.scope, agent) is None:
283
+ raise Conflict(NO_AGENT.format(slug=agent))
284
+ use = ToolUse(
285
+ call_id=tool_call.call_id, name=tool_call.name, arguments=dict(tool_call.arguments)
286
+ )
287
+ # Its socket is on another gateway, which may never have heard the call was bound to it.
288
+ if served.app is not None and served.app not in gateway.live.sockets:
289
+ gateway.live.pumped(call, after=0)
290
+ return await served.tools.ran(use, tool_call.speech_id)
291
+
292
+
293
+ # The worker seals the call's recording with it before storing it; asked again, the same key.
294
+ @router.post("/v1/calls/{call}/recording/key")
295
+ async def recording_key(call: str, key: WorkerKey, gateway: GatewayDep) -> RecordingKeyResponse:
296
+ """The key the call's recording is sealed under, made once for the call."""
297
+ served = await known(gateway, key, call)
298
+ connections = gateway.connections
299
+ found = await recording_keys.key_for(
300
+ connections.pool, connections.vault, served.scope.org, call
301
+ )
302
+ if found is None:
303
+ raise Conflict(UNOPENED_KEY.format(call=call))
304
+ return RecordingKeyResponse(key=base64.urlsafe_b64encode(found).decode())
305
+
306
+
307
+ # No id: commands have no seq, and a worker that loses the stream asks again.
308
+ @router.get("/v1/calls/{call}/commands")
309
+ async def stream_commands(call: str, key: WorkerKey, gateway: GatewayDep) -> StreamingResponse:
310
+ """The app's commands for the call, in order, until it is sealed."""
311
+ served = await known(gateway, key, call)
312
+ await gateway.live.commands_heard(call)
313
+ return streamed(_commanded(served.commands), gateway.closing)
314
+
315
+
316
+ @router.get("/v1/calls/{call}/judging")
317
+ async def call_judging(call: str, key: WorkerKey, gateway: GatewayDep) -> JudgingSettings:
318
+ """Whether the call's org judges its calls at hang-up."""
319
+ served = await known(gateway, key, call)
320
+ pool = gateway.connections.pool
321
+ on = await orgs.judged(pool, served.scope.org)
322
+ return JudgingSettings(on=on, ceiling_usd=judge_ceiling(await catalog.providers(pool)))
323
+
324
+
325
+ # The gateway writes memory.ops and docs.sources on the log itself: the worker has no database.
326
+ @router.post("/v1/calls/{call}/lookup")
327
+ async def lookup(
328
+ call: str, body: LookupRequest, key: WorkerKey, gateway: GatewayDep
329
+ ) -> LookupResponse:
330
+ """Recall or search for a call served here, answered as the model reads it."""
331
+ served = await known(gateway, key, call)
332
+ started = time.perf_counter()
333
+ output = await looked_up(gateway.serving, served, body)
334
+ return LookupResponse(output=output, took_ms=(time.perf_counter() - started) * 1000)
335
+
336
+
337
+ @router.post("/v1/calls/{call}/remember")
338
+ async def remember(call: str, key: WorkerKey, gateway: GatewayDep) -> RememberResponse:
339
+ """Write what the call taught into its contact's memory now, as the seal would."""
340
+ served = await known(gateway, key, call)
341
+ started = time.perf_counter()
342
+ written = await remembered(gateway.serving, served)
343
+ ops = 0 if written is None else len(written.op.facts)
344
+ return RememberResponse(ops=ops, took_ms=(time.perf_counter() - started) * 1000)
345
+
346
+
347
+ # A code nobody issued is a 404, and an ordinary one: the caller may be dialling an extension.
348
+ @router.post("/v1/calls/{call}/claim", status_code=204)
349
+ async def claim_keypad_code(
350
+ call: str, call_claim: CallClaim, key: WorkerKey, gateway: GatewayDep
351
+ ) -> None:
352
+ """The caller keyed a page's code: tie the call to it."""
353
+ served = await known(gateway, key, call)
354
+ if not await claim_code(gateway.codes, served, call_claim.code, via="keypad"):
355
+ raise NotFound(_deps.NOBODY_ISSUED.format(code=call_claim.code, agent=served.agent))
356
+
357
+
358
+ @router.get("/v1/calls/{call}/events", response_model=None)
359
+ async def stream_events(
360
+ call: str,
361
+ reading: CallReaderDep,
362
+ gateway: GatewayDep,
363
+ query: Annotated[LogQuery, Query()],
364
+ options: Annotated[StreamOptions, Header()],
365
+ ) -> Response | StreamingResponse | LogPage:
366
+ """A call's entries above the cursor: a page, or a stream that ends with the call."""
367
+ config = await _deps.check_readable(gateway, reading, call)
368
+ await _deps.record_read(gateway, reading, call, "log")
369
+ cursor = max(query.after, _seq_of(options.last_event_id))
370
+ only = parse_filter(query.types, durable=query.durable)
371
+ store = gateway.logs.store
372
+ over = await store.sealed(call)
373
+ if over and cursor >= await store.latest_seq(call):
374
+ return Response(status_code=204)
375
+ if wants_sse(options.accept):
376
+ entries = gateway.logs.reading(call).stream(after=cursor, only=only)
377
+ return streamed(_projected(entries, reading, config, ends=True), gateway.closing)
378
+ read = await store.since(call, after=cursor, limit=query.limit)
379
+ return _page(read, only, reading, config, live=not over)
380
+
381
+
382
+ @router.get("/v1/agents/{slug}/calls", response_model=None)
383
+ async def stream_agent_events(
384
+ slug: str,
385
+ reading: ReaderDep,
386
+ gateway: GatewayDep,
387
+ query: Annotated[LogQuery, Query()],
388
+ options: Annotated[StreamOptions, Header()],
389
+ ) -> StreamingResponse | LogPage:
390
+ """An agent's own log: its registrations, its declarations, its errors. It never ends."""
391
+ if reading.acting is None:
392
+ raise NotAllowed(_deps.NOT_YOURS)
393
+ owner = await gateway.logs.store.owner(slug)
394
+ if owner is not None and owner != reading.acting.org:
395
+ raise NotFound(NO_AGENT.format(slug=slug))
396
+ config = gateway.sockets.declared(slug)
397
+ only = parse_filter(query.types, durable=query.durable)
398
+ if wants_sse(options.accept):
399
+ entries = gateway.logs.agent(slug).stream(after=query.after, only=only)
400
+ return streamed(_projected(entries, reading, config, ends=False), gateway.closing)
401
+ read = await gateway.logs.store.since(f"@{slug}", after=query.after, limit=query.limit)
402
+ return _page(read, only, reading, config, live=True)
403
+
404
+
405
+ @router.get("/v1/calls/{call}/state")
406
+ async def call_state(call: str, reading: CallReaderDep, gateway: GatewayDep) -> JsonObject:
407
+ """The call's folded state as this reader may see it, and the seq a stream resumes from."""
408
+ config = await _deps.check_readable(gateway, reading, call)
409
+ await _deps.record_read(gateway, reading, call, "log")
410
+ state = await gateway.logs.reading(call).snapshot()
411
+ if state.seq == 0:
412
+ raise NotFound(_deps.NO_SUCH_CALL.format(call=call))
413
+ viewer = None if reading.visit is None else reading.visit.identity
414
+ return {
415
+ "state": project_state(state, reading.projection, config, viewer),
416
+ "last_seq": state.seq,
417
+ "live": not await gateway.logs.store.sealed(call),
418
+ }
419
+
420
+
421
+ # The one door that deletes from a call's log: through erasure's own path, never the trigger's.
422
+ @router.delete("/v1/calls/{call}")
423
+ async def erase_call(call: str, key: TeamKey, where: ScopeDep, gateway: GatewayDep) -> Erasure:
424
+ """Erase one call: its log, facts, tokens, the memories it taught and its recording."""
425
+ kept = await queries.scope_of_call(gateway.connections.pool, call)
426
+ if kept is None or kept.scope is None or not _sees_to_erase(where, kept.scope):
427
+ raise NotFound(_deps.NO_SUCH_CALL.format(call=call))
428
+ keys.check_agent(key.bearer, kept.agent)
429
+ if not kept.sealed:
430
+ raise Conflict(STILL_LIVE.format(call=call))
431
+ recordings = recordings_of(gateway.connections.settings, gateway.connections.http)
432
+ erased = await erasure.call(
433
+ gateway.connections.pool, recordings, kept.scope, call, by=asked_by(key)
434
+ )
435
+ gateway.logs.forget(call)
436
+ return erased.trail
437
+
438
+
439
+ # Live only, no cursor: every entry of the feed is also in a log, which is where to resume.
440
+ @router.get("/v1/events", response_model=None)
441
+ async def stream_org_events(reading: ReaderDep, gateway: GatewayDep) -> StreamingResponse:
442
+ """The org's calls and agents changing, as they change."""
443
+ if reading.acting is None:
444
+ raise NotAllowed(_deps.NOT_YOURS)
445
+ feed = await gateway.logs.feed_reader(reading.acting.org, reading.acting.env)
446
+ return streamed(_projected(feed, reading, None, ends=False), gateway.closing)
447
+
448
+
449
+ # An id grants nothing: another org's or world's call is the same 404 as a call nobody opened.
450
+ # The fleet's key serves every org of its world; a tenant's worker its own org alone.
451
+ # A call this gateway serves, or one another gateway opened, served here from what was kept when
452
+ # it opened: one read, the first time a door here asks. A call opened by a release that kept
453
+ # nothing is the 404 a worker answers by saying the call again (`/reopened`).
454
+ # What a door of a call this gateway serves starts from; the socket door starts from it too.
455
+ async def known(gateway: Gateway, key: Acting, call: str) -> Served:
456
+ """The call as this gateway serves it, first seen here if need be; a refusal names why."""
457
+ now = time.monotonic()
458
+ served = gateway.live.calls.get(call)
459
+ if served is not None:
460
+ _yours(key, served.scope, served.opened_by, call)
461
+ gateway.live.in_use(call, now)
462
+ return served
463
+ pool = gateway.connections.pool
464
+ kept = await queries.scope_of_call(pool, call)
465
+ if kept is None or kept.scope is None:
466
+ raise NotFound(NOT_OPEN.format(call=call))
467
+ _yours(key, kept.scope, kept.opened_by, call)
468
+ if kept.sealed:
469
+ raise Conflict(SEALED.format(call=call))
470
+ opening = await openings.opening_of(pool, call)
471
+ if opening is None:
472
+ raise NotFound(NOT_OPEN.format(call=call))
473
+ return first_seen(gateway.serving, opening.context, opening.config, kept, now)
474
+
475
+
476
+ def check_kind(kind: str) -> None:
477
+ """Refuse a type the protocol lacks, or one the gateway alone writes on a log."""
478
+ if kind not in EVENTS:
479
+ raise DeclarationRefused(UNKNOWN_EVENT.format(kind=kind))
480
+ if kind in GATEWAY_WRITES:
481
+ raise NotAllowed(THE_GATEWAYS.format(kind=kind))
482
+
483
+
484
+ # What /metrics reads: the append's time at the door, and each vendor's failures as they come in.
485
+ # A vendor fails a call when an error names its plugin, or when a stage switches away from it.
486
+ def counted(gateway: Gateway, seconds: float, entries: list[Entry]) -> None:
487
+ """Count an append's time and entries, and what they say of vendors, for /metrics."""
488
+ counters = gateway.counters
489
+ counters.appended_in(seconds, len(entries))
490
+ at = time.monotonic()
491
+ for entry in entries:
492
+ vendor = ""
493
+ if entry.type == ERROR:
494
+ code, message = entry.data.get("code"), entry.data.get("message")
495
+ vendor = vendor_named_in(message if isinstance(message, str) else "")
496
+ counters.failed(code if isinstance(code, str) else "", vendor)
497
+ elif entry.type == SWITCHED and entry.data.get("available") is False:
498
+ named = entry.data.get("vendor")
499
+ vendor = named if isinstance(named, str) else ""
500
+ if vendor and entry.call is not None:
501
+ counters.failed_on(vendor, entry.call, at)
502
+
503
+
504
+ # A tenant's worker opens its own org's calls in its own world; the fleet's opens any org's in
505
+ # its world, in the scope the dispatch named.
506
+ def _call_corner(key: Acting, context: CallContext) -> Scope:
507
+ named = Scope(context.route.org, context.env, context.holder or "")
508
+ fleet = THE_FLEET in key.bearer.key.scopes
509
+ if not fleet and context.route.org != key.org:
510
+ raise NotFound(_deps.NO_SUCH_CALL.format(call=context.call))
511
+ if not fleet and context.env != key.env:
512
+ raise NotAllowed(
513
+ f"this key opens {key.env}, and that call's route answers in {context.env}"
514
+ )
515
+ return keys.scope_of(key.bearer, key.env, dispatched=named)
516
+
517
+
518
+ # A call the fleet opened is the fleet's to write; a written call runs here and no worker door
519
+ # serves it; one an org's own worker opened is that org's. One opened before the head said who is
520
+ # served as before.
521
+ def _yours(key: Acting, scope: Scope, opened_by: Opener | None, call: str) -> None:
522
+ fleet = THE_FLEET in key.bearer.key.scopes
523
+ if scope.env != key.env or (not fleet and scope.org != key.org):
524
+ raise NotFound(NOT_OPEN.format(call=call))
525
+ if opened_by == "gateway" or (opened_by == "fleet" and not fleet):
526
+ raise NotFound(NOT_OPEN.format(call=call))
527
+
528
+
529
+ def _opener_of(key: Acting) -> Opener:
530
+ return "fleet" if THE_FLEET in key.bearer.key.scopes else "app"
531
+
532
+
533
+ # The carrier, the fence and the world rule worked, whatever is refused here. A number the org
534
+ # hooked rings nobody until the operator approves it is the org's; a call Twilio sent from an
535
+ # account the number does not live in is somebody else's Twilio pointed at it.
536
+ async def _arrived(gateway: Gateway, scope: Scope, number: str, came_from: str | None) -> None:
537
+ pool = gateway.connections.pool
538
+ await routes.called(pool, scope.org, number)
539
+ if await routes.is_waiting(pool, scope.org, number):
540
+ raise NotAllowed(NOT_APPROVED.format(number=number))
541
+ if came_from is None:
542
+ return
543
+ expected = await carrier.twilio_account_of(gateway.connections, scope.org, number)
544
+ if expected is not None and expected != came_from:
545
+ raise NotAllowed(NOT_ITS_ACCOUNT.format(number=number))
546
+
547
+
548
+ # A call a dial placed already has its head: the worker opens it in the scope the head keeps, or
549
+ # not at all, whatever its key; the first claim of a head stands, so this is the one check.
550
+ async def _unclaimed_or_in(gateway: Gateway, call: str, scope: Scope) -> None:
551
+ kept = await queries.scope_of_call(gateway.connections.pool, call)
552
+ if kept is not None and kept.scope is not None and kept.scope != scope:
553
+ raise NotFound(_deps.NO_SUCH_CALL.format(call=call))
554
+
555
+
556
+ # A web call opens by the token its room was minted with: once, and only in the org, the world
557
+ # and for the agent it was minted for. Asked of the ledger by the call's id, whatever the worker
558
+ # says: a call no token was minted for (a phone, a dial, a run) is not a token's to refuse.
559
+ async def _spent(gateway: Gateway, context: CallContext, scope: Scope, agent: str) -> None:
560
+ spending = await tokens.spend(gateway.connections.pool, context.call, scope, agent)
561
+ if spending in {"spent", "never_minted"}:
562
+ return
563
+ if spending == "minted_elsewhere":
564
+ logger.warning(ELSEWHERE, context.call, scope.org, scope.env, agent)
565
+ raise NotFound(_deps.NO_SUCH_CALL.format(call=context.call))
566
+ sentence = ALREADY_SPENT.format(call=context.call)
567
+ refused = ErrorEvent(code=SPENT, message=sentence, recoverable=True)
568
+ await gateway.logs.agent(agent).append("error", refused.written())
569
+ raise Conflict(sentence)
570
+
571
+
572
+ def _refuse_unserved(
573
+ gateway: Gateway, scope: Scope, body: OpenCallRequest, registration: Registration | None
574
+ ) -> None:
575
+ if body.app is not None and registration is None:
576
+ raise Conflict(NOT_THAT_APP.format(app=body.app, slug=body.agent))
577
+ # Held by consoles alone: refused before the greeting, rather than a call with no app.
578
+ if registration is None and gateway.sockets.of(scope, body.agent) is not None:
579
+ raise Conflict(NO_UNCLAIMED.format(slug=body.agent))
580
+
581
+
582
+ # The call's id picks its version where the scope stands on a canary, the same on a reopen.
583
+ async def _tuned(
584
+ gateway: Gateway, scope: Scope, agent: str, registration: Registration | None, call: str
585
+ ) -> tuple[AgentConfig, Versions]:
586
+ declared = gateway.sockets.of(scope, agent) if registration is None else registration
587
+ config = AgentConfig(slug=agent) if declared is None else declared.config
588
+ configured = await catalog.providers(gateway.connections.pool)
589
+ return await tuned(gateway.connections.pool, config, scope, configured, Picked(call=call))
590
+
591
+
592
+ # The worker says the disclosure before the greeting, and the notice only where it records.
593
+ async def _opening(
594
+ gateway: Gateway, scope: Scope, language: str | None
595
+ ) -> tuple[str | None, str | None]:
596
+ pool = gateway.connections.pool
597
+ kept = (await policy.policy_of(pool, scope.org)).policy
598
+ org = await orgs.find(pool, scope.org)
599
+ name = scope.org if org is None else org.name
600
+ return disclosure.disclosure_of(kept, name, language), disclosure.notice_of(kept, language)
601
+
602
+
603
+ def _page(
604
+ read: list[Entry], only: Filter, reading: Reader, config: AgentConfig | None, *, live: bool
605
+ ) -> LogPage:
606
+ viewer = None if reading.visit is None else reading.visit.identity
607
+ kept = (
608
+ project_entry(item, reading.projection, config, viewer)
609
+ for item in read
610
+ if only.passes(item)
611
+ )
612
+ # `next` is the last seq read, not kept, so a page that kept nothing still moves the cursor.
613
+ return LogPage(
614
+ entries=[kept_one for kept_one in kept if kept_one is not None],
615
+ live=live,
616
+ next=read[-1].seq if read else None,
617
+ )
618
+
619
+
620
+ async def _projected(
621
+ entries: AsyncIterator[Entry], reading: Reader, config: AgentConfig | None, *, ends: bool
622
+ ) -> AsyncIterator[str]:
623
+ viewer = None if reading.visit is None else reading.visit.identity
624
+ yield f"retry: {_streams.RETRY_MS}\n\n"
625
+ async for entry in paced(entries):
626
+ if entry is None:
627
+ yield _streams.PING
628
+ continue
629
+ data = project_entry(entry, reading.projection, config, viewer)
630
+ if data is not None:
631
+ yield frame(entry.type, data, seq=entry.seq)
632
+ if ends and entry.type == TERMINAL_EVENT:
633
+ return
634
+
635
+
636
+ async def _commanded(waiting: asyncio.Queue[Command | None]) -> AsyncIterator[str]:
637
+ async def taken() -> AsyncIterator[Command]:
638
+ while (command := await waiting.get()) is not None:
639
+ yield command
640
+
641
+ async for command in paced(taken()):
642
+ yield _streams.PING if command is None else frame(command.type, command.written())
643
+
644
+
645
+ def _seq_of(header: str | None) -> int:
646
+ try:
647
+ return max(int(header or 0), 0)
648
+ except ValueError:
649
+ return 0
650
+
651
+
652
+ # A key erases what it may read: its org and world, and a colleague's corner only as the org's own.
653
+ def _sees_to_erase(where: Scope, owner: Scope) -> bool:
654
+ same = where.org == owner.org and where.env == owner.env
655
+ return same and owner.holder in ("", where.holder)