feat(orchestration/audit): 子代理上下文可审计查询(全量/尾部/关键词定位)+ 主代理代审授权(审计日志+小字)
This commit is contained in:
1 parent
71697ca4f8
commit
8b723009f9
5 files changed
+658
No files matched your search
@@ -0,0 +1,122 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
)
|
||||
|
||||
// ErrQueryEmpty is returned by ContextAuditor.Keyword when the query is empty
|
||||
// or blank. Use errors.Is to test for it.
|
||||
var ErrQueryEmpty = errors.New("audit: query is empty")
|
||||
|
||||
// contextRadius is how many messages on each side of a keyword hit are included
|
||||
// in the surrounding window (one above and one below).
|
||||
const contextRadius = 1
|
||||
|
||||
// Located describes a single keyword match within a subagent context.
|
||||
type Located struct {
|
||||
// Line is the 1-based index of the matched message in the full context.
|
||||
Line int
|
||||
// Surrounding is a window centered on the matched message: the message
|
||||
// immediately before it, the matched message, and the message immediately
|
||||
// after it, clamped at the context boundaries. It always contains the
|
||||
// matched message.
|
||||
Surrounding []model.Message
|
||||
}
|
||||
|
||||
// ContextAuditor provides read-only, traceable access to a subagent's context.
|
||||
// It snapshots the message list at construction so later mutation by the caller
|
||||
// does not change what the auditor reports. It is a value type that holds no
|
||||
// storage or policy; the orchestral layer decides which context to inspect.
|
||||
type ContextAuditor struct {
|
||||
messages []model.Message
|
||||
}
|
||||
|
||||
// NewContextAuditor snapshots messages so subsequent caller mutations do not
|
||||
// affect the auditor. A nil input is allowed and yields an empty context.
|
||||
func NewContextAuditor(messages []model.Message) ContextAuditor {
|
||||
cp := make([]model.Message, len(messages))
|
||||
copy(cp, messages)
|
||||
return ContextAuditor{messages: cp}
|
||||
}
|
||||
|
||||
// Full returns a defensive copy of the entire context.
|
||||
func (a ContextAuditor) Full() []model.Message {
|
||||
cp := make([]model.Message, len(a.messages))
|
||||
copy(cp, a.messages)
|
||||
return cp
|
||||
}
|
||||
|
||||
// Tail returns the last n messages of the context in their original order.
|
||||
//
|
||||
// n <= 0 yields an empty, non-nil slice; n >= len returns a copy of the full
|
||||
// context. The result is a defensive copy.
|
||||
func (a ContextAuditor) Tail(n int) []model.Message {
|
||||
if n <= 0 {
|
||||
return []model.Message{}
|
||||
}
|
||||
if n >= len(a.messages) {
|
||||
return a.Full()
|
||||
}
|
||||
start := len(a.messages) - n
|
||||
out := make([]model.Message, n)
|
||||
copy(out, a.messages[start:])
|
||||
return out
|
||||
}
|
||||
|
||||
// Keyword locates the first message whose text contains query, using a
|
||||
// case-insensitive substring match against the message's text parts (image
|
||||
// payloads are ignored). It returns the message's 1-based index and a
|
||||
// surrounding window, or nil, nil when there is no match.
|
||||
//
|
||||
// It returns ErrQueryEmpty for a blank query and wraps ctx.Err() when ctx is
|
||||
// canceled. Only the earliest match is reported.
|
||||
func (a ContextAuditor) Keyword(ctx context.Context, query string) (*Located, error) {
|
||||
if strings.TrimSpace(query) == "" {
|
||||
return nil, fmt.Errorf("%w: query is blank", ErrQueryEmpty)
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, fmt.Errorf("audit: keyword: %w", err)
|
||||
}
|
||||
q := strings.ToLower(query)
|
||||
for i, m := range a.messages {
|
||||
if strings.Contains(strings.ToLower(messageText(m)), q) {
|
||||
return &Located{Line: i + 1, Surrounding: a.window(i)}, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// window returns up to 2*contextRadius+1 messages centered on index i, clamped
|
||||
// to the context boundaries.
|
||||
func (a ContextAuditor) window(i int) []model.Message {
|
||||
lo := i - contextRadius
|
||||
hi := i + contextRadius
|
||||
if lo < 0 {
|
||||
lo = 0
|
||||
}
|
||||
if hi >= len(a.messages) {
|
||||
hi = len(a.messages) - 1
|
||||
}
|
||||
if lo > hi {
|
||||
return nil
|
||||
}
|
||||
out := make([]model.Message, 0, hi-lo+1)
|
||||
out = append(out, a.messages[lo:hi+1]...)
|
||||
return out
|
||||
}
|
||||
|
||||
// messageText returns the concatenated text of a message's text parts.
|
||||
func messageText(m model.Message) string {
|
||||
var b strings.Builder
|
||||
for _, c := range m.Content {
|
||||
if c.Type == model.ContentTypeText && c.Text != "" {
|
||||
b.WriteString(c.Text)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
Reference in new issue
Block a user