package tools import ( "context" "fmt" "strings" ) // ContextReport is what the agent knows about its own budget. It is a plain // struct so this package stays free of any dependency on the agent: the loop // hands in a closure, and the formatting lives here with the other tools. type ContextReport struct { // UsedTokens is the size of the last request — the whole conversation as // the model saw it, which is what the window is actually spent on. UsedTokens int // WindowTokens is the model's context window. WindowTokens int // CompactAt is the fraction at which the conversation gets folded into a // summary, so the model can see how much room it has before that happens. CompactAt float64 // Turns is how many model turns this session has taken. Turns int // CostUSD is the session's spend so far, and CostKnown reports whether // this model's pricing is known at all. A made-up number would be worse // than none. CostUSD float64 CostKnown bool } // NewContextStatus builds the context_status tool. The model cannot otherwise // see what it is spending: it knows neither how full the window is nor how // many turns it has taken, and both change how it should work — a task that // has room can be done thoroughly, one that is close to the fold should be // finished and reported before the summary loses the detail. func NewContextStatus(report func() ContextReport) Tool { if report == nil { return nil } return New("context_status", strings.TrimSpace(` Report how full the context window is, how many turns this session has taken and what it has cost. Check it before starting a long stretch of work, and when deciding whether to finish and report rather than keep going.`), func(_ context.Context, _ struct{}) (string, error) { return formatContext(report()), nil }) } func formatContext(r ContextReport) string { var b strings.Builder if r.WindowTokens > 0 { pct := float64(r.UsedTokens) * 100 / float64(r.WindowTokens) fmt.Fprintf(&b, "Context: %s of %s tokens (%.0f%% full), %s left.", thousands(r.UsedTokens), thousands(r.WindowTokens), pct, thousands(max(r.WindowTokens-r.UsedTokens, 0))) if r.CompactAt > 0 { at := int(r.CompactAt * float64(r.WindowTokens)) if r.UsedTokens >= at { b.WriteString("\nThe next turn may fold the older half of this conversation into a summary.") } else { fmt.Fprintf(&b, "\nThat fold happens at %s tokens, %s away.", thousands(at), thousands(at-r.UsedTokens)) } } } else { fmt.Fprintf(&b, "Context: %s tokens used; the window size is not known.", thousands(r.UsedTokens)) } fmt.Fprintf(&b, "\nTurns so far: %d.", r.Turns) if r.CostKnown { fmt.Fprintf(&b, " Cost so far: $%.2f.", r.CostUSD) } // Said plainly because it is the thing the number is for: the whole // conversation is re-sent every turn, so turn count is the lever. b.WriteString("\nEvery turn re-sends the whole conversation, so fewer, fuller turns cost less than many small ones.") return b.String() } // thousands groups an integer for reading: 970000 becomes 970,000. func thousands(n int) string { if n < 0 { return "0" } s := fmt.Sprintf("%d", n) for i := len(s) - 3; i > 0; i -= 3 { s = s[:i] + "," + s[i:] } return s }