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,135 @@
1
+ """What a call's log keeps of the values its agent declared private: masked, and sealed aside."""
2
+
3
+ import json
4
+ import logging
5
+ from collections.abc import Sequence
6
+ from dataclasses import dataclass
7
+
8
+ from cryptography.fernet import InvalidToken, MultiFernet
9
+
10
+ from pinecall.domain.agent import AgentConfig
11
+ from pinecall.domain.names import Json, JsonObject
12
+ from pinecall.log.store import Store
13
+ from pinecall.wire.frames import Entry
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ # The key stays, so a reader knows a value exists; the value goes whole, so its type does not leak.
19
+ MASK = "***"
20
+
21
+
22
+ # The entries whose data carries the app's state, masked against the state fields declared pii.
23
+ CARRY_STATE = frozenset({"state.changed", "call.attached"})
24
+
25
+
26
+ # A tool's arguments, masked against the names its declaration lists under pii.
27
+ CARRY_ARGUMENTS = "tool.call"
28
+
29
+
30
+ # One row per entry that held a private value: the values, sealed under the box's vault key.
31
+ KEEP = """
32
+ INSERT INTO call_private (log, seq, sealed)
33
+ SELECT %(log)s, seq, sealed FROM unnest(%(seqs)s::bigint[], %(sealed)s::text[]) AS kept(seq, sealed)
34
+ ON CONFLICT (log, seq) DO NOTHING
35
+ """
36
+
37
+ OF_LOG = "SELECT seq, sealed FROM call_private WHERE log = %(log)s"
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class Privacy:
42
+ """What a call's writer masks by: the vault it seals with, the declaration it reads."""
43
+
44
+ vault: MultiFernet
45
+ config: AgentConfig
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class Split:
50
+ """An entry's data as the log keeps it, and the values masked in it, by their field."""
51
+
52
+ kept: JsonObject
53
+ private: JsonObject
54
+
55
+
56
+ def split(kind: str, data: JsonObject, config: AgentConfig) -> Split:
57
+ """The entry with every value its agent declared private masked, and those values apart."""
58
+ marked = _marked(kind, data, config)
59
+ if marked is None:
60
+ return Split(data, {})
61
+ field, names = marked
62
+ values = data.get(field)
63
+ if not isinstance(values, dict):
64
+ return Split(data, {})
65
+ private: JsonObject = {name: value for name, value in values.items() if name in names}
66
+ if not private:
67
+ return Split(data, {})
68
+ return Split({**data, field: {**values, **dict.fromkeys(private, MASK)}}, {field: private})
69
+
70
+
71
+ def masked_state(app_state: JsonObject, config: AgentConfig) -> JsonObject:
72
+ """The app's state with every field declared pii masked."""
73
+ return {
74
+ name: MASK if config.visibility_of(name) == "pii" else value
75
+ for name, value in app_state.items()
76
+ }
77
+
78
+
79
+ def restored(entry: Entry, private: JsonObject | None) -> Entry:
80
+ """The entry as its writer wrote it: the private values back where they were masked."""
81
+ if not private:
82
+ return entry
83
+ data = dict(entry.data)
84
+ for field, values in private.items():
85
+ kept = data.get(field)
86
+ if isinstance(kept, dict) and isinstance(values, dict):
87
+ data[field] = {**kept, **values}
88
+ return entry.model_copy(update={"data": data})
89
+
90
+
91
+ async def kept_aside(
92
+ store: Store, vault: MultiFernet, log: str, private: Sequence[tuple[int, JsonObject]]
93
+ ) -> None:
94
+ """Seal the private values of these entries of the log, by seq; one kept already stays."""
95
+ if not private:
96
+ return
97
+ rows = {
98
+ "log": log,
99
+ "seqs": [seq for seq, _ in private],
100
+ "sealed": [vault.encrypt(json.dumps(values).encode()).decode() for _, values in private],
101
+ }
102
+ async with store.pool.connection() as connection:
103
+ await connection.execute(KEEP, rows)
104
+
105
+
106
+ # Read only where a call is taken up: an app taking it over, a written call after a restart.
107
+ async def opened_whole(store: Store, vault: MultiFernet, log: str) -> list[Entry]:
108
+ """Every entry of the log as its writer wrote it, the private values opened."""
109
+ entries = await store.whole(log)
110
+ async with store.pool.connection() as connection:
111
+ rows = await (await connection.execute(OF_LOG, {"log": log})).fetchall()
112
+ sealed = {int(row["seq"]): str(row["sealed"]) for row in rows}
113
+ return [restored(entry, _opened(vault, sealed.get(entry.seq))) for entry in entries]
114
+
115
+
116
+ # A value sealed under a key PINECALL_VAULT_KEY no longer lists stays masked: the call goes on.
117
+ def _opened(vault: MultiFernet, token: str | None) -> JsonObject | None:
118
+ if token is None:
119
+ return None
120
+ try:
121
+ values: Json = json.loads(vault.decrypt(token.encode()))
122
+ except InvalidToken:
123
+ logger.warning("a private value sealed under a key the vault no longer lists stays masked")
124
+ return None
125
+ return values if isinstance(values, dict) else None
126
+
127
+
128
+ def _marked(kind: str, data: JsonObject, config: AgentConfig) -> tuple[str, frozenset[str]] | None:
129
+ if kind == CARRY_ARGUMENTS:
130
+ spec = config.tools_by_name.get(str(data.get("name")))
131
+ return None if spec is None else ("arguments", spec.pii)
132
+ if kind in CARRY_STATE:
133
+ declared = frozenset(name for name, seen in config.state_fields.items() if seen == "pii")
134
+ return "state", declared
135
+ return None
@@ -0,0 +1,229 @@
1
+ """One call's facts as a gateway asks them: where it opened, its tool calls, whether it sealed."""
2
+
3
+ from collections.abc import Sequence
4
+ from dataclasses import dataclass
5
+ from typing import LiteralString
6
+
7
+ from pinecall.domain.agent import Versions
8
+ from pinecall.domain.call import Opener
9
+ from pinecall.domain.names import Env, parse_env
10
+ from pinecall.domain.scope import Scope
11
+ from pinecall.log.facts import CallFacts, facts_of
12
+ from pinecall.log.store import entry_of
13
+ from pinecall.postgres.pool import Pool, box_wide
14
+ from pinecall.wire.frames import Entry
15
+ from pinecall.wire.parts import ToolResult
16
+
17
+ # Head rows older than the env and holder columns read as production, the org's own.
18
+ CORNER_OF_CALL = """
19
+ select org, coalesce(env, 'production') as env, coalesce(holder, '') as holder, agent,
20
+ config_version, lexicon_version, sealed, started_at, written, opened_by
21
+ from call_log_head where log = %(call)s and call is not null
22
+ """
23
+
24
+
25
+ ENVS_OF = """
26
+ select log as call, coalesce(env, 'production') as env
27
+ from call_log_head where log = any(%(calls)s) and call is not null
28
+ """
29
+
30
+
31
+ FACTS_OF = """
32
+ select f.*, head.agent
33
+ from call_facts f join call_log_head head on head.log = f.call
34
+ where f.call = any(%(calls)s)
35
+ """
36
+
37
+
38
+ # call_log is joined on log, its primary key's first column, never on call, which scans it.
39
+ # A call that never reached call.started may have no facts row, hence the left join.
40
+ # A head claimed at the open, whose first entry waits on the writer, moved last when it opened; a
41
+ # head with no entry, no start and no opening (a seal's lease) is never quiet. Under load this is
42
+ # the whole window: at 300 calls held the reaper sealed calls 36 ms after they opened (2026-10-01).
43
+ UNSEALED_SPOKEN = """
44
+ select head.log as call, head.agent,
45
+ coalesce(head.started_at, 0) as started_at,
46
+ coalesce(max(entry.ts), head.started_at, extract(epoch from opening.opened_at)) as last_at,
47
+ null as channel, head.env
48
+ from call_log_head head
49
+ left join call_facts f on f.call = head.log
50
+ left join call_openings opening on opening.call = head.log
51
+ left join call_log entry on entry.log = head.log
52
+ where head.call is not null and not head.sealed
53
+ and (coalesce(f.spoken, false)
54
+ or not exists (select 1 from call_log began
55
+ where began.log = head.log and began.type = 'call.started'))
56
+ group by head.log, head.agent, head.started_at, head.env, opening.opened_at
57
+ having coalesce(max(entry.ts), head.started_at, extract(epoch from opening.opened_at))
58
+ < %(quiet_since)s
59
+ order by last_at
60
+ limit %(limit)s
61
+ """
62
+
63
+
64
+ UNSEALED_WRITTEN = """
65
+ select head.log as call, head.agent,
66
+ coalesce(head.started_at, 0) as started_at,
67
+ coalesce(max(entry.ts), head.started_at, 0) as last_at, f.channel, head.env
68
+ from call_log_head head
69
+ join call_facts f on f.call = head.log
70
+ left join call_log entry on entry.log = head.log
71
+ where head.call is not null and not head.sealed and not coalesce(f.spoken, false)
72
+ and exists (select 1 from call_log began
73
+ where began.log = head.log and began.type = 'call.started')
74
+ group by head.log, head.agent, head.started_at, head.env, f.channel
75
+ having coalesce(max(entry.ts), head.started_at, 0) < %(quiet_since)s
76
+ order by last_at
77
+ limit %(limit)s
78
+ """
79
+
80
+
81
+ TOOL_ANSWERED = """
82
+ select data from call_log
83
+ where log = %(log)s and type = 'tool.result' and data->>'call_id' = %(call_id)s
84
+ order by seq
85
+ limit 1
86
+ """
87
+
88
+ # A tool call id the log asked already: a retry that reached another gateway waits for its answer.
89
+ TOOL_CALLED = """
90
+ select call, seq, ts, agent, type, ephemeral, data from call_log
91
+ where log = %(log)s and type = 'tool.call' and data->>'call_id' = %(call_id)s
92
+ order by seq
93
+ limit 1
94
+ """
95
+
96
+ # The head's word on who opened a call, read back as the type; anything else is nobody's.
97
+ OPENERS: dict[str, Opener] = {"fleet": "fleet", "app": "app", "gateway": "gateway"}
98
+
99
+
100
+ # Which of the calls a gateway serves another gateway sealed: one read of their heads.
101
+ SEALED_AMONG = """
102
+ select log from call_log_head where log = any(%(calls)s) and sealed
103
+ """
104
+
105
+
106
+ @dataclass(frozen=True, slots=True)
107
+ class CallScope:
108
+ """Where a call was opened: its scope, its agent, and what its head row says."""
109
+
110
+ scope: Scope | None
111
+ agent: str
112
+ versions: Versions
113
+ sealed: bool
114
+ started_at: float | None
115
+ # The entries its worker's writer sent: where a writer that takes the call over follows on.
116
+ written: int
117
+ # Who opened it; None for a call opened before the head said.
118
+ opened_by: Opener | None = None
119
+
120
+
121
+ @dataclass(frozen=True, slots=True)
122
+ class Unsealed:
123
+ """A call still open, for the reaper: its start, its last move, its channel and its world."""
124
+
125
+ call: str
126
+ agent: str
127
+ started_at: float
128
+ last_at: float
129
+ channel: str | None
130
+ # None for a log no scope ever claimed.
131
+ env: Env | None
132
+
133
+
134
+ async def sealed_among(pool: Pool, calls: list[str]) -> set[str]:
135
+ """The ones of these calls whose log is sealed, wherever it was sealed."""
136
+ if not calls:
137
+ return set()
138
+ async with pool.connection() as connection:
139
+ rows = await (await connection.execute(SEALED_AMONG, {"calls": calls})).fetchall()
140
+ return {str(row["log"]) for row in rows}
141
+
142
+
143
+ async def scope_of_call(pool: Pool, call: str) -> CallScope | None:
144
+ """Return where the call was opened, or None for a call nobody wrote to or claimed."""
145
+ # Whose a call is decides who may act on it: asked box-wide, so another org's call is that
146
+ # org's (a 404 to the asker), never a call nobody opened that the asker could claim.
147
+ with box_wide():
148
+ async with pool.connection() as connection:
149
+ found = await connection.execute(CORNER_OF_CALL, {"call": call})
150
+ row = await found.fetchone()
151
+ if row is None:
152
+ return None
153
+ env: Env = "sandbox" if row["env"] == "sandbox" else "production"
154
+ return CallScope(
155
+ scope=None if row["org"] is None else Scope(row["org"], env, row["holder"]),
156
+ agent=row["agent"] or "",
157
+ versions=Versions(config=row["config_version"], lexicon=row["lexicon_version"]),
158
+ sealed=row["sealed"],
159
+ started_at=row["started_at"],
160
+ written=row["written"],
161
+ opened_by=_opener(row["opened_by"]),
162
+ )
163
+
164
+
165
+ # Read only by a tool's round trip: the log's primary key starts with `log`, so it reads one call.
166
+ async def tool_answered(pool: Pool, call: str, call_id: str) -> ToolResult | None:
167
+ """The `tool.result` the call's log holds for this tool call id, or None."""
168
+ wanted = {"log": call, "call_id": call_id}
169
+ async with pool.connection() as connection:
170
+ row = await (await connection.execute(TOOL_ANSWERED, wanted)).fetchone()
171
+ return None if row is None else ToolResult.model_validate(row["data"])
172
+
173
+
174
+ async def tool_called(pool: Pool, call: str, call_id: str) -> Entry | None:
175
+ """The `tool.call` entry the call's log holds for this tool call id, or None."""
176
+ wanted = {"log": call, "call_id": call_id}
177
+ async with pool.connection() as connection:
178
+ row = await (await connection.execute(TOOL_CALLED, wanted)).fetchone()
179
+ return None if row is None else entry_of(row)
180
+
181
+
182
+ async def envs_of_calls(pool: Pool, calls: Sequence[str]) -> dict[str, Env]:
183
+ """Return the world each of the calls was opened in, for the calls that have a log."""
184
+ async with pool.connection() as connection:
185
+ rows = await (await connection.execute(ENVS_OF, {"calls": list(calls)})).fetchall()
186
+ return {str(row["call"]): parse_env(str(row["env"])) for row in rows}
187
+
188
+
189
+ async def facts_of_calls(pool: Pool, calls: Sequence[str]) -> dict[str, CallFacts]:
190
+ """Return the facts of each of the calls that has a row."""
191
+ async with pool.connection() as connection:
192
+ rows = await (await connection.execute(FACTS_OF, {"calls": list(calls)})).fetchall()
193
+ return {str(row["call"]): facts_of(row) for row in rows}
194
+
195
+
196
+ # Across every org: the reaper's question. A call that never reached call.started is reaped
197
+ # whatever its channel.
198
+ async def unsealed_spoken(pool: Pool, quiet_since: float, *, limit: int) -> list[Unsealed]:
199
+ """Return the spoken or never-started calls still open and quiet since then, oldest first."""
200
+ return await _unsealed(pool, UNSEALED_SPOKEN, quiet_since, limit)
201
+
202
+
203
+ # A written call idles out where it runs; this finds the ones a restart left open.
204
+ async def unsealed_written(pool: Pool, quiet_since: float, *, limit: int) -> list[Unsealed]:
205
+ """Return the written calls still open and quiet since then, with their channel."""
206
+ return await _unsealed(pool, UNSEALED_WRITTEN, quiet_since, limit)
207
+
208
+
209
+ def _opener(named: object) -> Opener | None:
210
+ return OPENERS.get(named) if isinstance(named, str) else None
211
+
212
+
213
+ async def _unsealed(
214
+ pool: Pool, query: LiteralString, quiet_since: float, limit: int
215
+ ) -> list[Unsealed]:
216
+ async with pool.connection() as connection:
217
+ cursor = await connection.execute(query, {"quiet_since": quiet_since, "limit": limit})
218
+ rows = await cursor.fetchall()
219
+ return [
220
+ Unsealed(
221
+ call=str(row["call"]),
222
+ agent=str(row["agent"] or ""),
223
+ started_at=float(row["started_at"]),
224
+ last_at=float(row["last_at"]),
225
+ channel=row["channel"],
226
+ env=None if row["env"] is None else parse_env(str(row["env"])),
227
+ )
228
+ for row in rows
229
+ ]
@@ -0,0 +1,207 @@
1
+ """What each reader receives: the filter it asked for, and the projection its credential allows."""
2
+
3
+ import re
4
+ from collections.abc import Mapping
5
+ from dataclasses import dataclass
6
+
7
+ from pinecall.domain.agent import AgentConfig
8
+ from pinecall.domain.errors import DeclarationRefused
9
+ from pinecall.domain.names import Json, JsonObject
10
+ from pinecall.log.private import masked_state, split
11
+ from pinecall.wire.events import TERMINAL_EVENT, AgentTurnEnded, LogGap, StateChanged, event_of
12
+ from pinecall.wire.frames import Entry, WireModel
13
+ from pinecall.wire.parts import Projection
14
+ from pinecall.wire.state import AgentTurn, State, Turn
15
+
16
+ MAX_TYPES = 32
17
+
18
+
19
+ _A_TYPE_NAME = re.compile(r"^[a-z0-9_.]+$")
20
+
21
+
22
+ # A reader that filtered these out could wait forever on a log that already ended.
23
+ ALWAYS_PASS = frozenset({"log.gap", "log.caught_up", "call.ended", TERMINAL_EVENT})
24
+
25
+
26
+ NEEDS_READING = frozenset({"turn.agent", "state.changed", "log.gap"})
27
+
28
+
29
+ # The contract's table (docs/protocol/projections.md), row by row: a type absent is dropped
30
+ # whole, a field absent is dropped.
31
+ PUBLIC_ENTRY_FIELDS: Mapping[str, tuple[str, ...]] = {
32
+ "call.ringing": ("channel",),
33
+ "call.dialing": ("channel",),
34
+ "call.started": ("channel", "direction", "started_at"),
35
+ "call.ended": ("reason", "ended_by", "ended_at", "duration_s"),
36
+ "user.state": ("state",),
37
+ "agent.state": ("state",),
38
+ "user.transcript": ("text", "final"),
39
+ "agent.transcript": ("speech_id", "text", "final"),
40
+ "turn.user": ("speech_id", "text"),
41
+ "turn.agent": ("speech_id", "text", "interrupted", "metrics"),
42
+ "room.opened": ("name", "sid"),
43
+ "participant.joined": ("identity", "kind", "name"),
44
+ "participant.left": ("identity",),
45
+ "participant.speaking": ("identity", "speaking"),
46
+ "confirm.request": ("phrase", "ttl_s"),
47
+ "confirm.granted": (),
48
+ "confirm.declined": (),
49
+ "call.transferred": ("to", "mode", "ok", "error"),
50
+ "call.line": ("held",),
51
+ "event.received": ("name", "data", "source", "identity"),
52
+ "state.changed": ("state", "changed"),
53
+ "log.gap": ("from_seq", "to_seq", "snapshot"),
54
+ "log.caught_up": ("seq",),
55
+ }
56
+
57
+
58
+ # agent and call are left out so a participant learns nothing of the tenant's setup.
59
+ PUBLIC_ENVELOPE = ("seq", "ts", "type", "ephemeral")
60
+
61
+
62
+ PUBLIC_TURN = frozenset({"role", "speech_id", "text", "interrupted"})
63
+
64
+
65
+ PUBLIC_SEAT = frozenset({"identity", "kind", "name", "joined_at", "speaking"})
66
+
67
+
68
+ PUBLIC_CONFIRM = frozenset({"phrase", "status"})
69
+
70
+
71
+ @dataclass(frozen=True, slots=True)
72
+ class Filter:
73
+ """One reader's filter: the types it wants (None for every one), and whether only durable."""
74
+
75
+ types: frozenset[str] | None = None
76
+ durable: bool = False
77
+
78
+ def passes(self, entry: Entry) -> bool:
79
+ """Return whether the entry reaches this reader."""
80
+ if entry.type in ALWAYS_PASS:
81
+ return True
82
+ if self.durable and entry.ephemeral:
83
+ return False
84
+ return self.types is None or entry.type in self.types
85
+
86
+
87
+ EVERYTHING = Filter()
88
+
89
+
90
+ def parse_filter(types: str | None, *, durable: bool) -> Filter:
91
+ """Return the filter a query string asks for (`types=a.b,c.d`), refusing a bad type name."""
92
+ if types is None:
93
+ return Filter(durable=durable)
94
+ wanted = [name.strip() for name in types.split(",") if name.strip()]
95
+ if len(wanted) > MAX_TYPES:
96
+ raise DeclarationRefused(f"a filter names at most {MAX_TYPES} types, not {len(wanted)}")
97
+ if bad := [name for name in wanted if not _A_TYPE_NAME.match(name)]:
98
+ raise DeclarationRefused(f"an event type is lowercase words joined by dots: {bad}")
99
+ return Filter(types=frozenset(wanted), durable=durable)
100
+
101
+
102
+ def project_state(
103
+ state: State, projection: Projection, config: AgentConfig | None, viewer: str | None = None
104
+ ) -> JsonObject:
105
+ """Return the reduced state as this audience may read it."""
106
+ if projection == "tenant":
107
+ app_state = state.app_state if config is None else masked_state(state.app_state, config)
108
+ return {**state.written(), "app_state": app_state}
109
+ written = state.written()
110
+ room: Json = None
111
+ if state.room is not None:
112
+ seats: list[Json] = [_kept(seat.written(), PUBLIC_SEAT) for seat in state.room.participants]
113
+ room = {**state.room.written(), "participants": seats}
114
+ return {
115
+ "seq": state.seq,
116
+ "status": state.status,
117
+ "user_state": written["user_state"],
118
+ "agent_state": written["agent_state"],
119
+ "live": written["live"],
120
+ "turns": [_public_turn(turn) for turn in state.turns],
121
+ "app_state": _only_public(state.app_state, config),
122
+ "room": room,
123
+ "confirms": [_kept(confirm.written(), PUBLIC_CONFIRM) for confirm in state.confirms],
124
+ "transfer": written["transfer"],
125
+ "held": state.held,
126
+ "events": [
127
+ event.written()
128
+ for event in state.events
129
+ if event.source == "participant" and event.identity == viewer
130
+ ],
131
+ }
132
+
133
+
134
+ def project_entry(
135
+ entry: Entry, projection: Projection, config: AgentConfig | None, viewer: str | None = None
136
+ ) -> JsonObject | None:
137
+ """Return the entry as this audience may read it, or None when it must not reach them."""
138
+ if projection == "tenant":
139
+ return {**entry.written(), "data": _tenant_data(entry, config)}
140
+ fields = PUBLIC_ENTRY_FIELDS.get(entry.type)
141
+ if fields is None:
142
+ return None
143
+ data: JsonObject = {name: entry.data[name] for name in fields if name in entry.data}
144
+ if entry.type == "event.received" and not (
145
+ data.get("source") == "participant" and data.get("identity") == viewer
146
+ ):
147
+ return None
148
+ match _readable(entry):
149
+ case AgentTurnEnded() as turn:
150
+ data["metrics"] = _public_metrics(turn.metrics.e2e_latency)
151
+ case StateChanged() as changed:
152
+ public = _only_public(changed.state, config)
153
+ kept: list[Json] = [name for name in changed.changed if name in public]
154
+ data = {"state": public, "changed": kept}
155
+ case LogGap(snapshot=State() as snapshot):
156
+ data["snapshot"] = project_state(snapshot, "public", config, viewer)
157
+ case None if entry.type in NEEDS_READING:
158
+ return None
159
+ case _:
160
+ pass
161
+ envelope = entry.written()
162
+ return {**{name: envelope[name] for name in PUBLIC_ENVELOPE}, "data": data}
163
+
164
+
165
+ # A log written since private values are masked when written holds them masked already; the
166
+ # rows written before are masked here, by the declaration the reader reads with.
167
+ def _tenant_data(entry: Entry, config: AgentConfig | None) -> JsonObject:
168
+ gap = _readable(entry)
169
+ if isinstance(gap, LogGap) and gap.snapshot is not None:
170
+ return {**entry.data, "snapshot": project_state(gap.snapshot, "tenant", config)}
171
+ return entry.data if config is None else split(entry.type, entry.data, config).kept
172
+
173
+
174
+ # Only the entries a projection reaches into are read; an old shape of one is withheld from the
175
+ # public, since what it would leak cannot be told, and passed whole to the tenant.
176
+ def _readable(entry: Entry) -> WireModel | None:
177
+ if entry.type not in NEEDS_READING:
178
+ return None
179
+ try:
180
+ return event_of(entry)
181
+ except DeclarationRefused:
182
+ return None
183
+
184
+
185
+ def _public_turn(turn: Turn) -> JsonObject:
186
+ kept = _kept(turn.written(), PUBLIC_TURN)
187
+ if isinstance(turn, AgentTurn):
188
+ kept["metrics"] = _public_metrics(turn.metrics.e2e_latency)
189
+ return kept
190
+
191
+
192
+ # How long the caller waited is the one number about them; the rest is the tenant's cost and tuning.
193
+ def _public_metrics(e2e_latency: float | None) -> JsonObject:
194
+ return {} if e2e_latency is None else {"e2e_latency": e2e_latency}
195
+
196
+
197
+ # Without a declaration every field is the tenant's: nothing is public, nothing is masked.
198
+ def _only_public(app_state: JsonObject, config: AgentConfig | None) -> JsonObject:
199
+ if config is None:
200
+ return {}
201
+ return {
202
+ name: value for name, value in app_state.items() if config.visibility_of(name) == "public"
203
+ }
204
+
205
+
206
+ def _kept(written: JsonObject, fields: frozenset[str]) -> dict[str, Json]:
207
+ return {name: value for name, value in written.items() if name in fields}