81 lines
3.2 KiB
Go
81 lines
3.2 KiB
Go
package runtime
|
|
|
|
import agenttools "agent-native/runtime/tools"
|
|
|
|
// materialBudgetState describes only usage returned by the provider for a
|
|
// material subagent. A response with no reported usage cannot authorize a
|
|
// further request. LastPrompt carries the size estimate of the next provider
|
|
// prompt, which is the previous prompt plus the previous completion because
|
|
// the next request always resends the prior assistant output.
|
|
type materialBudgetState struct {
|
|
Limit, Used, LastPrompt, OutputCap, SavedCheckpoints, Responses int
|
|
UsageKnown bool
|
|
}
|
|
|
|
// materialFinalDecision reserves room for an answer before allowing another
|
|
// checkpoint request. It is deliberately an estimate, not a billing limit for
|
|
// an already in-flight request.
|
|
func materialFinalDecision(s materialBudgetState) (finalOnly, stop bool) {
|
|
if s.Responses == 0 {
|
|
return false, false
|
|
}
|
|
if !s.UsageKnown {
|
|
return false, true
|
|
}
|
|
margin := s.LastPrompt / 4
|
|
if margin < 1024 {
|
|
margin = 1024
|
|
}
|
|
remaining := s.Limit - s.Used
|
|
if remaining <= s.LastPrompt+margin {
|
|
return false, true
|
|
}
|
|
reserve := s.LastPrompt + s.OutputCap + margin
|
|
return s.SavedCheckpoints >= 2 || remaining <= 2*reserve, false
|
|
}
|
|
|
|
const materialFinalOnlyNotice = "后续不再接受检查点:下一步直接用现有材料写出最终结论与未决项,不要再调用 publish_work_checkpoint。"
|
|
|
|
// materialOutputCap is the per-request output cap the material reserve
|
|
// assumes: the request cap clamped by models.chat.maxOutputTokens.
|
|
func materialOutputCap(rc RunCtx) int {
|
|
outputCap := outputTokenCap(rc.Profile().Budget, rc.Settings(), "models.chat.maxOutputTokens")
|
|
if configured := int(rc.Settings().GetNumber("models.chat.maxOutputTokens", 0)); configured > 0 && configured < outputCap {
|
|
outputCap = configured
|
|
}
|
|
return outputCap
|
|
}
|
|
|
|
// withFinalOnlyNotice announces the final-only transition on a tool result the
|
|
// model actually reads. The reserve can be reached by a round of ordinary tool
|
|
// calls, so the notice belongs on every result handed back after the decision
|
|
// flips — not only on checkpoint results. IsError is never changed.
|
|
func (st *subRun) withFinalOnlyNotice(res agenttools.Result) agenttools.Result {
|
|
if st.materialLimit <= 0 {
|
|
return res
|
|
}
|
|
if finalOnly, stop := st.materialDecision(); !finalOnly && !stop {
|
|
return res
|
|
}
|
|
if res.Content != "" {
|
|
res.Content += "\n\n"
|
|
}
|
|
res.Content += materialFinalOnlyNotice
|
|
return res
|
|
}
|
|
|
|
// materialDecision is the single source of the final-only/stop decision for
|
|
// both the next request and the checkpoint tool result. Only meaningful when
|
|
// materialLimit > 0. The next request always carries the previous assistant
|
|
// output, so the reserve estimates next-prompt size as
|
|
// LastPromptTokens+LastCompletionTokens rather than the raw provider prompt.
|
|
func (st *subRun) materialDecision() (finalOnly, stop bool) {
|
|
estimatedNextPrompt := st.usageTotal.LastPromptTokens + st.usageTotal.LastCompletionTokens
|
|
return materialFinalDecision(materialBudgetState{
|
|
Limit: st.materialLimit, Used: st.usageTotal.TotalTokens,
|
|
LastPrompt: estimatedNextPrompt, OutputCap: st.materialOutputCap,
|
|
SavedCheckpoints: len(st.savedCheckpointIDs), Responses: st.responses,
|
|
UsageKnown: st.usageKnown,
|
|
})
|
|
}
|