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,1039 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
5
|
+
|
|
6
|
+
from coco_core.language_models._compat_bridge import message_to_events
|
|
7
|
+
from coco_core.language_models.chat_model_stream import (
|
|
8
|
+
AsyncChatModelStream,
|
|
9
|
+
ChatModelStream,
|
|
10
|
+
)
|
|
11
|
+
from coco_core.messages import AIMessageChunk, BaseMessage, ToolMessage
|
|
12
|
+
from coco_core._protocol import LifecycleCause, MessagesData
|
|
13
|
+
from typing_extensions import NotRequired, TypedDict
|
|
14
|
+
|
|
15
|
+
from coco_graph.errors import GraphDrained, GraphInterrupt
|
|
16
|
+
from coco_graph.stream._types import ProtocolEvent, StreamTransformer
|
|
17
|
+
from coco_graph.stream.run_stream import AsyncSubgraphRunStream, SubgraphRunStream
|
|
18
|
+
from coco_graph.stream.stream_channel import StreamChannel
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Awaitable, Callable
|
|
22
|
+
|
|
23
|
+
from coco_graph.stream._mux import StreamMux
|
|
24
|
+
|
|
25
|
+
_logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ValuesTransformer(StreamTransformer):
|
|
29
|
+
"""Capture values events as a drainable stream of state snapshots.
|
|
30
|
+
|
|
31
|
+
Provides the `run.values` projection. `run.output`,
|
|
32
|
+
`run.interrupted` and `run.interrupts` are tracked directly
|
|
33
|
+
by the run stream and do not depend on this transformer.
|
|
34
|
+
|
|
35
|
+
Native transformer — projection keys are exposed as direct
|
|
36
|
+
attributes on the run stream (e.g. `run.values`).
|
|
37
|
+
|
|
38
|
+
Only values events at the run's own level are captured; snapshots
|
|
39
|
+
from deeper subgraphs are left in the main event log but excluded
|
|
40
|
+
from the projection. "Own level" is defined by `scope`, which
|
|
41
|
+
`stream_events(version="v3")` / `astream_events(version="v3")` populate from the caller's
|
|
42
|
+
checkpoint namespace so that a nested `stream_events(version="v3")` call still
|
|
43
|
+
sees its own root snapshots.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
_native = True
|
|
47
|
+
required_stream_modes = ("values",)
|
|
48
|
+
|
|
49
|
+
def __init__(self, scope: tuple[str, ...] = ()) -> None:
|
|
50
|
+
super().__init__(scope)
|
|
51
|
+
self._log: StreamChannel[dict[str, Any]] = StreamChannel()
|
|
52
|
+
self._latest: dict[str, Any] | None = None
|
|
53
|
+
self._interrupted = False
|
|
54
|
+
self._interrupts: list[Any] = []
|
|
55
|
+
# Cached as a list once for cheap equality with the protocol
|
|
56
|
+
# event's `namespace` field, which is `list[str]`.
|
|
57
|
+
self._scope_list: list[str] = list(scope)
|
|
58
|
+
|
|
59
|
+
def init(self) -> dict[str, Any]:
|
|
60
|
+
return {"values": self._log}
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def error(self) -> BaseException | None:
|
|
64
|
+
"""The error that ended the run, or `None` if it succeeded.
|
|
65
|
+
|
|
66
|
+
Set by the mux when it auto-fails the projection log.
|
|
67
|
+
"""
|
|
68
|
+
return self._log._error
|
|
69
|
+
|
|
70
|
+
def process(self, event: ProtocolEvent) -> bool:
|
|
71
|
+
if event["method"] != "values":
|
|
72
|
+
return True
|
|
73
|
+
params = event["params"]
|
|
74
|
+
if params["namespace"] != self._scope_list:
|
|
75
|
+
return True
|
|
76
|
+
self._latest = params["data"]
|
|
77
|
+
interrupts = params.get("interrupts", ())
|
|
78
|
+
if interrupts:
|
|
79
|
+
self._interrupted = True
|
|
80
|
+
self._interrupts.extend(interrupts)
|
|
81
|
+
self._log.push(params["data"])
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class CustomTransformer(StreamTransformer):
|
|
86
|
+
"""Capture custom events as a drainable stream of arbitrary payloads.
|
|
87
|
+
|
|
88
|
+
Nodes emit custom data via `get_stream_writer()`. This transformer
|
|
89
|
+
surfaces those events on `run.custom` as a `StreamChannel[Any]`,
|
|
90
|
+
preserving payloads in arrival order.
|
|
91
|
+
|
|
92
|
+
Only events at the run's own scope are captured; custom data from
|
|
93
|
+
deeper subgraphs is available on the respective subgraph handle's
|
|
94
|
+
`.custom` projection.
|
|
95
|
+
|
|
96
|
+
Native transformer — `run.custom` is a direct attribute.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
_native = True
|
|
100
|
+
required_stream_modes = ("custom",)
|
|
101
|
+
|
|
102
|
+
def __init__(self, scope: tuple[str, ...] = ()) -> None:
|
|
103
|
+
super().__init__(scope)
|
|
104
|
+
self._log: StreamChannel[Any] = StreamChannel()
|
|
105
|
+
self._scope_list: list[str] = list(scope)
|
|
106
|
+
|
|
107
|
+
def init(self) -> dict[str, Any]:
|
|
108
|
+
return {"custom": self._log}
|
|
109
|
+
|
|
110
|
+
def process(self, event: ProtocolEvent) -> bool:
|
|
111
|
+
if event["method"] != "custom":
|
|
112
|
+
return True
|
|
113
|
+
params = event["params"]
|
|
114
|
+
if params["namespace"] != self._scope_list:
|
|
115
|
+
return True
|
|
116
|
+
self._log.push(params["data"])
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class UpdatesTransformer(StreamTransformer):
|
|
121
|
+
"""Capture updates events as a drainable stream of node outputs.
|
|
122
|
+
|
|
123
|
+
Surfaces `stream_mode="updates"` data on `run.updates` as a
|
|
124
|
+
`StreamChannel[dict[str, Any]]`. Each item is a dict mapping a node
|
|
125
|
+
(or task) name to the update it returned after a step.
|
|
126
|
+
|
|
127
|
+
Only events at the run's own scope are captured; updates from deeper
|
|
128
|
+
subgraphs are available on the respective subgraph handle's
|
|
129
|
+
`.updates` projection.
|
|
130
|
+
|
|
131
|
+
Native transformer — `run.updates` is a direct attribute.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
_native = True
|
|
135
|
+
required_stream_modes = ("updates",)
|
|
136
|
+
|
|
137
|
+
def __init__(self, scope: tuple[str, ...] = ()) -> None:
|
|
138
|
+
super().__init__(scope)
|
|
139
|
+
self._log: StreamChannel[dict[str, Any]] = StreamChannel()
|
|
140
|
+
self._scope_list: list[str] = list(scope)
|
|
141
|
+
|
|
142
|
+
def init(self) -> dict[str, Any]:
|
|
143
|
+
return {"updates": self._log}
|
|
144
|
+
|
|
145
|
+
def process(self, event: ProtocolEvent) -> bool:
|
|
146
|
+
if event["method"] != "updates":
|
|
147
|
+
return True
|
|
148
|
+
params = event["params"]
|
|
149
|
+
if params["namespace"] != self._scope_list:
|
|
150
|
+
return True
|
|
151
|
+
self._log.push(params["data"])
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class MessagesTransformer(StreamTransformer):
|
|
156
|
+
"""Capture messages events as ChatModelStream objects.
|
|
157
|
+
|
|
158
|
+
The messages projection yields one `ChatModelStream` (or
|
|
159
|
+
`AsyncChatModelStream`) per LLM call. Consumers iterate
|
|
160
|
+
`run.messages` to get stream handles, then use each handle's typed
|
|
161
|
+
projections (`.text`, `.reasoning`, `.tool_calls`, `.usage`,
|
|
162
|
+
`.output`) for per-message content.
|
|
163
|
+
|
|
164
|
+
Two input shapes are handled (via `params["data"] = (payload,
|
|
165
|
+
metadata)` from `StreamMessagesHandler`):
|
|
166
|
+
|
|
167
|
+
1. Protocol event (dict with `"event"` key) — emitted by
|
|
168
|
+
`stream_events(version="v3")` / `astream_events(version="v3")` via the `on_stream_event`
|
|
169
|
+
callback. Routed to an existing `ChatModelStream` by
|
|
170
|
+
`metadata["run_id"]`. A `message-start` event creates a new
|
|
171
|
+
stream; `message-finish` closes it.
|
|
172
|
+
2. Whole `AIMessage` — emitted from `on_chain_end` when a node
|
|
173
|
+
returns a finalized message. Replayed as a synthetic protocol
|
|
174
|
+
event lifecycle via `message_to_events`, then the
|
|
175
|
+
already-complete stream is pushed to the log.
|
|
176
|
+
|
|
177
|
+
V1 `AIMessageChunk` tuples (from `on_llm_new_token`) are not
|
|
178
|
+
streamed into this projection: chat models that want to populate
|
|
179
|
+
`run.messages` with content-block streaming must use
|
|
180
|
+
`stream_events(version="v3")` / `astream_events(version="v3")`. Models called via the legacy
|
|
181
|
+
`stream()` method still surface their final `AIMessage` via
|
|
182
|
+
`on_chain_end` when a node returns it as state.
|
|
183
|
+
|
|
184
|
+
Only events at the run's own level are projected; tokens from
|
|
185
|
+
deeper subgraphs are left in the main event log but excluded from
|
|
186
|
+
`.messages`. "Own level" is defined by `scope`, which
|
|
187
|
+
`stream_events(version="v3")` / `astream_events(version="v3")` populate from the caller's checkpoint
|
|
188
|
+
namespace so that a `stream_events(version="v3")` call inside a node still sees its
|
|
189
|
+
own root chat model streams on `.messages`. Consumers that need
|
|
190
|
+
subgraph tokens should iterate the raw event stream or register a
|
|
191
|
+
custom transformer.
|
|
192
|
+
|
|
193
|
+
Native transformer — the `messages` projection is exposed as a
|
|
194
|
+
direct attribute on the run stream.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
_native = True
|
|
198
|
+
required_stream_modes = ("messages",)
|
|
199
|
+
|
|
200
|
+
def __init__(self, scope: tuple[str, ...] = ()) -> None:
|
|
201
|
+
super().__init__(scope)
|
|
202
|
+
self._log: StreamChannel[ChatModelStream] = StreamChannel()
|
|
203
|
+
# Correlate protocol events back to a ChatModelStream by run_id
|
|
204
|
+
# (attached to the event's metadata by StreamMessagesHandler).
|
|
205
|
+
self._by_run: dict[str, ChatModelStream] = {}
|
|
206
|
+
self._ignored_runs: set[str] = set()
|
|
207
|
+
self._pump_fn: Callable[[], bool] | None = None
|
|
208
|
+
self._apump_fn: Callable[[], Awaitable[bool]] | None = None
|
|
209
|
+
# Cached as a list once for cheap equality with the protocol
|
|
210
|
+
# event's `namespace` field, which is `list[str]`.
|
|
211
|
+
self._scope_list: list[str] = list(scope)
|
|
212
|
+
|
|
213
|
+
def init(self) -> dict[str, Any]:
|
|
214
|
+
return {"messages": self._log}
|
|
215
|
+
|
|
216
|
+
def _bind_pump(self, fn: Callable[[], bool]) -> None:
|
|
217
|
+
"""Wire the sync pull callback. Called by GraphRunStream._wire_request_more."""
|
|
218
|
+
self._pump_fn = fn
|
|
219
|
+
|
|
220
|
+
def _bind_apump(self, fn: Callable[[], Awaitable[bool]]) -> None:
|
|
221
|
+
"""Wire the async pull callback.
|
|
222
|
+
|
|
223
|
+
Called by `AsyncGraphRunStream._wire_arequest_more` so each
|
|
224
|
+
`AsyncChatModelStream` this transformer creates can drive the
|
|
225
|
+
shared graph pump from its projection cursors.
|
|
226
|
+
"""
|
|
227
|
+
self._apump_fn = fn
|
|
228
|
+
|
|
229
|
+
def _make_stream(
|
|
230
|
+
self,
|
|
231
|
+
*,
|
|
232
|
+
namespace: list[str],
|
|
233
|
+
node: str | None,
|
|
234
|
+
message_id: str | None,
|
|
235
|
+
) -> ChatModelStream:
|
|
236
|
+
"""Create a ChatModelStream (sync) or AsyncChatModelStream (async).
|
|
237
|
+
|
|
238
|
+
Wires whichever pump is bound. Prefers the async pump so nested
|
|
239
|
+
iteration under `AsyncGraphRunStream` drives the graph forward
|
|
240
|
+
without a background task. The unwired fallback (no pump bound)
|
|
241
|
+
is used by unit tests that dispatch events manually.
|
|
242
|
+
"""
|
|
243
|
+
if self._apump_fn is not None:
|
|
244
|
+
astream = AsyncChatModelStream(
|
|
245
|
+
namespace=namespace,
|
|
246
|
+
node=node,
|
|
247
|
+
message_id=message_id,
|
|
248
|
+
)
|
|
249
|
+
astream.set_arequest_more(self._apump_fn)
|
|
250
|
+
return astream
|
|
251
|
+
if self._pump_fn is not None:
|
|
252
|
+
stream: ChatModelStream = ChatModelStream(
|
|
253
|
+
namespace=namespace,
|
|
254
|
+
node=node,
|
|
255
|
+
message_id=message_id,
|
|
256
|
+
)
|
|
257
|
+
stream.set_request_more(self._pump_fn)
|
|
258
|
+
return stream
|
|
259
|
+
return AsyncChatModelStream(
|
|
260
|
+
namespace=namespace,
|
|
261
|
+
node=node,
|
|
262
|
+
message_id=message_id,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def process(self, event: ProtocolEvent) -> bool:
|
|
266
|
+
if event["method"] != "messages":
|
|
267
|
+
return True
|
|
268
|
+
params = event["params"]
|
|
269
|
+
if params["namespace"] != self._scope_list:
|
|
270
|
+
return True
|
|
271
|
+
|
|
272
|
+
payload, metadata = params["data"]
|
|
273
|
+
node: str | None = metadata.get("coco_graph_node")
|
|
274
|
+
run_id = str(metadata.get("run_id", "")) if metadata else ""
|
|
275
|
+
|
|
276
|
+
if isinstance(payload, dict) and "event" in payload:
|
|
277
|
+
self._route_protocol_event(
|
|
278
|
+
cast("MessagesData", payload), run_id=run_id, node=node
|
|
279
|
+
)
|
|
280
|
+
elif (
|
|
281
|
+
isinstance(payload, BaseMessage)
|
|
282
|
+
and not isinstance(payload, AIMessageChunk)
|
|
283
|
+
and not isinstance(payload, ToolMessage)
|
|
284
|
+
):
|
|
285
|
+
self._route_whole_message(payload, node=node)
|
|
286
|
+
# Legacy AIMessageChunk tuples (from on_llm_new_token) are ignored;
|
|
287
|
+
# v1 streaming callers must switch to stream_events(version="v3") to populate this
|
|
288
|
+
# projection.
|
|
289
|
+
|
|
290
|
+
return True
|
|
291
|
+
|
|
292
|
+
def _route_protocol_event(
|
|
293
|
+
self,
|
|
294
|
+
event: MessagesData,
|
|
295
|
+
*,
|
|
296
|
+
run_id: str,
|
|
297
|
+
node: str | None,
|
|
298
|
+
) -> None:
|
|
299
|
+
event_type = event.get("event")
|
|
300
|
+
if event_type == "message-start":
|
|
301
|
+
# Tool results are exposed on the tools projection and state
|
|
302
|
+
# snapshots; run.messages is the chat-token projection.
|
|
303
|
+
if event.get("role") == "tool":
|
|
304
|
+
self._ignored_runs.add(run_id)
|
|
305
|
+
return
|
|
306
|
+
message_id = event.get("message_id")
|
|
307
|
+
stream = self._make_stream(
|
|
308
|
+
namespace=[],
|
|
309
|
+
node=node,
|
|
310
|
+
message_id=str(message_id) if message_id is not None else None,
|
|
311
|
+
)
|
|
312
|
+
self._by_run[run_id] = stream
|
|
313
|
+
self._log.push(stream)
|
|
314
|
+
stream.dispatch(event)
|
|
315
|
+
elif run_id in self._ignored_runs:
|
|
316
|
+
if event_type == "message-finish":
|
|
317
|
+
self._ignored_runs.discard(run_id)
|
|
318
|
+
elif run_id in self._by_run:
|
|
319
|
+
stream = self._by_run[run_id]
|
|
320
|
+
stream.dispatch(event)
|
|
321
|
+
if event_type == "message-finish":
|
|
322
|
+
del self._by_run[run_id]
|
|
323
|
+
|
|
324
|
+
def _route_whole_message(self, message: BaseMessage, *, node: str | None) -> None:
|
|
325
|
+
stream = self._make_stream(namespace=[], node=node, message_id=message.id)
|
|
326
|
+
for evt in message_to_events(message, message_id=message.id):
|
|
327
|
+
stream.dispatch(evt)
|
|
328
|
+
self._log.push(stream)
|
|
329
|
+
|
|
330
|
+
def finalize(self) -> None:
|
|
331
|
+
"""Clear any routing state — streams close themselves via `message-finish`."""
|
|
332
|
+
self._by_run.clear()
|
|
333
|
+
self._ignored_runs.clear()
|
|
334
|
+
|
|
335
|
+
def fail(self, err: BaseException) -> None:
|
|
336
|
+
"""Propagate run error to any streams still open when the graph fails."""
|
|
337
|
+
for stream in list(self._by_run.values()):
|
|
338
|
+
stream.fail(err)
|
|
339
|
+
self._by_run.clear()
|
|
340
|
+
self._ignored_runs.clear()
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
SubgraphStatus = Literal["started", "completed", "failed", "interrupted", "drained"]
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _parse_ns_segment(segment: str) -> tuple[str, str | None]:
|
|
347
|
+
"""Split a namespace segment into `(graph_name, trigger_call_id)`.
|
|
348
|
+
|
|
349
|
+
Segments are formatted `node_name:task_id` by `prepare_next_tasks`.
|
|
350
|
+
Returns `(segment, None)` if no `:` is present.
|
|
351
|
+
"""
|
|
352
|
+
name, sep, task_id = segment.partition(":")
|
|
353
|
+
return name, task_id if sep else None
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class LifecyclePayload(TypedDict, total=False):
|
|
357
|
+
"""Payload of a lifecycle event surfaced on the `lifecycle` channel.
|
|
358
|
+
|
|
359
|
+
Auto-forwarded as `lifecycle` protocol events (no `custom:` prefix
|
|
360
|
+
because `LifecycleTransformer` is a native transformer) so remote
|
|
361
|
+
SDK clients receive the same data in-process consumers see via
|
|
362
|
+
`run.lifecycle`.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
event: SubgraphStatus
|
|
366
|
+
namespace: list[str]
|
|
367
|
+
graph_name: NotRequired[str]
|
|
368
|
+
trigger_call_id: NotRequired[str]
|
|
369
|
+
cause: NotRequired[LifecycleCause]
|
|
370
|
+
error: NotRequired[str]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class _TasksLifecycleBase(StreamTransformer):
|
|
374
|
+
"""Shared bookkeeping for `tasks`-event-driven lifecycle inference.
|
|
375
|
+
|
|
376
|
+
Both `LifecycleTransformer` (wire-serializable channel) and
|
|
377
|
+
`SubgraphTransformer` (in-process navigation handles) discover
|
|
378
|
+
subgraphs by watching the same `tasks` stream — `started` on the
|
|
379
|
+
first event at a tracked namespace, terminal status when the
|
|
380
|
+
parent's `TaskResultPayload` arrives. Centralizing the dispatch
|
|
381
|
+
+ open-set bookkeeping here keeps the inference rules from
|
|
382
|
+
drifting between the two surfaces.
|
|
383
|
+
|
|
384
|
+
Subclasses provide three template-method hooks:
|
|
385
|
+
|
|
386
|
+
- `_should_track(ns)` — scope filter (e.g. multi-depth vs
|
|
387
|
+
direct-children-only).
|
|
388
|
+
- `_on_started(ns, graph_name, trigger_call_id)` — first sighting
|
|
389
|
+
action (push payload / build handle / etc.). Called once per
|
|
390
|
+
discovered namespace.
|
|
391
|
+
- `_on_terminal(ns, status, error)` — terminal action (push
|
|
392
|
+
terminal payload / mark handle status). Called once per
|
|
393
|
+
tracked namespace at result time, or via `finalize` / `fail`
|
|
394
|
+
sweeps if no parent result arrived.
|
|
395
|
+
|
|
396
|
+
Tasks events are suppressed from the main event log (`process`
|
|
397
|
+
returns False) — they're folded into whichever projection the
|
|
398
|
+
subclass populates; consumers iterating the raw protocol stream
|
|
399
|
+
see the higher-level view.
|
|
400
|
+
"""
|
|
401
|
+
|
|
402
|
+
required_stream_modes = ("tasks",)
|
|
403
|
+
|
|
404
|
+
def __init__(self, scope: tuple[str, ...] = ()) -> None:
|
|
405
|
+
super().__init__(scope)
|
|
406
|
+
self._seen: set[tuple[str, ...]] = set()
|
|
407
|
+
# Maps tracked namespace -> task_id of the parent task whose
|
|
408
|
+
# `TaskResultPayload` will close it.
|
|
409
|
+
self._open: dict[tuple[str, ...], str] = {}
|
|
410
|
+
# lc_agent_name observed at each namespace (first task event wins).
|
|
411
|
+
# Not read by the base discriminator (which only checks whether the
|
|
412
|
+
# current task carries an lc_agent_name); maintained as extension state
|
|
413
|
+
# for subclasses that project named subagents — e.g. a `run.subagents`
|
|
414
|
+
# transformer reads this to filter to nested runs that have a name.
|
|
415
|
+
self._lc_by_ns: dict[tuple[str, ...], str | None] = {}
|
|
416
|
+
# Pregel task_id -> triggering LLM tool_call_id, harvested from a task
|
|
417
|
+
# whose `input` is a `tool_call_with_context` dict (current shape) or a
|
|
418
|
+
# list of tool-call dicts (legacy shape). The child subgraph's segment
|
|
419
|
+
# `node:<task_id>` shares this task_id, so a subagent recovers the tool
|
|
420
|
+
# call that spawned it (cross-payload).
|
|
421
|
+
self._pending_tool_calls: dict[str, str] = {}
|
|
422
|
+
# `cause` for the current `_on_started` dispatch, set immediately before
|
|
423
|
+
# the call and read by overrides that surface it. Keeping `cause` off the
|
|
424
|
+
# `_on_started` signature means overrides predating it (e.g. deepagents'
|
|
425
|
+
# `SubagentTransformer`) don't break.
|
|
426
|
+
self._pending_cause: LifecycleCause | None = None
|
|
427
|
+
|
|
428
|
+
# --- Template-method hooks (subclass overrides) ---
|
|
429
|
+
|
|
430
|
+
def _should_track(self, ns: tuple[str, ...]) -> bool:
|
|
431
|
+
"""Scope filter — return True iff `ns` is in this transformer's region."""
|
|
432
|
+
raise NotImplementedError
|
|
433
|
+
|
|
434
|
+
def _on_started(
|
|
435
|
+
self,
|
|
436
|
+
ns: tuple[str, ...],
|
|
437
|
+
graph_name: str | None,
|
|
438
|
+
trigger_call_id: str | None,
|
|
439
|
+
) -> None:
|
|
440
|
+
"""Fired once per discovered namespace (first observed task event).
|
|
441
|
+
|
|
442
|
+
The triggering `cause` (if any) is available as `self._pending_cause`.
|
|
443
|
+
"""
|
|
444
|
+
raise NotImplementedError
|
|
445
|
+
|
|
446
|
+
def _on_terminal(
|
|
447
|
+
self,
|
|
448
|
+
ns: tuple[str, ...],
|
|
449
|
+
status: SubgraphStatus,
|
|
450
|
+
error: str | None,
|
|
451
|
+
) -> None:
|
|
452
|
+
"""Fired once per tracked namespace when its parent's result arrives,
|
|
453
|
+
or via finalize/fail safety-net sweeps.
|
|
454
|
+
"""
|
|
455
|
+
raise NotImplementedError
|
|
456
|
+
|
|
457
|
+
# --- Dispatch + bookkeeping (shared) ---
|
|
458
|
+
|
|
459
|
+
def process(self, event: ProtocolEvent) -> bool:
|
|
460
|
+
if event["method"] != "tasks":
|
|
461
|
+
return True
|
|
462
|
+
ns = tuple(event["params"]["namespace"])
|
|
463
|
+
data = event["params"]["data"]
|
|
464
|
+
if "result" in data:
|
|
465
|
+
self._handle_task_result(ns, data)
|
|
466
|
+
else:
|
|
467
|
+
self._record_identity(ns, data)
|
|
468
|
+
self._record_pending_tool_calls(data)
|
|
469
|
+
self._handle_task_start(ns, data)
|
|
470
|
+
# Tasks events are folded into the synthesized projections;
|
|
471
|
+
# suppress from the main event log so iterators don't double-see
|
|
472
|
+
# the same information in two shapes.
|
|
473
|
+
return False
|
|
474
|
+
|
|
475
|
+
def _record_identity(self, ns: tuple[str, ...], data: dict[str, Any]) -> None:
|
|
476
|
+
"""Record this namespace's `lc_agent_name` (first task event wins).
|
|
477
|
+
|
|
478
|
+
Runs for every task-start event, including `ns == self.scope` and
|
|
479
|
+
tracked children. Pregel emits parent-namespace tasks before
|
|
480
|
+
child-namespace tasks, so under that ordering the parent's identity is
|
|
481
|
+
recorded by the time a child event is evaluated in `_handle_task_start`.
|
|
482
|
+
"""
|
|
483
|
+
if ns in self._lc_by_ns:
|
|
484
|
+
return
|
|
485
|
+
metadata = data.get("metadata") or {}
|
|
486
|
+
self._lc_by_ns[ns] = metadata.get("lc_agent_name")
|
|
487
|
+
|
|
488
|
+
def _record_pending_tool_calls(self, data: dict[str, Any]) -> None:
|
|
489
|
+
"""Harvest a task's triggering tool_call_id keyed by its task id.
|
|
490
|
+
|
|
491
|
+
A tool-dispatch task seeds `task_id -> tool_call_id`; the spawned
|
|
492
|
+
subgraph's namespace segment `node:<task_id>` shares that id, letting
|
|
493
|
+
a subagent recover the tool call that caused it across payloads. Two
|
|
494
|
+
input shapes are handled: the current Pregel push model schedules each
|
|
495
|
+
tool call as its own task whose `input` is a `tool_call_with_context`
|
|
496
|
+
dict, while a legacy / batched model passes a list of tool-call dicts.
|
|
497
|
+
"""
|
|
498
|
+
task_id = data.get("id")
|
|
499
|
+
if not isinstance(task_id, str):
|
|
500
|
+
return
|
|
501
|
+
payload = data.get("input")
|
|
502
|
+
tool_call_id: str | None = None
|
|
503
|
+
# Current coco_graph schedules each tool call as its own push task
|
|
504
|
+
# whose input is a `tool_call_with_context` dict.
|
|
505
|
+
if isinstance(payload, dict) and isinstance(payload.get("tool_call"), dict):
|
|
506
|
+
candidate = payload["tool_call"].get("id")
|
|
507
|
+
if isinstance(candidate, str):
|
|
508
|
+
tool_call_id = candidate
|
|
509
|
+
# Legacy / batched shape: input is a list of tool-call dicts.
|
|
510
|
+
elif isinstance(payload, list):
|
|
511
|
+
for tc in payload:
|
|
512
|
+
if isinstance(tc, dict) and isinstance(tc.get("id"), str):
|
|
513
|
+
tool_call_id = tc["id"] # first wins
|
|
514
|
+
break
|
|
515
|
+
if tool_call_id is not None:
|
|
516
|
+
self._pending_tool_calls[task_id] = tool_call_id
|
|
517
|
+
|
|
518
|
+
def _handle_task_start(self, ns: tuple[str, ...], data: dict[str, Any]) -> None:
|
|
519
|
+
if not self._should_track(ns) or ns in self._seen:
|
|
520
|
+
return
|
|
521
|
+
self._seen.add(ns)
|
|
522
|
+
parsed_name, trigger_call_id = _parse_ns_segment(ns[-1])
|
|
523
|
+
metadata = data.get("metadata") or {}
|
|
524
|
+
child_lc = metadata.get("lc_agent_name")
|
|
525
|
+
# A subagent boundary is any nested run carrying an lc_agent_name (set
|
|
526
|
+
# by create_agent). Unnamed runs (lc_agent_name None) are excluded.
|
|
527
|
+
#
|
|
528
|
+
# A same-named nested agent — e.g. a subagent that invokes itself — is
|
|
529
|
+
# surfaced because it re-asserts its own lc_agent_name. The trade-off:
|
|
530
|
+
# a non-agent subgraph invoked inside a tool inherits the parent's
|
|
531
|
+
# lc_agent_name and will also surface (named after the parent). A caller
|
|
532
|
+
# that needs to exclude such a graph can null lc_agent_name in the
|
|
533
|
+
# config it invokes that graph with.
|
|
534
|
+
is_subagent = child_lc is not None
|
|
535
|
+
graph_name = child_lc if is_subagent else (parsed_name or None)
|
|
536
|
+
cause: LifecycleCause | None = None
|
|
537
|
+
if is_subagent and trigger_call_id is not None:
|
|
538
|
+
tool_call_id = self._pending_tool_calls.get(trigger_call_id)
|
|
539
|
+
if tool_call_id:
|
|
540
|
+
cause = {"type": "toolCall", "tool_call_id": str(tool_call_id)}
|
|
541
|
+
# Deliver `cause` via instance state, not the call signature, so
|
|
542
|
+
# `_on_started` stays backward-compatible with overrides predating it.
|
|
543
|
+
self._pending_cause = cause
|
|
544
|
+
self._on_started(ns, graph_name, trigger_call_id)
|
|
545
|
+
if trigger_call_id is not None:
|
|
546
|
+
self._open[ns] = trigger_call_id
|
|
547
|
+
|
|
548
|
+
def _pop_terminal_transitions(
|
|
549
|
+
self, ns: tuple[str, ...], data: dict[str, Any]
|
|
550
|
+
) -> list[tuple[tuple[str, ...], SubgraphStatus, str | None]]:
|
|
551
|
+
"""Return and remove tracked children closed by this task result."""
|
|
552
|
+
result_id = data.get("id")
|
|
553
|
+
if not result_id:
|
|
554
|
+
return []
|
|
555
|
+
transitions: list[tuple[tuple[str, ...], SubgraphStatus, str | None]] = []
|
|
556
|
+
for child_ns, parent_task_id in list(self._open.items()):
|
|
557
|
+
if child_ns[:-1] != ns or parent_task_id != result_id:
|
|
558
|
+
continue
|
|
559
|
+
status, error = _terminal_from_result(data)
|
|
560
|
+
transitions.append((child_ns, status, error))
|
|
561
|
+
del self._open[child_ns]
|
|
562
|
+
return transitions
|
|
563
|
+
|
|
564
|
+
def _handle_task_result(self, ns: tuple[str, ...], data: dict[str, Any]) -> None:
|
|
565
|
+
for child_ns, status, error in self._pop_terminal_transitions(ns, data):
|
|
566
|
+
self._on_terminal(child_ns, status, error)
|
|
567
|
+
|
|
568
|
+
def finalize(self) -> None:
|
|
569
|
+
"""Emit `completed` for any tracked namespace still open at run end."""
|
|
570
|
+
for ns in list(self._open):
|
|
571
|
+
self._on_terminal(ns, "completed", None)
|
|
572
|
+
self._open.clear()
|
|
573
|
+
|
|
574
|
+
def fail(self, err: BaseException) -> None:
|
|
575
|
+
"""Emit terminal status for any tracked namespace still open."""
|
|
576
|
+
status, error_str = _status_from_exception(err)
|
|
577
|
+
for ns in list(self._open):
|
|
578
|
+
self._on_terminal(ns, status, error_str)
|
|
579
|
+
self._open.clear()
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _status_from_exception(err: BaseException) -> tuple[SubgraphStatus, str | None]:
|
|
583
|
+
"""Map a run exception to a subgraph terminal status and error string."""
|
|
584
|
+
if isinstance(err, GraphDrained):
|
|
585
|
+
return "drained", None
|
|
586
|
+
if isinstance(err, GraphInterrupt):
|
|
587
|
+
return "interrupted", None
|
|
588
|
+
return "failed", str(err)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _terminal_from_result(
|
|
592
|
+
payload: dict[str, Any],
|
|
593
|
+
) -> tuple[SubgraphStatus, str | None]:
|
|
594
|
+
"""Map a `TaskResultPayload` to a `(status, error)` pair.
|
|
595
|
+
|
|
596
|
+
Order matters: a result with both `error` and `interrupts` prefers
|
|
597
|
+
the interrupt classification, since `GraphInterrupt` manifests as
|
|
598
|
+
a populated `interrupts` list, not as `error`.
|
|
599
|
+
"""
|
|
600
|
+
if payload.get("interrupts"):
|
|
601
|
+
return "interrupted", None
|
|
602
|
+
error = payload.get("error")
|
|
603
|
+
if error:
|
|
604
|
+
return "failed", str(error)
|
|
605
|
+
return "completed", None
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
class LifecycleTransformer(_TasksLifecycleBase):
|
|
609
|
+
"""Surface subgraph lifecycle as `lifecycle` protocol events.
|
|
610
|
+
|
|
611
|
+
Pushes `LifecyclePayload` to a `StreamChannel` named `lifecycle`.
|
|
612
|
+
The channel is auto-forwarded by the mux so payloads land in the
|
|
613
|
+
main event log under `method = "lifecycle"` (native transformer —
|
|
614
|
+
no `custom:` prefix) — visible to remote SDK clients over the
|
|
615
|
+
wire and to in-process consumers via `run.lifecycle`.
|
|
616
|
+
|
|
617
|
+
Tracks subgraphs at every depth strictly below the transformer's
|
|
618
|
+
scope, so a graph → subgraph → subgraph chain produces lifecycle
|
|
619
|
+
events for both nested levels in a flat stream.
|
|
620
|
+
|
|
621
|
+
Native transformer — projection key `lifecycle` is exposed as
|
|
622
|
+
`run.lifecycle`.
|
|
623
|
+
"""
|
|
624
|
+
|
|
625
|
+
_native = True
|
|
626
|
+
|
|
627
|
+
def __init__(self, scope: tuple[str, ...] = ()) -> None:
|
|
628
|
+
super().__init__(scope)
|
|
629
|
+
self._channel: StreamChannel[LifecyclePayload] = StreamChannel("lifecycle")
|
|
630
|
+
|
|
631
|
+
def init(self) -> dict[str, Any]:
|
|
632
|
+
return {"lifecycle": self._channel}
|
|
633
|
+
|
|
634
|
+
def _should_track(self, ns: tuple[str, ...]) -> bool:
|
|
635
|
+
depth = len(self.scope)
|
|
636
|
+
return len(ns) > depth and ns[:depth] == self.scope
|
|
637
|
+
|
|
638
|
+
def _on_started(
|
|
639
|
+
self,
|
|
640
|
+
ns: tuple[str, ...],
|
|
641
|
+
graph_name: str | None,
|
|
642
|
+
trigger_call_id: str | None,
|
|
643
|
+
) -> None:
|
|
644
|
+
if trigger_call_id is None:
|
|
645
|
+
# Without a task id we can't correlate a parent-result
|
|
646
|
+
# event back to this namespace — skip the started payload
|
|
647
|
+
# and rely on finalize/fail to close.
|
|
648
|
+
return
|
|
649
|
+
payload: LifecyclePayload = {"event": "started", "namespace": list(ns)}
|
|
650
|
+
if graph_name:
|
|
651
|
+
payload["graph_name"] = graph_name
|
|
652
|
+
payload["trigger_call_id"] = trigger_call_id
|
|
653
|
+
cause = self._pending_cause
|
|
654
|
+
if cause is not None:
|
|
655
|
+
payload["cause"] = cause
|
|
656
|
+
self._channel.push(payload)
|
|
657
|
+
|
|
658
|
+
def _on_terminal(
|
|
659
|
+
self,
|
|
660
|
+
ns: tuple[str, ...],
|
|
661
|
+
status: SubgraphStatus,
|
|
662
|
+
error: str | None,
|
|
663
|
+
) -> None:
|
|
664
|
+
payload: LifecyclePayload = {"event": status, "namespace": list(ns)}
|
|
665
|
+
if error is not None:
|
|
666
|
+
payload["error"] = error
|
|
667
|
+
self._channel.push(payload)
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
class SubgraphTransformer(_TasksLifecycleBase):
|
|
671
|
+
"""Discover subgraph invocations as in-process navigation handles.
|
|
672
|
+
|
|
673
|
+
Per discovered direct-child subgraph, builds a `SubgraphRunStream`
|
|
674
|
+
(or `AsyncSubgraphRunStream`) wrapping a child mini-mux scoped to
|
|
675
|
+
the subgraph's namespace. Consumers iterate `run.subgraphs` to
|
|
676
|
+
receive handles, then drill into `handle.values` / `handle.messages`
|
|
677
|
+
/ `handle.subgraphs` (recursive grandchildren) / `handle.lifecycle`.
|
|
678
|
+
|
|
679
|
+
Each mini-mux owns its own scope and uses its own
|
|
680
|
+
`SubgraphTransformer` to discover its direct children, so
|
|
681
|
+
grandchildren live on the child handle — never on the root's
|
|
682
|
+
`subgraphs` log. Forwarding events into the matching child mini-mux
|
|
683
|
+
is what keeps the child's projections populated.
|
|
684
|
+
|
|
685
|
+
Native transformer — `subgraphs` is exposed as `run.subgraphs`.
|
|
686
|
+
"""
|
|
687
|
+
|
|
688
|
+
_native = True
|
|
689
|
+
supports_sync = True
|
|
690
|
+
|
|
691
|
+
def __init__(self, scope: tuple[str, ...] = ()) -> None:
|
|
692
|
+
super().__init__(scope)
|
|
693
|
+
self._log: StreamChannel[SubgraphRunStream | AsyncSubgraphRunStream] = (
|
|
694
|
+
StreamChannel()
|
|
695
|
+
)
|
|
696
|
+
self._handles: dict[
|
|
697
|
+
tuple[str, ...], SubgraphRunStream | AsyncSubgraphRunStream
|
|
698
|
+
] = {}
|
|
699
|
+
self._mux: StreamMux | None = None
|
|
700
|
+
|
|
701
|
+
def init(self) -> dict[str, Any]:
|
|
702
|
+
return {"subgraphs": self._log}
|
|
703
|
+
|
|
704
|
+
def _on_register(self, mux: Any) -> None:
|
|
705
|
+
self._mux = mux
|
|
706
|
+
|
|
707
|
+
def _should_track(self, ns: tuple[str, ...]) -> bool:
|
|
708
|
+
# Direct children only — grandchildren are picked up by the
|
|
709
|
+
# child mini-mux's own SubgraphTransformer.
|
|
710
|
+
depth = len(self.scope)
|
|
711
|
+
return len(ns) == depth + 1 and ns[:depth] == self.scope
|
|
712
|
+
|
|
713
|
+
def _on_started(
|
|
714
|
+
self,
|
|
715
|
+
ns: tuple[str, ...],
|
|
716
|
+
graph_name: str | None,
|
|
717
|
+
trigger_call_id: str | None,
|
|
718
|
+
) -> None:
|
|
719
|
+
if self._mux is None:
|
|
720
|
+
return
|
|
721
|
+
try:
|
|
722
|
+
child_mux = self._mux._make_child(ns)
|
|
723
|
+
except RuntimeError:
|
|
724
|
+
return
|
|
725
|
+
handle_cls = AsyncSubgraphRunStream if child_mux.is_async else SubgraphRunStream
|
|
726
|
+
# The triggering `cause` (on `self._pending_cause`) is a wire/lifecycle
|
|
727
|
+
# concern carried on `LifecyclePayload`; the in-process subgraph
|
|
728
|
+
# navigation handle does not expose it.
|
|
729
|
+
handle = handle_cls(
|
|
730
|
+
mux=child_mux,
|
|
731
|
+
path=ns,
|
|
732
|
+
graph_name=graph_name,
|
|
733
|
+
trigger_call_id=trigger_call_id,
|
|
734
|
+
)
|
|
735
|
+
self._handles[ns] = handle
|
|
736
|
+
self._log.push(handle)
|
|
737
|
+
|
|
738
|
+
def _on_terminal(
|
|
739
|
+
self,
|
|
740
|
+
ns: tuple[str, ...],
|
|
741
|
+
status: SubgraphStatus,
|
|
742
|
+
error: str | None,
|
|
743
|
+
) -> None:
|
|
744
|
+
handle = self._handles.get(ns)
|
|
745
|
+
if handle is None or not self._mark_terminal(handle, status, error):
|
|
746
|
+
return
|
|
747
|
+
self._close_or_fail_handle(handle, status, error)
|
|
748
|
+
|
|
749
|
+
async def _aon_terminal(
|
|
750
|
+
self,
|
|
751
|
+
ns: tuple[str, ...],
|
|
752
|
+
status: SubgraphStatus,
|
|
753
|
+
error: str | None,
|
|
754
|
+
) -> None:
|
|
755
|
+
handle = self._handles.get(ns)
|
|
756
|
+
if handle is None or not self._mark_terminal(handle, status, error):
|
|
757
|
+
return
|
|
758
|
+
await self._aclose_or_fail_handle(handle, status, error)
|
|
759
|
+
|
|
760
|
+
def _mark_terminal(
|
|
761
|
+
self,
|
|
762
|
+
handle: SubgraphRunStream | AsyncSubgraphRunStream,
|
|
763
|
+
status: SubgraphStatus,
|
|
764
|
+
error: str | None,
|
|
765
|
+
) -> bool:
|
|
766
|
+
"""Mark a handle terminal once. Returns True on first transition."""
|
|
767
|
+
if handle._seen_terminal:
|
|
768
|
+
return False
|
|
769
|
+
handle.status = status
|
|
770
|
+
if error is not None and handle.error is None:
|
|
771
|
+
handle.error = error
|
|
772
|
+
handle._seen_terminal = True
|
|
773
|
+
return True
|
|
774
|
+
|
|
775
|
+
def _close_or_fail_handle(
|
|
776
|
+
self,
|
|
777
|
+
handle: SubgraphRunStream | AsyncSubgraphRunStream,
|
|
778
|
+
status: SubgraphStatus,
|
|
779
|
+
error: str | None,
|
|
780
|
+
) -> None:
|
|
781
|
+
if handle._mux is None or handle._mux._events._closed:
|
|
782
|
+
return
|
|
783
|
+
if status == "failed":
|
|
784
|
+
handle._mux.fail(RuntimeError(error or "Subgraph failed"))
|
|
785
|
+
else:
|
|
786
|
+
handle._mux.close()
|
|
787
|
+
|
|
788
|
+
async def _aclose_or_fail_handle(
|
|
789
|
+
self,
|
|
790
|
+
handle: SubgraphRunStream | AsyncSubgraphRunStream,
|
|
791
|
+
status: SubgraphStatus,
|
|
792
|
+
error: str | None,
|
|
793
|
+
) -> None:
|
|
794
|
+
if handle._mux is None or handle._mux._events._closed:
|
|
795
|
+
return
|
|
796
|
+
if status == "failed":
|
|
797
|
+
await handle._mux.afail(RuntimeError(error or "Subgraph failed"))
|
|
798
|
+
else:
|
|
799
|
+
await handle._mux.aclose()
|
|
800
|
+
|
|
801
|
+
def _handle_for_event(
|
|
802
|
+
self, event: ProtocolEvent
|
|
803
|
+
) -> SubgraphRunStream | AsyncSubgraphRunStream | None:
|
|
804
|
+
ns = tuple(event["params"]["namespace"])
|
|
805
|
+
depth = len(self.scope)
|
|
806
|
+
if len(ns) < depth + 1:
|
|
807
|
+
return None
|
|
808
|
+
handle = self._handles.get(ns[: depth + 1])
|
|
809
|
+
if handle is None or handle._mux is None or handle._mux._events._closed:
|
|
810
|
+
return None
|
|
811
|
+
return handle
|
|
812
|
+
|
|
813
|
+
def process(self, event: ProtocolEvent) -> bool:
|
|
814
|
+
# Run tasks bookkeeping first so a `started` handle exists
|
|
815
|
+
# by the time we forward the event to the child mini-mux.
|
|
816
|
+
keep = super().process(event)
|
|
817
|
+
handle = self._handle_for_event(event)
|
|
818
|
+
if handle is not None:
|
|
819
|
+
handle._observe_event(event)
|
|
820
|
+
handle._mux.push(event)
|
|
821
|
+
return keep
|
|
822
|
+
|
|
823
|
+
async def aprocess(self, event: ProtocolEvent) -> bool:
|
|
824
|
+
# Async counterpart: repeats the tasks bookkeeping here so
|
|
825
|
+
# child mini-muxes receive events through their async lane.
|
|
826
|
+
if event["method"] == "tasks":
|
|
827
|
+
ns = tuple(event["params"]["namespace"])
|
|
828
|
+
data = event["params"]["data"]
|
|
829
|
+
if "result" in data:
|
|
830
|
+
for child_ns, status, error in self._pop_terminal_transitions(ns, data):
|
|
831
|
+
await self._aon_terminal(child_ns, status, error)
|
|
832
|
+
else:
|
|
833
|
+
# Mirror the sync `process` bookkeeping so the async lane
|
|
834
|
+
# observes parent identity / tool calls before discriminating
|
|
835
|
+
# a subagent boundary.
|
|
836
|
+
self._record_identity(ns, data)
|
|
837
|
+
self._record_pending_tool_calls(data)
|
|
838
|
+
self._handle_task_start(ns, data)
|
|
839
|
+
keep = False
|
|
840
|
+
else:
|
|
841
|
+
keep = True
|
|
842
|
+
handle = self._handle_for_event(event)
|
|
843
|
+
if handle is not None:
|
|
844
|
+
handle._observe_event(event)
|
|
845
|
+
await handle._mux.apush(event)
|
|
846
|
+
return keep
|
|
847
|
+
|
|
848
|
+
def _complete_open_handles(self) -> BaseException | None:
|
|
849
|
+
first_error: BaseException | None = None
|
|
850
|
+
for ns in list(self._open):
|
|
851
|
+
try:
|
|
852
|
+
self._on_terminal(ns, "completed", None)
|
|
853
|
+
except BaseException as e:
|
|
854
|
+
if first_error is None:
|
|
855
|
+
first_error = e
|
|
856
|
+
self._open.clear()
|
|
857
|
+
for handle in self._handles.values():
|
|
858
|
+
if self._mark_terminal(handle, "completed", None):
|
|
859
|
+
try:
|
|
860
|
+
self._close_or_fail_handle(handle, "completed", None)
|
|
861
|
+
except BaseException as e:
|
|
862
|
+
if first_error is None:
|
|
863
|
+
first_error = e
|
|
864
|
+
return first_error
|
|
865
|
+
|
|
866
|
+
async def _acomplete_open_handles(self) -> BaseException | None:
|
|
867
|
+
first_error: BaseException | None = None
|
|
868
|
+
for ns in list(self._open):
|
|
869
|
+
try:
|
|
870
|
+
await self._aon_terminal(ns, "completed", None)
|
|
871
|
+
except BaseException as e:
|
|
872
|
+
if first_error is None:
|
|
873
|
+
first_error = e
|
|
874
|
+
self._open.clear()
|
|
875
|
+
for handle in self._handles.values():
|
|
876
|
+
if self._mark_terminal(handle, "completed", None):
|
|
877
|
+
try:
|
|
878
|
+
await self._aclose_or_fail_handle(handle, "completed", None)
|
|
879
|
+
except BaseException as e:
|
|
880
|
+
if first_error is None:
|
|
881
|
+
first_error = e
|
|
882
|
+
return first_error
|
|
883
|
+
|
|
884
|
+
def finalize(self) -> None:
|
|
885
|
+
first_error = self._complete_open_handles()
|
|
886
|
+
if first_error is not None:
|
|
887
|
+
raise first_error
|
|
888
|
+
|
|
889
|
+
async def afinalize(self) -> None:
|
|
890
|
+
first_error = await self._acomplete_open_handles()
|
|
891
|
+
if first_error is not None:
|
|
892
|
+
raise first_error
|
|
893
|
+
|
|
894
|
+
def fail(self, err: BaseException) -> None:
|
|
895
|
+
status, error_str = _status_from_exception(err)
|
|
896
|
+
self._open.clear()
|
|
897
|
+
for handle in self._handles.values():
|
|
898
|
+
self._mark_terminal(handle, status, error_str)
|
|
899
|
+
if handle._mux is not None and not handle._mux._events._closed:
|
|
900
|
+
try:
|
|
901
|
+
handle._mux.fail(err)
|
|
902
|
+
except Exception:
|
|
903
|
+
_logger.warning(
|
|
904
|
+
"Error failing subgraph mini-mux at %s; "
|
|
905
|
+
"subscribers may not see the terminal error.",
|
|
906
|
+
handle.path,
|
|
907
|
+
exc_info=True,
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
async def afail(self, err: BaseException) -> None:
|
|
911
|
+
status, error_str = _status_from_exception(err)
|
|
912
|
+
self._open.clear()
|
|
913
|
+
for handle in self._handles.values():
|
|
914
|
+
self._mark_terminal(handle, status, error_str)
|
|
915
|
+
if handle._mux is not None and not handle._mux._events._closed:
|
|
916
|
+
try:
|
|
917
|
+
await handle._mux.afail(err)
|
|
918
|
+
except Exception:
|
|
919
|
+
_logger.warning(
|
|
920
|
+
"Error failing subgraph mini-mux at %s; "
|
|
921
|
+
"subscribers may not see the terminal error.",
|
|
922
|
+
handle.path,
|
|
923
|
+
exc_info=True,
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
class CheckpointsTransformer(StreamTransformer):
|
|
928
|
+
"""Capture checkpoint events as a drainable stream.
|
|
929
|
+
|
|
930
|
+
Surfaces `stream_mode="checkpoints"` data on `run.checkpoints` as
|
|
931
|
+
a `StreamChannel[dict[str, Any]]`. Each item is in the same format
|
|
932
|
+
as returned by `get_state()`.
|
|
933
|
+
|
|
934
|
+
Checkpoint events are only emitted when a checkpointer is configured
|
|
935
|
+
on the graph. When no checkpointer is present, the projection exists
|
|
936
|
+
but receives no events.
|
|
937
|
+
|
|
938
|
+
Only events at the run's own scope are captured; checkpoint data from
|
|
939
|
+
deeper subgraphs is available on the respective subgraph handle's
|
|
940
|
+
`.checkpoints` projection.
|
|
941
|
+
|
|
942
|
+
Native transformer — `run.checkpoints` is a direct attribute.
|
|
943
|
+
"""
|
|
944
|
+
|
|
945
|
+
_native = True
|
|
946
|
+
required_stream_modes = ("checkpoints",)
|
|
947
|
+
|
|
948
|
+
def __init__(self, scope: tuple[str, ...] = ()) -> None:
|
|
949
|
+
super().__init__(scope)
|
|
950
|
+
self._log: StreamChannel[dict[str, Any]] = StreamChannel()
|
|
951
|
+
self._scope_list: list[str] = list(scope)
|
|
952
|
+
|
|
953
|
+
def init(self) -> dict[str, Any]:
|
|
954
|
+
return {"checkpoints": self._log}
|
|
955
|
+
|
|
956
|
+
def process(self, event: ProtocolEvent) -> bool:
|
|
957
|
+
if event["method"] != "checkpoints":
|
|
958
|
+
return True
|
|
959
|
+
params = event["params"]
|
|
960
|
+
if params["namespace"] != self._scope_list:
|
|
961
|
+
return True
|
|
962
|
+
self._log.push(params["data"])
|
|
963
|
+
return True
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
class DebugTransformer(StreamTransformer):
|
|
967
|
+
"""Capture debug events as a drainable stream.
|
|
968
|
+
|
|
969
|
+
Surfaces `stream_mode="debug"` data on `run.debug` as a
|
|
970
|
+
`StreamChannel[dict[str, Any]]`. Each item is a debug event with
|
|
971
|
+
step-level detail (checkpoint snapshots, task payloads, and
|
|
972
|
+
task results wrapped with step number and timestamp).
|
|
973
|
+
|
|
974
|
+
Only events at the run's own scope are captured; debug data from
|
|
975
|
+
deeper subgraphs is available on the respective subgraph handle's
|
|
976
|
+
`.debug` projection.
|
|
977
|
+
|
|
978
|
+
Native transformer — `run.debug` is a direct attribute.
|
|
979
|
+
"""
|
|
980
|
+
|
|
981
|
+
_native = True
|
|
982
|
+
required_stream_modes = ("debug",)
|
|
983
|
+
|
|
984
|
+
def __init__(self, scope: tuple[str, ...] = ()) -> None:
|
|
985
|
+
super().__init__(scope)
|
|
986
|
+
self._log: StreamChannel[dict[str, Any]] = StreamChannel()
|
|
987
|
+
self._scope_list: list[str] = list(scope)
|
|
988
|
+
|
|
989
|
+
def init(self) -> dict[str, Any]:
|
|
990
|
+
return {"debug": self._log}
|
|
991
|
+
|
|
992
|
+
def process(self, event: ProtocolEvent) -> bool:
|
|
993
|
+
if event["method"] != "debug":
|
|
994
|
+
return True
|
|
995
|
+
params = event["params"]
|
|
996
|
+
if params["namespace"] != self._scope_list:
|
|
997
|
+
return True
|
|
998
|
+
self._log.push(params["data"])
|
|
999
|
+
return True
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
class TasksTransformer(StreamTransformer):
|
|
1003
|
+
"""Capture raw task events as a drainable stream.
|
|
1004
|
+
|
|
1005
|
+
Surfaces `stream_mode="tasks"` data on `run.tasks` as a
|
|
1006
|
+
`StreamChannel[dict[str, Any]]`. Each item is a task payload
|
|
1007
|
+
(start or result).
|
|
1008
|
+
|
|
1009
|
+
`LifecycleTransformer` and `SubgraphTransformer` also consume
|
|
1010
|
+
`tasks` events for subgraph discovery and lifecycle tracking.
|
|
1011
|
+
This transformer captures the raw payloads independently for
|
|
1012
|
+
consumers who need task-level detail.
|
|
1013
|
+
|
|
1014
|
+
Only events at the run's own scope are captured; task data from
|
|
1015
|
+
deeper subgraphs is available on the respective subgraph handle's
|
|
1016
|
+
`.tasks` projection.
|
|
1017
|
+
|
|
1018
|
+
Native transformer — `run.tasks` is a direct attribute.
|
|
1019
|
+
"""
|
|
1020
|
+
|
|
1021
|
+
_native = True
|
|
1022
|
+
required_stream_modes = ("tasks",)
|
|
1023
|
+
|
|
1024
|
+
def __init__(self, scope: tuple[str, ...] = ()) -> None:
|
|
1025
|
+
super().__init__(scope)
|
|
1026
|
+
self._log: StreamChannel[dict[str, Any]] = StreamChannel()
|
|
1027
|
+
self._scope_list: list[str] = list(scope)
|
|
1028
|
+
|
|
1029
|
+
def init(self) -> dict[str, Any]:
|
|
1030
|
+
return {"tasks": self._log}
|
|
1031
|
+
|
|
1032
|
+
def process(self, event: ProtocolEvent) -> bool:
|
|
1033
|
+
if event["method"] != "tasks":
|
|
1034
|
+
return True
|
|
1035
|
+
params = event["params"]
|
|
1036
|
+
if params["namespace"] != self._scope_list:
|
|
1037
|
+
return True
|
|
1038
|
+
self._log.push(params["data"])
|
|
1039
|
+
return True
|