Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# a2learn docs

The [landing page](./index.html) carries the why; these pages carry the
how. Start from who you are:

- **Building an AI tutor?** [Quickstart](./quickstart.md) →
[The evidence contract](./evidence.md) — point your agent at the server,
then read what flows back when a student works.
- **Evaluating for a platform or district?**
[How the loop works](./the-loop.md) → [Architecture](./architecture.md)
— the mechanism end to end, then the seams and where student data
cannot go.
- **An educator or instructional designer?**
[The learning science behind the design](./pedagogy.md) — every
structural choice, traced to its finding.
- **Contributing an activity kind or a standards source?**
[The widget registry](./registry.md) →
[CONTRIBUTING](../CONTRIBUTING.md).

## All pages

**Learn** — how and why it works

| Page | Answers |
|---|---|
| [How the loop works](./the-loop.md) | Practice as a tool call: one request edge in, three return edges out |
| [Pedagogy](./pedagogy.md) | The learning-science lineage of the design, and what deeper grounding is coming |
| [Architecture](./architecture.md) | The four seams, the protocol facades, and the student-data boundary |

**Guides** — get something running

| Page | Answers |
|---|---|
| [Quickstart](./quickstart.md) | Point an MCP host at it, or run your own instance, in minutes |
| [Configuration](./configuration.md) | Every environment variable, and the two operational warnings |

**Reference** — the contracts

| Page | Answers |
|---|---|
| [The widget registry](./registry.md) | The core extension API: catalog entries, generators, adding a kind |
| [MCP tools](./mcp-tools.md) | The wire surface an agent calls, shipped and planned |
| [The evidence contract](./evidence.md) | The event shapes, their honesty rules, and what reads them |

**Project** — for people who represent it

| Page | Answers |
|---|---|
| [Messaging guide](./messaging.md) | The sentence, the category, the claim-hygiene rules, and the boilerplate — so everyone pitches it identically |

The a2learn document format draft (surface + manifest, conformance classes)
lives separately under `docs/a2learn/` once its branch merges.

Two ground rules that shape everything here:

1. **The model never writes code that runs.** Generated output fills Zod
schemas that render against components humans wrote and reviewed.
2. **Student identity never reaches this server from an integration.**
Anonymous ids only on the wire; anything richer lives in the host.
87 changes: 87 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Architecture

One Next.js process, four deliberate seams. The app you see at `/` is the
reference client for the server underneath — adopt the seams, not the app.

```
agent / MCP host product backend teachers & students
│ │ │
▼ ▼ ▼
/api/mcp ◄──────────► /api/widget (REST) reference app (/)
└──────────────┬─────────────┘
service core (buildWidget, pathway pipeline)
┌─────────────────┼──────────────────┐
▼ ▼ ▼
widget registry standards source storage adapter
(catalog + (verify / (memory | supabase)
generators) decompose /
progression)
```

## Seam 1 — the widget registry

The core extension API: catalog entries (schema + component + pedagogy
metadata) and server-only generators, covered in depth in
[registry.md](./registry.md).

## Seam 2 — the standards source

Verification is pluggable. A source implements one interface
([`src/lib/standards/types.ts`](../src/lib/standards/types.ts)):

```ts
interface StandardsSource {
id: string;
label: string;
proposalPromptFragment: string; // notation and examples for the propose step
verify(code: string, jurisdiction?: string): Promise<StandardRef | null>;
decompose(standard: StandardRef): Promise<LearningComponentRef[]>;
progression(standard: StandardRef, direction: 'backward' | 'forward'): Promise<StandardRef[]>;
}
```

`verify` returning `null` is the honest answer "that code doesn't exist" —
the pipeline surfaces it instead of building on a hallucination. Learning
Commons is the default source; `src/lib/standards/example.ts` is a built-in,
keyless source that exists to be copied for a state framework, IB, or a
district curriculum map. Select with `STANDARDS_SOURCE`. Nothing downstream
assumes US notation or English.

## Seam 3 — the storage adapter

All persistence goes through one interface
([`src/lib/storage/types.ts`](../src/lib/storage/types.ts)): sessions,
interaction events, assignments, profiles. Two implementations ship —
`memory` (the default; nothing survives a restart, perfect for trying the
server) and `supabase` (a local stack via `pnpm db:start`). Select with
`STORAGE_ADAPTER`. A different backend implements the interface; report
aggregation logic is shared, not duplicated per adapter.

## Seam 4 — the protocol facades

MCP (`/api/mcp`) and REST (`/api/widget`, growing into `/api/v0`) are thin
facades over the same service core. The rule is structural: logic never
lives in a facade, so a new transport is a new file, not a fork of the
business logic. [mcp-tools.md](./mcp-tools.md) documents the surface.

## Where student data goes — and doesn't

- **Model calls** (generation, scoring) run under the deployment's own keys.
Student work sent for scoring goes to the LLM provider *you* configured,
from *your* instance — never to a third party of ours.
- **On the wire from integrations**, students exist only as anonymous ids.
There is no integration path that sends student identity to this server.
- **The reference app's optional roster** (names a teacher enters) and all
interaction evidence live in the deployment's own storage adapter. Running
`memory`, they evaporate on restart; running your own Supabase, they're in
your database, under your data-processing terms.
- **Educator identity** is the one identity that may eventually reach the
server edge (per-educator OAuth for hosted multi-user instances is
roadmapped, via an identity adapter). Student identity never does — hosts
own students.

That boundary is the architecture expressing a value, the same way
offline-first is for Kolibri: a district evaluating this doesn't have to
trust a promise, just read where the data can't go.
25 changes: 25 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Configuration reference

Every environment variable the server reads. One secret is required (an LLM
key); everything else has a working default — no database, no accounts.

| Variable | Default | What it does |
|---|---|---|
| `LLM_PROVIDER` | `anthropic` | Which provider builds activities: `anthropic`, `openai`, `bedrock`, or `openrouter` |
| `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `OPENROUTER_API_KEY` | — | The key for the chosen provider |
| `AWS_REGION`, `AWS_BEARER_TOKEN_BEDROCK` | `us-west-2`, — | Bedrock configuration when `LLM_PROVIDER=bedrock` |
| `OPENAI_FALLBACK_API_KEY`, `OPENAI_FALLBACK_MODEL_ID` | —, `gpt-4o-mini` | Optional second provider used when the primary fails |
| `OPENROUTER_EMBED_MODEL_ID` / `BEDROCK_EMBED_MODEL_ID` | `openai/text-embedding-3-small` / `amazon.titan-embed-text-v2:0` | Embedding model for semantic activity discovery. Discovery ranks semantically when the provider can embed (OpenRouter, Bedrock, or an armed OpenAI fallback key) and falls back to lexical ranking otherwise — the result says which ran |
| `STANDARDS_SOURCE` | `learning-commons` | Which standards graph verifies codes; `example` is the built-in keyless source |
| `LEARNING_COMMONS_API_KEY`, `LEARNING_COMMONS_MCP_URL` | — | Credentials for the default standards source |
| `STORAGE_ADAPTER` | auto | `memory` (nothing persists across restarts) or `supabase`; unset picks Supabase only when its vars are configured |
| `NEXT_PUBLIC_SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY` | — | Only when using the Supabase adapter; `pnpm db:start` runs a local stack |
| `SEED_DEMO_ROSTER` | off | Seeds demo students in the reference app |

Two operational notes:

- Model calls run under **your** keys, inside **your** deployment — scoring
a student's work never leaves the instance you control.
- **Public instances:** anyone who has your URL can spend your LLM budget.
Keep instances private until token auth (`MCP_ACCESS_TOKEN`, planned for
v0.1) lands, or put your own gateway in front.
83 changes: 83 additions & 0 deletions docs/evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# The evidence contract

The project exists so students *do* — think critically, practice, produce —
instead of passively consuming AI output. The extensible widgets and the
standards-verified loop are that commitment made structural. The evidence
contract is how the loop closes: when a student works through an activity,
what happened flows back in a shape a program can act on — the agent that
assigned it, the teacher's report, the remediation loop. Chat alone can't
see whether the doing happened; this contract is what makes it visible.

## The interaction event

Every widget that assesses emits events through the shared telemetry hook
(`useWidgetTelemetry`). The persisted shape
([`src/lib/storage/types.ts`](../src/lib/storage/types.ts)):

```ts
type InteractionEvent = {
sessionId: string;
studentId: string; // anonymous id — see the boundary note below
widgetKind: string;
eventType: string;
standardCode: string | null;
learningComponentId: string | null;
elapsedMs: number;
correct: boolean | null; // null = this event carries no verdict
payload: Record<string, unknown>; // per-kind detail; stepIndex rides here
};
```

`eventType` is a string by design (adapters shouldn't need a migration for a
new event), with these conventional values:

| Event | When | `correct` |
|---|---|---|
| `widget_shown` | the activity mounts | `null` |
| `answer_checked` | a checkable attempt is judged | `true` / `false` |
| `hint_requested` | the student asks for help | `null` |
| `widget_completed` | the activity's done moment | the final verdict, or `null` for non-assessing kinds |
| `hesitation` | a long pause on an open question | `null` |

Two rules keep the data honest:

- **A verdict is a claim.** In-progress work (a draft being written, an
unfinished sort) reports `correct: null` — being mid-task is not being
wrong. Only kinds whose catalog entry says `assesses: true` emit verdicts
at all; see [the registry](./registry.md).
- **`stepIndex` rides inside `payload`.** In a pathway, `widget_completed`
carries which step it was, which is what makes per-step reporting possible.

## What reads it

- **The session report** aggregates per-student accuracy, attempts, hints,
median time, and a per-step evidence strip (first try / needed attempts /
still wrong / not reached — the strip never guesses about events recorded
before `stepIndex` existed).
- **The remediation loop** watches `widget_completed` verdicts; a wrong one
can inject a re-teach step after the student's current position. Injected
steps are announced, and cost the student nothing.
- **The student profile** weights later pathway generation toward what a
student found hard.
- **A hosting agent** — in a chat host, finish-line results are also
reported into the conversation in prose, so the assistant that assigned
the activity can adapt.

## The boundary

`studentId` is an anonymous identifier minted by the deployment. No
integration path sends a student's identity to this server; when the
reference app's optional roster maps anonymous ids to names, that mapping
lives in **your** storage adapter, inside **your** deployment, and never
leaves it. Educator identity may eventually reach the server edge (OAuth for
hosted instances is on the roadmap); student identity never does.

## Where the contract is going

v0.1's headline work is a **universal `WidgetResult`** — one typed result
shape across all sixteen kinds (status, verdict, attempts, and a per-kind
`detail` naming what the struggle was), so an SDK consumer writes one
`onResult` handler, not sixteen. The event stream above stays; the result is
its per-activity summary. An xAPI statement export for LRS pipelines is a
mapping over this contract, not a rewrite, and sits on the pull-gated
roadmap.
Loading
Loading