234 lines
5.7 KiB
Go
234 lines
5.7 KiB
Go
package node
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
|
|
"iop/apps/node/internal/runtime"
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
type protoSender interface {
|
|
Send(m proto.Message) error
|
|
}
|
|
|
|
type noopSender struct{}
|
|
|
|
func (noopSender) Send(proto.Message) error { return nil }
|
|
|
|
// terminalDeferringSink holds terminal events until Node-local admission has
|
|
// released its slot. Edge uses terminal run events to advance queued work, so
|
|
// emitting them before the local slot is free can over-dispatch back into Node.
|
|
type terminalDeferringSink struct {
|
|
inner runtime.EventSink
|
|
|
|
mu sync.Mutex
|
|
deferring bool
|
|
terminalObserved bool
|
|
deferred []runtime.RuntimeEvent
|
|
}
|
|
|
|
func (s *terminalDeferringSink) Emit(ctx context.Context, event runtime.RuntimeEvent) error {
|
|
s.mu.Lock()
|
|
if isTerminalRuntimeEvent(event.Type) {
|
|
s.terminalObserved = true
|
|
}
|
|
if s.deferring || isTerminalRuntimeEvent(event.Type) {
|
|
s.deferring = true
|
|
s.deferred = append(s.deferred, event)
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
s.mu.Unlock()
|
|
return s.inner.Emit(ctx, event)
|
|
}
|
|
|
|
func (s *terminalDeferringSink) Flush(ctx context.Context) error {
|
|
s.mu.Lock()
|
|
events := append([]runtime.RuntimeEvent(nil), s.deferred...)
|
|
s.deferred = nil
|
|
s.deferring = false
|
|
s.mu.Unlock()
|
|
|
|
for _, event := range events {
|
|
if err := s.inner.Emit(ctx, event); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *terminalDeferringSink) hasTerminalObserved() bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.terminalObserved
|
|
}
|
|
|
|
func isTerminalRuntimeEvent(t runtime.EventType) bool {
|
|
return t == runtime.EventTypeComplete || t == runtime.EventTypeError || t == runtime.EventTypeCancelled
|
|
}
|
|
|
|
// sessionSink wraps a transport.Session to implement runtime.EventSink.
|
|
type sessionSink struct {
|
|
sess protoSender
|
|
out io.Writer
|
|
nodeID string
|
|
sessionID string
|
|
background bool
|
|
streaming bool
|
|
lineEnded bool
|
|
}
|
|
|
|
func (s *sessionSink) Emit(_ context.Context, event runtime.RuntimeEvent) error {
|
|
s.printEvent(event)
|
|
re := &iop.RunEvent{
|
|
RunId: event.RunID,
|
|
Type: string(event.Type),
|
|
Delta: event.Delta,
|
|
Message: event.Message,
|
|
Error: event.Error,
|
|
Metadata: event.Metadata,
|
|
Timestamp: event.Timestamp.UnixNano(),
|
|
SessionId: s.sessionID,
|
|
Background: s.background,
|
|
NodeId: s.nodeID,
|
|
}
|
|
if event.Usage != nil {
|
|
re.Usage = &iop.Usage{
|
|
InputTokens: int32(event.Usage.InputTokens),
|
|
OutputTokens: int32(event.Usage.OutputTokens),
|
|
ReasoningTokens: int32(event.Usage.ReasoningTokens),
|
|
CachedInputTokens: int32(event.Usage.CachedInputTokens),
|
|
}
|
|
}
|
|
return s.sess.Send(re)
|
|
}
|
|
|
|
func (s *sessionSink) printEvent(event runtime.RuntimeEvent) {
|
|
if s.out == nil {
|
|
return
|
|
}
|
|
switch event.Type {
|
|
case runtime.EventTypeStart:
|
|
s.streaming = false
|
|
s.lineEnded = true
|
|
fmt.Fprintf(s.out, "[node-event] start run_id=%s\n", event.RunID)
|
|
case runtime.EventTypeDelta:
|
|
if event.Delta == "" {
|
|
return
|
|
}
|
|
if !s.streaming {
|
|
fmt.Fprint(s.out, "[node-message] ")
|
|
s.streaming = true
|
|
}
|
|
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)
|
|
}
|
|
s.streaming = false
|
|
s.lineEnded = true
|
|
fmt.Fprintf(s.out, "[node-event] complete run_id=%s detail=%q\n", event.RunID, event.Message)
|
|
case runtime.EventTypeError:
|
|
if s.streaming && !s.lineEnded {
|
|
fmt.Fprintln(s.out)
|
|
}
|
|
s.streaming = false
|
|
s.lineEnded = true
|
|
fmt.Fprintf(s.out, "[node-event] error run_id=%s detail=%q\n", event.RunID, event.Error)
|
|
case runtime.EventTypeCancelled:
|
|
if s.streaming && !s.lineEnded {
|
|
fmt.Fprintln(s.out)
|
|
}
|
|
s.streaming = false
|
|
s.lineEnded = true
|
|
fmt.Fprintf(s.out, "[node-event] cancelled run_id=%s\n", event.RunID)
|
|
default:
|
|
fmt.Fprintf(s.out, "[node-event] %s run_id=%s detail=%q\n", event.Type, event.RunID, event.Message)
|
|
}
|
|
}
|
|
|
|
func printEdgeMessage(out io.Writer, input map[string]any) {
|
|
if out == nil {
|
|
return
|
|
}
|
|
if input == nil {
|
|
printTaggedMessage(out, "edge-message", "")
|
|
return
|
|
}
|
|
if prompt, ok := input["prompt"].(string); ok {
|
|
printTaggedMessage(out, "edge-message", prompt)
|
|
return
|
|
}
|
|
b, err := json.MarshalIndent(input, "", " ")
|
|
if err != nil {
|
|
printTaggedMessage(out, "edge-message", fmt.Sprintf("%v", input))
|
|
return
|
|
}
|
|
fmt.Fprintf(out, "[edge-message]\n%s\n", b)
|
|
}
|
|
|
|
func printTaggedMessage(out io.Writer, tag, message string) {
|
|
message = strings.TrimSpace(message)
|
|
if message == "" {
|
|
fmt.Fprintf(out, "[%s] <empty>\n", tag)
|
|
return
|
|
}
|
|
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
|
|
}
|
|
return s.AsMap()
|
|
}
|
|
|
|
func sessionModeFromProto(m iop.RunSessionMode) runtime.SessionMode {
|
|
if m == iop.RunSessionMode_RUN_SESSION_MODE_REQUIRE_EXISTING {
|
|
return runtime.SessionModeRequireExisting
|
|
}
|
|
return runtime.SessionModeCreateIfMissing
|
|
}
|
|
|
|
func cancelActionFromProto(a iop.CancelAction) runtime.CancelAction {
|
|
if a == iop.CancelAction_CANCEL_ACTION_TERMINATE_SESSION {
|
|
return runtime.CancelActionTerminateSession
|
|
}
|
|
return runtime.CancelActionCancelRun
|
|
}
|
|
|
|
func normalizeSessionID(id string) string {
|
|
if id == "" {
|
|
return runtime.DefaultSessionID
|
|
}
|
|
return id
|
|
}
|