Draft — Startr.Team Agent Framework Specification v0.1.0

Examples

These pages render the literal source of the runnable examples in the site repo's examples/ directory — built from the same files you run, so they cannot drift. Each example assumes startr-team is installed and an LLM is configured (Configuration).

hello_agent

The smallest Startr.Team agent: core only, no messaging. One persona file, one registered demo tool, one call to `Agent().run(...)`. Every run writes a `vault/` directory you can read: the plan, each step's artifacts, and full LLM transcripts.

hello_agent/agent.yaml
# Who this agent is. Swap this file and the same engine is a different agent.
name: Hello
pronouns: they/them
role: a note-taking assistant
focus: short, useful working notes

mission: >
  Turn small requests into clear, ready-to-use notes. Nothing fancy;
  everything accurate.

priorities:
  - Never invent facts, names, dates, or commitments
  - Clarity and brevity over length

disposition:
  tone: plain and friendly
  traits: [methodical, concise]

scope_note: >
  If a request needs more than a note, say so plainly rather than guessing.
hello_agent/run.py
"""hello_agent — the smallest Startr.Team agent.

Setup:
    export OPENAI_API_KEY=...        # or OPENAI_BASE_URL for a local model
    export AGENT_PROFILE=agent.yaml  # this folder's persona
    python run.py "Draft a Tuesday standup note: shipped the vault, next is intake."
"""

import sys

try:  # optional: read a .env if python-dotenv is around
    from dotenv import load_dotenv

    load_dotenv()
except ImportError:
    pass

from startr_team import Agent, register_tools
from startr_team.tools import todays_date

register_tools(todays_date)  # the registry starts empty; you add your tools

instruction = " ".join(sys.argv[1:]) or "Write a two-line note on why plans should be readable."

agent = Agent(verbose=True)
answer = agent.run(instruction)

print("\n--- final answer ---\n")
print(answer)
print(f"\nAudit trail: {agent.vault.run_dir}/")

herald

A messaging agent: composes a dispatch from an instruction, resolves recipients from a roll (names and groups, not raw addresses), and delivers over a channel — **dry-run by default**. Nothing is sent unless a human passes `--send`. Every attempt, real or dry, writes a delivery record to the vault.

herald/agent.yaml
name: Herald
pronouns: they/them
role: a herald agent
focus: carrying word to the right people, faithfully

mission: >
  Compose dispatches that read human, carry them out only when told, and
  keep an honest record of every delivery.

priorities:
  - Never invent facts, names, dates, or commitments
  - Respect people's time and attention
  - Every dispatch traces back to who authorized it

disposition:
  tone: warm, precise, and calm
  traits: [methodical, discreet, dependable]

scope_note: >
  If a request falls outside coordination and communication, say so plainly
  rather than guessing.
herald/participants.example.csv
id,name,pronouns,email,signal,whatsapp,groups,aliases,role,standing,avg_response_hours,asked,fulfilled,last_contacted,last_responded,notes,created,updated
izzy,Izzy,she/her,[email protected],+15550001111,,design team,iz,,,,0,0,,,,2026-01-01T00:00:00+00:00,2026-01-01T00:00:00+00:00
alex,Alex,he/him,[email protected],,+15550002222,design team;ops,,,,,0,0,,,,2026-01-01T00:00:00+00:00,2026-01-01T00:00:00+00:00
herald/run.py
"""herald — compose a dispatch, resolve recipients from the roll, deliver gated.

Dry run (default — validates recipients, sends NOTHING):
    python run.py "Tell the design team standup moved to 10" --to "design team"

Real send (a human flips the switch; SMTP_* env must be configured):
    python run.py "..." --to "design team" --send
"""

import argparse

try:
    from dotenv import load_dotenv

    load_dotenv()
except ImportError:
    pass

from startr_team import Agent
from startr_team.channels import get_channel
from startr_team.clean import clean_body, split_subject
from startr_team.dispatch import Dispatch, deliver, first_line_subject
from startr_team.roll import ParticipantStore, resolve_recipients

parser = argparse.ArgumentParser(description="Compose a dispatch; optionally send it.")
parser.add_argument("instruction", help="What to say.")
parser.add_argument("--to", required=True, help="Names, groups, or literal addresses (comma-separated).")
parser.add_argument("--channel", choices=["email", "signal", "whatsapp"], default="email")
parser.add_argument("--send", action="store_true", help="Actually send (default: dry-run).")
args = parser.parse_args()

# 1. Resolve who — from the roll, by name or group. Fails loudly on unknowns.
store = ParticipantStore(path="participants.example.csv")
recipients = resolve_recipients(
    [t.strip() for t in args.to.split(",")], args.channel, store
)

# 2. Compose — the agent writes; the cleanup pipeline makes it ready-to-send.
agent = Agent()
subject, body = split_subject(clean_body(agent.run(args.instruction)))

# 3. Deliver — deterministic and gated. The LLM never touches this switch.
dispatch = Dispatch(
    body=body,
    recipients=recipients,
    subject=subject or first_line_subject(body),
    channel=args.channel,
)
result = deliver(dispatch, get_channel(args.channel), dry_run=not args.send, vault=agent.vault)

print()
print(result["record"])
if result["dry_run"]:
    print("\n[dry-run] Nothing was sent. Re-run with --send to deliver.")

review_loop

A graph plan: `plans/` is a wikilinked markdown vault — open it in Obsidian or SilverBullet and you'll see the graph. The agent drafts, reviews its own work, and loops back to revise until the review approves — with a visit budget so the loop cannot spin. The `review` node runs under `role: review`, so `LLM_REVIEW_MODEL` can put a stronger model on judgment while a cheap one drafts. The run vault records the traversal trace: every node, every edge taken, and why.

review_loop/plans/draft.md
---
type: start
status: pending
---
# Draft the note

Write a short, warm note for the requested update. Keep it under six sentences,
plain prose, no headings.

## Next
- next -> [[review]]
review_loop/plans/finish.md
---
type: step
status: pending
---
# Finish

Present the approved note exactly as it should be delivered.
review_loop/plans/review.md
---
type: step
status: pending
role: review
max_visits: 3
---
# Review the draft

Read the latest draft critically. Is it accurate to the request, warm in tone,
and free of invented facts? State clearly whether it is approved or what must
change.

## Next
- approved -> [[finish]]
- needs revision -> [[draft]]
review_loop/run.py
"""review_loop — an agent traversing a graph plan with a draft → review cycle.

The plan is the `plans/` folder: three markdown notes wired with [[wikilinks]].
Open it in Obsidian while this runs — node `status:` frontmatter updates live,
and an interrupted run resumes where it stopped.

    export OPENAI_API_KEY=...          # or OPENAI_BASE_URL for a local model
    # optional: a stronger judge than drafter
    # export LLM_REVIEW_MODEL=gpt-4.1
    python run.py "a note thanking the team for shipping the vault feature"
"""

import sys
from pathlib import Path

try:
    from dotenv import load_dotenv

    load_dotenv()
except ImportError:
    pass

from startr_team import Agent
from startr_team.plans import load_plan
from startr_team.plans.markdown_io import update_status

args = [a for a in sys.argv[1:] if a != "--resume"]
resume = "--resume" in sys.argv[1:]
topic = " ".join(args) or "a note thanking the team for a good sprint"

plan_dir = Path(__file__).parent / "plans"

# Fresh demo run by default; pass --resume to pick up an interrupted traversal.
if not resume:
    for node_id in load_plan(plan_dir).nodes:
        update_status(plan_dir, node_id, "pending")

agent = Agent(verbose=True)
answer = agent.run_plan(plan_dir, query=f"Write {topic}")

print("\n--- final answer ---\n")
print(answer)
print(f"\nTraversal trace and artifacts: {agent.vault.run_dir}/")

To run one: clone the site repo, cd examples/<name>, set your environment, and python run.py. The review_loop plan vault (examples/review_loop/plans/) opens directly in Obsidian or SilverBullet — watch node status change while the agent runs.