Skip to main content
Connic
Platform

Logs & Debugging

Capture output from custom code and use run context, traces, and token patterns to find the first point of failure.

Last updated

Logging from Custom Code

Anything you print or log inside a tool, middleware, hook, or guardrail is captured per run. Logs appear in the project Logs tab, where you can filter across runs, and in the detail view for the individual run. You do not need a Connic-specific logger: stdout, stderr, and Python's standard logging module are captured automatically.

What Gets Captured

You WriteShown As
print("hello")A log line at info level.
print("boom", file=sys.stderr)A log line at error level.
logging.getLogger("tools.x").info(...)A line at the exact debug, info, warning, or error level you called.

Standard-library logger names must begin with tools., middleware., hooks., or guardrails.. The usual logging.getLogger(__name__) pattern already creates those names because Connic imports files under the matching package roots.

Source Labels

Every line is tagged with the code path that emitted it:

SourceProduced By
ToolA custom tool, tagged with its tool name.
MiddlewareBefore and after middleware, tagged as before or after.
HookA tool hook, tagged with the tool it wraps.
GuardrailA custom guardrail, tagged with its guardrail name.

Example

tools/lookup_customer.py
"""A custom tool that emits logs Connic will surface in the dashboard."""
import logging
import sys
from typing import Any, Dict

log = logging.getLogger(__name__)  # -> "tools.lookup_customer"

async def lookup_customer(args: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
    customer_id = args["customer_id"]

    # Plain print -> captured as an info-level log line
    print(f"Looking up customer {customer_id}")

    # stdlib logging -> captured at the level you called
    log.warning("cache miss for %s", customer_id)

    try:
        record = await fetch_from_crm(customer_id)
    except Exception:
        # Writes to stderr -> captured as an error-level log line
        print(f"CRM lookup failed for {customer_id}", file=sys.stderr)
        raise

    return record

This produces an info line from print, a warning from the standard logger, and an error from stderr if the CRM call fails. All three are tagged tool.lookup_customer and retain their emission order.

Good to know
  • Connic retains up to 500 log lines per run; additional lines are dropped.
  • Unhandled exceptions from tools, middleware, hooks, and guardrails are logged with their traceback before normal error handling continues.
  • StopProcessing and AbortTool are intentional control flow and are not logged as errors.
  • Use logging.exception(...) inside a caught exception when you still want the traceback.
  • Lines flush on newlines. Output without a trailing newline may remain buffered until the code returns.
  • For structured logs, serialize the value with json.dumps(...) and log the resulting string.

Debugging an Agent

For intermittent incorrect results, narrow the issue to a comparable set of runs and locate the first trace span where a bad execution diverges from a healthy one. That point usually reveals the cause faster than working backward from the final response.

Make runs easy to search

Tag runs in middleware with stable keys such as customer_id, request_type, plan, locale, or workflow_version. The Logs search can then isolate the precise slice you need with key=value.

middleware/invoice-processor.py
from typing import Any, Dict

async def before(content: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
    payload = context.get("payload", {})
    context["request_type"] = "invoice_validation"
    context["customer_tier"] = payload.get("customer_tier", "unknown")
    context["workflow_version"] = "2026-03-debug-pass"
    context["debug_bucket"] = "high-risk"
    return content
1

Isolate the Failing Pattern

Start in Logs and narrow to the agent. Filter outright failures by status. For incorrect successful responses, search context values to determine whether the pattern belongs to an input shape, customer segment, deployment, or workflow version.
2

Compare Like-for-Like Runs

Open one incorrect run and one healthy run side by side. Choose the same agent and deployment with similar inputs and context, then compare their traces. Focus on the first span where they diverge.
3

Inspect the Decision Path

If the first divergence is an LLM span, inspect its prompt, tool choice, arguments, and Thoughts when reasoning is enabled. A middleware or tool divergence points earlier in the data path: bad enrichment, stale upstream data, malformed arguments, or an unexpected response shape.
  • Wrong tool or arguments: improve the system prompt or tool descriptions.
  • Empty or unexpected tool output: inspect the tool or upstream service.
  • Extra LLM iterations: look for unsatisfying tool results or a missing stopping condition.
4

Use Token Patterns as Clues

Compare token patterns between passing and failing runs. These are diagnostic signals; see Usage and Billing for the complete token and cost reference.

SignalWhat It Suggests
High input tokensPrompt or middleware context may be too large and obscuring relevant details.
Thinking tokens hit the budgetThe model may be cutting reasoning short on harder cases.
Very low thinking tokensThe model may not be applying enough reasoning to a multi-step request.
Trace reaches max iterationsThe run stopped at max_iterations, not because the task was complete.
Failing runs use far more tokensThe agent may be wandering, retrying, or compensating for unclear instructions or weak tool output.
5

Verify Tool Inputs and Outputs

Inspect the arguments the model sent and the data the tool returned. Different arguments with the same context usually point to model decision-making. Matching arguments with different outputs usually point to the tool or its upstream data.
6

Re-run to Verify Fixes

After changing a prompt, middleware, tool, or agent setting, use Run Again on the original failure. Compare the new trace with the original to confirm that the execution path changed where expected.

Configuration for Better Observability

These agent settings directly change what you can infer from a trace:

SettingImpact on Observability
reasoning_effortSet it above off to capture reasoning content when the provider returns it.
reasoning_budgetIncrease it when trace thoughts appear truncated, or use -1 to let the model decide.
max_iterationsIf failing traces consistently end at this count, increase the limit or refine the prompt to remove unnecessary loops.
middleware contextValues are saved with the run and searchable with key=value.

See Agent Configuration for the complete setting reference.

Accessing Run Data in Code

The middleware after() hook receives system fields after the agent finishes. Use them to send run metadata to your own monitoring or analytics service.

Context KeyDescription
run_idUnique identifier for the run.
agent_nameName of the agent that executed.
duration_msExecution time excluding approval wait time.
token_usageInput, output, thinking, cached input, and total token counts for the run.
middleware/invoice-processor.py
import httpx
from typing import Any, Dict

async def after(response: str, context: Dict[str, Any]) -> str:
    """Send run metadata to an external monitoring system."""
    try:
        async with httpx.AsyncClient() as client:
            await client.post(
                "https://monitoring.internal/events",
                json={
                    "run_id": context.get("run_id"),
                    "agent": context.get("agent_name"),
                    "duration_ms": context.get("duration_ms"),
                    "tokens": context.get("token_usage", {}),
                    "request_type": context.get("request_type"),
                }
            )
    except Exception:
        pass
    return response
Values you set in before() are also available in after(). Include identifiers such as request_type or customer_id in outbound monitoring events. See Context for the full reference.
Debug from the first divergence
Search for a comparable set of runs, compare their traces, inspect the first differing span, and replay the original input after your fix.