1use serde::{Deserialize, Serialize};
31use serde_json::Value;
32
33use agent_registry_contract::{AgentId, ResourceRelations};
34
35pub const SERVICE_NAME: &str = "agent-trace-infra";
36pub const DEFAULT_PORT: u16 = 5140;
37pub const AUDIENCE: &str = "agent-trace-infra";
38
39pub const SERVICE_TOKEN_ENV: &str = "AGENT_TRACE_INFRA_SERVICE_TOKEN";
41
42pub const HEALTH_PATH: &str = "/health";
43pub const EVENTS_APPEND_PATH: &str = "/internal/v1/trace-infra/events/batch";
44pub const EVENTS_SEARCH_PATH: &str = "/internal/v1/trace-infra/events/search";
45
46pub const LANGFUSE_PROJECTS_PATH: &str = "/internal/v1/trace-infra/langfuse-projects";
48pub const LANGFUSE_PROJECT_PATH: &str = "/internal/v1/trace-infra/langfuse-projects/{projectId}";
49pub const ADMIN_LANGFUSE_PROJECTS_PATH: &str = "/admin/api/langfuse-projects";
50pub const MAX_LANGFUSE_PROJECT_PAGE_SIZE: u32 = 200;
51pub const RUNTIME_BINDING_PATH: &str = "/internal/v1/trace-infra/runtime-bindings/{runtimeId}";
52pub const RUNTIME_BINDING_INITIALIZE_PATH: &str =
53 "/internal/v1/trace-infra/runtime-bindings/{runtimeId}/initialize";
54pub const RUNTIME_BINDINGS_RESOLVE_PATH: &str = "/internal/v1/trace-infra/runtime-bindings:resolve";
55pub const MAX_RUNTIME_BINDING_RESOLVE_SIZE: usize = 200;
56pub const ADMIN_LANGFUSE_CONFIG_PATH: &str = "/internal/v1/trace-infra/langfuse-config";
57pub const ADMIN_CLICKHOUSE_CONFIG_PATH: &str = "/internal/v1/trace-infra/clickhouse-config";
58pub const ADMIN_GATEWAY_LANGFUSE_CONFIG_PATH: &str = "/admin/api/langfuse-config";
59pub const ADMIN_GATEWAY_CLICKHOUSE_CONFIG_PATH: &str = "/admin/api/clickhouse-config";
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(deny_unknown_fields, rename_all = "camelCase")]
64pub struct CreateLangfuseProjectRequest {
65 pub name: String,
66 pub owner_email: String,
67 #[serde(default)]
68 pub relations: ResourceRelations,
69}
70
71#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(deny_unknown_fields, rename_all = "camelCase")]
74pub struct ListLangfuseProjectsRequest {
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub related_agent_id: Option<AgentId>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub related_runtime_id: Option<String>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub related_user_id: Option<String>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub cursor: Option<String>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub limit: Option<u32>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct LangfuseProject {
92 pub id: String,
93 pub name: String,
94 pub owner_email: String,
95 pub public_key: String,
96 pub base_url: String,
97 #[serde(default)]
98 pub relations: ResourceRelations,
99 pub created_at_ms: u64,
100}
101
102#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
103#[serde(deny_unknown_fields, rename_all = "camelCase")]
104pub struct InitializeRuntimeBindingRequest {
105 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub project_name: Option<String>,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub agent_id: Option<AgentId>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub owner_email: Option<String>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct RuntimeLangfuseBinding {
116 pub runtime_id: String,
117 pub project_name: String,
118 pub langfuse_project_id: String,
119 pub public_key: String,
120 pub status: String,
121}
122
123#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
125#[serde(deny_unknown_fields, rename_all = "camelCase")]
126pub struct ResolveRuntimeBindingsRequest {
127 pub runtime_ids: Vec<String>,
128}
129
130#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct ResolveRuntimeBindingsResponse {
134 pub bindings: Vec<RuntimeLangfuseBinding>,
135 pub unbound_runtime_ids: Vec<String>,
136}
137
138#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct LangfuseAdminConfig {
142 #[serde(default)]
143 pub base_url: String,
144 pub public_key_configured: bool,
145 pub secret_key_configured: bool,
146 pub managed: bool,
147}
148
149#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct ClickHouseAdminConfig {
153 #[serde(default)]
154 pub host: String,
155 #[serde(default)]
156 pub port: String,
157 #[serde(default)]
158 pub database: String,
159 #[serde(default)]
160 pub user: String,
161 pub password_configured: bool,
162 pub managed: bool,
163}
164
165#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
166#[serde(rename_all = "camelCase", deny_unknown_fields)]
167pub struct UpdateLangfuseAdminConfigRequest {
168 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub base_url: Option<String>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub public_key: Option<String>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub secret_key: Option<String>,
174 #[serde(default)]
175 pub clear_public_key: bool,
176 #[serde(default)]
177 pub clear_secret_key: bool,
178}
179
180#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase", deny_unknown_fields)]
182pub struct UpdateClickHouseAdminConfigRequest {
183 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub host: Option<String>,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub port: Option<String>,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub database: Option<String>,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub user: Option<String>,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub password: Option<String>,
193 #[serde(default)]
194 pub clear_password: bool,
195}
196
197#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
200#[serde(rename_all = "camelCase")]
201pub struct ProvisionedLangfuseProject {
202 pub project: LangfuseProject,
203 pub secret_key: String,
204}
205
206impl std::fmt::Debug for ProvisionedLangfuseProject {
207 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 formatter
209 .debug_struct("ProvisionedLangfuseProject")
210 .field("project", &self.project)
211 .field("secret_key", &"[REDACTED]")
212 .finish()
213 }
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(rename_all = "camelCase")]
218pub struct LangfuseProjectPage {
219 pub items: Vec<LangfuseProject>,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
221 pub next_cursor: Option<String>,
222}
223
224pub const METRICS_TOKEN_USAGE_PATH: &str = "/internal/v1/trace-infra/metrics/token-usage";
226pub const METRICS_MODEL_BREAKDOWN_PATH: &str = "/internal/v1/trace-infra/metrics/model-breakdown";
227pub const METRICS_PROJECT_SUMMARY_PATH: &str = "/internal/v1/trace-infra/metrics/project-summary";
228pub const METRICS_LATENCY_PATH: &str = "/internal/v1/trace-infra/metrics/latency";
229pub const METRICS_ERROR_RATE_PATH: &str = "/internal/v1/trace-infra/metrics/error-rate";
230pub const METRICS_CACHE_HIT_RATE_PATH: &str = "/internal/v1/trace-infra/metrics/cache-hit-rate";
231pub const METRICS_COST_BREAKDOWN_PATH: &str = "/internal/v1/trace-infra/metrics/cost-breakdown";
232pub const METRICS_MODEL_TREND_PATH: &str = "/internal/v1/trace-infra/metrics/model-trend";
233pub const METRICS_TOKEN_DISTRIBUTION_PATH: &str =
234 "/internal/v1/trace-infra/metrics/token-distribution";
235pub const METRICS_ACTIVITY_PATH: &str = "/internal/v1/trace-infra/metrics/activity";
236pub const METRICS_PRICING_TIER_PATH: &str = "/internal/v1/trace-infra/metrics/pricing-tier";
237pub const MAX_METRICS_LOOKBACK_DAYS: u32 = 3_650;
238
239#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
245#[serde(rename_all = "camelCase")]
246pub struct TraceScope {
247 pub conversation_id: String,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub runtime_id: Option<String>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub run_id: Option<String>,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub user_id: Option<String>,
256 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub agent_id: Option<String>,
258 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub agent_space_id: Option<String>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub runtime_type: Option<String>,
262}
263
264impl TraceScope {
265 pub fn run(conversation_id: impl Into<String>, run_id: impl Into<String>) -> Self {
266 Self {
267 conversation_id: conversation_id.into(),
268 run_id: Some(run_id.into()),
269 ..Default::default()
270 }
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
276#[serde(rename_all = "camelCase")]
277pub struct RuntimeEventRecord {
278 pub id: String,
281 pub scope: TraceScope,
282 pub event_type: String,
284 #[serde(default)]
285 pub payload: Value,
286 #[serde(default)]
288 pub sequence: i64,
289 #[serde(default)]
290 pub created_at_ms: i64,
291 #[serde(default, skip_serializing_if = "Value::is_null")]
292 pub metadata: Value,
293}
294
295#[derive(Debug, Clone, Default, Serialize, Deserialize)]
296#[serde(rename_all = "camelCase")]
297pub struct AppendEventsRequest {
298 pub events: Vec<RuntimeEventRecord>,
299}
300
301#[derive(Debug, Clone, Default, Serialize, Deserialize)]
304#[serde(rename_all = "camelCase")]
305pub struct AppendResponse {
306 pub appended: usize,
307 pub skipped: usize,
308}
309
310#[derive(Debug, Clone, Default, Serialize, Deserialize)]
311#[serde(rename_all = "camelCase")]
312pub struct SearchEventsRequest {
313 pub scope: TraceScope,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub event_type: Option<String>,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub limit: Option<usize>,
319}
320
321#[derive(Debug, Clone, Default, Serialize, Deserialize)]
326#[serde(rename_all = "camelCase")]
327pub struct EventsResponse {
328 pub items: Vec<RuntimeEventRecord>,
329}
330
331pub const MAX_PAGE_SIZE: usize = 10_000;
334
335pub const DEFAULT_PAGE_SIZE: usize = 100;
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct ApiError {
341 pub status: u16,
342 pub message: String,
343}
344
345#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
349#[serde(rename_all = "camelCase")]
350pub enum MetricsDataSource {
351 #[serde(rename = "v3")]
353 V3,
354 #[serde(rename = "v4")]
356 V4,
357 #[serde(rename = "auto")]
359 #[default]
360 Auto,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
365#[serde(rename_all = "camelCase")]
366pub struct MetricsQuery {
367 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub project_ids: Option<Vec<String>>,
370 #[serde(default = "default_days")]
372 pub days: u32,
373 #[serde(default)]
375 pub data_source: MetricsDataSource,
376 #[serde(default = "default_v4_cutover")]
379 pub v4_cutover_date: String,
380}
381
382impl Default for MetricsQuery {
383 fn default() -> Self {
384 Self {
385 project_ids: None,
386 days: default_days(),
387 data_source: MetricsDataSource::default(),
388 v4_cutover_date: default_v4_cutover(),
389 }
390 }
391}
392
393fn default_v4_cutover() -> String {
394 "2026-09-01 00:00:00".to_string()
395}
396
397const fn default_days() -> u32 {
398 30
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
403#[serde(rename_all = "camelCase")]
404pub struct TokenUsagePoint {
405 pub day: String,
406 pub total_input_tokens: u64,
407 pub total_output_tokens: u64,
408 pub total_tokens: u64,
409 pub observation_count: u64,
410 pub generation_count: u64,
411 pub total_cost: Option<f64>,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
416#[serde(rename_all = "camelCase")]
417pub struct TokenUsageResponse {
418 pub project_id: String,
419 pub points: Vec<TokenUsagePoint>,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
424#[serde(rename_all = "camelCase")]
425pub struct ModelBreakdownEntry {
426 pub project_id: String,
427 pub model_name: String,
428 pub observation_count: u64,
429 pub total_input_tokens: u64,
430 pub total_output_tokens: u64,
431 pub total_tokens: u64,
432 pub total_cost: Option<f64>,
433 pub pricing_tier: Option<String>,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
438#[serde(rename_all = "camelCase")]
439pub struct ProjectSummary {
440 pub project_id: String,
441 pub total_tokens: u64,
442 pub total_input_tokens: u64,
443 pub total_output_tokens: u64,
444 pub observation_count: u64,
445 pub generation_count: u64,
446 pub total_cost: Option<f64>,
447 pub unique_models: u64,
448 pub unique_traces: u64,
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
453#[serde(rename_all = "camelCase")]
454pub struct LatencyPoint {
455 pub day: String,
456 pub project_id: String,
457 pub model_name: Option<String>,
458 pub observation_count: u64,
459 pub avg_duration_seconds: Option<f64>,
461 pub p50_duration_seconds: Option<f64>,
463 pub p95_duration_seconds: Option<f64>,
465 pub p99_duration_seconds: Option<f64>,
467 pub avg_ttft_seconds: Option<f64>,
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize)]
473#[serde(rename_all = "camelCase")]
474pub struct LatencyResponse {
475 pub project_id: String,
476 pub points: Vec<LatencyPoint>,
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
481#[serde(rename_all = "camelCase")]
482pub struct ErrorRatePoint {
483 pub day: String,
484 pub project_id: String,
485 pub total_observations: u64,
486 pub error_count: u64,
487 pub error_rate: f64,
489 pub model_name: Option<String>,
490}
491
492#[derive(Debug, Clone, Serialize, Deserialize)]
494#[serde(rename_all = "camelCase")]
495pub struct ErrorRateResponse {
496 pub project_id: String,
497 pub points: Vec<ErrorRatePoint>,
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize)]
504#[serde(rename_all = "camelCase")]
505pub struct CacheHitRatePoint {
506 pub project_id: String,
507 pub model_name: Option<String>,
508 pub total_input_tokens: u64,
509 pub cached_tokens: u64,
510 pub cache_hit_pct: f64,
512 #[serde(default)]
514 pub observation_count: u64,
515 #[serde(default)]
517 pub cache_measured_count: u64,
518}
519
520#[derive(Debug, Clone, Serialize, Deserialize)]
522#[serde(rename_all = "camelCase")]
523pub struct CacheHitRateResponse {
524 pub items: Vec<CacheHitRatePoint>,
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
531#[serde(rename_all = "camelCase")]
532pub struct CostBreakdownPoint {
533 pub project_id: String,
534 pub model_name: Option<String>,
535 pub total_input_cost: Option<f64>,
536 pub total_output_cost: Option<f64>,
537 pub total_cost: Option<f64>,
538 pub observation_count: u64,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize)]
543#[serde(rename_all = "camelCase")]
544pub struct CostBreakdownResponse {
545 pub items: Vec<CostBreakdownPoint>,
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
552#[serde(rename_all = "camelCase")]
553pub struct ModelTrendPoint {
554 pub day: String,
555 pub project_id: String,
556 pub model_name: Option<String>,
557 pub call_count: u64,
558 pub total_tokens: u64,
559 pub total_cost: Option<f64>,
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize)]
564#[serde(rename_all = "camelCase")]
565pub struct ModelTrendResponse {
566 pub project_id: String,
567 pub points: Vec<ModelTrendPoint>,
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize)]
574#[serde(rename_all = "camelCase")]
575pub struct TokenDistributionPoint {
576 pub project_id: String,
577 pub model_name: Option<String>,
578 pub observation_count: u64,
579 pub avg_tokens: Option<f64>,
580 pub p50_tokens: Option<f64>,
581 pub p95_tokens: Option<f64>,
582 pub p99_tokens: Option<f64>,
583 pub max_tokens: Option<u64>,
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize)]
588#[serde(rename_all = "camelCase")]
589pub struct TokenDistributionResponse {
590 pub items: Vec<TokenDistributionPoint>,
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize)]
597#[serde(rename_all = "camelCase")]
598pub struct ActivityPoint {
599 pub day: String,
600 pub project_id: String,
601 pub active_traces: u64,
602 pub observation_count: u64,
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize)]
607#[serde(rename_all = "camelCase")]
608pub struct ActivityResponse {
609 pub project_id: String,
610 pub points: Vec<ActivityPoint>,
611}
612
613#[derive(Debug, Clone, Serialize, Deserialize)]
617#[serde(rename_all = "camelCase")]
618pub struct PricingTierPoint {
619 pub project_id: String,
620 pub pricing_tier: Option<String>,
621 pub model_name: Option<String>,
622 pub observation_count: u64,
623 pub total_tokens: u64,
624 pub total_cost: Option<f64>,
625}
626
627#[derive(Debug, Clone, Serialize, Deserialize)]
629#[serde(rename_all = "camelCase")]
630pub struct PricingTierResponse {
631 pub items: Vec<PricingTierPoint>,
632}
633
634pub const REDACTED: &str = "<redacted>";
636
637pub fn redact_payload(value: &mut Value) {
650 match value {
651 Value::Object(fields) => {
652 for (key, field) in fields.iter_mut() {
653 if is_sensitive_key(key) {
654 *field = Value::String(REDACTED.to_string());
655 } else {
656 redact_payload(field);
657 }
658 }
659 }
660 Value::Array(items) => items.iter_mut().for_each(redact_payload),
661 _ => {}
662 }
663}
664
665fn is_sensitive_key(key: &str) -> bool {
670 let normalised = key
671 .chars()
672 .filter(|character| character.is_ascii_alphanumeric())
673 .flat_map(|character| character.to_lowercase())
674 .collect::<String>();
675
676 const EXACT: &[&str] = &[
677 "apikey",
678 "authorization",
679 "gatewaytoken",
680 "password",
681 "secret",
682 "token",
683 ];
684 if EXACT.contains(&normalised.as_str()) {
685 return true;
686 }
687
688 ["apikey", "secret", "token", "password"]
690 .iter()
691 .any(|needle| normalised.ends_with(needle))
692}
693
694#[cfg(test)]
695mod tests {
696 use super::*;
697 use serde_json::json;
698
699 #[test]
700 fn scope_serialises_camel_case_and_omits_absent_provenance() {
701 let value = serde_json::to_value(TraceScope::run("conv-1", "run-1")).expect("serialize");
702 assert_eq!(value, json!({"conversationId": "conv-1", "runId": "run-1"}));
703 }
704
705 #[test]
706 fn redaction_masks_credential_fields_at_any_depth() {
707 let mut payload = json!({
708 "model": "test-model",
709 "api_key": "sensitive-api-value",
710 "headers": {"Authorization": "sensitive-authorization-value"},
711 "calls": [
712 {"accessToken": "sensitive-access-value"},
713 {"clientSecret": "sensitive-client-value"}
714 ],
715 "nested": {"deep": {"apiKey": "sensitive-nested-value"}}
716 });
717
718 redact_payload(&mut payload);
719
720 assert_eq!(payload["model"], "test-model", "non-secrets must survive");
721 assert_eq!(payload["api_key"], REDACTED);
722 assert_eq!(payload["headers"]["Authorization"], REDACTED);
723 assert_eq!(payload["calls"][0]["accessToken"], REDACTED);
724 assert_eq!(payload["calls"][1]["clientSecret"], REDACTED);
725 assert_eq!(payload["nested"]["deep"]["apiKey"], REDACTED);
726 }
727
728 #[test]
729 fn redaction_does_not_mask_lookalike_field_names() {
730 let mut payload = json!({
733 "tokenCount": 42,
734 "secretsManagerArn": "arn:aws:secretsmanager:...",
735 "passwordPolicy": {"minLength": 12}
736 });
737
738 redact_payload(&mut payload);
739
740 assert_eq!(payload["tokenCount"], 42);
741 assert_eq!(
742 payload["secretsManagerArn"], "arn:aws:secretsmanager:...",
743 "a name that merely contains 'secret' is not a secret"
744 );
745 assert_eq!(payload["passwordPolicy"]["minLength"], 12);
746 }
747
748 #[test]
749 fn an_event_round_trips_through_json() {
750 let record = RuntimeEventRecord {
751 id: "e-1".into(),
752 scope: TraceScope::run("conv-1", "run-1"),
753 event_type: "tool_call".into(),
754 payload: json!({"name": "execute_cmd"}),
755 sequence: 3,
756 created_at_ms: 1_700_000_000_000,
757 metadata: Value::Null,
758 };
759 let encoded = serde_json::to_value(&record).expect("serialize");
760 assert_eq!(encoded["eventType"], "tool_call");
761 assert_eq!(encoded["createdAtMs"], 1_700_000_000_000_i64);
762 let decoded: RuntimeEventRecord = serde_json::from_value(encoded).expect("deserialize");
763 assert_eq!(decoded, record);
764 }
765}