Loading the notebook…
Loading the notebook…
Notebooks · Workspace, live UI and observability · 68
This notebook shows:
Its imports and arguments are checked against shipit-agent 3.0. The outputs were saved when it was written, for an earlier release.
from pathlib import Path
import sys
ROOT = (
Path.cwd().resolve().parent
if Path.cwd().name == "notebooks"
else Path.cwd().resolve()
)
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))TodoTool.run(context, todos=[...])TodoTool.run takes a ToolContext and a todos list of {content, status}
objects. status defaults to "pending" and must be one of
"pending" | "in_progress" | "completed". It returns a ToolOutput whose
text is a glyph checklist and whose metadata carries the normalized list
and a summary.
from shipit_agent.tools.todo.todo_tool import TodoTool
from shipit_agent.tools.base import ToolContext
todo = TodoTool()
print("tool name:", todo.name)
# A ToolContext carries the prompt + a mutable `state` dict the tool writes to.
ctx = ToolContext(prompt="Ship the new export feature")
out = todo.run(
ctx,
todos=[
{"content": "Read the existing exporter", "status": "completed"},
{"content": "Add CSV serializer", "status": "in_progress"},
{"content": "Wire up the API route"}, # status defaults to pending
{"content": "Write tests", "status": "pending"},
],
)
print(out.text)tool name: todo ☑ Read the existing exporter ▶ Add CSV serializer ☐ Wire up the API route ☐ Write tests (1/4 completed, 1 in progress, 2 pending)
Glyphs: ☑ completed, ▶ in_progress, ☐ pending. The trailing line is a
live progress tally. Now inspect the structured side — context.state and the
summary metadata the UI/runtime reads:
print("context.state['todos']:")
for item in ctx.state["todos"]:
print(" ", item)
print()
print("summary metadata:", out.metadata["summary"])
print("persist flag :", out.metadata["persist"])context.state['todos']:
{'content': 'Read the existing exporter', 'status': 'completed'}
{'content': 'Add CSV serializer', 'status': 'in_progress'}
{'content': 'Wire up the API route', 'status': 'pending'}
{'content': 'Write tests', 'status': 'pending'}
summary metadata: {'total': 4, 'completed': 1, 'in_progress': 1, 'pending': 2}
persist flag : FalseEvery call replaces the entire list — the model always passes the full,
updated set, never a delta. Below, a second (shorter) call completely
overwrites the first; context.state["todos"] reflects only the second list.
todo = TodoTool()
ctx = ToolContext(prompt="multi-step job")
# First call: 3 items.
todo.run(ctx, todos=[
{"content": "Step A", "status": "completed"},
{"content": "Step B", "status": "in_progress"},
{"content": "Step C", "status": "pending"},
])
print("after call 1:", [t['content'] for t in ctx.state['todos']])
# Second call: a DIFFERENT, shorter list — fully replaces the first.
out = todo.run(ctx, todos=[
{"content": "Step B", "status": "completed"},
{"content": "Step C", "status": "in_progress"},
])
print("after call 2:", [t['content'] for t in ctx.state['todos']])
print()
print(out.text)after call 1: ['Step A', 'Step B', 'Step C'] after call 2: ['Step B', 'Step C'] ☑ Step B ▶ Step C (1/2 completed, 1 in progress, 0 pending)
Step A is gone after call 2 — the second list replaced the first wholesale.
This is why the model is instructed to always send the full updated set.
Only the three statuses are accepted; content is required. Invalid input
returns a ToolOutput carrying an error (and does not mutate state), so
the model gets actionable feedback instead of a crash.
todo = TodoTool()
ctx = ToolContext(prompt="x")
# Valid: all three statuses.
ok = todo.run(ctx, todos=[
{"content": "a", "status": "pending"},
{"content": "b", "status": "in_progress"},
{"content": "c", "status": "completed"},
])
print("valid summary:", ok.metadata["summary"])
# Invalid status -> error, state unchanged.
bad = todo.run(ctx, todos=[{"content": "a", "status": "blocked"}])
print("bad status text :", bad.text)
print("bad status error:", bad.metadata.get("error"))
# Empty content -> error.
empty = todo.run(ctx, todos=[{"content": " "}])
print("empty content :", empty.metadata.get("error"))
# state still holds the last VALID list (the invalid calls didn't touch it).
print("state preserved :", [t['content'] for t in ctx.state['todos']])valid summary: {'total': 3, 'completed': 1, 'in_progress': 1, 'pending': 1}
bad status text : Invalid todo at index 0: unknown status 'blocked'. Must be one of pending, in_progress, completed.
bad status error: todo at index 0 has invalid status 'blocked'
empty content : todo at index 0 has empty content
state preserved : ['a', 'b', 'c']"todo"Agent.with_builtins(...) includes the todo tool in its catalogue, so a
tool-calling model can maintain its own checklist with no extra wiring. We
just confirm it's present (offline — no model call needed).
from shipit_agent import Agent
from shipit_agent.llms import ShipitLLM
agent = Agent.with_builtins(llm=ShipitLLM(), project_root="/tmp")
tool_names = [getattr(t, "name", None) for t in agent.tools]
print("'todo' is a builtin tool:", "todo" in tool_names)
todo_tool = next(t for t in agent.tools if getattr(t, "name", None) == "todo")
print("schema function name :", todo_tool.schema()["function"]["name"])
print("required params :",
todo_tool.schema()["function"]["parameters"]["required"])'todo' is a builtin tool: True schema function name : todo required params : ['todos']
On a long, multi-step task it's easy to lose the thread — what's done, what's
in flight, what's left? By writing all the steps up front and keeping exactly
one item in_progress, the agent turns its internal plan into a durable,
inspectable artifact:
context.state["todos"] and the summary give the UI a
live progress bar — no log scraping.in_progress discipline keeps the agent on one thing
at a time, which makes long runs steadier and easier to follow.It's a small tool with an outsized effect on how legible an autonomous run feels — and it's completely provider-agnostic.