Reduce MCP token usage
This commit is contained in:
@@ -65,6 +65,15 @@ type toolHost struct {
|
||||
timers *timerManager
|
||||
}
|
||||
|
||||
const (
|
||||
defaultMCPContentBytes = 12_000
|
||||
maxMCPContentBytes = 65_536
|
||||
defaultMCPTailBytes = 8_000
|
||||
defaultScratchpadReadBytes = 12_000
|
||||
defaultSearchLineBytes = 2_000
|
||||
maxSearchMatches = 50
|
||||
)
|
||||
|
||||
func newToolHost(sess *Session, pads *scratchpad.Store, launcher *Launcher, presets preset.Set, tr *trust.Store, cols, rows uint16) *toolHost {
|
||||
h := &toolHost{
|
||||
sess: sess,
|
||||
@@ -353,8 +362,8 @@ func (h *toolHost) GetProcessStatus(callerID, processID string) (mcp.ProcessStat
|
||||
return st, nil
|
||||
}
|
||||
|
||||
func (h *toolHost) GetProjectStatus(callerID string) (mcp.ProjectStatus, error) {
|
||||
caller := h.WhoAmI(callerID)
|
||||
func (h *toolHost) GetProjectStatus(callerID string, includeTools bool) (mcp.ProjectStatus, error) {
|
||||
caller := h.WhoAmI(callerID, includeTools)
|
||||
processes := h.ListProcesses(callerID, "")
|
||||
pads, _ := h.pads.List()
|
||||
return mcp.ProjectStatus{
|
||||
@@ -365,7 +374,8 @@ func (h *toolHost) GetProjectStatus(callerID string) (mcp.ProjectStatus, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *toolHost) GetProcessOutput(callerID, processID, mode string, sinceOffset int64) (mcp.ProcessOutput, error) {
|
||||
func (h *toolHost) GetProcessOutput(callerID string, args mcp.ProcessOutputArgs) (mcp.ProcessOutput, error) {
|
||||
processID, mode, sinceOffset := args.ProcessID, args.Mode, args.SinceOffset
|
||||
c := h.sess.FindChild(processID)
|
||||
if c == nil {
|
||||
return mcp.ProcessOutput{}, mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", processID)
|
||||
@@ -399,11 +409,12 @@ func (h *toolHost) GetProcessOutput(callerID, processID, mode string, sinceOffse
|
||||
if c.Kind == KindAgent {
|
||||
txt = applyChromeTrim(txt, h.chromeHintsFor(c.PresetRef))
|
||||
}
|
||||
out.Content = normalizeGridText(txt)
|
||||
content := normalizeGridText(txt)
|
||||
out.Content, out.ContentBytes, out.Truncated, out.TruncatedBytes = capTextMiddle(content, capLimit(args.MaxBytes, defaultMCPContentBytes))
|
||||
return out, nil
|
||||
case "stream":
|
||||
b, end := c.StreamRead(sinceOffset)
|
||||
out.Content = string(stripANSIBytes(nil, b))
|
||||
out.Content, out.ContentBytes, out.Truncated, out.TruncatedBytes = capBytesTail(stripANSIBytes(nil, b), capLimit(args.MaxBytes, defaultMCPContentBytes))
|
||||
out.NewOffset = end
|
||||
return out, nil
|
||||
default:
|
||||
@@ -411,34 +422,46 @@ func (h *toolHost) GetProcessOutput(callerID, processID, mode string, sinceOffse
|
||||
}
|
||||
}
|
||||
|
||||
func (h *toolHost) GetProcessRawOutput(callerID, processID string, sinceOffset int64) (mcp.RawOutput, error) {
|
||||
c := h.sess.FindChild(processID)
|
||||
func (h *toolHost) GetProcessRawOutput(callerID string, args mcp.RawOutputArgs) (mcp.RawOutput, error) {
|
||||
c := h.sess.FindChild(args.ProcessID)
|
||||
if c == nil {
|
||||
return mcp.RawOutput{}, mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", processID)
|
||||
return mcp.RawOutput{}, mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", args.ProcessID)
|
||||
}
|
||||
b, end := c.StreamRead(sinceOffset)
|
||||
b, end := c.StreamRead(args.SinceOffset)
|
||||
content, contentBytes, truncated, truncatedBytes := capBytesTail(b, capLimit(args.MaxBytes, defaultMCPContentBytes))
|
||||
return mcp.RawOutput{
|
||||
Content: string(b),
|
||||
NewOffset: end,
|
||||
Status: string(c.Status()),
|
||||
Content: content,
|
||||
NewOffset: end,
|
||||
Status: string(c.Status()),
|
||||
ContentBytes: contentBytes,
|
||||
Truncated: truncated,
|
||||
TruncatedBytes: truncatedBytes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *toolHost) SearchOutput(callerID, processID, pattern, kind string, limit int) (mcp.SearchResult, error) {
|
||||
c := h.sess.FindChild(processID)
|
||||
func (h *toolHost) SearchOutput(callerID string, args mcp.SearchOutputArgs) (mcp.SearchResult, error) {
|
||||
c := h.sess.FindChild(args.ProcessID)
|
||||
if c == nil {
|
||||
return mcp.SearchResult{}, mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", processID)
|
||||
return mcp.SearchResult{}, mcp.Errorf(mcp.ErrorKindNotFound, "no such process %q", args.ProcessID)
|
||||
}
|
||||
re, err := regexp.Compile(pattern)
|
||||
re, err := regexp.Compile(args.Pattern)
|
||||
if err != nil {
|
||||
return mcp.SearchResult{}, mcp.Errorf(mcp.ErrorKindInvalidArgs, "regex: %v", err)
|
||||
}
|
||||
b, _ := c.StreamRead(0)
|
||||
if kind == "rendered" {
|
||||
if args.Kind == "rendered" {
|
||||
b = stripANSIBytes(nil, b)
|
||||
}
|
||||
text := string(b)
|
||||
lines := strings.Split(text, "\n")
|
||||
limit := args.Limit
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
if limit > maxSearchMatches {
|
||||
limit = maxSearchMatches
|
||||
}
|
||||
lineLimit := capLimit(args.MaxBytes, defaultSearchLineBytes)
|
||||
matches := make([]mcp.SearchMatch, 0, limit)
|
||||
truncated := false
|
||||
for i, line := range lines {
|
||||
@@ -447,6 +470,8 @@ func (h *toolHost) SearchOutput(callerID, processID, pattern, kind string, limit
|
||||
truncated = true
|
||||
break
|
||||
}
|
||||
line, _, lineTruncated, _ := capTextTail(line, lineLimit)
|
||||
truncated = truncated || lineTruncated
|
||||
matches = append(matches, mcp.SearchMatch{LineNo: i + 1, Text: line})
|
||||
}
|
||||
}
|
||||
@@ -588,6 +613,7 @@ func (h *toolHost) SendInput(callerID string, args mcp.SendInputArgs) (mcp.SendI
|
||||
if err != nil {
|
||||
return mcp.SendInputResult{}, err
|
||||
}
|
||||
tailSince := c.StreamOffset()
|
||||
if err := c.InjectAsOrchestrator(payload); err != nil {
|
||||
return mcp.SendInputResult{}, err
|
||||
}
|
||||
@@ -599,7 +625,12 @@ func (h *toolHost) SendInput(callerID string, args mcp.SendInputArgs) (mcp.SendI
|
||||
}
|
||||
if mode != "none" {
|
||||
time.Sleep(time.Duration(args.WaitMS) * time.Millisecond)
|
||||
tail, err := h.GetProcessOutput(callerID, args.ProcessID, mode, 0)
|
||||
tail, err := h.GetProcessOutput(callerID, mcp.ProcessOutputArgs{
|
||||
ProcessID: args.ProcessID,
|
||||
Mode: mode,
|
||||
SinceOffset: tailSince,
|
||||
MaxBytes: capLimit(args.TailMaxBytes, defaultMCPTailBytes),
|
||||
})
|
||||
if err == nil {
|
||||
res.Tail = &tail
|
||||
}
|
||||
@@ -813,8 +844,30 @@ func (h *toolHost) TimerList(callerID string) ([]mcp.TimerInfo, error) {
|
||||
|
||||
func (h *toolHost) ScratchpadList() ([]scratchpad.Entry, error) { return h.pads.List() }
|
||||
|
||||
func (h *toolHost) ScratchpadRead(name string) (string, string, error) {
|
||||
return h.pads.Read(name)
|
||||
func (h *toolHost) ScratchpadRead(args mcp.ScratchpadReadArgs) (mcp.ScratchpadReadResult, error) {
|
||||
content, rev, err := h.pads.Read(args.Name)
|
||||
if err != nil {
|
||||
return mcp.ScratchpadReadResult{}, err
|
||||
}
|
||||
offset := args.Offset
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
if offset > len(content) {
|
||||
offset = len(content)
|
||||
}
|
||||
limited, contentBytes, truncated, truncatedBytes := capTextHead(content[offset:], capLimit(args.MaxBytes, defaultScratchpadReadBytes))
|
||||
next := offset + contentBytes
|
||||
return mcp.ScratchpadReadResult{
|
||||
Content: limited,
|
||||
Revision: rev,
|
||||
Offset: offset,
|
||||
NextOffset: next,
|
||||
ContentBytes: contentBytes,
|
||||
TotalBytes: len(content),
|
||||
Truncated: truncated,
|
||||
TruncatedBytes: truncatedBytes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *toolHost) ScratchpadWrite(name, content, expectedRevision string) (string, error) {
|
||||
@@ -841,7 +894,7 @@ func (h *toolHost) ScratchpadDelete(name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *toolHost) WhoAmI(callerID string) mcp.WhoAmI {
|
||||
func (h *toolHost) WhoAmI(callerID string, includeTools bool) mcp.WhoAmI {
|
||||
w := mcp.WhoAmI{
|
||||
ProcessID: callerID,
|
||||
Role: h.CallerRole(callerID),
|
||||
@@ -849,7 +902,9 @@ func (h *toolHost) WhoAmI(callerID string) mcp.WhoAmI {
|
||||
Path: h.sess.projectDir,
|
||||
Key: h.sess.projectKey,
|
||||
},
|
||||
AvailableTools: availableToolsForRole(h.CallerRole(callerID)),
|
||||
}
|
||||
if includeTools {
|
||||
w.AvailableTools = availableToolsForRole(h.CallerRole(callerID))
|
||||
}
|
||||
if c := h.sess.FindChild(callerID); c != nil {
|
||||
w.Name = c.DisplayName()
|
||||
@@ -1043,6 +1098,51 @@ func normalizeGridText(s string) string {
|
||||
return strings.Join(out, "\n")
|
||||
}
|
||||
|
||||
func capLimit(requested, def int) int {
|
||||
if requested <= 0 {
|
||||
requested = def
|
||||
}
|
||||
if requested > maxMCPContentBytes {
|
||||
requested = maxMCPContentBytes
|
||||
}
|
||||
if requested < 0 {
|
||||
return 0
|
||||
}
|
||||
return requested
|
||||
}
|
||||
|
||||
func capBytesTail(b []byte, limit int) (string, int, bool, int) {
|
||||
if limit <= 0 || len(b) <= limit {
|
||||
return string(b), len(b), false, 0
|
||||
}
|
||||
dropped := len(b) - limit
|
||||
return string(b[dropped:]), limit, true, dropped
|
||||
}
|
||||
|
||||
func capTextTail(s string, limit int) (string, int, bool, int) {
|
||||
return capBytesTail([]byte(s), limit)
|
||||
}
|
||||
|
||||
func capTextHead(s string, limit int) (string, int, bool, int) {
|
||||
if limit <= 0 || len(s) <= limit {
|
||||
return s, len(s), false, 0
|
||||
}
|
||||
return s[:limit], limit, true, len(s) - limit
|
||||
}
|
||||
|
||||
func capTextMiddle(s string, limit int) (string, int, bool, int) {
|
||||
if limit <= 0 || len(s) <= limit {
|
||||
return s, len(s), false, 0
|
||||
}
|
||||
const marker = "\n...[truncated]...\n"
|
||||
if limit <= len(marker)+2 {
|
||||
return s[len(s)-limit:], limit, true, len(s) - limit
|
||||
}
|
||||
head := (limit - len(marker)) / 2
|
||||
tail := limit - len(marker) - head
|
||||
return s[:head] + marker + s[len(s)-tail:], limit, true, len(s) - limit
|
||||
}
|
||||
|
||||
// stripANSIBytes is the byte-slice form of stripANSI. Skips the
|
||||
// string conversion and the regex DFA — useful when the caller will
|
||||
// itself walk the result line-by-line (SearchOutput) or feed it to a
|
||||
|
||||
Reference in New Issue
Block a user