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
26 changes: 25 additions & 1 deletion docs/dev/wiki.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,17 @@ Docs:
profile) as the
final user turn, runs `runHeadlessToolLoop` against
`wikiToolbox`. Side effects (the `wiki_*` tool calls) ARE
the output; final text is discarded.
the persistent output; the model's final text is a one-or-
two-sentence operator-facing summary of its choices ("Updated
Nak article with March 2026 logo details" / "No edits -
generic Q&A with no user-centric subject") that the cycle
driver inlines as `reasoning="..."` on the finished-thread
log line, matching the journal worker's shape. The prompt's
"Final reply" block instructs the model to surface both
decisions made and decisions skipped (e.g. why a topic that
came up was deliberately NOT given its own article), so a
human skimming the log drawer can see WHY a cycle was a no-op
without having to re-read the conversation.
- `WikiAgent.updateOne({ articleId, currentTitle,
currentContent, userInstructions, signal })` - the
main-thread per-article manual entry. Single Venice
Expand Down Expand Up @@ -437,6 +447,20 @@ JSON).
pointer. Without this, every cycle would re-process the
same "the model decided this conversation has nothing
worth wiki-ing" conversation forever.
- **Final-text is load-bearing now.** Both the per-conversation
wiki agent and the librarian historically ended with "reply
with a single word; the word is discarded" so the tool loop
would terminate cleanly. That changed: the final reply is now
the operator-facing reasoning surfaced as `reasoning="..."`
on the cycle's `finished thread` / `librarian finished` log
line. The prompts ask for one or two plain-text sentences
naming what the agent did or skipped and why; the loop
normalises whitespace and inlines that string. Do not revert
the prompt to "single word" without also dropping the
reasoning surface on the loop side - users debug "why did the
agent decide X" by reading those summaries in the log drawer,
and the librarian's "two articles I considered merging but
left alone" case is only visible there.
- **`wiki_create` rephrases unique-violations.** The
autonomous agent reads tool-error text as guidance; the
raw Postgres `duplicate key value violates unique
Expand Down
5 changes: 4 additions & 1 deletion src/lib/agents/wiki-librarian/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
* for each, and runs `runHeadlessToolLoop` against the
* `wikiLibrarianToolbox` (wiki_search + wiki_update + wiki_delete +
* conversation_search). Side effects from those tool calls ARE the
* output; the model's final text is discarded.
* persistent output; the model's final text is its one-or-two-
* sentence operator summary of merges, deletions, and considered-
* but-left-alone cases (see WikiLibrarianOutput.finalText), surfaced
* in the log drawer by the cycle driver.
*
* No per-thread claim, no entry_date, no terminal-message slicing -
* the librarian operates on the wiki as a whole, on a separate
Expand Down
11 changes: 10 additions & 1 deletion src/lib/agents/wiki-librarian/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,18 @@ export async function runOneCycle(ctx: CycleContext): Promise<CycleResult> {
return 'error';
}

// Reasoning is the agent's brief operator-facing summary of what
// it merged / deleted / left alone (see the "Final reply" block in
// ../wiki-librarian/prompt.ts). Normalise whitespace so a multi-
// line reply still fits on one log line, and fall back to a
// sentinel when the model returned empty (shouldn't happen in
// production but better surfaced as "(none)" than a dangling
// `reasoning=""`).
const reasoning =
runResult.output.finalText.replace(/\s+/g, ' ').trim() || '(none)';
log.info(
`librarian finished (${runResult.toolCalls} tool calls over ` +
`${runResult.output.articleCount} articles)`
`${runResult.output.articleCount} articles, reasoning="${reasoning}")`
);
return 'reviewed';
}
Expand Down
24 changes: 21 additions & 3 deletions src/lib/agents/wiki-librarian/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,26 @@ export function buildWikiLibrarianPrompt(opts: {
" directly (a first name, the project name) rather than \"the",
' user".',
'',
'When you have nothing more to do, reply with a single word. The',
'word is discarded - only the tool calls matter. Zero edits is a',
'normal outcome on a small or already-coherent wiki.',
'**Final reply: one or two sentences explaining your choices.**',
'After your last tool call (or instead of any tool call, if you',
'decided the wiki was already coherent), reply with a brief',
'operator-facing summary of what you did and WHY. This text',
'surfaces in the user\'s log drawer as the cycle\'s outcome, so make',
'it useful to a human skimming the log - name the articles you',
'merged or deleted, and name the cases you considered but left',
'alone. The "considered but left alone" half is as valuable as the',
'"changed it" half: if two articles looked like duplicates but you',
'decided they cover different subjects, say so. Examples:',
' "Deleted \'Kermit protocol\' as out-of-scope; merged the two',
' \'Maya\' articles into one (the household one absorbed the',
' sister article)."',
' "Left \'Maya\' and \'household\' separate - they overlap on the',
' household-finances paragraph but cover different subjects, and',
' merging would make either article harder to find."',
' "No edits - wiki is small and coherent."',
'Skip filler ("Great work!", "I have finished"); lead with the',
'decisions. Keep it under two sentences. Plain text, no Markdown.',
'Zero edits is a normal outcome on a small or already-coherent',
'wiki - say so plainly.',
].join('\n');
}
11 changes: 10 additions & 1 deletion src/lib/agents/wiki-librarian/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,16 @@ export interface WikiLibrarianInput {
}

export interface WikiLibrarianOutput {
/** Final post-tool-loop text. Discarded by production callers. */
/**
* The model's final (post-tool-loop) text. Used as the operator-
* facing reasoning surfaced in the log drawer - the prompt's "Final
* reply" block instructs the librarian to emit a one-or-two-sentence
* summary of what it merged, deleted, or considered-but-left-alone
* (e.g. "Merged the two Maya articles; left 'Maya' and 'household'
* separate because they cover different subjects."). The loop trims
* and inlines this as `reasoning="..."` on the librarian-finished
* log line, matching the shape the journal and wiki workers use.
*/
finalText: string;
/** Number of articles in the snapshot. Surface for observability. */
articleCount: number;
Expand Down
6 changes: 4 additions & 2 deletions src/lib/agents/wiki/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
* settled thread, appends `WIKI_AUTONOMOUS_PROMPT` as the final
* user turn, and runs the headless tool loop with `wikiToolbox`.
* The loop's side effects (wiki_search / wiki_create /
* wiki_update / wiki_delete calls) ARE the output; final text is
* discarded after being captured for logs.
* wiki_update / wiki_delete calls) ARE the persistent output;
* the final text is the model's one-or-two-sentence operator
* summary of its choices (see WikiOutput.finalText), surfaced
* in the log drawer by the cycle driver.
*
* - **Manual**: `updateOne()` runs synchronously on the main thread
* when the user clicks "Ask agent to update" on a single article.
Expand Down
12 changes: 11 additions & 1 deletion src/lib/agents/wiki/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,19 @@ export async function runOneCycle(ctx: CycleContext): Promise<CycleResult> {
return 'error';
}
if (marked) {
// Reasoning is the agent's brief operator-facing summary of what
// it did and why (see WIKI_AUTONOMOUS_BODY_LINES' "Final reply"
// block in ../wiki/prompt.ts). Normalise whitespace so a stray
// newline does not break the single-line log convention, and
// fall back to a sentinel when the model returned an empty
// string (shouldn't happen in production, but a missing summary
// is still better surfaced as "(none)" than a dangling `reasoning=""`).
const reasoning =
runResult.output.finalText.replace(/\s+/g, ' ').trim() || '(none)';
log.info(
`finished thread ${claim.threadId} ` +
`(${runResult.toolCalls} tool calls over ${runResult.output.inputMessageCount} messages) ${titleTag}`
`(${runResult.toolCalls} tool calls over ${runResult.output.inputMessageCount} messages, ` +
`reasoning="${reasoning}") ${titleTag}`
);
return 'processed';
}
Expand Down
18 changes: 16 additions & 2 deletions src/lib/agents/wiki/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,8 +483,22 @@ const WIKI_AUTONOMOUS_BODY_LINES = [
'the subject". The bar for creating is "this is a coherent subject',
'the user will want to look up by name later", not "this came up".',
'',
'When you have nothing more to write, reply with a single word. The',
'word is discarded - only the tool calls matter.',
'**Final reply: one or two sentences explaining your choices.** After',
'your last tool call (or instead of any tool call, if you decided no',
'edits were warranted), reply with a brief operator-facing summary of',
'what you did and WHY. This text surfaces in the user\'s log drawer as',
'the cycle\'s outcome, so make it useful to a human skimming the log -',
'name the article(s) you touched, or name the reason you skipped.',
'Examples of good summaries:',
' "Updated the Nak article with March 2026 logo-brainstorm details;',
' added a Markdown link out to the Kermit Wikipedia entry."',
' "No edits - the conversation was generic technical Q&A about regex',
' with no user-centric subject."',
' "No edits - the conversation discussed Kermit at length, but it is',
' not user-centric and no existing Nak article was available to link',
' it from."',
'Skip filler ("Great work!", "I have finished"); lead with the',
'decision. Keep it under two sentences. Plain text, no Markdown.',
];

/**
Expand Down
11 changes: 8 additions & 3 deletions src/lib/agents/wiki/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ export interface WikiInput {

export interface WikiOutput {
/**
* The model's final (post-tool-loop) text. Always discarded by
* production callers - "reply with a single word" per the prompt -
* but returned here for debug logs and test assertions.
* The model's final (post-tool-loop) text. Used as the operator-
* facing reasoning surfaced in the log drawer - the prompt's "Final
* reply" block instructs the model to emit a one-or-two-sentence
* summary of what it did and why (e.g. "Updated the Nak article
* with March 2026 logo details." / "No edits - generic Q&A with no
* user-centric subject."). The loop trims and inlines this as
* `reasoning="..."` on the finished-thread log line, matching the
* shape the journal worker uses.
*/
finalText: string;
/**
Expand Down
Loading