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, }) }