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
1 change: 1 addition & 0 deletions git-hooks/spec-registry.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@
1.24 1 47 acceptance tag
1.4a 1 48 sub skip
1.25 1 49 main tag
1.21.1 1 50 sub tag
65 changes: 0 additions & 65 deletions internal/app/cli/llmapp/bullet_test.go

This file was deleted.

11 changes: 6 additions & 5 deletions internal/app/cli/llmapp/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
// ctrl chan (pure) and returns loopStartCmd; the goroutine + diagnose.
// Loop.Run + provider IO + cancel fire live in Cmds / Cleanup.
//
// text/control split (R2 CRIT-2 + spec-1.21 D-6 union extension):
// renderable text → TokenStream (Drained in View); control
// (VisibleContent / ThinkingToken + *llm.ToolUse / *llm.ToolResult / Finish +
// TermCode) → ctrl chan → reader-Cmd → Update. Thinking / tool events
// never enter the FROZEN streaming.Chunk.
// text/control split (R2 CRIT-2 + spec-1.21.1 ordering fix):
// renderable text is previewed through TokenStream but committed through
// sealed text control messages in the same ctrl FIFO as tool events.
// PreviewTick wakes Update to refresh previewNodes; ThinkingToken,
// *llm.ToolUse, *llm.ToolResult, Finish and TermCode remain control-only.
// Thinking / tool events never enter the FROZEN streaming.Chunk.
//
// cli-tree root (peer to cmd/opendbx); not part of the §3.1 render DAG.
package llmapp
67 changes: 44 additions & 23 deletions internal/app/cli/llmapp/loop_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
//
// Author: sqlrush

// File loop_adapter.go — bridges diagnose.Loop events into the existing
// llmapp control plumbing (streamControlMsg + readControlMsg + ctrl
// channel), preserving the spec-1.20 R2 CRIT-2 token-vs-control split:
// File loop_adapter.go — bridges diagnose.Loop events into llmapp's
// control plumbing (streamControlMsg + readControlMsg + ctrl channel).
// spec-1.21.1 fixes the old token-vs-control merge race by making text
// authoritative through the same ctrl FIFO as tool events:
//
// - EventText visible → TokenStream.AppendChunk + streamControlMsg
// - EventText visible → segBuf + TokenStream preview + PreviewTick
// - EventText thinking → streamControlMsg{ThinkingToken}, never TokenStream
// - EventToolCall → streamControlMsg carrying *llm.ToolUse
// - EventToolResult → streamControlMsg carrying *llm.ToolResult
// - EventFinish → streamControlMsg carrying Finish + TermCode + Err
// - EventTurnStart → no-op on the UI (turn boundaries are not yet
// - EventToolCall → seal visible segBuf, then *llm.ToolUse
// - EventToolResult → seal visible segBuf, then *llm.ToolResult
// - EventFinish → seal visible segBuf, then Finish + TermCode + Err
// - EventTurnStart → no-op on the UI (turn boundaries are not yet
// rendered; reserved for future progress indicators)
//
// Backpressure parity (spec-1.21 T-2.1 HIGH-2 / D-6): every ctrl send
Expand All @@ -22,6 +23,7 @@ package llmapp

import (
"context"
"strings"
"time"

"github.com/sqlrush/opendbx/internal/app/cli/render/scheduler"
Expand All @@ -47,6 +49,21 @@ func makeEmit(ts *streaming.TokenStream, ctrl chan<- streamControlMsg, stripThin
return ctx.Err()
}
}

// spec-1.21.1 segment-mode contract: makeEmit is called by the
// diagnose.Loop producer goroutine in event order, so segBuf is
// intentionally goroutine-local and unsynchronized. Do not reuse this
// closure from multiple producers without re-specifying ordering.
var segBuf strings.Builder // spec-1.21.1 R-fix MED-1: O(n) accumulation (was string concat)
seal := func(ctx context.Context, truncated bool) error {
if segBuf.Len() == 0 {
return nil
}
text := segBuf.String()
segBuf.Reset()
return send(ctx, streamControlMsg{SealedText: text, SealedTruncated: truncated})
}

return func(ctx context.Context, e diagnose.Event) error {
switch e.Kind {
case diagnose.EventText:
Expand All @@ -57,27 +74,29 @@ func makeEmit(ts *streaming.TokenStream, ctrl chan<- streamControlMsg, stripThin
}
return send(ctx, msg)
}
visible := e.Text != ""
if visible {
_ = ts.AppendChunk(streaming.Chunk{Token: e.Text})
sb.addText(e.Text) // spec-1.23 D-3: accumulate the visible final answer
if e.Text == "" {
return nil
}
return send(ctx, streamControlMsg{VisibleContent: visible})
segBuf.WriteString(e.Text)
_ = ts.AppendChunk(streaming.Chunk{Token: e.Text})
sb.addText(e.Text) // spec-1.23 D-3: accumulate the visible final answer
return send(ctx, streamControlMsg{PreviewTick: true})
case diagnose.EventToolCall:
if err := seal(ctx, false); err != nil {
return err
}
sb.addToolCall(e.ToolUse) // spec-1.23 D-3
return send(ctx, streamControlMsg{ToolUse: e.ToolUse})
case diagnose.EventToolResult:
if err := seal(ctx, false); err != nil {
return err
}
sb.addToolResult(e.ToolResult, e.Cached) // spec-1.23 D-3
return send(ctx, streamControlMsg{ToolResult: e.ToolResult, Cached: e.Cached})
case diagnose.EventFinish:
// Push a terminal chunk so the TokenStream's per-finish
// branches (Length truncation marker / cancel state) keep
// working under the new emit path (spec-1.6 R2.2 / spec-1.20
// thinking-only Empty placeholder).
_ = ts.AppendChunk(streaming.Chunk{
FinishReason: mapToRender(e.Finish),
Err: e.Err,
})
if err := seal(ctx, e.Finish == llm.FinishLength); err != nil {
return err
}
// spec-1.23 D-3: seal the run snapshot atomically with the finish
// and ride it on this same control msg — no separate post-Run send,
// so no channel-close race (three-route T-2 收敛 fix).
Expand All @@ -99,8 +118,10 @@ func makeEmit(ts *streaming.TokenStream, ctrl chan<- streamControlMsg, stripThin
// loopStartCmd launches a diagnose.Loop.Run on the worker pool. The
// goroutine drains the loop to terminal, then closes ctrl and ts so the
// reader-Cmd observes a done signal (mirrors the spec-1.20 consumeStream
// teardown order — close ts BEFORE ctrl is the convention so the
// streamDoneMsg drain sees a fully-flushed TokenStream).
// teardown order — close ts BEFORE ctrl is the convention). On the normal
// path authoritative text has already travelled through ctrl as SealedText, so
// streamDoneMsg only performs cleanup; on a cancel mid-segment streamDoneMsg
// recovers the residual partial text from previewNodes (spec-1.21.1 R-fix H-1).
func loopStartCmd(
ctx context.Context,
loop *diagnose.Loop,
Expand Down
Loading
Loading