Skip to content
Merged
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
74 changes: 71 additions & 3 deletions src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,17 @@ import {
import { isFileNotFoundError } from "../fs-errors.js";
import { SECRET_KEY_PATTERN_SOURCE } from "../diagnostics.js";
import { openWikiLocalWikiDir, openWikiSkillsDir } from "../openwiki-home.js";
import { resolveLanguage } from "../language.js";
import {
resolveConceptTypeLabel,
resolveIndexLabels,
} from "../okf/index-labels.js";
import { OpenWikiLocalShellBackend } from "./docs-only-backend.js";
import { createOpenWikiIndexMiddleware } from "./okf-middleware.js";
import {
createWikiTranslationMiddleware,
resolveTranslationPlan,
} from "./translation-middleware.js";
import {
CODEX_ORIGINATOR,
CODEX_RESPONSES_BASE_URL,
Expand Down Expand Up @@ -244,7 +253,12 @@ async function runOpenWikiAgentCore(
providerRetryAttempts: number,
): Promise<OpenWikiRunResult> {
const outputMode = options.outputMode ?? "local-wiki";
const context = await createRunContext(command, cwd, outputMode);
const context = await createRunContext(
command,
cwd,
outputMode,
options.language,
);
emitDebug(options, "context=created");
const openWikiSnapshotBefore =
command === "chat"
Expand Down Expand Up @@ -278,6 +292,21 @@ async function runOpenWikiAgentCore(
virtualMode: true,
}),
});
// An update inherits the wiki's persisted language unless --language requests a
// different one. The plan drives a beforeAgent pass that, on a switch,
// retranslates every page so the incremental update does not leave a mix of the
// old and new language, and on any update retries pages a prior run left
// pending. It is undefined for init and chat, which never translate.
const translation = resolveTranslationPlan(
command,
resolveLanguage(options.language).language,
context.lastUpdate?.language,
);
// Localized headings for the deterministic directory indexes, plus the
// localized fallback `type` stamped on pages the code has to repair. Both fall
// back to English for any language not in the static maps.
const indexLabels = resolveIndexLabels(context.language);
const conceptType = resolveConceptTypeLabel(context.language);
const agent = createDeepAgent({
model,
tools: createOpenWikiConnectorTools(),
Expand All @@ -286,12 +315,48 @@ async function runOpenWikiAgentCore(
middleware:
command === "chat"
? []
: [createOpenWikiIndexMiddleware(wikiBackend, outputMode)],
: [
...(translation
? [
createWikiTranslationMiddleware(
wikiBackend,
outputMode,
model,
translation,
(message) => {
options.onEvent?.({ type: "text", text: message });
// Also emit to stderr so the warning survives the TUI
// re-render and --print's discard of streamed text.
process.stderr.write(`${message}\n`);
},
// The pass announces itself with one line in place of the
// suppressed per-token translation output. It is routine
// progress, so unlike a warning it is not mirrored to stderr.
// The trailing blank line keeps it a distinct Markdown block:
// the TUI coalesces consecutive text events into one
// block-lexed log item, so without it the status would run
// straight into the agent's first streamed line.
(message) => {
options.onEvent?.({
type: "text",
text: `${message}\n\n`,
});
},
),
]
: []),
createOpenWikiIndexMiddleware(
wikiBackend,
outputMode,
indexLabels,
conceptType,
),
],
skills: ["/skills/"],
permissions: [
{ operations: ["write"], paths: ["/skills/**"], mode: "deny" },
],
systemPrompt: createSystemPrompt(command, outputMode),
systemPrompt: createSystemPrompt(command, outputMode, context.language),
});
emitDebug(options, "agent=created");

Expand Down Expand Up @@ -354,6 +419,7 @@ async function runOpenWikiAgentCore(
outputMode,
openWikiSnapshotBefore,
"interrupted",
context.language,
);
emitDebug(
options,
Expand All @@ -378,6 +444,8 @@ async function runOpenWikiAgentCore(
modelId,
outputMode,
openWikiSnapshotBefore,
"complete",
context.language,
);

if (metadataWritten) {
Expand Down
11 changes: 9 additions & 2 deletions src/agent/okf-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import {
type FrontmatterIssue,
} from "../okf/frontmatter.js";
import { migrateWikiToOkf, synchronizeWikiIndexes } from "../okf/index-sync.js";
import {
ENGLISH_CONCEPT_TYPE,
ENGLISH_INDEX_LABELS,
type IndexLabels,
} from "../okf/index-labels.js";
import { MUTATION_PATH_METADATA_KEY } from "./docs-only-backend.js";
import type { OpenWikiOutputMode } from "./types.js";

Expand All @@ -22,11 +27,13 @@ const WRITE_TOOLS = new Set(["write_file", "edit_file"]);
export function createOpenWikiIndexMiddleware(
backend: BackendProtocolV2,
outputMode: OpenWikiOutputMode,
labels: IndexLabels = ENGLISH_INDEX_LABELS,
conceptType: string = ENGLISH_CONCEPT_TYPE,
) {
return createMiddleware({
name: "OpenWikiIndexMiddleware",
beforeAgent: async () => {
await migrateWikiToOkf(backend, outputMode);
await migrateWikiToOkf(backend, outputMode, conceptType);
},
wrapToolCall: async (request, handler) =>
addFrontmatterWarning(
Expand All @@ -37,7 +44,7 @@ export function createOpenWikiIndexMiddleware(
),
afterAgent: async () => {
await validateWikiMermaid(backend, outputMode);
await synchronizeWikiIndexes(backend, outputMode);
await synchronizeWikiIndexes(backend, outputMode, labels, conceptType);
},
});
}
Expand Down
20 changes: 19 additions & 1 deletion src/agent/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ function formatLastUpdate(lastUpdate: UpdateMetadata | null): string {
export function createSystemPrompt(
command: OpenWikiCommand,
outputMode: OpenWikiOutputMode = "local-wiki",
language?: string,
): string {
const output = getOutputPromptConfig(outputMode);
const languageInstructions = createLanguageInstructions(language);

return `
You are OpenWiki, an expert technical writer, software architect, and product analyst.

Your job is to inspect the relevant source evidence and local OpenWiki knowledge sources, then produce documentation in ${output.docsLocation} that is excellent for both humans and future agents. OpenWiki can maintain a local general-purpose knowledge wiki from connector raw dumps under ~/.openwiki.
Your job is to inspect the relevant source evidence and local OpenWiki knowledge sources, then produce documentation in ${output.docsLocation} that is excellent for both humans and future agents. OpenWiki can maintain a local general-purpose knowledge wiki from connector raw dumps under ~/.openwiki.${languageInstructions}

${output.canonicalLocationInstruction}

Expand Down Expand Up @@ -167,6 +169,7 @@ timestamp: <Optional ISO 8601 datetime>
- The description field is especially useful for retrieval tools. When present, make it clear, detailed, and optimized for search.
- When updating an existing Markdown concept, preserve accurate body content and correct its opening front matter only when needed for compliance or accuracy.
- OpenWiki repairs front matter deterministically after every run, so a page is never rejected for missing or invalid front matter. If a page's front matter contains \`openwiki_generated: true\`, that metadata was code-derived as a fallback: replace it with an accurate \`type\`, \`title\`, and \`description\` grounded in the page body, then remove the \`openwiki_generated\` field.
- If a page's front matter contains an \`openwiki_translation_pending\` field, ignore it: it is a translation-system marker that OpenWiki manages automatically. Do not add, edit, remove, or act on it.

Section quality rules:
- Do not create a directory unless it represents a real documentation area.
Expand Down Expand Up @@ -200,6 +203,21 @@ ${createModeInstructions(command, outputMode)}
`.trim();
}

function createLanguageInstructions(language: string | undefined): string {
if (!language) {
return "";
}

return `

Output language:
- Write generated wiki prose, headings, table content, and documentation in ${language}.
- OpenWiki has already brought existing pages into ${language} in a separate deterministic pass before you run, so treat the wiki as already in ${language}. Do not translate or rewrite an existing page just because it, or the recorded run metadata, still shows a different language; that whole-wiki reconciliation is code-owned. Write only your own new or changed content in ${language} and leave otherwise-accurate pages alone.
- In each page's YAML front matter, write the human-readable "title", "description", and "type" values in ${language}. Do this even when the value is dense with product names, feature names, or technical terminology; within those values keep unchanged only literal code identifiers, file paths, commands, and URLs. Write the "tags" values in English so they stay stable across languages as cross-cutting aggregation keys. Keep the YAML keys as written, and copy any URL, file path, timestamp, or identifier-like value byte-for-byte.
- Apply this language only to generated wiki files. Do not translate OpenWiki CLI text or runtime messages.
- Keep code identifiers, file paths, commands, API names, URLs, and code blocks unchanged where translation would reduce technical accuracy or usability.`;
}

export function createDiagramInstructions(): string {
return `
Diagram discipline:
Expand Down
Loading