AI Agent Scaffold
Generates a production-grade AI agent skeleton directly in the target project. The scaffold follows battle-tested patterns from the ai-skel-ts reference implementation.
When to Use
- User asks to create a new AI agent or AI-powered application
- User asks to add LLM integration to an existing project
- User mentions "scaffold", "skeleton", "ai-skel", or "agent framework"
Architecture Overview
The skeleton consists of 10 modules organized into 4 layers:
+---------------------------------------------------+
| Agent Layer |
| Agent (stateful conversation + tool dispatch) |
+----------------+----------------+-----------------+
| LLM Layer | Tools Layer | Session Layer |
| LLM Request | MCP Client | History Compact|
| (retry, | (protocol | (trim or |
| validate, | bridge, | summarize, |
| observe) | namespace) | tool-pair |
| | | consistency) |
+----------------+----------------+-----------------+
| Observability Layer |
| Logger | Cost Tracker | Run Context | Debug I/O |
+---------------------------------------------------+
| Content Layer |
| Local Fetcher | Brave Search | Jina Scraper |
+---------------------------------------------------+
Scaffold Workflow
When generating the scaffold, follow these steps:
Step 1: Detect Project Context
- Identify the target programming language and runtime
- Identify the package manager and dependency management approach
- Identify existing project structure conventions (src/, lib/, internal/, etc.)
- Determine which LLM SDK is appropriate for the language:
- TypeScript/JavaScript: Vercel AI SDK (
aipackage) - Python:
openai,anthropic,google-generativeai, orlitellm - Go:
github.com/sashabaranov/go-openaior provider SDKs - Rust:
async-openaior provider SDKs - Other: use the provider's HTTP API directly
- TypeScript/JavaScript: Vercel AI SDK (
Step 2: Generate Modules Bottom-Up
Generate modules in this order (each depends only on previously generated ones):
- Logger — zero dependencies
- Cost Tracker — depends on Logger
- Run Context — depends on Logger
- LLM Requester — depends on Logger, Cost Tracker, Run Context
- Session Compactor — depends on Logger
- MCP Client — depends on Logger (optional, skip if not needed)
- Agent — depends on LLM Requester, Session Compactor, MCP Client, Run Context
- Fetchers — depends on Run Context (optional, add as needed)
Step 3: Add Tests
For each module, generate at minimum:
- Unit tests with mocked dependencies
- One integration-style test with real interfaces (but mocked HTTP/LLM)
Step 4: Wire Entry Point
Create a main entry point (mod.ts / main.py / main.go / etc.) that re-exports public API.
Module Specifications
Each module below includes: purpose, key interfaces, algorithm, and critical implementation details. For full reference implementations in TypeScript, see the reference files.
1. Logger
Purpose: Structured logging with levels, context, and timestamps.
Interface:
- Constructor:
(context: string, logLevel: "debug"|"info"|"warn"|"error") - Methods:
debug(msg, meta?),info(msg, meta?),warn(msg, meta?),error(msg, error?) - Factory:
createFromLevelString(context, levelString)— validates level, warns and falls back to "debug"
Algorithm: Compare numeric level values (debug=0, info=1, warn=2, error=3). Log only if message level >= configured level.
Key detail: Error objects need special serialization (JSON.stringify(Error) returns "{}"). Extract name, message, stack explicitly.
Reference: reference-observability.md
2. Cost Tracker
Purpose: Accumulate LLM token usage and costs across requests.
Interface:
addCost(cost: number)— adds USD cost, increments request counteraddTokens(inputTokens, outputTokens)— accumulates token countsgetReport()→{totalCost, totalInputTokens, totalOutputTokens, totalTokens, requestCount}getFormattedReport()→ human-readable stringreset()— zeroes all counters
Pattern: Singleton (for global tracking) OR instance (for per-run tracking). Provide both options.
Reference: reference-observability.md
3. Run Context
Purpose: Immutable execution context with unique run ID and debug artifact management.
Interface:
RunContext { runId, debugDir, logger, startTime, saveDebugFile() }- Factory:
createRunContext(logger, debugDir, runId?)— auto-generates reverse-sortable ID if not provided getSubDebugDir(ctx, stageDir)— returns subdirectory path for a stagesafeSanitize(obj)— recursively handles Error, Buffer, circular refs for serialization
Critical algorithm — Reverse-sortable Run ID:
maxMs = Date.UTC(9999, 11, 31, 23, 59, 59, 999)
reversedMs = maxMs - Date.now()
runId = toISO(reversedMs) + microSequence
This makes newest runs sort first in file listings.
Reference: reference-core.md
4. LLM Requester
Purpose: Provider-agnostic LLM interface with retry, validation, self-correction, and observability.
This is the most complex module. Read reference-core.md carefully.
Interface:
ModelURI— parsesprotocol://provider/model?params(e.g.,chat://openai/gpt-4o?timeout=60000)createLlmRequester(modelUri, logger, costTracker, ctx)→ requester function- Requester function:
(messages, identifier, schema?, tools?, maxSteps?, stageName, settings?) → GenerateResult GenerateResult { result, text, toolCalls, toolResults, newMessages, steps, estimatedCost, inputTokens, outputTokens, validationError?, rawResponse? }
Core algorithm (retry loop with self-correction):
parse URI → create provider instance → return requester function
requester(messages, schema, tools, ...):
write initial YAML log file
for attempt = 1..MAX_RETRIES(3):
set timeout via AbortController
call LLM SDK (generateText / chat completion)
on success:
track cost/tokens
aggregate newMessages from steps
update YAML log
return result
on validation error (schema mismatch):
append assistant message (raw response)
append user message (error description) <- SELF-CORRECTION
update YAML log
retry
on fatal API error (401/403/400):
return error immediately (no retry)
on transient error:
exponential backoff: delay = 1000ms * 2^(attempt-1) + jitter(20%)
retry
Key details:
- API key resolution: URI param > environment variable (
{PROVIDER}_API_KEY) - AbortController.abort() must be wrapped in try-catch (listeners can throw)
- Mask apiKey in URI toString() for logging
- Settings from URI are defaults; per-request settings override them
Reference: reference-core.md
5. Session Compactor
Purpose: Manage conversation context window by trimming or summarizing history.
Interface:
HistoryCompactor { compact(messages) → messages, estimateSymbols(message) → number }SimpleHistoryCompactor(maxSymbols)— trims oldest messagesSummarizingHistoryCompactor(maxSymbols, summaryTokenThreshold, summaryGenerator)— LLM-powered
Algorithm (SimpleHistoryCompactor):
trimBySymbolLimit:
iterate messages from newest to oldest
accumulate symbol weight (JSON.stringify length)
stop when budget exceeded
ensureToolConsistency:
collect all tool-call IDs and tool-result IDs
remove messages with orphaned tool-calls (no matching result)
remove messages with orphaned tool-results (no matching call)
Algorithm (SummarizingHistoryCompactor):
estimate token count (symbols / 4)
if under threshold → delegate to SimpleHistoryCompactor
if over threshold:
split: messages[0..splitPoint] → summarize, messages[splitPoint..] → keep
splitPoint = index before last assistant message
call SummaryGenerator.generateSummary(toSummarize) → summary message
ensure proper message alternation (add dummy user message if needed)
apply tool consistency check
on error → fallback to SimpleHistoryCompactor
Reference: reference-session.md
6. MCP Client (optional)
Purpose: Bridge between Model Context Protocol servers and the LLM tool interface.
Interface:
McpClientWrapper(config, logger, name)— config is either{type:"stdio", command, args}or{type:"sse", url}connect()/disconnect()— lifecycle managementgetTools()→Record<string, Tool>— discovers and converts tools
Key details:
- Tool namespacing: prefix tool names with server name (
serverName__toolName) to prevent collisions - Tool conversion: MCP JSON Schema → LLM SDK Tool format (use
jsonSchema()helper if available) - Execute: call MCP
tools/call, extract text content from response parts
Reference: reference-session.md
7. Agent
Purpose: Stateful conversation runner with tool integration and history management.
Interface:
Agent(llm, mcpClients?, ctx, systemPrompt?, compactor?, tools?)init()— connects MCP clients, aggregates all toolsrun(input)→GenerateResult— full access to resultschat(input)→string— convenience wrappergetHistory()→ messages array
Algorithm:
constructor:
store params
if systemPrompt: push system message to history
init:
for each mcpClient:
connect()
getTools() → merge into this.tools
run(input):
push user message to history
if compactor: compact history
call llm(messages, tools, maxSteps=10, ...)
if error: throw
append all newMessages to history
return result
chat(input):
return run(input).text
Reference: reference-core.md
8-10. Fetchers (optional)
Three content acquisition strategies. Add only what the project needs.
Local Fetcher: HTML → clean text + metadata via Readability algorithm Brave Search: REST API client with 429 retry and rate-limited batch search Jina Scraper: Dual-endpoint (search + reader) API client with rich options
Reference: reference-fetchers.md
Language Adaptation Guidelines
When adapting patterns to a target language:
| Concept | TypeScript | Python | Go | Rust |
|---|---|---|---|---|
| Interfaces | interface / type |
Protocol / ABC |
interface |
trait |
| Generics | <T> |
Generic[T] |
[T any] |
<T> |
| Async | async/await |
async/await |
goroutines/channels | async/await |
| Error handling | try/catch + types | try/except + types | error return | Result<T, E> |
| Dependency injection | constructor params | constructor / __init__ |
struct fields | struct fields |
| Singleton | static instance | module-level / __new__ |
sync.Once |
once_cell::Lazy |
| Schema validation | Zod | Pydantic | struct tags + validator | serde + validator |
| URI parsing | URL class |
urllib.parse |
net/url |
url crate |
File Organization
Adapt to the project's conventions. Suggested structure:
{src_dir}/
+-- agent/
| +-- agent.{ext}
| +-- agent_test.{ext}
+-- llm/
| +-- requester.{ext}
| +-- model_uri.{ext}
| +-- requester_test.{ext}
+-- run_context/
| +-- context.{ext}
| +-- context_test.{ext}
+-- cost_tracker/
| +-- tracker.{ext}
| +-- tracker_test.{ext}
+-- logger/
| +-- logger.{ext}
| +-- logger_test.{ext}
+-- session/
| +-- compactor.{ext}
| +-- summary_generator.{ext}
| +-- compactor_test.{ext}
+-- mcp/ # optional
| +-- client.{ext}
+-- fetchers/ # optional
| +-- local/
| +-- brave/
| +-- jina/
+-- mod.{ext} # public API entry point
Critical Rules
- No stubs: Every function must have a real implementation
- Observability first: Every LLM call writes a YAML debug artifact to debugDir
- Self-correction: On schema validation failure, feed the error back to the LLM
- Tool-pair consistency: Never leave orphaned tool-calls or tool-results in history
- Reverse-sortable IDs: Run IDs sort newest-first in file listings
- Mask secrets: API keys must never appear in logs or debug files
- Exponential backoff:
delay = baseDelay * 2^(attempt-1) + jitter - Safe serialization: Handle Error objects, Buffers, circular references before serializing
- URI-based config: Model configuration via
protocol://provider/model?params - Graceful degradation: Summarization fails → fall back to trimming; MCP fails → continue without tools
Checklist
After generating the scaffold, verify:
- All modules compile/lint without errors
- Tests pass
- LLM Requester has retry loop with self-correction
- Agent preserves all intermediate tool-call messages in history
- Session Compactor maintains tool-call/tool-result pair consistency
- Run Context generates reverse-sortable IDs
- Logger handles Error serialization
- Cost Tracker accumulates across requests
- Debug artifacts written as YAML to structured directories
- API keys masked in all log output