04: Build Result
Assemble the final GenerationTurnResult from accumulated state.
File: src/utils/chat/toolLoop.ts:411-455
(includes resolveThoughtLogOwner at 441-450 and mergeDetails at 452-455)
Mission
Section titled “Mission”Construct the GenerationTurnResult returned by runToolLoop to its caller
(runGenerationTurn). Merges the NovelAI scene-metadata suffix into the
response text, packages the persona response objects, and resolves which
identity owns the thought log for display purposes.
This function is called from every exit point in the outer loop — both
terminal-status exits (completed, error, timeout, etc.) and the post-tool
endTurn/shouldEndAfterPreToolText exits.
status: GenerationTurnResult["status"]— the final loop status.context: ChatTurnContext— provides persona identity and impersonation flags.streamResults: StreamResult[]— all stream results accumulated across iterations (included verbatim in the result).responseText: string— the final accumulated response text (empty string when no text was produced).detailsText: string— accumulated NovelAI scene-metadata content fromstreamResult.detailsContentfields across tool-call iterations.thoughtLog: GenerationTurnResult["thoughtLog"] | undefined— the last thought log payload emitted by any iteration, orundefined.
Output
Section titled “Output”GenerationTurnResult — defined in src/utils/chat/types.ts:
{ status: StreamResult["status"] | "skipped"; streamResults: StreamResult[]; personaResponses: ChatPersonaResponse[]; // empty if no text thoughtLog?: ThoughtLogPayload; thoughtLogOwner?: ThoughtLogOwner;}mergeDetails — scene-metadata suffix
Section titled “mergeDetails — scene-metadata suffix”detailsText is produced by NovelAI when the model returns structured scene
metadata alongside its response. mergeDetails appends it as:
<responseText>
[Scene Metadata]<detailsText>If detailsText is empty or whitespace-only, responseText is used unchanged.
personaResponses assembly
Section titled “personaResponses assembly”When the merged text is non-empty, a single ChatPersonaResponse is emitted:
{ personaName: context.currentPersona.persona_nickname, text: mergedText, personaId: context.currentPersona.persona_id, personaLineageId: context.currentPersona.persona_lineage_id,}When the merged text is empty (e.g. status "error" with no pre-error text),
personaResponses is an empty array. Post-turn effects and the caller
distinguish empty personaResponses from the "skipped" status.
resolveThoughtLogOwner — identity resolution
Section titled “resolveThoughtLogOwner — identity resolution”Maps the turn context to the thought-log owner shape:
| Condition | thoughtLogOwner |
|---|---|
context.isUserImpersonation === true | { type: "user_impersonation", username, avatarUrl } |
context.currentPersona.is_alter === true | { type: "persona", persona: currentPersona } |
| Default | { type: "default" } |
thoughtLogOwner is undefined when thoughtLog is undefined.
Side effects
Section titled “Side effects”None — this function is pure assembly; it reads state but does not write to Discord, the database, the cache, or any external system.
Invariants
Section titled “Invariants”After this stage runs:
streamResultscontains everyStreamResultfrom every iteration, regardless of status.personaResponses.length === 0when there is nothing to display;responseSink.finalize(caller ofrunGenerationTurn) handles this case.- If
thoughtLogis present,thoughtLogOwneris also present.
Extension points
Section titled “Extension points”| Surface | Plugin-relevance |
|---|---|
ChatPersonaResponse shape | Internal — the shape is consumed by responseSink.finalize and post-turn effects; changing it requires updating both consumers |
resolveThoughtLogOwner identity types | Internal — "user_impersonation", "persona", "default" map to distinct display behaviors in the stream orchestrator |
mergeDetails scene-metadata format | Internal — the [Scene Metadata] block format is NovelAI-specific; no plugin surface |
Related docs
Section titled “Related docs”- Result consumer: →
responseSink.finalizeindocs/pipelines/chat/06-per-turn/02-create-response-sink.md - Post-turn effects (reads
personaResponses): →docs/pipelines/chat/06-per-turn/04-post-turn-effects.md - Tool-loop coordinator: →
README.md