@fenixforce/edition-voices 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/addon.d.ts +45 -0
  2. package/dist/api/platform-middleware.d.ts +41 -0
  3. package/dist/api/platform-routes.d.ts +71 -0
  4. package/dist/assessment/hardened-grader.d.ts +76 -0
  5. package/dist/curriculum/zpd-selector.d.ts +57 -0
  6. package/dist/index.d.ts +68 -0
  7. package/dist/index.js +54 -0
  8. package/dist/manifest-merger.d.ts +13 -0
  9. package/dist/manifest.d.ts +13 -0
  10. package/dist/memory/learner-diary.d.ts +91 -0
  11. package/dist/system-prompt-patch.d.ts +17 -0
  12. package/dist/teacher/assessment/evaluator.d.ts +35 -0
  13. package/dist/teacher/assessment/feedback-generator.d.ts +11 -0
  14. package/dist/teacher/assessment/session-summary.d.ts +33 -0
  15. package/dist/teacher/corpus-importer.d.ts +44 -0
  16. package/dist/teacher/corpus-storage.d.ts +31 -0
  17. package/dist/teacher/curriculum/generator.d.ts +13 -0
  18. package/dist/teacher/curriculum/loader.d.ts +14 -0
  19. package/dist/teacher/curriculum/progress-tracker.d.ts +35 -0
  20. package/dist/teacher/curriculum/schema.d.ts +36 -0
  21. package/dist/teacher/exercises/builders/dialogue.d.ts +6 -0
  22. package/dist/teacher/exercises/builders/dictation.d.ts +6 -0
  23. package/dist/teacher/exercises/builders/error-correction.d.ts +6 -0
  24. package/dist/teacher/exercises/builders/fill-in-blank.d.ts +5 -0
  25. package/dist/teacher/exercises/builders/listening.d.ts +6 -0
  26. package/dist/teacher/exercises/builders/pronunciation.d.ts +6 -0
  27. package/dist/teacher/exercises/builders/reading.d.ts +6 -0
  28. package/dist/teacher/exercises/builders/sentence-construction.d.ts +6 -0
  29. package/dist/teacher/exercises/builders/translation.d.ts +7 -0
  30. package/dist/teacher/exercises/builders/vocabulary-drill.d.ts +5 -0
  31. package/dist/teacher/exercises/generator.d.ts +45 -0
  32. package/dist/teacher/exercises/index.d.ts +7 -0
  33. package/dist/teacher/importers/csv-importer.d.ts +22 -0
  34. package/dist/teacher/importers/json-importer.d.ts +11 -0
  35. package/dist/teacher/learner-model.d.ts +84 -0
  36. package/dist/teacher/learner-storage.d.ts +61 -0
  37. package/dist/teacher/level-classifier.d.ts +34 -0
  38. package/dist/teacher/loop/message-builder.d.ts +32 -0
  39. package/dist/teacher/loop/scheduler-integration.d.ts +45 -0
  40. package/dist/teacher/loop/teaching-loop.d.ts +72 -0
  41. package/dist/teacher/scrapthing/batch-job.d.ts +65 -0
  42. package/dist/teacher/scrapthing/content-cleaner.d.ts +27 -0
  43. package/dist/teacher/scrapthing/mcp-adapter.d.ts +50 -0
  44. package/dist/teacher/skills/index.d.ts +35 -0
  45. package/dist/teacher/skills/placement-test.d.ts +47 -0
  46. package/dist/teacher/skills/plan-lesson.d.ts +46 -0
  47. package/dist/teacher/spaced-repetition.d.ts +49 -0
  48. package/dist/teacher/teacher-memory.d.ts +79 -0
  49. package/dist/teacher/voice/custom-vocabulary.d.ts +28 -0
  50. package/dist/teacher/voice/exercise-voice.d.ts +39 -0
  51. package/dist/teacher/voice/language-voices.d.ts +26 -0
  52. package/dist/teacher/voice/pronunciation-scorer.d.ts +38 -0
  53. package/package.json +36 -0
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Voices Add-on — Loads teacher layer into a running Pro kernel.
3
+ *
4
+ * Pro keeps all existing tools (browser automation, skill marketplace, subagents).
5
+ * Teacher skills, platform API, and teaching loop are added alongside.
6
+ */
7
+ import type { EditionManifest } from "./manifest.js";
8
+ export interface FenixKernel {
9
+ manifest: EditionManifest;
10
+ systemPrompt: string;
11
+ toolRegistry: {
12
+ tool: (serverName: string, toolName: string, def: {
13
+ name: string;
14
+ description?: string;
15
+ handler: (input: any, ctx: any) => Promise<any>;
16
+ }) => void;
17
+ tools: () => Array<{
18
+ name: string;
19
+ }>;
20
+ };
21
+ httpServer: {
22
+ mount: (handler: (req: Request) => Promise<Response>) => void;
23
+ };
24
+ memory: {
25
+ registerCategories: (categories: string[]) => void;
26
+ };
27
+ events: {
28
+ emit: (event: string, data?: unknown) => void;
29
+ };
30
+ }
31
+ export interface VoicesAddonOptions {
32
+ kernel: FenixKernel;
33
+ licenseKey: string;
34
+ }
35
+ export interface VoicesAddonDeps {
36
+ validateLicense: (key: string) => Promise<boolean>;
37
+ runMigrations: (databaseUrl?: string) => Promise<void>;
38
+ createApiHandler: () => (req: Request) => Promise<Response>;
39
+ startScheduler: (learnerIds: string[]) => Promise<{
40
+ stop: () => void;
41
+ }>;
42
+ getActiveLearnerIds: () => Promise<string[]>;
43
+ }
44
+ export declare function validateLicenseKey(key: string, validate: (key: string) => Promise<boolean>): Promise<boolean>;
45
+ export declare function loadVoicesAddon(options: VoicesAddonOptions, deps: VoicesAddonDeps): Promise<void>;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Platform Middleware — API key validation, rate limiting, error formatting.
3
+ *
4
+ * All middleware operates on standard Request/Response objects and is
5
+ * framework-agnostic (works with Bun.serve, Express adapter, etc.).
6
+ */
7
+ export interface ApiKeyRecord {
8
+ key: string;
9
+ name: string;
10
+ enabled: boolean;
11
+ rateLimit: number;
12
+ }
13
+ export interface RateLimitEntry {
14
+ count: number;
15
+ windowStart: number;
16
+ }
17
+ export interface PlatformMiddlewareDeps {
18
+ lookupApiKey: (key: string) => Promise<ApiKeyRecord | null>;
19
+ }
20
+ export interface ApiError {
21
+ error: string;
22
+ code: string;
23
+ details?: unknown;
24
+ }
25
+ export interface PaginationParams {
26
+ cursor?: string;
27
+ limit: number;
28
+ }
29
+ export interface PaginatedResponse<T> {
30
+ data: T[];
31
+ cursor: string | null;
32
+ hasMore: boolean;
33
+ }
34
+ export declare function parsePagination(url: URL, defaultLimit?: number, maxLimit?: number): PaginationParams;
35
+ export declare function paginatedJson<T>(data: T[], limit: number, getCursor: (item: T) => string): Response;
36
+ export declare function validateApiKey(request: Request, deps: PlatformMiddlewareDeps): Promise<ApiKeyRecord | Response>;
37
+ export declare function checkKeyRateLimit(apiKey: string, maxPerMinute: number): Response | null;
38
+ /** Reset rate limit store (for testing). */
39
+ export declare function resetRateLimitStore(): void;
40
+ export declare function errorResponse(status: number, code: string, message: string, details?: unknown): Response;
41
+ export declare function jsonResponse(data: unknown, status?: number): Response;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Platform Routes — REST API endpoints for third-party integration.
3
+ *
4
+ * All routes are mounted under /api/v1 and require X-API-Key auth.
5
+ * List endpoints use cursor-based pagination.
6
+ */
7
+ import type { LearnerProfile, LearnerPreferences } from "../teacher/learner-model.js";
8
+ import type { Exercise } from "../teacher/exercises/generator.js";
9
+ import { type PlatformMiddlewareDeps } from "./platform-middleware.js";
10
+ export interface SessionRecord {
11
+ id: string;
12
+ learnerId: string;
13
+ exercises: Exercise[];
14
+ responses: ExerciseResponse[];
15
+ status: "active" | "completed";
16
+ summary?: SessionSummary;
17
+ startedAt: string;
18
+ completedAt?: string;
19
+ }
20
+ export interface ExerciseResponse {
21
+ exerciseId: string;
22
+ answer: string;
23
+ score: number;
24
+ feedback: string;
25
+ }
26
+ export interface SessionSummary {
27
+ totalExercises: number;
28
+ averageScore: number;
29
+ duration: number;
30
+ }
31
+ export interface ContentItem {
32
+ id: string;
33
+ language: string;
34
+ level: string;
35
+ type: string;
36
+ text: string;
37
+ tags: string[];
38
+ }
39
+ export interface LanguageInfo {
40
+ code: string;
41
+ name: string;
42
+ corpusSize: number;
43
+ learnerCount: number;
44
+ }
45
+ export interface PlatformRouteDeps extends PlatformMiddlewareDeps {
46
+ createLearner: (data: Partial<LearnerProfile>) => Promise<LearnerProfile>;
47
+ getLearner: (id: string) => Promise<LearnerProfile | null>;
48
+ updatePreferences: (id: string, prefs: Partial<LearnerPreferences>) => Promise<LearnerProfile>;
49
+ getProgress: (id: string) => Promise<unknown>;
50
+ getVocabulary: (id: string, cursor?: string, limit?: number) => Promise<ContentItem[]>;
51
+ getDueReviews: (id: string) => Promise<unknown>;
52
+ createSession: (learnerId: string) => Promise<SessionRecord>;
53
+ getSession: (id: string) => Promise<SessionRecord | null>;
54
+ submitResponse: (sessionId: string, exerciseId: string, answer: string) => Promise<ExerciseResponse>;
55
+ completeSession: (id: string) => Promise<SessionRecord>;
56
+ searchContent: (query: string, language?: string, level?: string, type?: string, cursor?: string, limit?: number) => Promise<ContentItem[]>;
57
+ addContent: (item: Partial<ContentItem>) => Promise<ContentItem>;
58
+ updateContent: (id: string, updates: Partial<ContentItem>) => Promise<ContentItem>;
59
+ importContent: (format: "csv" | "json", data: string) => Promise<{
60
+ imported: number;
61
+ }>;
62
+ listLanguages: () => Promise<LanguageInfo[]>;
63
+ getCurriculum: (code: string) => Promise<unknown | null>;
64
+ updateCurriculum: (code: string, data: unknown) => Promise<unknown>;
65
+ getLearnerAnalytics: (id: string) => Promise<unknown>;
66
+ getPlatformAnalytics: () => Promise<unknown>;
67
+ }
68
+ /**
69
+ * Create the platform API router.
70
+ */
71
+ export declare function createPlatformRouter(deps: PlatformRouteDeps): (request: Request) => Promise<Response>;
@@ -0,0 +1,76 @@
1
+ export interface GradeCriteria {
2
+ semantic_accuracy: number;
3
+ grammatical_form: number;
4
+ pronunciation_match?: number;
5
+ response_latency_ok: boolean;
6
+ length_normalized: boolean;
7
+ }
8
+ export interface GradeResult {
9
+ criteria: GradeCriteria;
10
+ /** Weighted composite score (0.0–1.0) */
11
+ composite: number;
12
+ /** Hacking signals detected */
13
+ flags: string[];
14
+ /** Learner-facing explanation */
15
+ feedback: string;
16
+ }
17
+ export interface GradeInput {
18
+ /** Learner's response */
19
+ response: string;
20
+ /** Expected answer(s) */
21
+ expectedAnswers: string[];
22
+ /** The exercise prompt/question */
23
+ exercisePrompt: string;
24
+ /** Expected response length (characters) */
25
+ expectedLength: number;
26
+ /** Time taken to respond (ms) */
27
+ responseLatencyMs: number;
28
+ /** Whether this is a voice exercise */
29
+ isVoiceExercise: boolean;
30
+ /** Previous scores for this exercise (for consistency check) */
31
+ previousScores?: number[];
32
+ }
33
+ export interface GraderConfig {
34
+ /** Weight for semantic accuracy in composite */
35
+ weightSemantic: number;
36
+ /** Weight for grammatical form in composite */
37
+ weightGrammar: number;
38
+ /** Weight for pronunciation (voice only) */
39
+ weightPronunciation: number;
40
+ /** Max response/expected length ratio before penalty */
41
+ maxLengthRatio: number;
42
+ /** Minimum response latency before flagging (ms) */
43
+ minLatencyMs: number;
44
+ /** Penalty multiplier for excessive length */
45
+ lengthPenalty: number;
46
+ }
47
+ export declare const DEFAULT_GRADER_CONFIG: GraderConfig;
48
+ export type SemanticScorerFn = (response: string, expectedAnswers: string[]) => Promise<number>;
49
+ export type GrammarScorerFn = (response: string, languageCode: string) => Promise<number>;
50
+ export type PronunciationScorerFn = (response: string, expected: string) => Promise<number>;
51
+ export type FeedbackGeneratorFn = (criteria: GradeCriteria, flags: string[], exercisePrompt: string) => Promise<string>;
52
+ export declare class HardenedGrader {
53
+ private config;
54
+ private scoreSemantic;
55
+ private scoreGrammar;
56
+ private scorePronunciation;
57
+ private generateFeedback;
58
+ constructor(deps: {
59
+ scoreSemantic: SemanticScorerFn;
60
+ scoreGrammar: GrammarScorerFn;
61
+ scorePronunciation?: PronunciationScorerFn;
62
+ generateFeedback: FeedbackGeneratorFn;
63
+ }, config?: Partial<GraderConfig>);
64
+ /**
65
+ * Grade a learner response with anti-hack defenses.
66
+ */
67
+ grade(input: GradeInput): Promise<GradeResult>;
68
+ /**
69
+ * Strip common cheating patterns from response.
70
+ */
71
+ private stripCheatingPatterns;
72
+ /**
73
+ * Compute weighted composite from criteria scores.
74
+ */
75
+ private computeComposite;
76
+ }
@@ -0,0 +1,57 @@
1
+ export interface ZPDWindow {
2
+ learnerId: string;
3
+ languageCode: string;
4
+ skillArea: string;
5
+ /** Highest fully mastered level (0–100) */
6
+ masteredLevel: number;
7
+ /** masteredLevel + stride */
8
+ stretchLevel: number;
9
+ /** masteredLevel + 2×stride — exercises above this cause abandonment */
10
+ abandonmentLevel: number;
11
+ }
12
+ export interface ZPDConfig {
13
+ /** Default stride (difficulty range above mastery) */
14
+ defaultStride: number;
15
+ /** Minimum stride (floor after repeated failures) */
16
+ minStride: number;
17
+ /** Sessions to look back for novelty filtering */
18
+ noveltyWindow: number;
19
+ /** Consecutive high-score threshold for boredom detection */
20
+ boredomThreshold: number;
21
+ /** Score threshold to count as "high" for boredom */
22
+ boredomScoreThreshold: number;
23
+ }
24
+ export declare const DEFAULT_ZPD_CONFIG: ZPDConfig;
25
+ export interface SkillMastery {
26
+ skillArea: string;
27
+ masteryLevel: number;
28
+ stride: number;
29
+ recentScores: number[];
30
+ }
31
+ export interface ExerciseCandidate {
32
+ id: string;
33
+ skillArea: string;
34
+ difficulty: number;
35
+ topic: string;
36
+ lastSeenSessionIndex?: number;
37
+ }
38
+ export interface ZPDSelection {
39
+ candidates: ExerciseCandidate[];
40
+ zpdWindow: ZPDWindow;
41
+ strideAdjusted: boolean;
42
+ }
43
+ export type LearnerMasteryFn = (learnerId: string, languageCode: string) => Promise<SkillMastery[]>;
44
+ export type ExerciseQueryFn = (languageCode: string, skillArea: string, minDifficulty: number, maxDifficulty: number) => Promise<ExerciseCandidate[]>;
45
+ export type RecentExercisesFn = (learnerId: string, sessionCount: number) => Promise<Set<string>>;
46
+ /**
47
+ * Compute the ZPD window for a skill area.
48
+ */
49
+ export declare function computeZPDWindow(learnerId: string, languageCode: string, mastery: SkillMastery, config?: ZPDConfig): ZPDWindow;
50
+ /**
51
+ * Select top-3 exercise candidates within the ZPD window.
52
+ */
53
+ export declare function selectExercises(learnerId: string, languageCode: string, skillArea: string, getMastery: LearnerMasteryFn, queryExercises: ExerciseQueryFn, getRecentExercises: RecentExercisesFn, config?: ZPDConfig): Promise<ZPDSelection>;
54
+ /**
55
+ * Update stride after an assessment score.
56
+ */
57
+ export declare function adjustStride(currentStride: number, score: number, config?: ZPDConfig): number;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Fenix Voices Edition — Voice-first agent with teacher layer
3
+ * for language learning, coaching, and conversational practice.
4
+ *
5
+ * Two deployment modes:
6
+ * 1. Standalone — `initVoicesStandalone()` boots the full Voices edition.
7
+ * 2. Kernel-integrated — `startVoices()` creates a kernel with teacher layers.
8
+ */
9
+ import { createKernel, bootKernel, KERNEL_VERSION, type KernelConfig, type BootConfig, type Kernel, type RunningKernel, type EditionManifest as KernelEditionManifest } from "@fenixforce/kernel";
10
+ import { type EditionManifest } from "./manifest.js";
11
+ export declare const EDITION: "voices";
12
+ export declare const VOICES_MANIFEST: KernelEditionManifest;
13
+ export declare const VOICES_KERNEL_CONFIG: KernelConfig;
14
+ export declare function startVoices(overrides?: Partial<KernelConfig>): Kernel;
15
+ /** Boot a fully-wired Voices kernel with agent loop, providers, tools, and voice pipeline. */
16
+ export declare function bootVoices(overrides?: Partial<BootConfig>): Promise<RunningKernel>;
17
+ export { createKernel, bootKernel, KERNEL_VERSION };
18
+ export interface VoicesConfig {
19
+ /** Postgres connection string. */
20
+ databaseUrl: string;
21
+ /** HTTP port for the platform API (default 3333). */
22
+ port?: number;
23
+ /** Voice defaults. */
24
+ ttsProvider?: string;
25
+ sttProvider?: string;
26
+ /** Speechmatics API key (for classroom STT with speaker focus). */
27
+ speechmaticsApiKey?: string;
28
+ /** Turn detection preset override (default: smart_turn for Voices). */
29
+ turnDetectionPreset?: "fast" | "fixed" | "adaptive" | "smart_turn" | "scribe" | "captions" | "external";
30
+ /** Workspace directory for speaker voiceprint persistence. */
31
+ workspaceDir?: string;
32
+ }
33
+ export interface VoicesInstance {
34
+ manifest: EditionManifest;
35
+ systemPrompt: string;
36
+ shutdown: () => Promise<void>;
37
+ }
38
+ export interface VoicesInitDeps {
39
+ runMigrations: (databaseUrl: string) => Promise<void>;
40
+ createToolRegistry: () => {
41
+ tool: (serverName: string, toolName: string, def: {
42
+ name: string;
43
+ description?: string;
44
+ handler: (input: any, ctx: any) => Promise<any>;
45
+ }) => void;
46
+ tools: () => Array<{
47
+ name: string;
48
+ }>;
49
+ };
50
+ mountApi: (port: number) => Promise<{
51
+ stop: () => void;
52
+ }>;
53
+ startScheduler: (learnerIds: string[]) => Promise<{
54
+ stop: () => void;
55
+ }>;
56
+ registerMemoryCategories: (categories: string[]) => void;
57
+ getActiveLearnerIds: () => Promise<string[]>;
58
+ }
59
+ export declare const VOICE_DEFAULTS: {
60
+ readonly ttsProvider: "fenix-tts";
61
+ readonly ttsdProvider: "fenix-ttsd";
62
+ readonly sttProvider: "groq-whisper";
63
+ /** Speechmatics for classroom scenarios with speaker focus + custom vocab */
64
+ readonly sttProviderClassroom: "speechmatics";
65
+ /** Turn detection: smart_turn for multi-speaker classroom */
66
+ readonly turnDetectionPreset: "smart_turn";
67
+ };
68
+ export declare function initVoicesStandalone(config: VoicesConfig, deps: VoicesInitDeps): Promise<VoicesInstance>;
package/dist/index.js ADDED
@@ -0,0 +1,54 @@
1
+ import{createKernel as oe,bootKernel as le,KERNEL_VERSION as ke,EDITION_VOICES as ce}from"@fenixforce/kernel";var _={name:"fenix-voices",channels:["telegram","http-api","websocket","voice-telnyx","cli"],tools:["memory","web","files","schedule","curriculum","exercises","assessment","content-pipeline"],storage:"postgres",extensions:["default/*","voices/*"],features:{sandbox:!0,subagents:!0,teamMemory:!0,observability:!0,skillMarketplace:!1,teacherLayer:!0,teachingLoop:!0,contentPipeline:!0}};var j=`You are an autonomous language teacher powered by the Fenix Voices platform.
2
+ Your goal is to help learners achieve fluency through structured, adaptive lessons.
3
+ Be patient, encouraging, and culturally sensitive.`,H=`## Available Teacher Tools
4
+ - **plan_lesson** \u2014 Create a lesson plan mixing new curriculum material with spaced-repetition reviews.
5
+ - **placement_test** \u2014 Run an adaptive placement test to determine a learner's CEFR level.
6
+ - **curriculum** \u2014 Browse and manage curriculum templates and units.
7
+ - **exercises** \u2014 Generate exercises (vocabulary drill, fill-in-blank, translation, listening, pronunciation, dialogue roleplay, etc.).
8
+ - **assessment** \u2014 Evaluate exercise responses, generate feedback, and produce session summaries.
9
+ - **content-pipeline** \u2014 Import, tag, and manage corpus content (CSV, JSON, or web scraping via Scrapthing MCP).`,W=`## Learner Interaction Guidelines
10
+ - Use the learner's target language progressively: start with simple phrases and increase as they advance.
11
+ - Match your language complexity to the learner's CEFR level (pre-A1 through C2).
12
+ - For levels pre-A1 to A2: use the bridge/native language for explanations, target language for practice.
13
+ - For levels B1+: conduct most interaction in the target language.
14
+ - Celebrate milestones and streaks to keep motivation high.
15
+ - When the learner asks a general question, respond conversationally \u2014 only call plan_lesson when starting a structured practice session.`,G=`## When to Use Tools
16
+ - **Starting a session**: Call plan_lesson to generate a structured lesson.
17
+ - **First interaction with a new learner**: Call placement_test to assess their level.
18
+ - **Learner submits an answer**: Use assessment to evaluate and provide feedback.
19
+ - **Casual conversation**: Respond naturally without tool calls \u2014 use the target language when appropriate.
20
+ - **Learner asks about their progress**: Retrieve stats from teacher memory.`;function B(){return[j,H,W,G].join(`
21
+
22
+ `)}function L(e,t){let r=B();return t==="standalone"||!e?r:`${e}
23
+
24
+ ---
25
+
26
+ ${r}`}import{sql as m}from"kysely";async function d(e,t){let r=await m`
27
+ SELECT * FROM learner_profiles WHERE id = ${t}
28
+ `.execute(e);return r.rows[0]?w(r.rows[0]):null}async function v(e,t,r){let a=[],n=[];if(r.currentLevel!==void 0&&(a.push("current_level = $"+(n.length+1)),n.push(r.currentLevel)),r.bridgeLanguage!==void 0&&(a.push("bridge_language = $"+(n.length+1)),n.push(r.bridgeLanguage)),r.goals!==void 0&&(a.push("goals = $"+(n.length+1)+"::jsonb"),n.push(JSON.stringify(r.goals))),r.preferences!==void 0&&(a.push("preferences = $"+(n.length+1)+"::jsonb"),n.push(JSON.stringify(r.preferences))),r.stats!==void 0&&(a.push("stats = $"+(n.length+1)+"::jsonb"),n.push(JSON.stringify(r.stats))),a.length===0)return d(e,t);let s=await m`
29
+ UPDATE learner_profiles
30
+ SET ${m.raw(a.join(", "))}, updated_at = now()
31
+ WHERE id = ${t}
32
+ RETURNING *
33
+ `.execute(e);return s.rows[0]?w(s.rows[0]):null}async function S(e,t,r=20){let a=await m`
34
+ SELECT * FROM vocab_entries
35
+ WHERE learner_id = ${t} AND next_review_at <= now()
36
+ ORDER BY next_review_at ASC
37
+ LIMIT ${r}
38
+ `.execute(e),n=await m`
39
+ SELECT * FROM grammar_entries
40
+ WHERE learner_id = ${t} AND next_review_at <= now()
41
+ ORDER BY next_review_at ASC
42
+ LIMIT ${r}
43
+ `.execute(e);return{vocab:a.rows.map(k),grammar:n.rows.map(J)}}function w(e){return{id:e.id,userId:e.user_id,targetLanguage:e.target_language,nativeLanguage:e.native_language,bridgeLanguage:e.bridge_language??void 0,currentLevel:e.current_level,goals:e.goals??[],preferences:e.preferences??{},stats:e.stats??{},createdAt:new Date(e.created_at).toISOString(),updatedAt:new Date(e.updated_at).toISOString()}}function k(e){return{id:e.id,learnerId:e.learner_id,word:e.word,targetLanguage:e.target_language,translation:e.translation,mastery:e.mastery,nextReviewAt:new Date(e.next_review_at).toISOString(),reviewCount:e.review_count,lastReviewedAt:e.last_reviewed_at?new Date(e.last_reviewed_at).toISOString():void 0,context:e.context??void 0}}function J(e){return{id:e.id,learnerId:e.learner_id,concept:e.concept,targetLanguage:e.target_language,mastery:e.mastery,nextReviewAt:new Date(e.next_review_at).toISOString(),reviewCount:e.review_count,lastReviewedAt:e.last_reviewed_at?new Date(e.last_reviewed_at).toISOString():void 0,notes:e.notes??void 0}}import{sql as Z}from"kysely";import{readFileSync as N,readdirSync as Ne,existsSync as R}from"fs";import{join as E}from"path";var X=new Set(["pre-A1","A1","A2","B1","B2","C1","C2"]),Y=/^[a-z]{2,3}$/;function x(e){let t=[];if(!e||typeof e!="object")return t.push({path:"",message:"template must be an object"}),t;let r=e;if((!r.language||typeof r.language!="string"||!Y.test(r.language))&&t.push({path:"language",message:"must be a valid ISO 639 code"}),(!r.version||typeof r.version!="string")&&t.push({path:"version",message:"must be a string"}),!Array.isArray(r.units))return t.push({path:"units",message:"must be an array"}),t;let a=new Set;for(let n=0;n<r.units.length;n++){let s=r.units[n],i=`units[${n}]`;if(!s||typeof s!="object"){t.push({path:i,message:"must be an object"});continue}!s.id||typeof s.id!="string"?t.push({path:`${i}.id`,message:"must be a non-empty string"}):(a.has(s.id)&&t.push({path:`${i}.id`,message:`duplicate unit id: ${s.id}`}),a.add(s.id)),(!s.title||typeof s.title!="string")&&t.push({path:`${i}.title`,message:"must be a non-empty string"}),(!s.level||!X.has(s.level))&&t.push({path:`${i}.level`,message:"must be a valid CEFR level"}),Array.isArray(s.objectives)||t.push({path:`${i}.objectives`,message:"must be an array"}),Array.isArray(s.vocabularyTargets)||t.push({path:`${i}.vocabularyTargets`,message:"must be an array"}),Array.isArray(s.grammarTargets)||t.push({path:`${i}.grammarTargets`,message:"must be an array"}),Array.isArray(s.prerequisiteUnits)||t.push({path:`${i}.prerequisiteUnits`,message:"must be an array"}),(typeof s.estimatedSessions!="number"||s.estimatedSessions<1)&&t.push({path:`${i}.estimatedSessions`,message:"must be a positive number"}),(!s.exerciseTypeWeights||typeof s.exerciseTypeWeights!="object")&&t.push({path:`${i}.exerciseTypeWeights`,message:"must be an object"})}for(let n=0;n<r.units.length;n++){let s=r.units[n];if(Array.isArray(s.prerequisiteUnits))for(let i of s.prerequisiteUnits)a.has(i)||t.push({path:`units[${n}].prerequisiteUnits`,message:`references unknown unit: ${i}`})}return t}var A=E(process.cwd(),"data","curricula");function q(e,t=A){let r=E(t,`${e}.json`);if(!R(r)){let a=E(t,"sample.json");if(R(a))try{let n=N(a,"utf-8"),s=JSON.parse(n);if(s.language===e)return s}catch{}return null}try{let a=N(r,"utf-8"),n=JSON.parse(a);return x(n).length>0?null:n}catch{return null}}async function h(e,t,r=A){let a=await Z`
44
+ SELECT template FROM curriculum_templates WHERE language = ${t}
45
+ `.execute(e);return a.rows[0]?a.rows[0].template:q(t,r)}import{sql as O}from"kysely";async function T(e,t,r){let n=(await O`
46
+ SELECT * FROM learner_progress
47
+ WHERE learner_id = ${t} AND template_language = ${r}
48
+ `.execute(e)).rows[0];return n?z(n):null}async function U(e,t,r,a){return await O`
49
+ INSERT INTO learner_progress
50
+ (learner_id, template_language, current_unit_id, current_session, completed_units, unit_scores)
51
+ VALUES
52
+ (${t}, ${r}, ${a}, 1, '[]'::jsonb, '{}'::jsonb)
53
+ ON CONFLICT (learner_id, template_language) DO NOTHING
54
+ `.execute(e),await T(e,t,r)}function z(e){return{learnerId:e.learner_id,templateLanguage:e.template_language,completedUnits:e.completed_units??[],currentUnitId:e.current_unit_id,currentSession:e.current_session,unitScores:e.unit_scores??{}}}var Q=10,ee=30;function te(e){return{entryId:e.id,entryType:"vocab",word:e.word,lastScore:e.mastery}}function re(e){return{entryId:e.id,entryType:"grammar",concept:e.concept,lastScore:e.mastery}}function ne(e){return{"pre-A1":.1,A1:.2,A2:.35,B1:.5,B2:.65,C1:.8,C2:.95}[e]??.3}function I(e,t,r=6){let a=[],n=ne(e.level),s=e.exerciseTypeWeights,i=Object.values(s).reduce((o,l)=>o+l,0)||1;for(let[o,l]of Object.entries(s)){let g=Math.max(1,Math.round(l/i*r));for(let c=0;c<g&&a.length<r;c++)a.push({type:o,objective:e.objectives[a.length%e.objectives.length]??e.title,targetVocabulary:e.vocabularyTargets.length>0?e.vocabularyTargets.slice(0,3):void 0,targetGrammar:e.grammarTargets.length>0?e.grammarTargets.slice(0,2):void 0,contentQuery:`${e.title} ${e.level}`,difficulty:n})}return a}function b(e,t=8){return e.slice(0,t).map(r=>({type:r.entryType==="vocab"?"vocabulary_drill":"grammar_exercise",objective:`Review: ${r.word??r.concept??"item"}`,targetVocabulary:r.word?[r.word]:void 0,targetGrammar:r.concept?[r.concept]:void 0,difficulty:Math.max(.1,1-r.lastScore)}))}async function P(e,t){let{db:r}=e,a=await d(r,t);if(!a)throw new Error(`Learner not found: ${t}`);let n=await h(r,a.targetLanguage);if(!n&&e.generateTemplate&&(n=await e.generateTemplate(a.targetLanguage)),!n)throw new Error(`No curriculum template for language: ${a.targetLanguage}`);let s=await T(r,t,a.targetLanguage);if(!s){let u=n.units[0];if(!u)throw new Error("Curriculum has no units");s=await U(r,t,a.targetLanguage,u.id)}let i=n.units.find(u=>u.id===s.currentUnitId);if(!i)throw new Error(`Unit not found: ${s.currentUnitId}`);let o=await S(r,t,20),l=[...o.vocab.map(te),...o.grammar.map(re)],g=a.preferences?.sessionDuration??ee,c;if(l.length>=Q)c=b(l);else{let u=b(l,3),p=I(i,s.currentSession,6-u.length);c=[...u,...p]}return{learnerId:t,unitId:i.id,sessionNumber:s.currentSession,objectives:i.objectives,exercises:c,estimatedDuration:g,reviewItems:l}}var f={name:"plan_lesson",description:"Plan a language lesson for the current learner",handler:async(e,t)=>P(t.deps,e.learnerId)};var se=["pre-A1","A1","A2","B1","B2"],ae=.5,C=2;function $(e){let t="pre-A1";for(let r of e)r.passed&&(t=r.level);return t}function D(e){return e.length<C?!0:!e.slice(-C).every(r=>!r.passed)}async function F(e,t,r){let{db:a,generateExercises:n,scoreExercises:s,getResponses:i}=e,o=[];for(let g of se){if(!D(o))break;let c=await n(g,r);if(c.length===0)continue;let u=await i(c),p=await s(c,u),K=p>=ae;o.push({level:g,score:p,passed:K})}let l=$(o);return await v(a,t,{currentLevel:l}),{learnerId:t,testedLevels:o,assignedLevel:l}}var y={name:"placement_test",description:"Assess learner's current level in target language",handler:async(e,t)=>F(t.deps,e.learnerId,e.language)};var ie=[f,y];function M(e,t="teacher_skills"){for(let r of ie)e.tool(t,r.name,r)}var V="voices",Ze={...ce},ue={edition:V,channels:["telegram","web","voice"],tools:["*"],layers:["memory","knowledge-graph","teacher"]};function qe(e){return oe({...ue,...e})}async function ze(e){return le({edition:V,tools:["*"],maxTurns:20,...e})}var Qe={ttsProvider:"fenix-tts",ttsdProvider:"fenix-ttsd",sttProvider:"groq-whisper",sttProviderClassroom:"speechmatics",turnDetectionPreset:"smart_turn"};async function et(e,t){await t.runMigrations(e.databaseUrl);let r=t.createToolRegistry();M(r);let a=e.port??3333,n=await t.mountApi(a);t.registerMemoryCategories(["teacher:profile","teacher:vocab","teacher:grammar","teacher:session"]);let s=await t.getActiveLearnerIds(),i=await t.startScheduler(s),o=L(void 0,"standalone");return{manifest:_,systemPrompt:o,shutdown:async()=>{i.stop(),n.stop()}}}export{V as EDITION,ke as KERNEL_VERSION,ue as VOICES_KERNEL_CONFIG,Ze as VOICES_MANIFEST,Qe as VOICE_DEFAULTS,le as bootKernel,ze as bootVoices,oe as createKernel,et as initVoicesStandalone,qe as startVoices};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Manifest Merger — Combines an add-on manifest into a running base manifest.
3
+ *
4
+ * - Tools: union (no duplicates)
5
+ * - Extensions: union (no duplicates)
6
+ * - Features: merge (addon overrides on conflict)
7
+ * - Channels: base channels unchanged
8
+ */
9
+ import type { EditionManifest } from "./manifest.js";
10
+ /**
11
+ * Merge an add-on manifest into a base manifest, returning a new combined manifest.
12
+ */
13
+ export declare function mergeManifest(base: EditionManifest, addon: Partial<EditionManifest>): EditionManifest;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Voices Edition Manifests — Standalone and add-on configurations.
3
+ */
4
+ export interface EditionManifest {
5
+ name: string;
6
+ channels: string[];
7
+ tools: string[];
8
+ storage: string;
9
+ extensions: string[];
10
+ features: Record<string, boolean>;
11
+ }
12
+ export declare const standaloneManifest: EditionManifest;
13
+ export declare const addonManifest: Partial<EditionManifest>;
@@ -0,0 +1,91 @@
1
+ export interface LearnerDiaryEntry {
2
+ sessionId: string;
3
+ learnerId: string;
4
+ languageCode: string;
5
+ date: Date;
6
+ exercisesAttempted: number;
7
+ exercisesCompleted: number;
8
+ averageScore: number;
9
+ /** Error patterns seen 3+ times in the session */
10
+ persistentErrors: string[];
11
+ /** Exercises where score jumped 30%+ from recent average */
12
+ breakthroughMoments: string[];
13
+ engagementSignals: {
14
+ averageLatencyMs: number;
15
+ earlyExits: number;
16
+ /** Learner requested more exercises */
17
+ voluntaryExtensions: number;
18
+ };
19
+ }
20
+ export interface LearnerInsights {
21
+ learnerId: string;
22
+ languageCode: string;
23
+ synthesizedAt: Date;
24
+ /** Recurring error patterns across sessions */
25
+ persistentErrorPatterns: string[];
26
+ /** Skills where breakthroughs occurred */
27
+ breakthroughAreas: string[];
28
+ /** Engagement trend: improving, stable, declining */
29
+ engagementTrend: "improving" | "stable" | "declining";
30
+ /** Summary paragraph from LLM synthesis */
31
+ narrativeSummary: string;
32
+ /** Skill areas needing stride increase (from breakthroughs) */
33
+ strideBoostAreas: string[];
34
+ }
35
+ export interface DiaryConfig {
36
+ /** Number of entries to load for synthesis */
37
+ synthesisWindow: number;
38
+ /** Trigger synthesis after this many sessions */
39
+ synthesisThreshold: number;
40
+ /** Score jump percentage to count as breakthrough */
41
+ breakthroughJumpPercent: number;
42
+ /** Min occurrences of an error to count as persistent */
43
+ persistentErrorMin: number;
44
+ }
45
+ export declare const DEFAULT_DIARY_CONFIG: DiaryConfig;
46
+ export type DiaryStoreFn = (entry: LearnerDiaryEntry) => Promise<void>;
47
+ export type DiaryLoadFn = (learnerId: string, limit: number) => Promise<LearnerDiaryEntry[]>;
48
+ export type InsightsStoreFn = (insights: LearnerInsights) => Promise<void>;
49
+ export type SynthesisLLMFn = (prompt: string) => Promise<string>;
50
+ export declare class LearnerDiary {
51
+ private config;
52
+ private store;
53
+ private load;
54
+ private storeInsights;
55
+ private llmSynthesize;
56
+ constructor(deps: {
57
+ store: DiaryStoreFn;
58
+ load: DiaryLoadFn;
59
+ storeInsights: InsightsStoreFn;
60
+ llmSynthesize: SynthesisLLMFn;
61
+ }, config?: Partial<DiaryConfig>);
62
+ /**
63
+ * Record a diary entry for a completed session.
64
+ */
65
+ recordSession(entry: LearnerDiaryEntry): Promise<void>;
66
+ /**
67
+ * Build a diary entry from raw session data.
68
+ */
69
+ buildEntry(sessionId: string, learnerId: string, languageCode: string, exercises: ExerciseRecord[], engagement: {
70
+ earlyExits: number;
71
+ voluntaryExtensions: number;
72
+ }): LearnerDiaryEntry;
73
+ /**
74
+ * Synthesize insights from recent diary entries.
75
+ * Run weekly or when session count crosses threshold.
76
+ */
77
+ synthesize(learnerId: string, languageCode: string): Promise<LearnerInsights>;
78
+ /**
79
+ * Format insights for injection into the teaching loop.
80
+ */
81
+ formatForContext(insights: LearnerInsights): string;
82
+ }
83
+ export interface ExerciseRecord {
84
+ exerciseId: string;
85
+ skillArea: string;
86
+ score: number;
87
+ completed: boolean;
88
+ errors: string[];
89
+ latencyMs: number;
90
+ previousAvgScore?: number;
91
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * System Prompt Patch — Voices-specific system prompt additions.
3
+ *
4
+ * In standalone mode the patch IS the system prompt.
5
+ * In add-on mode the patch is appended to the existing Pro system prompt.
6
+ */
7
+ /**
8
+ * Build the complete Voices system prompt patch.
9
+ */
10
+ export declare function buildVoicesPromptPatch(): string;
11
+ /**
12
+ * Apply the Voices patch to a system prompt.
13
+ *
14
+ * - Standalone: returns the patch as the full prompt.
15
+ * - Add-on: appends the patch to the existing Pro prompt.
16
+ */
17
+ export declare function applyPromptPatch(existingPrompt: string | undefined, mode: "standalone" | "addon"): string;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Exercise Evaluator — Scores exercise responses using deterministic
3
+ * matching where possible and LLM scoring for open-ended types.
4
+ */
5
+ import type { LearnerProfile, CorrectionStyle } from "../learner-model.js";
6
+ import type { Exercise, ExerciseType } from "../exercises/generator.js";
7
+ export interface ExerciseResult {
8
+ exerciseId: string;
9
+ exerciseType: ExerciseType;
10
+ response: string;
11
+ score: number;
12
+ errors: ErrorClassification[];
13
+ feedback: string;
14
+ timeSpent: number;
15
+ hintsUsed: number;
16
+ }
17
+ export interface ErrorClassification {
18
+ type: "vocabulary" | "grammar" | "pronunciation" | "comprehension" | "spelling";
19
+ concept: string;
20
+ explanation: string;
21
+ severity: "minor" | "major";
22
+ }
23
+ export type LLMScoreFn = (exercise: Exercise, response: string, language: string) => Promise<{
24
+ score: number;
25
+ errors: ErrorClassification[];
26
+ }>;
27
+ export type LLMFeedbackFn = (exercise: Exercise, response: string, score: number, errors: ErrorClassification[], style: CorrectionStyle, language: string) => Promise<string>;
28
+ export declare function scoreDeterministic(exercise: Exercise, response: string): number | null;
29
+ export declare function wordOverlapScore(expected: string, actual: string): number;
30
+ export declare function classifyBasicErrors(exercise: Exercise, response: string): ErrorClassification[];
31
+ export interface EvaluateDeps {
32
+ llmScore?: LLMScoreFn;
33
+ llmFeedback?: LLMFeedbackFn;
34
+ }
35
+ export declare function evaluate(deps: EvaluateDeps, exercise: Exercise, response: string, learner: LearnerProfile, timeSpent?: number, hintsUsed?: number): Promise<ExerciseResult>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Feedback Generator — Generates teaching feedback matching the
3
+ * learner's correctionStyle preference.
4
+ *
5
+ * Three styles: gentle, direct, socratic.
6
+ */
7
+ import type { CorrectionStyle } from "../learner-model.js";
8
+ import type { Exercise } from "../exercises/generator.js";
9
+ import type { ErrorClassification } from "./evaluator.js";
10
+ export declare function buildFeedbackPrompt(exercise: Exercise, response: string, score: number, errors: ErrorClassification[], style: CorrectionStyle, language: string): string;
11
+ export declare function generateLocalFeedback(score: number, errors: ErrorClassification[], style: CorrectionStyle, expectedAnswer?: string): string;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Session Summary — End-of-session aggregation that updates the
3
+ * learner model, spaced repetition, and profile stats.
4
+ */
5
+ import type { Kysely } from "kysely";
6
+ import type { ExerciseResult } from "./evaluator.js";
7
+ export interface SessionSummary {
8
+ sessionId: string;
9
+ learnerId: string;
10
+ duration: number;
11
+ exercisesCompleted: number;
12
+ averageScore: number;
13
+ newVocabulary: string[];
14
+ masteredItems: string[];
15
+ strugglingAreas: string[];
16
+ nextSessionFocus: string;
17
+ streakUpdate: {
18
+ current: number;
19
+ longest: number;
20
+ };
21
+ }
22
+ export declare function calculateStreak(lastSessionAt: string | undefined, currentStreak: number, longestStreak: number, now?: Date): {
23
+ current: number;
24
+ longest: number;
25
+ };
26
+ export declare function extractVocabulary(results: ExerciseResult[]): string[];
27
+ export declare function findMasteredItems(results: ExerciseResult[]): string[];
28
+ export declare function findStrugglingAreas(results: ExerciseResult[]): string[];
29
+ export declare function determineNextFocus(strugglingAreas: string[], results: ExerciseResult[]): string;
30
+ export interface SummarizeDeps {
31
+ db: Kysely<any>;
32
+ }
33
+ export declare function summarize(deps: SummarizeDeps, sessionId: string, learnerId: string, results: ExerciseResult[], startedAt: string, endedAt: string): Promise<SessionSummary>;