yoagent

repository·main·Indexed 41 days ago

https://github.com/yologdev/yoagent

A specialized agent loop for Rust focusing on tool execution and multi-turn LLM interactions. Version 0.18.1 provides native support for 7 LLM protocols (including Anthropic, OpenAI, Gemini, and Bedrock), tree-based session history, and tool middleware. It features integration with Model Context Protocol (MCP) servers, OpenAPI 3.0 specifications, and support for sub-agents and shared state management.

Tokens
73.6K
Snippets
190
Records
287
Agent score
54%

What's inside yoagent

  1. Overview of yoagent features and capabilities

    main

    yoagent is a Rust library for building LLM-powered agents with the following core capabilities:

    Control & Lifecycle

    • Streaming & Events: Full AgentEvent stream for text deltas, thinking, and tool execution.
    • Tool Middleware: Async hooks to allow, modify, or deny tool calls.
    • Execution Limits: Control via max turns, max tokens, wall-clock time, and CancellationToken.
    • Error Handling: Built-in retry logic with exponential backoff and jitter for rate-limits and network errors.

    Models & Tools

    • Multi-Provider Support: Native implementations for Anthropic, OpenAI (Completions and Responses), Azure, Gemini, Vertex, Bedrock, and OpenAI-compatible gateways.
    • Tool Integration: Built-in tools (bash, file operations, ripgrep) and support for MCP (Model Context Protocol) servers and OpenAPI specs.
    • Structured Outputs: Native schema-validated replies where supported by the provider.

    Scaling & Production

    • Sub-agents: Delegate to child loops with independent models, tools, and limits.
    • Context Management: Token tracking and tiered compaction (truncating tool outputs, summarizing old turns, or dropping middle turns).
    • Persistence: Save/restore runs and session trees (branching history with checkpoints).
    • Observability: tracing integration for spans (loop, LLM stream, tools) including token and cost tracking.
  2. How the OpenAPI Tool Adapter works

    main

    The adapter maps OpenAPI operations to AgentTool implementations using the following logic:

    AgentTool methodMapped from
    name()operationId (with optional prefix)
    label()summary or operationId
    description()description or summary
    parameters_schema()Combined JSON Schema from path/query/header params + request body

    Request Lifecycle:

    1. The adapter substitutes path parameters (e.g., /pets/{petId} → /pets/123).
    2. Query parameters are added as ?key=value.
    3. Header parameters are added.
    4. Authentication from OpenApiConfig is applied.
    5. The request body is sent as JSON (if applicable).
    6. The response text (including status code) is returned to the LLM.

    Note: Non-2xx responses are not treated as errors; they are returned as text so the LLM can reason about them and attempt retries or adjustments.

  3. How lifecycle callbacks are timed within the loop

    main

    Understanding the execution order is critical for managing state and limits. The sequence is as follows:

    1. Inject pending messages (steering/follow-up)
    2. Check execution limits
    3. before_turn(messages, turn_number) (Return false to abort)
    4. Compact context
    5. Stream LLM response
    6. Check for error/abort
      • If StopReason::Error $\rightarrow$ on_error(message)
      • after_turn(messages, usage) is called even on error/abort
    7. Execute tool calls
    8. Track turn
    9. after_turn(messages, usage)
    10. Emit TurnEnd event
  4. Interrupt the agent using Steering

    main

    Steering messages allow you to interrupt the agent between tool executions. When a steering message is injected:

    1. The current tool finishes normally.
    2. All remaining tool calls in the current batch are skipped (marked with is_error: true and the reason "Skipped due to queued user message").
    3. The steering message is added to the context.
    4. The loop continues with a new LLM call incorporating the interruption.

    Example usage:

    // While agent is running tools, redirect it:
    agent.steer(AgentMessage::Llm(Message::user("Stop that. Instead, explain what you found.")));
  5. Understand ToolContext for execution control

    main

    The ToolContext struct provides the execution environment for a tool call. It allows tools to interact with the agent's lifecycle and the UI:

    • tool_call_id: Correlates events for a specific tool call.
    • cancel: A CancellationToken. Long-running tools should periodically check ctx.cancel.is_cancelled() to support graceful termination.
    • on_update: An optional callback (ToolUpdateFn) to stream partial ToolResult updates to the UI.
    • on_progress: An optional callback (ProgressFn) to emit user-facing progress messages.
    pub struct ToolContext {
        pub tool_call_id: String,
        pub tool_name: String,
        pub cancel: CancellationToken,
        pub on_update: Option<ToolUpdateFn>,
        pub on_progress: Option<ProgressFn>,
    }
  6. What are Skills and how do they work

    main

    Skills extend an agent with domain expertise using the AgentSkills open standard. A skill is a directory containing a SKILL.md file with instructions that the agent can load on demand.

    Skills use progressive disclosure to manage context efficiently:

    1. Metadata (~100 tokens/skill): The name and description are always included in the system prompt.
    2. Instructions (<5k tokens): The body of SKILL.md is loaded only when the agent decides the skill is relevant based on its description.
    3. Resources (unlimited): Scripts, references, and assets are loaded only when needed.

    Because the agent decides when to activate a skill based on the description alone, no specialized trigger engine is required.

  7. Detect and handle tool call loops

    main

    To prevent an agent from repeatedly calling the same tool with identical arguments (a common failure mode), yoagent provides loop detection. By default, max_consecutive_identical_tool_calls is set to Some(3).

    When a loop is detected, the system follows a steering pattern:

    1. First trip: Injects a steering message to guide the model and continues the run.
    2. Subsequent trip: If the same signature repeats, the run is aborted.

    Both events emit AgentEvent::LoopDetected { tool_name, repetitions, aborted }, allowing your UI to notify the user or your audit logs to distinguish a loop abort from a standard turn limit.

    Key behaviors:

    • Signature Matching: Comparisons use serde_json::Value, meaning key order in JSON does not affect detection.
    • Batch Awareness: Detection works both across turns and within a single batch (e.g., if a model emits three identical parallel tool calls in one message).
    • Consecutive Requirement: The detector only tracks consecutive identical calls. An alternating pattern like [a, b, a, b] will not trigger detection, as this allows agents to legitimately iterate through lists.

    You can disable this feature by setting the limit to None in your ExecutionLimits configuration.

    ExecutionLimits::default().with_max_consecutive_identical_tool_calls(None)  // off
  8. How Cache-Stable Compaction works in yoagent

    main

    To ensure prompt caching is effective, yoagent uses 'Cache-Stable Compaction'. A cache hit requires the new request to share a byte-identical prefix with the previous one. If compaction rewrites a message that has already been sent, the prefix is invalidated, and all subsequent tokens are billed at full price.

    yoagent (v0.15+) implements three design constraints to prevent this churn:

    1. Idempotent History: Compaction is a pure function. Tool-output truncation uses a constant [... N lines truncated ...] marker, and generated summaries inherit the timestamp of the content they replace, ensuring the same history always produces the same bytes.
    2. Bounded Output: Instead of a global cap, yoagent applies limits on append to keep prefixes intact.
      • Command output (bash, search): Uses tool_output_max_lines (default 200), providing a head+tail summary.
      • File reads (read_file): Uses DEFAULT_READ_MAX_LINES (500) and uses paging instead of truncation to remain lossless.
    3. Adaptive Compaction Targets: Instead of a fixed ratio, yoagent uses compact_headroom_turns to target a specific number of turns before the next compaction occurs, allowing the system to self-adjust to the session's growth rate.
  9. Understand what GASP records and how to handle secrets

    main

    GASP records a semantic event log (state/events.jsonl) that tracks:

    • run.started: When a loop starts (the goal is included in the commit trailer).
    • model.called / model.finished: Paired nodes for each assistant turn.
    • tool.called / tool.finished: Tool executions with success flags.
    • run.finished: The final outcome (completed, error, aborted, etc.).

    Data Retention & Privacy:

    • The log stores bounded one-line summaries: the verbatim task string, model IDs, and the first 200 characters of tool inputs, tool outputs, and assistant text. It does not store full transcripts.
    • Security: Because the Git history is append-only and intended to be shared, it is difficult to scrub. If your tools handle sensitive data, you must install a redacting summarizer using recorder.with_summarizer(...) before starting the recording.
  10. How Session Trees work in yoagent

    main

    In yoagent, a Session manages conversation history as a tree rather than a flat list. Each entry contains an id and a parent_id. The current state of the conversation is represented by a head pointer to a specific branch tip.

    This tree structure enables several non-linear workflows:

    • Forking: You can seek to any previous point in history and start a new branch. Appending new messages after a seek creates a new branch without overwriting the original history.
    • Checkpoints: You can label specific states using checkpoint(label) and return to them later using seek_checkpoint(label).
    • Edit & Re-run: You can change an earlier turn by branching from that point; the original branch remains intact.

    When using a session with an Agent, you can retrieve the messages for the current branch using session.path_messages() and initialize an agent with that history using Agent::with_messages().

    use yoagent::{Agent, Session, provider::ModelConfig};
    
    let mut session = Session::new();
    
    // ... run agent turns ...
    
    session.checkpoint("first-draft")?;
    
    // Rewind to the checkpoint and branch.
    session.seek_checkpoint("first-draft")?;
    
    // Initialize agent with only this branch's history
    let mut agent = Agent::from_config(ModelConfig::anthropic("claude-sonnet-5", "Sonnet 5"))
        .with_messages(session.path_messages());
  11. How the Agent Loop works

    main

    The agent loop is the core execution cycle of yoagent. It follows a pattern of:

    1. Adding user prompts to the context.
    2. Entering an inner loop that handles LLM calls, tool extraction, and tool execution.
    3. Checking for Steering messages (interruptions) between tool calls.
    4. Checking for Follow-Up messages after the agent would normally stop.
    5. Repeating if follow-ups exist or emitting the final response.

    This cycle allows the agent to react to user interruptions mid-task and automatically proceed to subsequent tasks without manual re-prompting.

    User prompt → LLM call → Tool execution → LLM call → ... → Final response
  12. Provider enforcement mechanisms for structured outputs

    main

    Different providers use different underlying mechanisms to enforce JSON schemas. yoagent handles the translation, but you must be aware of provider-specific dialect requirements:

    • Anthropic: Uses a forced tool call. A synthetic tool is built from your schema and tool_choice is used to force it. Note: This disables 'extended thinking' for that specific request as they are mutually exclusive at the API level.
    • OpenAI-compatible: Uses response_format: {type: "json_schema", strict: true}.
    • Google Gemini: Uses generationConfig.responseSchema with a JSON mime type. Gemini uses an OpenAPI-style schema dialect; schemas are passed through as given. Note: Gemini rejects $defs or $ref.
    • OpenAI Responses / Azure / Vertex / Bedrock: These are not yet wired. A warning is logged, and the model replies as free text, which yoagent then attempts to parse into your target type T.