coco-framework 0.1.0__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.
- coco/__init__.py +3 -0
- coco/agents/__init__.py +9 -0
- coco/agents/_subagent_transformer.py +307 -0
- coco/agents/factory.py +2164 -0
- coco/agents/middleware/__init__.py +100 -0
- coco/agents/middleware/_execution.py +385 -0
- coco/agents/middleware/_redaction.py +454 -0
- coco/agents/middleware/_retry.py +134 -0
- coco/agents/middleware/_trace_policy.py +61 -0
- coco/agents/middleware/context_editing.py +306 -0
- coco/agents/middleware/file_search.py +433 -0
- coco/agents/middleware/human_in_the_loop.py +680 -0
- coco/agents/middleware/internal_call_transformer.py +108 -0
- coco/agents/middleware/model_call_limit.py +267 -0
- coco/agents/middleware/model_fallback.py +417 -0
- coco/agents/middleware/model_retry.py +327 -0
- coco/agents/middleware/pii.py +878 -0
- coco/agents/middleware/provider_tool_search.py +307 -0
- coco/agents/middleware/shell_tool.py +949 -0
- coco/agents/middleware/summarization.py +910 -0
- coco/agents/middleware/todo.py +357 -0
- coco/agents/middleware/tool_call_limit.py +495 -0
- coco/agents/middleware/tool_emulator.py +251 -0
- coco/agents/middleware/tool_error.py +209 -0
- coco/agents/middleware/tool_retry.py +421 -0
- coco/agents/middleware/tool_selection.py +515 -0
- coco/agents/middleware/types.py +2201 -0
- coco/agents/structured_output.py +472 -0
- coco/chat_models/__init__.py +7 -0
- coco/chat_models/base.py +1082 -0
- coco/embeddings/__init__.py +18 -0
- coco/embeddings/base.py +279 -0
- coco/mcp/__init__.py +36 -0
- coco/mcp/adapter.py +245 -0
- coco/mcp/elicitation.py +394 -0
- coco/mcp/tools.py +307 -0
- coco/messages/__init__.py +73 -0
- coco/py.typed +0 -0
- coco/rate_limiters/__init__.py +13 -0
- coco/tools/__init__.py +22 -0
- coco/tools/tool_node.py +20 -0
- coco_anthropic/__init__.py +15 -0
- coco_anthropic/_client_utils.py +81 -0
- coco_anthropic/_compat.py +228 -0
- coco_anthropic/_sdk_compat.py +124 -0
- coco_anthropic/_version.py +3 -0
- coco_anthropic/chat_models.py +3349 -0
- coco_anthropic/data/__init__.py +1 -0
- coco_anthropic/data/_profiles.py +491 -0
- coco_anthropic/data/profile_augmentations.toml +69 -0
- coco_anthropic/experimental.py +140 -0
- coco_anthropic/llms.py +428 -0
- coco_anthropic/middleware/__init__.py +25 -0
- coco_anthropic/middleware/anthropic_tools.py +1197 -0
- coco_anthropic/middleware/bash.py +84 -0
- coco_anthropic/middleware/file_search.py +360 -0
- coco_anthropic/middleware/prompt_caching.py +276 -0
- coco_anthropic/output_parsers.py +93 -0
- coco_anthropic/py.typed +0 -0
- coco_core/__init__.py +20 -0
- coco_core/_api/__init__.py +87 -0
- coco_core/_api/beta_decorator.py +266 -0
- coco_core/_api/deprecation.py +637 -0
- coco_core/_api/internal.py +23 -0
- coco_core/_api/path.py +50 -0
- coco_core/_import_utils.py +41 -0
- coco_core/_protocol.py +625 -0
- coco_core/_security/__init__.py +36 -0
- coco_core/_security/_exceptions.py +9 -0
- coco_core/_security/_policy.py +313 -0
- coco_core/_security/_ssrf_protection.py +155 -0
- coco_core/_security/_transport.py +254 -0
- coco_core/agents.py +258 -0
- coco_core/caches.py +272 -0
- coco_core/callbacks/__init__.py +132 -0
- coco_core/callbacks/base.py +1229 -0
- coco_core/callbacks/file.py +266 -0
- coco_core/callbacks/manager.py +2826 -0
- coco_core/callbacks/stdout.py +123 -0
- coco_core/callbacks/streaming_stdout.py +154 -0
- coco_core/callbacks/usage.py +124 -0
- coco_core/chat_history.py +267 -0
- coco_core/chat_loaders.py +26 -0
- coco_core/chat_sessions.py +19 -0
- coco_core/cross_encoders.py +18 -0
- coco_core/document_loaders/__init__.py +36 -0
- coco_core/document_loaders/base.py +155 -0
- coco_core/document_loaders/blob_loaders.py +38 -0
- coco_core/documents/__init__.py +55 -0
- coco_core/documents/base.py +347 -0
- coco_core/documents/compressor.py +74 -0
- coco_core/documents/transformers.py +79 -0
- coco_core/embeddings/__init__.py +31 -0
- coco_core/embeddings/embeddings.py +78 -0
- coco_core/embeddings/fake.py +129 -0
- coco_core/env.py +22 -0
- coco_core/example_selectors/__init__.py +47 -0
- coco_core/example_selectors/base.py +60 -0
- coco_core/example_selectors/length_based.py +131 -0
- coco_core/example_selectors/semantic_similarity.py +366 -0
- coco_core/exceptions.py +166 -0
- coco_core/globals.py +72 -0
- coco_core/indexing/__init__.py +53 -0
- coco_core/indexing/api.py +954 -0
- coco_core/indexing/base.py +661 -0
- coco_core/indexing/in_memory.py +104 -0
- coco_core/language_models/__init__.py +116 -0
- coco_core/language_models/_compat_bridge.py +844 -0
- coco_core/language_models/_utils.py +343 -0
- coco_core/language_models/base.py +500 -0
- coco_core/language_models/chat_model_stream.py +1525 -0
- coco_core/language_models/chat_models.py +2748 -0
- coco_core/language_models/fake.py +137 -0
- coco_core/language_models/fake_chat_models.py +396 -0
- coco_core/language_models/llms.py +1569 -0
- coco_core/language_models/model_profile.py +183 -0
- coco_core/load/__init__.py +44 -0
- coco_core/load/_validation.py +191 -0
- coco_core/load/dump.py +120 -0
- coco_core/load/load.py +828 -0
- coco_core/load/mapping.py +1097 -0
- coco_core/load/serializable.py +406 -0
- coco_core/messages/__init__.py +198 -0
- coco_core/messages/ai.py +854 -0
- coco_core/messages/base.py +518 -0
- coco_core/messages/block_translators/__init__.py +112 -0
- coco_core/messages/block_translators/anthropic.py +534 -0
- coco_core/messages/block_translators/bedrock.py +108 -0
- coco_core/messages/block_translators/bedrock_converse.py +329 -0
- coco_core/messages/block_translators/coco_v0.py +303 -0
- coco_core/messages/block_translators/google_genai.py +610 -0
- coco_core/messages/block_translators/google_vertexai.py +21 -0
- coco_core/messages/block_translators/groq.py +157 -0
- coco_core/messages/block_translators/openai.py +1089 -0
- coco_core/messages/chat.py +64 -0
- coco_core/messages/content.py +1488 -0
- coco_core/messages/function.py +62 -0
- coco_core/messages/human.py +70 -0
- coco_core/messages/modifier.py +33 -0
- coco_core/messages/system.py +70 -0
- coco_core/messages/tool.py +418 -0
- coco_core/messages/utils.py +2442 -0
- coco_core/output_parsers/__init__.py +101 -0
- coco_core/output_parsers/base.py +361 -0
- coco_core/output_parsers/format_instructions.py +16 -0
- coco_core/output_parsers/json.py +139 -0
- coco_core/output_parsers/list.py +249 -0
- coco_core/output_parsers/openai_functions.py +324 -0
- coco_core/output_parsers/openai_tools.py +383 -0
- coco_core/output_parsers/pydantic.py +141 -0
- coco_core/output_parsers/string.py +63 -0
- coco_core/output_parsers/transform.py +175 -0
- coco_core/output_parsers/xml.py +301 -0
- coco_core/outputs/__init__.py +66 -0
- coco_core/outputs/chat_generation.py +147 -0
- coco_core/outputs/chat_result.py +39 -0
- coco_core/outputs/generation.py +80 -0
- coco_core/outputs/llm_result.py +112 -0
- coco_core/outputs/run_info.py +22 -0
- coco_core/prompt_values.py +161 -0
- coco_core/prompts/__init__.py +101 -0
- coco_core/prompts/base.py +508 -0
- coco_core/prompts/chat.py +1495 -0
- coco_core/prompts/dict.py +177 -0
- coco_core/prompts/few_shot.py +483 -0
- coco_core/prompts/few_shot_with_templates.py +239 -0
- coco_core/prompts/image.py +177 -0
- coco_core/prompts/loading.py +296 -0
- coco_core/prompts/message.py +97 -0
- coco_core/prompts/prompt.py +312 -0
- coco_core/prompts/string.py +399 -0
- coco_core/prompts/structured.py +214 -0
- coco_core/py.typed +0 -0
- coco_core/rate_limiters.py +254 -0
- coco_core/retrievers.py +328 -0
- coco_core/runnables/__init__.py +136 -0
- coco_core/runnables/base.py +6725 -0
- coco_core/runnables/branch.py +457 -0
- coco_core/runnables/config.py +712 -0
- coco_core/runnables/configurable.py +710 -0
- coco_core/runnables/fallbacks.py +663 -0
- coco_core/runnables/graph.py +741 -0
- coco_core/runnables/graph_ascii.py +366 -0
- coco_core/runnables/graph_mermaid.py +503 -0
- coco_core/runnables/graph_png.py +215 -0
- coco_core/runnables/history.py +631 -0
- coco_core/runnables/passthrough.py +849 -0
- coco_core/runnables/retry.py +379 -0
- coco_core/runnables/router.py +239 -0
- coco_core/runnables/schema.py +188 -0
- coco_core/runnables/utils.py +798 -0
- coco_core/stores.py +291 -0
- coco_core/structured_query.py +203 -0
- coco_core/sys_info.py +154 -0
- coco_core/tools/__init__.py +95 -0
- coco_core/tools/base.py +1948 -0
- coco_core/tools/convert.py +497 -0
- coco_core/tools/render.py +67 -0
- coco_core/tools/retriever.py +94 -0
- coco_core/tools/simple.py +208 -0
- coco_core/tools/structured.py +322 -0
- coco_core/tracers/__init__.py +47 -0
- coco_core/tracers/_compat.py +95 -0
- coco_core/tracers/_context.py +337 -0
- coco_core/tracers/_streaming.py +53 -0
- coco_core/tracers/base.py +955 -0
- coco_core/tracers/coco.py +279 -0
- coco_core/tracers/context.py +205 -0
- coco_core/tracers/core.py +766 -0
- coco_core/tracers/event_stream.py +1101 -0
- coco_core/tracers/log_stream.py +765 -0
- coco_core/tracers/memory_stream.py +200 -0
- coco_core/tracers/root_listeners.py +130 -0
- coco_core/tracers/run_collector.py +40 -0
- coco_core/tracers/schemas.py +14 -0
- coco_core/tracers/stdout.py +205 -0
- coco_core/utils/__init__.py +111 -0
- coco_core/utils/_gateway.py +274 -0
- coco_core/utils/_merge.py +218 -0
- coco_core/utils/aiter.py +357 -0
- coco_core/utils/env.py +86 -0
- coco_core/utils/formatting.py +80 -0
- coco_core/utils/function_calling.py +869 -0
- coco_core/utils/html.py +132 -0
- coco_core/utils/image.py +15 -0
- coco_core/utils/input.py +82 -0
- coco_core/utils/interactive_env.py +12 -0
- coco_core/utils/iter.py +229 -0
- coco_core/utils/json.py +230 -0
- coco_core/utils/json_schema.py +273 -0
- coco_core/utils/mustache.py +706 -0
- coco_core/utils/pydantic.py +631 -0
- coco_core/utils/strings.py +69 -0
- coco_core/utils/usage.py +61 -0
- coco_core/utils/utils.py +528 -0
- coco_core/utils/uuid.py +57 -0
- coco_core/vectorstores/__init__.py +53 -0
- coco_core/vectorstores/base.py +1111 -0
- coco_core/vectorstores/in_memory.py +546 -0
- coco_core/vectorstores/utils.py +163 -0
- coco_core/version.py +3 -0
- coco_framework-0.1.0.dist-info/METADATA +184 -0
- coco_framework-0.1.0.dist-info/RECORD +400 -0
- coco_framework-0.1.0.dist-info/WHEEL +4 -0
- coco_framework-0.1.0.dist-info/licenses/LICENSE +24 -0
- coco_framework-0.1.0.dist-info/licenses/NOTICE +15 -0
- coco_graph/_internal/__init__.py +4 -0
- coco_graph/_internal/_cache.py +31 -0
- coco_graph/_internal/_config.py +474 -0
- coco_graph/_internal/_constants.py +140 -0
- coco_graph/_internal/_fields.py +213 -0
- coco_graph/_internal/_future.py +220 -0
- coco_graph/_internal/_pydantic.py +275 -0
- coco_graph/_internal/_queue.py +124 -0
- coco_graph/_internal/_replay.py +90 -0
- coco_graph/_internal/_retry.py +29 -0
- coco_graph/_internal/_runnable.py +969 -0
- coco_graph/_internal/_scratchpad.py +19 -0
- coco_graph/_internal/_serde.py +253 -0
- coco_graph/_internal/_timeout.py +25 -0
- coco_graph/_internal/_typing.py +54 -0
- coco_graph/cache/base/__init__.py +48 -0
- coco_graph/cache/base/py.typed +0 -0
- coco_graph/cache/memory/__init__.py +73 -0
- coco_graph/cache/redis/__init__.py +144 -0
- coco_graph/cache/sqlite/__init__.py +120 -0
- coco_graph/callbacks.py +394 -0
- coco_graph/channels/__init__.py +29 -0
- coco_graph/channels/any_value.py +72 -0
- coco_graph/channels/base.py +121 -0
- coco_graph/channels/binop.py +155 -0
- coco_graph/channels/delta.py +202 -0
- coco_graph/channels/ephemeral_value.py +79 -0
- coco_graph/channels/last_value.py +151 -0
- coco_graph/channels/named_barrier_value.py +167 -0
- coco_graph/channels/topic.py +94 -0
- coco_graph/channels/untracked_value.py +73 -0
- coco_graph/checkpoint/base/__init__.py +861 -0
- coco_graph/checkpoint/base/id.py +109 -0
- coco_graph/checkpoint/base/py.typed +0 -0
- coco_graph/checkpoint/memory/__init__.py +698 -0
- coco_graph/checkpoint/memory/py.typed +0 -0
- coco_graph/checkpoint/postgres/__init__.py +602 -0
- coco_graph/checkpoint/postgres/_ainternal.py +23 -0
- coco_graph/checkpoint/postgres/_internal.py +21 -0
- coco_graph/checkpoint/postgres/aio.py +691 -0
- coco_graph/checkpoint/postgres/base.py +666 -0
- coco_graph/checkpoint/postgres/py.typed +0 -0
- coco_graph/checkpoint/postgres/shallow.py +967 -0
- coco_graph/checkpoint/serde/__init__.py +0 -0
- coco_graph/checkpoint/serde/_msgpack.py +97 -0
- coco_graph/checkpoint/serde/base.py +64 -0
- coco_graph/checkpoint/serde/encrypted.py +80 -0
- coco_graph/checkpoint/serde/event_hooks.py +52 -0
- coco_graph/checkpoint/serde/jsonplus.py +883 -0
- coco_graph/checkpoint/serde/py.typed +0 -0
- coco_graph/checkpoint/serde/types.py +68 -0
- coco_graph/checkpoint/sqlite/__init__.py +645 -0
- coco_graph/checkpoint/sqlite/_delta.py +172 -0
- coco_graph/checkpoint/sqlite/aio.py +738 -0
- coco_graph/checkpoint/sqlite/py.typed +0 -0
- coco_graph/checkpoint/sqlite/utils.py +116 -0
- coco_graph/config.py +196 -0
- coco_graph/constants.py +64 -0
- coco_graph/errors.py +241 -0
- coco_graph/func/__init__.py +620 -0
- coco_graph/graph/__init__.py +12 -0
- coco_graph/graph/_branch.py +225 -0
- coco_graph/graph/_node.py +103 -0
- coco_graph/graph/message.py +437 -0
- coco_graph/graph/state.py +1978 -0
- coco_graph/graph/ui.py +227 -0
- coco_graph/managed/__init__.py +3 -0
- coco_graph/managed/base.py +31 -0
- coco_graph/managed/is_last_step.py +24 -0
- coco_graph/prebuilt/__init__.py +23 -0
- coco_graph/prebuilt/_tool_call_stream.py +117 -0
- coco_graph/prebuilt/_tool_call_transformer.py +165 -0
- coco_graph/prebuilt/chat_agent_executor.py +1015 -0
- coco_graph/prebuilt/interrupt.py +105 -0
- coco_graph/prebuilt/py.typed +0 -0
- coco_graph/prebuilt/tool_node.py +2030 -0
- coco_graph/prebuilt/tool_validator.py +221 -0
- coco_graph/pregel/__init__.py +3 -0
- coco_graph/pregel/_algo.py +1460 -0
- coco_graph/pregel/_call.py +298 -0
- coco_graph/pregel/_checkpoint.py +331 -0
- coco_graph/pregel/_config.py +0 -0
- coco_graph/pregel/_draw.py +294 -0
- coco_graph/pregel/_executor.py +223 -0
- coco_graph/pregel/_io.py +174 -0
- coco_graph/pregel/_log.py +3 -0
- coco_graph/pregel/_loop.py +1988 -0
- coco_graph/pregel/_messages.py +461 -0
- coco_graph/pregel/_read.py +312 -0
- coco_graph/pregel/_retry.py +854 -0
- coco_graph/pregel/_runner.py +941 -0
- coco_graph/pregel/_tools.py +268 -0
- coco_graph/pregel/_utils.py +221 -0
- coco_graph/pregel/_validate.py +120 -0
- coco_graph/pregel/_write.py +192 -0
- coco_graph/pregel/debug.py +302 -0
- coco_graph/pregel/main.py +4364 -0
- coco_graph/pregel/protocol.py +288 -0
- coco_graph/pregel/types.py +38 -0
- coco_graph/py.typed +0 -0
- coco_graph/runtime.py +350 -0
- coco_graph/store/base/__init__.py +1322 -0
- coco_graph/store/base/batch.py +371 -0
- coco_graph/store/base/embed.py +436 -0
- coco_graph/store/base/py.typed +0 -0
- coco_graph/store/memory/__init__.py +592 -0
- coco_graph/store/memory/py.typed +0 -0
- coco_graph/store/postgres/__init__.py +4 -0
- coco_graph/store/postgres/aio.py +592 -0
- coco_graph/store/postgres/base.py +1480 -0
- coco_graph/store/postgres/py.typed +0 -0
- coco_graph/store/sqlite/__init__.py +4 -0
- coco_graph/store/sqlite/aio.py +632 -0
- coco_graph/store/sqlite/base.py +1514 -0
- coco_graph/stream/__init__.py +45 -0
- coco_graph/stream/_convert.py +32 -0
- coco_graph/stream/_mux.py +523 -0
- coco_graph/stream/_types.py +330 -0
- coco_graph/stream/run_stream.py +735 -0
- coco_graph/stream/stream_channel.py +341 -0
- coco_graph/stream/transformers.py +1039 -0
- coco_graph/types.py +1079 -0
- coco_graph/typing.py +48 -0
- coco_graph/utils/__init__.py +1 -0
- coco_graph/utils/config.py +4 -0
- coco_graph/utils/runnable.py +3 -0
- coco_graph/version.py +13 -0
- coco_graph/warnings.py +69 -0
- coco_openai/__init__.py +20 -0
- coco_openai/_compat.py +45 -0
- coco_openai/_version.py +3 -0
- coco_openai/chat_models/__init__.py +6 -0
- coco_openai/chat_models/_client_utils.py +684 -0
- coco_openai/chat_models/_compat.py +525 -0
- coco_openai/chat_models/azure.py +1227 -0
- coco_openai/chat_models/base.py +5654 -0
- coco_openai/chat_models/codex.py +557 -0
- coco_openai/chatgpt_oauth.py +1063 -0
- coco_openai/data/__init__.py +1 -0
- coco_openai/data/_profiles.py +1655 -0
- coco_openai/data/profile_augmentations.toml +127 -0
- coco_openai/embeddings/__init__.py +6 -0
- coco_openai/embeddings/azure.py +240 -0
- coco_openai/embeddings/base.py +820 -0
- coco_openai/llms/__init__.py +6 -0
- coco_openai/llms/azure.py +249 -0
- coco_openai/llms/base.py +919 -0
- coco_openai/middleware/__init__.py +11 -0
- coco_openai/middleware/openai_moderation.py +484 -0
- coco_openai/output_parsers/__init__.py +9 -0
- coco_openai/output_parsers/tools.py +9 -0
- coco_openai/py.typed +0 -0
- coco_openai/tools/__init__.py +5 -0
- coco_openai/tools/custom_tool.py +111 -0
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import AsyncIterator, Collection, Iterator, Mapping, Sequence
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Generic,
|
|
9
|
+
Literal,
|
|
10
|
+
NamedTuple,
|
|
11
|
+
TypedDict,
|
|
12
|
+
TypeVar,
|
|
13
|
+
cast,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from coco_core.runnables import RunnableConfig
|
|
17
|
+
from typing_extensions import NotRequired
|
|
18
|
+
|
|
19
|
+
from coco_graph.checkpoint.base.id import uuid6
|
|
20
|
+
from coco_graph.checkpoint.serde.base import SerializerProtocol, maybe_add_typed_methods
|
|
21
|
+
from coco_graph.checkpoint.serde.encrypted import EncryptedSerializer
|
|
22
|
+
from coco_graph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
|
23
|
+
from coco_graph.checkpoint.serde.types import (
|
|
24
|
+
ERROR,
|
|
25
|
+
INTERRUPT,
|
|
26
|
+
RESUME,
|
|
27
|
+
SCHEDULED,
|
|
28
|
+
ChannelProtocol,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
V = TypeVar("V", int, float, str)
|
|
32
|
+
PendingWrite = tuple[str, str, Any]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Marked as total=False to allow for future expansion.
|
|
39
|
+
class CheckpointMetadata(TypedDict, total=False):
|
|
40
|
+
"""Metadata associated with a checkpoint."""
|
|
41
|
+
|
|
42
|
+
source: Literal["input", "loop", "update", "fork"]
|
|
43
|
+
"""The source of the checkpoint.
|
|
44
|
+
|
|
45
|
+
- `"input"`: The checkpoint was created from an input to invoke/stream/batch.
|
|
46
|
+
- `"loop"`: The checkpoint was created from inside the pregel loop.
|
|
47
|
+
- `"update"`: The checkpoint was created from a manual state update.
|
|
48
|
+
- `"fork"`: The checkpoint was created as a copy of another checkpoint.
|
|
49
|
+
"""
|
|
50
|
+
step: int
|
|
51
|
+
"""The step number of the checkpoint.
|
|
52
|
+
|
|
53
|
+
`-1` for the first `"input"` checkpoint.
|
|
54
|
+
`0` for the first `"loop"` checkpoint.
|
|
55
|
+
`...` for the `nth` checkpoint afterwards.
|
|
56
|
+
"""
|
|
57
|
+
parents: dict[str, str]
|
|
58
|
+
"""The IDs of the parent checkpoints.
|
|
59
|
+
|
|
60
|
+
Mapping from checkpoint namespace to checkpoint ID.
|
|
61
|
+
"""
|
|
62
|
+
run_id: str
|
|
63
|
+
"""The ID of the run that created this checkpoint."""
|
|
64
|
+
counters_since_delta_snapshot: dict[str, tuple[int, int]]
|
|
65
|
+
"""Per-channel counters since the last `_DeltaSnapshot` was written.
|
|
66
|
+
|
|
67
|
+
!!! warning "Beta"
|
|
68
|
+
|
|
69
|
+
This metadata field backs `DeltaChannel` (beta). The key name and
|
|
70
|
+
contents may change while the delta-channel design stabilizes.
|
|
71
|
+
|
|
72
|
+
Maps channel name -> `(updates, supersteps)`:
|
|
73
|
+
|
|
74
|
+
- index 0 (`updates`): number of supersteps that wrote to this channel
|
|
75
|
+
since its last snapshot blob.
|
|
76
|
+
- index 1 (`supersteps`): total supersteps elapsed since this channel's
|
|
77
|
+
last snapshot, regardless of whether the channel was written.
|
|
78
|
+
|
|
79
|
+
A snapshot fires when EITHER `updates >= ch.snapshot_frequency` OR
|
|
80
|
+
`supersteps >= DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT` (system-wide bound,
|
|
81
|
+
default 5000, env `COCO_GRAPH_DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT`).
|
|
82
|
+
The supersteps bound prevents unbounded ancestor walks on threads where
|
|
83
|
+
a delta channel exists but is no longer being updated.
|
|
84
|
+
|
|
85
|
+
Absent on threads that don't use delta channels. Persisted as a
|
|
86
|
+
2-element list in JSON (no native tuple).
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
ChannelVersions = dict[str, str | int | float]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Checkpoint(TypedDict):
|
|
94
|
+
"""State snapshot at a given point in time."""
|
|
95
|
+
|
|
96
|
+
v: int
|
|
97
|
+
"""The version of the checkpoint format. Currently `1`."""
|
|
98
|
+
id: str
|
|
99
|
+
"""The ID of the checkpoint.
|
|
100
|
+
|
|
101
|
+
This is both unique and monotonically increasing, so can be used for sorting
|
|
102
|
+
checkpoints from first to last."""
|
|
103
|
+
ts: str
|
|
104
|
+
"""The timestamp of the checkpoint in ISO 8601 format."""
|
|
105
|
+
channel_values: dict[str, Any]
|
|
106
|
+
"""The values of the channels at the time of the checkpoint.
|
|
107
|
+
|
|
108
|
+
Mapping from channel name to deserialized channel snapshot value.
|
|
109
|
+
"""
|
|
110
|
+
channel_versions: ChannelVersions
|
|
111
|
+
"""The versions of the channels at the time of the checkpoint.
|
|
112
|
+
|
|
113
|
+
The keys are channel names and the values are monotonically increasing
|
|
114
|
+
version strings for each channel.
|
|
115
|
+
"""
|
|
116
|
+
versions_seen: dict[str, ChannelVersions]
|
|
117
|
+
"""Map from node ID to map from channel name to version seen.
|
|
118
|
+
|
|
119
|
+
This keeps track of the versions of the channels that each node has seen.
|
|
120
|
+
Used to determine which nodes to execute next.
|
|
121
|
+
"""
|
|
122
|
+
updated_channels: list[str] | None
|
|
123
|
+
"""The channels that were updated in this checkpoint.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint:
|
|
128
|
+
return Checkpoint(
|
|
129
|
+
v=checkpoint["v"],
|
|
130
|
+
ts=checkpoint["ts"],
|
|
131
|
+
id=checkpoint["id"],
|
|
132
|
+
channel_values=checkpoint["channel_values"].copy(),
|
|
133
|
+
channel_versions=checkpoint["channel_versions"].copy(),
|
|
134
|
+
versions_seen={k: v.copy() for k, v in checkpoint["versions_seen"].items()},
|
|
135
|
+
pending_sends=checkpoint.get("pending_sends", []).copy(),
|
|
136
|
+
updated_channels=checkpoint.get("updated_channels", None),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class CheckpointTuple(NamedTuple):
|
|
141
|
+
"""A tuple containing a checkpoint and its associated data."""
|
|
142
|
+
|
|
143
|
+
config: RunnableConfig
|
|
144
|
+
checkpoint: Checkpoint
|
|
145
|
+
metadata: CheckpointMetadata
|
|
146
|
+
parent_config: RunnableConfig | None = None
|
|
147
|
+
pending_writes: list[PendingWrite] | None = None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class DeltaChannelHistory(TypedDict):
|
|
151
|
+
"""Per-channel result entry from `BaseCheckpointSaver.get_delta_channel_history`.
|
|
152
|
+
|
|
153
|
+
!!! warning "Beta"
|
|
154
|
+
|
|
155
|
+
Part of the `DeltaChannel` support surface; in beta. Field names and
|
|
156
|
+
semantics may change.
|
|
157
|
+
|
|
158
|
+
Storage-level view of what one channel contributed across the ancestor
|
|
159
|
+
chain of a target checkpoint:
|
|
160
|
+
|
|
161
|
+
* `writes` — on-path deltas oldest→newest as `PendingWrite` tuples.
|
|
162
|
+
Always present; possibly empty. Already filtered to one channel.
|
|
163
|
+
Writes stored at the target checkpoint itself are pending for the
|
|
164
|
+
next super-step and are excluded.
|
|
165
|
+
* `seed` — the stored value at the nearest ancestor whose
|
|
166
|
+
`channel_values[ch]` is populated. Omitted if the walk reached the
|
|
167
|
+
root without finding any stored value (consumer treats absence as
|
|
168
|
+
"start empty"). Typically a `_DeltaSnapshot` for delta channels with
|
|
169
|
+
finite snapshot frequency, or a plain value for threads migrated
|
|
170
|
+
from a pre-delta channel type.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
writes: list[PendingWrite]
|
|
174
|
+
seed: NotRequired[Any]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class BaseCheckpointSaver(Generic[V]):
|
|
178
|
+
"""Base class for creating a graph checkpointer.
|
|
179
|
+
|
|
180
|
+
Checkpointers allow CocoGraph agents to persist their state
|
|
181
|
+
within and across multiple interactions.
|
|
182
|
+
|
|
183
|
+
When a checkpointer is configured, you should pass a `thread_id` in the config when
|
|
184
|
+
invoking the graph:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
config = {"configurable": {"thread_id": "my-thread"}}
|
|
188
|
+
graph.invoke(inputs, config)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The `thread_id` is the primary key used to store and retrieve checkpoints. Without
|
|
192
|
+
it, the checkpointer cannot save state, resume from interrupts, or enable
|
|
193
|
+
time-travel debugging.
|
|
194
|
+
|
|
195
|
+
How you choose ``thread_id`` depends on your use case:
|
|
196
|
+
|
|
197
|
+
- **Single-shot workflows**: Use a unique ID (e.g., uuid4) for each run when
|
|
198
|
+
executions are independent.
|
|
199
|
+
- **Conversational memory**: Reuse the same `thread_id` across invocations
|
|
200
|
+
to accumulate state (e.g., chat history) within a conversation.
|
|
201
|
+
|
|
202
|
+
Attributes:
|
|
203
|
+
serde (SerializerProtocol): Serializer for encoding/decoding checkpoints.
|
|
204
|
+
|
|
205
|
+
Note:
|
|
206
|
+
When creating a custom checkpoint saver, consider implementing async
|
|
207
|
+
versions to avoid blocking the main thread.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
serde: SerializerProtocol = JsonPlusSerializer()
|
|
211
|
+
|
|
212
|
+
def __init__(
|
|
213
|
+
self,
|
|
214
|
+
*,
|
|
215
|
+
serde: SerializerProtocol | None = None,
|
|
216
|
+
) -> None:
|
|
217
|
+
self.serde = maybe_add_typed_methods(serde or self.serde)
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def config_specs(self) -> list:
|
|
221
|
+
"""Define the configuration options for the checkpoint saver.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
list: List of configuration field specs.
|
|
225
|
+
"""
|
|
226
|
+
return []
|
|
227
|
+
|
|
228
|
+
def get(self, config: RunnableConfig) -> Checkpoint | None:
|
|
229
|
+
"""Fetch a checkpoint using the given configuration.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
config: Configuration specifying which checkpoint to retrieve.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
The requested checkpoint, or `None` if not found.
|
|
236
|
+
"""
|
|
237
|
+
if value := self.get_tuple(config):
|
|
238
|
+
return value.checkpoint
|
|
239
|
+
|
|
240
|
+
def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
|
241
|
+
"""Fetch a checkpoint tuple using the given configuration.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
config: Configuration specifying which checkpoint to retrieve.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
The requested checkpoint tuple, or `None` if not found.
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
NotImplementedError: Implement this method in your custom checkpoint saver.
|
|
251
|
+
"""
|
|
252
|
+
raise NotImplementedError
|
|
253
|
+
|
|
254
|
+
def list(
|
|
255
|
+
self,
|
|
256
|
+
config: RunnableConfig | None,
|
|
257
|
+
*,
|
|
258
|
+
filter: dict[str, Any] | None = None,
|
|
259
|
+
before: RunnableConfig | None = None,
|
|
260
|
+
limit: int | None = None,
|
|
261
|
+
) -> Iterator[CheckpointTuple]:
|
|
262
|
+
"""List checkpoints that match the given criteria.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
config: Base configuration for filtering checkpoints.
|
|
266
|
+
filter: Additional filtering criteria.
|
|
267
|
+
before: List checkpoints created before this configuration.
|
|
268
|
+
limit: Maximum number of checkpoints to return.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Iterator of matching checkpoint tuples.
|
|
272
|
+
|
|
273
|
+
Raises:
|
|
274
|
+
NotImplementedError: Implement this method in your custom checkpoint saver.
|
|
275
|
+
"""
|
|
276
|
+
raise NotImplementedError
|
|
277
|
+
|
|
278
|
+
def put(
|
|
279
|
+
self,
|
|
280
|
+
config: RunnableConfig,
|
|
281
|
+
checkpoint: Checkpoint,
|
|
282
|
+
metadata: CheckpointMetadata,
|
|
283
|
+
new_versions: ChannelVersions,
|
|
284
|
+
) -> RunnableConfig:
|
|
285
|
+
"""Store a checkpoint with its configuration and metadata.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
config: Configuration for the checkpoint.
|
|
289
|
+
checkpoint: The checkpoint to store.
|
|
290
|
+
metadata: Additional metadata for the checkpoint.
|
|
291
|
+
new_versions: New channel versions as of this write.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
RunnableConfig: Updated configuration after storing the checkpoint.
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
NotImplementedError: Implement this method in your custom checkpoint saver.
|
|
298
|
+
"""
|
|
299
|
+
raise NotImplementedError
|
|
300
|
+
|
|
301
|
+
def put_writes(
|
|
302
|
+
self,
|
|
303
|
+
config: RunnableConfig,
|
|
304
|
+
writes: Sequence[tuple[str, Any]],
|
|
305
|
+
task_id: str,
|
|
306
|
+
task_path: str = "",
|
|
307
|
+
) -> None:
|
|
308
|
+
"""Store intermediate writes linked to a checkpoint.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
config: Configuration of the related checkpoint.
|
|
312
|
+
writes: List of writes to store.
|
|
313
|
+
task_id: Identifier for the task creating the writes.
|
|
314
|
+
task_path: Path of the task creating the writes.
|
|
315
|
+
|
|
316
|
+
Raises:
|
|
317
|
+
NotImplementedError: Implement this method in your custom checkpoint saver.
|
|
318
|
+
"""
|
|
319
|
+
raise NotImplementedError
|
|
320
|
+
|
|
321
|
+
def delete_thread(
|
|
322
|
+
self,
|
|
323
|
+
thread_id: str,
|
|
324
|
+
) -> None:
|
|
325
|
+
"""Delete all checkpoints and writes associated with a specific thread ID.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
thread_id: The thread ID whose checkpoints should be deleted.
|
|
329
|
+
"""
|
|
330
|
+
raise NotImplementedError
|
|
331
|
+
|
|
332
|
+
def delete_for_runs(
|
|
333
|
+
self,
|
|
334
|
+
run_ids: Sequence[str],
|
|
335
|
+
) -> None:
|
|
336
|
+
"""Delete all checkpoints and writes associated with the given run IDs.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
run_ids: The run IDs whose checkpoints should be deleted.
|
|
340
|
+
|
|
341
|
+
!!! warning "DeltaChannel"
|
|
342
|
+
|
|
343
|
+
Deleting a run that produced ancestor `checkpoint_writes` — or
|
|
344
|
+
the only `_DeltaSnapshot` blob — for a still-live thread will
|
|
345
|
+
break reconstruction of any `DeltaChannel` whose history
|
|
346
|
+
depended on those rows. See the `DeltaChannel` note on `prune`
|
|
347
|
+
for safe-recovery strategies.
|
|
348
|
+
"""
|
|
349
|
+
raise NotImplementedError
|
|
350
|
+
|
|
351
|
+
def copy_thread(
|
|
352
|
+
self,
|
|
353
|
+
source_thread_id: str,
|
|
354
|
+
target_thread_id: str,
|
|
355
|
+
) -> None:
|
|
356
|
+
"""Copy all checkpoints and writes from one thread to another.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
source_thread_id: The thread ID to copy from.
|
|
360
|
+
target_thread_id: The thread ID to copy to.
|
|
361
|
+
|
|
362
|
+
!!! warning "DeltaChannel"
|
|
363
|
+
|
|
364
|
+
Implementations must copy the **complete** parent chain (all
|
|
365
|
+
ancestor checkpoints and their `checkpoint_writes`) — copying
|
|
366
|
+
only the head checkpoint will leave the target thread with
|
|
367
|
+
`DeltaChannel` state that cannot be reconstructed (no path back
|
|
368
|
+
to a `_DeltaSnapshot` ancestor). Equivalently, the copy must
|
|
369
|
+
include enough ancestors that every `DeltaChannel`-backed key
|
|
370
|
+
has either a `_DeltaSnapshot` in `channel_values` somewhere in
|
|
371
|
+
the chain, or a complete write history back to the chain root.
|
|
372
|
+
"""
|
|
373
|
+
raise NotImplementedError
|
|
374
|
+
|
|
375
|
+
def prune(
|
|
376
|
+
self,
|
|
377
|
+
thread_ids: Sequence[str],
|
|
378
|
+
*,
|
|
379
|
+
strategy: str = "keep_latest",
|
|
380
|
+
) -> None:
|
|
381
|
+
"""Prune checkpoints for the given threads.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
thread_ids: The thread IDs to prune.
|
|
385
|
+
strategy: The pruning strategy. `"keep_latest"` retains only the most
|
|
386
|
+
recent checkpoint per namespace. `"delete"` removes all checkpoints.
|
|
387
|
+
|
|
388
|
+
!!! warning "DeltaChannel"
|
|
389
|
+
|
|
390
|
+
Custom implementations must be `DeltaChannel`-aware. `DeltaChannel`
|
|
391
|
+
stores only a sentinel in `channel_values` for non-snapshot steps;
|
|
392
|
+
reconstruction walks the parent chain via
|
|
393
|
+
`get_delta_channel_history`, accumulating rows from
|
|
394
|
+
`checkpoint_writes` until it reaches an ancestor whose
|
|
395
|
+
`channel_values` contains a `_DeltaSnapshot` blob (written every
|
|
396
|
+
`snapshot_frequency` updates).
|
|
397
|
+
|
|
398
|
+
A naive `"keep_latest"` that drops intermediate checkpoints and
|
|
399
|
+
their writes can sever that chain: the surviving "latest"
|
|
400
|
+
checkpoint is rarely a snapshot point itself, so its delta
|
|
401
|
+
channels would silently reconstruct as empty (no error raised —
|
|
402
|
+
`get_delta_channel_history` simply returns no `seed`). Safe
|
|
403
|
+
options when the graph uses `DeltaChannel`:
|
|
404
|
+
|
|
405
|
+
* Walk back from each kept checkpoint and preserve every
|
|
406
|
+
ancestor (plus its `checkpoint_writes`) up to the nearest one
|
|
407
|
+
whose `channel_values` already contains a `_DeltaSnapshot` for
|
|
408
|
+
every `DeltaChannel`-backed key.
|
|
409
|
+
* Force a fresh snapshot on the kept checkpoint before deleting
|
|
410
|
+
ancestors — rewrite `channel_values[k] = _DeltaSnapshot(value)`
|
|
411
|
+
for each delta channel `k` (resolving `value` via the existing
|
|
412
|
+
ancestor walk first), then prune.
|
|
413
|
+
* Skip pruning threads whose graph uses `DeltaChannel` until one
|
|
414
|
+
of the above is implemented.
|
|
415
|
+
"""
|
|
416
|
+
raise NotImplementedError
|
|
417
|
+
|
|
418
|
+
async def aget(self, config: RunnableConfig) -> Checkpoint | None:
|
|
419
|
+
"""Asynchronously fetch a checkpoint using the given configuration.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
config: Configuration specifying which checkpoint to retrieve.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
The requested checkpoint, or `None` if not found.
|
|
426
|
+
"""
|
|
427
|
+
if value := await self.aget_tuple(config):
|
|
428
|
+
return value.checkpoint
|
|
429
|
+
|
|
430
|
+
async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
|
431
|
+
"""Asynchronously fetch a checkpoint tuple using the given configuration.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
config: Configuration specifying which checkpoint to retrieve.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
The requested checkpoint tuple, or `None` if not found.
|
|
438
|
+
|
|
439
|
+
Raises:
|
|
440
|
+
NotImplementedError: Implement this method in your custom checkpoint saver.
|
|
441
|
+
"""
|
|
442
|
+
raise NotImplementedError
|
|
443
|
+
|
|
444
|
+
async def alist(
|
|
445
|
+
self,
|
|
446
|
+
config: RunnableConfig | None,
|
|
447
|
+
*,
|
|
448
|
+
filter: dict[str, Any] | None = None,
|
|
449
|
+
before: RunnableConfig | None = None,
|
|
450
|
+
limit: int | None = None,
|
|
451
|
+
) -> AsyncIterator[CheckpointTuple]:
|
|
452
|
+
"""Asynchronously list checkpoints that match the given criteria.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
config: Base configuration for filtering checkpoints.
|
|
456
|
+
filter: Additional filtering criteria for metadata.
|
|
457
|
+
before: List checkpoints created before this configuration.
|
|
458
|
+
limit: Maximum number of checkpoints to return.
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Async iterator of matching checkpoint tuples.
|
|
462
|
+
|
|
463
|
+
Raises:
|
|
464
|
+
NotImplementedError: Implement this method in your custom checkpoint saver.
|
|
465
|
+
"""
|
|
466
|
+
raise NotImplementedError
|
|
467
|
+
yield
|
|
468
|
+
|
|
469
|
+
async def aput(
|
|
470
|
+
self,
|
|
471
|
+
config: RunnableConfig,
|
|
472
|
+
checkpoint: Checkpoint,
|
|
473
|
+
metadata: CheckpointMetadata,
|
|
474
|
+
new_versions: ChannelVersions,
|
|
475
|
+
) -> RunnableConfig:
|
|
476
|
+
"""Asynchronously store a checkpoint with its configuration and metadata.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
config: Configuration for the checkpoint.
|
|
480
|
+
checkpoint: The checkpoint to store.
|
|
481
|
+
metadata: Additional metadata for the checkpoint.
|
|
482
|
+
new_versions: New channel versions as of this write.
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
RunnableConfig: Updated configuration after storing the checkpoint.
|
|
486
|
+
|
|
487
|
+
Raises:
|
|
488
|
+
NotImplementedError: Implement this method in your custom checkpoint saver.
|
|
489
|
+
"""
|
|
490
|
+
raise NotImplementedError
|
|
491
|
+
|
|
492
|
+
async def aput_writes(
|
|
493
|
+
self,
|
|
494
|
+
config: RunnableConfig,
|
|
495
|
+
writes: Sequence[tuple[str, Any]],
|
|
496
|
+
task_id: str,
|
|
497
|
+
task_path: str = "",
|
|
498
|
+
) -> None:
|
|
499
|
+
"""Asynchronously store intermediate writes linked to a checkpoint.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
config: Configuration of the related checkpoint.
|
|
503
|
+
writes: List of writes to store.
|
|
504
|
+
task_id: Identifier for the task creating the writes.
|
|
505
|
+
task_path: Path of the task creating the writes.
|
|
506
|
+
|
|
507
|
+
Raises:
|
|
508
|
+
NotImplementedError: Implement this method in your custom checkpoint saver.
|
|
509
|
+
"""
|
|
510
|
+
raise NotImplementedError
|
|
511
|
+
|
|
512
|
+
async def adelete_thread(
|
|
513
|
+
self,
|
|
514
|
+
thread_id: str,
|
|
515
|
+
) -> None:
|
|
516
|
+
"""Delete all checkpoints and writes associated with a specific thread ID.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
thread_id: The thread ID whose checkpoints should be deleted.
|
|
520
|
+
"""
|
|
521
|
+
raise NotImplementedError
|
|
522
|
+
|
|
523
|
+
async def adelete_for_runs(
|
|
524
|
+
self,
|
|
525
|
+
run_ids: Sequence[str],
|
|
526
|
+
) -> None:
|
|
527
|
+
"""Asynchronously delete all checkpoints and writes for the given run IDs.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
run_ids: The run IDs whose checkpoints should be deleted.
|
|
531
|
+
|
|
532
|
+
!!! warning "DeltaChannel"
|
|
533
|
+
|
|
534
|
+
See `delete_for_runs` — deleting rows a still-live thread's
|
|
535
|
+
`DeltaChannel` reconstruction depends on (writes between the
|
|
536
|
+
head and its nearest `_DeltaSnapshot` ancestor) will silently
|
|
537
|
+
corrupt that channel's state.
|
|
538
|
+
"""
|
|
539
|
+
raise NotImplementedError
|
|
540
|
+
|
|
541
|
+
async def acopy_thread(
|
|
542
|
+
self,
|
|
543
|
+
source_thread_id: str,
|
|
544
|
+
target_thread_id: str,
|
|
545
|
+
) -> None:
|
|
546
|
+
"""Asynchronously copy all checkpoints and writes from one thread to another.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
source_thread_id: The thread ID to copy from.
|
|
550
|
+
target_thread_id: The thread ID to copy to.
|
|
551
|
+
|
|
552
|
+
!!! warning "DeltaChannel"
|
|
553
|
+
|
|
554
|
+
See `copy_thread` — the copy must carry the complete parent
|
|
555
|
+
chain (or at least back to a `_DeltaSnapshot` ancestor for every
|
|
556
|
+
`DeltaChannel`) so the target thread can reconstruct delta
|
|
557
|
+
state.
|
|
558
|
+
"""
|
|
559
|
+
raise NotImplementedError
|
|
560
|
+
|
|
561
|
+
async def aprune(
|
|
562
|
+
self,
|
|
563
|
+
thread_ids: Sequence[str],
|
|
564
|
+
*,
|
|
565
|
+
strategy: str = "keep_latest",
|
|
566
|
+
) -> None:
|
|
567
|
+
"""Asynchronously prune checkpoints for the given threads.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
thread_ids: The thread IDs to prune.
|
|
571
|
+
strategy: The pruning strategy. `"keep_latest"` retains only the most
|
|
572
|
+
recent checkpoint per namespace. `"delete"` removes all checkpoints.
|
|
573
|
+
|
|
574
|
+
!!! warning "DeltaChannel"
|
|
575
|
+
|
|
576
|
+
See `prune` for the full `DeltaChannel` caveat. In short:
|
|
577
|
+
`"keep_latest"` must not drop ancestor checkpoints / writes that
|
|
578
|
+
sit between the kept checkpoint and the nearest `_DeltaSnapshot`
|
|
579
|
+
ancestor, or delta channels will silently reconstruct as empty.
|
|
580
|
+
"""
|
|
581
|
+
raise NotImplementedError
|
|
582
|
+
|
|
583
|
+
def get_delta_channel_history(
|
|
584
|
+
self, *, config: RunnableConfig, channels: Sequence[str]
|
|
585
|
+
) -> Mapping[str, DeltaChannelHistory]:
|
|
586
|
+
"""Walk the parent chain returning per-channel writes + seed.
|
|
587
|
+
|
|
588
|
+
!!! warning "Beta"
|
|
589
|
+
|
|
590
|
+
This method is part of the `DeltaChannel` support surface and is
|
|
591
|
+
in beta. The signature, return shape (`DeltaChannelHistory`), and
|
|
592
|
+
interaction with `_DeltaSnapshot` blobs may change. Override at
|
|
593
|
+
your own risk; the default implementation will continue to work
|
|
594
|
+
against the public `BaseCheckpointSaver` contract.
|
|
595
|
+
|
|
596
|
+
For each requested channel, walks ancestors of the checkpoint
|
|
597
|
+
identified by `config` (following `parent_config`) and accumulates
|
|
598
|
+
`pending_writes` for that channel. The walk terminates per-channel
|
|
599
|
+
at the nearest ancestor whose `channel_values[ch]` is populated;
|
|
600
|
+
that value is returned as `seed`. If the walk reaches the root
|
|
601
|
+
without finding a stored value, `seed` is omitted from that
|
|
602
|
+
channel's entry — the consumer treats the absence as "start
|
|
603
|
+
empty."
|
|
604
|
+
|
|
605
|
+
Walks the **parent chain** (not `list(before=...)`): for forked
|
|
606
|
+
threads, only on-path ancestors contribute.
|
|
607
|
+
|
|
608
|
+
The default implementation walks `get_tuple` + `parent_config`
|
|
609
|
+
once for all channels — each ancestor visited once, not once per
|
|
610
|
+
channel. Savers with direct storage access (`InMemorySaver`,
|
|
611
|
+
`PostgresSaver`) override for performance; the return contract is
|
|
612
|
+
fixed here.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
config: Configuration identifying the target checkpoint.
|
|
616
|
+
channels: Channel names to walk for. Empty → empty mapping.
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
Per-channel `DeltaChannelHistory` for every name in `channels`.
|
|
620
|
+
"""
|
|
621
|
+
if not channels:
|
|
622
|
+
return {}
|
|
623
|
+
collected_by_ch: dict[str, list[PendingWrite]] = {c: [] for c in channels}
|
|
624
|
+
seed_by_ch: dict[str, Any] = {}
|
|
625
|
+
remaining: set[str] = set(channels)
|
|
626
|
+
target_tuple = self.get_tuple(config)
|
|
627
|
+
cursor_config: RunnableConfig | None = (
|
|
628
|
+
target_tuple.parent_config if target_tuple else None
|
|
629
|
+
)
|
|
630
|
+
while cursor_config is not None and remaining:
|
|
631
|
+
tup = self.get_tuple(cursor_config)
|
|
632
|
+
if tup is None:
|
|
633
|
+
break
|
|
634
|
+
if tup.pending_writes:
|
|
635
|
+
for write in reversed(tup.pending_writes):
|
|
636
|
+
ch = write[1]
|
|
637
|
+
if ch in remaining:
|
|
638
|
+
collected_by_ch[ch].append(write)
|
|
639
|
+
for ch in list(remaining):
|
|
640
|
+
if ch in tup.checkpoint["channel_values"]:
|
|
641
|
+
seed_by_ch[ch] = tup.checkpoint["channel_values"][ch]
|
|
642
|
+
remaining.discard(ch)
|
|
643
|
+
cursor_config = tup.parent_config
|
|
644
|
+
result: dict[str, DeltaChannelHistory] = {}
|
|
645
|
+
for ch in channels:
|
|
646
|
+
entry: DeltaChannelHistory = {"writes": list(reversed(collected_by_ch[ch]))}
|
|
647
|
+
if ch in seed_by_ch:
|
|
648
|
+
entry["seed"] = seed_by_ch[ch]
|
|
649
|
+
result[ch] = entry
|
|
650
|
+
return result
|
|
651
|
+
|
|
652
|
+
async def aget_delta_channel_history(
|
|
653
|
+
self, *, config: RunnableConfig, channels: Sequence[str]
|
|
654
|
+
) -> Mapping[str, DeltaChannelHistory]:
|
|
655
|
+
"""Async version of `get_delta_channel_history`.
|
|
656
|
+
|
|
657
|
+
!!! warning "Beta"
|
|
658
|
+
|
|
659
|
+
This method is part of the `DeltaChannel` support surface and is
|
|
660
|
+
in beta. See `get_delta_channel_history` for caveats.
|
|
661
|
+
"""
|
|
662
|
+
if not channels:
|
|
663
|
+
return {}
|
|
664
|
+
collected_by_ch: dict[str, list[PendingWrite]] = {c: [] for c in channels}
|
|
665
|
+
seed_by_ch: dict[str, Any] = {}
|
|
666
|
+
remaining: set[str] = set(channels)
|
|
667
|
+
target_tuple = await self.aget_tuple(config)
|
|
668
|
+
cursor_config: RunnableConfig | None = (
|
|
669
|
+
target_tuple.parent_config if target_tuple else None
|
|
670
|
+
)
|
|
671
|
+
while cursor_config is not None and remaining:
|
|
672
|
+
tup = await self.aget_tuple(cursor_config)
|
|
673
|
+
if tup is None:
|
|
674
|
+
break
|
|
675
|
+
if tup.pending_writes:
|
|
676
|
+
for write in reversed(tup.pending_writes):
|
|
677
|
+
ch = write[1]
|
|
678
|
+
if ch in remaining:
|
|
679
|
+
collected_by_ch[ch].append(write)
|
|
680
|
+
for ch in list(remaining):
|
|
681
|
+
if ch in tup.checkpoint["channel_values"]:
|
|
682
|
+
seed_by_ch[ch] = tup.checkpoint["channel_values"][ch]
|
|
683
|
+
remaining.discard(ch)
|
|
684
|
+
cursor_config = tup.parent_config
|
|
685
|
+
result: dict[str, DeltaChannelHistory] = {}
|
|
686
|
+
for ch in channels:
|
|
687
|
+
entry: DeltaChannelHistory = {"writes": list(reversed(collected_by_ch[ch]))}
|
|
688
|
+
if ch in seed_by_ch:
|
|
689
|
+
entry["seed"] = seed_by_ch[ch]
|
|
690
|
+
result[ch] = entry
|
|
691
|
+
return result
|
|
692
|
+
|
|
693
|
+
def get_next_version(self, current: V | None, channel: None) -> V:
|
|
694
|
+
"""Generate the next version ID for a channel.
|
|
695
|
+
|
|
696
|
+
Default is to use integer versions, incrementing by `1`.
|
|
697
|
+
|
|
698
|
+
If you override, you can use `str`/`int`/`float` versions, as long as they are monotonically increasing.
|
|
699
|
+
|
|
700
|
+
Args:
|
|
701
|
+
current: The current version identifier (`int`, `float`, or `str`).
|
|
702
|
+
channel: Deprecated argument, kept for backwards compatibility.
|
|
703
|
+
|
|
704
|
+
Returns:
|
|
705
|
+
V: The next version identifier, which must be increasing.
|
|
706
|
+
"""
|
|
707
|
+
if isinstance(current, str):
|
|
708
|
+
raise NotImplementedError
|
|
709
|
+
elif current is None:
|
|
710
|
+
return 1
|
|
711
|
+
else:
|
|
712
|
+
return current + 1
|
|
713
|
+
|
|
714
|
+
def with_allowlist(
|
|
715
|
+
self, extra_allowlist: Collection[tuple[str, ...]]
|
|
716
|
+
) -> BaseCheckpointSaver[V]:
|
|
717
|
+
"""Return a shallow clone with a derived msgpack allowlist."""
|
|
718
|
+
serde = _with_msgpack_allowlist(self.serde, extra_allowlist)
|
|
719
|
+
if serde is self.serde:
|
|
720
|
+
return self
|
|
721
|
+
clone = copy.copy(self)
|
|
722
|
+
clone.serde = maybe_add_typed_methods(serde)
|
|
723
|
+
return clone
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _with_msgpack_allowlist(
|
|
727
|
+
serde: SerializerProtocol, extra_allowlist: Collection[tuple[str, ...]]
|
|
728
|
+
) -> SerializerProtocol:
|
|
729
|
+
if isinstance(serde, JsonPlusSerializer):
|
|
730
|
+
return serde.with_msgpack_allowlist(extra_allowlist)
|
|
731
|
+
if isinstance(serde, EncryptedSerializer):
|
|
732
|
+
inner = serde.serde
|
|
733
|
+
if isinstance(inner, JsonPlusSerializer):
|
|
734
|
+
updated_inner = inner.with_msgpack_allowlist(extra_allowlist)
|
|
735
|
+
if updated_inner is inner:
|
|
736
|
+
return serde
|
|
737
|
+
return EncryptedSerializer(serde.cipher, updated_inner)
|
|
738
|
+
logger.warning(
|
|
739
|
+
"Serializer %s does not support msgpack allowlist. "
|
|
740
|
+
"Strict msgpack deserialization will not be enforced.",
|
|
741
|
+
type(serde).__name__,
|
|
742
|
+
)
|
|
743
|
+
return serde
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
class EmptyChannelError(Exception):
|
|
747
|
+
"""Raised when attempting to get the value of a channel that hasn't been updated
|
|
748
|
+
for the first time yet."""
|
|
749
|
+
|
|
750
|
+
pass
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def get_checkpoint_id(config: RunnableConfig) -> str | None:
|
|
754
|
+
"""Get checkpoint ID."""
|
|
755
|
+
return config["configurable"].get("checkpoint_id")
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def get_checkpoint_metadata(
|
|
759
|
+
config: RunnableConfig, metadata: CheckpointMetadata
|
|
760
|
+
) -> CheckpointMetadata:
|
|
761
|
+
"""Get checkpoint metadata in a backwards-compatible manner."""
|
|
762
|
+
metadata = {
|
|
763
|
+
k: v.replace("\u0000", "") if isinstance(v, str) else v
|
|
764
|
+
for k, v in metadata.items()
|
|
765
|
+
}
|
|
766
|
+
for obj in (config.get("metadata"), config.get("configurable")):
|
|
767
|
+
if not obj:
|
|
768
|
+
continue
|
|
769
|
+
for key, v in obj.items():
|
|
770
|
+
if key in metadata or key in EXCLUDED_METADATA_KEYS or key.startswith("__"):
|
|
771
|
+
continue
|
|
772
|
+
elif isinstance(v, str):
|
|
773
|
+
metadata[key] = v.replace("\u0000", "")
|
|
774
|
+
elif isinstance(v, (int, bool, float)):
|
|
775
|
+
metadata[key] = v
|
|
776
|
+
return metadata
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def get_serializable_checkpoint_metadata(
|
|
780
|
+
config: RunnableConfig, metadata: CheckpointMetadata
|
|
781
|
+
) -> CheckpointMetadata:
|
|
782
|
+
"""Get checkpoint metadata in a backwards-compatible manner."""
|
|
783
|
+
checkpoint_metadata = get_checkpoint_metadata(config, metadata)
|
|
784
|
+
if "writes" in checkpoint_metadata:
|
|
785
|
+
cast(dict[str, Any], checkpoint_metadata).pop("writes")
|
|
786
|
+
return checkpoint_metadata
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
"""
|
|
790
|
+
Mapping from error type to error index.
|
|
791
|
+
Regular writes just map to their index in the list of writes being saved.
|
|
792
|
+
Special writes (e.g. errors) map to negative indices, to avoid those writes from
|
|
793
|
+
conflicting with regular writes.
|
|
794
|
+
Each Checkpointer implementation should use this mapping in put_writes.
|
|
795
|
+
"""
|
|
796
|
+
WRITES_IDX_MAP = {ERROR: -1, SCHEDULED: -2, INTERRUPT: -3, RESUME: -4}
|
|
797
|
+
|
|
798
|
+
EXCLUDED_METADATA_KEYS = {
|
|
799
|
+
"thread_id",
|
|
800
|
+
"checkpoint_id",
|
|
801
|
+
"checkpoint_ns",
|
|
802
|
+
"checkpoint_map",
|
|
803
|
+
"coco_graph_step",
|
|
804
|
+
"coco_graph_node",
|
|
805
|
+
"coco_graph_triggers",
|
|
806
|
+
"coco_graph_path",
|
|
807
|
+
"coco_graph_checkpoint_ns",
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
# --- below are deprecated utilities used by past versions of CocoGraph ---
|
|
811
|
+
|
|
812
|
+
LATEST_VERSION = 2
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def empty_checkpoint() -> Checkpoint:
|
|
816
|
+
from datetime import datetime, timezone
|
|
817
|
+
|
|
818
|
+
return Checkpoint(
|
|
819
|
+
v=LATEST_VERSION,
|
|
820
|
+
id=str(uuid6(clock_seq=-2)),
|
|
821
|
+
ts=datetime.now(timezone.utc).isoformat(),
|
|
822
|
+
channel_values={},
|
|
823
|
+
channel_versions={},
|
|
824
|
+
versions_seen={},
|
|
825
|
+
pending_sends=[],
|
|
826
|
+
updated_channels=None,
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def create_checkpoint(
|
|
831
|
+
checkpoint: Checkpoint,
|
|
832
|
+
channels: Mapping[str, ChannelProtocol] | None,
|
|
833
|
+
step: int,
|
|
834
|
+
*,
|
|
835
|
+
id: str | None = None,
|
|
836
|
+
) -> Checkpoint:
|
|
837
|
+
"""Create a checkpoint for the given channels."""
|
|
838
|
+
from datetime import datetime, timezone
|
|
839
|
+
|
|
840
|
+
ts = datetime.now(timezone.utc).isoformat()
|
|
841
|
+
if channels is None:
|
|
842
|
+
values = checkpoint["channel_values"]
|
|
843
|
+
else:
|
|
844
|
+
values = {}
|
|
845
|
+
for k, v in channels.items():
|
|
846
|
+
if k not in checkpoint["channel_versions"]:
|
|
847
|
+
continue
|
|
848
|
+
try:
|
|
849
|
+
values[k] = v.checkpoint()
|
|
850
|
+
except EmptyChannelError:
|
|
851
|
+
pass
|
|
852
|
+
return Checkpoint(
|
|
853
|
+
v=LATEST_VERSION,
|
|
854
|
+
ts=ts,
|
|
855
|
+
id=id or str(uuid6(clock_seq=step)),
|
|
856
|
+
channel_values=values,
|
|
857
|
+
channel_versions=checkpoint["channel_versions"],
|
|
858
|
+
versions_seen=checkpoint["versions_seen"],
|
|
859
|
+
pending_sends=checkpoint.get("pending_sends", []),
|
|
860
|
+
updated_channels=None,
|
|
861
|
+
)
|