@@ -41,7 +41,7 @@ type Server struct {
4141 srv * http.Server
4242 mu sync.RWMutex
4343 logger * slog.Logger
44- conversation * st.PTYConversation
44+ conversation st.Conversation
4545 agentio * termexec.Process
4646 agentType mf.AgentType
4747 emitter * EventEmitter
@@ -244,6 +244,14 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) {
244244 return mf .FormatToolCall (config .AgentType , message )
245245 }
246246
247+ emitter := NewEventEmitter (1024 )
248+
249+ // Format initial prompt into message parts if provided
250+ var initialPrompt []st.MessagePart
251+ if config .InitialPrompt != "" {
252+ initialPrompt = FormatMessage (config .AgentType , config .InitialPrompt )
253+ }
254+
247255 conversation := st .NewPTY (ctx , st.PTYConversationConfig {
248256 AgentType : config .AgentType ,
249257 AgentIO : config .Process ,
@@ -253,9 +261,17 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) {
253261 FormatMessage : formatMessage ,
254262 ReadyForInitialPrompt : isAgentReadyForInitialPrompt ,
255263 FormatToolCall : formatToolCall ,
256- Logger : logger ,
257- }, config .InitialPrompt )
258- emitter := NewEventEmitter (1024 )
264+ InitialPrompt : initialPrompt ,
265+ // OnSnapshot uses a callback rather than passing the emitter directly
266+ // to keep the screentracker package decoupled from httpapi concerns.
267+ // This preserves clean package boundaries and avoids import cycles.
268+ OnSnapshot : func (status st.ConversationStatus , messages []st.ConversationMessage , screen string ) {
269+ emitter .UpdateStatusAndEmitChanges (status , config .AgentType )
270+ emitter .UpdateMessagesAndEmitChanges (messages )
271+ emitter .UpdateScreenAndEmitChanges (screen )
272+ },
273+ Logger : logger ,
274+ })
259275
260276 // Create temporary directory for uploads
261277 tempDir , err := os .MkdirTemp ("" , "agentapi-uploads-" )
@@ -281,6 +297,16 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) {
281297 // Register API routes
282298 s .registerRoutes ()
283299
300+ // Start the conversation polling loop if we have a process.
301+ // Process is nil only when --print-openapi is used (no agent runs).
302+ // The process is already running at this point - termexec.StartProcess()
303+ // blocks until the PTY is created and the process is active. Agent
304+ // readiness (waiting for the prompt) is handled asynchronously inside
305+ // conversation.Start() via ReadyForInitialPrompt.
306+ if config .Process != nil {
307+ s .conversation .Start (ctx )
308+ }
309+
284310 return s , nil
285311}
286312
@@ -336,38 +362,6 @@ func sseMiddleware(ctx huma.Context, next func(huma.Context)) {
336362 next (ctx )
337363}
338364
339- func (s * Server ) StartSnapshotLoop (ctx context.Context ) {
340- s .conversation .Start (ctx )
341- go func () {
342- ticker := s .clock .NewTicker (snapshotInterval )
343- defer ticker .Stop ()
344- for {
345- currentStatus := s .conversation .Status ()
346-
347- // Send initial prompt when agent becomes stable for the first time
348- if ! s .conversation .InitialPromptSent && convertStatus (currentStatus ) == AgentStatusStable {
349- if err := s .conversation .Send (FormatMessage (s .agentType , s .conversation .InitialPrompt )... ); err != nil {
350- s .logger .Error ("Failed to send initial prompt" , "error" , err )
351- } else {
352- s .conversation .InitialPromptSent = true
353- s .conversation .ReadyForInitialPrompt = false
354- currentStatus = st .ConversationStatusChanging
355- s .logger .Info ("Initial prompt sent successfully" )
356- }
357- }
358- s .emitter .UpdateStatusAndEmitChanges (currentStatus , s .agentType )
359- s .emitter .UpdateMessagesAndEmitChanges (s .conversation .Messages ())
360- s .emitter .UpdateScreenAndEmitChanges (s .conversation .Text ())
361-
362- select {
363- case <- ctx .Done ():
364- return
365- case <- ticker .C :
366- }
367- }
368- }()
369- }
370-
371365// registerRoutes sets up all API endpoints
372366func (s * Server ) registerRoutes () {
373367 // GET /status endpoint
0 commit comments