mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 07:43:58 +00:00
fix: preserve interrupt source metadata without duplicate processing
- annotate queued and interrupt inputs with source/output channel context - drain all pending interrupts instead of only one - avoid double-processing by routing interrupts exclusively: active LLM -> intercept channel, idle -> input queue - keep interrupt source/channel in payload and system context
This commit is contained in:
@ -84,7 +84,7 @@ type Agent struct {
|
|||||||
childResults map[string]string
|
childResults map[string]string
|
||||||
|
|
||||||
// 高优先级打断通道:interceptLoop 注入,process() 在工具循环轮次间非阻塞读取
|
// 高优先级打断通道:interceptLoop 注入,process() 在工具循环轮次间非阻塞读取
|
||||||
interceptCh chan string
|
interceptCh chan *agentIO.InputEvent
|
||||||
|
|
||||||
// 进行中的 LLM 请求取消函数,interceptLoop 可调用以在请求中打断
|
// 进行中的 LLM 请求取消函数,interceptLoop 可调用以在请求中打断
|
||||||
cancelLLM context.CancelFunc
|
cancelLLM context.CancelFunc
|
||||||
@ -166,7 +166,7 @@ func New(cfg AgentConfig) *Agent {
|
|||||||
eventBus: cfg.EventBus,
|
eventBus: cfg.EventBus,
|
||||||
selfInputCh: make(chan string, 64),
|
selfInputCh: make(chan string, 64),
|
||||||
childResults: make(map[string]string),
|
childResults: make(map[string]string),
|
||||||
interceptCh: make(chan string, 64),
|
interceptCh: make(chan *agentIO.InputEvent, 64),
|
||||||
thinkingEnabled: cfg.ThinkingEnabled,
|
thinkingEnabled: cfg.ThinkingEnabled,
|
||||||
inputCfg: cfg.InputProcessing,
|
inputCfg: cfg.InputProcessing,
|
||||||
}
|
}
|
||||||
@ -225,27 +225,52 @@ func (a *Agent) interceptLoop() {
|
|||||||
if text == "" {
|
if text == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Printf("[agent] interrupt from %s: %s", evt.Source, truncateStr(text, 80))
|
log.Printf("[agent] interrupt from %s/%s: %s", evt.Source, evt.OutputChannel, truncateStr(text, 80))
|
||||||
|
|
||||||
// (a) 直接取消进行中的 LLM 请求
|
clone := &agentIO.InputEvent{
|
||||||
|
RequestID: evt.RequestID,
|
||||||
|
Source: evt.Source,
|
||||||
|
Type: evt.Type,
|
||||||
|
Payload: map[string]interface{}{},
|
||||||
|
OutputChannel: evt.OutputChannel,
|
||||||
|
}
|
||||||
|
for k, v := range evt.Payload {
|
||||||
|
clone.Payload[k] = v
|
||||||
|
}
|
||||||
|
clone.Payload["interrupt"] = true
|
||||||
|
clone.Payload["interrupt_source"] = evt.Source
|
||||||
|
clone.Payload["interrupt_channel"] = evt.OutputChannel
|
||||||
|
|
||||||
|
// 若当前有进行中的 LLM 请求,则打断并走 interceptCh;否则直接回注入普通输入队列。
|
||||||
a.llmMu.Lock()
|
a.llmMu.Lock()
|
||||||
if a.cancelLLM != nil {
|
hasActiveLLM := a.cancelLLM != nil
|
||||||
|
if hasActiveLLM {
|
||||||
a.cancelLLM()
|
a.cancelLLM()
|
||||||
log.Printf("[agent] LLM request cancelled by interrupt")
|
log.Printf("[agent] LLM request cancelled by interrupt")
|
||||||
}
|
}
|
||||||
a.llmMu.Unlock()
|
a.llmMu.Unlock()
|
||||||
|
|
||||||
// 注入拦截通道 — process() 在工具循环中非阻塞读取
|
if hasActiveLLM {
|
||||||
select {
|
select {
|
||||||
case a.interceptCh <- text:
|
case a.interceptCh <- clone:
|
||||||
default:
|
default:
|
||||||
|
log.Printf("[agent] intercept channel full, queuing input for %s", evt.Source)
|
||||||
|
a.io.InjectInputTo(evt.Source, evt.OutputChannel, "text", map[string]interface{}{
|
||||||
|
"content": text,
|
||||||
|
"interrupt": true,
|
||||||
|
"interrupt_source": evt.Source,
|
||||||
|
"interrupt_channel": evt.OutputChannel,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
a.io.InjectInputTo(evt.Source, evt.OutputChannel, "text", map[string]interface{}{
|
||||||
|
"content": text,
|
||||||
|
"interrupt": true,
|
||||||
|
"interrupt_source": evt.Source,
|
||||||
|
"interrupt_channel": evt.OutputChannel,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// (b) 投递为新输入 — 代理空闲时 eventLoop 会消费
|
|
||||||
a.io.InjectInput("interrupt", "text", map[string]interface{}{
|
|
||||||
"content": fmt.Sprintf("[interrupt] %s: %s", evt.Source, text),
|
|
||||||
})
|
|
||||||
|
|
||||||
case <-a.ctx.Done():
|
case <-a.ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -309,9 +334,12 @@ func (a *Agent) processMediaInput(evt *agentIO.InputEvent) {
|
|||||||
// stage 上下文携带 blocks,process() 会将其附着到 user message 上
|
// stage 上下文携带 blocks,process() 会将其附着到 user message 上
|
||||||
stageCtx := a.stageCtxFromInput(fallback, evt.Source, "")
|
stageCtx := a.stageCtxFromInput(fallback, evt.Source, "")
|
||||||
stageCtx.Extra = map[string]interface{}{
|
stageCtx.Extra = map[string]interface{}{
|
||||||
"media_blocks": blocks,
|
"media_blocks": blocks,
|
||||||
"media_type": evt.Type,
|
"media_type": evt.Type,
|
||||||
|
"input_source": evt.Source,
|
||||||
|
"output_channel": evt.OutputChannel,
|
||||||
}
|
}
|
||||||
|
a.injectSourceContext(stageCtx, evt)
|
||||||
|
|
||||||
a.publishEvent(events.EventRawInput, map[string]interface{}{
|
a.publishEvent(events.EventRawInput, map[string]interface{}{
|
||||||
"content": evt.Payload,
|
"content": evt.Payload,
|
||||||
@ -429,15 +457,13 @@ func (a *Agent) processTextInput(evt *agentIO.InputEvent, input string) {
|
|||||||
|
|
||||||
// === Stage: on_input — 消息到达,插件可拦截 ===
|
// === Stage: on_input — 消息到达,插件可拦截 ===
|
||||||
stageCtx := a.stageCtxFromInput(input, evt.Source, "")
|
stageCtx := a.stageCtxFromInput(input, evt.Source, "")
|
||||||
stageCtx.NoMemory = noMemory
|
stageCtx.Extra["input_source"] = evt.Source
|
||||||
a.publishEvent(events.EventRawInput, map[string]interface{}{
|
stageCtx.Extra["output_channel"] = evt.OutputChannel
|
||||||
"content": input,
|
if noMemory {
|
||||||
"source": evt.Source,
|
stageCtx.NoMemory = true
|
||||||
})
|
|
||||||
if a.runStage(sdk.StageOnInput, stageCtx) {
|
|
||||||
a.emitResponse(evt, *stageCtx.Response)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
a.injectSourceContext(stageCtx, evt)
|
||||||
|
|
||||||
input = stageCtx.RawMessage
|
input = stageCtx.RawMessage
|
||||||
|
|
||||||
a.context.Append(ContextEvent{
|
a.context.Append(ContextEvent{
|
||||||
@ -573,14 +599,14 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri
|
|||||||
|
|
||||||
for turn := 0; ; turn++ {
|
for turn := 0; ; turn++ {
|
||||||
// === 高优先级打断:每次 LLM 调用前检查拦截通道 ===
|
// === 高优先级打断:每次 LLM 调用前检查拦截通道 ===
|
||||||
if text := a.drainInterrupt(); text != "" {
|
for _, interrupt := range a.drainInterrupts() {
|
||||||
msgs = append(msgs, agentAPI.Message{
|
msgs = append(msgs, agentAPI.Message{
|
||||||
Role: "system",
|
Role: "system",
|
||||||
Content: fmt.Sprintf("[打断消息] 用户发来一条紧急消息,请优先处理:\n%s", text),
|
Content: interrupt,
|
||||||
})
|
})
|
||||||
log.Printf("[agent] interrupt injected before LLM call (turn %d)", turn)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
eb := map[string]interface{}{}
|
eb := map[string]interface{}{}
|
||||||
if !a.thinkingEnabled {
|
if !a.thinkingEnabled {
|
||||||
eb["thinking"] = map[string]interface{}{"type": "disabled"}
|
eb["thinking"] = map[string]interface{}{"type": "disabled"}
|
||||||
@ -2551,13 +2577,52 @@ func truncateStr(s string, max int) string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// drainInterrupt 非阻塞读取 interceptCh 中的一条打断消息。
|
func (a *Agent) injectSourceContext(stageCtx *sdk.StageContext, evt *agentIO.InputEvent) {
|
||||||
// 若有多条,只取最先到达的一条(丢弃后续)。
|
if stageCtx == nil || evt == nil {
|
||||||
func (a *Agent) drainInterrupt() string {
|
return
|
||||||
select {
|
}
|
||||||
case text := <-a.interceptCh:
|
source := evt.Source
|
||||||
return text
|
if source == "" {
|
||||||
default:
|
source = "unknown"
|
||||||
return ""
|
}
|
||||||
|
channel := evt.OutputChannel
|
||||||
|
if channel == "" {
|
||||||
|
channel = source
|
||||||
|
}
|
||||||
|
content := fmt.Sprintf("当前输入来源: %s;默认输出通道: %s。", source, channel)
|
||||||
|
if flag, _ := evt.Payload["interrupt"].(bool); flag {
|
||||||
|
content = fmt.Sprintf("这是一条打断输入。来源: %s;默认输出通道: %s。", source, channel)
|
||||||
|
}
|
||||||
|
stageCtx.ContextMsgs = append(stageCtx.ContextMsgs, map[string]interface{}{
|
||||||
|
"role": "system",
|
||||||
|
"content": content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// drainInterrupts 非阻塞读取 interceptCh 中全部待处理打断消息,逐条保留来源信息。
|
||||||
|
func (a *Agent) drainInterrupts() []string {
|
||||||
|
var out []string
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case evt := <-a.interceptCh:
|
||||||
|
if evt == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
text, _ := evt.Payload["content"].(string)
|
||||||
|
if text == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
source := evt.Source
|
||||||
|
if source == "" {
|
||||||
|
source = "unknown"
|
||||||
|
}
|
||||||
|
channel := evt.OutputChannel
|
||||||
|
if channel == "" {
|
||||||
|
channel = source
|
||||||
|
}
|
||||||
|
out = append(out, fmt.Sprintf("[打断消息][来源:%s][输出通道:%s] %s", source, channel, text))
|
||||||
|
default:
|
||||||
|
return out
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user