打通前端 ws.ts 里那批死消费者事件(thinking/mood/tool_used/subagent_event/
task_output/device_update/reminder/backthink),并给 loop 接真 token 流式。
- wire.go:loop 建时传 OnStream(streamChunk 闭包把 delta 发 bus ws.stream_chunk);
gateway.Deps 注入 Scheduler/Tasks/Streaming;iot 接共享总线 iot.WithBus(a.bus)。
- ws_bridge.go:hub 额外订阅 iot.>/task.>/automation.>/reminder.>/cyrene.agent.*.event,
normalizeEvent 归一为 ws.*(device_update/task_output/task_done/automation_result/
reminder/subagent_event);不订阅 cyrene.agent.> 以免 ask/reply 内部事件进 relay。
- ws_route.go:voice_input→ws.voice_input;subagent_ask→Scheduler.AuditContext(真实
上下文审计,替换 pending stub);task_output_query→Manager.TaskOutput(真实输出,
替换空 {})。
- server.go:新增 SubagentAuditor/TaskOutputStore 接口 + Deps.Scheduler/Tasks/Streaming。
- orchestration/auditcontext.go:Scheduler.AuditContext(full/tail/keyword)。
- task/manager.go:Manager.TaskOutput(id)。
- backthink:record() 补发 ws.backthink 事件。
- json tag 修正(device_update/reminder/task output 结构体加小写 tag + DeviceUpdate
补 message),使前端分支能真正渲染。
实机验证:WS 握手 101、task manager ready、iot manager initialized(WithBus 生效)。
go build/vet/test 全绿;TestHubBridgeDeviceUpdate/TestHubBridgeSubagentProgress 覆盖
映射。dev 推至后续版本。
95 lines
3.2 KiB
Go
95 lines
3.2 KiB
Go
package orchestration
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration/audit"
|
|
)
|
|
|
|
// AuditMode selects how a sub-agent context is shaped for the subagent_ask
|
|
// upstream WS route. It mirrors the audit.ContextAuditor surface (Full/Tail/
|
|
// Keyword) so the gateway can request exactly the window the client asks for.
|
|
type AuditMode string
|
|
|
|
const (
|
|
// AuditFull returns the entire conversation context.
|
|
AuditFull AuditMode = "full"
|
|
// AuditTail returns the last n messages of the context.
|
|
AuditTail AuditMode = "tail"
|
|
// AuditKeyword returns the window around the first keyword match.
|
|
AuditKeyword AuditMode = "keyword"
|
|
)
|
|
|
|
// AuditResult is the read-only result of auditing a sub-agent's conversation
|
|
// context. Messages carries the selected window; Line and Match are populated for
|
|
// a keyword query. It is the wire payload for a ws.subagent_event reply.
|
|
type AuditResult struct {
|
|
AgentID string `json:"agentId"`
|
|
Status Status `json:"status"`
|
|
Goal string `json:"goal,omitempty"`
|
|
Mode AuditMode `json:"mode"`
|
|
Messages []model.Message `json:"messages,omitempty"`
|
|
Line int `json:"line,omitempty"`
|
|
Match string `json:"match,omitempty"`
|
|
}
|
|
|
|
// AuditContext returns a read-only view of sub-agent id's conversation context
|
|
// for the subagent_ask WS route. mode selects the window: AuditFull returns the
|
|
// whole context, AuditTail returns the last n messages, AuditKeyword returns the
|
|
// window around the first query hit. It wraps ErrAgentNotFound for an unknown id
|
|
// and audit.ErrQueryEmpty for a blank keyword query, both with %w so callers can
|
|
// classify with errors.Is.
|
|
func (s *Scheduler) AuditContext(ctx context.Context, id string, mode AuditMode, n int, query string) (*AuditResult, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, fmt.Errorf("orchestration: audit %q: %w", id, err)
|
|
}
|
|
s.mu.Lock()
|
|
sub := s.agents[id]
|
|
s.mu.Unlock()
|
|
if sub == nil {
|
|
return nil, fmt.Errorf("orchestration: audit %q: %w", id, ErrAgentNotFound)
|
|
}
|
|
|
|
a := audit.NewContextAuditor(sub.Context)
|
|
res := &AuditResult{AgentID: sub.ID, Status: sub.Status(), Goal: sub.Goal, Mode: mode}
|
|
switch mode {
|
|
case AuditFull:
|
|
res.Messages = a.Full()
|
|
case AuditTail:
|
|
res.Messages = a.Tail(n)
|
|
case AuditKeyword:
|
|
loc, err := a.Keyword(ctx, query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("orchestration: audit %q: %w", id, err)
|
|
}
|
|
if loc == nil {
|
|
res.Messages = []model.Message{}
|
|
break
|
|
}
|
|
res.Line = loc.Line
|
|
res.Messages = loc.Surrounding
|
|
res.Match = messageText(loc.Surrounding[len(loc.Surrounding)-1])
|
|
default:
|
|
return nil, fmt.Errorf("orchestration: audit %q: unknown mode %q", id, mode)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// messageText concatenates the text parts of one message. It mirrors the audit
|
|
// subpackage's own helper so the result payload can surface a human-readable
|
|
// match without exporting a cross-package text extractor.
|
|
func messageText(m model.Message) string {
|
|
var text string
|
|
for _, c := range m.Content {
|
|
if c.Type == model.ContentTypeText && c.Text != "" {
|
|
if text != "" {
|
|
text += " "
|
|
}
|
|
text += c.Text
|
|
}
|
|
}
|
|
return text
|
|
}
|