Skip to main content

Tool Definitions

Minimal (No Schema)

Use when you trust the model and want minimal overhead:
# System prompt
TOOLS = """
Available tools:
- search{query:str max_results:int} - Search the web
- calculate{expression:str} - Evaluate math
- browse{url:str} - Fetch webpage content
"""

# Parse tool call from model output
tool_call = glyph.parse(model_output)
name = tool_call.type_name  # "search"
args = tool_call.fields     # {"query": "weather NYC", "max_results": 10}

With Full Validation

Recommended for production systems:
registry = glyph.ToolRegistry()

registry.register("search", {
    "query": {"type": "str", "required": True, "min_len": 1},
    "max_results": {"type": "int", "min": 1, "max": 100, "default": 10},
})

registry.register("calculate", {
    "expression": {"type": "str", "required": True},
})

registry.register("browse", {
    "url": {"type": "str", "required": True, "pattern": r"^https?://"},
})

# Validate before execution
result = registry.validate(tool_call)
if not result.valid:
    return f"Invalid tool call: {result.errors}"

System Prompt Pattern

Clear, concise tool documentation for LLMs:
You have access to these tools:

search{query:str max_results:int[1..100]=10}
  Search the web. Returns list of results.

calculate{expression:str}
  Evaluate a mathematical expression. Returns number.

browse{url:str}
  Fetch and summarize a webpage. Returns text.

To use a tool, output:
ToolName{arg1=value arg2=value}

Example:
search{query="python async tutorial" max_results=5}

Agent Patterns

ReAct Loop

Reason and Act pattern with GLYPH state:
async def react_loop(goal: str, max_turns: int = 10):
    state = {"goal": goal, "observations": [], "turn": 0}

    for turn in range(max_turns):
        # Format state as GLYPH (compact)
        state_glyph = glyph.from_json(state)

        prompt = f"""
State: {state_glyph}

Think step by step, then either:
1. Use a tool: ToolName{{args}}
2. Return final answer: Answer{{result="..."}}
"""

        response = await llm.generate(prompt)
        parsed = glyph.parse(response)

        if parsed.type_name == "Answer":
            return parsed.fields["result"]

        # Execute tool
        result = await execute_tool(parsed.type_name, parsed.fields)
        state["observations"].append({
            "tool": parsed.type_name,
            "args": parsed.fields,
            "result": result,
        })
        state["turn"] += 1

    raise MaxTurnsExceeded()

Multi-Agent Coordination

Using stream IDs for agent communication:
# Coordinator assigns SIDs
PLANNER_SID = 1
EXECUTOR_SID = 2
CRITIC_SID = 3

# Planner sends task
writer.write_frame(
    sid=EXECUTOR_SID,
    seq=0,
    kind="doc",
    payload=glyph.emit(glyph.struct("Task",
        action="search",
        query="latest AI news",
    ))
)

# Executor sends result
writer.write_frame(
    sid=PLANNER_SID,
    seq=0,
    kind="doc",
    payload=glyph.emit(glyph.struct("Result",
        task_id=1,
        status="complete",
        data=search_results,
    ))
)

# Critic sends feedback
writer.write_frame(
    sid=EXECUTOR_SID,
    seq=1,
    kind="doc",
    payload=glyph.emit(glyph.struct("Feedback",
        task_id=1,
        score=0.8,
        suggestion="Include source URLs",
    ))
)

Checkpoint / Resume Pattern

Safe state persistence:
def save_checkpoint(agent_state, path: str):
    with open(path, "w") as f:
        f.write(glyph.emit(agent_state))

def load_checkpoint(path: str):
    with open(path) as f:
        return glyph.parse(f.read()).value

# Save periodically
if turn % 5 == 0:
    save_checkpoint(state, f"checkpoint_{turn}.glyph")

# Resume from crash
state = load_checkpoint("checkpoint_latest.glyph")

State Management

Simple: Full State Per Message

Best for short sessions:
# Agent state as GLYPH
state = glyph.struct("AgentState",
    goal="Find weather in NYC",
    memory=[
        {"query": "NYC weather", "result": "72F sunny"},
    ],
    turn=3,
)

# Include in context
context = f"""
Current state:
{glyph.emit(state)}

Continue toward the goal.
"""

Advanced: Patches with Verification

Optimal for long-running agents:
from glyph import stream

# Initial state
writer.write_frame(sid=1, seq=0, kind="doc", payload=glyph.emit(state))

# After each action, send patch
patch = glyph.patch([
    ("=", "turn", 4),                           # Set value
    ("+", "memory", new_memory_entry),          # Append
    ("~", "token_count", tokens_used),          # Increment
])

# Include base hash for safety
base_hash = stream.state_hash(current_state)
writer.write_frame(
    sid=1,
    seq=1,
    kind="patch",
    payload=patch,
    base=base_hash,  # Receiver rejects if state diverged
)

Progress Reporting

Non-intrusive status updates:
# Send progress during long operations
writer.write_frame(
    sid=1,
    seq=seq,
    kind="ui",
    payload=glyph.emit(glyph.struct("Progress",
        pct=0.45,
        msg="Processing batch 9 of 20",
        eta_seconds=120,
    ))
)

# Client handles UI updates
@handler.on_ui
def handle_ui(sid, seq, payload, state):
    event = glyph.parse(payload)
    if event.type_name == "Progress":
        update_progress_bar(event.fields["pct"], event.fields["msg"])

Anti-Patterns

Don’t: Parse with Regex

# BAD - breaks on nested structures
match = re.search(r'search\{query="([^"]+)"', response)

Don’t: Validate After Full Generation

# BAD - wastes tokens on invalid calls
response = await llm.generate(prompt)  # 50 tokens
result = glyph.parse(response)
if result.type_name not in allowed_tools:  # Discovered too late
    raise Error()

Don’t: Send Full State Every Turn

# BAD - O(n) tokens per turn for n observations
state["observations"].append(new_obs)
send_full_state(state)  # Gets bigger every turn

Don’t: Inline Large Data

# BAD - bloats context
state = {"embeddings": [[0.1, 0.2, ...] * 1536] * 100}  # Huge

Best Practices

Streaming Validation Decision Tree

ConditionAction
Unknown tool nameCancel at token ~3-5
Wrong argument typeCancel when type detected
Constraint violationCancel when value complete
Missing required argWait until } then error

Token Efficiency Guidelines

High-value scenarios for GLYPH:
  • System prompts with tool definitions (sent every request)
  • Conversation history (grows over time)
  • Batch operations (thousands of records)
  • Multi-turn agents (state persisted across calls)
Token savings by structure:
PatternJSONGLYPHSavings
Tool call422833%
Tool list (5 tools)1809547%
Agent state (small)1207538%
Agent state (large)50029042%
Tabular data (10 rows)32014555%

State Management Guidelines

Use full state when:
  • Sessions are short (< 10 turns)
  • State is small (< 1KB)
  • Simplicity is more important than efficiency
Use patches when:
  • Long-running sessions (> 10 turns)
  • State grows over time
  • Multiple agents share state
  • Network bandwidth is limited

Error Handling

Graceful degradation:
try:
    result = glyph.parse(llm_output)
except ParseError:
    # Fall back to extracting data with LLM
    result = extract_structured_data(llm_output)
Progressive validation:
# Validate what you can, execute what's valid
if result.partial_valid:
    # Execute with defaults for missing fields
    execute_with_defaults(result.tool_name, result.fields)
else:
    # Full validation failure
    request_clarification()
Production Checklist
  • Use streaming validation for all tool calls
  • Implement checkpoint/resume for long tasks
  • Add state hash verification for multi-agent systems
  • Use tabular mode for homogeneous lists (>3 items)
  • Monitor token savings with analytics
  • Add fallback parsing for malformed outputs

Build docs developers (and LLMs) love