@maximem/synap-js-sdk 0.3.0 → 0.4.1

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 (55) hide show
  1. package/MIGRATING.md +147 -0
  2. package/README.md +95 -82
  3. package/bin/synap-js-sdk.js +45 -122
  4. package/dist/anticipation-cache-ARYEEKMU.cjs +10 -0
  5. package/dist/anticipation-cache-ARYEEKMU.cjs.map +1 -0
  6. package/dist/anticipation-cache-BMVF5sXb.d.cts +190 -0
  7. package/dist/anticipation-cache-BMVF5sXb.d.ts +190 -0
  8. package/dist/anticipation-cache-H7HEVWQC.js +10 -0
  9. package/dist/anticipation-cache-H7HEVWQC.js.map +1 -0
  10. package/dist/chunk-5SC3ELNK.js +856 -0
  11. package/dist/chunk-5SC3ELNK.js.map +1 -0
  12. package/dist/chunk-6LDUDKHS.js +832 -0
  13. package/dist/chunk-6LDUDKHS.js.map +1 -0
  14. package/dist/chunk-HR5Y3KXS.cjs +172 -0
  15. package/dist/chunk-HR5Y3KXS.cjs.map +1 -0
  16. package/dist/chunk-QBCVMPGK.js +28 -0
  17. package/dist/chunk-QBCVMPGK.js.map +1 -0
  18. package/dist/chunk-RB57I2IQ.cjs +832 -0
  19. package/dist/chunk-RB57I2IQ.cjs.map +1 -0
  20. package/dist/chunk-T3NZBIV2.js +172 -0
  21. package/dist/chunk-T3NZBIV2.js.map +1 -0
  22. package/dist/chunk-VXRCBMCY.cjs +856 -0
  23. package/dist/chunk-VXRCBMCY.cjs.map +1 -0
  24. package/dist/chunk-ZKOJDUN4.cjs +28 -0
  25. package/dist/chunk-ZKOJDUN4.cjs.map +1 -0
  26. package/dist/env-55ULHNJZ.cjs +11 -0
  27. package/dist/env-55ULHNJZ.cjs.map +1 -0
  28. package/dist/env-QCUKKW36.js +11 -0
  29. package/dist/env-QCUKKW36.js.map +1 -0
  30. package/dist/grpc/index.cjs +87 -0
  31. package/dist/grpc/index.cjs.map +1 -0
  32. package/dist/grpc/index.d.cts +162 -0
  33. package/dist/grpc/index.d.ts +162 -0
  34. package/dist/grpc/index.js +87 -0
  35. package/dist/grpc/index.js.map +1 -0
  36. package/dist/index.cjs +2702 -0
  37. package/dist/index.cjs.map +1 -0
  38. package/dist/index.d.cts +1491 -0
  39. package/dist/index.d.ts +1491 -0
  40. package/dist/index.js +2702 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/stream-client-ME5N5NK4.js +7 -0
  43. package/dist/stream-client-ME5N5NK4.js.map +1 -0
  44. package/dist/stream-client-ZN6ADZBR.cjs +7 -0
  45. package/dist/stream-client-ZN6ADZBR.cjs.map +1 -0
  46. package/package.json +68 -23
  47. package/.env.example +0 -39
  48. package/bridge/synap_bridge.py +0 -768
  49. package/src/bridge-manager.js +0 -262
  50. package/src/errors.js +0 -168
  51. package/src/index.js +0 -13
  52. package/src/runtime.js +0 -161
  53. package/src/setup-typescript.js +0 -161
  54. package/src/synap-client.js +0 -464
  55. package/types/index.d.ts +0 -475
@@ -1,768 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Synap SDK Bridge: JSON-RPC over stdin/stdout for the Node.js wrapper.
3
-
4
- Protocol:
5
- stdin -> {"id": 1, "method": "init", "params": {...}}\n
6
- stdout <- {"id": 1, "result": {...}, "error": null}\n
7
- Methods:
8
- init, add_memory, create_memory, record_message, search_memory, get_memories,
9
- fetch_user_context, fetch_customer_context, fetch_client_context,
10
- get_context_for_prompt, delete_memory, shutdown
11
- """
12
-
13
- import asyncio
14
- import json
15
- import logging
16
- import sys
17
- import time
18
- import traceback
19
- from typing import Dict, List, Optional
20
- from uuid import UUID
21
-
22
- # Keep stdout clean for protocol responses.
23
- logging.basicConfig(
24
- stream=sys.stderr,
25
- level=logging.INFO,
26
- format="%(asctime)s %(name)s %(levelname)s %(message)s",
27
- )
28
- logger = logging.getLogger("synap.bridge")
29
-
30
- try:
31
- from maximem_synap import MaximemSynapSDK
32
- except Exception as import_error: # pragma: no cover
33
- sys.stderr.write(
34
- "Failed to import maximem_synap. Run `synap-js-sdk setup` first.\n"
35
- )
36
- sys.stderr.write(f"Import error: {import_error}\n")
37
- raise
38
-
39
- sdk = None
40
- user_memory_ids: Dict[str, List[str]] = {}
41
-
42
-
43
- def ms_since(start: float) -> int:
44
- return int((time.perf_counter() - start) * 1000)
45
-
46
-
47
- def append_step(timings: List[dict], step: str, started: float) -> None:
48
- timings.append({"step": step, "ms": ms_since(started)})
49
-
50
-
51
- def write_response(obj: dict) -> None:
52
- sys.stdout.write(json.dumps(obj, default=str) + "\n")
53
- sys.stdout.flush()
54
-
55
-
56
- def tracking_key(user_id: str, customer_id: Optional[str]) -> str:
57
- """Scope tracked memory IDs by both customer and user."""
58
- return f"{customer_id or ''}::{user_id}"
59
-
60
-
61
- def serialize_context_response(context) -> dict:
62
- """Serialize a Python ContextResponse for the JS bridge."""
63
- payload = context.model_dump(mode="json")
64
- payload["raw_response"] = context.raw if hasattr(context, "raw") else {}
65
- return payload
66
-
67
-
68
- def serialize_context_for_prompt_response(response) -> dict:
69
- """Serialize a Python ContextForPromptResponse for the JS bridge."""
70
- return response.model_dump(mode="json")
71
-
72
-
73
- def flatten_context_items(context) -> List[dict]:
74
- """Convert typed context collections into a flat memory list."""
75
- items: List[dict] = []
76
- for fact in context.facts:
77
- items.append({
78
- "id": fact.id,
79
- "memory": fact.content,
80
- "score": fact.confidence,
81
- "source": fact.source,
82
- "metadata": fact.metadata,
83
- "context_type": "fact",
84
- "event_date": str(fact.event_date) if getattr(fact, "event_date", None) else None,
85
- "valid_until": str(fact.valid_until) if getattr(fact, "valid_until", None) else None,
86
- "temporal_category": getattr(fact, "temporal_category", None),
87
- "temporal_confidence": getattr(fact, "temporal_confidence", 0.0),
88
- })
89
- for preference in context.preferences:
90
- items.append({
91
- "id": preference.id,
92
- "memory": preference.content,
93
- "score": preference.strength,
94
- "source": getattr(preference, "source", ""),
95
- "metadata": preference.metadata,
96
- "context_type": "preference",
97
- "event_date": str(preference.event_date) if getattr(preference, "event_date", None) else None,
98
- "valid_until": str(preference.valid_until) if getattr(preference, "valid_until", None) else None,
99
- "temporal_category": getattr(preference, "temporal_category", None),
100
- "temporal_confidence": getattr(preference, "temporal_confidence", 0.0),
101
- })
102
- for episode in context.episodes:
103
- items.append({
104
- "id": episode.id,
105
- "memory": episode.summary,
106
- "score": episode.significance,
107
- "metadata": episode.metadata,
108
- "context_type": "episode",
109
- "event_date": str(episode.event_date) if getattr(episode, "event_date", None) else None,
110
- "valid_until": str(episode.valid_until) if getattr(episode, "valid_until", None) else None,
111
- "temporal_category": getattr(episode, "temporal_category", None),
112
- "temporal_confidence": getattr(episode, "temporal_confidence", 0.0),
113
- })
114
- for emotion in context.emotions:
115
- items.append({
116
- "id": emotion.id,
117
- "memory": emotion.context,
118
- "score": emotion.intensity,
119
- "metadata": emotion.metadata,
120
- "context_type": "emotion",
121
- "event_date": str(emotion.event_date) if getattr(emotion, "event_date", None) else None,
122
- "valid_until": str(emotion.valid_until) if getattr(emotion, "valid_until", None) else None,
123
- "temporal_category": getattr(emotion, "temporal_category", None),
124
- "temporal_confidence": getattr(emotion, "temporal_confidence", 0.0),
125
- })
126
- for event in getattr(context, "temporal_events", []):
127
- items.append({
128
- "id": event.id,
129
- "memory": event.content,
130
- "score": event.temporal_confidence,
131
- "source": event.source,
132
- "metadata": event.metadata,
133
- "context_type": "temporal_event",
134
- "event_date": str(event.event_date) if event.event_date else None,
135
- "valid_until": str(event.valid_until) if event.valid_until else None,
136
- "temporal_category": event.temporal_category,
137
- "temporal_confidence": event.temporal_confidence,
138
- })
139
- return items
140
-
141
-
142
- def messages_to_text(messages: List[dict]) -> str:
143
- lines: List[str] = []
144
- for message in messages:
145
- content = (message.get("content") or "").strip()
146
- if not content:
147
- continue
148
- role = "Assistant" if message.get("role") == "assistant" else "User"
149
- lines.append(f"{role}: {content}")
150
- return "\n".join(lines)
151
-
152
-
153
- def on_anticipated_context(bundle: dict) -> None:
154
- logger.info(
155
- "Anticipated context bundle received: %s",
156
- json.dumps(bundle, default=str)[:300],
157
- )
158
-
159
-
160
- async def handle_init(params: dict) -> dict:
161
- global sdk
162
-
163
- handler_start = time.perf_counter()
164
- timings: List[dict] = []
165
-
166
- instance_id = params.get("instance_id", "")
167
- api_key = params.get("api_key")
168
-
169
- step = time.perf_counter()
170
- sdk = MaximemSynapSDK(
171
- instance_id=instance_id,
172
- api_key=api_key,
173
- _force_new=True,
174
- )
175
- append_step(timings, "construct_sdk", step)
176
-
177
- config_kwargs = {"log_level": "DEBUG", "cache_backend": "sqlite"}
178
-
179
- if params.get("base_url"):
180
- config_kwargs["api_base_url"] = params["base_url"]
181
- if params.get("grpc_host"):
182
- config_kwargs["grpc_host"] = params["grpc_host"]
183
- if params.get("grpc_port"):
184
- config_kwargs["grpc_port"] = int(params["grpc_port"])
185
- if "grpc_use_tls" in params:
186
- config_kwargs["grpc_use_tls"] = bool(params["grpc_use_tls"])
187
-
188
- step = time.perf_counter()
189
- sdk.configure(**config_kwargs)
190
- append_step(timings, "configure_sdk", step)
191
-
192
- step = time.perf_counter()
193
- await sdk.initialize()
194
- append_step(timings, "initialize_sdk", step)
195
-
196
- grpc_listening = False
197
-
198
- step = time.perf_counter()
199
- try:
200
- await sdk.instance.listen(
201
- on_context=on_anticipated_context,
202
- on_reconnect=lambda attempt: logger.info(
203
- "gRPC reconnect attempt %d", attempt
204
- ),
205
- on_disconnect=lambda reason: logger.warning(
206
- "gRPC disconnected: %s", reason
207
- ),
208
- )
209
- grpc_listening = sdk.instance.is_listening
210
- except Exception as exc: # non-fatal
211
- logger.warning("gRPC listen failed (non-fatal): %s", exc)
212
- append_step(timings, "start_grpc_listener", step)
213
-
214
- return {
215
- "success": True,
216
- "instance_id": instance_id,
217
- "grpc_listening": grpc_listening,
218
- "bridgeTiming": {
219
- "python_total_ms": ms_since(handler_start),
220
- "steps": timings,
221
- },
222
- }
223
-
224
-
225
- async def handle_add_memory(params: dict) -> dict:
226
- handler_start = time.perf_counter()
227
- timings: List[dict] = []
228
-
229
- user_id = params["user_id"]
230
- customer_id = params.get("customer_id")
231
- messages = params["messages"]
232
- conversation_id = params.get("conversation_id")
233
- session_id = params.get("session_id")
234
-
235
- if not customer_id:
236
- raise ValueError("customer_id is required")
237
-
238
- step = time.perf_counter()
239
- transcript = messages_to_text(messages)
240
- append_step(timings, "build_transcript", step)
241
-
242
- if not transcript:
243
- return {
244
- "success": True,
245
- "latencyMs": 0,
246
- "rawResponse": {"note": "Empty input; skipped"},
247
- "note": "No text content to ingest",
248
- "bridgeTiming": {
249
- "python_total_ms": ms_since(handler_start),
250
- "steps": timings,
251
- },
252
- }
253
-
254
- start = time.perf_counter()
255
-
256
- step = time.perf_counter()
257
- mode = params.get("mode", "long-range")
258
- document_type = params.get("document_type", "ai-chat-conversation")
259
- document_id = params.get("document_id")
260
- document_created_at = params.get("document_created_at")
261
- metadata = params.get("metadata")
262
-
263
- create_kwargs: dict = {
264
- "document": transcript,
265
- "document_type": document_type,
266
- "user_id": user_id,
267
- "customer_id": customer_id,
268
- "mode": mode,
269
- }
270
- if document_id is not None:
271
- create_kwargs["document_id"] = document_id
272
- if document_created_at is not None:
273
- create_kwargs["document_created_at"] = document_created_at
274
- if metadata is not None:
275
- create_kwargs["metadata"] = metadata
276
-
277
- create_result = await sdk.memories.create(**create_kwargs)
278
- append_step(timings, "memories_create", step)
279
-
280
- ingestion_id = create_result.ingestion_id
281
-
282
- step = time.perf_counter()
283
- if sdk.instance.is_listening:
284
- for message in messages:
285
- content = (message.get("content") or "").strip()
286
- if not content:
287
- continue
288
- try:
289
- role = message.get("role", "user")
290
- await sdk.instance.send_message(
291
- content=content,
292
- role=role,
293
- conversation_id=conversation_id,
294
- user_id=user_id,
295
- customer_id=customer_id,
296
- session_id=session_id,
297
- event_type="assistant_message" if role == "assistant" else "user_message",
298
- metadata=message.get("metadata"),
299
- )
300
- except Exception as exc:
301
- logger.debug("gRPC send_message failed (non-fatal): %s", exc)
302
- append_step(timings, "grpc_send_messages", step)
303
-
304
- step = time.perf_counter()
305
- try:
306
- final_status = await sdk.memories.wait_for_completion(
307
- ingestion_id,
308
- timeout_seconds=60,
309
- poll_interval_seconds=2,
310
- )
311
- except TimeoutError:
312
- append_step(timings, "wait_for_completion_timeout", step)
313
- return {
314
- "success": True,
315
- "latencyMs": ms_since(start),
316
- "rawResponse": {"ingestion_id": str(ingestion_id), "status": "timeout"},
317
- "note": "Ingestion timed out after 60s and may still complete later",
318
- "bridgeTiming": {
319
- "python_total_ms": ms_since(handler_start),
320
- "steps": timings,
321
- },
322
- }
323
- append_step(timings, "wait_for_completion", step)
324
-
325
- memory_ids = [str(memory_id) for memory_id in (final_status.memory_ids or [])]
326
- if memory_ids:
327
- user_memory_ids.setdefault(tracking_key(user_id, customer_id), []).extend(memory_ids)
328
-
329
- return {
330
- "success": final_status.status.value != "failed",
331
- "latencyMs": ms_since(start),
332
- "rawResponse": {
333
- "ingestion_id": str(ingestion_id),
334
- "status": final_status.status.value,
335
- "memories_created": final_status.memories_created,
336
- "memory_ids": memory_ids,
337
- },
338
- "note": f"Ingestion {final_status.status.value}; created {final_status.memories_created}",
339
- "bridgeTiming": {
340
- "python_total_ms": ms_since(handler_start),
341
- "steps": timings,
342
- },
343
- }
344
-
345
-
346
- async def handle_search_memory(params: dict) -> dict:
347
- handler_start = time.perf_counter()
348
- timings: List[dict] = []
349
-
350
- user_id = params["user_id"]
351
- customer_id = params.get("customer_id")
352
- query = params["query"]
353
- max_results = params.get("max_results", 10)
354
- mode = params.get("mode", "fast")
355
- conversation_id = params.get("conversation_id")
356
- types = params.get("types", ["all"])
357
-
358
- start = time.perf_counter()
359
-
360
- step = time.perf_counter()
361
- context = await sdk.user.context.fetch(
362
- user_id=user_id,
363
- customer_id=customer_id,
364
- conversation_id=conversation_id,
365
- search_query=[query],
366
- max_results=max_results,
367
- types=types,
368
- mode=mode,
369
- )
370
- append_step(timings, "context_fetch", step)
371
-
372
- step = time.perf_counter()
373
- results = flatten_context_items(context)
374
- append_step(timings, "map_context_results", step)
375
-
376
- return {
377
- "success": True,
378
- "latencyMs": ms_since(start),
379
- "results": results,
380
- "resultsCount": len(results),
381
- "rawResponse": context.raw if hasattr(context, "raw") else {},
382
- "source": context.metadata.source if context.metadata else "unknown",
383
- "bridgeTiming": {
384
- "python_total_ms": ms_since(handler_start),
385
- "steps": timings,
386
- },
387
- }
388
-
389
-
390
- async def handle_get_memories(params: dict) -> dict:
391
- handler_start = time.perf_counter()
392
- timings: List[dict] = []
393
-
394
- user_id = params["user_id"]
395
- customer_id = params.get("customer_id")
396
- mode = params.get("mode", "fast")
397
- conversation_id = params.get("conversation_id")
398
- max_results = params.get("max_results", 100)
399
- types = params.get("types", ["all"])
400
-
401
- start = time.perf_counter()
402
-
403
- step = time.perf_counter()
404
- context = await sdk.user.context.fetch(
405
- user_id=user_id,
406
- customer_id=customer_id,
407
- conversation_id=conversation_id,
408
- search_query=[],
409
- max_results=max_results,
410
- types=types,
411
- mode=mode,
412
- )
413
- append_step(timings, "context_fetch_all", step)
414
-
415
- step = time.perf_counter()
416
- memories = flatten_context_items(context)
417
- append_step(timings, "map_memories", step)
418
-
419
- return {
420
- "success": True,
421
- "latencyMs": ms_since(start),
422
- "memories": memories,
423
- "memoriesCount": len(memories),
424
- "totalCount": len(memories),
425
- "rawResponse": context.raw if hasattr(context, "raw") else {},
426
- "source": context.metadata.source if context.metadata else "unknown",
427
- "bridgeTiming": {
428
- "python_total_ms": ms_since(handler_start),
429
- "steps": timings,
430
- },
431
- }
432
-
433
-
434
- async def handle_fetch_user_context(params: dict) -> dict:
435
- handler_start = time.perf_counter()
436
- timings: List[dict] = []
437
- start = time.perf_counter()
438
-
439
- step = time.perf_counter()
440
- context = await sdk.user.context.fetch(
441
- user_id=params["user_id"],
442
- customer_id=params.get("customer_id"),
443
- conversation_id=params.get("conversation_id"),
444
- search_query=params.get("search_query"),
445
- max_results=params.get("max_results", 10),
446
- types=params.get("types"),
447
- mode=params.get("mode", "fast"),
448
- )
449
- append_step(timings, "context_fetch", step)
450
-
451
- return {
452
- "success": True,
453
- "latencyMs": ms_since(start),
454
- "context": serialize_context_response(context),
455
- "bridgeTiming": {
456
- "python_total_ms": ms_since(handler_start),
457
- "steps": timings,
458
- },
459
- }
460
-
461
-
462
- async def handle_fetch_customer_context(params: dict) -> dict:
463
- handler_start = time.perf_counter()
464
- timings: List[dict] = []
465
- start = time.perf_counter()
466
-
467
- step = time.perf_counter()
468
- context = await sdk.customer.context.fetch(
469
- customer_id=params["customer_id"],
470
- conversation_id=params.get("conversation_id"),
471
- search_query=params.get("search_query"),
472
- max_results=params.get("max_results", 10),
473
- types=params.get("types"),
474
- mode=params.get("mode", "fast"),
475
- )
476
- append_step(timings, "context_fetch", step)
477
-
478
- return {
479
- "success": True,
480
- "latencyMs": ms_since(start),
481
- "context": serialize_context_response(context),
482
- "bridgeTiming": {
483
- "python_total_ms": ms_since(handler_start),
484
- "steps": timings,
485
- },
486
- }
487
-
488
-
489
- async def handle_fetch_client_context(params: dict) -> dict:
490
- handler_start = time.perf_counter()
491
- timings: List[dict] = []
492
- start = time.perf_counter()
493
-
494
- step = time.perf_counter()
495
- context = await sdk.client.context.fetch(
496
- conversation_id=params.get("conversation_id"),
497
- search_query=params.get("search_query"),
498
- max_results=params.get("max_results", 10),
499
- types=params.get("types"),
500
- mode=params.get("mode", "fast"),
501
- )
502
- append_step(timings, "context_fetch", step)
503
-
504
- return {
505
- "success": True,
506
- "latencyMs": ms_since(start),
507
- "context": serialize_context_response(context),
508
- "bridgeTiming": {
509
- "python_total_ms": ms_since(handler_start),
510
- "steps": timings,
511
- },
512
- }
513
-
514
-
515
- async def handle_get_context_for_prompt(params: dict) -> dict:
516
- handler_start = time.perf_counter()
517
- timings: List[dict] = []
518
- start = time.perf_counter()
519
-
520
- step = time.perf_counter()
521
- response = await sdk.conversation.get_context_for_prompt(
522
- conversation_id=params["conversation_id"],
523
- style=params.get("style", "structured"),
524
- )
525
- append_step(timings, "get_context_for_prompt", step)
526
-
527
- return {
528
- "success": True,
529
- "latencyMs": ms_since(start),
530
- "context_for_prompt": serialize_context_for_prompt_response(response),
531
- "bridgeTiming": {
532
- "python_total_ms": ms_since(handler_start),
533
- "steps": timings,
534
- },
535
- }
536
-
537
-
538
- async def handle_delete_memory(params: dict) -> dict:
539
- handler_start = time.perf_counter()
540
- timings: List[dict] = []
541
-
542
- user_id = params["user_id"]
543
- customer_id = params.get("customer_id")
544
- memory_id = params.get("memory_id")
545
-
546
- start = time.perf_counter()
547
-
548
- step = time.perf_counter()
549
- if memory_id:
550
- await sdk.memories.delete(UUID(memory_id))
551
- append_step(timings, "delete_single_memory", step)
552
- return {
553
- "success": True,
554
- "latencyMs": ms_since(start),
555
- "deletedCount": 1,
556
- "rawResponse": {"deleted": 1},
557
- "bridgeTiming": {
558
- "python_total_ms": ms_since(handler_start),
559
- "steps": timings,
560
- },
561
- }
562
-
563
- if not customer_id:
564
- raise ValueError("customer_id is required when memory_id is not provided")
565
-
566
- tracked_ids = user_memory_ids.get(tracking_key(user_id, customer_id), [])
567
- if not tracked_ids:
568
- return {
569
- "success": True,
570
- "latencyMs": 0,
571
- "deletedCount": 0,
572
- "rawResponse": None,
573
- "note": "No tracked memory IDs for this user",
574
- "bridgeTiming": {
575
- "python_total_ms": ms_since(handler_start),
576
- "steps": timings,
577
- },
578
- }
579
-
580
- last_error: Optional[str] = None
581
-
582
- step = time.perf_counter()
583
- for tracked_id in tracked_ids:
584
- try:
585
- await sdk.memories.delete(UUID(tracked_id))
586
- except Exception as exc:
587
- last_error = str(exc)
588
- append_step(timings, "delete_tracked_memories", step)
589
-
590
- user_memory_ids.pop(tracking_key(user_id, customer_id), None)
591
-
592
- if last_error:
593
- return {
594
- "success": False,
595
- "latencyMs": ms_since(start),
596
- "error": last_error,
597
- "bridgeTiming": {
598
- "python_total_ms": ms_since(handler_start),
599
- "steps": timings,
600
- },
601
- }
602
-
603
- return {
604
- "success": True,
605
- "latencyMs": ms_since(start),
606
- "deletedCount": len(tracked_ids),
607
- "rawResponse": {"deleted": len(tracked_ids)},
608
- "note": f"Deleted {len(tracked_ids)} memories",
609
- "bridgeTiming": {
610
- "python_total_ms": ms_since(handler_start),
611
- "steps": timings,
612
- },
613
- }
614
-
615
-
616
- async def handle_shutdown(_params: dict) -> dict:
617
- handler_start = time.perf_counter()
618
- timings: List[dict] = []
619
-
620
- if sdk:
621
- step = time.perf_counter()
622
- try:
623
- await sdk.instance.stop_listening()
624
- except Exception:
625
- pass
626
- append_step(timings, "stop_listener", step)
627
-
628
- step = time.perf_counter()
629
- await sdk.shutdown()
630
- append_step(timings, "sdk_shutdown", step)
631
- return {
632
- "success": True,
633
- "bridgeTiming": {
634
- "python_total_ms": ms_since(handler_start),
635
- "steps": timings,
636
- },
637
- }
638
-
639
-
640
- async def handle_record_message(params: dict) -> dict:
641
- """Record a single conversation turn (registers the conversation_id).
642
-
643
- Mirrors sdk.conversation.record_message(...) so the JS client can expose
644
- the same namespaced surface the integration packages duck-type against.
645
- """
646
- handler_start = time.perf_counter()
647
- timings: List[dict] = []
648
- start = time.perf_counter()
649
- step = time.perf_counter()
650
- result = await sdk.conversation.record_message(
651
- conversation_id=params["conversation_id"],
652
- role=params["role"],
653
- content=params["content"],
654
- user_id=params["user_id"],
655
- customer_id=params["customer_id"],
656
- session_id=params.get("session_id"),
657
- metadata=params.get("metadata"),
658
- )
659
- append_step(timings, "record_message", step)
660
- return {
661
- "success": True,
662
- "latencyMs": ms_since(start),
663
- "result": result,
664
- "bridgeTiming": {"python_total_ms": ms_since(handler_start), "steps": timings},
665
- }
666
-
667
-
668
- async def handle_create_memory(params: dict) -> dict:
669
- """Create a durable memory from a single document string.
670
-
671
- Mirrors sdk.memories.create(document=...). Distinct from add_memory,
672
- which builds a transcript from a messages array.
673
- """
674
- handler_start = time.perf_counter()
675
- timings: List[dict] = []
676
- start = time.perf_counter()
677
- create_kwargs: dict = {"document": params["document"]}
678
- for key in ("user_id", "customer_id", "document_type", "document_id",
679
- "document_created_at", "mode", "metadata"):
680
- if params.get(key) is not None:
681
- create_kwargs[key] = params[key]
682
- step = time.perf_counter()
683
- result = await sdk.memories.create(**create_kwargs)
684
- append_step(timings, "memories_create", step)
685
- status = result.status.value if hasattr(result.status, "value") else result.status
686
- return {
687
- "success": True,
688
- "latencyMs": ms_since(start),
689
- "result": {
690
- "ingestion_id": str(result.ingestion_id),
691
- "document_id": result.document_id,
692
- "status": status,
693
- },
694
- "bridgeTiming": {"python_total_ms": ms_since(handler_start), "steps": timings},
695
- }
696
-
697
-
698
- HANDLERS = {
699
- "init": handle_init,
700
- "add_memory": handle_add_memory,
701
- "create_memory": handle_create_memory,
702
- "record_message": handle_record_message,
703
- "search_memory": handle_search_memory,
704
- "get_memories": handle_get_memories,
705
- "fetch_user_context": handle_fetch_user_context,
706
- "fetch_customer_context": handle_fetch_customer_context,
707
- "fetch_client_context": handle_fetch_client_context,
708
- "get_context_for_prompt": handle_get_context_for_prompt,
709
- "delete_memory": handle_delete_memory,
710
- "shutdown": handle_shutdown,
711
- }
712
-
713
-
714
- async def main() -> None:
715
- logger.info("Synap bridge starting")
716
-
717
- reader = asyncio.StreamReader()
718
- protocol = asyncio.StreamReaderProtocol(reader)
719
- await asyncio.get_event_loop().connect_read_pipe(lambda: protocol, sys.stdin)
720
-
721
- while True:
722
- line = await reader.readline()
723
- if not line:
724
- logger.info("stdin closed; shutting down bridge")
725
- if sdk and getattr(sdk, "_initialized", False):
726
- await handle_shutdown({})
727
- break
728
-
729
- payload = line.decode().strip()
730
- if not payload:
731
- continue
732
-
733
- try:
734
- request = json.loads(payload)
735
- except json.JSONDecodeError as exc:
736
- write_response({"id": None, "result": None, "error": f"Invalid JSON: {exc}"})
737
- continue
738
-
739
- req_id = request.get("id")
740
- method = request.get("method")
741
- params = request.get("params", {})
742
-
743
- handler = HANDLERS.get(method)
744
- if not handler:
745
- write_response({"id": req_id, "result": None, "error": f"Unknown method: {method}"})
746
- continue
747
-
748
- try:
749
- handler_started = time.perf_counter()
750
- result = await handler(params)
751
- if isinstance(result, dict):
752
- bridge_timing = result.get("bridgeTiming")
753
- if isinstance(bridge_timing, dict):
754
- bridge_timing.setdefault("python_total_ms", ms_since(handler_started))
755
- bridge_timing.setdefault("steps", [])
756
- else:
757
- result["bridgeTiming"] = {
758
- "python_total_ms": ms_since(handler_started),
759
- "steps": [],
760
- }
761
- write_response({"id": req_id, "result": result, "error": None})
762
- except Exception as exc:
763
- logger.error("Handler error for %s: %s", method, traceback.format_exc())
764
- write_response({"id": req_id, "result": None, "error": str(exc), "error_type": type(exc).__name__})
765
-
766
-
767
- if __name__ == "__main__":
768
- asyncio.run(main())