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,108 @@
|
|
|
1
|
+
"""Tag and filter middleware-internal model calls.
|
|
2
|
+
|
|
3
|
+
Tag internal calls with `internal_call_metadata()` and declare
|
|
4
|
+
`transformers = (InternalCallTransformer,)` on the middleware class to keep
|
|
5
|
+
them out of `run.messages`. Both APIs are public for third-party middleware.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import secrets
|
|
11
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
12
|
+
|
|
13
|
+
from coco_core.messages import BaseMessage
|
|
14
|
+
from coco_graph.stream import StreamTransformer
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from coco_graph.stream._types import ProtocolEvent
|
|
18
|
+
|
|
19
|
+
INTERNAL_CALL_METADATA_KEY = "lc_internal_call"
|
|
20
|
+
"""Metadata key marking a model call as internal to middleware.
|
|
21
|
+
|
|
22
|
+
Kept separate from `lc_source` so filtering doesn't affect its existing
|
|
23
|
+
semantics.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
_INTERNAL_CALL_TOKEN = secrets.token_hex(16)
|
|
27
|
+
"""Process-local marker used to prevent callers from spoofing internal calls.
|
|
28
|
+
|
|
29
|
+
A random token prevents user-supplied metadata from hiding real model calls
|
|
30
|
+
from `run.messages`.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def internal_call_metadata() -> dict[str, Any]:
|
|
35
|
+
"""Return metadata that marks a model call as internal to middleware.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
A mapping to merge into a model call's `config["metadata"]`.
|
|
39
|
+
"""
|
|
40
|
+
return {INTERNAL_CALL_METADATA_KEY: _INTERNAL_CALL_TOKEN}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class InternalCallTransformer(StreamTransformer):
|
|
44
|
+
"""Keep internal model calls out of `run.messages` and the raw event log.
|
|
45
|
+
|
|
46
|
+
Used by middleware that makes internal model calls and runs before built-in
|
|
47
|
+
transformers.
|
|
48
|
+
|
|
49
|
+
For tagged events, streamed `message-start` events are marked as tool-role and
|
|
50
|
+
whole-`AIMessage` payloads are cleared so `MessagesTransformer` ignores them.
|
|
51
|
+
The mutated events are then dropped from the raw log.
|
|
52
|
+
|
|
53
|
+
Only events within this transformer's scope are modified.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
before_builtins: ClassVar[bool] = True
|
|
57
|
+
required_stream_modes: ClassVar[tuple[str, ...]] = ("messages",)
|
|
58
|
+
|
|
59
|
+
def __init__(self, scope: tuple[str, ...] = ()) -> None:
|
|
60
|
+
"""Initialize the transformer, scoped to a single mux/namespace.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
scope: The namespace tuple the owning mux is scoped to.
|
|
64
|
+
"""
|
|
65
|
+
super().__init__(scope)
|
|
66
|
+
self._scope_list: list[str] = list(scope)
|
|
67
|
+
|
|
68
|
+
def init(self) -> dict[str, Any]:
|
|
69
|
+
"""Return an empty projection — this transformer only suppresses events.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
An empty mapping; no `run.<key>` projection is added.
|
|
73
|
+
"""
|
|
74
|
+
return {}
|
|
75
|
+
|
|
76
|
+
def process(self, event: ProtocolEvent) -> bool:
|
|
77
|
+
"""Drop tagged `messages` events after nudging `MessagesTransformer` to ignore them.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
event: The protocol event to observe, and possibly mutate.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
`False` to drop a tagged internal call from the raw event log and
|
|
84
|
+
`run.messages`; `True` otherwise.
|
|
85
|
+
"""
|
|
86
|
+
if event["method"] != "messages":
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
params = event["params"]
|
|
90
|
+
if params["namespace"] != self._scope_list:
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
payload, metadata = params["data"]
|
|
94
|
+
is_internal = (
|
|
95
|
+
bool(metadata) and metadata.get(INTERNAL_CALL_METADATA_KEY) == _INTERNAL_CALL_TOKEN
|
|
96
|
+
)
|
|
97
|
+
if not is_internal:
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
# Only `message-start` needs mutation: marking it as `"tool"` makes
|
|
101
|
+
# `MessagesTransformer` ignore the rest of that run. All events are still
|
|
102
|
+
# dropped from the raw log below.
|
|
103
|
+
if isinstance(payload, dict) and payload.get("event") == "message-start":
|
|
104
|
+
payload["role"] = "tool"
|
|
105
|
+
elif isinstance(payload, BaseMessage):
|
|
106
|
+
params["data"] = (None, metadata)
|
|
107
|
+
|
|
108
|
+
return False
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Call tracking middleware for agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal
|
|
6
|
+
|
|
7
|
+
from coco_core.messages import AIMessage
|
|
8
|
+
from coco_graph.channels.untracked_value import UntrackedValue
|
|
9
|
+
from typing_extensions import NotRequired, override
|
|
10
|
+
|
|
11
|
+
from coco.agents.middleware.types import (
|
|
12
|
+
AgentMiddleware,
|
|
13
|
+
AgentState,
|
|
14
|
+
ContextT,
|
|
15
|
+
PrivateStateAttr,
|
|
16
|
+
ResponseT,
|
|
17
|
+
hook_config,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from coco_graph.runtime import Runtime
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ModelCallLimitState(AgentState[ResponseT]):
|
|
25
|
+
"""State schema for `ModelCallLimitMiddleware`.
|
|
26
|
+
|
|
27
|
+
Extends `AgentState` with model call tracking fields.
|
|
28
|
+
|
|
29
|
+
Type Parameters:
|
|
30
|
+
ResponseT: The type of the structured response. Defaults to `Any`.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
thread_model_call_count: NotRequired[Annotated[int, PrivateStateAttr]]
|
|
34
|
+
run_model_call_count: NotRequired[Annotated[int, UntrackedValue, PrivateStateAttr]]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _build_limit_exceeded_message(
|
|
38
|
+
thread_count: int,
|
|
39
|
+
run_count: int,
|
|
40
|
+
thread_limit: int | None,
|
|
41
|
+
run_limit: int | None,
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Build a message indicating which limits were exceeded.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
thread_count: Current thread model call count.
|
|
47
|
+
run_count: Current run model call count.
|
|
48
|
+
thread_limit: Thread model call limit (if set).
|
|
49
|
+
run_limit: Run model call limit (if set).
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
A formatted message describing which limits were exceeded.
|
|
53
|
+
"""
|
|
54
|
+
exceeded_limits = []
|
|
55
|
+
if thread_limit is not None and thread_count >= thread_limit:
|
|
56
|
+
exceeded_limits.append(f"thread limit ({thread_count}/{thread_limit})")
|
|
57
|
+
if run_limit is not None and run_count >= run_limit:
|
|
58
|
+
exceeded_limits.append(f"run limit ({run_count}/{run_limit})")
|
|
59
|
+
|
|
60
|
+
return f"Model call limits exceeded: {', '.join(exceeded_limits)}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ModelCallLimitExceededError(Exception):
|
|
64
|
+
"""Exception raised when model call limits are exceeded.
|
|
65
|
+
|
|
66
|
+
This exception is raised when the configured exit behavior is `'error'` and either
|
|
67
|
+
the thread or run model call limit has been exceeded.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
thread_count: int,
|
|
73
|
+
run_count: int,
|
|
74
|
+
thread_limit: int | None,
|
|
75
|
+
run_limit: int | None,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Initialize the exception with call count information.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
thread_count: Current thread model call count.
|
|
81
|
+
run_count: Current run model call count.
|
|
82
|
+
thread_limit: Thread model call limit (if set).
|
|
83
|
+
run_limit: Run model call limit (if set).
|
|
84
|
+
"""
|
|
85
|
+
self.thread_count = thread_count
|
|
86
|
+
self.run_count = run_count
|
|
87
|
+
self.thread_limit = thread_limit
|
|
88
|
+
self.run_limit = run_limit
|
|
89
|
+
|
|
90
|
+
msg = _build_limit_exceeded_message(thread_count, run_count, thread_limit, run_limit)
|
|
91
|
+
super().__init__(msg)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ModelCallLimitMiddleware(
|
|
95
|
+
AgentMiddleware[ModelCallLimitState[ResponseT], ContextT, ResponseT]
|
|
96
|
+
):
|
|
97
|
+
"""Tracks model call counts and enforces limits.
|
|
98
|
+
|
|
99
|
+
This middleware monitors the number of model calls made during agent execution
|
|
100
|
+
and can terminate the agent when specified limits are reached. It supports
|
|
101
|
+
both thread-level and run-level call counting with configurable exit behaviors.
|
|
102
|
+
|
|
103
|
+
Thread-level: The middleware tracks the number of model calls and persists
|
|
104
|
+
call count across multiple runs (invocations) of the agent.
|
|
105
|
+
|
|
106
|
+
Run-level: The middleware tracks the number of model calls made during a single
|
|
107
|
+
run (invocation) of the agent.
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
```python
|
|
111
|
+
from coco.agents.middleware import ModelCallLimitMiddleware
|
|
112
|
+
from coco.agents import create_agent
|
|
113
|
+
|
|
114
|
+
# Create middleware with limits
|
|
115
|
+
call_tracker = ModelCallLimitMiddleware(thread_limit=10, run_limit=5, exit_behavior="end")
|
|
116
|
+
|
|
117
|
+
agent = create_agent("openai:gpt-5.5", middleware=[call_tracker])
|
|
118
|
+
|
|
119
|
+
# Agent will automatically jump to end when limits are exceeded
|
|
120
|
+
result = await agent.invoke({"messages": [HumanMessage("Help me with a task")]})
|
|
121
|
+
```
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
state_schema = ModelCallLimitState # type: ignore[assignment]
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
thread_limit: int | None = None,
|
|
130
|
+
run_limit: int | None = None,
|
|
131
|
+
exit_behavior: Literal["end", "error"] = "end",
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Initialize the call tracking middleware.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
thread_limit: Maximum number of model calls allowed per thread.
|
|
137
|
+
|
|
138
|
+
`None` means no limit.
|
|
139
|
+
run_limit: Maximum number of model calls allowed per run.
|
|
140
|
+
|
|
141
|
+
`None` means no limit.
|
|
142
|
+
exit_behavior: What to do when limits are exceeded.
|
|
143
|
+
|
|
144
|
+
- `'end'`: Jump to the end of the agent execution and
|
|
145
|
+
inject an artificial AI message indicating that the limit was
|
|
146
|
+
exceeded.
|
|
147
|
+
- `'error'`: Raise a `ModelCallLimitExceededError`
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
ValueError: If both limits are `None` or if `exit_behavior` is invalid.
|
|
151
|
+
"""
|
|
152
|
+
super().__init__()
|
|
153
|
+
|
|
154
|
+
if thread_limit is None and run_limit is None:
|
|
155
|
+
msg = "At least one limit must be specified (thread_limit or run_limit)"
|
|
156
|
+
raise ValueError(msg)
|
|
157
|
+
|
|
158
|
+
if exit_behavior not in {"end", "error"}:
|
|
159
|
+
msg = f"Invalid exit_behavior: {exit_behavior}. Must be 'end' or 'error'"
|
|
160
|
+
raise ValueError(msg)
|
|
161
|
+
|
|
162
|
+
self.thread_limit = thread_limit
|
|
163
|
+
self.run_limit = run_limit
|
|
164
|
+
self.exit_behavior = exit_behavior
|
|
165
|
+
|
|
166
|
+
@hook_config(can_jump_to=["end"])
|
|
167
|
+
@override
|
|
168
|
+
def before_model(
|
|
169
|
+
self, state: ModelCallLimitState[ResponseT], runtime: Runtime[ContextT]
|
|
170
|
+
) -> dict[str, Any] | None:
|
|
171
|
+
"""Check model call limits before making a model call.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
state: The current agent state containing call counts.
|
|
175
|
+
runtime: The coco_graph runtime.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
If limits are exceeded and exit_behavior is `'end'`, returns
|
|
179
|
+
a `Command` to jump to the end with a limit exceeded message. Otherwise
|
|
180
|
+
returns `None`.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
ModelCallLimitExceededError: If limits are exceeded and `exit_behavior`
|
|
184
|
+
is `'error'`.
|
|
185
|
+
"""
|
|
186
|
+
thread_count = state.get("thread_model_call_count", 0)
|
|
187
|
+
run_count = state.get("run_model_call_count", 0)
|
|
188
|
+
|
|
189
|
+
# Check if any limits will be exceeded after the next call
|
|
190
|
+
thread_limit_exceeded = self.thread_limit is not None and thread_count >= self.thread_limit
|
|
191
|
+
run_limit_exceeded = self.run_limit is not None and run_count >= self.run_limit
|
|
192
|
+
|
|
193
|
+
if thread_limit_exceeded or run_limit_exceeded:
|
|
194
|
+
if self.exit_behavior == "error":
|
|
195
|
+
raise ModelCallLimitExceededError(
|
|
196
|
+
thread_count=thread_count,
|
|
197
|
+
run_count=run_count,
|
|
198
|
+
thread_limit=self.thread_limit,
|
|
199
|
+
run_limit=self.run_limit,
|
|
200
|
+
)
|
|
201
|
+
if self.exit_behavior == "end":
|
|
202
|
+
# Create a message indicating the limit was exceeded
|
|
203
|
+
limit_message = _build_limit_exceeded_message(
|
|
204
|
+
thread_count, run_count, self.thread_limit, self.run_limit
|
|
205
|
+
)
|
|
206
|
+
limit_ai_message = AIMessage(content=limit_message)
|
|
207
|
+
|
|
208
|
+
return {"jump_to": "end", "messages": [limit_ai_message]}
|
|
209
|
+
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
@hook_config(can_jump_to=["end"])
|
|
213
|
+
async def abefore_model(
|
|
214
|
+
self,
|
|
215
|
+
state: ModelCallLimitState[ResponseT],
|
|
216
|
+
runtime: Runtime[ContextT],
|
|
217
|
+
) -> dict[str, Any] | None:
|
|
218
|
+
"""Async check model call limits before making a model call.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
state: The current agent state containing call counts.
|
|
222
|
+
runtime: The coco_graph runtime.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
If limits are exceeded and exit_behavior is `'end'`, returns
|
|
226
|
+
a `Command` to jump to the end with a limit exceeded message. Otherwise
|
|
227
|
+
returns `None`.
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
ModelCallLimitExceededError: If limits are exceeded and `exit_behavior`
|
|
231
|
+
is `'error'`.
|
|
232
|
+
"""
|
|
233
|
+
return self.before_model(state, runtime)
|
|
234
|
+
|
|
235
|
+
@override
|
|
236
|
+
def after_model(
|
|
237
|
+
self, state: ModelCallLimitState[ResponseT], runtime: Runtime[ContextT]
|
|
238
|
+
) -> dict[str, Any] | None:
|
|
239
|
+
"""Increment model call counts after a model call.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
state: The current agent state.
|
|
243
|
+
runtime: The coco_graph runtime.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
State updates with incremented call counts.
|
|
247
|
+
"""
|
|
248
|
+
return {
|
|
249
|
+
"thread_model_call_count": state.get("thread_model_call_count", 0) + 1,
|
|
250
|
+
"run_model_call_count": state.get("run_model_call_count", 0) + 1,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async def aafter_model(
|
|
254
|
+
self,
|
|
255
|
+
state: ModelCallLimitState[ResponseT],
|
|
256
|
+
runtime: Runtime[ContextT],
|
|
257
|
+
) -> dict[str, Any] | None:
|
|
258
|
+
"""Async increment model call counts after a model call.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
state: The current agent state.
|
|
262
|
+
runtime: The coco_graph runtime.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
State updates with incremented call counts.
|
|
266
|
+
"""
|
|
267
|
+
return self.after_model(state, runtime)
|