LIVE
Publish Flash items in Admin to fill the ticker
The Roman Road of AI in the New Era
Sign InSubscribe ProAdmin
LearnFREE

Build a Minimal Tool Agent with Observability

|

Step-by-step guide to build a minimal tool-using AI agent: allowlisted tools, run_id traces, JSON logs, hard stops, and a human review gate.

Build a Minimal Tool Agent with Observability

Evidence note: Deploy Desk implementation guide as of 2026-08-22. This is a desk synthesis of production patterns for tool-using agents (OpenAI-compatible tool calls, structured logs, policy gates). It is not a claim that any single framework is required. Adjust retention, PII redaction, and legal review to your environment.

Quick answer

To build a minimal tool agent with observability, do not start with autonomy demos. Start with a bounded loop: assign a run_id, let the model propose at most one allowlisted tool, execute it behind a policy check, log sanitized input/output, feed the observation back, and stop on success, budget exhaustion, or a write that needs human review. Observability is not a dashboard you add later—it is the run log that makes the loop debuggable. If you cannot answer “which tool ran with which arguments,” you do not have an agent you can operate.

Key takeaways

  • One task = one run_id shared by plan, tool, model, and final status events.
  • Allowlist tools; deny shell and unconstrained writes by default.
  • Cap max_steps, max_tool_calls, and wall-clock timeout before the first demo.
  • Log tool calls as first-class spans: name, redacted args, status, latency.
  • Ship read-only tools first; gate email/DB writes behind human review.
Minimal tool agent loop from user task through plan tool observe answer with observability bus
Figure 1. The product is the loop plus the observability bus—not the chat UI.
Screenshot of OpenAI function calling documentation for tool-using models
Figure 2. Live capture from platform.openai.com docs (Function calling)—the common tool-schema pattern many OpenAI-compatible servers mirror.

Who this guide is for

  • Engineers wiring a first internal agent that can call HTTP, search, or a calculator.
  • Platform teams who already serve an LLM API and need a safe tool layer on top.
  • PMs evaluating whether a vendor “agent” exposes enough logs to debug incidents.

Who should skip

Architecture: five objects, not twenty frameworks

Minimum objects in a operable tool agent
ObjectResponsibilityMust log
RunOne user task lifecyclerun_id, task_type, budgets, final_status
Model stepPlan or answermodel id, tokens, latency
Tool registryAllowlisted callablestool name, version, permission tier
Policy gateAllow / deny / reviewdecision, reason
ObservationTool result fed backstatus, latency, redacted payload summary

Frameworks (LangGraph, custom loops, vendor SDKs) are optional. The objects are not.

Step 1: write the contract for one task type

Pick a single boring task, for example: “Fetch an allowlisted URL and summarize it in eight bullets.” Write down:

  • Success criteria (what “done” means)
  • Allowed tools (e.g., http_get only)
  • Forbidden actions (no email, no DB write, no shell)
  • Budgets: max steps 4–6, max tool calls 4–8, timeout 30–60s

If you cannot write the contract in one paragraph, the agent is not minimal yet.

Step 2: implement the tool registry with deny-by-default

Lab mock policy panel showing allowlisted read tools and denied write tools with budget caps
Figure 3. Policy panel mock: allow reads with host allowlists; gate or deny writes.
# Pseudocode — keep tools boring and typed
TOOLS = {
  "http_get": {
    "fn": http_get,
    "permission": "read",
    "hosts_allow": ["docs.example.com"],
  },
  "calculator": {
    "fn": calculator,
    "permission": "read",
  },
  # "send_email" exists only in review mode, never auto
}

Host allowlists matter. An unrestricted HTTP tool is a data-exfiltration primitive.

Step 3: run the loop with hard stops

run_id = new_id()
log(run.start, run_id, task, budgets)

for step in range(1, max_steps + 1):
  decision = model.plan(messages, tools=allowlist)
  log(model.plan, run_id, step, decision.summary)

  if decision.type == "final":
    log(run.end, run_id, status="success")
    return decision.answer

  if decision.type == "tool":
    gate = policy.check(decision.tool, decision.args)
    if gate == "deny":
      log(policy.deny, run_id, decision.tool)
      return safe_refusal()
    if gate == "review":
      log(policy.review, run_id, decision.tool)
      return queue_human_review(run_id, decision)

    result = execute(decision.tool, decision.args)
    log(tool.result, run_id, status=result.status, latency_ms=result.ms)
    messages.append(observation(result))

log(run.end, run_id, status="failed", reason="budget_exhausted")

The model never executes tools directly. Your process does—after policy.

Step 4: make observability the default transport

Annotated JSON log screenshot of agent run_id tool.call tool.result and run.end events
Figure 4. Annotated JSON log: every event shares run_id; tool payloads are redacted in production.
Screenshot of OpenTelemetry traces concept documentation for distributed tracing signals
Figure 5. Live capture from opentelemetry.io (Traces concepts)—same mental model as agent run_id / span logging when you graduate from JSONL to an APM.

Minimum event types:

  • run.start / run.end
  • model.plan / model.answer
  • tool.call / tool.result
  • policy.deny / policy.review

Field set to steal from the ops guide: run_id, span_id, tool_name, input_summary, output_summary, latency_ms, final_status, token counts. Deep dive: traces, tool calls, and failure logs.

Step 5: wire an OpenAI-compatible model endpoint

Your agent can call any OpenAI-compatible chat API that supports tool/function calling for your chosen model. If you self-host, complete the serving checklist in ship vLLM in production first, then point the agent at the private proxy—not at a raw GPU port.

# Shape only — use your provider's tool schema
response = client.chat.completions.create(
  model=MODEL,
  messages=messages,
  tools=openai_tool_schemas(allowlist),
  tool_choice="auto",
  temperature=0,
)

Pin model IDs. Temperature 0 is a reasonable default for tool routing while you debug.

Step 6: add one human gate before any write tool

When you eventually add send_email or ticket_create:

  1. Policy returns review instead of executing.
  2. UI shows the exact tool args from the log.
  3. Human approves → execute once with the same run_id.
  4. Rejection is also logged as a first-class outcome.

Skipping this step is how demos become incident reports.

Step 7: acceptance tests (not vibes)

Frozen acceptance cases for a minimal agent
CaseExpect
Allowlisted URL summarizesuccess; ≥1 http_get; run.end success
Disallowed hostpolicy.deny; no network call
Tool error / timeouttool.result status=error; agent stops or retries within budget
Budget exhaustionrun.end failed with reason budget_exhausted
Write tool requestedpolicy.review; no side effect until approval

Automate these five before inviting broader users.

Common mistakes

Desk-reviewed failure modes
MistakeSymptomFix
No run_idCannot reconstruct incidentsGenerate at task start; thread everywhere
Unrestricted HTTP toolSSRF / data leaksHost allowlist + size limits
Logging secretsCompliance incidentRedact auth headers and bodies
Unlimited stepsCost blowups; loopsHard max_steps + timeout
Framework firstOpaque graphs, weak logsShip the five objects first

FAQ

Do I need multi-agent orchestration?

Not for v1. One planner + allowlisted tools is enough to learn ops. Multi-agent patterns come after you can debug a single run. See multi-agent orchestration patterns when you outgrow one loop.

Is computer-use required?

No. Computer-use expands the blast radius. Start with typed tools. Read tool use vs computer use before enabling UI control.

Where should logs live?

Anywhere queryable by run_id: JSONL files in staging, then OpenSearch/ClickHouse/your APM. The schema matters more than the vendor.

How is this different from a chatbot with function calling?

The policy gate, budgets, and mandatory span logs. Vendors often show function calling; operators need deny/review paths.

Can I use my own API key from a browser?

Prefer a server-side proxy so keys are not embedded in clients. If you experiment with BYOK patterns, still keep tool execution server-side.

What to do next

  1. Implement one read-only tool + run_id JSON logs.
  2. Pass the five acceptance cases.
  3. Only then add a gated write tool.

Continue with agent observability field guide for dashboard design, and from chatbot to agent for the conceptual map.

Soft CTA: share a redacted run_id timeline in Community when your first deny-path test passes—that artifact teaches more than a demo GIF.