원격 터미널 브리지 POC 전에 Client HTTP lifecycle과 Edge run result surface 계약을 고정해야 하므로 관련 구현, 테스트, 로드맵 상태를 함께 정리한다.
267 lines
6.3 KiB
Go
267 lines
6.3 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
|
|
eventpkg "iop/packages/go/events"
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
type SubmitRunRequest struct {
|
|
NodeRef string
|
|
RunID string
|
|
Adapter string
|
|
Target string
|
|
SessionID string
|
|
Prompt string
|
|
Input map[string]any
|
|
Background bool
|
|
TimeoutSec int
|
|
Metadata map[string]string
|
|
}
|
|
|
|
// RunDispatch describes a dispatched run in surface-neutral terms. It is the
|
|
// metadata any caller (console, HTTP, future RPC) needs after submission.
|
|
type RunDispatch struct {
|
|
RunID string
|
|
NodeID string
|
|
NodeLabel string
|
|
Adapter string
|
|
Target string
|
|
SessionID string
|
|
Background bool
|
|
TimeoutSec int
|
|
}
|
|
|
|
// RunStream carries asynchronous events for a dispatched foreground run.
|
|
// Background runs leave both channels nil.
|
|
type RunStream struct {
|
|
Events <-chan *iop.RunEvent
|
|
NodeEvents <-chan *iop.EdgeNodeEvent
|
|
}
|
|
|
|
// RunResult describes the interface for a submitted execution run, allowing
|
|
// surface-neutral consumption of its dispatch info and event stream.
|
|
type RunResult interface {
|
|
Dispatch() RunDispatch
|
|
Stream() RunStream
|
|
Close()
|
|
WaitTimeout() time.Duration
|
|
}
|
|
|
|
// RunHandle is the legacy combined surface kept for existing callers; it
|
|
// embeds the surface-neutral DTOs so HTTP/API code can consume RunDispatch
|
|
// or RunStream directly without depending on this struct.
|
|
type RunHandle struct {
|
|
RunDispatch
|
|
RunStream
|
|
close func()
|
|
}
|
|
|
|
func (h *RunHandle) Close() {
|
|
if h != nil && h.close != nil {
|
|
h.close()
|
|
}
|
|
}
|
|
|
|
func (h *RunHandle) WaitTimeout() time.Duration {
|
|
if h == nil {
|
|
return time.Duration(DefaultTimeoutSec+5) * time.Second
|
|
}
|
|
return time.Duration(normalizeTimeoutSec(h.TimeoutSec)+5) * time.Second
|
|
}
|
|
|
|
func (h *RunHandle) Dispatch() RunDispatch {
|
|
if h == nil {
|
|
return RunDispatch{}
|
|
}
|
|
return h.RunDispatch
|
|
}
|
|
|
|
func (h *RunHandle) Stream() RunStream {
|
|
if h == nil {
|
|
return RunStream{}
|
|
}
|
|
return h.RunStream
|
|
}
|
|
|
|
func (s *Service) SubmitRun(_ context.Context, req SubmitRunRequest) (RunResult, error) {
|
|
entry, err := s.ResolveNode(req.NodeRef)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
runReq, runID, err := BuildRunRequest(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var runEvents <-chan *iop.RunEvent
|
|
var unregisterRun func()
|
|
var nodeEvents <-chan *iop.EdgeNodeEvent
|
|
var unregisterNode func()
|
|
if !runReq.GetBackground() {
|
|
if s.events == nil {
|
|
return nil, fmt.Errorf("event bus is not configured")
|
|
}
|
|
runEvents, unregisterRun = s.events.SubscribeRun(runID, 4096)
|
|
nodeEvents, unregisterNode = s.events.SubscribeNode(entry.NodeID, 16)
|
|
}
|
|
|
|
if err := entry.Client.Send(runReq); err != nil {
|
|
if unregisterRun != nil {
|
|
unregisterRun()
|
|
}
|
|
if unregisterNode != nil {
|
|
unregisterNode()
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
return &RunHandle{
|
|
RunDispatch: RunDispatch{
|
|
RunID: runID,
|
|
NodeID: entry.NodeID,
|
|
NodeLabel: nodeLabel(entry),
|
|
Adapter: runReq.GetAdapter(),
|
|
Target: runReq.GetTarget(),
|
|
SessionID: runReq.GetSessionId(),
|
|
Background: runReq.GetBackground(),
|
|
TimeoutSec: int(runReq.GetTimeoutSec()),
|
|
},
|
|
RunStream: RunStream{
|
|
Events: runEvents,
|
|
NodeEvents: nodeEvents,
|
|
},
|
|
close: func() {
|
|
if unregisterRun != nil {
|
|
unregisterRun()
|
|
}
|
|
if unregisterNode != nil {
|
|
unregisterNode()
|
|
}
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
type CancelRunRequest struct {
|
|
NodeRef string
|
|
RunID string
|
|
Adapter string
|
|
Target string
|
|
SessionID string
|
|
}
|
|
|
|
func BuildCancelRunRequest(req CancelRunRequest) *iop.CancelRequest {
|
|
return &iop.CancelRequest{
|
|
RunId: req.RunID,
|
|
Adapter: req.Adapter,
|
|
Target: req.Target,
|
|
SessionId: NormalizeSessionID(req.SessionID),
|
|
Action: iop.CancelAction_CANCEL_ACTION_CANCEL_RUN,
|
|
}
|
|
}
|
|
|
|
func (s *Service) CancelRun(_ context.Context, req CancelRunRequest) (CommandResult, error) {
|
|
entry, err := s.ResolveNode(req.NodeRef)
|
|
if err != nil {
|
|
return CommandResult{}, err
|
|
}
|
|
cancelReq := BuildCancelRunRequest(req)
|
|
if err := entry.Client.Send(cancelReq); err != nil {
|
|
return CommandResult{}, err
|
|
}
|
|
return CommandResult{
|
|
NodeID: entry.NodeID,
|
|
NodeLabel: nodeLabel(entry),
|
|
SessionID: cancelReq.GetSessionId(),
|
|
}, nil
|
|
}
|
|
|
|
type TerminateSessionRequest struct {
|
|
NodeRef string
|
|
Adapter string
|
|
Target string
|
|
SessionID string
|
|
}
|
|
|
|
// CommandResult is the surface-neutral acknowledgement for one-shot node
|
|
// commands (terminate-session, future control RPCs).
|
|
type CommandResult struct {
|
|
NodeID string
|
|
NodeLabel string
|
|
SessionID string
|
|
}
|
|
|
|
// TerminateSessionResult is retained as an alias for backward compatibility.
|
|
type TerminateSessionResult = CommandResult
|
|
|
|
func (s *Service) TerminateSession(_ context.Context, req TerminateSessionRequest) (TerminateSessionResult, error) {
|
|
entry, err := s.ResolveNode(req.NodeRef)
|
|
if err != nil {
|
|
return TerminateSessionResult{}, err
|
|
}
|
|
|
|
sessionID := NormalizeSessionID(req.SessionID)
|
|
cancelReq := &iop.CancelRequest{
|
|
Adapter: req.Adapter,
|
|
Target: req.Target,
|
|
SessionId: sessionID,
|
|
Action: iop.CancelAction_CANCEL_ACTION_TERMINATE_SESSION,
|
|
}
|
|
if err := entry.Client.Send(cancelReq); err != nil {
|
|
return TerminateSessionResult{}, err
|
|
}
|
|
return TerminateSessionResult{
|
|
NodeID: entry.NodeID,
|
|
NodeLabel: nodeLabel(entry),
|
|
SessionID: sessionID,
|
|
}, nil
|
|
}
|
|
|
|
func NewRunID() string {
|
|
return fmt.Sprintf("manual-%d", time.Now().UnixNano())
|
|
}
|
|
|
|
func IsNodeDisconnected(event *iop.EdgeNodeEvent) bool {
|
|
return event.GetType() == eventpkg.TypeNodeDisconnected
|
|
}
|
|
|
|
func BuildRunRequest(req SubmitRunRequest) (*iop.RunRequest, string, error) {
|
|
inputMap := make(map[string]any, len(req.Input)+1)
|
|
for k, v := range req.Input {
|
|
inputMap[k] = v
|
|
}
|
|
if req.Prompt != "" {
|
|
if _, ok := inputMap["prompt"]; !ok {
|
|
inputMap["prompt"] = req.Prompt
|
|
}
|
|
}
|
|
input, err := structpb.NewStruct(inputMap)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
runID := req.RunID
|
|
if runID == "" {
|
|
runID = NewRunID()
|
|
}
|
|
metadata := make(map[string]string, len(req.Metadata))
|
|
for k, v := range req.Metadata {
|
|
metadata[k] = v
|
|
}
|
|
return &iop.RunRequest{
|
|
RunId: runID,
|
|
Adapter: req.Adapter,
|
|
Target: req.Target,
|
|
SessionId: NormalizeSessionID(req.SessionID),
|
|
SessionMode: iop.RunSessionMode_RUN_SESSION_MODE_CREATE_IF_MISSING,
|
|
Background: req.Background,
|
|
Input: input,
|
|
TimeoutSec: int32(normalizeTimeoutSec(req.TimeoutSec)),
|
|
Metadata: metadata,
|
|
}, runID, nil
|
|
}
|