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"
}