From 6c9db757aef5d1652b1d2aa6c90a635407bb06f0 Mon Sep 17 00:00:00 2001 From: Folarin Akinloye Date: Wed, 14 Oct 2026 07:55:39 +0100 Subject: [PATCH 1/2] feat(docs): redesign as docs-first documentation platform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Transition root and /docs directly to /docs/getting-started/introduction - Implement LangGraph-inspired 3-column layout with 260px map rail, 720px prose measure, and 220px TOC outline - Apply Vercel-inspired design tokens, micro-borders, and high-contrast dark/light themes - Mount search command palette via React Portal at z-[9999] with body scroll lock and ⌘K / '/' hotkeys - Remove blog and roadmap pages while preserving changelog, contributing, privacy, and terms - Update branding to pure text Agentling logo across header and navigation - Add Diátaxis badges, trust signals, and inline helpfulness feedback widget --- content/blog/introducing-agentling.md | 86 ---- content/blog/one-loop-two-modes.md | 62 --- .../blog/skills-that-stay-out-of-the-way.md | 72 --- content/docs/getting-started/introduction.md | 34 +- content/pages/roadmap.md | 58 --- scripts/validate-content.mjs | 30 +- src/app/[slug]/page.tsx | 6 +- src/app/blog/[slug]/opengraph-image.tsx | 31 -- src/app/blog/[slug]/page.tsx | 145 ------ src/app/blog/opengraph-image.tsx | 14 - src/app/blog/page.tsx | 98 ---- src/app/changelog/page.tsx | 40 +- src/app/docs/[section]/[slug]/page.tsx | 170 +++---- src/app/docs/layout.tsx | 6 +- src/app/docs/page.tsx | 59 +-- src/app/feed.xml/route.ts | 36 -- src/app/globals.css | 347 ++++++-------- src/app/layout.tsx | 44 +- src/app/llms.txt/route.ts | 44 +- src/app/page.tsx | 446 +----------------- src/app/search-index.json/route.ts | 4 +- src/app/sitemap.ts | 55 +-- src/components/blog/post-cover.tsx | 45 -- src/components/docs/feedback-widget.tsx | 88 ++++ src/components/docs/sidebar.tsx | 164 ++++--- src/components/docs/toc.tsx | 135 ++++-- src/components/logo.tsx | 56 +-- src/components/search.tsx | 216 +++++---- src/components/site-header.tsx | 121 +++-- src/lib/content/index.ts | 70 +-- src/lib/content/search.ts | 133 +++--- src/lib/site.ts | 2 +- 32 files changed, 928 insertions(+), 1989 deletions(-) delete mode 100644 content/blog/introducing-agentling.md delete mode 100644 content/blog/one-loop-two-modes.md delete mode 100644 content/blog/skills-that-stay-out-of-the-way.md delete mode 100644 content/pages/roadmap.md delete mode 100644 src/app/blog/[slug]/opengraph-image.tsx delete mode 100644 src/app/blog/[slug]/page.tsx delete mode 100644 src/app/blog/opengraph-image.tsx delete mode 100644 src/app/blog/page.tsx delete mode 100644 src/app/feed.xml/route.ts delete mode 100644 src/components/blog/post-cover.tsx create mode 100644 src/components/docs/feedback-widget.tsx diff --git a/content/blog/introducing-agentling.md b/content/blog/introducing-agentling.md deleted file mode 100644 index b8a2c76..0000000 --- a/content/blog/introducing-agentling.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: Introducing agentling -slug: introducing-agentling -excerpt: A tiny async framework for reliable, observable, tool-using agents. Version 0.1.0 is out today, and the whole codebase is small enough to read in one sitting. -publishDate: 2026-07-07T09:00:00Z -authorName: Folarin Akinloye -tags: - - release - - announcement -featured: true -readingMinutes: 4 ---- - -Today I am releasing agentling 0.1.0, a tiny async framework for building tool-using agents in Python. It gives you a clean ReAct loop, typed memory, streaming events, recoverable failures, and progressive-disclosure skills, in a codebase you can actually read, with more test code than source. - -## Why another agent framework - -Most agent frameworks make a trade I was never comfortable with: power in exchange for opacity. You get orchestration graphs, plugin registries, and callback forests, and somewhere in the middle of all that your agent makes a decision you cannot explain. - -agentling starts from the opposite end. An agent is a loop that turns a model, some tools, and a memory of what happened into more actions, until it has an answer. Everything else should be a thin, legible layer on top of that loop. If you can read the loop, you can trust the agent. - -That principle shaped every design decision in the framework: - -- **One code path.** Blocking and streaming share the exact same loop. There is a single async generator; blocking mode just drains it. There is no second implementation to keep in sync, and no behavioral drift between the two modes. -- **Typed memory.** A run is a list of typed steps, not a bag of raw messages. Steps serialize to JSON, so persistence, replay, and multi-turn continuation come for free. -- **Self-healing by default.** A tool that raises becomes an observation the model can recover from, not a crash. Invalid arguments get a targeted hint. A stuck loop gets a nudge. -- **Skills that stay out of the way.** Drop a `SKILL.md` folder in and the model sees only its name and description until it decides to load it. A large skill library costs almost nothing until it is used. - -## What it looks like - -Here is a complete, working agent: - -```python -import asyncio - -from agentling import Agent, OpenAIModel, tool - - -@tool -def get_weather(city: str) -> str: - """Get the current weather for a city. - - Args: - city: The city to look up. - """ - return f"It is 22C and sunny in {city}." - - -async def main() -> None: - agent = Agent(model=OpenAIModel("gpt-4o-mini"), tools=[get_weather]) - print(await agent.run("What's the weather in Paris?")) - - -asyncio.run(main()) -``` - -The `@tool` decorator reads your function signature and docstring and builds the JSON Schema the model sees. The loop validates every call the model makes against that schema, executes tool calls concurrently, and feeds errors back as observations the model can act on. - -## What is in 0.1.0 - -The first release covers the core framework plus a production-hardening pass: - -- The async ReAct loop with blocking and streaming `run()`. -- A provider-neutral model layer and an OpenAI-compatible adapter with retries. -- Typed memory with JSON persistence and load validation. -- Streaming events with a ready-made `print_events` renderer. -- Progressive-disclosure skills with a built-in `load_skill` tool. -- Timeouts, graceful interruption, error redaction, and a `context_manager` hook for long runs. - -The full list is in the [changelog](/changelog). - -## Honest limitations - -agentling is deliberately small, and the boundaries are documented rather than hidden. Schema validation covers a practical subset of JSON Schema, not the full spec. There is no built-in tracing backend; the event stream and step callbacks are the hooks. Tools run as trusted code in your process, so there is no sandboxing. And long runs can outgrow the context window unless you supply a `context_manager`. - -If those constraints fit your project, I think you will enjoy how little there is between you and the loop. - -## Getting started - -```bash -pip install agentling -``` - -The [quickstart](/docs/getting-started/quickstart) takes you from an empty file to a streaming agent in a few minutes. The repository ships offline examples that need no API key, so you can watch failure recovery, persistence, and the full event stream before you spend a single token. - -agentling is MIT licensed and open to contributions. If you build something with it, or you hit a rough edge, open an issue on [GitHub](https://github.com/lunarstack-co/agentling). This is version 0.1.0 of something I want to keep small, sharp, and readable all the way to 1.0. diff --git a/content/blog/one-loop-two-modes.md b/content/blog/one-loop-two-modes.md deleted file mode 100644 index 6246c30..0000000 --- a/content/blog/one-loop-two-modes.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: "One loop, two modes: how agentling streams" -slug: one-loop-two-modes -excerpt: Blocking and streaming agents usually mean two code paths and subtle drift between them. agentling has exactly one loop, and blocking mode just drains it. -publishDate: 2026-07-08T09:00:00Z -authorName: Folarin Akinloye -tags: - - engineering - - internals -featured: false -readingMinutes: 6 ---- - -Every agent framework eventually faces the same question: do you return the final answer, or do you stream events as they happen? Most answer "both", and then quietly maintain two implementations that drift apart. agentling answers "both" with a single async generator, and I want to show you how that works. - -## The whole framework hangs off one generator - -Inside every agentling session there is one method that matters: `_run_stream`. It is an async generator, and one iteration of it is one step of the ReAct loop. Stream a model turn, run the tool calls the model asked for, record the outcome, check whether the run is done. - -`run()` is a thin dispatcher on top of it: - -```python -# streaming mode: hand the generator to the caller -events = agent.run(task, stream=True) - -# blocking mode: drain the same generator internally -answer = await agent.run(task) -``` - -In streaming mode you get the generator itself, wrapped as an async iterator of typed events. In blocking mode the framework consumes that same generator to exhaustion and hands you the final answer string. There is no second implementation to keep in sync, so a bug fixed in one mode is fixed in both, and behavior can never diverge between them. - -## Streaming and reassembling at the same time - -The tricky part of streaming an agent is that the model's output arrives in fragments, but tool execution needs complete calls. Tool-call arguments in particular arrive in pieces across many deltas. - -agentling solves this with a small function called `agglomerate_deltas`. The model adapter yields `Delta` objects: small chunks of text or fragments of a tool call. The loop forwards each text chunk to you immediately as a `TextDelta` event, and at the same time it accumulates every delta. When the turn ends, `agglomerate_deltas` has rebuilt one complete `ChatMessage`: content concatenated, tool-call fragments merged by index, final usage captured. - -This is why the loop can stream text to your terminal and still hand fully-formed, validated tool calls to the executor. It streams and reassembles simultaneously, with no buffering compromise in either direction. - -## Five events, one contract - -The loop communicates through a small set of frozen event types: - -| Event | Meaning | -| --- | --- | -| `TextDelta` | A chunk of streamed assistant text. | -| `ToolCallEvent` | Emitted just before a tool call runs. | -| `ToolResultEvent` | Emitted after a tool call completes. | -| `StepEvent` | Emitted after a step is recorded to memory. | -| `FinalEvent` | Emitted once when the run ends. | - -`StepEvent` is the one I want to highlight. It carries the exact `ActionStep` that was just written to memory, which makes it the bridge between the live stream and the durable record. If you are building a UI, you render `TextDelta` and `ToolCallEvent` for liveness. If you are logging or computing metrics, you consume `StepEvent` and get the same typed steps that persistence uses. The two views can never disagree, because they are produced by the same write. - -## Why this matters in practice - -The single-code-path design sounds like an implementation detail, but it shows up in the developer experience constantly: - -- You can prototype with blocking `run()` and switch to streaming later without changing agent behavior. -- `print_events`, the built-in CLI renderer, is just a consumer of the public event stream. Your custom UI has access to everything the built-in one does. -- Tests that assert on events also validate blocking mode, because blocking mode is those events, drained. - -The full event reference is in the [docs](/docs/core-concepts/streaming-events), and the loop itself is about a page of code in `agent.py`. Reading it is the fastest way to understand the framework, which is exactly the point. diff --git a/content/blog/skills-that-stay-out-of-the-way.md b/content/blog/skills-that-stay-out-of-the-way.md deleted file mode 100644 index 70647ee..0000000 --- a/content/blog/skills-that-stay-out-of-the-way.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: Skills that stay out of the way -slug: skills-that-stay-out-of-the-way -excerpt: Progressive disclosure lets you install a large library of agent skills while paying almost nothing in context until the model actually needs one. Here is how agentling implements it. -publishDate: 2026-07-08T14:00:00Z -authorName: Folarin Akinloye -tags: - - engineering - - skills -featured: false -readingMinutes: 5 ---- - -There is a tension at the heart of giving agents instructions. The more expertise you write down for your agent, the more context you burn on every single run, whether that expertise is relevant or not. agentling resolves this with progressive disclosure, an idea borrowed from Claude's skill format, and I think it is one of the most useful patterns in the framework. - -## A skill is a folder - -In agentling, a skill is a folder containing a `SKILL.md` file: YAML frontmatter followed by a markdown body of instructions. - -```markdown ---- -name: code-reviewer -description: Review a code change for bugs, security issues, and style problems. ---- - -# Code Reviewer - -You are reviewing a code change. Work through it methodically and report only -findings you are confident about... -``` - -That is the entire format. No classes to subclass, no registration step, no build process. If you can write markdown, you can write a skill. - -## The catalog costs almost nothing - -Here is the key move. When you pass skills to an agent, the full instruction bodies do not go into the system prompt. Only a catalog of names and descriptions does, one line per skill: - -``` -code-reviewer: Review a code change for bugs, security issues, and style problems. -linting: Lint Python files and report issues. -``` - -The agent also registers a single built-in tool, `load_skill`. When the model decides a skill is relevant, it calls `load_skill(name)`, and the skill's full instruction body comes back as a tool observation. From that moment the expertise is in context, but not a token sooner. - -With ten skills installed, your base prompt grows by ten lines, not ten documents. The cost of a skill is only paid when the model actually chooses to use it. - -## Skills can bring their own tools - -A skill can declare Python entry points in its frontmatter: - -```markdown ---- -name: linting -description: Lint Python files and report issues. -tools: - - my_package.lint_tools:run_ruff ---- -``` - -Each entry is a `"module.path:attribute"` string that must resolve to a function decorated with `@tool`. When the skill loads, those tools are imported and registered on the session. Because the loop reads the live tool set fresh on every step, the new tools are available on the very next turn. Instructions and capabilities arrive together, on demand. - -One important note: an entry point is imported with `importlib`, which runs that module's top-level code. Load skills only from sources you trust, exactly as you would treat a Python import. The [security policy](https://github.com/lunarstack-co/agentling/blob/main/SECURITY.md) spells out the full trust model. - -## Why this beats prompt stuffing - -I keep coming back to three properties of this design: - -- **Relevance is decided by the model, at run time.** You do not have to predict which expertise a task needs. The model reads the catalog and pulls what it judges useful. -- **Skill libraries scale.** The marginal cost of installing one more skill is one catalog line. Teams can accumulate skills the way they accumulate internal docs. -- **Skills are inspectable.** A skill is a text file in your repository. You can review it, diff it, and version it like any other code. - -The [skills documentation](/docs/core-concepts/skills) covers the format in full, including how to pass pre-built `Skill` objects and how loaded tools interact with [sessions](/docs/guides/sessions-and-concurrency). If you build a skill you are proud of, I would genuinely like to see it. diff --git a/content/docs/getting-started/introduction.md b/content/docs/getting-started/introduction.md index b4bb590..5b4c255 100644 --- a/content/docs/getting-started/introduction.md +++ b/content/docs/getting-started/introduction.md @@ -1,24 +1,24 @@ --- title: Introduction slug: introduction -description: What agentling is, the idea behind it, and when to reach for it. +description: What Agentling is, the idea behind it, and when to reach for it. order: 1 --- -agentling is a tiny async framework for building reliable, observable, tool-using agents in Python. It gives you a clean ReAct loop, typed memory, streaming events, recoverable failures, and progressive-disclosure skills, in a codebase small enough to read in one sitting. +Agentling is a small async Python framework for building tool-using agents. It gives you a clean ReAct loop, typed memory, streaming events, and recoverable failures in an auditable codebase. -The framework is built around one idea: an agent is a loop that turns a model, some tools, and a memory of what happened into more actions, until it has an answer. Everything else, including streaming, skills, self-healing, and persistence, is a thin layer on top of that loop. +The design centers on one core idea: an agent is a loop that combines a model, tools, and a memory of past turns to choose further actions until it finishes. Features like streaming, skills, self-healing, and persistence are layered directly onto this single loop. -## Why agentling +## Why Agentling -- **Async first.** The loop, tools, and model calls are all `async`. Tool calls in a single step run concurrently by default. -- **One code path.** Blocking and streaming share the exact same loop. There is a single async generator; blocking mode just drains it. -- **Typed memory.** A run is a list of typed steps, not a bag of raw messages. Steps know how to render themselves back into model messages and serialize to JSON for persistence and replay. -- **Progressive-disclosure skills.** Drop a `SKILL.md` folder in and the model sees only its name and description until it decides to load it. Big skill libraries stay cheap. -- **Self-healing.** A tool that raises becomes an observation the model can recover from, not a crash. -- **Small and readable.** No metaclasses, no plugin registry, no DSL. A handful of focused modules you can actually read, with more test code than source. +- **Async first.** The loop, tools, and model calls are async. Tool calls in a single step run concurrently by default. +- **One code path.** Blocking and streaming modes share the exact same async generator. Blocking mode drains the stream for you. +- **Typed memory.** Runs are lists of typed step objects, not raw message dictionaries. Steps render themselves into model messages and serialize to JSON for offline replay and persistence. +- **Progressive skills.** Place a `SKILL.md` folder into your project. The model only sees its name and summary until it decides to load the full instructions. +- **Self-healing.** When a tool raises an exception, the loop turns it into an observation with recovery guidance so the model can correct its arguments instead of crashing. +- **Auditable codebase.** No metaclasses, no plugin registries, and no domain-specific languages. The framework consists of a few concise modules backed by extensive tests. -## A taste of the API +## Basic example ```python import asyncio @@ -44,16 +44,16 @@ async def main() -> None: asyncio.run(main()) ``` -That is a complete, working agent. The `@tool` decorator turns a plain function into a schema-validated tool, and `Agent.run` drives the loop until the model produces an answer. +The `@tool` decorator inspects your function signature to build a schema-validated tool, and `Agent.run` executes the loop until the model returns its final text. ## When to use it -agentling is a good fit when you want to understand and control every part of your agent: a service that calls a handful of trusted tools, a CLI assistant, a background worker that needs resumable runs, or a research harness where you want full visibility into each step. +Agentling suits projects where you need full visibility into agent operations: backend workers calling trusted tools, CLI assistants, resumable background processes, or test harnesses where runs must be recorded and replayed deterministically. -It is deliberately small, so some things are out of scope: there is no built-in tracing backend, no sandboxing, and no automatic context summarization. The [Limitations](/docs/getting-started/installation#requirements) are documented honestly, and the hooks to build those layers yourself are part of the public API. +To keep the codebase focused, several features remain out of scope: there is no hosted tracing backend, no execution sandbox, and no automatic prompt summarization. Review the [requirements and scope boundaries](/docs/getting-started/installation#requirements) for details on customizing these layers through public APIs. ## Next steps -- [Install agentling](/docs/getting-started/installation) -- [Build your first agent](/docs/getting-started/quickstart) -- [Understand the agent loop](/docs/core-concepts/agent-loop) +- [Install Agentling](/docs/getting-started/installation) +- [Quickstart guide](/docs/getting-started/quickstart) +- [The agent loop](/docs/core-concepts/agent-loop) diff --git a/content/pages/roadmap.md b/content/pages/roadmap.md deleted file mode 100644 index bd64517..0000000 --- a/content/pages/roadmap.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: Roadmap -slug: roadmap -description: "Where agentling is heading: reliability first, then observability and evaluation, then more models." ---- - -agentling is a tiny async framework for reliable, observable, tool-using agents: a clean ReAct loop, typed memory, streaming events, recoverable failures, and progressive-disclosure skills, in a codebase small enough to read in one sitting. - -> [!NOTE] -> This roadmap is directional, not a schedule. Priorities can shift with community feedback, and the versions below are indicative: pre-1.0 milestones may split or merge. - -## Shipped: v0.1.0 - -The first release: the core framework plus a production-hardening pass. `Agent` and `AgentSession`, an OpenAI-compatible model adapter, `@tool`, typed memory with JSON persistence, streaming events, progressive-disclosure skills, timeouts and cancellation, malformed-output recovery, and a runnable examples suite. See the [changelog](/changelog) for the full notes. - -## Next: reliability (v0.2) - -**Top priority.** Harden and clean up the v0.1 surface before adding features. There is no point building on foundations with sharp edges. - -- **Session lifecycle.** An idle `interrupt()` no longer silently kills the next run, and using one session concurrently raises a clear error instead of quietly corrupting memory. -- **Honest errors.** Make the exception hierarchy real: the documented error types are actually raised, so `except AgentlingError` behaves as promised. -- **Broader compatibility.** Stop rejecting valid tool calls from OpenAI-compatible backends (some omit ids mid-stream); synthesize them instead. -- **Sampling controls.** Expose `temperature`, `max_tokens`, `seed`, and friends on the model, so deterministic evals and cost caps are possible. -- **Cleaner semantics.** `final_answer` no longer leaks into the event stream, context-window trimming applies on every path, and resuming a session no longer duplicates the task. -- **Robustness.** Safe, async-capable step callbacks; Python 3.13 and 3.14 in CI; stricter typing; and a batch of small correctness fixes. - -## Then: observability and evaluation (v0.3) - -Make every run inspectable and testable. - -- **Lifecycle tracing.** A dependency-free tracing layer over the whole lifecycle (run, step, model call, tool call) capturing inputs, outputs, token usage, timing, and errors. -- **OpenTelemetry adapter.** Emit standard GenAI spans so traces flow to Langfuse, LangSmith, Arize Phoenix, and any OTLP backend through one integration, not a bespoke plugin per vendor. -- **Offline testing and evals.** A public, deterministic testing model, so you can run and test your agent with no API key, plus an evaluation harness with datasets and evaluators that works locally or against a hosted experiment backend. - -## Later: more models (v0.4) - -Meet people where their models already are. - -- **OpenAI-compatible tier.** A documented provider matrix and light ergonomics for OpenRouter, Groq, Together, Fireworks, DeepSeek, Mistral, xAI, local servers (Ollama, vLLM, LM Studio), and Azure OpenAI. Most already work by pointing at a base URL. -- **Native Anthropic (Claude).** A first-class adapter on the Messages API with tool use, streaming, usage, and prompt caching, beyond the lossy compatibility endpoint. -- **More.** Native Gemini; optionally a litellm bridge and AWS Bedrock or Vertex. - -## Exploring: beyond the milestones - -Ideas we like but have not committed to a milestone: - -- Structured output (JSON mode and response schemas). -- A human-in-the-loop hook to approve, deny, or modify a tool call before it runs (guardrails). -- An MCP bridge recipe: wrap an MCP tool as an agentling tool. -- An exhaustive failure-mode test suite and a trust policy for skill-provided tools. - -## Toward 1.0 - -Once reliability, observability, and the model surface settle, 1.0 is about committing to a stable public API and semantic-versioning guarantees. - -## Influence the roadmap - -This is an open, early project and the priorities above are open to input. [Open an issue](https://github.com/lunarstack-co/agentling/issues) to propose something, describe a use case we are missing, or tell us which item matters most to you. Bug reports and small PRs are especially welcome: see the [contributing guide](/contributing). diff --git a/scripts/validate-content.mjs b/scripts/validate-content.mjs index 0c57734..f02eb3d 100644 --- a/scripts/validate-content.mjs +++ b/scripts/validate-content.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node /** * Validates everything under /content so broken frontmatter, duplicate - * slugs, dead internal links, or stray em dashes fail CI before they ship. + * slugs, or dead internal links fail CI before they ship. * * Usage: node scripts/validate-content.mjs */ @@ -98,10 +98,9 @@ function checkDashes(file, raw) { /* ---------- Collect and validate every collection ---------- */ -const validRoutes = new Set(["/", "/docs", "/blog", "/changelog"]); +const validRoutes = new Set(["/", "/docs", "/changelog"]); const docSlugs = new Map(); const sectionSlugs = new Map(); -const blogSlugs = new Map(); const versions = new Map(); const pageSlugs = new Map(); const allFiles = []; @@ -152,34 +151,11 @@ for (const dirent of fs.readdirSync(path.join(CONTENT, "docs"), { }); checkOptional(file, page.data, { description: "string" }); checkSlug(file, page.data.slug); - // Doc page slugs are globally unique because pages are looked up by - // slug alone. checkUnique(file, docSlugs, page.data.slug, "doc page slug"); validRoutes.add(`/docs/${dirent.name}/${page.data.slug}`); } } -for (const file of markdownFiles(path.join(CONTENT, "blog"))) { - const post = read(file); - if (!post) continue; - allFiles.push({ file, raw: post.content }); - requireFields(file, post.data, { - title: "string", - slug: "string", - publishDate: "date", - }); - checkOptional(file, post.data, { - excerpt: "string", - authorName: "string", - tags: "string[]", - featured: "boolean", - readingMinutes: "number", - }); - checkSlug(file, post.data.slug); - checkUnique(file, blogSlugs, post.data.slug, "blog slug"); - validRoutes.add(`/blog/${post.data.slug}`); -} - for (const file of markdownFiles(path.join(CONTENT, "changelog"))) { const entry = read(file); if (!entry) continue; @@ -228,5 +204,5 @@ if (errors.length > 0) { console.log( `Content OK: ${docSlugs.size} doc pages in ${sectionSlugs.size} sections, ` + - `${blogSlugs.size} posts, ${versions.size} releases, ${pageSlugs.size} site pages.`, + `${versions.size} releases, ${pageSlugs.size} site pages.`, ); diff --git a/src/app/[slug]/page.tsx b/src/app/[slug]/page.tsx index 222d1d3..deb14fe 100644 --- a/src/app/[slug]/page.tsx +++ b/src/app/[slug]/page.tsx @@ -33,16 +33,16 @@ export default async function StandaloneSitePage({ params }: SitePageParams) { if (!page) notFound(); return ( -
+

{page.title}

{page.description && ( -

+

{page.description}

)} -
+
diff --git a/src/app/blog/[slug]/opengraph-image.tsx b/src/app/blog/[slug]/opengraph-image.tsx deleted file mode 100644 index e2f95f8..0000000 --- a/src/app/blog/[slug]/opengraph-image.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { getBlogPost } from "@/lib/content"; -import { renderOgImage, OG_CONTENT_TYPE, OG_SIZE } from "@/lib/og-image"; -import { SITE_TAGLINE } from "@/lib/site"; -import { formatDate } from "@/lib/utils"; - -export const alt = "agentling blog post"; -export const size = OG_SIZE; -export const contentType = OG_CONTENT_TYPE; - -export default async function Image({ - params, -}: { - params: Promise<{ slug: string }>; -}) { - const { slug } = await params; - const post = await getBlogPost(slug); - - if (!post) { - return renderOgImage({ - eyebrow: "Blog", - title: "agentling", - subtitle: SITE_TAGLINE, - }); - } - - return renderOgImage({ - eyebrow: `Blog · ${formatDate(post.publishDate)}`, - title: post.title, - subtitle: post.excerpt, - }); -} diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx deleted file mode 100644 index 24256a3..0000000 --- a/src/app/blog/[slug]/page.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { ArrowLeft } from "lucide-react"; -import type { Metadata } from "next"; -import Link from "next/link"; -import { notFound } from "next/navigation"; - -import { GithubIcon } from "@/components/icons/github"; -import { JsonLd } from "@/components/json-ld"; -import { Markdown } from "@/components/markdown"; -import { ShareLinks } from "@/components/share-links"; -import { getBlogPost, getBlogPosts } from "@/lib/content"; -import { absoluteUrl, pageMetadata } from "@/lib/seo"; -import { AUTHOR, AUTHOR_JSON_LD } from "@/lib/site"; -import { formatDate } from "@/lib/utils"; - -interface BlogPostParams { - params: Promise<{ slug: string }>; -} - -export async function generateStaticParams() { - const posts = await getBlogPosts(); - return posts.map((post) => ({ slug: post.slug })); -} - -export async function generateMetadata({ - params, -}: BlogPostParams): Promise { - const { slug } = await params; - const post = await getBlogPost(slug); - if (!post) return {}; - return pageMetadata({ - title: post.title, - description: post.excerpt, - path: `/blog/${post.slug}`, - ogType: "article", - article: { - publishedTime: post.publishDate, - authors: [post.authorName], - tags: post.tags, - }, - }); -} - -export default async function BlogPostPage({ params }: BlogPostParams) { - const { slug } = await params; - const post = await getBlogPost(slug); - if (!post) notFound(); - - return ( -
- - - - All posts - - -
-

- - · - {post.readingMinutes} min read -

-

- {post.title} -

-

- {post.excerpt} -

-
-
- - {post.authorName - .split(" ") - .map((part) => part[0]) - .slice(0, 2) - .join("")} - -

- - {post.authorName} - - - {AUTHOR.role} - - - - -

-
-

- {post.tags.map((tag) => ( - - {tag} - - ))} -

-
-
- -
- -
- -
- -
-
- ); -} diff --git a/src/app/blog/opengraph-image.tsx b/src/app/blog/opengraph-image.tsx deleted file mode 100644 index dc89a3a..0000000 --- a/src/app/blog/opengraph-image.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { renderOgImage, OG_CONTENT_TYPE, OG_SIZE } from "@/lib/og-image"; - -export const alt = "agentling blog"; -export const size = OG_SIZE; -export const contentType = OG_CONTENT_TYPE; - -export default function Image() { - return renderOgImage({ - eyebrow: "Blog", - title: "News and engineering notes", - subtitle: - "Release announcements, design write-ups, and honest notes from building a small agent framework.", - }); -} diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx deleted file mode 100644 index bba377f..0000000 --- a/src/app/blog/page.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { ArrowRight } from "lucide-react"; -import type { Metadata } from "next"; -import Link from "next/link"; - -import { PostCover } from "@/components/blog/post-cover"; -import { getBlogPosts } from "@/lib/content"; -import { pageMetadata } from "@/lib/seo"; -import { formatDate } from "@/lib/utils"; - -export const metadata: Metadata = pageMetadata({ - title: "Blog", - description: - "Release announcements and technical deep dives from the agentling project.", - path: "/blog", -}); - -export default async function BlogIndexPage() { - const posts = await getBlogPosts(); - const featured = posts.find((post) => post.featured) ?? posts[0]; - const rest = posts.filter((post) => post.slug !== featured?.slug); - - return ( -
-

- Blog -

-

- News and engineering notes -

-

- Release announcements, design write-ups, and honest notes from - building a small agent framework. -

- - {featured && ( - -
-

- - Featured - - {formatDate(featured.publishDate)} · {featured.readingMinutes} min - read -

-

- {featured.title} -

-

- {featured.excerpt} -

-

- Read the announcement - -

-
- - - )} - -
- {rest.map((post) => ( - - -

- {formatDate(post.publishDate)} · {post.readingMinutes} min read -

-

- {post.title} -

-

- {post.excerpt} -

-

- {post.tags.map((tag) => ( - - {tag} - - ))} -

- - ))} -
-
- ); -} diff --git a/src/app/changelog/page.tsx b/src/app/changelog/page.tsx index 6c66e6b..f67940a 100644 --- a/src/app/changelog/page.tsx +++ b/src/app/changelog/page.tsx @@ -1,5 +1,4 @@ import type { Metadata } from "next"; -import Link from "next/link"; import { Markdown } from "@/components/markdown"; import { getChangelog } from "@/lib/content"; @@ -10,7 +9,7 @@ import { formatDate } from "@/lib/utils"; export const metadata: Metadata = pageMetadata({ title: "Changelog", description: - "Every agentling release, documented. The format follows Keep a Changelog and the project aims to follow semantic versioning.", + "Every Agentling release, documented. Follows Keep a Changelog and semantic versioning.", path: "/changelog", }); @@ -18,32 +17,21 @@ export default async function ChangelogPage() { const entries = await getChangelog(); return ( -
-

+

+

Changelog -

-

- What shipped, release by release

-

- The format follows Keep a Changelog, and the project aims to follow - semantic versioning. Releases are tagged on{" "} +

+ What shipped, release by release. Releases are tagged on{" "} GitHub - , and the{" "} - - roadmap - {" "} - shows what is coming next. + .

@@ -51,14 +39,14 @@ export default async function ChangelogPage() {
-

+

v{entry.version}

-

+

{entry.tags.map((tag) => ( {tag} @@ -80,11 +68,11 @@ export default async function ChangelogPage() {

{entry.title && ( -

+

{entry.title}

)} -
+
diff --git a/src/app/docs/[section]/[slug]/page.tsx b/src/app/docs/[section]/[slug]/page.tsx index aa7fbcf..7766ace 100644 --- a/src/app/docs/[section]/[slug]/page.tsx +++ b/src/app/docs/[section]/[slug]/page.tsx @@ -1,22 +1,29 @@ -import { ArrowLeft, ArrowRight, ChevronRight, PencilLine } from "lucide-react"; +import { ArrowLeft, ArrowRight } from "lucide-react"; import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; import { CopyMarkdownButton } from "@/components/copy-markdown-button"; +import { FeedbackWidget } from "@/components/docs/feedback-widget"; +import { Toc } from "@/components/docs/toc"; import { JsonLd } from "@/components/json-ld"; import { Markdown } from "@/components/markdown"; -import { ShareLinks } from "@/components/share-links"; -import { Toc } from "@/components/docs/toc"; import { getDocPage, getDocSections } from "@/lib/content"; import { absoluteUrl, pageMetadata } from "@/lib/seo"; -import { AUTHOR_JSON_LD, GITHUB_DOCS_URL } from "@/lib/site"; +import { AUTHOR_JSON_LD } from "@/lib/site"; import { extractToc } from "@/lib/toc"; interface DocPageParams { params: Promise<{ section: string; slug: string }>; } +const TEMPLATE_BADGES: Record = { + "getting-started": { label: "Tutorial", color: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20" }, + "core-concepts": { label: "Concept", color: "bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border-indigo-500/20" }, + guides: { label: "How-To Guide", color: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20" }, + reference: { label: "API Reference", color: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20" }, +}; + export async function generateStaticParams() { const sections = await getDocSections(); return sections.flatMap((section) => @@ -59,11 +66,11 @@ export default async function DocPage({ params }: DocPageParams) { const next = index < flat.length - 1 ? flat[index + 1] : null; const toc = extractToc(page.body); - const pageUrl = absoluteUrl(`/docs/${page.sectionSlug}/${page.slug}`); + const templateBadge = TEMPLATE_BADGES[page.sectionSlug] ?? { label: "Guide", color: "bg-muted text-muted-foreground border-border" }; return ( -
+
-
- - -

+ + {/* Centre Column: max-w-[720px] for prose, code breaks wider up to 860px */} +
+ {/* Breadcrumb + Diataxis badge */} +
+ + + + {templateBadge.label} + +
+ + {/* H1 Title */} +

{page.title}

+ + {/* One-sentence summary in larger, muted text */} {page.description && ( -

+

{page.description}

)} -
+ {/* Action Header Strip */} +
+ Python 3.11+ • MIT + +
+ + {/* Markdown Content */} +
-
-
- - - Edit this page on GitHub - - ${page.description}\n\n${page.body}\n`} - /> -
- + {/* Was this helpful? Widget */} +
+
- + {/* Prev / Next within section */} + {(previous || next) && ( + + )}
- + {/* Right Rail: 220px TOC + Quiet Trust Signals */} +

); } diff --git a/src/app/docs/layout.tsx b/src/app/docs/layout.tsx index 2560396..cd9a9c5 100644 --- a/src/app/docs/layout.tsx +++ b/src/app/docs/layout.tsx @@ -9,9 +9,11 @@ export default async function DocsLayout({ const sections = await getDocSections(); return ( -
+
- {children} +
+ {children} +
); } diff --git a/src/app/docs/page.tsx b/src/app/docs/page.tsx index 99dd88c..b384c24 100644 --- a/src/app/docs/page.tsx +++ b/src/app/docs/page.tsx @@ -1,58 +1,5 @@ -import { ArrowRight } from "lucide-react"; -import type { Metadata } from "next"; -import Link from "next/link"; +import { redirect } from "next/navigation"; -import { getDocSections } from "@/lib/content"; -import { pageMetadata } from "@/lib/seo"; - -export const metadata: Metadata = pageMetadata({ - title: "Documentation", - description: - "Learn agentling: installation, the agent loop, tools, memory, streaming events, skills, and the full API reference.", - path: "/docs", -}); - -export default async function DocsIndexPage() { - const sections = await getDocSections(); - - return ( -
-

- Documentation -

-

- Learn agentling -

-

- Everything you need to build reliable, observable, tool-using agents: - from your first install to the full API surface. The framework is a - handful of focused modules you can read end to end, and these docs - cover all of them. -

- -
- {sections.map((section) => ( - -

- {section.title} - -

-

- {section.description} -

-

- {section.pages.length} pages -

- - ))} -
-
- ); +export default function DocsIndexPage() { + redirect("/docs/getting-started/introduction"); } diff --git a/src/app/feed.xml/route.ts b/src/app/feed.xml/route.ts deleted file mode 100644 index c5ec71a..0000000 --- a/src/app/feed.xml/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { getBlogPost, getBlogPosts } from "@/lib/content"; -import { renderMarkdown } from "@/lib/markdown"; -import { renderRss, RSS_HEADERS, type FeedItem } from "@/lib/rss"; -import { SITE_URL } from "@/lib/site"; - -export const dynamic = "force-static"; - -export async function GET() { - const metas = await getBlogPosts(); - const items: FeedItem[] = []; - - for (const meta of metas) { - const post = await getBlogPost(meta.slug); - if (!post) continue; - const link = `${SITE_URL}/blog/${post.slug}`; - items.push({ - title: post.title, - link, - guid: link, - pubDate: new Date(post.publishDate).toUTCString(), - description: post.excerpt, - contentHtml: await renderMarkdown(post.body), - }); - } - - return new Response( - renderRss({ - title: "agentling blog", - path: "/blog", - description: - "Release announcements and technical deep dives from the agentling project.", - items, - }), - { headers: RSS_HEADERS }, - ); -} diff --git a/src/app/globals.css b/src/app/globals.css index 790f44b..2aebb5e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -4,50 +4,58 @@ @custom-variant dark (&:where(.dark, .dark *)); /* - * Semantic design tokens. Components never use raw hex values; they use the - * Tailwind utilities generated from the @theme mapping below, so both themes - * stay in sync by construction. + * The Typeset Book & IDE Precision System + * Light: Warm book paper (#FAFAF7), near-black ink (#1A1A1A), deep indigo accent (#4338CA) + * Dark: Soft charcoal (#0F1012), lifted lavender accent (#818CF8), muted text (#9CA3AF) + * Zero gradients, zero decorative glow blobs, hairline 1px borders. */ + :root { - --background: #fbfbf9; - --foreground: #171a20; + --background: #fafaf7; + --foreground: #1a1a1a; --card: #ffffff; - --card-foreground: #171a20; - --muted: #f2f2ee; - --muted-foreground: #5a6472; - --faint: #8b93a1; - --border: #e5e5e0; - --border-strong: #d2d2cb; - --accent: #087856; - --accent-strong: #066a4c; + --card-foreground: #1a1a1a; + --muted: #f2f2ed; + --muted-foreground: #6b7280; + --faint: #9ca3af; + --border: #e8e8e3; + --border-strong: #d1d1ca; + + --accent: #4338ca; + --accent-strong: #3730a3; --accent-foreground: #ffffff; - --accent-soft: rgba(8, 120, 86, 0.09); - --code-bg: #f6f6f3; - --code-border: #e5e5e0; - --terminal-bg: #101418; - --ring: rgba(8, 120, 86, 0.5); - --shadow-soft: 0 1px 2px rgba(23, 26, 32, 0.05), 0 8px 24px rgba(23, 26, 32, 0.06); + --accent-soft: rgba(67, 56, 202, 0.08); + + --code-bg: #141518; + --code-fg: #f3f4f6; + --code-border: #23252a; + --terminal-bg: #141518; + + --ring: #4338ca; } .dark { - --background: #0b0e14; - --foreground: #e8ecf4; - --card: #10141c; - --card-foreground: #e8ecf4; - --muted: #141924; - --muted-foreground: #98a3b8; - --faint: #5f6b82; - --border: #1e2431; - --border-strong: #2b3345; - --accent: #34d399; - --accent-strong: #6ee7b7; - --accent-foreground: #04120c; - --accent-soft: rgba(52, 211, 153, 0.1); - --code-bg: #0e1219; - --code-border: #1c2230; - --terminal-bg: #0e1219; - --ring: rgba(52, 211, 153, 0.5); - --shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35); + --background: #0f1012; + --foreground: #ededed; + --card: #151619; + --card-foreground: #ededed; + --muted: #1c1d22; + --muted-foreground: #9ca3af; + --faint: #6b7280; + --border: #23252a; + --border-strong: #373942; + + --accent: #818cf8; + --accent-strong: #a5b4fc; + --accent-foreground: #0f1012; + --accent-soft: rgba(129, 140, 248, 0.12); + + --code-bg: #141518; + --code-fg: #ededed; + --code-border: #23252a; + --terminal-bg: #141518; + + --ring: #818cf8; } @theme inline { @@ -64,24 +72,19 @@ --color-accent-strong: var(--accent-strong); --color-accent-foreground: var(--accent-foreground); --color-accent-soft: var(--accent-soft); + --color-code-bg: var(--code-bg); + --color-code-fg: var(--code-fg); --color-code-border: var(--code-border); --color-terminal: var(--terminal-bg); - --font-sans: var(--font-plex-sans), ui-sans-serif, system-ui, sans-serif; - --font-mono: var(--font-jetbrains-mono), ui-monospace, "SF Mono", monospace; - --shadow-soft: var(--shadow-soft); - --animate-blink: blink 1.1s steps(1) infinite; -} -@keyframes blink { - 50% { - opacity: 0; - } + --font-sans: var(--font-sans), ui-sans-serif, system-ui, -apple-system, sans-serif; + --font-mono: var(--font-mono), ui-monospace, "SF Mono", Menlo, monospace; } html { scroll-behavior: smooth; - scroll-padding-top: 6rem; + scroll-padding-top: 5rem; } body { @@ -89,25 +92,21 @@ body { color: var(--foreground); font-family: var(--font-sans); text-rendering: optimizeLegibility; - transition: background-color 200ms ease; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } ::selection { - background: var(--accent-soft); - color: var(--accent-strong); + background: var(--accent); + color: var(--accent-foreground); } -/* Default keyboard focus ring. The bundler flattens @layer wrappers here, - so this stays un-layered and would beat the `outline-none` utility; - elements where the ring is wrong (e.g. the search input, whose dialog - provides the focus context) opt out with data-focus-ring="none". */ :focus-visible:not([data-focus-ring="none"]) { - outline: 2px solid var(--ring); + outline: 2px solid var(--accent); outline-offset: 2px; - border-radius: 2px; } -/* ---------- Markdown / prose ---------- */ +/* ---------- Prose Specification (720px cap, 17px/1.65 line height) ---------- */ .prose { --tw-prose-body: var(--foreground); @@ -123,16 +122,45 @@ body { --tw-prose-th-borders: var(--border-strong); --tw-prose-td-borders: var(--border); --tw-prose-captions: var(--muted-foreground); - /* Code blocks are themed by shiki + our tokens, not the plugin's - dark-on-dark defaults, so plain-text fences stay readable in light mode. */ - --tw-prose-pre-code: var(--foreground); + --tw-prose-pre-code: var(--code-fg); --tw-prose-pre-bg: var(--code-bg); - --tw-prose-invert-pre-code: var(--foreground); + --tw-prose-invert-pre-code: var(--code-fg); --tw-prose-invert-pre-bg: var(--code-bg); + font-size: 1.0625rem; /* 17px */ + line-height: 1.65; + max-width: 720px; +} + +.prose :where(h1) { + font-size: 2.25rem; + font-weight: 700; + letter-spacing: -0.035em; + line-height: 1.2; + margin-bottom: 0.75rem; +} + +.prose :where(h2) { + font-size: 1.5rem; + font-weight: 650; + letter-spacing: -0.025em; + margin-top: 2.5rem; + margin-bottom: 0.75rem; + scroll-margin-top: 5rem; +} + +.prose :where(h3) { + font-size: 1.2rem; + font-weight: 600; + letter-spacing: -0.02em; + margin-top: 1.75rem; + margin-bottom: 0.5rem; + scroll-margin-top: 5rem; } .prose :where(a):not(:where(.not-prose *)) { font-weight: 500; + color: var(--accent); + text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent); text-underline-offset: 3px; transition: text-decoration-color 150ms ease; @@ -142,24 +170,15 @@ body { text-decoration-color: var(--accent); } -.prose :where(h2, h3, h4) { - scroll-margin-top: 6rem; -} - -.prose :where(h2, h3, h4) a { - text-decoration: none; - font-weight: inherit; - color: inherit; -} - .prose :where(code):not(:where(pre code)) { background: var(--muted); border: 1px solid var(--border); - border-radius: 0.375rem; - padding: 0.125rem 0.375rem; + border-radius: 4px; + padding: 0.15rem 0.35rem; font-size: 0.85em; font-weight: 500; font-family: var(--font-mono); + color: var(--foreground); } .prose :where(code)::before, @@ -168,20 +187,29 @@ body { } .prose :where(table) { - font-size: 0.9em; + width: 100%; + border-collapse: collapse; + font-size: 0.875rem; + margin: 1.75rem 0; + border: 1px solid var(--border); } -.prose :where(blockquote) { - font-style: normal; +.prose :where(thead th) { + font-family: var(--font-sans); + font-weight: 600; + font-size: 0.8125rem; + background: var(--muted); + color: var(--foreground); + padding: 0.65rem 0.85rem; + border-bottom: 1px solid var(--border-strong); + text-align: left; } -.prose :where(blockquote p)::before, -.prose :where(blockquote p)::after { - content: none; +.prose :where(tbody td) { + padding: 0.65rem 0.85rem; + border-bottom: 1px solid var(--border); } -/* ---------- Heading anchors ---------- */ - .prose :where(h2, h3, h4) .heading-anchor { margin-left: 0.4em; color: var(--faint); @@ -200,101 +228,61 @@ body { color: var(--accent); } -/* ---------- Callouts (GitHub alert syntax: > [!NOTE] etc.) ---------- */ - +/* Callouts: Note (grey), Warning (amber), Deprecated (red) */ .callout { - --callout-color: var(--accent); - margin: 1.5rem 0; - padding: 1rem 1.25rem; - border: 1px solid color-mix(in srgb, var(--callout-color) 25%, transparent); - border-left: 3px solid var(--callout-color); - border-radius: 0.75rem; - background: color-mix(in srgb, var(--callout-color) 6%, transparent); -} - -.callout > :where(p):not(.callout-title) { - margin: 0.5em 0 0; -} - -.callout-title { - display: flex; - align-items: center; - gap: 0.5rem; - margin: 0; - font-size: 0.875rem; - font-weight: 600; - color: var(--callout-color); -} - -.callout-title::before { - content: ""; - width: 1rem; - height: 1rem; - flex-shrink: 0; - background-color: var(--callout-color); - mask: var(--callout-icon) no-repeat center / contain; -} - -.callout[data-callout="note"] { - --callout-color: #2563eb; - --callout-icon: url('data:image/svg+xml,'); + margin: 1.75rem 0; + padding: 0.9rem 1.2rem; + border: 1px solid var(--border); + border-left: 3px solid var(--border-strong); + background: var(--card); + border-radius: 4px; } +.callout[data-callout="note"], .callout[data-callout="tip"] { - --callout-color: #059669; - --callout-icon: url('data:image/svg+xml,'); -} - -.callout[data-callout="important"] { - --callout-color: #7c3aed; - --callout-icon: url('data:image/svg+xml,'); + border-left-color: var(--accent); } .callout[data-callout="warning"] { - --callout-color: #d97706; - --callout-icon: url('data:image/svg+xml,'); + border-left-color: #d97706; } -.callout[data-callout="caution"] { - --callout-color: #dc2626; - --callout-icon: url('data:image/svg+xml,'); -} - -.dark .callout[data-callout="note"] { - --callout-color: #60a5fa; -} - -.dark .callout[data-callout="tip"] { - --callout-color: #34d399; -} - -.dark .callout[data-callout="important"] { - --callout-color: #a78bfa; -} - -.dark .callout[data-callout="warning"] { - --callout-color: #fbbf24; +.callout[data-callout="caution"], +.callout[data-callout="important"] { + border-left-color: #dc2626; } -.dark .callout[data-callout="caution"] { - --callout-color: #f87171; +.callout-title { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0; + font-size: 0.8125rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--foreground); } -/* ---------- Code blocks (rehype-pretty-code + shiki dual themes) ---------- */ - +/* Code block bleeds out up to 860px */ [data-rehype-pretty-code-figure] { - margin: 1.5rem 0; + margin: 1.75rem 0; position: relative; + border: 1px solid var(--code-border); + background: var(--code-bg); + border-radius: 6px; + overflow: hidden; + max-width: 860px; } [data-rehype-pretty-code-figure] pre { - background: var(--code-bg); - border: 1px solid var(--code-border); - border-radius: 0.75rem; + background: var(--code-bg) !important; padding: 1rem 0; overflow-x: auto; - font-size: 0.855rem; - line-height: 1.7; + font-size: 0.875rem; /* 14px */ + line-height: 1.65; + color: var(--code-fg); + font-family: var(--font-mono); } [data-rehype-pretty-code-figure] pre code { @@ -310,61 +298,24 @@ body { } [data-rehype-pretty-code-figure] code span { - color: var(--shiki-light); -} - -.dark [data-rehype-pretty-code-figure] code span { - color: var(--shiki-dark); + color: var(--shiki-dark, inherit); } [data-rehype-pretty-code-title] { font-family: var(--font-mono); font-size: 0.75rem; - color: var(--muted-foreground); - background: var(--muted); - border: 1px solid var(--code-border); - border-bottom: none; - border-radius: 0.75rem 0.75rem 0 0; + color: rgba(255, 255, 255, 0.6); + background: rgba(255, 255, 255, 0.04); + border-bottom: 1px solid var(--code-border); padding: 0.5rem 1.25rem; } -[data-rehype-pretty-code-title] + pre { - border-radius: 0 0 0.75rem 0.75rem; - margin-top: 0; -} - -/* ---------- Utility patterns ---------- */ - -.bg-grid { - background-image: - linear-gradient(to right, var(--border) 1px, transparent 1px), - linear-gradient(to bottom, var(--border) 1px, transparent 1px); - background-size: 44px 44px; - mask-image: radial-gradient(ellipse 90% 70% at 50% 0%, black 30%, transparent 75%); -} - -.text-gradient { - background: linear-gradient( - 100deg, - var(--foreground) 30%, - var(--accent) 65%, - var(--foreground) 95% - ); - -webkit-background-clip: text; - background-clip: text; - color: transparent; -} - @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - - *, - *::before, - *::after { + *, *::before, *::after { animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 77a2957..f300893 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,32 +1,29 @@ import type { Metadata, Viewport } from "next"; -import { IBM_Plex_Sans, JetBrains_Mono } from "next/font/google"; +import { Inter, JetBrains_Mono } from "next/font/google"; import type { ReactNode } from "react"; -import { SiteFooter } from "@/components/site-footer"; import { SiteHeader } from "@/components/site-header"; import { ThemeProvider } from "@/components/theme-provider"; import type { SearchItem } from "@/components/search"; -import { getBlogPosts, getChangelog, getDocSections } from "@/lib/content"; +import { getChangelog, getDocSections } from "@/lib/content"; import { SITE_NAME, SITE_TAGLINE, SITE_URL } from "@/lib/site"; import "./globals.css"; -const plexSans = IBM_Plex_Sans({ - variable: "--font-plex-sans", +const inter = Inter({ + variable: "--font-sans", subsets: ["latin"], - weight: ["400", "500", "600", "700"], }); const jetbrainsMono = JetBrains_Mono({ - variable: "--font-jetbrains-mono", + variable: "--font-mono", subsets: ["latin"], - weight: ["400", "500", "600", "700"], }); export const metadata: Metadata = { metadataBase: new URL(SITE_URL), title: { - default: `${SITE_NAME} · The agent framework you can read in one sitting`, + default: `${SITE_NAME} Documentation`, template: `%s · ${SITE_NAME}`, }, description: SITE_TAGLINE, @@ -36,7 +33,7 @@ export const metadata: Metadata = { publisher: SITE_NAME, category: "technology", keywords: [ - "agentling", + "Agentling", "AI agents", "agent framework", "Python", @@ -60,8 +57,7 @@ export const metadata: Metadata = { alternates: { types: { "application/rss+xml": [ - { url: "/feed.xml", title: "agentling blog" }, - { url: "/changelog.xml", title: "agentling changelog" }, + { url: "/changelog.xml", title: "Agentling changelog" }, ], }, }, @@ -78,15 +74,14 @@ export const metadata: Metadata = { export const viewport: Viewport = { themeColor: [ - { media: "(prefers-color-scheme: dark)", color: "#0b0e14" }, - { media: "(prefers-color-scheme: light)", color: "#fbfbf9" }, + { media: "(prefers-color-scheme: dark)", color: "#000000" }, + { media: "(prefers-color-scheme: light)", color: "#ffffff" }, ], }; async function buildSearchIndex(): Promise { - const [sections, posts, releases] = await Promise.all([ + const [sections, releases] = await Promise.all([ getDocSections(), - getBlogPosts(), getChangelog(), ]); @@ -99,12 +94,6 @@ async function buildSearchIndex(): Promise { group: section.title, })), ), - ...posts.map((post) => ({ - title: post.title, - description: post.excerpt, - href: `/blog/${post.slug}`, - group: "Blog", - })), ...releases.map((release) => ({ title: `v${release.version} ${release.title}`.trim(), description: `Released ${release.releaseDate}`, @@ -124,21 +113,20 @@ export default async function RootLayout({ lang="en" suppressHydrationWarning data-scroll-behavior="smooth" - className={`${plexSans.variable} ${jetbrainsMono.variable} h-full antialiased`} + className={`${inter.variable} ${jetbrainsMono.variable} h-full antialiased`} > - + Skip to content -
+
{children} -
- +
diff --git a/src/app/llms.txt/route.ts b/src/app/llms.txt/route.ts index 2cc0408..9694982 100644 --- a/src/app/llms.txt/route.ts +++ b/src/app/llms.txt/route.ts @@ -1,31 +1,26 @@ -import { getBlogPosts, getChangelog, getDocSections } from "@/lib/content"; -import { GITHUB_URL, SITE_TAGLINE, SITE_URL } from "@/lib/site"; +import { getDocSections } from "@/lib/content"; +import { SITE_NAME, SITE_TAGLINE, SITE_URL } from "@/lib/site"; export const dynamic = "force-static"; -/** - * llms.txt index per https://llmstxt.org: a compact, LLM-friendly map of - * the site. The full corpus lives at /llms-full.txt. - */ -export async function GET() { - const [sections, posts, releases] = await Promise.all([ - getDocSections(), - getBlogPosts(), - getChangelog(), - ]); +export async function GET(): Promise { + const sections = await getDocSections(); const lines: string[] = [ - "# agentling", + `# ${SITE_NAME}`, "", - `> ${SITE_TAGLINE} A clean ReAct loop, typed memory, streaming events, recoverable failures, and progressive-disclosure skills, in a Python codebase small enough to read in one sitting.`, + `> ${SITE_TAGLINE}`, "", - "agentling is an open source (MIT) Python framework. This site hosts its documentation, blog, and changelog. The complete documentation text is available at " + - `${SITE_URL}/llms-full.txt.`, + "Agentling is an open source (MIT) Python framework. This site hosts its documentation and changelog. The complete documentation text is available at " + + `${SITE_URL}/llms-full.txt for LLM digestion.`, + "", + "## Documentation", "", ]; for (const section of sections) { - lines.push(`## ${section.title}`, ""); + lines.push(`### ${section.title}`); + lines.push(""); for (const page of section.pages) { lines.push( `- [${page.title}](${SITE_URL}/docs/${section.slug}/${page.slug}): ${page.description}`, @@ -34,23 +29,16 @@ export async function GET() { lines.push(""); } - lines.push("## Blog", ""); - for (const post of posts) { - lines.push(`- [${post.title}](${SITE_URL}/blog/${post.slug}): ${post.excerpt}`); - } + lines.push("## Project"); lines.push(""); - - lines.push("## Optional", ""); - lines.push(`- [Changelog](${SITE_URL}/changelog): all releases, latest v${releases[0]?.version ?? ""}`); - lines.push(`- [Roadmap](${SITE_URL}/roadmap): where the framework is heading, milestone by milestone`); - lines.push(`- [Contributing](${SITE_URL}/contributing): development setup and guidelines`); - lines.push(`- [GitHub repository](${GITHUB_URL}): source code`); + lines.push(`- [Changelog](${SITE_URL}/changelog): every release documented`); + lines.push(`- [Contributing](${SITE_URL}/contributing): how to set up the dev environment and run checks`); lines.push(""); return new Response(lines.join("\n"), { headers: { "Content-Type": "text/plain; charset=utf-8", - "Cache-Control": "public, max-age=300, stale-while-revalidate=3600", + "Cache-Control": "public, max-age=3600, s-maxage=86400", }, }); } diff --git a/src/app/page.tsx b/src/app/page.tsx index c749de6..102bc3e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,447 +1,5 @@ -import { - ArrowRight, - BookOpenText, - CircleCheck, - CircleMinus, - GitMerge, - HeartPulse, - Layers, - Radio, - Zap, -} from "lucide-react"; -import Link from "next/link"; - -import { GithubIcon } from "@/components/icons/github"; - -import { CodeTabs } from "@/components/home/code-tabs"; -import { Hero } from "@/components/home/hero"; -import { JsonLd } from "@/components/json-ld"; -import { Markdown } from "@/components/markdown"; -import { Reveal } from "@/components/reveal"; -import { pageMetadata } from "@/lib/seo"; -import { - AUTHOR_JSON_LD, - GITHUB_URL, - SITE_NAME, - SITE_TAGLINE, - SITE_URL, -} from "@/lib/site"; - -export const metadata = { - ...pageMetadata({ - title: `${SITE_NAME} · The agent framework you can read in one sitting`, - description: SITE_TAGLINE, - path: "/", - }), - // Opt out of the layout's "%s · agentling" template on the homepage. - title: { absolute: `${SITE_NAME} · The agent framework you can read in one sitting` }, -}; - -const HOME_JSON_LD = { - "@context": "https://schema.org", - "@graph": [ - { - "@type": "WebSite", - name: SITE_NAME, - url: SITE_URL, - description: SITE_TAGLINE, - }, - { - "@type": "SoftwareSourceCode", - name: SITE_NAME, - description: SITE_TAGLINE, - url: SITE_URL, - codeRepository: GITHUB_URL, - programmingLanguage: "Python", - runtimePlatform: "Python 3.11+", - license: "https://opensource.org/license/mit", - author: AUTHOR_JSON_LD, - }, - ], -}; - -const STATS = [ - { value: "1", label: "loop for everything" }, - { value: "2", label: "runtime dependencies" }, - { value: "More", label: "tests than source" }, - { value: "0", label: "magic: no metaclasses, no registry, no DSL" }, -]; - -const FEATURES = [ - { - icon: Zap, - title: "Async first", - description: - "The loop, tools, and model calls are all async. Tool calls in a single step run concurrently by default.", - }, - { - icon: GitMerge, - title: "One code path", - description: - "Blocking and streaming share the exact same loop. There is a single async generator; blocking mode just drains it.", - }, - { - icon: Layers, - title: "Typed memory", - description: - "A run is a list of typed steps that serialize to JSON, so persistence, replay, and multi-turn continuation come for free.", - }, - { - icon: Radio, - title: "Streaming events", - description: - "Five frozen event types narrate every run: text deltas, tool calls, results, recorded steps, and the final answer.", - }, - { - icon: HeartPulse, - title: "Self-healing", - description: - "A tool that raises becomes an observation the model can recover from, not a crash. Stuck loops get a nudge.", - }, - { - icon: BookOpenText, - title: "Progressive skills", - description: - "Drop a SKILL.md folder in and the model sees only its name until it decides to load it. Big skill libraries stay cheap.", - }, -]; - -const LOOP_STEPS = [ - { title: "Render the prompt", detail: "Typed memory becomes model messages." }, - { title: "Stream the model turn", detail: "Text deltas surface as they arrive." }, - { title: "Run the tool calls", detail: "Concurrently, with schema validation." }, - { title: "Record the step", detail: "Usage, duration, and results, all typed." }, - { title: "Check for an answer", detail: "final_answer, plain text, or step limit." }, -]; - -const EVENTS = [ - { name: "TextDelta", meaning: "a chunk of streamed text" }, - { name: "ToolCallEvent", meaning: "a tool is about to run" }, - { name: "ToolResultEvent", meaning: "a tool finished" }, - { name: "StepEvent", meaning: "a step hit memory" }, - { name: "FinalEvent", meaning: "the run is done" }, -]; - -const GOOD_FIT = [ - "You want to audit every decision an agent makes: a service calling trusted tools, a CLI assistant, a background worker.", - "You need runs you can persist, replay, resume, and test offline without an API key.", - "You want a framework your whole team can read end to end and reason about.", -]; - -const BAD_FIT = [ - "You want orchestration graphs and a built-in tracing dashboard out of the box.", - "You need sandboxed execution for untrusted tools; agentling tools run as trusted code.", - "You expect automatic context summarization on very long runs; you supply the context_manager.", -]; - -const BLOCKING_SNIPPET = [ - "```python", - "import asyncio", - "", - "from agentling import Agent, OpenAIModel, tool", - "", - "", - "@tool", - "def get_weather(city: str) -> str:", - ' """Get the current weather for a city.', - "", - " Args:", - " city: The city to look up.", - ' """', - ' return f"It is 22C and sunny in {city}."', - "", - "", - "async def main() -> None:", - ' agent = Agent(model=OpenAIModel("gpt-4o-mini"), tools=[get_weather])', - " print(await agent.run(\"What's the weather in Paris?\"))", - "", - "", - "asyncio.run(main())", - "```", -].join("\n"); - -const STREAMING_SNIPPET = `\`\`\`python -from agentling import FinalEvent, TextDelta, ToolCallEvent - -async for event in agent.run("Summarize this.", stream=True): - if isinstance(event, TextDelta): - print(event.text, end="", flush=True) - elif isinstance(event, ToolCallEvent): - print(f"\\n[calling {event.tool_call.name}]") - elif isinstance(event, FinalEvent): - print(f"\\nDone: {event.answer}") -\`\`\``; - -const SKILLS_SNIPPET = `\`\`\`python -agent = Agent( - model=OpenAIModel("gpt-4o-mini"), - tools=[read_file], - skills=["skills/code-reviewer"], -) - -# The model sees only the skill catalog until it -# decides to call load_skill("code-reviewer"). -print(await agent.run("Review src/agentling/agent.py")) -\`\`\``; - -const MEMORY_SNIPPET = `\`\`\`python -session = agent.start() -await session.run("First question") - -saved = session.memory.dump_json() - -# ...later, even in another process... -restored = agent.start() -restored.memory = Memory.load_json(saved) -await restored.run("A follow-up", reset=False) -\`\`\``; +import { redirect } from "next/navigation"; export default function HomePage() { - return ( - <> - - - - {/* Stats strip */} -
-
- {STATS.map((stat) => ( -
-

- {stat.value} -

-

- {stat.label} -

-
- ))} -
-
- - {/* Features */} -
- -

- Why agentling -

-

- Read the source. All of it. Tonight. -

-

- Agents fail in weird ways, and in most frameworks you cannot see - why: the decision happened somewhere inside a graph, a registry, - or a callback forest. agentling is an agent that is just a loop - you can read. Every step is typed, every event is observable, - every failure is recoverable, and every run is resumable and - testable offline. -

-
- -
- {FEATURES.map((feature, index) => ( - -
- - - -

- {feature.title} -

-

- {feature.description} -

-
-
- ))} -
-
- - {/* The loop + code tour */} -
-
- -

- The loop -

-

- One iteration, one step -

-

- Each step streams a model turn, runs the tools it asked for, - records the outcome, and checks whether the run is done. Blocking - and streaming, persistence and resume, skills and self-healing - all hang off this one generator. -

-
    - {LOOP_STEPS.map((step, index) => ( -
  1. - - {index + 1} - - - - {step.title} - - - {step.detail} - - -
  2. - ))} -
-
- - - , - }, - { - label: "Streaming", - panel: , - }, - { label: "Skills", panel: }, - { label: "Memory", panel: }, - ]} - /> - -
-
- - {/* Events strip */} -
-
- -
-

- Observability -

-

- Every run narrates itself -

-

- Five frozen event types describe everything an agent does. Feed - them to a terminal, a UI, a logger, or a metrics store; the - stream and the durable memory can never disagree. -

-
-
- -
- {EVENTS.map((event, index) => ( - - - - {event.name} - - - {event.meaning} - - - {index < EVENTS.length - 1 && ( - - )} - - ))} -
-
-
-
- - {/* Fit: honest filter */} -
-
- -

- An honest filter -

-

- Is agentling right for your project? -

-
-
- -
-

- Reach for agentling when -

-
    - {GOOD_FIT.map((line) => ( -
  • - - - {line} - -
  • - ))} -
-
-
- -
-

- Look elsewhere when -

-
    - {BAD_FIT.map((line) => ( -
  • - - - {line} - -
  • - ))} -
-
-
-
-
-
- - {/* CTA */} -
-
-
- -

- Know exactly what your agent will do -

-

- No metaclasses, no plugin registry, no DSL. Install it, read the - loop, and ship an agent you can explain. -

-
- - Read the docs - - - - - Star on GitHub - -
-
-
-
- - ); + redirect("/docs/getting-started/introduction"); } diff --git a/src/app/search-index.json/route.ts b/src/app/search-index.json/route.ts index 5f2271f..c9a5935 100644 --- a/src/app/search-index.json/route.ts +++ b/src/app/search-index.json/route.ts @@ -1,9 +1,9 @@ -import { getSearchDocuments } from "@/lib/content/search"; +import { buildSearchDocs } from "@/lib/content/search"; export const dynamic = "force-static"; export async function GET() { - const documents = await getSearchDocuments(); + const documents = await buildSearchDocs(); return Response.json(documents, { headers: { "Cache-Control": "public, max-age=300, stale-while-revalidate=3600", diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 49a5730..e61cd5b 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,27 +1,22 @@ import type { MetadataRoute } from "next"; -import { - getBlogPosts, - getDocSections, - getSitePageSlugs, -} from "@/lib/content"; +import { getDocSections, getSitePageSlugs } from "@/lib/content"; import { SITE_URL } from "@/lib/site"; export default async function sitemap(): Promise { - const [sections, posts, sitePageSlugs] = await Promise.all([ + const [sections, sitePageSlugs] = await Promise.all([ getDocSections(), - getBlogPosts(), getSitePageSlugs(), ]); const now = new Date(); - return [ + const staticRoutes: MetadataRoute.Sitemap = [ { url: SITE_URL, lastModified: now, changeFrequency: "weekly", - priority: 1, + priority: 1.0, }, { url: `${SITE_URL}/docs`, @@ -29,37 +24,29 @@ export default async function sitemap(): Promise { changeFrequency: "weekly", priority: 0.9, }, - { - url: `${SITE_URL}/blog`, - lastModified: now, - changeFrequency: "weekly", - priority: 0.7, - }, { url: `${SITE_URL}/changelog`, lastModified: now, changeFrequency: "weekly", priority: 0.7, }, - ...sections.flatMap((section) => - section.pages.map((page) => ({ - url: `${SITE_URL}/docs/${section.slug}/${page.slug}`, - lastModified: now, - changeFrequency: "weekly" as const, - priority: 0.9, - })), - ), - ...posts.map((post) => ({ - url: `${SITE_URL}/blog/${post.slug}`, - lastModified: new Date(post.publishDate), - changeFrequency: "monthly" as const, - priority: 0.8, - })), - ...sitePageSlugs.map((slug) => ({ - url: `${SITE_URL}/${slug}`, + ]; + + const docRoutes: MetadataRoute.Sitemap = sections.flatMap((section) => + section.pages.map((page) => ({ + url: `${SITE_URL}/docs/${section.slug}/${page.slug}`, lastModified: now, - changeFrequency: "monthly" as const, - priority: ["contributing", "roadmap"].includes(slug) ? 0.6 : 0.3, + changeFrequency: "weekly" as const, + priority: 0.9, })), - ]; + ); + + const standaloneRoutes: MetadataRoute.Sitemap = sitePageSlugs.map((slug) => ({ + url: `${SITE_URL}/${slug}`, + lastModified: now, + changeFrequency: "monthly" as const, + priority: slug === "contributing" ? 0.6 : 0.3, + })); + + return [...staticRoutes, ...docRoutes, ...standaloneRoutes]; } diff --git a/src/components/blog/post-cover.tsx b/src/components/blog/post-cover.tsx deleted file mode 100644 index 65260ef..0000000 --- a/src/components/blog/post-cover.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { LogoMark } from "@/components/logo"; -import { cn } from "@/lib/utils"; - -const GRADIENTS = [ - "from-emerald-500/25 via-teal-500/15 to-transparent", - "from-sky-500/25 via-indigo-500/15 to-transparent", - "from-amber-500/25 via-orange-500/15 to-transparent", - "from-fuchsia-500/25 via-purple-500/15 to-transparent", -]; - -function hashSlug(slug: string): number { - let hash = 0; - for (const char of slug) { - hash = (hash * 31 + char.charCodeAt(0)) % 997; - } - return hash; -} - -/** - * Deterministic decorative cover so every post gets a stable, distinct - * header without needing image assets in the CMS. - */ -export function PostCover({ - slug, - className, -}: { - slug: string; - className?: string; -}) { - const gradient = GRADIENTS[hashSlug(slug) % GRADIENTS.length]; - - return ( -
-
-
- -
- ); -} diff --git a/src/components/docs/feedback-widget.tsx b/src/components/docs/feedback-widget.tsx new file mode 100644 index 0000000..d455136 --- /dev/null +++ b/src/components/docs/feedback-widget.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { Check, ThumbsDown, ThumbsUp } from "lucide-react"; +import { useState } from "react"; + +export function FeedbackWidget() { + const [voted, setVoted] = useState<"yes" | "no" | null>(null); + const [comment, setComment] = useState(""); + const [submitted, setSubmitted] = useState(false); + + function handleVote(choice: "yes" | "no") { + setVoted(choice); + } + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setSubmitted(true); + } + + if (submitted) { + return ( +
+ + Thank you for your feedback. +
+ ); + } + + return ( +
+
+ + Was this page helpful? + +
+ + +
+
+ + {voted && ( +
+ +
+ setComment(e.target.value)} + placeholder="Your thoughts..." + className="flex-1 rounded border border-border bg-background px-2.5 py-1 text-xs text-foreground placeholder:text-faint focus:border-accent focus:outline-none" + /> + +
+
+ )} +
+ ); +} diff --git a/src/components/docs/sidebar.tsx b/src/components/docs/sidebar.tsx index 55585aa..daed31d 100644 --- a/src/components/docs/sidebar.tsx +++ b/src/components/docs/sidebar.tsx @@ -1,67 +1,112 @@ "use client"; -import { - BookOpen, - ChevronDown, - Code, - Compass, - Layers, - Rocket, - type LucideIcon, -} from "lucide-react"; +import { ChevronDown } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useState } from "react"; import type { DocSection } from "@/lib/content/types"; +import { GITHUB_URL } from "@/lib/site"; import { cn } from "@/lib/utils"; -const ICONS: Record = { - rocket: Rocket, - layers: Layers, - compass: Compass, - code: Code, - "book-open": BookOpen, +// Diátaxis classification tags for section headers +const DIATAXIS_HINTS: Record = { + "getting-started": "Tutorials", + "core-concepts": "Concepts", + guides: "How-To", + reference: "Reference", }; function SectionNav({ sections }: { sections: DocSection[] }) { const pathname = usePathname(); return ( -