edgeone 1.6.1 → 1.6.3
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.
- package/edgeone-dist/cli.js +2326 -1118
- package/edgeone-dist/pages/dev/runner-worker.js +2078 -872
- package/edgeone-dist/pages/templates/agent-python/adapter.py +164 -32
- package/edgeone-dist/pages/templates/agent-python/context.py +15 -0
- package/edgeone-dist/pages/templates/agent-python/memory.py +272 -101
- package/edgeone-dist/pages/templates/agent-python/runtime.py +19 -13
- package/package.json +1 -1
|
@@ -257,6 +257,18 @@ def _generate_message_id() -> str:
|
|
|
257
257
|
return f"msg_{uuid.uuid4().hex[:16]}"
|
|
258
258
|
|
|
259
259
|
|
|
260
|
+
def _extract_message_id_from_key(key: str) -> str:
|
|
261
|
+
"""Extract messageId from a message key.
|
|
262
|
+
|
|
263
|
+
Key format: conversations/{cid}/messages/{created_at}_{message_id}
|
|
264
|
+
The message_id part starts with 'msg_' so we split on the first '_' after
|
|
265
|
+
the timestamp to get everything after it.
|
|
266
|
+
"""
|
|
267
|
+
basename = key.rsplit("/", 1)[-1] # "{created_at}_{message_id}"
|
|
268
|
+
parts = basename.split("_", 1)
|
|
269
|
+
return parts[1] if len(parts) > 1 else ""
|
|
270
|
+
|
|
271
|
+
|
|
260
272
|
# ─── Cursor Helpers ───
|
|
261
273
|
|
|
262
274
|
|
|
@@ -424,10 +436,19 @@ class ConversationMemory:
|
|
|
424
436
|
except Exception as e:
|
|
425
437
|
raise MemoryStorageError(f"Failed to delete key '{key}': {e}") from e
|
|
426
438
|
|
|
427
|
-
async def _blob_list_keys(self, prefix: str, **kwargs) -> List[str]:
|
|
428
|
-
"""List
|
|
439
|
+
async def _blob_list_keys(self, prefix: str, *, limit: Optional[int] = None, **kwargs) -> List[str]:
|
|
440
|
+
"""List keys with given prefix, sorted lexicographically.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
prefix: Key prefix to filter.
|
|
444
|
+
limit: Max keys to return (None = all). Passed to blob SDK.
|
|
445
|
+
"""
|
|
429
446
|
try:
|
|
430
|
-
|
|
447
|
+
list_kwargs = {"prefix": prefix, **kwargs}
|
|
448
|
+
if limit is not None:
|
|
449
|
+
list_kwargs["limit"] = limit
|
|
450
|
+
list_kwargs["paginate"] = False
|
|
451
|
+
result = await self._blob.list(**list_kwargs)
|
|
431
452
|
return [blob.key for blob in result.blobs]
|
|
432
453
|
except MemoryStorageError:
|
|
433
454
|
raise
|
|
@@ -523,46 +544,53 @@ class ConversationMemory:
|
|
|
523
544
|
metadata=metadata if metadata else None,
|
|
524
545
|
)
|
|
525
546
|
|
|
526
|
-
#
|
|
527
|
-
|
|
528
|
-
await self._blob_set_json(msg_key, message.to_dict())
|
|
547
|
+
# --- Parallel writes (no data dependency between these) ---
|
|
548
|
+
import asyncio
|
|
529
549
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
{
|
|
534
|
-
"conversationId": conversation_id,
|
|
535
|
-
"key": msg_key,
|
|
536
|
-
"messageId": message_id,
|
|
537
|
-
"createdAt": now_ms,
|
|
538
|
-
},
|
|
539
|
-
)
|
|
550
|
+
msg_key = _message_key(encoded_cid, now_ms, message_id)
|
|
551
|
+
new_index_key = _index_key(now_ms, encoded_cid)
|
|
552
|
+
index_entry = {"conversationId": conversation_id, "lastMessageAt": now_ms}
|
|
540
553
|
|
|
541
|
-
# Update conversation meta
|
|
554
|
+
# Update conversation meta (prepare before write)
|
|
542
555
|
meta.last_message_at = now_ms
|
|
543
556
|
meta.message_count += 1
|
|
544
|
-
await self._blob_set_json(meta_k, meta.to_dict())
|
|
545
|
-
|
|
546
|
-
# Update conversation index: write new first, then delete old (safety)
|
|
547
|
-
new_index_key = _index_key(now_ms, encoded_cid)
|
|
548
|
-
index_entry = {"conversationId": conversation_id, "lastMessageAt": now_ms}
|
|
549
|
-
await self._blob_set_json(new_index_key, index_entry)
|
|
550
557
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
558
|
+
# Batch write: message body + message index + meta + conversation index
|
|
559
|
+
write_tasks = [
|
|
560
|
+
self._blob_set_json(msg_key, message.to_dict()),
|
|
561
|
+
self._blob_set_json(
|
|
562
|
+
_message_index_key(message_id),
|
|
563
|
+
{
|
|
564
|
+
"conversationId": conversation_id,
|
|
565
|
+
"key": msg_key,
|
|
566
|
+
"messageId": message_id,
|
|
567
|
+
"createdAt": now_ms,
|
|
568
|
+
},
|
|
569
|
+
),
|
|
570
|
+
self._blob_set_json(meta_k, meta.to_dict()),
|
|
571
|
+
self._blob_set_json(new_index_key, index_entry),
|
|
572
|
+
]
|
|
554
573
|
|
|
555
|
-
#
|
|
574
|
+
# User conversation index (if user_id provided)
|
|
556
575
|
if user_id is not None:
|
|
557
576
|
encoded_user_id = _encode_segment(user_id)
|
|
558
577
|
new_user_key = _user_index_key(encoded_user_id, now_ms, encoded_cid)
|
|
559
578
|
index_entry_user = {"conversationId": conversation_id, "lastMessageAt": now_ms}
|
|
560
|
-
|
|
579
|
+
write_tasks.append(self._blob_set_json(new_user_key, index_entry_user))
|
|
561
580
|
|
|
562
|
-
|
|
581
|
+
await asyncio.gather(*write_tasks)
|
|
582
|
+
|
|
583
|
+
# --- Cleanup old index keys (can also be parallel) ---
|
|
584
|
+
delete_tasks = []
|
|
585
|
+
if old_last_message_at is not None and old_last_message_at != now_ms:
|
|
586
|
+
old_index_key = _index_key(old_last_message_at, encoded_cid)
|
|
587
|
+
delete_tasks.append(self._blob_delete(old_index_key))
|
|
588
|
+
|
|
589
|
+
if user_id is not None:
|
|
590
|
+
encoded_user_id = _encode_segment(user_id)
|
|
563
591
|
if old_last_message_at is not None and old_last_message_at != now_ms:
|
|
564
592
|
old_user_key = _user_index_key(encoded_user_id, old_last_message_at, encoded_cid)
|
|
565
|
-
|
|
593
|
+
delete_tasks.append(self._blob_delete(old_user_key))
|
|
566
594
|
elif old_last_message_at is None:
|
|
567
595
|
# First message in this conversation — scan for stale user index
|
|
568
596
|
# (edge case: conversation was deleted + recreated under same id)
|
|
@@ -570,19 +598,121 @@ class ConversationMemory:
|
|
|
570
598
|
existing_keys = await self._blob_list_keys(prefix)
|
|
571
599
|
suffix = f"_{encoded_cid}"
|
|
572
600
|
for k in existing_keys:
|
|
573
|
-
if k.endswith(suffix) and k !=
|
|
574
|
-
|
|
601
|
+
if k.endswith(suffix) and k != _user_index_key(encoded_user_id, now_ms, encoded_cid):
|
|
602
|
+
delete_tasks.append(self._blob_delete(k))
|
|
603
|
+
|
|
604
|
+
if delete_tasks:
|
|
605
|
+
await asyncio.gather(*delete_tasks)
|
|
575
606
|
|
|
576
607
|
return message_id
|
|
577
608
|
|
|
609
|
+
async def _bulk_append_messages(
|
|
610
|
+
self,
|
|
611
|
+
conversation_id: str,
|
|
612
|
+
messages_data: List[dict],
|
|
613
|
+
) -> List[str]:
|
|
614
|
+
"""Internal bulk append: write N messages with only 1 meta/index update.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
conversation_id: Conversation identifier.
|
|
618
|
+
messages_data: List of dicts with keys: role, content, metadata.
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
List of generated message_ids.
|
|
622
|
+
"""
|
|
623
|
+
import asyncio
|
|
624
|
+
|
|
625
|
+
if not messages_data:
|
|
626
|
+
return []
|
|
627
|
+
|
|
628
|
+
encoded_cid = _encode_cid(conversation_id)
|
|
629
|
+
now_ms = int(time.time() * 1000)
|
|
630
|
+
|
|
631
|
+
# Load meta once
|
|
632
|
+
meta_k = _meta_key(encoded_cid)
|
|
633
|
+
meta_data = await self._blob_get_json(meta_k)
|
|
634
|
+
|
|
635
|
+
if meta_data is not None:
|
|
636
|
+
meta = ConversationMeta.from_dict(meta_data)
|
|
637
|
+
if meta.message_count + len(messages_data) > _MAX_MESSAGES_PER_CONVERSATION:
|
|
638
|
+
raise MemoryQuotaExceededError(
|
|
639
|
+
f"Conversation '{conversation_id}' would exceed {_MAX_MESSAGES_PER_CONVERSATION} messages"
|
|
640
|
+
)
|
|
641
|
+
old_last_message_at = meta.last_message_at
|
|
642
|
+
else:
|
|
643
|
+
meta = ConversationMeta(
|
|
644
|
+
conversation_id=conversation_id,
|
|
645
|
+
created_at=now_ms,
|
|
646
|
+
last_message_at=0,
|
|
647
|
+
message_count=0,
|
|
648
|
+
metadata=None,
|
|
649
|
+
)
|
|
650
|
+
old_last_message_at = None
|
|
651
|
+
|
|
652
|
+
# Build all messages and collect write tasks
|
|
653
|
+
write_tasks = []
|
|
654
|
+
message_ids = []
|
|
655
|
+
last_ts = now_ms
|
|
656
|
+
|
|
657
|
+
for i, msg_data in enumerate(messages_data):
|
|
658
|
+
# Increment timestamp by 1ms per message to ensure unique keys + ordering
|
|
659
|
+
ts = now_ms + i
|
|
660
|
+
last_ts = ts
|
|
661
|
+
message_id = _generate_message_id()
|
|
662
|
+
message_ids.append(message_id)
|
|
663
|
+
|
|
664
|
+
msg_metadata = dict(msg_data.get("metadata") or {})
|
|
665
|
+
if "run_id" not in msg_metadata:
|
|
666
|
+
msg_metadata["run_id"] = self._run_id
|
|
667
|
+
|
|
668
|
+
message = Message(
|
|
669
|
+
message_id=message_id,
|
|
670
|
+
role=msg_data["role"],
|
|
671
|
+
content=msg_data["content"],
|
|
672
|
+
created_at=ts,
|
|
673
|
+
metadata=msg_metadata if msg_metadata else None,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
msg_key = _message_key(encoded_cid, ts, message_id)
|
|
677
|
+
write_tasks.append(self._blob_set_json(msg_key, message.to_dict()))
|
|
678
|
+
write_tasks.append(self._blob_set_json(
|
|
679
|
+
_message_index_key(message_id),
|
|
680
|
+
{"conversationId": conversation_id, "key": msg_key, "messageId": message_id, "createdAt": ts},
|
|
681
|
+
))
|
|
682
|
+
|
|
683
|
+
# Update meta once (for all messages)
|
|
684
|
+
meta.last_message_at = last_ts
|
|
685
|
+
meta.message_count += len(messages_data)
|
|
686
|
+
write_tasks.append(self._blob_set_json(meta_k, meta.to_dict()))
|
|
687
|
+
|
|
688
|
+
# Update conversation index once
|
|
689
|
+
new_index_key = _index_key(last_ts, encoded_cid)
|
|
690
|
+
write_tasks.append(self._blob_set_json(
|
|
691
|
+
new_index_key, {"conversationId": conversation_id, "lastMessageAt": last_ts}
|
|
692
|
+
))
|
|
693
|
+
|
|
694
|
+
# Parallel write all
|
|
695
|
+
await asyncio.gather(*write_tasks)
|
|
696
|
+
|
|
697
|
+
# Cleanup old index
|
|
698
|
+
if old_last_message_at is not None and old_last_message_at != last_ts:
|
|
699
|
+
await self._blob_delete(_index_key(old_last_message_at, encoded_cid))
|
|
700
|
+
|
|
701
|
+
return message_ids
|
|
702
|
+
|
|
578
703
|
async def _check_idempotency(self, encoded_cid: str, idempotency_key: str) -> Optional[str]:
|
|
579
704
|
"""Scan recent messages for matching idempotency_key. Returns message_id if found."""
|
|
705
|
+
import asyncio
|
|
706
|
+
|
|
580
707
|
prefix = _messages_prefix(encoded_cid)
|
|
581
708
|
keys = await self._blob_list_keys(prefix)
|
|
582
|
-
# Scan at most last 50 messages for
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
709
|
+
# Scan at most last 50 messages — parallel fetch for performance
|
|
710
|
+
check_keys = keys[-min(len(keys), 50):]
|
|
711
|
+
if not check_keys:
|
|
712
|
+
return None
|
|
713
|
+
|
|
714
|
+
results = await asyncio.gather(*[self._blob_get_json(k) for k in check_keys])
|
|
715
|
+
for data in results:
|
|
586
716
|
if data and isinstance(data.get("metadata"), dict):
|
|
587
717
|
if data["metadata"].get("idempotency_key") == idempotency_key:
|
|
588
718
|
return data.get("messageId") or data.get("message_id", "")
|
|
@@ -766,26 +896,31 @@ class ConversationMemory:
|
|
|
766
896
|
raise MemoryNotFoundError(f"Conversation '{conversation_id}' not found")
|
|
767
897
|
previous_meta = ConversationMeta.from_dict(meta_data)
|
|
768
898
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
899
|
+
import asyncio
|
|
900
|
+
|
|
901
|
+
# 删消息本体 + 二级索引 (parallel)
|
|
902
|
+
await asyncio.gather(
|
|
903
|
+
self._blob_delete(msg_key),
|
|
904
|
+
self._blob_delete(_message_index_key(message_id)),
|
|
905
|
+
)
|
|
772
906
|
|
|
773
907
|
# 重算 meta:扫剩余消息得到最新 last_message_at;空则回退到 created_at
|
|
774
908
|
next_meta = await self._recalculate_conversation_meta(encoded_cid, previous_meta)
|
|
775
|
-
await self._blob_set_json(meta_k, next_meta.to_dict())
|
|
776
909
|
|
|
777
|
-
# 更新 conversation_index
|
|
910
|
+
# 更新 meta + conversation_index (parallel)
|
|
778
911
|
new_index_key = _index_key(next_meta.last_message_at, encoded_cid)
|
|
779
912
|
old_index_key = _index_key(previous_meta.last_message_at, encoded_cid)
|
|
913
|
+
write_ops: list = [self._blob_set_json(meta_k, next_meta.to_dict())]
|
|
780
914
|
if new_index_key != old_index_key:
|
|
781
|
-
|
|
915
|
+
write_ops.append(self._blob_set_json(
|
|
782
916
|
new_index_key,
|
|
783
917
|
{
|
|
784
918
|
"conversationId": conversation_id,
|
|
785
919
|
"lastMessageAt": next_meta.last_message_at,
|
|
786
920
|
},
|
|
787
|
-
)
|
|
788
|
-
|
|
921
|
+
))
|
|
922
|
+
write_ops.append(self._blob_delete(old_index_key))
|
|
923
|
+
await asyncio.gather(*write_ops)
|
|
789
924
|
|
|
790
925
|
async def _recalculate_conversation_meta(
|
|
791
926
|
self,
|
|
@@ -830,6 +965,8 @@ class ConversationMemory:
|
|
|
830
965
|
Raises:
|
|
831
966
|
MemoryNotFoundError: conversation 不存在。
|
|
832
967
|
"""
|
|
968
|
+
import asyncio
|
|
969
|
+
|
|
833
970
|
self._validate_conversation_id(conversation_id)
|
|
834
971
|
|
|
835
972
|
encoded_cid = _encode_cid(conversation_id)
|
|
@@ -841,17 +978,21 @@ class ConversationMemory:
|
|
|
841
978
|
|
|
842
979
|
previous_meta = ConversationMeta.from_dict(meta_data)
|
|
843
980
|
|
|
844
|
-
#
|
|
981
|
+
# Delete all message bodies + message_index entries in parallel.
|
|
982
|
+
# Extract messageId from key name (no need to read message body).
|
|
845
983
|
prefix = _messages_prefix(encoded_cid)
|
|
846
984
|
keys = await self._blob_list_keys(prefix)
|
|
847
|
-
for key in keys:
|
|
848
|
-
data = await self._blob_get_json(key)
|
|
849
|
-
await self._blob_delete(key)
|
|
850
|
-
if data and isinstance(data.get("messageId") or data.get("message_id"), str):
|
|
851
|
-
await self._blob_delete(_message_index_key(data.get("messageId") or data.get("message_id", "")))
|
|
852
985
|
|
|
853
|
-
|
|
854
|
-
|
|
986
|
+
delete_ops: list = []
|
|
987
|
+
for key in keys:
|
|
988
|
+
delete_ops.append(self._blob_delete(key))
|
|
989
|
+
msg_id = _extract_message_id_from_key(key)
|
|
990
|
+
if msg_id:
|
|
991
|
+
delete_ops.append(self._blob_delete(_message_index_key(msg_id)))
|
|
992
|
+
if delete_ops:
|
|
993
|
+
await asyncio.gather(*delete_ops)
|
|
994
|
+
|
|
995
|
+
# 重置 meta + 更新 conversation_index in parallel
|
|
855
996
|
next_meta = ConversationMeta(
|
|
856
997
|
conversation_id=previous_meta.conversation_id,
|
|
857
998
|
created_at=previous_meta.created_at,
|
|
@@ -859,20 +1000,20 @@ class ConversationMemory:
|
|
|
859
1000
|
message_count=0,
|
|
860
1001
|
metadata=previous_meta.metadata,
|
|
861
1002
|
)
|
|
862
|
-
await self._blob_set_json(meta_k, next_meta.to_dict())
|
|
863
|
-
|
|
864
|
-
# 更新 conversation_index
|
|
865
1003
|
new_index_key = _index_key(next_meta.last_message_at, encoded_cid)
|
|
866
1004
|
old_index_key = _index_key(previous_meta.last_message_at, encoded_cid)
|
|
1005
|
+
|
|
1006
|
+
write_ops: list = [self._blob_set_json(meta_k, next_meta.to_dict())]
|
|
867
1007
|
if new_index_key != old_index_key:
|
|
868
|
-
|
|
1008
|
+
write_ops.append(self._blob_set_json(
|
|
869
1009
|
new_index_key,
|
|
870
1010
|
{
|
|
871
1011
|
"conversationId": conversation_id,
|
|
872
1012
|
"lastMessageAt": next_meta.last_message_at,
|
|
873
1013
|
},
|
|
874
|
-
)
|
|
875
|
-
|
|
1014
|
+
))
|
|
1015
|
+
write_ops.append(self._blob_delete(old_index_key))
|
|
1016
|
+
await asyncio.gather(*write_ops)
|
|
876
1017
|
|
|
877
1018
|
async def list_conversations(
|
|
878
1019
|
self,
|
|
@@ -908,9 +1049,19 @@ class ConversationMemory:
|
|
|
908
1049
|
else:
|
|
909
1050
|
prefix = _index_prefix()
|
|
910
1051
|
|
|
911
|
-
#
|
|
912
|
-
#
|
|
913
|
-
|
|
1052
|
+
# Optimization: in the default case (desc order, no before cursor, using after or fresh),
|
|
1053
|
+
# blob list's natural lexicographic order = most-recent-first (reversed timestamp),
|
|
1054
|
+
# so we can pass limit directly to avoid full scan.
|
|
1055
|
+
can_use_sdk_limit = (order == "desc" and before is None)
|
|
1056
|
+
|
|
1057
|
+
if can_use_sdk_limit:
|
|
1058
|
+
# Request limit+1 extra to detect hasMore; if using after cursor,
|
|
1059
|
+
# we can't easily skip via SDK cursor, so fetch more and filter client-side.
|
|
1060
|
+
# For the common first-page case (no after), this is optimal.
|
|
1061
|
+
fetch_limit = limit + 1 if after is None else None
|
|
1062
|
+
keys = await self._blob_list_keys(prefix, consistency="strong", limit=fetch_limit)
|
|
1063
|
+
else:
|
|
1064
|
+
keys = await self._blob_list_keys(prefix, consistency="strong")
|
|
914
1065
|
|
|
915
1066
|
# Default order is desc (most recent first). With revTs, ascending lex order
|
|
916
1067
|
# already means most-recent-first, so desc = natural order, asc = reversed.
|
|
@@ -929,26 +1080,35 @@ class ConversationMemory:
|
|
|
929
1080
|
cursor_sk = _cursor_sort_key(cursor_data["lastMessageAt"], cursor_data["conversationId"])
|
|
930
1081
|
keys = self._apply_before_cursor(keys, prefix_len, cursor_sk, order)
|
|
931
1082
|
|
|
932
|
-
#
|
|
933
|
-
|
|
934
|
-
|
|
1083
|
+
# Parse encoded_cids from key basenames, take limit+1 to detect next page
|
|
1084
|
+
import asyncio
|
|
1085
|
+
|
|
1086
|
+
candidate_cids: List[str] = []
|
|
935
1087
|
for key in keys:
|
|
936
|
-
if len(
|
|
937
|
-
has_more = True
|
|
1088
|
+
if len(candidate_cids) > limit:
|
|
938
1089
|
break
|
|
939
|
-
# Parse encoded_cid from key basename: {revTs:016d}_{encoded_cid}
|
|
940
1090
|
remainder = key[prefix_len:]
|
|
941
1091
|
sep_idx = remainder.find("_")
|
|
942
1092
|
if sep_idx < 0:
|
|
943
1093
|
continue
|
|
944
|
-
|
|
945
|
-
|
|
1094
|
+
candidate_cids.append(remainder[sep_idx + 1:])
|
|
1095
|
+
|
|
1096
|
+
# Fetch metas in parallel (aligned with Node Promise.all approach)
|
|
1097
|
+
meta_results = await asyncio.gather(
|
|
1098
|
+
*[self._blob_get_json(_meta_key(cid), consistency="strong") for cid in candidate_cids]
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
items: List[ConversationMeta] = []
|
|
1102
|
+
has_more = False
|
|
1103
|
+
for meta_data in meta_results:
|
|
1104
|
+
if len(items) >= limit:
|
|
1105
|
+
has_more = True
|
|
1106
|
+
break
|
|
946
1107
|
if meta_data:
|
|
947
1108
|
try:
|
|
948
1109
|
items.append(ConversationMeta.from_dict(meta_data))
|
|
949
1110
|
except (KeyError, TypeError):
|
|
950
|
-
continue
|
|
951
|
-
# else: residual index (conversation deleted), skip silently
|
|
1111
|
+
continue
|
|
952
1112
|
|
|
953
1113
|
# Build cursors
|
|
954
1114
|
next_cursor: Optional[str] = None
|
|
@@ -1021,32 +1181,39 @@ class ConversationMemory:
|
|
|
1021
1181
|
Raises:
|
|
1022
1182
|
MemoryNotFoundError: Conversation doesn't exist.
|
|
1023
1183
|
"""
|
|
1184
|
+
import asyncio
|
|
1185
|
+
|
|
1024
1186
|
self._validate_conversation_id(conversation_id)
|
|
1025
1187
|
|
|
1026
1188
|
encoded_cid = _encode_cid(conversation_id)
|
|
1027
1189
|
meta_k = _meta_key(encoded_cid)
|
|
1028
|
-
|
|
1190
|
+
|
|
1191
|
+
# Read meta + list messages in parallel
|
|
1192
|
+
prefix = _messages_prefix(encoded_cid)
|
|
1193
|
+
meta_data, keys = await asyncio.gather(
|
|
1194
|
+
self._blob_get_json(meta_k),
|
|
1195
|
+
self._blob_list_keys(prefix),
|
|
1196
|
+
)
|
|
1029
1197
|
|
|
1030
1198
|
if meta_data is None:
|
|
1031
1199
|
raise MemoryNotFoundError(f"Conversation '{conversation_id}' not found")
|
|
1032
1200
|
|
|
1033
1201
|
meta = ConversationMeta.from_dict(meta_data)
|
|
1034
1202
|
|
|
1035
|
-
# Delete all message
|
|
1036
|
-
|
|
1037
|
-
|
|
1203
|
+
# Delete all message bodies + message_index entries in parallel.
|
|
1204
|
+
# Extract messageId from key name (no need to read message body).
|
|
1205
|
+
delete_ops: list = []
|
|
1038
1206
|
for key in keys:
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
if
|
|
1042
|
-
|
|
1207
|
+
delete_ops.append(self._blob_delete(key))
|
|
1208
|
+
msg_id = _extract_message_id_from_key(key)
|
|
1209
|
+
if msg_id:
|
|
1210
|
+
delete_ops.append(self._blob_delete(_message_index_key(msg_id)))
|
|
1043
1211
|
|
|
1044
|
-
# Delete index entry
|
|
1045
|
-
|
|
1046
|
-
|
|
1212
|
+
# Delete meta + conversation index entry
|
|
1213
|
+
delete_ops.append(self._blob_delete(meta_k))
|
|
1214
|
+
delete_ops.append(self._blob_delete(_index_key(meta.last_message_at, encoded_cid)))
|
|
1047
1215
|
|
|
1048
|
-
|
|
1049
|
-
await self._blob_delete(meta_k)
|
|
1216
|
+
await asyncio.gather(*delete_ops)
|
|
1050
1217
|
|
|
1051
1218
|
async def update_conversation(
|
|
1052
1219
|
self, conversation_id: str, metadata: dict
|
|
@@ -1400,13 +1567,13 @@ class _LangGraphStoreAdapter(_LangGraphBaseStore):
|
|
|
1400
1567
|
prefix = self._search_prefix(ns_prefix)
|
|
1401
1568
|
listed = await self._memory._blob.list(prefix=prefix, consistency="strong")
|
|
1402
1569
|
keys = [blob.key for blob in listed.blobs]
|
|
1403
|
-
|
|
1404
|
-
for bk in keys
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
it
|
|
1408
|
-
if it and self._namespace_starts_with(it["namespace"], ns_prefix)
|
|
1409
|
-
|
|
1570
|
+
# Filter keys that contain the separator, then parallel fetch
|
|
1571
|
+
valid_keys = [bk for bk in keys if f"/{_LANGGRAPH_STORE_KEY_SEPARATOR}/" in bk]
|
|
1572
|
+
items_raw = await asyncio.gather(*[self._parse_stored_item(bk) for bk in valid_keys])
|
|
1573
|
+
items: list[dict] = [
|
|
1574
|
+
it for it in items_raw
|
|
1575
|
+
if it and self._namespace_starts_with(it["namespace"], ns_prefix)
|
|
1576
|
+
]
|
|
1410
1577
|
filtered = [it for it in items if self._matches_filter(it, getattr(op, "filter", None))]
|
|
1411
1578
|
filtered.sort(key=lambda it: ("/".join(it["namespace"]), it["key"]))
|
|
1412
1579
|
offset = getattr(op, "offset", 0) or 0
|
|
@@ -1431,11 +1598,12 @@ class _LangGraphStoreAdapter(_LangGraphBaseStore):
|
|
|
1431
1598
|
all_prefix = f"{_LANGGRAPH_STORE_PREFIX}/items/"
|
|
1432
1599
|
listed = await self._memory._blob.list(prefix=all_prefix, consistency="strong")
|
|
1433
1600
|
keys = [blob.key for blob in listed.blobs]
|
|
1601
|
+
# Filter keys with separator, then parallel fetch
|
|
1602
|
+
valid_keys = [bk for bk in keys if f"/{_LANGGRAPH_STORE_KEY_SEPARATOR}/" in bk]
|
|
1603
|
+
items_raw = await asyncio.gather(*[self._parse_stored_item(bk) for bk in valid_keys])
|
|
1604
|
+
|
|
1434
1605
|
namespaces: set[str] = set()
|
|
1435
|
-
for
|
|
1436
|
-
if f"/{_LANGGRAPH_STORE_KEY_SEPARATOR}/" not in bk:
|
|
1437
|
-
continue
|
|
1438
|
-
it = await self._parse_stored_item(bk)
|
|
1606
|
+
for it in items_raw:
|
|
1439
1607
|
if not it:
|
|
1440
1608
|
continue
|
|
1441
1609
|
ns = [str(s) for s in it["namespace"]]
|
|
@@ -2031,18 +2199,21 @@ class _EdgeOneMemorySession:
|
|
|
2031
2199
|
if not items:
|
|
2032
2200
|
return
|
|
2033
2201
|
|
|
2202
|
+
# Use bulk append for efficiency (1 meta update instead of N)
|
|
2203
|
+
messages_data = []
|
|
2034
2204
|
for item in items:
|
|
2035
2205
|
normalized = self._jsonable(item)
|
|
2036
2206
|
role = self._role_for_item(normalized)
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
metadata={
|
|
2207
|
+
messages_data.append({
|
|
2208
|
+
"role": role,
|
|
2209
|
+
"content": normalized,
|
|
2210
|
+
"metadata": {
|
|
2042
2211
|
**_SESSION_METADATA_MARKER,
|
|
2043
2212
|
"item_type": normalized.get("type") if isinstance(normalized, dict) else None,
|
|
2044
2213
|
},
|
|
2045
|
-
)
|
|
2214
|
+
})
|
|
2215
|
+
|
|
2216
|
+
await self.memory._bulk_append_messages(self.session_id, messages_data)
|
|
2046
2217
|
|
|
2047
2218
|
async def pop_item(self) -> Optional[dict]:
|
|
2048
2219
|
"""Remove and return the most recent item from the session."""
|
|
@@ -12,7 +12,7 @@ from dataclasses import dataclass
|
|
|
12
12
|
from typing import Any, AsyncIterator, Callable, Awaitable, Optional
|
|
13
13
|
from uuid import uuid4
|
|
14
14
|
|
|
15
|
-
from .context import AgentContext, AbortActiveRunResult, RequestInfo, SandboxRuntimeError, StreamResponse
|
|
15
|
+
from .context import AgentContext, AbortActiveRunResult, EoContext, RequestInfo, SandboxRuntimeError, StreamResponse
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
# LangGraph 通过抛出这些异常实现 graph 暂停 / 控制流跳转,并非业务错误。
|
|
@@ -238,10 +238,12 @@ class AgentsApi:
|
|
|
238
238
|
headers=_context.request.headers,
|
|
239
239
|
query=_context.request.query,
|
|
240
240
|
signal=_context.request.signal,
|
|
241
|
+
eo=_context.request.eo,
|
|
241
242
|
),
|
|
242
243
|
env=_context.env,
|
|
243
244
|
kv=self._store_resolver(route_path),
|
|
244
245
|
agents=self,
|
|
246
|
+
eo=_context.eo,
|
|
245
247
|
# active_run_id 不向 child 传递:child 是「当前 run 内部派生的子调用」,
|
|
246
248
|
# 沿用 parent 的 active_run_id 反而会让业务误以为有冲突。
|
|
247
249
|
active_run_id=None,
|
|
@@ -377,6 +379,7 @@ class AgentRuntime:
|
|
|
377
379
|
headers: dict,
|
|
378
380
|
query: dict,
|
|
379
381
|
body: dict,
|
|
382
|
+
eo: Optional[EoContext] = None,
|
|
380
383
|
) -> RuntimeResult:
|
|
381
384
|
# Route match — user handler takes priority
|
|
382
385
|
entry = self._registry.get(path)
|
|
@@ -406,18 +409,17 @@ class AgentRuntime:
|
|
|
406
409
|
return RuntimeResult(404, {"code": "AGENT_NOT_FOUND", "message": f"No handler for {path}"}, {})
|
|
407
410
|
|
|
408
411
|
# Parse IDs
|
|
409
|
-
# 与 Node runtime
|
|
410
|
-
#
|
|
411
|
-
# body 里的「目标 conversation_id」当成自己的 conversation,进而覆盖
|
|
412
|
-
# active_signals 里被取消方的注册,导致 ctx.utils.abortActiveRun 失效。
|
|
413
|
-
# (body 里的 conversation_id 应该由业务 handler 自行解析作为参数。)
|
|
412
|
+
# 与 Node runtime 对齐:conversation_id 必须从 header 提供,不自动生成。
|
|
413
|
+
# 不带 makers-conversation-id header 的请求直接返回 400,与 Node 行为一致。
|
|
414
414
|
header_conversation_id = _header(headers, "makers-conversation-id") or _header(headers, "conversation-id")
|
|
415
|
-
if header_conversation_id:
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
415
|
+
if not header_conversation_id:
|
|
416
|
+
_log(f"Agent error: {path} code=AGENT_CONVERSATION_ID_REQUIRED")
|
|
417
|
+
return RuntimeResult(400, {
|
|
418
|
+
"code": "AGENT_CONVERSATION_ID_REQUIRED",
|
|
419
|
+
"message": "conversationId is required. Pass makers-conversation-id header.",
|
|
420
|
+
}, {})
|
|
421
|
+
conversation_id = header_conversation_id
|
|
422
|
+
conversation_id_source = "makers-conversation-id"
|
|
421
423
|
run_id = str(uuid4())
|
|
422
424
|
start_time = time.monotonic()
|
|
423
425
|
|
|
@@ -442,15 +444,19 @@ class AgentRuntime:
|
|
|
442
444
|
"makers-run-id": run_id,
|
|
443
445
|
}
|
|
444
446
|
|
|
447
|
+
# Normalize eo
|
|
448
|
+
_eo = eo or EoContext()
|
|
449
|
+
|
|
445
450
|
# Build context
|
|
446
451
|
context = AgentContext(
|
|
447
452
|
conversation_id=conversation_id,
|
|
448
453
|
run_id=run_id,
|
|
449
454
|
parent_run_id=None,
|
|
450
|
-
request=RequestInfo(body=body, headers=headers, query=query, signal=cancel_event),
|
|
455
|
+
request=RequestInfo(body=body, headers=headers, query=query, signal=cancel_event, eo=_eo),
|
|
451
456
|
env=dict(os.environ),
|
|
452
457
|
kv=self._store_resolver(path),
|
|
453
458
|
agents=self._agents_api,
|
|
459
|
+
eo=_eo,
|
|
454
460
|
active_run_id=existing_active_run_id,
|
|
455
461
|
_store_blob=self._store_blob,
|
|
456
462
|
_tracer=_get_obs_tracer(),
|