Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 75
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sgp/agentex-sdk-7acaeb315af90255109ae17afc71e32a8e5851bb8a956a2a284cb4d344dfab51.yml
openapi_spec_hash: 3044e94b48d60311b6048e8df88e7552
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/sgp/agentex-sdk-ee0c521f0612c31b874bd595b90cd9209545bab603983552a1a7a87f38ed931e.yml
openapi_spec_hash: 917a1ffe9e353bed2740524dec786ed2
config_hash: 593e89b291976a5e84e4c3c3f8324354
4 changes: 4 additions & 0 deletions src/agentex/lib/adk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@

# Data-source refs for lineage (SGP-6513); implementation lives in core.tracing
from agentex.lib.core.tracing import lineage

# Opt-in commit-SHA stamping (AGX1-969); implementation in core.tracing
from agentex.lib.core.tracing import code_revision
from agentex.lib.core.tracing.lineage import DataSourceRef, data_sources

# Unified harness surface (AGX1-375)
Expand Down Expand Up @@ -73,6 +76,7 @@
"TurnSpan",
# Lineage data-source refs (SGP-6513)
"lineage",
"code_revision",
"DataSourceRef",
"data_sources",
# Checkpointing / LangGraph
Expand Down
3 changes: 3 additions & 0 deletions src/agentex/lib/cli/debug/debug_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
pass

from agentex.lib.utils.logging import make_logger
from agentex.lib.cli.utils.cli_utils import SUBPROCESS_STREAM_LIMIT

from .debug_config import DebugConfig, resolve_debug_port

Expand Down Expand Up @@ -66,6 +67,7 @@ async def start_temporal_worker_debug(
env=debug_env,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
limit=SUBPROCESS_STREAM_LIMIT,
)


Expand Down Expand Up @@ -119,6 +121,7 @@ async def start_acp_server_debug(
env=debug_env,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
limit=SUBPROCESS_STREAM_LIMIT,
)


Expand Down
60 changes: 56 additions & 4 deletions src/agentex/lib/cli/handlers/run_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from agentex.lib.cli.debug import DebugConfig, start_acp_server_debug, start_temporal_worker_debug
from agentex.lib.utils.logging import make_logger
from agentex.config.agent_manifest import AgentManifest
from agentex.lib.cli.utils.cli_utils import SUBPROCESS_STREAM_LIMIT
from agentex.lib.cli.utils.path_utils import (
get_file_paths,
calculate_uvicorn_target_for_local,
Expand All @@ -23,6 +24,11 @@
logger = make_logger(__name__)
console = Console()

# How many consecutive unreadable lines to skip before giving up on the stream.
# Skipping is only known-safe for the limit-overrun case; this bounds the damage
# if some other error repeats without consuming anything.
MAX_CONSECUTIVE_READ_ERRORS = 100


class RunError(Exception):
"""An error occurred during agent run"""
Expand Down Expand Up @@ -215,6 +221,7 @@ async def start_acp_server(
env=env,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
limit=SUBPROCESS_STREAM_LIMIT,
)


Expand All @@ -234,23 +241,68 @@ async def start_temporal_worker(
env=env,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
limit=SUBPROCESS_STREAM_LIMIT,
)


async def stream_process_output(process: asyncio.subprocess.Process, prefix: str):
"""Stream process output with prefix"""
"""Stream process output with prefix.

This loop is the only reader of the child's stdout pipe. If it ever stops
reading, the pipe fills and the child blocks forever inside ``write()``,
which presents as a silent freeze: 0% CPU, no further logs, no traceback.
So a single unreadable line must never end the loop.
"""
try:
if process.stdout is None:
return
consecutive_read_errors = 0
while True:
line = await process.stdout.readline()
try:
line = await process.stdout.readline()
except ValueError as e:
# readline() raises ValueError when a line exceeds the stream limit.
# In *that* case it has already discarded the line and resumed the
# transport, so skipping it makes guaranteed progress. Any other
# ValueError carries no such guarantee, and retrying it forever would
# spin without draining. We cannot tell the two apart (readline
# flattens LimitOverrunError into a bare ValueError), so bound the
# retries and let the outer handler report the hang risk.
consecutive_read_errors += 1
if consecutive_read_errors > MAX_CONSECUTIVE_READ_ERRORS:
raise
logger.warning(
f"Skipping an unreadable line from {prefix}: {e!r} "
f"(consecutive failure {consecutive_read_errors}/{MAX_CONSECUTIVE_READ_ERRORS}). "
f"If this says the chunk exceeded the limit, raise limit= on this "
f"process's create_subprocess_exec."
)
continue

consecutive_read_errors = 0

if not line:
break
decoded_line = line.decode("utf-8").rstrip()

try:
decoded_line = line.decode("utf-8").rstrip()
except UnicodeDecodeError as e:
logger.warning(f"Dropped an undecodable log line from {prefix} ({e}).")
continue

if decoded_line: # Only print non-empty lines
console.print(f"[dim]{prefix}:[/dim] {decoded_line}")
except Exception as e:
logger.debug(f"Output streaming ended for {prefix}: {e}")
# The escalation path, including for the re-raise above. Anything reaching
# here ends the loop, so the child is now at risk of blocking on a full pipe.
# Warning rather than debug: this used to be a debug() that make_logger could
# never emit, which is why three freezes produced no clue.
# CancelledError derives from BaseException, so the auto-reload path that
# cancels these tasks passes straight through and is unaffected.
logger.warning(
f"Output streaming for {prefix} stopped on {e!r}. "
f"Nothing is draining its stdout now, so {prefix} will hang once the pipe fills."
)


async def run_agent(manifest_path: str, debug_config: "DebugConfig | None" = None):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,20 @@ from agentex.lib.adk import ClaudeCodeTurn
from agentex.lib.types.acp import SendEventParams, CancelTaskParams, CreateTaskParams
from agentex.lib.core.harness import UnifiedEmitter
from agentex.lib.types.fastacp import AsyncACPConfig
from agentex.lib.types.tracing import SGPTracingProcessorConfig
from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig
from agentex.lib.utils.logging import make_logger
from agentex.types.text_content import TextContent
from agentex.lib.sdk.fastacp.fastacp import FastACP
from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config

logger = make_logger(__name__)

# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped
# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend
# as disabled and skips agent registration too).
if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"):
add_tracing_processor_config(AgentexTracingProcessorConfig())

add_tracing_processor_config(
SGPTracingProcessorConfig(
sgp_api_key=os.environ.get("SGP_API_KEY", ""),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,20 @@ from agentex.lib.types.acp import SendEventParams, CancelTaskParams, CreateTaskP
from agentex.types.text_content import TextContent
from agentex.lib.core.harness import UnifiedEmitter
from agentex.lib.types.fastacp import AsyncACPConfig
from agentex.lib.types.tracing import SGPTracingProcessorConfig
from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig
from agentex.lib.utils.logging import make_logger
from agentex.lib.utils.model_utils import BaseModel
from agentex.lib.sdk.fastacp.fastacp import FastACP
from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config

logger = make_logger(__name__)

# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped
# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend
# as disabled and skips agent registration too).
if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"):
add_tracing_processor_config(AgentexTracingProcessorConfig())

add_tracing_processor_config(
SGPTracingProcessorConfig(
sgp_api_key=os.environ.get("SGP_API_KEY", ""),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce
from agentex.lib.sdk.fastacp.fastacp import FastACP
from agentex.protocol.acp import SendEventParams, CancelTaskParams, CreateTaskParams
from agentex.lib.types.fastacp import AsyncACPConfig
from agentex.lib.types.tracing import SGPTracingProcessorConfig
from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig
from agentex.lib.utils.logging import make_logger
from agentex.types.text_content import TextContent
from agentex.lib.adk import LangGraphTurn
Expand All @@ -29,6 +29,12 @@ from project.graph import create_graph

logger = make_logger(__name__)

# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped
# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend
# as disabled and skips agent registration too).
if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"):
add_tracing_processor_config(AgentexTracingProcessorConfig())

add_tracing_processor_config(
SGPTracingProcessorConfig(
sgp_api_key=os.environ.get("SGP_API_KEY", ""),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ from agents import Agent, Runner, function_tool, set_tracing_disabled
from agentex.lib import adk
from agentex.lib.types.acp import SendEventParams, CancelTaskParams, CreateTaskParams
from agentex.lib.types.fastacp import AsyncACPConfig
from agentex.lib.types.tracing import SGPTracingProcessorConfig
from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig
from agentex.lib.utils.logging import make_logger
from agentex.types.text_content import TextContent
from agentex.lib.utils.model_utils import BaseModel
Expand All @@ -48,6 +48,12 @@ if _litellm_key and not os.environ.get("OPENAI_API_KEY"):

_sgp_api_key = os.environ.get("SGP_API_KEY", "")
_sgp_account_id = os.environ.get("SGP_ACCOUNT_ID", "")
# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped
# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend
# as disabled and skips agent registration too).
if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"):
add_tracing_processor_config(AgentexTracingProcessorConfig())

if _sgp_api_key and _sgp_account_id:
add_tracing_processor_config(
SGPTracingProcessorConfig(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import agentex.lib.adk as adk
from agentex.protocol.acp import SendEventParams, CancelTaskParams, CreateTaskParams
from agentex.lib.core.harness import UnifiedEmitter
from agentex.lib.types.fastacp import AsyncACPConfig
from agentex.lib.types.tracing import SGPTracingProcessorConfig
from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig
from agentex.lib.utils.logging import make_logger
from agentex.types.text_content import TextContent
from agentex.lib.utils.model_utils import BaseModel
Expand All @@ -42,6 +42,12 @@ logger = make_logger(__name__)
# so they show up in the per-task spans dropdown out of the box.
SGP_API_KEY = os.environ.get("SGP_API_KEY", "")
SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "")
# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped
# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend
# as disabled and skips agent registration too).
if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"):
add_tracing_processor_config(AgentexTracingProcessorConfig())

if SGP_API_KEY and SGP_ACCOUNT_ID:
add_tracing_processor_config(
SGPTracingProcessorConfig(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import agentex.lib.adk as adk
from agentex.lib.adk import ClaudeCodeTurn
from agentex.lib.types.acp import SendMessageParams
from agentex.lib.core.harness import UnifiedEmitter
from agentex.lib.types.tracing import SGPTracingProcessorConfig
from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig
from agentex.lib.utils.logging import make_logger
from agentex.types.text_content import TextContent
from agentex.lib.sdk.fastacp.fastacp import FastACP
Expand All @@ -35,6 +35,12 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce

logger = make_logger(__name__)

# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped
# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend
# as disabled and skips agent registration too).
if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"):
add_tracing_processor_config(AgentexTracingProcessorConfig())

add_tracing_processor_config(
SGPTracingProcessorConfig(
sgp_api_key=os.environ.get("SGP_API_KEY", ""),
Expand Down
8 changes: 7 additions & 1 deletion src/agentex/lib/cli/templates/sync-codex/project/acp.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import agentex.lib.adk as adk
from agentex.lib.adk import CodexTurn
from agentex.lib.types.acp import SendMessageParams
from agentex.lib.core.harness import UnifiedEmitter
from agentex.lib.types.tracing import SGPTracingProcessorConfig
from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig
from agentex.lib.utils.logging import make_logger
from agentex.types.text_content import TextContent
from agentex.lib.sdk.fastacp.fastacp import FastACP
Expand All @@ -44,6 +44,12 @@ from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_proce

logger = make_logger(__name__)

# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped
# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend
# as disabled and skips agent registration too).
if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"):
add_tracing_processor_config(AgentexTracingProcessorConfig())

add_tracing_processor_config(
SGPTracingProcessorConfig(
sgp_api_key=os.environ.get("SGP_API_KEY", ""),
Expand Down
10 changes: 8 additions & 2 deletions src/agentex/lib/cli/templates/sync-langgraph/project/acp.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ from agentex.lib.core.harness import UnifiedEmitter
from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config
from agentex.lib.sdk.fastacp.fastacp import FastACP
from agentex.protocol.acp import SendMessageParams
from agentex.lib.types.tracing import SGPTracingProcessorConfig
from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig
from agentex.lib.utils.logging import make_logger
from agentex.types.text_content import TextContent
from agentex.lib.adk import LangGraphTurn
Expand All @@ -33,7 +33,13 @@ from project.graph import create_graph

logger = make_logger(__name__)

# Register the Agentex tracing processor so spans are shipped to the backend
# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped
# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend
# as disabled and skips agent registration too).
if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"):
add_tracing_processor_config(AgentexTracingProcessorConfig())

# Register the Scale GenAI Platform (SGP) tracing processor
add_tracing_processor_config(
SGPTracingProcessorConfig(
sgp_api_key=os.environ.get("SGP_API_KEY", ""),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ load_dotenv()
from agentex.lib import adk
from project.agent import run_agent
from agentex.protocol.acp import SendMessageParams
from agentex.lib.types.tracing import SGPTracingProcessorConfig
from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig
from agentex.lib.utils.logging import make_logger
from agentex.types.text_content import TextContent
from agentex.lib.sdk.fastacp.fastacp import FastACP
Expand All @@ -42,6 +42,12 @@ SGP_API_KEY = os.environ.get("SGP_API_KEY", "")
SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "")
SGP_CLIENT_BASE_URL = os.environ.get("SGP_CLIENT_BASE_URL", "")

# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped
# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend
# as disabled and skips agent registration too).
if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"):
add_tracing_processor_config(AgentexTracingProcessorConfig())

if SGP_API_KEY and SGP_ACCOUNT_ID:
add_tracing_processor_config(
SGPTracingProcessorConfig(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ from agentex.lib.adk.providers._modules.sync_provider import SyncStreamingProvid
from agentex.lib.sdk.fastacp.fastacp import FastACP
from agentex.protocol.acp import SendMessageParams
from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config
from agentex.lib.types.tracing import SGPTracingProcessorConfig
from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig
from agentex.lib.utils.model_utils import BaseModel

from agentex.types.task_message_update import TaskMessageUpdate, StreamTaskMessageFull
Expand All @@ -32,6 +32,12 @@ SGP_API_KEY = os.environ.get("SGP_API_KEY", "")
SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "")
SGP_CLIENT_BASE_URL = os.environ.get("SGP_CLIENT_BASE_URL", "")

# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped
# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend
# as disabled and skips agent registration too).
if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"):
add_tracing_processor_config(AgentexTracingProcessorConfig())

if SGP_API_KEY and SGP_ACCOUNT_ID:
add_tracing_processor_config(
SGPTracingProcessorConfig(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ from project.agent import MODEL_NAME, create_agent
import agentex.lib.adk as adk
from agentex.protocol.acp import SendMessageParams
from agentex.lib.core.harness import UnifiedEmitter
from agentex.lib.types.tracing import SGPTracingProcessorConfig
from agentex.lib.types.tracing import SGPTracingProcessorConfig, AgentexTracingProcessorConfig
from agentex.lib.utils.logging import make_logger
from agentex.types.text_content import TextContent
from agentex.lib.sdk.fastacp.fastacp import FastACP
Expand All @@ -36,6 +36,12 @@ logger = make_logger(__name__)
# processor that's lazy-initialised on first span.
SGP_API_KEY = os.environ.get("SGP_API_KEY", "")
SGP_ACCOUNT_ID = os.environ.get("SGP_ACCOUNT_ID", "")
# Local Agentex backend: spans appear in the developer UI's traces tab. Skipped
# when AGENTEX_BASE_URL is explicitly empty (the runtime then treats the backend
# as disabled and skips agent registration too).
if os.environ.get("AGENTEX_BASE_URL", "http://localhost:5003"):
add_tracing_processor_config(AgentexTracingProcessorConfig())

if SGP_API_KEY and SGP_ACCOUNT_ID:
add_tracing_processor_config(
SGPTracingProcessorConfig(
Expand Down
Loading