feat: add Russian localization and independent STT languages
CI / Typecheck, tests, lint (push) Canceled after 0s
CI / Standalone (mac-arm64) (push) Canceled after 0s
CI / Standalone (linux-x64) (push) Canceled after 0s
CI / Standalone (windows-x64) (push) Canceled after 0s

This commit is contained in:
2026-10-25 17:18:44 +03:00
parent b277ee3e0f
commit a50ca4b867
93 changed files with 3261 additions and 904 deletions
+3 -1
View File
@@ -1,3 +1,4 @@
import { learningLanguageInstruction, type Language } from "../../core/i18n.ts";
import { Type } from "typebox";
import type { AgentTool } from "@earendil-works/pi-agent-core";
import type { Usage } from "@earendil-works/pi-ai";
@@ -31,6 +32,7 @@ export interface DemoBuilderInput {
}
export interface DemoBuilderOptions {
language?: Language;
/** Test seam: scripted engine; production uses the demoBuilder role. */
engine?: PiEngine;
headless?: HeadlessApi;
@@ -207,7 +209,7 @@ export async function runDemoBuilder(input: DemoBuilderInput, opts: DemoBuilderO
const result = await runSubagent(Object.values(createDemoBuilderTools(state, headless, opts.onActivity)), {
role: "demoBuilder",
engine: opts.engine,
systemPrompt: INSTRUCTIONS,
systemPrompt: `${INSTRUCTIONS}\n\n${learningLanguageInstruction(opts.language ?? "en")}`,
prompt,
maxTurns: opts.maxSteps ?? 24,
abortSignal: opts.abortSignal,
+11 -4
View File
@@ -1,3 +1,4 @@
import { learningLanguageInstruction, type Language } from "../../core/i18n.ts";
import * as crypto from "node:crypto";
import type { Usage } from "@earendil-works/pi-ai";
import type { AgentTool } from "@earendil-works/pi-agent-core";
@@ -471,6 +472,7 @@ function runExtractor(
batchIds: ReadonlySet<string>,
signal: AbortSignal | undefined,
onStep: (usage: Usage, model: string, step: number, finishReason: string, tools: string) => void,
language: Language = "en",
): Promise<SessionFindings> {
// Chains in (the wire), chains out (the store) — no translation seam
// since chains-primary. Validation runs at the submit seam (error-back)
@@ -486,7 +488,7 @@ function runExtractor(
parameters: sessionFindingsSchema,
fill: fillFindings,
validate: (value) => validateSessionAnchors(value, batchIds) ?? validateFindingsChains(value, existingIds),
systemPrompt: EXTRACTOR_INSTRUCTIONS,
systemPrompt: `${EXTRACTOR_INSTRUCTIONS}\n\n${learningLanguageInstruction(language)}`,
prompt,
engine,
signal,
@@ -507,6 +509,7 @@ function runNotesExtractor(
existingIds: ReadonlySet<string>,
signal: AbortSignal | undefined,
onStep: (usage: Usage, model: string, step: number, finishReason: string, tools: string) => void,
language: Language = "en",
): Promise<NotesFindings> {
return runSubmitAgent<FindingsSubmit>({
role: "extractor",
@@ -517,7 +520,7 @@ function runNotesExtractor(
parameters: findingsSchema,
fill: fillNotesFindings,
validate: (value) => validateFindingsChains(value, existingIds),
systemPrompt: NOTES_EXTRACTOR_INSTRUCTIONS,
systemPrompt: `${NOTES_EXTRACTOR_INSTRUCTIONS}\n\n${learningLanguageInstruction(language)}`,
prompt,
engine,
signal,
@@ -725,6 +728,7 @@ export interface ExtractionModels {
}
export interface ExtractionPipelineDeps extends GraphDeps, ExtractionModels {
languageForSource?: (sourceId: string) => Language;
/** The chunk budget — the transcript-token share of the extractor's
* context; batches fill to it (the trigger fires on the same number,
* read fresh per turn by the composition root). OPTIONAL with the
@@ -735,6 +739,7 @@ export interface ExtractionPipelineDeps extends GraphDeps, ExtractionModels {
}
export interface NotesImportInput {
language?: Language;
/** The source's file path — its identity (deterministic src id) AND its
* provenance label in the detail card. */
filePath: string;
@@ -940,6 +945,7 @@ export function createExtractionPipeline(deps: ExtractionPipelineDeps): Extracti
recordStep(jobKey, "extractor", callUsage(usage), model);
console.info(`[extraction] extractor step ${step}: ${finishReason}, tools: ${tools}`);
},
deps.languageForSource?.(sourceId) ?? chatStore.getChat(sourceId)?.language ?? "en",
);
console.info(`[extraction ${jobKey}] extractor done in ${Math.round(performance.now() - t0)}ms`);
return findings;
@@ -1262,7 +1268,7 @@ export function createExtractionPipeline(deps: ExtractionPipelineDeps): Extracti
async function notesExtract(
jobKey: string,
input: { id: string; filePath: string; text: string; part?: { i: number; n: number } },
input: { id: string; filePath: string; text: string; language?: Language; part?: { i: number; n: number } },
): Promise<NotesFindings> {
// jobKey (per-chunk jobs, owner 2026-09-28): the registry key —
// "src#i" for chunk jobs — so the abort signal reaches the call.
@@ -1290,6 +1296,7 @@ export function createExtractionPipeline(deps: ExtractionPipelineDeps): Extracti
recordStep(jobKey, "extractor", callUsage(usage), model);
console.info(`[extraction] extractor step ${step}: ${finishReason}, tools: ${tools}`);
},
input.language ?? deps.languageForSource?.(input.id) ?? "en",
);
console.info(`[extraction ${jobKey}] notes extractor done in ${Math.round(performance.now() - t0)}ms`);
return findings;
@@ -1298,7 +1305,7 @@ export function createExtractionPipeline(deps: ExtractionPipelineDeps): Extracti
// The notes auditor is the SAME identity-linker as the chat one (one
// schema now that attribution is gone) — audit() above, verbatim.
async function runNotesExtraction(input: { id: string; filePath: string; text: string; importedAt: string }): Promise<EmissionStats> {
async function runNotesExtraction(input: { id: string; filePath: string; text: string; importedAt: string; language?: Language }): Promise<EmissionStats> {
const { id } = input;
// importNotes owns the source exclusion, including metadata and cleanup.
const t0 = performance.now();
+3 -1
View File
@@ -1,3 +1,4 @@
import { learningLanguageInstruction, type Language } from "../../core/i18n.ts";
import { randomUUID } from "node:crypto";
import type { AssistantMessage, Message, ToolResultMessage, Usage, Tool } from "@earendil-works/pi-ai";
import { normalizeContext } from "@earendil-works/pi-ai";
@@ -62,6 +63,7 @@ const RENDER_BATCH_CAP = 6;
const EXTRACT_CAP = 24_000;
export interface PdfReaderOptions {
language?: Language;
engine?: PiEngine;
abortSignal?: AbortSignal;
onUsage?: (usage: Usage) => void;
@@ -206,7 +208,7 @@ export async function runPdfReaderAgent(
for (let iteration = 0; iteration < maxIterations; iteration++) {
const result = await streamOnce(
engine,
INSTRUCTIONS,
`${INSTRUCTIONS}\n\n${learningLanguageInstruction(opts.language ?? "en")}`,
messages,
toolDeclarations,
sessionId,
+3 -1
View File
@@ -1,3 +1,4 @@
import { learningLanguageInstruction, type Language } from "../../core/i18n.ts";
import type { AgentTool } from "@earendil-works/pi-agent-core";
import type { Usage } from "@earendil-works/pi-ai";
import { createWebTools } from "../tools/web.ts";
@@ -68,6 +69,7 @@ Numbered findings with inline source citations:
What couldn't be answered. Suggested next steps.`;
export interface ResearcherRunOptions {
language?: Language;
engine?: PiEngine;
/** The search seam (search port wave, 2026-09-20): resolves the LIVE
* provider per call, null = honestly unavailable (the web_search tool
@@ -98,7 +100,7 @@ export async function runResearcherAgent(prompt: string, opts: ResearcherRunOpti
role: "researcher",
prompt,
engine: opts.engine,
systemPrompt: INSTRUCTIONS,
systemPrompt: `${INSTRUCTIONS}\n\n${learningLanguageInstruction(opts.language ?? "en")}`,
maxTurns: 12,
abortSignal: opts.abortSignal,
onUsage: opts.onUsage,
+6
View File
@@ -1,3 +1,4 @@
import type { Language } from "../../core/i18n.ts";
import type { PiEngine } from "../models.ts";
import type { SearchProvider } from "../../core/search.ts";
import {
@@ -64,6 +65,7 @@ export const pdfModelOverride: { current?: PiEngine } = {};
type JobToolInput = ResearchToolInput | DemoToolInput | VisualToolInput | PdfToolInput;
export interface JobRunnerDeps {
languageForChat?: (chatId: string) => Language;
chatStore: ChatStore;
jobStore: JobStore;
/**
@@ -364,6 +366,7 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
jobStore.updateJob(jobId, { activity: "researching" });
const brief = await runResearcherAgent(prompt, {
engine: model,
language: deps.languageForChat?.(job.chatId) ?? chatStore.getChat(job.chatId)?.language ?? "en",
search: deps.searchProvider,
pdfDir,
abortSignal: loop.abortSignal,
@@ -417,6 +420,7 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
{ title, brief },
{
engine: model,
language: deps.languageForChat?.(job.chatId) ?? chatStore.getChat(job.chatId)?.language ?? "en",
abortSignal: loop.abortSignal,
chatId: loop.chatId,
onUsage: (usage) => loop.onUsage(piUsageToStep(usage)),
@@ -453,6 +457,7 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
{ brief },
{
engine: model,
language: deps.languageForChat?.(job.chatId) ?? chatStore.getChat(job.chatId)?.language ?? "en",
abortSignal: loop.abortSignal,
chatId: loop.chatId,
onUsage: (usage) => loop.onUsage(piUsageToStep(usage)),
@@ -492,6 +497,7 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
{ path, question },
{
engine: model,
language: deps.languageForChat?.(job.chatId) ?? chatStore.getChat(job.chatId)?.language ?? "en",
abortSignal: loop.abortSignal,
chatId: loop.chatId,
onUsage: (usage) => loop.onUsage(piUsageToStep(usage)),
+3 -1
View File
@@ -1,3 +1,4 @@
import { learningLanguageInstruction, type Language } from "../../core/i18n.ts";
import { randomUUID } from "node:crypto";
import type { AssistantMessage, Message, Tool, ToolResultMessage, Usage } from "@earendil-works/pi-ai";
import { normalizeContext } from "@earendil-works/pi-ai";
@@ -35,6 +36,7 @@ export interface VisualMakerInput {
}
export interface VisualMakerOptions {
language?: Language;
/** Test seam: scripted engine; production uses the visualMaker role. */
engine?: PiEngine;
headless?: HeadlessApi;
@@ -163,7 +165,7 @@ export async function runVisualMaker(input: VisualMakerInput, opts: VisualMakerO
// loop owns tool execution and the image-in-the-loop injection).
const result = await streamOnce(
engine,
INSTRUCTIONS,
`${INSTRUCTIONS}\n\n${learningLanguageInstruction(opts.language ?? "en")}`,
messages,
toolDeclarations,
sessionId,