Files
learnharness/src/cli.ts
T
Amos a72c902d30 v3: AI SDK pivot — pi removed, Next.js 16 + AI SDK 7 + AI Elements teacher app
The teaching agent no longer runs on pi. Stack:
- ToolLoopAgent (ai@7) + OpenRouter (OPENROUTER_API_KEY), model via LH_MODEL,
  default anthropic/claude-sonnet-4.5 with reasoning tokens
- quiz/ask_user_question = client-side tools (no server execute): the call
  lands as a typed tool part in input-available state; the card grades via
  src/core/question-core.ts; addToolOutput sends the verdict; the teacher's
  reaction continues the same assistant message
- resume-mid-quiz is free: the pending tool part persists in the transcript
- persistence: one JSON per chat (data/chats/), full UIMessage transcripts,
  saved onFinish (also on abort)
- UI: AI Elements (owned copy-paste components, pruned to the five in use)
  + button-based QuizCard/AnsweredCard (ask answers never graded)
- pi fully removed: TUI, extensions, Fastify+WS server, bridge, custom
  protocol all deleted (recoverable from git history)
- tests: question-core units carry over; chat-flow integration drives
  handleChatRequest with MockLanguageModelV4 (48 tests, zero tokens);
  render smoke tests guard the cards (renderToStaticMarkup)

Carried over unchanged: teacher system prompt, question-core, schema/
ledger/derive/extract pipeline, data model.
2026-08-22 02:18:27 +03:00

70 lines
2.2 KiB
JavaScript

#!/usr/bin/env node
/**
* learning-harness CLI
*
* ingest <file> normalize a session JSONL and print the canonical document
* (Stage 1 only — extractor/merger not yet implemented)
* graph print graph/ledger stats
* what-next (not yet implemented)
*
* The web app (Next.js + AI SDK): npm run dev
*/
import * as fs from "node:fs";
import { normalizeSession } from "./extract/normalize.ts";
import { ledgers } from "./ledger.ts";
import { deriveAll } from "./derive.ts";
import { PATHS } from "../config.ts";
async function main() {
const [command, ...rest] = process.argv.slice(2);
switch (command) {
case "ingest": {
const file = rest[0];
if (!file) {
console.error("Usage: ingest <session.jsonl>");
process.exit(1);
}
const doc = normalizeSession(file);
ledgers.appendSession(doc.session);
console.log(`Session: ${doc.session.id}`);
console.log(`Topic: ${doc.session.topic}`);
console.log(`Entries: ${doc.entries.length}`);
const quizzes = doc.entries.filter((e) => e.kind === "quiz");
console.log(`Quizzes: ${quizzes.length}`);
// Full canonical document for inspection / later pipeline stages.
fs.mkdirSync(PATHS.graph, { recursive: true });
const out = `${PATHS.graph}/canonical-${doc.session.id.slice(0, 8)}.json`;
fs.writeFileSync(out, JSON.stringify(doc, null, "\t"), "utf-8");
console.log(`Canonical document written: ${out}`);
break;
}
case "graph": {
const nodes = ledgers.nodes();
const edges = ledgers.edges();
const events = ledgers.events();
const sessions = ledgers.sessions();
const derived = deriveAll();
console.log(`Sessions: ${sessions.length}`);
console.log(`Nodes: ${nodes.length} (known: ${derived.filter((d) => d.known).length})`);
console.log(`Edges: ${edges.length}`);
console.log(`Events: ${events.length}`);
break;
}
case "what-next": {
console.error("Not implemented yet — needs extractor + merger first.");
process.exit(1);
}
default: {
console.log("Commands: ingest <file> | graph | what-next");
process.exit(command ? 1 : 0);
}
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});