feat: reasoning_delta 이벤트 처리 및 출력 개선
- reasoning_delta 이벤트 전파: edge console와 node sink에서 explicit 케이스로 처리 - 각 줄에 [node-reasoning] prefix를 붙여 reasoning 출력을 구분 - 빈 delta는 무시하고, 멀티라인 content도 각 줄에 prefix 적용 - 관련单元测试 추가
This commit is contained in:
parent
5b1255abc5
commit
7acb8e59fa
6 changed files with 166 additions and 0 deletions
|
|
@ -282,6 +282,8 @@ func SendRun(ctx context.Context, edgeSvc *edgeservice.Service, events *EventRou
|
|||
switch event.GetType() {
|
||||
case "start":
|
||||
fmt.Fprintf(out, "%s start run_id=%s\n", nodeEventPrefix(label), dispatch.RunID)
|
||||
case "reasoning_delta":
|
||||
writeReasoningDelta(out, label, event.GetDelta())
|
||||
case "delta":
|
||||
response.Write(event.GetDelta())
|
||||
case "complete":
|
||||
|
|
|
|||
|
|
@ -233,6 +233,28 @@ func TestSendRun_SubmitRunRequest_MetadataSource(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWriteReasoningDeltaPrefixesEachLine(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
writeReasoningDelta(&out, "node0", "사용자가 인사했습니다.\n\x1f\x1f\n")
|
||||
got := out.String()
|
||||
want1 := "[node0-reasoning] 사용자가 인사했습니다."
|
||||
want2 := "[node0-reasoning] \x1f\x1f"
|
||||
if !strings.Contains(got, want1) {
|
||||
t.Errorf("expected %q, got:\n%s", want1, got)
|
||||
}
|
||||
if !strings.Contains(got, want2) {
|
||||
t.Errorf("expected %q, got:\n%s", want2, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteReasoningDeltaIgnoresEmptyDelta(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
writeReasoningDelta(&out, "node0", "")
|
||||
if out.Len() != 0 {
|
||||
t.Errorf("expected no output for empty delta, got:\n%s", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatNodeCommandView_RendersSortedResult(t *testing.T) {
|
||||
view := edgeservice.NodeCommandView{
|
||||
NodeLabel: "node0",
|
||||
|
|
|
|||
|
|
@ -202,6 +202,8 @@ func (r *EventRouter) printAsyncLocked(event *iop.RunEvent) {
|
|||
switch event.GetType() {
|
||||
case "start":
|
||||
fmt.Fprintf(r.out, "%s start run_id=%s session=%s background=%v\n", nodeEventPrefix(label), runID, event.GetSessionId(), event.GetBackground())
|
||||
case "reasoning_delta":
|
||||
writeReasoningDelta(r.out, label, event.GetDelta())
|
||||
case "delta":
|
||||
r.responseStream(runID, label).Write(event.GetDelta())
|
||||
case "complete":
|
||||
|
|
@ -301,3 +303,16 @@ func (s *ResponseStream) FinishIfStarted() {
|
|||
s.Finish()
|
||||
}
|
||||
}
|
||||
|
||||
func nodeReasoningPrefix(label string) string {
|
||||
return fmt.Sprintf("[%s-reasoning] ", label)
|
||||
}
|
||||
|
||||
func writeReasoningDelta(out io.Writer, label, delta string) {
|
||||
if delta == "" {
|
||||
return
|
||||
}
|
||||
stream := NewResponseStream(out, nodeReasoningPrefix(label))
|
||||
stream.Write(delta)
|
||||
stream.FinishIfStarted()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,6 +148,37 @@ func TestEventRouterFallsBackToNodeID(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestEventRouterPrintsReasoningDeltaForAsyncRun(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
reg := edgenode.NewRegistry()
|
||||
reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "node1"})
|
||||
router := NewEventRouter(&out, reg, nil)
|
||||
|
||||
router.Handle(&iop.RunEvent{
|
||||
RunId: "run-rd",
|
||||
Type: "reasoning_delta",
|
||||
Delta: "사용자가 인사했습니다.\n\x1f\x1f\n",
|
||||
NodeId: "node-1",
|
||||
Background: true,
|
||||
})
|
||||
|
||||
got := out.String()
|
||||
want1 := "[node0-reasoning] 사용자가 인사했습니다."
|
||||
want2 := "[node0-reasoning] \x1f\x1f"
|
||||
if !strings.Contains(got, want1) {
|
||||
t.Errorf("expected %q in output, got:\n%s", want1, got)
|
||||
}
|
||||
if !strings.Contains(got, want2) {
|
||||
t.Errorf("expected %q in output, got:\n%s", want2, got)
|
||||
}
|
||||
if strings.Contains(got, "reasoning_delta run_id=") || strings.Contains(got, `detail=""`) {
|
||||
t.Errorf("reasoning delta should not fall through to detail, got:\n%s", got)
|
||||
}
|
||||
if strings.Contains(got, "[node0-msg]") {
|
||||
t.Errorf("reasoning delta should not appear as msg content, got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventRouterPrintsNodeLifecycleEvents(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
reg := edgenode.NewRegistry()
|
||||
|
|
|
|||
|
|
@ -823,6 +823,16 @@ func (s *sessionSink) printEvent(event runtime.RuntimeEvent) {
|
|||
}
|
||||
fmt.Fprint(s.out, event.Delta)
|
||||
s.lineEnded = strings.HasSuffix(event.Delta, "\n")
|
||||
case runtime.EventTypeReasoningDelta:
|
||||
if event.Delta == "" {
|
||||
return
|
||||
}
|
||||
if s.streaming && !s.lineEnded {
|
||||
fmt.Fprintln(s.out)
|
||||
}
|
||||
s.streaming = false
|
||||
s.lineEnded = true
|
||||
printPrefixedLines(s.out, "[node-reasoning] ", event.Delta)
|
||||
case runtime.EventTypeComplete:
|
||||
if s.streaming && !s.lineEnded {
|
||||
fmt.Fprintln(s.out)
|
||||
|
|
@ -878,6 +888,16 @@ func printTaggedMessage(out io.Writer, tag, message string) {
|
|||
fmt.Fprintf(out, "[%s] %s\n", tag, message)
|
||||
}
|
||||
|
||||
func printPrefixedLines(out io.Writer, prefix, text string) {
|
||||
text = strings.TrimRight(text, "\n")
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
fmt.Fprintf(out, "%s%s\n", prefix, line)
|
||||
}
|
||||
}
|
||||
|
||||
func structAsMap(s *structpb.Struct) map[string]any {
|
||||
if s == nil {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -358,3 +358,79 @@ func TestTerminalDeferringSinkSynthesizedTerminalNotDuplicated(t *testing.T) {
|
|||
t.Fatal("expected terminalObserved to be true after flush")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionSinkPrintEventPreservesReasoningDelta(t *testing.T) {
|
||||
ms := &mockSender{}
|
||||
var out bytes.Buffer
|
||||
sink := &sessionSink{
|
||||
sess: ms,
|
||||
out: &out,
|
||||
nodeID: "test-node",
|
||||
sessionID: "session-x",
|
||||
}
|
||||
|
||||
runID := "run-reasoning"
|
||||
|
||||
// Emit start.
|
||||
if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
|
||||
RunID: runID,
|
||||
Type: runtime.EventTypeStart,
|
||||
Timestamp: time.Now(),
|
||||
}); err != nil {
|
||||
t.Fatalf("Emit start: %v", err)
|
||||
}
|
||||
|
||||
// Emit reasoning delta with multiline content.
|
||||
reasoning := runtime.RuntimeEvent{
|
||||
RunID: runID,
|
||||
Type: runtime.EventTypeReasoningDelta,
|
||||
Delta: "사용자가 인사했습니다.\n😀\n",
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
if err := sink.Emit(context.Background(), reasoning); err != nil {
|
||||
t.Fatalf("Emit reasoning_delta: %v", err)
|
||||
}
|
||||
|
||||
// Emit complete.
|
||||
if err := sink.Emit(context.Background(), runtime.RuntimeEvent{
|
||||
RunID: runID,
|
||||
Type: runtime.EventTypeComplete,
|
||||
Message: "done",
|
||||
Timestamp: time.Now(),
|
||||
}); err != nil {
|
||||
t.Fatalf("Emit complete: %v", err)
|
||||
}
|
||||
|
||||
got := out.String()
|
||||
|
||||
// Reasoning delta should be preserved with [node-reasoning] prefix.
|
||||
if !strings.Contains(got, "[node-reasoning] 사용자가 인사했습니다.") {
|
||||
t.Fatalf("expected [node-reasoning] line 1, got %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "[node-reasoning] 😀") {
|
||||
if !strings.Contains(got, "[node-reasoning] ") {
|
||||
t.Fatalf("expected [node-reasoning] prefix in stdout, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// stdout should NOT contain the raw reasoning_delta fallback.
|
||||
if strings.Contains(got, "reasoning_delta run_id=") && strings.Contains(got, "detail=\"\"") {
|
||||
t.Fatalf("reasoning_delta should not fall through to default case, got %q", got)
|
||||
}
|
||||
|
||||
// [node-message] should not appear for reasoning-only output.
|
||||
if strings.Contains(got, "[node-message]") {
|
||||
t.Fatalf("[node-message] should not appear for reasoning-only output, got %q", got)
|
||||
}
|
||||
|
||||
// Verify proto event preserves reasoning_delta type and delta content.
|
||||
if len(ms.sentEvents) != 3 {
|
||||
t.Fatalf("expected 3 proto events, got %d", len(ms.sentEvents))
|
||||
}
|
||||
if ms.sentEvents[1].GetType() != "reasoning_delta" {
|
||||
t.Fatalf("proto event type = %q, want reasoning_delta", ms.sentEvents[1].GetType())
|
||||
}
|
||||
if ms.sentEvents[1].Delta != "사용자가 인사했습니다.\n😀\n" {
|
||||
t.Fatalf("proto event Delta = %q, expected multiline reasoning text", ms.sentEvents[1].Delta)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue