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.
- pinecall/__init__.py +0 -0
- pinecall/channels/__init__.py +0 -0
- pinecall/channels/_chooser.py +45 -0
- pinecall/channels/offers.py +219 -0
- pinecall/channels/rooms.py +176 -0
- pinecall/channels/routes.py +275 -0
- pinecall/channels/telephony/__init__.py +0 -0
- pinecall/channels/telephony/_twilio.py +349 -0
- pinecall/channels/telephony/carrier.py +229 -0
- pinecall/channels/telephony/carrier_catalog.py +92 -0
- pinecall/channels/telephony/carriers.csv +41 -0
- pinecall/channels/telephony/dialing.py +419 -0
- pinecall/channels/telephony/firewall.py +58 -0
- pinecall/channels/telephony/hand_over.py +78 -0
- pinecall/channels/telephony/number_path.py +176 -0
- pinecall/channels/telephony/numbers.py +530 -0
- pinecall/channels/telephony/origination.py +77 -0
- pinecall/channels/telephony/sip.py +448 -0
- pinecall/channels/whatsapp.py +413 -0
- pinecall/cli/__init__.py +0 -0
- pinecall/cli/_archive.py +85 -0
- pinecall/cli/_drift.py +38 -0
- pinecall/cli/_facts.py +67 -0
- pinecall/cli/_load.py +433 -0
- pinecall/cli/_operator.py +626 -0
- pinecall/cli/_sessions.py +164 -0
- pinecall/cli/_traceback.py +95 -0
- pinecall/cli/_usage.py +34 -0
- pinecall/cli/main.py +521 -0
- pinecall/domain/__init__.py +0 -0
- pinecall/domain/agent.py +446 -0
- pinecall/domain/call.py +152 -0
- pinecall/domain/country_codes.py +32 -0
- pinecall/domain/errors.py +151 -0
- pinecall/domain/names.py +115 -0
- pinecall/domain/org.py +120 -0
- pinecall/domain/person.py +208 -0
- pinecall/domain/scope.py +21 -0
- pinecall/evals/__init__.py +0 -0
- pinecall/evals/_evidence.py +158 -0
- pinecall/evals/calibration.py +112 -0
- pinecall/evals/callers.py +160 -0
- pinecall/evals/case.py +327 -0
- pinecall/evals/checks.py +437 -0
- pinecall/evals/compliance.py +197 -0
- pinecall/evals/dataset.py +451 -0
- pinecall/evals/goldens.py +331 -0
- pinecall/evals/judges.py +525 -0
- pinecall/evals/runs.py +252 -0
- pinecall/evals/spoken.py +356 -0
- pinecall/fleet/__init__.py +0 -0
- pinecall/fleet/client.py +569 -0
- pinecall/fleet/demand.py +51 -0
- pinecall/fleet/heartbeat.py +194 -0
- pinecall/fleet/measures.py +157 -0
- pinecall/fleet/roster.py +232 -0
- pinecall/fleet/worlds.py +44 -0
- pinecall/gateway/__init__.py +0 -0
- pinecall/gateway/_call_setup.py +53 -0
- pinecall/gateway/_deps.py +556 -0
- pinecall/gateway/_gateway.py +75 -0
- pinecall/gateway/_served.py +396 -0
- pinecall/gateway/_sockets.py +444 -0
- pinecall/gateway/_streams.py +83 -0
- pinecall/gateway/_text_calls.py +182 -0
- pinecall/gateway/api/__init__.py +0 -0
- pinecall/gateway/api/accounts.py +324 -0
- pinecall/gateway/api/agents.py +150 -0
- pinecall/gateway/api/apps.py +478 -0
- pinecall/gateway/api/box.py +658 -0
- pinecall/gateway/api/calibration.py +58 -0
- pinecall/gateway/api/callbacks.py +105 -0
- pinecall/gateway/api/calls.py +655 -0
- pinecall/gateway/api/chat.py +223 -0
- pinecall/gateway/api/dataset.py +163 -0
- pinecall/gateway/api/desk.py +113 -0
- pinecall/gateway/api/entries.py +77 -0
- pinecall/gateway/api/evals.py +647 -0
- pinecall/gateway/api/fleet.py +73 -0
- pinecall/gateway/api/hosting.py +241 -0
- pinecall/gateway/api/judges.py +97 -0
- pinecall/gateway/api/keys.py +110 -0
- pinecall/gateway/api/line.py +142 -0
- pinecall/gateway/api/members.py +197 -0
- pinecall/gateway/api/metrics.py +226 -0
- pinecall/gateway/api/numbers.py +476 -0
- pinecall/gateway/api/ops.py +428 -0
- pinecall/gateway/api/org.py +283 -0
- pinecall/gateway/api/personas.py +157 -0
- pinecall/gateway/api/pipeline.py +206 -0
- pinecall/gateway/api/prompts.py +50 -0
- pinecall/gateway/api/providers.py +240 -0
- pinecall/gateway/api/recordings.py +89 -0
- pinecall/gateway/api/relay.py +169 -0
- pinecall/gateway/api/retrieval.py +403 -0
- pinecall/gateway/api/runner.py +109 -0
- pinecall/gateway/api/sessions.py +102 -0
- pinecall/gateway/api/settings.py +463 -0
- pinecall/gateway/api/signup.py +164 -0
- pinecall/gateway/api/sso_login.py +168 -0
- pinecall/gateway/api/threads.py +187 -0
- pinecall/gateway/api/usage.py +303 -0
- pinecall/gateway/api/visitors.py +222 -0
- pinecall/gateway/api/whatsapp.py +48 -0
- pinecall/gateway/api/widget.py +49 -0
- pinecall/gateway/app.py +504 -0
- pinecall/gateway/calls/__init__.py +0 -0
- pinecall/gateway/calls/binding.py +63 -0
- pinecall/gateway/calls/commands.py +18 -0
- pinecall/gateway/calls/inbox.py +100 -0
- pinecall/gateway/calls/known.py +23 -0
- pinecall/gateway/calls/owners.py +87 -0
- pinecall/gateway/calls/pump.py +33 -0
- pinecall/gateway/calls/serving.py +118 -0
- pinecall/gateway/calls/threads.py +367 -0
- pinecall/gateway/dispatching/__init__.py +0 -0
- pinecall/gateway/dispatching/arrivals.py +58 -0
- pinecall/gateway/dispatching/sweep.py +43 -0
- pinecall/gateway/ending/__init__.py +0 -0
- pinecall/gateway/ending/reaper.py +160 -0
- pinecall/gateway/ending/seal.py +312 -0
- pinecall/gateway/ending/stranded.py +117 -0
- pinecall/log/__init__.py +0 -0
- pinecall/log/_histogram.py +74 -0
- pinecall/log/_relay.py +259 -0
- pinecall/log/_writer.py +454 -0
- pinecall/log/days.py +154 -0
- pinecall/log/drift.py +550 -0
- pinecall/log/facts.py +358 -0
- pinecall/log/inbox.py +169 -0
- pinecall/log/lists.py +155 -0
- pinecall/log/logs.py +581 -0
- pinecall/log/openings.py +71 -0
- pinecall/log/private.py +135 -0
- pinecall/log/queries.py +229 -0
- pinecall/log/readers.py +207 -0
- pinecall/log/reduce.py +659 -0
- pinecall/log/refold.py +217 -0
- pinecall/log/store.py +370 -0
- pinecall/log/usage.py +242 -0
- pinecall/postgres/__init__.py +0 -0
- pinecall/postgres/migrate.py +202 -0
- pinecall/postgres/migrations/0001_schema.sql +591 -0
- pinecall/postgres/migrations/0002_worlds.sql +15 -0
- pinecall/postgres/migrations/0003_lent.sql +4 -0
- pinecall/postgres/migrations/0004_carriers.sql +23 -0
- pinecall/postgres/migrations/0005_retrieved_only.sql +11 -0
- pinecall/postgres/migrations/0006_persona_agents.sql +9 -0
- pinecall/postgres/migrations/0007_money_in_dollars.sql +27 -0
- pinecall/postgres/migrations/0008_lexicon_per_agent.sql +27 -0
- pinecall/postgres/migrations/0009_personas_per_agent.sql +31 -0
- pinecall/postgres/migrations/0010_agent_judges.sql +12 -0
- pinecall/postgres/migrations/0011_erasure.sql +32 -0
- pinecall/postgres/migrations/0012_org_policy.sql +10 -0
- pinecall/postgres/migrations/0013_calling_rules.sql +12 -0
- pinecall/postgres/migrations/0014_consents.sql +24 -0
- pinecall/postgres/migrations/0015_disclosure.sql +7 -0
- pinecall/postgres/migrations/0016_call_records.sql +21 -0
- pinecall/postgres/migrations/0017_reads.sql +17 -0
- pinecall/postgres/migrations/0018_hosting.sql +41 -0
- pinecall/postgres/migrations/0019_hosted_state.sql +10 -0
- pinecall/postgres/migrations/0020_written_once.sql +7 -0
- pinecall/postgres/migrations/0021_hosted_running.sql +21 -0
- pinecall/postgres/migrations/0022_sealed.sql +39 -0
- pinecall/postgres/migrations/0023_usage_totals.sql +158 -0
- pinecall/postgres/migrations/0024_log_key_by_day.sql +8 -0
- pinecall/postgres/migrations/0025_log_before_the_days.sql +15 -0
- pinecall/postgres/migrations/0026_log_before_the_days_validated.sql +4 -0
- pinecall/postgres/migrations/0027_log_by_day.sql +96 -0
- pinecall/postgres/migrations/0030_whatsapp_seen.sql +17 -0
- pinecall/postgres/migrations/0040_reads_widened.sql +15 -0
- pinecall/postgres/migrations/0060_drift.sql +62 -0
- pinecall/postgres/migrations/0061_canaries.sql +24 -0
- pinecall/postgres/migrations/0062_eval_cases.sql +23 -0
- pinecall/postgres/migrations/0063_prompts.sql +16 -0
- pinecall/postgres/migrations/0075_call_private.sql +14 -0
- pinecall/postgres/migrations/0076_recording_keys.sql +13 -0
- pinecall/postgres/migrations/0077_tokens_world.sql +7 -0
- pinecall/postgres/migrations/0078_facts_cost_by_stage.sql +12 -0
- pinecall/postgres/migrations/0079_judge_labels.sql +20 -0
- pinecall/postgres/migrations/0080_call_openings.sql +20 -0
- pinecall/postgres/migrations/0081_seal_lease.sql +7 -0
- pinecall/postgres/migrations/0082_caller_codes.sql +14 -0
- pinecall/postgres/migrations/0083_one_use_words.sql +15 -0
- pinecall/postgres/migrations/0084_knocks_and_run_leases.sql +23 -0
- pinecall/postgres/migrations/0085_seal_lease_holder.sql +8 -0
- pinecall/postgres/migrations/0086_route_origin.sql +17 -0
- pinecall/postgres/migrations/0087_carrier_networks.sql +20 -0
- pinecall/postgres/migrations/0088_route_last_call.sql +3 -0
- pinecall/postgres/migrations/0089_offers.sql +21 -0
- pinecall/postgres/migrations/0090_invitation_purpose.sql +5 -0
- pinecall/postgres/migrations/0091_head_opened_by.sql +6 -0
- pinecall/postgres/migrations/0092_key_parent.sql +6 -0
- pinecall/postgres/migrations/0093_code_tries.sql +10 -0
- pinecall/postgres/migrations/0094_route_approval.sql +12 -0
- pinecall/postgres/migrations/0095_sso_domain_proofs.sql +17 -0
- pinecall/postgres/migrations/0096_row_level_security.sql +38 -0
- pinecall/postgres/migrations/0097_case_status.sql +19 -0
- pinecall/postgres/pool.py +190 -0
- pinecall/process/__init__.py +0 -0
- pinecall/process/_objects.py +97 -0
- pinecall/process/box_settings.py +23 -0
- pinecall/process/connections.py +124 -0
- pinecall/process/metrics.py +127 -0
- pinecall/process/recordings.py +323 -0
- pinecall/process/resolver.py +41 -0
- pinecall/process/sealed_audio.py +173 -0
- pinecall/process/settings.py +534 -0
- pinecall/process/shared.py +158 -0
- pinecall/process/signal.py +416 -0
- pinecall/providers/__init__.py +0 -0
- pinecall/providers/build.py +570 -0
- pinecall/providers/catalog.py +266 -0
- pinecall/providers/credentials.py +230 -0
- pinecall/providers/declared.py +69 -0
- pinecall/providers/prices.py +240 -0
- pinecall/providers/tried.py +104 -0
- pinecall/providers/voices.py +174 -0
- pinecall/public/console/assets/index-BIfKPSgn.js +164 -0
- pinecall/public/console/assets/index-ClOTU-xX.css +1 -0
- pinecall/public/console/favicon.png +0 -0
- pinecall/public/console/index.html +20 -0
- pinecall/public/console/logo-mark.png +0 -0
- pinecall/public/console/pinecall-logo-dark.png +0 -0
- pinecall/public/console/pinecall-logo.png +0 -0
- pinecall/public/console/pinecall-mark.png +0 -0
- pinecall/public/console/sw.js +37 -0
- pinecall/public/widget/pinecall-widget.js +853 -0
- pinecall/py.typed +0 -0
- pinecall/retrieval/__init__.py +0 -0
- pinecall/retrieval/_search.py +186 -0
- pinecall/retrieval/_sections.py +70 -0
- pinecall/retrieval/embed.py +340 -0
- pinecall/retrieval/extraction.py +403 -0
- pinecall/retrieval/knowledge.py +659 -0
- pinecall/retrieval/lookups.py +192 -0
- pinecall/retrieval/memory.py +603 -0
- pinecall/runner/__init__.py +0 -0
- pinecall/runner/_kube.py +459 -0
- pinecall/runner/_plan.py +157 -0
- pinecall/runner/main.py +306 -0
- pinecall/session/__init__.py +0 -0
- pinecall/session/_agent.py +141 -0
- pinecall/session/_hearing.py +101 -0
- pinecall/session/_livekit.py +195 -0
- pinecall/session/_prompt.py +147 -0
- pinecall/session/_recording.py +237 -0
- pinecall/session/call.py +367 -0
- pinecall/session/clock.py +40 -0
- pinecall/session/hold.py +239 -0
- pinecall/session/room.py +626 -0
- pinecall/session/session.py +690 -0
- pinecall/session/text.py +144 -0
- pinecall/session/tools.py +483 -0
- pinecall/session/voice.py +118 -0
- pinecall/session/widget.py +201 -0
- pinecall/tenancy/__init__.py +0 -0
- pinecall/tenancy/admission.py +204 -0
- pinecall/tenancy/agents.py +164 -0
- pinecall/tenancy/canary.py +126 -0
- pinecall/tenancy/carrier_networks.py +156 -0
- pinecall/tenancy/carriers.py +265 -0
- pinecall/tenancy/codes.py +196 -0
- pinecall/tenancy/consents.py +167 -0
- pinecall/tenancy/dial_policy.py +405 -0
- pinecall/tenancy/disclosure.py +41 -0
- pinecall/tenancy/erasure.py +204 -0
- pinecall/tenancy/export.py +167 -0
- pinecall/tenancy/hosted_running.py +166 -0
- pinecall/tenancy/hosting.py +475 -0
- pinecall/tenancy/judges.py +126 -0
- pinecall/tenancy/keys.py +467 -0
- pinecall/tenancy/knocks.py +40 -0
- pinecall/tenancy/letters.py +378 -0
- pinecall/tenancy/mail.py +458 -0
- pinecall/tenancy/org_secrets.py +123 -0
- pinecall/tenancy/orgs.py +78 -0
- pinecall/tenancy/people.py +543 -0
- pinecall/tenancy/personas.py +145 -0
- pinecall/tenancy/policy.py +65 -0
- pinecall/tenancy/prompts.py +48 -0
- pinecall/tenancy/reads.py +73 -0
- pinecall/tenancy/recording_keys.py +41 -0
- pinecall/tenancy/remembered.py +148 -0
- pinecall/tenancy/retention.py +86 -0
- pinecall/tenancy/scopes.py +406 -0
- pinecall/tenancy/signin.py +622 -0
- pinecall/tenancy/spend.py +69 -0
- pinecall/tenancy/sso.py +431 -0
- pinecall/tenancy/sso_domains.py +132 -0
- pinecall/tenancy/throttle.py +88 -0
- pinecall/tenancy/tokens.py +400 -0
- pinecall/tenancy/traceback.py +70 -0
- pinecall/tenancy/usage.py +136 -0
- pinecall/tenancy/vault.py +204 -0
- pinecall/tenancy/words.py +144 -0
- pinecall/wire/__init__.py +0 -0
- pinecall/wire/commands.py +350 -0
- pinecall/wire/events.py +655 -0
- pinecall/wire/frames.py +65 -0
- pinecall/wire/metrics.py +287 -0
- pinecall/wire/parts.py +320 -0
- pinecall/wire/rest/__init__.py +0 -0
- pinecall/wire/rest/accounts.py +512 -0
- pinecall/wire/rest/agents.py +172 -0
- pinecall/wire/rest/calls.py +397 -0
- pinecall/wire/rest/evals.py +453 -0
- pinecall/wire/rest/fleet.py +69 -0
- pinecall/wire/rest/hosting.py +174 -0
- pinecall/wire/rest/numbers.py +280 -0
- pinecall/wire/rest/ops.py +342 -0
- pinecall/wire/rest/providers.py +83 -0
- pinecall/wire/rest/retrieval.py +301 -0
- pinecall/wire/rest/settings.py +184 -0
- pinecall/wire/rest/usage.py +245 -0
- pinecall/wire/room.py +61 -0
- pinecall/wire/scores.py +49 -0
- pinecall/wire/state.py +243 -0
- pinecall/worker/__init__.py +0 -0
- pinecall/worker/_job.py +655 -0
- pinecall/worker/_recorder.py +82 -0
- pinecall/worker/_traces.py +48 -0
- pinecall/worker/main.py +307 -0
- pinecall_runtime-0.1.6.dist-info/METADATA +212 -0
- pinecall_runtime-0.1.6.dist-info/RECORD +328 -0
- pinecall_runtime-0.1.6.dist-info/WHEEL +4 -0
- pinecall_runtime-0.1.6.dist-info/entry_points.txt +2 -0
- pinecall_runtime-0.1.6.dist-info/licenses/LICENSE +202 -0
pinecall/log/private.py
ADDED
|
@@ -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
|
pinecall/log/queries.py
ADDED
|
@@ -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
|
+
]
|
pinecall/log/readers.py
ADDED
|
@@ -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}
|