diff --git a/pkg/agent/core.go b/pkg/agent/core.go index b93e8aa..bee6728 100644 --- a/pkg/agent/core.go +++ b/pkg/agent/core.go @@ -389,12 +389,20 @@ func (s *agentCore) Interrupt(cause error) bool { } func (s *agentCore) Inject(prompt string) { - s.pendingInject.Store(&prompt) - // Cancel the current action so blocking tools (execute_code, etc.) - // are killed immediately and the model sees the interruption. - if cancel := s.getActionCancel(); cancel != nil { - cancel(ErrInterrupted) + // If an inject is already pending, fall back to the normal FIFO so nothing + // is lost. Use CompareAndSwap to avoid a race between the nil check and store. + if !s.pendingInject.CompareAndSwap(nil, &prompt) { + s.fifo.Push(prompt) + return } + emit(s.loopcfg, Event{Role: "info", Content: "\n"}) + emit(s.loopcfg, Event{Role: "user", Content: prompt}) +} + +func (s *agentCore) injectRewrite(prompt string) { + s.pendingInject.Store(&prompt) + emit(s.loopcfg, Event{Role: "info", Content: "\n"}) + emit(s.loopcfg, Event{Role: "user", Content: prompt}) } func (s *agentCore) Queue(prompt string) { @@ -487,12 +495,18 @@ func firstSentence(s string) string { // Submit implements Core. It processes one line of user input: slash commands // and shell shortcuts are dispatched immediately via handler; any other input -// starts an agent turn that streams events to handler. +// starts an agent turn that streams events to handler. If a turn is already +// in progress the prompt is queued as an in-stream interruption instead. func (s *agentCore) Submit(ctx context.Context, input string, handler EventHandler) { if s.handleCommand(ctx, input, handler) { return } + if s.IsRunning() { + s.Inject(input) + return + } + handler(Event{Role: "user", Content: input}) hookResult := s.hooks.Run(ctx, HookUserPromptSubmit, map[string]string{ @@ -635,6 +649,15 @@ func (s *agentCore) handleCommand(ctx context.Context, input string, handler Eve args := parts[1:] switch cmd { + case "/irw": + prompt := strings.Join(args, " ") + if prompt == "" { + handler(infoEvent("error: /irw requires a prompt")) + return true + } + s.injectRewrite(prompt) + return true + case "/backend": if len(args) == 0 { handler(infoEvent("error: /backend requires an argument (e.g., /backend ollama)")) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index e743245..c1eb572 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -151,6 +151,11 @@ func run(ctx context.Context, cfg loopConfig, state state) error { isErr = true // If cancelled during execution, fill remaining and break. if ctx.Err() != nil { + if cfg.PopInject != nil { + if injected := cfg.PopInject(); injected != "" { + result += "\n\n\n" + injected + "\n" + } + } results = append(results, toolResult{ ToolCallID: tc.ID, Name: tc.Name, @@ -177,8 +182,7 @@ func run(ctx context.Context, cfg loopConfig, state state) error { } if !interrupted { - // Append any pending user interruption to the last tool result. - if i == len(toolCalls)-1 && cfg.PopInject != nil { + if cfg.PopInject != nil { if injected := cfg.PopInject(); injected != "" { result += "\n\n\n" + injected + "\n" }