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,45 @@
|
|
|
1
|
+
"""Streaming infrastructure for CocoGraph.
|
|
2
|
+
|
|
3
|
+
Compile a graph with `transformers=[...]` and call `graph.stream_events(version="v3")` /
|
|
4
|
+
`graph.astream_events(version="v3")` to drive a transformer pipeline that projects the
|
|
5
|
+
graph's raw events into ergonomic per-channel streams.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from coco_graph.stream._types import ProtocolEvent, StreamTransformer
|
|
9
|
+
from coco_graph.stream.run_stream import (
|
|
10
|
+
AsyncGraphRunStream,
|
|
11
|
+
AsyncSubgraphRunStream,
|
|
12
|
+
GraphRunStream,
|
|
13
|
+
SubgraphRunStream,
|
|
14
|
+
)
|
|
15
|
+
from coco_graph.stream.stream_channel import StreamChannel
|
|
16
|
+
from coco_graph.stream.transformers import (
|
|
17
|
+
CheckpointsTransformer,
|
|
18
|
+
CustomTransformer,
|
|
19
|
+
DebugTransformer,
|
|
20
|
+
LifecyclePayload,
|
|
21
|
+
LifecycleTransformer,
|
|
22
|
+
SubgraphStatus,
|
|
23
|
+
SubgraphTransformer,
|
|
24
|
+
TasksTransformer,
|
|
25
|
+
UpdatesTransformer,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"AsyncGraphRunStream",
|
|
30
|
+
"AsyncSubgraphRunStream",
|
|
31
|
+
"CheckpointsTransformer",
|
|
32
|
+
"CustomTransformer",
|
|
33
|
+
"DebugTransformer",
|
|
34
|
+
"GraphRunStream",
|
|
35
|
+
"LifecyclePayload",
|
|
36
|
+
"LifecycleTransformer",
|
|
37
|
+
"ProtocolEvent",
|
|
38
|
+
"StreamChannel",
|
|
39
|
+
"StreamTransformer",
|
|
40
|
+
"SubgraphRunStream",
|
|
41
|
+
"SubgraphStatus",
|
|
42
|
+
"SubgraphTransformer",
|
|
43
|
+
"TasksTransformer",
|
|
44
|
+
"UpdatesTransformer",
|
|
45
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
|
|
6
|
+
from coco_graph.stream._types import ProtocolEvent, _ProtocolEventParams
|
|
7
|
+
from coco_graph.types import StreamPart
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def convert_to_protocol_event(part: StreamPart) -> ProtocolEvent:
|
|
11
|
+
"""Convert a v2 StreamPart to a ProtocolEvent.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
part: A stream part with keys `type`, `ns`, `data`, and
|
|
15
|
+
optionally `interrupts` (present on values events).
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
The equivalent ProtocolEvent.
|
|
19
|
+
"""
|
|
20
|
+
part_dict = cast(dict[str, Any], part)
|
|
21
|
+
params: _ProtocolEventParams = {
|
|
22
|
+
"namespace": list(part_dict["ns"]),
|
|
23
|
+
"timestamp": int(time.time() * 1000),
|
|
24
|
+
"data": part_dict["data"],
|
|
25
|
+
}
|
|
26
|
+
if "interrupts" in part_dict:
|
|
27
|
+
params["interrupts"] = part_dict["interrupts"]
|
|
28
|
+
return {
|
|
29
|
+
"type": "event",
|
|
30
|
+
"method": part_dict["type"],
|
|
31
|
+
"params": params,
|
|
32
|
+
}
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from coco_graph.stream._types import (
|
|
9
|
+
ProtocolEvent,
|
|
10
|
+
StreamTransformer,
|
|
11
|
+
transformer_requires_async,
|
|
12
|
+
)
|
|
13
|
+
from coco_graph.stream.stream_channel import StreamChannel
|
|
14
|
+
|
|
15
|
+
TransformerFactory = Callable[["tuple[str, ...]"], StreamTransformer]
|
|
16
|
+
"""Factory that builds a scoped transformer for a mux.
|
|
17
|
+
|
|
18
|
+
Called once per `StreamMux` with the mux's scope (typically `()` for
|
|
19
|
+
the root). Standard transformer classes accept a single positional
|
|
20
|
+
scope argument, so the class itself is a valid factory. User
|
|
21
|
+
transformers can close over their config:
|
|
22
|
+
`lambda scope: MyTransformer(scope, foo=...)`.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StreamMux:
|
|
27
|
+
"""Central event dispatcher for the streaming infrastructure.
|
|
28
|
+
|
|
29
|
+
Owns the main event log and routes events through a transformer
|
|
30
|
+
pipeline. StreamChannels with a name discovered in transformer
|
|
31
|
+
projections are auto-wired so that every `push()` also injects a
|
|
32
|
+
`ProtocolEvent` into the main log. StreamChannels without a name
|
|
33
|
+
are local-only.
|
|
34
|
+
|
|
35
|
+
Pass `is_async=True` when the mux will be consumed via async
|
|
36
|
+
iteration (`handler.astream()`). All StreamChannel instances
|
|
37
|
+
discovered during registration are automatically bound to the
|
|
38
|
+
matching mode.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
extensions: Merged projection dict across all registered
|
|
42
|
+
transformers. Treat as read-only — mutations won't be
|
|
43
|
+
reflected back in individual transformers' state.
|
|
44
|
+
native_keys: Projection keys contributed by transformers with
|
|
45
|
+
`_native = True`.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
transformers: list[StreamTransformer] | None = None,
|
|
51
|
+
*,
|
|
52
|
+
is_async: bool = False,
|
|
53
|
+
factories: list[TransformerFactory] | None = None,
|
|
54
|
+
scope: tuple[str, ...] = (),
|
|
55
|
+
_assign_seq: bool = True,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Initialize the mux and register transformers in order.
|
|
58
|
+
|
|
59
|
+
Callers pass either `transformers` (pre-built instances) or
|
|
60
|
+
`factories` (callables producing fresh instances per mux). Each
|
|
61
|
+
transformer's `init()` is called, projections are merged into
|
|
62
|
+
`extensions`, `_native` keys are recorded in `native_keys`, and
|
|
63
|
+
any StreamChannel instances are bound and (if named) wired.
|
|
64
|
+
|
|
65
|
+
Transformers with `StreamTransformer.before_builtins = True` are
|
|
66
|
+
registered ahead of the rest, preserving relative order within
|
|
67
|
+
each lane. This lets content-mutating transformers (PII
|
|
68
|
+
redaction, content filters, etc.) run before built-ins like
|
|
69
|
+
`MessagesTransformer` that eagerly snapshot text fields into
|
|
70
|
+
their projections. See `StreamTransformer.before_builtins` for
|
|
71
|
+
the contract and foot-guns.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
transformers: Already-built transformer instances. Registered
|
|
75
|
+
only on this mux — they are NOT cloned into child
|
|
76
|
+
mini-muxes built by `_make_child`. Use `factories` for
|
|
77
|
+
transformers that should propagate to nested scopes.
|
|
78
|
+
is_async: True for async dispatch (`apush` / `aclose` /
|
|
79
|
+
`afail`), False for the sync path.
|
|
80
|
+
factories: One-argument callables `(scope) -> StreamTransformer`.
|
|
81
|
+
Called once with this mux's `scope` here, and cloned
|
|
82
|
+
again per child scope by `_make_child` so each
|
|
83
|
+
sub-mux gets fresh instances.
|
|
84
|
+
scope: The namespace the mux operates within. The root mux
|
|
85
|
+
is `()`.
|
|
86
|
+
_assign_seq: Internal flag for child muxes. Root muxes assign
|
|
87
|
+
monotonic `seq` numbers when appending to their main event
|
|
88
|
+
log; child muxes share forwarded event objects and must not
|
|
89
|
+
mutate their envelopes.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
RuntimeError: If any transformer requires an async run but
|
|
93
|
+
the mux is in sync mode.
|
|
94
|
+
TypeError: If a transformer's `init()` doesn't return a dict.
|
|
95
|
+
ValueError: If transformers' projection keys collide.
|
|
96
|
+
"""
|
|
97
|
+
self.is_async = is_async
|
|
98
|
+
self.scope: tuple[str, ...] = scope
|
|
99
|
+
self._assign_seq = _assign_seq
|
|
100
|
+
self._events: StreamChannel[ProtocolEvent] = StreamChannel()
|
|
101
|
+
self._events._bind(is_async=is_async)
|
|
102
|
+
self._events._bind_mux(self)
|
|
103
|
+
self._transformers: list[StreamTransformer] = []
|
|
104
|
+
self._channels: list[StreamChannel[Any]] = []
|
|
105
|
+
self._seq = 0
|
|
106
|
+
self._push_seq = 0
|
|
107
|
+
|
|
108
|
+
self.extensions: dict[str, Any] = {}
|
|
109
|
+
self.native_keys: set[str] = set()
|
|
110
|
+
self._projection_owners: dict[str, str] = {}
|
|
111
|
+
self._transformer_by_key: dict[str, StreamTransformer] = {}
|
|
112
|
+
|
|
113
|
+
# Stored only when constructed from factories — used by
|
|
114
|
+
# `_make_child` to clone the transformer pipeline at a deeper
|
|
115
|
+
# scope. Pre-built transformers can't be cloned, so a mux
|
|
116
|
+
# built with `transformers=` rejects child construction.
|
|
117
|
+
self._factories: list[TransformerFactory] | None = (
|
|
118
|
+
list(factories) if factories is not None else None
|
|
119
|
+
)
|
|
120
|
+
self._pump_fn: Callable[[], bool] | None = None
|
|
121
|
+
self._apump_fn: Callable[[], Awaitable[bool]] | None = None
|
|
122
|
+
|
|
123
|
+
# Factories run first (they propagate to child mini-muxes via
|
|
124
|
+
# `_make_child`), then any pre-built `transformers=` instances
|
|
125
|
+
# are registered as root-only — they aren't cloned for child
|
|
126
|
+
# scopes. Within each group, transformers with
|
|
127
|
+
# `before_builtins = True` are registered ahead of the rest so
|
|
128
|
+
# they observe (and may mutate) events before built-ins like
|
|
129
|
+
# `MessagesTransformer`. The order *within* each lane matches
|
|
130
|
+
# the supplied sequence.
|
|
131
|
+
pre: list[StreamTransformer] = []
|
|
132
|
+
rest: list[StreamTransformer] = []
|
|
133
|
+
if factories is not None:
|
|
134
|
+
for factory in factories:
|
|
135
|
+
transformer = factory(scope)
|
|
136
|
+
(
|
|
137
|
+
pre if getattr(transformer, "before_builtins", False) else rest
|
|
138
|
+
).append(transformer)
|
|
139
|
+
for transformer in (*pre, *rest):
|
|
140
|
+
self._register(transformer)
|
|
141
|
+
pre.clear()
|
|
142
|
+
rest.clear()
|
|
143
|
+
for transformer in transformers or ():
|
|
144
|
+
(pre if getattr(transformer, "before_builtins", False) else rest).append(
|
|
145
|
+
transformer
|
|
146
|
+
)
|
|
147
|
+
for transformer in (*pre, *rest):
|
|
148
|
+
self._register(transformer)
|
|
149
|
+
|
|
150
|
+
def transformer_by_key(self, key: str) -> StreamTransformer | None:
|
|
151
|
+
"""Return the transformer that contributed `key` to the projection."""
|
|
152
|
+
return self._transformer_by_key.get(key)
|
|
153
|
+
|
|
154
|
+
def _next_push_seq(self) -> int:
|
|
155
|
+
self._push_seq += 1
|
|
156
|
+
return self._push_seq
|
|
157
|
+
|
|
158
|
+
# ------------------------------------------------------------------
|
|
159
|
+
# Pump wiring + mini-mux nesting
|
|
160
|
+
# ------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def bind_pump(self, fn: Callable[[], bool]) -> None:
|
|
163
|
+
"""Wire the sync pull callback onto every projection in this mux.
|
|
164
|
+
|
|
165
|
+
Records the pump on the mux so child mini-muxes built by
|
|
166
|
+
`_make_child` can inherit it. Propagates to:
|
|
167
|
+
- the main event log (`self._events`)
|
|
168
|
+
- every projection StreamChannel in `extensions`
|
|
169
|
+
- any registered transformer that exposes `_bind_pump` (e.g.
|
|
170
|
+
`MessagesTransformer` so `ChatModelStream` instances drive the
|
|
171
|
+
shared pump from their cursors)
|
|
172
|
+
"""
|
|
173
|
+
self._pump_fn = fn
|
|
174
|
+
self._events._request_more = fn
|
|
175
|
+
for ch in self._channels:
|
|
176
|
+
ch._request_more = fn
|
|
177
|
+
for transformer in self._transformers:
|
|
178
|
+
bind = getattr(transformer, "_bind_pump", None)
|
|
179
|
+
if bind is not None:
|
|
180
|
+
bind(fn)
|
|
181
|
+
|
|
182
|
+
def bind_apump(self, fn: Callable[[], Awaitable[bool]]) -> None:
|
|
183
|
+
"""Async counterpart to `bind_pump`."""
|
|
184
|
+
self._apump_fn = fn
|
|
185
|
+
self._events._arequest_more = fn
|
|
186
|
+
for ch in self._channels:
|
|
187
|
+
ch._arequest_more = fn
|
|
188
|
+
for transformer in self._transformers:
|
|
189
|
+
abind = getattr(transformer, "_bind_apump", None)
|
|
190
|
+
if abind is not None:
|
|
191
|
+
abind(fn)
|
|
192
|
+
|
|
193
|
+
def _make_child(self, scope: tuple[str, ...]) -> StreamMux:
|
|
194
|
+
"""Build a mini-mux with the same factories scoped to `scope`.
|
|
195
|
+
|
|
196
|
+
Used by `SubgraphTransformer` to attach a fresh transformer
|
|
197
|
+
pipeline to each discovered subgraph handle. The child mux
|
|
198
|
+
inherits the current pump bindings (so cursors on its
|
|
199
|
+
projection logs drive the root pump), carries the same factory
|
|
200
|
+
list forward to any grandchild subgraphs, and does not assign
|
|
201
|
+
`seq` numbers so forwarded events can be shared without
|
|
202
|
+
mutating their envelope.
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
RuntimeError: If the mux was not constructed with
|
|
206
|
+
`factories=`. Mini-muxes require factories so each scope
|
|
207
|
+
gets its own fresh transformer instances.
|
|
208
|
+
"""
|
|
209
|
+
if self._factories is None:
|
|
210
|
+
raise RuntimeError(
|
|
211
|
+
"StreamMux._make_child requires the mux to be constructed "
|
|
212
|
+
"with `factories=`; pre-built transformers can't be "
|
|
213
|
+
"cloned to a new scope."
|
|
214
|
+
)
|
|
215
|
+
child = StreamMux(
|
|
216
|
+
factories=self._factories,
|
|
217
|
+
is_async=self.is_async,
|
|
218
|
+
scope=scope,
|
|
219
|
+
_assign_seq=False,
|
|
220
|
+
)
|
|
221
|
+
if self._pump_fn is not None:
|
|
222
|
+
child.bind_pump(self._pump_fn)
|
|
223
|
+
if self._apump_fn is not None:
|
|
224
|
+
child.bind_apump(self._apump_fn)
|
|
225
|
+
return child
|
|
226
|
+
|
|
227
|
+
def _register(self, transformer: StreamTransformer) -> None:
|
|
228
|
+
"""Register a single transformer.
|
|
229
|
+
|
|
230
|
+
Calls `transformer.init()`, stores the transformer for event
|
|
231
|
+
processing, binds any StreamChannel instances in the projection,
|
|
232
|
+
and merges the projection into `extensions`.
|
|
233
|
+
"""
|
|
234
|
+
if transformer_requires_async(transformer) and not self.is_async:
|
|
235
|
+
raise RuntimeError(
|
|
236
|
+
f"{type(transformer).__name__} requires an async run — "
|
|
237
|
+
"it overrides aprocess/afinalize/afail or sets "
|
|
238
|
+
"requires_async=True. Use astream(), not stream()."
|
|
239
|
+
)
|
|
240
|
+
projection = transformer.init()
|
|
241
|
+
if not isinstance(projection, dict):
|
|
242
|
+
raise TypeError(
|
|
243
|
+
f"StreamTransformer.init() must return a dict, "
|
|
244
|
+
f"got {type(projection).__name__}"
|
|
245
|
+
)
|
|
246
|
+
conflicts = set(projection) & set(self.extensions)
|
|
247
|
+
if conflicts:
|
|
248
|
+
attributions = ", ".join(
|
|
249
|
+
f"{key!r} (owned by {self._projection_owners[key]})"
|
|
250
|
+
for key in sorted(conflicts)
|
|
251
|
+
)
|
|
252
|
+
raise ValueError(
|
|
253
|
+
f"Transformer {type(transformer).__name__} returned "
|
|
254
|
+
f"projection keys that conflict with already-registered "
|
|
255
|
+
f"keys: {attributions}"
|
|
256
|
+
)
|
|
257
|
+
is_native = bool(getattr(transformer, "_native", False))
|
|
258
|
+
self._transformers.append(transformer)
|
|
259
|
+
self._bind_and_wire(projection, native=is_native)
|
|
260
|
+
self.extensions.update(projection)
|
|
261
|
+
owner_name = type(transformer).__name__
|
|
262
|
+
for key in projection:
|
|
263
|
+
self._projection_owners[key] = owner_name
|
|
264
|
+
self._transformer_by_key[key] = transformer
|
|
265
|
+
if is_native:
|
|
266
|
+
self.native_keys.update(projection.keys())
|
|
267
|
+
transformer._on_register(self)
|
|
268
|
+
|
|
269
|
+
def push(self, event: ProtocolEvent) -> None:
|
|
270
|
+
"""Route an event through all transformers, then append to the main log.
|
|
271
|
+
|
|
272
|
+
Each transformer's `process()` is called in registration order.
|
|
273
|
+
If any transformer returns False, the event is suppressed from
|
|
274
|
+
the main log, but transformers that already saw it keep their
|
|
275
|
+
side effects.
|
|
276
|
+
|
|
277
|
+
On the root mux, `seq` is assigned right before an event enters
|
|
278
|
+
the main log, not before the transformer pipeline runs. This
|
|
279
|
+
ensures that events auto-forwarded from StreamChannels during
|
|
280
|
+
`process()` get earlier seq numbers than the original event,
|
|
281
|
+
preserving monotonic ordering in the root log. Child muxes do
|
|
282
|
+
not assign `seq`, so subgraph forwarding can share event objects
|
|
283
|
+
without mutating their envelopes.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
event: The protocol event to dispatch.
|
|
287
|
+
"""
|
|
288
|
+
keep = True
|
|
289
|
+
for transformer in self._transformers:
|
|
290
|
+
if not transformer.process(event):
|
|
291
|
+
keep = False
|
|
292
|
+
if keep:
|
|
293
|
+
if self._assign_seq:
|
|
294
|
+
self._seq += 1
|
|
295
|
+
event["seq"] = self._seq
|
|
296
|
+
self._events.push(event)
|
|
297
|
+
|
|
298
|
+
def close(self) -> None:
|
|
299
|
+
"""Finalize all transformers, close all projections and the main log.
|
|
300
|
+
|
|
301
|
+
StreamChannels discovered in transformer projections are
|
|
302
|
+
auto-closed after `finalize()` runs — transformers don't need
|
|
303
|
+
to close them manually. If any transformer's `finalize()` raises,
|
|
304
|
+
the remaining transformers, projections, and the main log are
|
|
305
|
+
still closed; the first error is re-raised after cleanup
|
|
306
|
+
completes.
|
|
307
|
+
|
|
308
|
+
Raises:
|
|
309
|
+
BaseException: The first error raised by a transformer's
|
|
310
|
+
`finalize()`, re-raised after cleanup finishes.
|
|
311
|
+
"""
|
|
312
|
+
first_error: BaseException | None = None
|
|
313
|
+
for transformer in self._transformers:
|
|
314
|
+
try:
|
|
315
|
+
transformer.finalize()
|
|
316
|
+
except BaseException as e:
|
|
317
|
+
if first_error is None:
|
|
318
|
+
first_error = e
|
|
319
|
+
for ch in self._channels:
|
|
320
|
+
if not ch._closed:
|
|
321
|
+
ch.close()
|
|
322
|
+
self._events.close()
|
|
323
|
+
if first_error is not None:
|
|
324
|
+
raise first_error
|
|
325
|
+
|
|
326
|
+
def fail(self, err: BaseException) -> None:
|
|
327
|
+
"""Fail all transformers, projections, and the main log.
|
|
328
|
+
|
|
329
|
+
StreamChannels discovered in transformer projections are
|
|
330
|
+
auto-failed — transformers don't need to fail them manually.
|
|
331
|
+
If any transformer's `fail()` raises, the remaining
|
|
332
|
+
transformers, projections, and the main log are still failed.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
err: The exception that ended the run.
|
|
336
|
+
"""
|
|
337
|
+
for transformer in self._transformers:
|
|
338
|
+
try:
|
|
339
|
+
transformer.fail(err)
|
|
340
|
+
except BaseException:
|
|
341
|
+
pass
|
|
342
|
+
for ch in self._channels:
|
|
343
|
+
if not ch._closed:
|
|
344
|
+
ch.fail(err)
|
|
345
|
+
self._events.fail(err)
|
|
346
|
+
|
|
347
|
+
# ------------------------------------------------------------------
|
|
348
|
+
# Async dispatch
|
|
349
|
+
# ------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
async def apush(self, event: ProtocolEvent) -> None:
|
|
352
|
+
"""Dispatch an event on the async lane.
|
|
353
|
+
|
|
354
|
+
Awaits each transformer's `aprocess` in registration order
|
|
355
|
+
before appending to the main log. A slow `aprocess` serializes
|
|
356
|
+
the pipeline by design — that's the guarantee that lets a later
|
|
357
|
+
transformer (or a synchronous consumer) see the result of the
|
|
358
|
+
async work. For decoupled work, use `schedule()` from inside
|
|
359
|
+
`process` / `aprocess` instead.
|
|
360
|
+
|
|
361
|
+
The main log append is a non-blocking `push` — matching v1's
|
|
362
|
+
`put_nowait` shape. The root mux assigns `seq`; child muxes do
|
|
363
|
+
not, so forwarded subgraph events can be shared without copying.
|
|
364
|
+
Memory is bounded by caller pace via the caller-driven pump; see
|
|
365
|
+
`StreamChannel` for the full tradeoff story.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
event: The protocol event to dispatch.
|
|
369
|
+
"""
|
|
370
|
+
keep = True
|
|
371
|
+
for transformer in self._transformers:
|
|
372
|
+
if not await transformer.aprocess(event):
|
|
373
|
+
keep = False
|
|
374
|
+
if keep:
|
|
375
|
+
if self._assign_seq:
|
|
376
|
+
self._seq += 1
|
|
377
|
+
event["seq"] = self._seq
|
|
378
|
+
self._events.push(event)
|
|
379
|
+
|
|
380
|
+
async def aclose(self) -> None:
|
|
381
|
+
"""Finalize on the async lane.
|
|
382
|
+
|
|
383
|
+
Awaits every task started via `StreamTransformer.schedule()`
|
|
384
|
+
across all transformers, then calls `afinalize()` on each,
|
|
385
|
+
then auto-closes channels and the main event log.
|
|
386
|
+
|
|
387
|
+
If any scheduled task raised under `on_error="raise"`, or any
|
|
388
|
+
transformer's `afinalize` raises, the exception propagates.
|
|
389
|
+
The caller (the pump) handles it by routing into `afail`.
|
|
390
|
+
|
|
391
|
+
Raises:
|
|
392
|
+
BaseException: The first scheduled-task or `afinalize`
|
|
393
|
+
error, re-raised after cleanup.
|
|
394
|
+
"""
|
|
395
|
+
pending = self._collect_scheduled_tasks()
|
|
396
|
+
if pending:
|
|
397
|
+
results = await asyncio.gather(*pending, return_exceptions=True)
|
|
398
|
+
first_err = next(
|
|
399
|
+
(
|
|
400
|
+
r
|
|
401
|
+
for r in results
|
|
402
|
+
if isinstance(r, BaseException)
|
|
403
|
+
and not isinstance(r, asyncio.CancelledError)
|
|
404
|
+
),
|
|
405
|
+
None,
|
|
406
|
+
)
|
|
407
|
+
if first_err is not None:
|
|
408
|
+
raise first_err
|
|
409
|
+
|
|
410
|
+
first_error: BaseException | None = None
|
|
411
|
+
for transformer in self._transformers:
|
|
412
|
+
try:
|
|
413
|
+
await transformer.afinalize()
|
|
414
|
+
except BaseException as e:
|
|
415
|
+
if first_error is None:
|
|
416
|
+
first_error = e
|
|
417
|
+
for ch in self._channels:
|
|
418
|
+
if not ch._closed:
|
|
419
|
+
ch.close()
|
|
420
|
+
self._events.close()
|
|
421
|
+
if first_error is not None:
|
|
422
|
+
raise first_error
|
|
423
|
+
|
|
424
|
+
async def afail(self, err: BaseException) -> None:
|
|
425
|
+
"""Fail on the async lane.
|
|
426
|
+
|
|
427
|
+
Cancels every scheduled task across all transformers, awaits
|
|
428
|
+
them to completion, then runs each transformer's `afail` hook
|
|
429
|
+
and auto-fails channels and the main event log.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
err: The exception that ended the run.
|
|
433
|
+
"""
|
|
434
|
+
pending = self._collect_scheduled_tasks()
|
|
435
|
+
for task in pending:
|
|
436
|
+
task.cancel()
|
|
437
|
+
if pending:
|
|
438
|
+
await asyncio.gather(*pending, return_exceptions=True)
|
|
439
|
+
|
|
440
|
+
for transformer in self._transformers:
|
|
441
|
+
try:
|
|
442
|
+
await transformer.afail(err)
|
|
443
|
+
except BaseException:
|
|
444
|
+
pass
|
|
445
|
+
for ch in self._channels:
|
|
446
|
+
if not ch._closed:
|
|
447
|
+
ch.fail(err)
|
|
448
|
+
if not self._events._closed:
|
|
449
|
+
self._events.fail(err)
|
|
450
|
+
|
|
451
|
+
def _collect_scheduled_tasks(self) -> list[asyncio.Task[Any]]:
|
|
452
|
+
"""Return a snapshot of in-flight tasks scheduled via transformers."""
|
|
453
|
+
return [
|
|
454
|
+
task
|
|
455
|
+
for transformer in self._transformers
|
|
456
|
+
for task in getattr(transformer, "_stream_scheduled_tasks", ())
|
|
457
|
+
if not task.done()
|
|
458
|
+
]
|
|
459
|
+
|
|
460
|
+
# ------------------------------------------------------------------
|
|
461
|
+
# Binding and StreamChannel auto-wiring
|
|
462
|
+
# ------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
def _bind_and_wire(
|
|
465
|
+
self, projection: dict[str, Any], *, native: bool = False
|
|
466
|
+
) -> None:
|
|
467
|
+
"""Bind and optionally wire StreamChannel instances in a projection.
|
|
468
|
+
|
|
469
|
+
All StreamChannels are bound and tracked. Channels with a name
|
|
470
|
+
are additionally wired for protocol auto-forwarding.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
projection: The projection dict returned by a transformer's
|
|
474
|
+
`init()`.
|
|
475
|
+
native: True when the owning transformer is `_native`.
|
|
476
|
+
Named channels owned by a native transformer use the
|
|
477
|
+
channel name directly as the protocol method;
|
|
478
|
+
user-defined channels are prefixed with `custom:`.
|
|
479
|
+
"""
|
|
480
|
+
for value in projection.values():
|
|
481
|
+
if isinstance(value, StreamChannel):
|
|
482
|
+
value._bind(is_async=self.is_async)
|
|
483
|
+
value._bind_mux(self)
|
|
484
|
+
self._channels.append(value)
|
|
485
|
+
if value.name is not None:
|
|
486
|
+
method = value.name if native else f"custom:{value.name}"
|
|
487
|
+
|
|
488
|
+
def _make_forward(method_name: str) -> Callable[[Any], None]:
|
|
489
|
+
def _forward(item: Any) -> None:
|
|
490
|
+
self._forward(method_name, item)
|
|
491
|
+
|
|
492
|
+
return _forward
|
|
493
|
+
|
|
494
|
+
value._wire(_make_forward(method))
|
|
495
|
+
|
|
496
|
+
def _forward(self, method: str, item: Any) -> None:
|
|
497
|
+
"""Inject a ProtocolEvent for a StreamChannel push.
|
|
498
|
+
|
|
499
|
+
Forwarded events bypass the transformer pipeline to avoid
|
|
500
|
+
infinite recursion (a transformer that pushes to a channel
|
|
501
|
+
during `process()` would re-trigger itself). These events are
|
|
502
|
+
visible in this mux's main event log but are not passed through
|
|
503
|
+
transformers' `process()` methods. Only the root mux assigns
|
|
504
|
+
`seq` to forwarded channel events.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
method: The full protocol method (already with or without
|
|
508
|
+
the `custom:` prefix; resolved by `_bind_and_wire`).
|
|
509
|
+
item: The payload pushed onto the channel.
|
|
510
|
+
"""
|
|
511
|
+
event: ProtocolEvent = {
|
|
512
|
+
"type": "event",
|
|
513
|
+
"method": method,
|
|
514
|
+
"params": {
|
|
515
|
+
"namespace": [],
|
|
516
|
+
"timestamp": int(time.time() * 1000),
|
|
517
|
+
"data": item,
|
|
518
|
+
},
|
|
519
|
+
}
|
|
520
|
+
if self._assign_seq:
|
|
521
|
+
self._seq += 1
|
|
522
|
+
event["seq"] = self._seq
|
|
523
|
+
self._events.push(event)
|