agent: inject mid-turn prompts at tool boundaries instead of queueing
- Submit routes to Inject when a turn is already running, keeping the routing decision in core rather than the frontend or 9p layer - Inject no longer cancels the current action; the pending prompt is stored and consumed at the next tool result boundary (success or failure), matching goq interrupt semantics - If an inject is already pending, fall back to the FIFO so nothing is lost; /irw (interrupt-rewrite) explicitly overwrites the pending inject - PopInject is now called after every tool result, not just the last one - Injected prompts are echoed to the chat via the running turn's output Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
52999c8560
commit
ae82e0eff0
|
|
@ -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)"))
|
||||
|
|
|
|||
|
|
@ -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<system-user-interruption>\n" + injected + "\n</system-user-interruption>"
|
||||
}
|
||||
}
|
||||
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<system-user-interruption>\n" + injected + "\n</system-user-interruption>"
|
||||
}
|
||||
|
|
|
|||
Reference in New Issue