From 087acf83eda8afbda973df4b17f7a51253c53c6e Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Thu, 2 Oct 2025 12:15:53 +0530 Subject: [PATCH 1/2] feat: accept initial prompt --- cmd/server/server.go | 3 +++ lib/httpapi/server.go | 59 ++++++++++++++++++++++++++++--------------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/cmd/server/server.go b/cmd/server/server.go index 56b07ea..3afe050 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -112,6 +112,7 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er ChatBasePath: viper.GetString(FlagChatBasePath), AllowedHosts: viper.GetStringSlice(FlagAllowedHosts), AllowedOrigins: viper.GetStringSlice(FlagAllowedOrigins), + InitialPrompt: viper.GetString(FlagInitialPrompt), }) if err != nil { return xerrors.Errorf("failed to create server: %w", err) @@ -174,6 +175,7 @@ const ( FlagAllowedHosts = "allowed-hosts" FlagAllowedOrigins = "allowed-origins" FlagExit = "exit" + FlagInitialPrompt = "initial-prompt" ) func CreateServerCmd() *cobra.Command { @@ -211,6 +213,7 @@ func CreateServerCmd() *cobra.Command { {FlagAllowedHosts, "a", []string{"localhost", "127.0.0.1", "[::1]"}, "HTTP allowed hosts (hostnames only, no ports). Use '*' for all, comma-separated list via flag, space-separated list via AGENTAPI_ALLOWED_HOSTS env var", "stringSlice"}, // localhost:3284 is the default origin when you open the chat interface in your browser. localhost:3000 and 3001 are used during development. {FlagAllowedOrigins, "o", []string{"http://localhost:3284", "http://localhost:3000", "http://localhost:3001"}, "HTTP allowed origins. Use '*' for all, comma-separated list via flag, space-separated list via AGENTAPI_ALLOWED_ORIGINS env var", "stringSlice"}, + {FlagInitialPrompt, "I", "", "Initial prompt for the agent (recommended only if the agent doesn't support initial prompt in interaction mode)", "string"}, } for _, spec := range flagSpecs { diff --git a/lib/httpapi/server.go b/lib/httpapi/server.go index 08d92c4..63ff9bb 100644 --- a/lib/httpapi/server.go +++ b/lib/httpapi/server.go @@ -29,17 +29,19 @@ import ( // Server represents the HTTP server type Server struct { - router chi.Router - api huma.API - port int - srv *http.Server - mu sync.RWMutex - logger *slog.Logger - conversation *st.Conversation - agentio *termexec.Process - agentType mf.AgentType - emitter *EventEmitter - chatBasePath string + router chi.Router + api huma.API + port int + srv *http.Server + mu sync.RWMutex + logger *slog.Logger + conversation *st.Conversation + agentio *termexec.Process + agentType mf.AgentType + emitter *EventEmitter + chatBasePath string + initialPrompt string + initialPromptSent bool } func (s *Server) NormalizeSchema(schema any) any { @@ -95,6 +97,7 @@ type ServerConfig struct { ChatBasePath string AllowedHosts []string AllowedOrigins []string + InitialPrompt string } // Validate allowed hosts don't contain whitespace, commas, schemes, or ports. @@ -233,15 +236,17 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) { }) emitter := NewEventEmitter(1024) s := &Server{ - router: router, - api: api, - port: config.Port, - conversation: conversation, - logger: logger, - agentio: config.Process, - agentType: config.AgentType, - emitter: emitter, - chatBasePath: strings.TrimSuffix(config.ChatBasePath, "/"), + router: router, + api: api, + port: config.Port, + conversation: conversation, + logger: logger, + agentio: config.Process, + agentType: config.AgentType, + emitter: emitter, + chatBasePath: strings.TrimSuffix(config.ChatBasePath, "/"), + initialPrompt: config.InitialPrompt, + initialPromptSent: len(config.InitialPrompt) == 0, } // Register API routes @@ -306,7 +311,19 @@ func (s *Server) StartSnapshotLoop(ctx context.Context) { s.conversation.StartSnapshotLoop(ctx) go func() { for { - s.emitter.UpdateStatusAndEmitChanges(s.conversation.Status()) + currentStatus := s.conversation.Status() + + // Send initial prompt when agent becomes stable for the first time + if !s.initialPromptSent && convertStatus(currentStatus) == AgentStatusStable { + if err := s.conversation.SendMessage(FormatMessage(s.agentType, s.initialPrompt)...); err != nil { + s.logger.Error("Failed to send initial prompt", "error", err) + } else { + s.initialPromptSent = true + currentStatus = st.ConversationStatusChanging + s.logger.Info("Initial prompt sent successfully") + } + } + s.emitter.UpdateStatusAndEmitChanges(currentStatus) s.emitter.UpdateMessagesAndEmitChanges(s.conversation.Messages()) s.emitter.UpdateScreenAndEmitChanges(s.conversation.Screen()) time.Sleep(snapshotInterval) From db3af7b3cb264b5cef38004c4e862a999c45fec4 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Thu, 2 Oct 2025 15:21:55 +0530 Subject: [PATCH 2/2] feat: move initial prompt to conversation --- lib/httpapi/server.go | 52 ++++++++++++-------------- lib/screentracker/conversation.go | 8 +++- lib/screentracker/conversation_test.go | 4 +- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/lib/httpapi/server.go b/lib/httpapi/server.go index 63ff9bb..11994f0 100644 --- a/lib/httpapi/server.go +++ b/lib/httpapi/server.go @@ -29,19 +29,17 @@ import ( // Server represents the HTTP server type Server struct { - router chi.Router - api huma.API - port int - srv *http.Server - mu sync.RWMutex - logger *slog.Logger - conversation *st.Conversation - agentio *termexec.Process - agentType mf.AgentType - emitter *EventEmitter - chatBasePath string - initialPrompt string - initialPromptSent bool + router chi.Router + api huma.API + port int + srv *http.Server + mu sync.RWMutex + logger *slog.Logger + conversation *st.Conversation + agentio *termexec.Process + agentType mf.AgentType + emitter *EventEmitter + chatBasePath string } func (s *Server) NormalizeSchema(schema any) any { @@ -233,20 +231,18 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) { SnapshotInterval: snapshotInterval, ScreenStabilityLength: 2 * time.Second, FormatMessage: formatMessage, - }) + }, config.InitialPrompt) emitter := NewEventEmitter(1024) s := &Server{ - router: router, - api: api, - port: config.Port, - conversation: conversation, - logger: logger, - agentio: config.Process, - agentType: config.AgentType, - emitter: emitter, - chatBasePath: strings.TrimSuffix(config.ChatBasePath, "/"), - initialPrompt: config.InitialPrompt, - initialPromptSent: len(config.InitialPrompt) == 0, + router: router, + api: api, + port: config.Port, + conversation: conversation, + logger: logger, + agentio: config.Process, + agentType: config.AgentType, + emitter: emitter, + chatBasePath: strings.TrimSuffix(config.ChatBasePath, "/"), } // Register API routes @@ -314,11 +310,11 @@ func (s *Server) StartSnapshotLoop(ctx context.Context) { currentStatus := s.conversation.Status() // Send initial prompt when agent becomes stable for the first time - if !s.initialPromptSent && convertStatus(currentStatus) == AgentStatusStable { - if err := s.conversation.SendMessage(FormatMessage(s.agentType, s.initialPrompt)...); err != nil { + if !s.conversation.InitialPromptSent && convertStatus(currentStatus) == AgentStatusStable { + if err := s.conversation.SendMessage(FormatMessage(s.agentType, s.conversation.InitialPrompt)...); err != nil { s.logger.Error("Failed to send initial prompt", "error", err) } else { - s.initialPromptSent = true + s.conversation.InitialPromptSent = true currentStatus = st.ConversationStatusChanging s.logger.Info("Initial prompt sent successfully") } diff --git a/lib/screentracker/conversation.go b/lib/screentracker/conversation.go index 7777e04..4617e8e 100644 --- a/lib/screentracker/conversation.go +++ b/lib/screentracker/conversation.go @@ -74,6 +74,10 @@ type Conversation struct { messages []ConversationMessage screenBeforeLastUserMessage string lock sync.Mutex + // InitialPrompt is the initial prompt passed to the agent + InitialPrompt string + // InitialPromptSent keeps track if the InitialPrompt has been successfully sent to the agents + InitialPromptSent bool } type ConversationStatus string @@ -94,7 +98,7 @@ func getStableSnapshotsThreshold(cfg ConversationConfig) int { return threshold + 1 } -func NewConversation(ctx context.Context, cfg ConversationConfig) *Conversation { +func NewConversation(ctx context.Context, cfg ConversationConfig, initialPrompt string) *Conversation { threshold := getStableSnapshotsThreshold(cfg) c := &Conversation{ cfg: cfg, @@ -107,6 +111,8 @@ func NewConversation(ctx context.Context, cfg ConversationConfig) *Conversation Time: cfg.GetTime(), }, }, + InitialPrompt: initialPrompt, + InitialPromptSent: len(initialPrompt) == 0, } return c } diff --git a/lib/screentracker/conversation_test.go b/lib/screentracker/conversation_test.go index 53c77fd..92fe5ac 100644 --- a/lib/screentracker/conversation_test.go +++ b/lib/screentracker/conversation_test.go @@ -42,7 +42,7 @@ func statusTest(t *testing.T, params statusTestParams) { if params.cfg.GetTime == nil { params.cfg.GetTime = func() time.Time { return time.Now() } } - c := st.NewConversation(ctx, params.cfg) + c := st.NewConversation(ctx, params.cfg, "") assert.Equal(t, st.ConversationStatusInitializing, c.Status()) for i, step := range params.steps { @@ -147,7 +147,7 @@ func TestMessages(t *testing.T) { for _, opt := range opts { opt(&cfg) } - return st.NewConversation(context.Background(), cfg) + return st.NewConversation(context.Background(), cfg, "") } t.Run("messages are copied", func(t *testing.T) {