Loading the notebook…
Loading the notebook…
Notebooks · In-depth guides · Guide 02
Every output below is real: the notebook was run end to end on Haiku 5.5 (Anthropic) and Gemma 4 31B.
Run end to end on shipit-agent 3.0 with real model calls (Haiku 5.5 on Anthropic, Gemma 4 31B on Bedrock). Every output below is from that run.
Guide 01 covers providers. Here, Haiku 5.5 (Anthropic) from the environment, and Gemma 4 on Bedrock for the comparison at the end.
import os
from shipit_agent import Agent
from shipit_agent.llms import build_llm_from_env
from shipit_agent.llms.factory import build_llm_from_settings
os.environ["SHIPIT_ANTHROPIC_MODEL"] = "claude-haiku-5-5"
llm = build_llm_from_env("anthropic")
gemma = build_llm_from_settings({"model": "google.gemma-4-31b"}, provider="bedrock")
print(llm.model, "|", gemma.model)claude-haiku-5-5 | google.gemma-4-31b
An agent needs only llm. prompt is its system prompt and name labels it in traces and logs.
agent = Agent(llm=llm, prompt="You are a terse release engineer.", name="releaser")
print(agent.name, "| tools:", len(agent.tools), "| max_iterations:", agent.max_iterations, "(None = no limit)")releaser | tools: 0 | max_iterations: None (None = no limit)
run() returns an AgentResultresult = agent.run("What is a semantic version? One sentence.")
print(result.output)A semantic version is a three-part version number (MAJOR.MINOR.PATCH) where the MAJOR number increments for incompatible API changes, the MINOR number for backward-compatible new features, and the PATCH number for backward-compatible bug fixes.
| field | what it holds |
|---|---|
output | the final answer text |
messages | the conversation the model saw, including tool calls and results |
events | every event the run emitted, the same ones stream() yields |
tool_results | each tool call's output |
artifacts | files and objects tools produced |
metadata | usage, timing and run details |
parsed | the typed answer when you pass output_schema (Guide 08) |
rag_sources | the passages a RAG answer used (Guide 06) |
print("messages:", [m.role for m in result.messages])
print("events: ", len(result.events))
print("metadata keys:", sorted(result.metadata))
print("usage:", result.metadata["usage"])messages: ['system', 'user', 'assistant']
events: 22
metadata keys: ['finish_reason', 'model', 'provider', 'run_summary', 'usage', 'used_skill_tools', 'used_skills']
usage: {'prompt_tokens': 32, 'completion_tokens': 86, 'total_tokens': 118, 'cache_read_input_tokens': 0, 'cache_creation_input_tokens': 0}Any typed Python function becomes a tool: its name, docstring and type hints are what the model sees. Guide 03 goes deep on tools.
from shipit_agent.tools import FunctionTool
RELEASES = {"shipit-agent": "3.0.0", "vouchline": "1.4.2"}
def latest_release(package: str) -> str:
"""Return the latest released version of a package we publish."""
return RELEASES.get(package, f"no release found for {package}")
tool = FunctionTool.from_callable(latest_release, read_only=True)
def release_agent(**changes) -> Agent:
"""A fresh agent with the release tool: a new conversation each time."""
settings = {"llm": llm, "prompt": "Answer with the tools; be brief.", "tools": [tool]}
return Agent(**{**settings, **changes})
agent = release_agent()
result = agent.run("What's the latest shipit-agent release, and what would the next minor be?")
print(result.output)The latest shipit-agent release is **3.0.0**, so the next minor version would be **3.1.0**.
tool_results holds each call's output; the tool call itself is in messages.
for r in result.tool_results:
print(r.name, "->", r.output)
for m in result.messages:
if m.role == "tool":
print("tool message:", m.content[:80])latest_release -> 3.0.0 tool message: 3.0.0
An Agent keeps its conversation in a session between calls, so a second run() sees the first. That's
what you want for a chat, and not what you want for unrelated jobs: give those a fresh agent (or a new
session_id). The rest of this guide uses release_agent() for each independent example.
memo = Agent(llm=llm)
memo.run("Remember the code word: kiwi.")
print("same agent: ", memo.run("What was the code word? One word.").output)
print("fresh agent: ", Agent(llm=llm).run("What was the code word? One word.").output[:90])same agent: Kiwi
fresh agent: I don't see a code word anywhere in this conversation. This message is the first one I hav
stream() runs the same loop and yields each event as it happens. Every event has a type, a human
message, a payload dict and a timestamp.
from collections import Counter
events = list(release_agent().stream("What's the latest vouchline release?"))
for e in events:
if e.type != "text_delta":
print(f"{e.type:22} {e.message[:60]}")
print("\ncounts:", dict(Counter(e.type for e in events)))run_started Agent run started
step_started LLM completion started
usage_tick Usage updated
assistant_message Model response
tool_group_started Tool group started
tool_called Tool called: latest_release
tool_output_started Tool output started: latest_release
tool_output_delta Tool output: latest_release
tool_completed Tool completed: latest_release
tool_group_completed Tool group completed
step_started LLM completion started
usage_tick Usage updated
assistant_message Model response
final_answer Final answer generated
run_summary Run finished: 2 iterations, 1 tool calls, 75 tokens, $0.0001
run_completed Agent run completed
counts: {'run_started': 1, 'step_started': 2, 'usage_tick': 2, 'assistant_message': 2, 'tool_group_started': 1, 'tool_called': 1, 'tool_output_started': 1, 'tool_output_delta': 1, 'tool_completed': 1, 'tool_group_completed': 1, 'text_delta': 2, 'final_answer': 1, 'run_summary': 1, 'run_completed': 1}The payload carries the details a UI needs. A tool call and its completion, in full:
import json
for e in events:
if e.type in ("tool_called", "tool_completed"):
print(e.type, json.dumps(e.payload, default=str)[:300])tool_called {"tool": "latest_release", "call_id": "toolu_018EpXGAwYwyAEntyyan3TBP", "group_id": "tool_group_1", "arguments": {"package": "vouchline"}, "iteration": 1, "run_id": "8954051c-86eb-4932-869a-875801b3fd8a", "session_id": "50d21ce2-8431-43e6-a4cb-ac4cdc502bbd", "sequence": 6, "tool_call_id": "toolu_018
tool_completed {"tool": "latest_release", "call_id": "toolu_018EpXGAwYwyAEntyyan3TBP", "group_id": "tool_group_1", "output": "1.4.2", "output_chars": 5, "model_output_chars": 5, "model_output_reduced": false, "metadata": {}, "error": "", "iteration": 1, "duration_ms": 0.2, "run_id": "8954051c-86eb-4932-869a-875801run_completed closes every run. Its payload has the answer and the usage, so a streaming UI never needs a
second call.
final = events[-1]
print(final.type)
print({k: final.payload[k] for k in final.payload if k in ("output", "usage")})run_completed
{'output': 'The latest vouchline release is 1.4.2.', 'usage': {'prompt_tokens': 6, 'completion_tokens': 69, 'total_tokens': 75, 'cache_read_input_tokens': 2210, 'cache_creation_input_tokens': 0}}text_delta events carry the answer chunk by chunk. Print them with end="" to watch it appear.
for e in Agent(llm=llm).stream("Count from one to five in words, comma separated."):
if e.type == "text_delta":
print(e.payload.get("chunk", ""), end="", flush=True)
print()One, two, three, four,
five
arun() and astream()Inside an event loop, use the async versions. Notebooks run top-level await directly. Two runs at once:
import asyncio
import time
started = time.perf_counter()
a, b = await asyncio.gather(
release_agent().arun("Latest shipit-agent release?"),
release_agent().arun("Latest vouchline release?"),
)
print(a.output.strip(), "|", b.output.strip())
print(f"both in {time.perf_counter() - started:.1f}s")The latest shipit-agent release is 3.0.0. | The latest vouchline release is 1.4.2. both in 1.7s
kinds = []
async for e in release_agent().astream("Latest vouchline release?"):
kinds.append(e.type)
print(kinds[0], "…", kinds[-1], f"({len(kinds)} events)")run_started … run_completed (18 events)
chat_session() keeps the conversation between send() calls. Guide 07 adds stores so it survives a restart.
chat = release_agent().chat_session(session_id="release-chat")
print(chat.send("Our package is shipit-agent. What's its latest release?").output)
print(chat.send("And what would the next patch release be called?").output)
print("turns in history:", len(chat.history()))The latest release of shipit-agent is 3.0.0.
Under semantic versioning, the next patch release after 3.0.0 would be **3.0.1**. That's an inference from the version number, not a planned or published release. My tool only returns the latest release, so I can't confirm whether 3.0.1 exists or is scheduled. turns in history: 7
historyPass earlier messages to start an agent mid-conversation. (The built-in system prompt tells the model to trust only what tools show it, so a plain prompt suits a seeded chat better.)
from shipit_agent.models import Message
seeded = Agent(llm=llm, prompt="You are a helpful assistant.", history=[
Message(role="user", content="Hi, I'm Ada. I'm planning the 3.1 release for next Tuesday."),
Message(role="assistant", content="Hi Ada, got it: 3.1, next Tuesday."),
])
print(seeded.run("Remind me: which release am I planning, and when?").output)You're planning the 3.1 release, and it's scheduled for next Tuesday.
clone()clone(**changes) returns a new agent with the same setup and some parameters changed; the original is
untouched. A clone continues the original's conversation, so clone a fresh agent for a clean start.
base = release_agent()
formal = base.clone(prompt="Answer with the tools, formally, in one sentence.", name="formal")
print(base.name, "->", formal.name)
print(formal.run("Latest vouchline release?").output)shipit -> formal
The latest vouchline release is 1.4.2.
as_tool() wraps an agent so another agent can call it. The inner agent runs its own loop with its own
tools and returns its answer.
releases_expert = release_agent().as_tool(name="releases_expert", description="Knows our package release versions.")
lead = Agent(llm=llm, prompt="Delegate release questions to your expert.", tools=[releases_expert])
result = lead.run("Ask the expert for the latest shipit-agent and vouchline releases, then list them.")
print(result.output)
print("tools the lead called:", [r.name for r in result.tool_results])Here are the latest releases the expert found: 1. **shipit-agent**: 3.0.0 2. **vouchline**: 1.4.2 The lookup tool only returns version numbers, so I don't have release dates or change summaries for either. The registry page or the project changelog would have those. tools the lead called: ['releases_expert']
max_iterations caps the loop's tool-calling steps; max_turns is another name for the same
limit. With a limit of 1, the agent makes one round of lookups and then has to answer with what it has,
so it can't finish a two-step job: the run ends instead of looping.
result = release_agent(max_turns=1).run("Look up shipit-agent, then vouchline, one call at a time, then compare them.")
print(result.output[:300])
print("finish:", result.metadata.get("finish_reason"), "|", result.metadata["run_summary"])Shipit-agent's latest release is 3.0.0. Next I'll look up vouchline.
finish: end_turn | {'headline': 'Run finished: 2 iterations, 1 tool calls, 118 tokens, $0.0001', 'iterations': 2, 'tool_calls': 1, 'tool_results': 1, 'compactions': 0, 'usage': {'prompt_tokens': 6, 'completion_tokens': 112, 'total_tokens': 118, 'cache_read_input_tokens': 1873, 'cache_creation_input_tokens': 0, 'input_tokens': 6, 'output_tokens': 112}, 'usage_diagnostics': {'estimated_main_request_content_tokens': {'system': 447, 'conversation': 134, 'tool_results': 0, 'schemas': 67}, 'reported_tokens_by_purpose': {'main': 1991, 'compaction': 0}, 'prefix_changes': 0, 'cache_counters_reported': … (877 more characters)narrate()narrate() streams the same run as readable rows, the ones shipit chat prints: verbs, tool work and the
answer, already grouped.
for row in release_agent().narrate("Compare the latest shipit-agent and vouchline releases."):
print(type(row).__name__, "|", str(row)[:110])WorkRow | WorkRow(group=WorkGroup(key='toolu_01NF5qQJ2eTQoZ4jmVEF3uZV', icon='◆', label='Ran latest release 2 times', de
ProseRow | ProseRow(text="The latest release of **shipit-agent is 3.0.0**, and the latest release of **vouchline is 1.4.2
doctor()doctor() checks the agent's setup (model credentials, tools, MCP servers, settings) without calling the
model.
report = release_agent().doctor()
summary = report.to_dict()
print("passed:", summary["passed"], "| checks:", summary["check_count"],
"| failures:", summary["failure_count"], "| warnings:", summary["warning_count"])
for check in summary["checks"][:6]:
print(" ", check)passed: True | checks: 8 | failures: 0 | warnings: 2
{'name': 'llm_provider', 'status': 'pass', 'message': 'anthropic configuration looks ready.', 'details': {'class_name': 'AnthropicChatLLM', 'model': 'claude-haiku-5-5'}}
{'name': 'tools', 'status': 'pass', 'message': 'Tool registry and callable schemas look consistent.', 'details': {'tool_count': 1, 'family_count': 1, 'families': 'custom=1', 'tools': 'latest_release'}}
{'name': 'skills', 'status': 'pass', 'message': 'Skill catalog and runtime dependencies look consistent.', 'details': {'catalog_size': 37, 'always_on': 0, 'default_count': 0, 'auto_use': True, 'skill_match_limit': 3, 'bundle_errors': 'none', 'missing_tools': 'none', 'missing_mcps': 'none'}}
{'name': 'mcps', 'status': 'warn', 'message': 'No MCP servers are attached.', 'details': {'mcp_count': 0}}
{'name': 'stores', 'status': 'pass', 'message': 'Memory, session, and trace stores are configured.', 'details': {'memory_store': 'InMemoryMemoryStore', 'session_store': 'InMemorySessionStore', 'trace_store': 'None', 'session_id': 'e6567952-43a4-4c61-89e5-c4823be69c4a', 'trace_id': 'unset'}}
{'name': 'connectors', 'status': 'pass', 'message': 'No connector tools require credentials.', 'details': {'connector_count': 0}}Nothing changes but the model. Gemma 4 31B on Bedrock calls the same tool.
on_gemma = release_agent(llm=gemma)
result = on_gemma.run("What's the latest shipit-agent release?")
print(result.output)
print("tool calls:", [r.name for r in result.tool_results], "| usage:", result.metadata["usage"])The latest release of shipit-agent is 3.0.0.
tool calls: ['latest_release'] | usage: {'prompt_tokens': 1223, 'completion_tokens': 50, 'total_tokens': 1273, 'cache_read_input_tokens': 0, 'cache_creation_input_tokens': 0}session_id for unrelated jobs.run() returns an AgentResult; stream() yields the same run as events.type, message and payload; run_completed has the answer and usage.arun() / astream() for async; chat_session() for multi-turn; clone() and as_tool() to compose.narrate() gives readable rows; doctor() checks the setup.Next: Guide 03 — tools: built-in, custom, errors and parallel calls.