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,735 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Mapping
|
|
5
|
+
from types import MappingProxyType, TracebackType
|
|
6
|
+
from typing import TYPE_CHECKING, Any, NoReturn
|
|
7
|
+
|
|
8
|
+
from coco_core._api import beta
|
|
9
|
+
|
|
10
|
+
from coco_graph.stream._convert import convert_to_protocol_event
|
|
11
|
+
from coco_graph.stream._mux import StreamMux
|
|
12
|
+
from coco_graph.stream._types import ProtocolEvent
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from coco_core.language_models.chat_model_stream import (
|
|
16
|
+
AsyncChatModelStream,
|
|
17
|
+
ChatModelStream,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from coco_graph.stream.stream_channel import StreamChannel
|
|
21
|
+
from coco_graph.stream.transformers import LifecyclePayload, SubgraphStatus
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _drive_until_done(pump: Callable[[], bool]) -> None:
|
|
25
|
+
"""Call the sync pump until it returns False."""
|
|
26
|
+
while pump():
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def _adrive_until_done(pump: Callable[[], Awaitable[bool]]) -> None:
|
|
31
|
+
"""Call the async pump until it returns False."""
|
|
32
|
+
while await pump():
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _raise_missing_projection(run: object, name: str) -> NoReturn:
|
|
37
|
+
"""Raise after normal attribute lookup fails for a projection.
|
|
38
|
+
|
|
39
|
+
Registered native projections are installed directly on the run instance
|
|
40
|
+
during `__init__`, so `__getattr__` is never called for them. At this point
|
|
41
|
+
the requested name is necessarily missing; the mux is inspected only to
|
|
42
|
+
include the valid registered projection names in the error message.
|
|
43
|
+
|
|
44
|
+
Read `_mux` directly from `__dict__` because it may not exist yet on a
|
|
45
|
+
partially initialized instance. Accessing `run._mux` in that case would
|
|
46
|
+
invoke `__getattr__` again and recurse indefinitely.
|
|
47
|
+
"""
|
|
48
|
+
mux = run.__dict__.get("_mux")
|
|
49
|
+
registered = sorted(mux.native_keys) if mux is not None else []
|
|
50
|
+
raise AttributeError(
|
|
51
|
+
f"{type(run).__name__!r} object has no attribute {name!r} "
|
|
52
|
+
f"(registered projections: {', '.join(registered) or 'none'})"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@beta(message="The v3 streaming protocol on Pregel is experimental.")
|
|
57
|
+
class GraphRunStream:
|
|
58
|
+
"""Sync run stream with caller-driven pumping.
|
|
59
|
+
|
|
60
|
+
The caller's iteration on any projection (`values`, `messages`,
|
|
61
|
+
raw events, or `output`) drives the graph forward. No background
|
|
62
|
+
thread is used — the caller's `for` loop is the pump.
|
|
63
|
+
|
|
64
|
+
Projections are single-consumer — iterating `run.values` twice
|
|
65
|
+
raises. Use `projection.tee(n)` if you genuinely need fan-out.
|
|
66
|
+
|
|
67
|
+
All transformer projections live in `extensions`. Native transformer
|
|
68
|
+
projections (those with `_native = True`) are also set as direct
|
|
69
|
+
attributes on this instance (e.g. `run.values`, `run.messages`).
|
|
70
|
+
|
|
71
|
+
!!! warning
|
|
72
|
+
|
|
73
|
+
Returned by `Pregel.stream_events(version="v3")`, which is
|
|
74
|
+
experimental and may change.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
# Native projections, attached dynamically by the `setattr` loop in
|
|
78
|
+
# `__init__` and declared here so type checkers see them.
|
|
79
|
+
#
|
|
80
|
+
# Always registered by `stream_events(version="v3")`:
|
|
81
|
+
values: StreamChannel[dict[str, Any]]
|
|
82
|
+
messages: StreamChannel[ChatModelStream]
|
|
83
|
+
lifecycle: StreamChannel[LifecyclePayload]
|
|
84
|
+
subgraphs: StreamChannel[SubgraphRunStream]
|
|
85
|
+
# Registered on demand via `compile(transformers=...)` or
|
|
86
|
+
# `stream_events(transformers=...)`; reading one whose transformer was not
|
|
87
|
+
# registered raises AttributeError. Projections contributed by transformers
|
|
88
|
+
# outside this package are covered by `__getattr__` instead.
|
|
89
|
+
updates: StreamChannel[dict[str, Any]]
|
|
90
|
+
custom: StreamChannel[Any]
|
|
91
|
+
checkpoints: StreamChannel[dict[str, Any]]
|
|
92
|
+
debug: StreamChannel[dict[str, Any]]
|
|
93
|
+
tasks: StreamChannel[dict[str, Any]]
|
|
94
|
+
|
|
95
|
+
def __getattr__(self, name: str) -> StreamChannel[Any]:
|
|
96
|
+
"""Type the projections of transformers declared outside this package.
|
|
97
|
+
|
|
98
|
+
Projection names come from a registry, so no annotation here can name
|
|
99
|
+
them all. The cost is that a misspelling type-checks too, and fails at
|
|
100
|
+
runtime instead.
|
|
101
|
+
"""
|
|
102
|
+
_raise_missing_projection(self, name)
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
graph_iter: Iterator[Any] | None,
|
|
107
|
+
mux: StreamMux,
|
|
108
|
+
*,
|
|
109
|
+
wire_pump: bool = True,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Initialize the run stream.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
graph_iter: Pull-based iterator over the graph's stream,
|
|
115
|
+
or `None` for nested run streams whose pump is driven
|
|
116
|
+
by an outer run (e.g. `SubgraphRunStream`).
|
|
117
|
+
mux: The StreamMux owning projections and the main log.
|
|
118
|
+
wire_pump: When True (default), bind `_pump_next` as the
|
|
119
|
+
mux's pump callable. Subclasses that inherit a parent
|
|
120
|
+
pump via `StreamMux._make_child` should pass False to
|
|
121
|
+
preserve the parent binding.
|
|
122
|
+
"""
|
|
123
|
+
self._graph_iter = graph_iter
|
|
124
|
+
self._mux = mux
|
|
125
|
+
self.extensions: Mapping[str, Any] = MappingProxyType(mux.extensions)
|
|
126
|
+
self._exhausted = False
|
|
127
|
+
self._latest: dict[str, Any] | None = None
|
|
128
|
+
self._interrupted = False
|
|
129
|
+
self._interrupts: list[Any] = []
|
|
130
|
+
self._scope_list: list[str] = list(mux.scope)
|
|
131
|
+
for key in mux.native_keys:
|
|
132
|
+
setattr(self, key, mux.extensions[key])
|
|
133
|
+
if wire_pump:
|
|
134
|
+
self._wire_request_more(mux)
|
|
135
|
+
|
|
136
|
+
def _wire_request_more(self, mux: StreamMux) -> None:
|
|
137
|
+
"""Wire the sync pull callback through the mux.
|
|
138
|
+
|
|
139
|
+
Routing through `mux.bind_pump` (rather than walking
|
|
140
|
+
projections directly here) lets child mini-muxes built by
|
|
141
|
+
`mux._make_child(...)` inherit the same pump callable, so
|
|
142
|
+
cursors on a subgraph handle's projections drive the root
|
|
143
|
+
pump just like cursors on `run.values` do.
|
|
144
|
+
"""
|
|
145
|
+
mux.bind_pump(self._pump_next)
|
|
146
|
+
|
|
147
|
+
def _observe_event(self, event: ProtocolEvent) -> None:
|
|
148
|
+
"""Track values-event state for output/interrupted/interrupts."""
|
|
149
|
+
if event["method"] != "values":
|
|
150
|
+
return
|
|
151
|
+
params = event["params"]
|
|
152
|
+
if params["namespace"] != self._scope_list:
|
|
153
|
+
return
|
|
154
|
+
self._latest = params["data"]
|
|
155
|
+
interrupts = params.get("interrupts", ())
|
|
156
|
+
if interrupts:
|
|
157
|
+
self._interrupted = True
|
|
158
|
+
self._interrupts.extend(interrupts)
|
|
159
|
+
|
|
160
|
+
def _pump_next(self) -> bool:
|
|
161
|
+
"""Pull one event from the graph and push it through the mux.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
True if an event was pulled, False if the graph is exhausted
|
|
165
|
+
or has raised. Always False when constructed with
|
|
166
|
+
`graph_iter=None` (the run is driven by an outer pump).
|
|
167
|
+
"""
|
|
168
|
+
if self._exhausted or self._graph_iter is None:
|
|
169
|
+
return False
|
|
170
|
+
try:
|
|
171
|
+
part = next(self._graph_iter)
|
|
172
|
+
event = convert_to_protocol_event(part)
|
|
173
|
+
self._observe_event(event)
|
|
174
|
+
self._mux.push(event)
|
|
175
|
+
return True
|
|
176
|
+
except StopIteration:
|
|
177
|
+
self._mux.close()
|
|
178
|
+
self._exhausted = True
|
|
179
|
+
return False
|
|
180
|
+
except Exception as e:
|
|
181
|
+
self._mux.fail(e)
|
|
182
|
+
self._exhausted = True
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
def abort(self) -> None:
|
|
186
|
+
"""Stop the run early.
|
|
187
|
+
|
|
188
|
+
Closes the underlying graph iterator (propagating `GeneratorExit`
|
|
189
|
+
so in-flight nodes and subgraphs are cancelled), closes the mux,
|
|
190
|
+
and marks the stream exhausted. Idempotent.
|
|
191
|
+
"""
|
|
192
|
+
if self._exhausted:
|
|
193
|
+
return
|
|
194
|
+
self._exhausted = True
|
|
195
|
+
graph_iter = self._graph_iter
|
|
196
|
+
self._graph_iter = None
|
|
197
|
+
if (
|
|
198
|
+
graph_iter is not None
|
|
199
|
+
and (close := getattr(graph_iter, "close", None)) is not None
|
|
200
|
+
):
|
|
201
|
+
try:
|
|
202
|
+
close()
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
try:
|
|
206
|
+
self._mux.close()
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
def __enter__(self) -> GraphRunStream:
|
|
211
|
+
return self
|
|
212
|
+
|
|
213
|
+
def __exit__(
|
|
214
|
+
self,
|
|
215
|
+
exc_type: type[BaseException] | None,
|
|
216
|
+
exc: BaseException | None,
|
|
217
|
+
tb: TracebackType | None,
|
|
218
|
+
) -> None:
|
|
219
|
+
self.abort()
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def output(self) -> dict[str, Any] | None:
|
|
223
|
+
"""Drive the run to completion and return the final state."""
|
|
224
|
+
_drive_until_done(self._pump_next)
|
|
225
|
+
if (err := self._mux._events._error) is not None:
|
|
226
|
+
raise err
|
|
227
|
+
return self._latest
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def interrupted(self) -> bool:
|
|
231
|
+
"""Drive the run to completion, then return whether it was
|
|
232
|
+
interrupted.
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
BaseException: If the run ended with an error.
|
|
236
|
+
"""
|
|
237
|
+
_drive_until_done(self._pump_next)
|
|
238
|
+
if (err := self._mux._events._error) is not None:
|
|
239
|
+
raise err
|
|
240
|
+
return self._interrupted
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def interrupts(self) -> list[Any]:
|
|
244
|
+
"""Drive the run to completion, then return interrupt payloads.
|
|
245
|
+
|
|
246
|
+
Raises:
|
|
247
|
+
BaseException: If the run ended with an error.
|
|
248
|
+
"""
|
|
249
|
+
_drive_until_done(self._pump_next)
|
|
250
|
+
if (err := self._mux._events._error) is not None:
|
|
251
|
+
raise err
|
|
252
|
+
return self._interrupts
|
|
253
|
+
|
|
254
|
+
def __iter__(self) -> Iterator[ProtocolEvent]:
|
|
255
|
+
"""Subscribe to the main event log and iterate protocol events."""
|
|
256
|
+
return iter(self._mux._events)
|
|
257
|
+
|
|
258
|
+
def interleave(self, *names: str) -> Iterator[tuple[str, Any]]:
|
|
259
|
+
"""Iterate multiple projections in arrival order, yielding ``(name, item)``.
|
|
260
|
+
|
|
261
|
+
Items are ordered by a monotonic push stamp assigned when each
|
|
262
|
+
transformer pushes into its `StreamChannel`. This gives strict
|
|
263
|
+
arrival ordering across projections, unlike round-robin.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
*names: Projection keys to interleave. Must match keys in
|
|
267
|
+
``extensions``.
|
|
268
|
+
|
|
269
|
+
Yields:
|
|
270
|
+
``(name, item)`` tuples in arrival order across the named
|
|
271
|
+
projections.
|
|
272
|
+
|
|
273
|
+
Each named channel is locked for the duration of iteration and
|
|
274
|
+
released when the generator completes, is closed, or raises.
|
|
275
|
+
Channels cannot be subscribed concurrently — use `.tee(n)` if
|
|
276
|
+
you need fan-out.
|
|
277
|
+
|
|
278
|
+
Raises:
|
|
279
|
+
KeyError: If a name doesn't match a registered projection.
|
|
280
|
+
|
|
281
|
+
Example:
|
|
282
|
+
```python
|
|
283
|
+
for name, item in run.interleave("messages", "values"):
|
|
284
|
+
if name == "messages":
|
|
285
|
+
print("msg:", item)
|
|
286
|
+
else:
|
|
287
|
+
print("val:", item)
|
|
288
|
+
```
|
|
289
|
+
"""
|
|
290
|
+
from coco_graph.stream.stream_channel import StreamChannel
|
|
291
|
+
|
|
292
|
+
channels: dict[str, StreamChannel[Any]] = {}
|
|
293
|
+
try:
|
|
294
|
+
for name in names:
|
|
295
|
+
ch = self.extensions[name]
|
|
296
|
+
if not isinstance(ch, StreamChannel):
|
|
297
|
+
raise TypeError(
|
|
298
|
+
f"interleave() requires StreamChannel projections, "
|
|
299
|
+
f"got {type(ch).__name__} for {name!r}"
|
|
300
|
+
)
|
|
301
|
+
if ch._is_async is None:
|
|
302
|
+
raise TypeError(
|
|
303
|
+
f"StreamChannel {name!r} has not been bound yet. "
|
|
304
|
+
"Register the transformer with a StreamMux first."
|
|
305
|
+
)
|
|
306
|
+
if ch._is_async:
|
|
307
|
+
raise TypeError(
|
|
308
|
+
f"StreamChannel {name!r} is bound to async mode — "
|
|
309
|
+
"sync interleave() cannot consume async channels."
|
|
310
|
+
)
|
|
311
|
+
if ch._subscribed:
|
|
312
|
+
raise RuntimeError(
|
|
313
|
+
f"StreamChannel {name!r} already has a subscriber; "
|
|
314
|
+
"use .tee(n) for fan-out."
|
|
315
|
+
)
|
|
316
|
+
ch._subscribed = True
|
|
317
|
+
channels[name] = ch
|
|
318
|
+
|
|
319
|
+
done: set[str] = set()
|
|
320
|
+
|
|
321
|
+
while len(done) < len(channels):
|
|
322
|
+
best: tuple[int, str] | None = None
|
|
323
|
+
for name, ch in channels.items():
|
|
324
|
+
if name in done:
|
|
325
|
+
continue
|
|
326
|
+
if ch._closed and not ch._items:
|
|
327
|
+
if ch._error is not None:
|
|
328
|
+
raise ch._error
|
|
329
|
+
done.add(name)
|
|
330
|
+
continue
|
|
331
|
+
if ch._items:
|
|
332
|
+
stamp = ch._items[0][0]
|
|
333
|
+
if best is None or stamp < best[0]:
|
|
334
|
+
best = (stamp, name)
|
|
335
|
+
|
|
336
|
+
if best is not None:
|
|
337
|
+
_stamp, item = channels[best[1]]._items.popleft()
|
|
338
|
+
yield (best[1], item)
|
|
339
|
+
else:
|
|
340
|
+
pump = self._mux._pump_fn
|
|
341
|
+
if pump is None or not pump():
|
|
342
|
+
before = len(done)
|
|
343
|
+
for name, ch in channels.items():
|
|
344
|
+
if name not in done and not ch._items:
|
|
345
|
+
if ch._closed:
|
|
346
|
+
if ch._error is not None:
|
|
347
|
+
raise ch._error
|
|
348
|
+
done.add(name)
|
|
349
|
+
if len(done) == before:
|
|
350
|
+
break
|
|
351
|
+
finally:
|
|
352
|
+
for ch in channels.values():
|
|
353
|
+
ch._subscribed = False
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@beta(message="The v3 streaming protocol on Pregel is experimental.")
|
|
357
|
+
class AsyncGraphRunStream:
|
|
358
|
+
"""Async run stream with caller-driven pumping.
|
|
359
|
+
|
|
360
|
+
Async iteration on any projection drives the graph forward — there
|
|
361
|
+
is no background task. Concurrent consumers share a single-flight
|
|
362
|
+
pump via an `asyncio.Lock`, so each awaiting cursor contributes one
|
|
363
|
+
event per acquisition. Backpressure comes from the logs: when a
|
|
364
|
+
subscribed log's buffer reaches `maxlen`, `apush` awaits the
|
|
365
|
+
subscriber to drain, which holds back the pump and paces the graph.
|
|
366
|
+
|
|
367
|
+
Projections are single-consumer — a second `aiter(run.values)`
|
|
368
|
+
raises. Use `projection.tee(n)` for fan-out.
|
|
369
|
+
|
|
370
|
+
Use as an async context manager to guarantee clean shutdown on
|
|
371
|
+
early exit:
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
async with await handler.astream(input) as run:
|
|
375
|
+
async for msg in run.messages:
|
|
376
|
+
...
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
!!! warning
|
|
380
|
+
|
|
381
|
+
Awaited from `Pregel.astream_events(version="v3")`, which is
|
|
382
|
+
experimental and may change.
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
# Native projections, attached dynamically by the `setattr` loop in
|
|
386
|
+
# `__init__` and declared here so type checkers see them.
|
|
387
|
+
#
|
|
388
|
+
# Always registered by `astream_events(version="v3")`:
|
|
389
|
+
values: StreamChannel[dict[str, Any]]
|
|
390
|
+
messages: StreamChannel[AsyncChatModelStream]
|
|
391
|
+
lifecycle: StreamChannel[LifecyclePayload]
|
|
392
|
+
subgraphs: StreamChannel[AsyncSubgraphRunStream]
|
|
393
|
+
# Registered on demand; see `GraphRunStream`.
|
|
394
|
+
updates: StreamChannel[dict[str, Any]]
|
|
395
|
+
custom: StreamChannel[Any]
|
|
396
|
+
checkpoints: StreamChannel[dict[str, Any]]
|
|
397
|
+
debug: StreamChannel[dict[str, Any]]
|
|
398
|
+
tasks: StreamChannel[dict[str, Any]]
|
|
399
|
+
|
|
400
|
+
def __getattr__(self, name: str) -> StreamChannel[Any]:
|
|
401
|
+
"""Type projections declared elsewhere. See `GraphRunStream`."""
|
|
402
|
+
_raise_missing_projection(self, name)
|
|
403
|
+
|
|
404
|
+
def __init__(
|
|
405
|
+
self,
|
|
406
|
+
graph_aiter: AsyncIterator[Any] | None,
|
|
407
|
+
mux: StreamMux,
|
|
408
|
+
*,
|
|
409
|
+
wire_pump: bool = True,
|
|
410
|
+
) -> None:
|
|
411
|
+
"""Initialize the async run stream.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
graph_aiter: Async iterator over the graph's stream, or
|
|
415
|
+
`None` for nested run streams whose pump is driven by
|
|
416
|
+
an outer run (e.g. `AsyncSubgraphRunStream`).
|
|
417
|
+
mux: The StreamMux owning projections and the main log.
|
|
418
|
+
wire_pump: When True (default), bind `_apump_next` as the
|
|
419
|
+
mux's async pump callable. Subclasses that inherit a
|
|
420
|
+
parent pump via `StreamMux._make_child` should pass
|
|
421
|
+
False to preserve the parent binding.
|
|
422
|
+
"""
|
|
423
|
+
self._graph_aiter = graph_aiter
|
|
424
|
+
self._mux = mux
|
|
425
|
+
self.extensions: Mapping[str, Any] = MappingProxyType(mux.extensions)
|
|
426
|
+
self._exhausted = False
|
|
427
|
+
self._latest: dict[str, Any] | None = None
|
|
428
|
+
self._interrupted = False
|
|
429
|
+
self._interrupts: list[Any] = []
|
|
430
|
+
self._scope_list: list[str] = list(mux.scope)
|
|
431
|
+
self._pump_cond = asyncio.Condition()
|
|
432
|
+
self._pumping = False
|
|
433
|
+
self._anext_task: asyncio.Future[Any] | None = None
|
|
434
|
+
self._aborting = False
|
|
435
|
+
for key in mux.native_keys:
|
|
436
|
+
setattr(self, key, mux.extensions[key])
|
|
437
|
+
if wire_pump:
|
|
438
|
+
self._wire_arequest_more(mux)
|
|
439
|
+
|
|
440
|
+
def _observe_event(self, event: ProtocolEvent) -> None:
|
|
441
|
+
"""Track values-event state for output/interrupted/interrupts."""
|
|
442
|
+
if event["method"] != "values":
|
|
443
|
+
return
|
|
444
|
+
params = event["params"]
|
|
445
|
+
if params["namespace"] != self._scope_list:
|
|
446
|
+
return
|
|
447
|
+
self._latest = params["data"]
|
|
448
|
+
interrupts = params.get("interrupts", ())
|
|
449
|
+
if interrupts:
|
|
450
|
+
self._interrupted = True
|
|
451
|
+
self._interrupts.extend(interrupts)
|
|
452
|
+
|
|
453
|
+
def _wire_arequest_more(self, mux: StreamMux) -> None:
|
|
454
|
+
"""Wire the async pull callback through the mux.
|
|
455
|
+
|
|
456
|
+
Mirrors `_wire_request_more`: routing through
|
|
457
|
+
`mux.bind_apump` lets child mini-muxes inherit the pump
|
|
458
|
+
callable so cursors on subgraph handles drive the root
|
|
459
|
+
pump.
|
|
460
|
+
"""
|
|
461
|
+
mux.bind_apump(self._apump_next)
|
|
462
|
+
|
|
463
|
+
async def _apump_next(self) -> bool:
|
|
464
|
+
"""Drive one pump step, or wait for the active pumper to drive one.
|
|
465
|
+
|
|
466
|
+
"Take-a-number" semantics: at most one task at a time calls
|
|
467
|
+
`graph_aiter.__anext__()` (asyncio iterators can't be advanced
|
|
468
|
+
concurrently). Other callers wait on a Condition that the
|
|
469
|
+
active pumper notifies after each step. This lets a "passive"
|
|
470
|
+
consumer — one whose projection's buffer is being filled by the
|
|
471
|
+
active pumper's push — wake up as soon as its data lands,
|
|
472
|
+
instead of queueing on the pump and only observing its data one
|
|
473
|
+
graph event late.
|
|
474
|
+
|
|
475
|
+
`except Exception` is intentional — `CancelledError` and other
|
|
476
|
+
`BaseException` subclasses propagate, matching asyncio's
|
|
477
|
+
cancellation contract.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
True if a pump step completed (by this task or another),
|
|
481
|
+
False if the graph is exhausted.
|
|
482
|
+
"""
|
|
483
|
+
async with self._pump_cond:
|
|
484
|
+
if self._exhausted or self._graph_aiter is None:
|
|
485
|
+
return False
|
|
486
|
+
if self._pumping:
|
|
487
|
+
# Another task is pumping; wait for its progress signal.
|
|
488
|
+
await self._pump_cond.wait()
|
|
489
|
+
return not self._exhausted
|
|
490
|
+
self._pumping = True
|
|
491
|
+
|
|
492
|
+
try:
|
|
493
|
+
try:
|
|
494
|
+
# Run the pull as a child task so `abort()` can cancel it
|
|
495
|
+
# mid-flight. Cancelling propagates `CancelledError` into the
|
|
496
|
+
# graph generator frame -> Pregel loop -> nested subgraph
|
|
497
|
+
# nodes, which a bare `aclose()` cannot do while the generator
|
|
498
|
+
# is running ("asynchronous generator is already running").
|
|
499
|
+
self._anext_task = asyncio.ensure_future(self._graph_aiter.__anext__())
|
|
500
|
+
try:
|
|
501
|
+
part = await self._anext_task
|
|
502
|
+
except asyncio.CancelledError:
|
|
503
|
+
if self._aborting:
|
|
504
|
+
# Abort-initiated cancel: stop gracefully.
|
|
505
|
+
self._exhausted = True
|
|
506
|
+
return False
|
|
507
|
+
# Genuine external cancel of this task: also stop the
|
|
508
|
+
# in-flight pull, then propagate.
|
|
509
|
+
self._anext_task.cancel()
|
|
510
|
+
raise
|
|
511
|
+
finally:
|
|
512
|
+
self._anext_task = None
|
|
513
|
+
event = convert_to_protocol_event(part)
|
|
514
|
+
self._observe_event(event)
|
|
515
|
+
await self._mux.apush(event)
|
|
516
|
+
return True
|
|
517
|
+
except StopAsyncIteration:
|
|
518
|
+
self._exhausted = True
|
|
519
|
+
await self._mux.aclose()
|
|
520
|
+
return False
|
|
521
|
+
except Exception as e:
|
|
522
|
+
self._exhausted = True
|
|
523
|
+
await self._mux.afail(e)
|
|
524
|
+
return False
|
|
525
|
+
finally:
|
|
526
|
+
async with self._pump_cond:
|
|
527
|
+
self._pumping = False
|
|
528
|
+
self._pump_cond.notify_all()
|
|
529
|
+
|
|
530
|
+
async def abort(self) -> None:
|
|
531
|
+
"""Stop the run early.
|
|
532
|
+
|
|
533
|
+
Marks the stream exhausted and wakes any pump-waiters. Cancels an
|
|
534
|
+
in-flight pull if one is running, then closes the underlying graph
|
|
535
|
+
iterator, so running nodes and nested subgraphs are cancelled
|
|
536
|
+
whether or not a pump is mid-pull. Closes the mux; any `apush`
|
|
537
|
+
blocked on backpressure wakes and returns without appending.
|
|
538
|
+
Idempotent.
|
|
539
|
+
"""
|
|
540
|
+
async with self._pump_cond:
|
|
541
|
+
if self._exhausted:
|
|
542
|
+
return
|
|
543
|
+
self._exhausted = True
|
|
544
|
+
self._aborting = True
|
|
545
|
+
graph_aiter = self._graph_aiter
|
|
546
|
+
self._graph_aiter = None
|
|
547
|
+
anext_task = self._anext_task
|
|
548
|
+
self._pump_cond.notify_all()
|
|
549
|
+
# If a pump is mid-pull, cancel it so the cancellation propagates
|
|
550
|
+
# into running nodes and nested subgraphs. Once it settles the
|
|
551
|
+
# generator is no longer running, so the `aclose()` below is a safe
|
|
552
|
+
# final cleanup (and handles the no-in-flight-pull case directly).
|
|
553
|
+
if anext_task is not None and not anext_task.done():
|
|
554
|
+
anext_task.cancel()
|
|
555
|
+
try:
|
|
556
|
+
await anext_task
|
|
557
|
+
except (asyncio.CancelledError, Exception):
|
|
558
|
+
pass
|
|
559
|
+
if (
|
|
560
|
+
graph_aiter is not None
|
|
561
|
+
and (aclose := getattr(graph_aiter, "aclose", None)) is not None
|
|
562
|
+
):
|
|
563
|
+
try:
|
|
564
|
+
await aclose()
|
|
565
|
+
except Exception:
|
|
566
|
+
pass
|
|
567
|
+
try:
|
|
568
|
+
await self._mux.aclose()
|
|
569
|
+
except Exception:
|
|
570
|
+
pass
|
|
571
|
+
|
|
572
|
+
async def __aenter__(self) -> AsyncGraphRunStream:
|
|
573
|
+
return self
|
|
574
|
+
|
|
575
|
+
async def __aexit__(
|
|
576
|
+
self,
|
|
577
|
+
exc_type: type[BaseException] | None,
|
|
578
|
+
exc: BaseException | None,
|
|
579
|
+
tb: TracebackType | None,
|
|
580
|
+
) -> None:
|
|
581
|
+
await self.abort()
|
|
582
|
+
|
|
583
|
+
async def output(self) -> dict[str, Any] | None:
|
|
584
|
+
"""Drive the run to completion and return the final state.
|
|
585
|
+
|
|
586
|
+
Methods (not properties) on the async lane so `run.output`
|
|
587
|
+
without `await` raises at type-check time instead of silently
|
|
588
|
+
yielding a coroutine object.
|
|
589
|
+
|
|
590
|
+
Example:
|
|
591
|
+
```python
|
|
592
|
+
output = await run.output()
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
Raises:
|
|
596
|
+
BaseException: If the run ended with an error.
|
|
597
|
+
"""
|
|
598
|
+
await _adrive_until_done(self._apump_next)
|
|
599
|
+
if (err := self._mux._events._error) is not None:
|
|
600
|
+
raise err
|
|
601
|
+
return self._latest
|
|
602
|
+
|
|
603
|
+
async def interrupted(self) -> bool:
|
|
604
|
+
"""Drive the run to completion and return whether it was
|
|
605
|
+
interrupted.
|
|
606
|
+
|
|
607
|
+
Raises:
|
|
608
|
+
BaseException: If the run ended with an error.
|
|
609
|
+
"""
|
|
610
|
+
await _adrive_until_done(self._apump_next)
|
|
611
|
+
if (err := self._mux._events._error) is not None:
|
|
612
|
+
raise err
|
|
613
|
+
return self._interrupted
|
|
614
|
+
|
|
615
|
+
async def interrupts(self) -> list[Any]:
|
|
616
|
+
"""Drive the run to completion and return interrupt payloads.
|
|
617
|
+
|
|
618
|
+
Raises:
|
|
619
|
+
BaseException: If the run ended with an error.
|
|
620
|
+
"""
|
|
621
|
+
await _adrive_until_done(self._apump_next)
|
|
622
|
+
if (err := self._mux._events._error) is not None:
|
|
623
|
+
raise err
|
|
624
|
+
return self._interrupts
|
|
625
|
+
|
|
626
|
+
def __aiter__(self) -> AsyncIterator[ProtocolEvent]:
|
|
627
|
+
"""Subscribe to the main event log and iterate protocol events."""
|
|
628
|
+
return self._mux._events.__aiter__()
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
class _SubgraphRunStreamMixin:
|
|
632
|
+
"""Subgraph metadata + parent-pump delegation shared by both lanes.
|
|
633
|
+
|
|
634
|
+
Inherits from `GraphRunStream` (or `AsyncGraphRunStream`) with
|
|
635
|
+
`graph_iter=None` + `wire_pump=False` — the mini-mux is driven
|
|
636
|
+
by the parent's pump (inherited via `StreamMux._make_child`), and
|
|
637
|
+
the handle never pulls upstream itself. Pump-driving methods
|
|
638
|
+
delegate to the parent pump so `handle.output` and friends drive
|
|
639
|
+
the root run.
|
|
640
|
+
|
|
641
|
+
Subclasses set the parent pump function captured at construction
|
|
642
|
+
(`_parent_pump_fn` / `_parent_apump_fn`) and override
|
|
643
|
+
`_pump_next` / `_apump_next` to delegate to it.
|
|
644
|
+
|
|
645
|
+
Status is updated in place by `SubgraphTransformer`. Iterate
|
|
646
|
+
`run.subgraphs` to receive handles as subgraphs spawn, then
|
|
647
|
+
drill into projections inside the loop body **before** the next
|
|
648
|
+
pump cycle — same lazy-subscribe constraint as root projections.
|
|
649
|
+
"""
|
|
650
|
+
|
|
651
|
+
path: tuple[str, ...]
|
|
652
|
+
graph_name: str | None
|
|
653
|
+
trigger_call_id: str | None
|
|
654
|
+
status: SubgraphStatus
|
|
655
|
+
error: str | None
|
|
656
|
+
_seen_terminal: bool
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
class SubgraphRunStream(GraphRunStream, _SubgraphRunStreamMixin):
|
|
660
|
+
"""Sync handle for a discovered subgraph (extends `GraphRunStream`)."""
|
|
661
|
+
|
|
662
|
+
def __init__(
|
|
663
|
+
self,
|
|
664
|
+
mux: StreamMux,
|
|
665
|
+
*,
|
|
666
|
+
path: tuple[str, ...],
|
|
667
|
+
graph_name: str | None = None,
|
|
668
|
+
trigger_call_id: str | None = None,
|
|
669
|
+
) -> None:
|
|
670
|
+
# Capture the parent-inherited pump before super().__init__
|
|
671
|
+
# touches anything; we delegate to it from `_pump_next`.
|
|
672
|
+
self._parent_pump_fn: Callable[[], bool] | None = mux._pump_fn
|
|
673
|
+
super().__init__(
|
|
674
|
+
graph_iter=None,
|
|
675
|
+
mux=mux,
|
|
676
|
+
wire_pump=False,
|
|
677
|
+
)
|
|
678
|
+
self.path = path
|
|
679
|
+
self.graph_name = graph_name
|
|
680
|
+
self.trigger_call_id = trigger_call_id
|
|
681
|
+
self.status = "started"
|
|
682
|
+
self.error = None
|
|
683
|
+
self._seen_terminal = False
|
|
684
|
+
|
|
685
|
+
def _pump_next(self) -> bool:
|
|
686
|
+
"""Delegate to the parent's pump.
|
|
687
|
+
|
|
688
|
+
Cursors on this handle's projections call here when their
|
|
689
|
+
buffers empty. Driving the parent fans events into our
|
|
690
|
+
mini-mux, transparently advancing the whole run.
|
|
691
|
+
"""
|
|
692
|
+
if (
|
|
693
|
+
self._exhausted
|
|
694
|
+
or self._seen_terminal
|
|
695
|
+
or self._mux._events._closed
|
|
696
|
+
or self._parent_pump_fn is None
|
|
697
|
+
):
|
|
698
|
+
return False
|
|
699
|
+
return self._parent_pump_fn()
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
class AsyncSubgraphRunStream(AsyncGraphRunStream, _SubgraphRunStreamMixin):
|
|
703
|
+
"""Async handle for a discovered subgraph (extends `AsyncGraphRunStream`)."""
|
|
704
|
+
|
|
705
|
+
def __init__(
|
|
706
|
+
self,
|
|
707
|
+
mux: StreamMux,
|
|
708
|
+
*,
|
|
709
|
+
path: tuple[str, ...],
|
|
710
|
+
graph_name: str | None = None,
|
|
711
|
+
trigger_call_id: str | None = None,
|
|
712
|
+
) -> None:
|
|
713
|
+
self._parent_apump_fn: Callable[[], Awaitable[bool]] | None = mux._apump_fn
|
|
714
|
+
super().__init__(
|
|
715
|
+
graph_aiter=None,
|
|
716
|
+
mux=mux,
|
|
717
|
+
wire_pump=False,
|
|
718
|
+
)
|
|
719
|
+
self.path = path
|
|
720
|
+
self.graph_name = graph_name
|
|
721
|
+
self.trigger_call_id = trigger_call_id
|
|
722
|
+
self.status = "started"
|
|
723
|
+
self.error = None
|
|
724
|
+
self._seen_terminal = False
|
|
725
|
+
|
|
726
|
+
async def _apump_next(self) -> bool:
|
|
727
|
+
"""Delegate to the parent's async pump."""
|
|
728
|
+
if (
|
|
729
|
+
self._exhausted
|
|
730
|
+
or self._seen_terminal
|
|
731
|
+
or self._mux._events._closed
|
|
732
|
+
or self._parent_apump_fn is None
|
|
733
|
+
):
|
|
734
|
+
return False
|
|
735
|
+
return await self._parent_apump_fn()
|