agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
83 lines • 4.16 kB
JavaScript
/**
* thinkingSupport — `.thinking({ budget })` checked at BUILD against what each
* provider declares for the model it will call (`LLMProvider.thinkingMode`).
*
* Every (provider, model) pair the agent's thinking request can reach is
* asked: the declared reliability list (or the agent's own pair), each
* per-skill brain, the escalation brain. A
* model that cannot think (`'none'`) is refused here, by name, with an
* `UnsupportedThinkingError` — rather than on the first call, or on the call
* a fallback serves. An `'adaptive'` model takes no budget, and dev mode says
* so once per model: configured and silently ignored must not look like
* configured and working.
*
* Not checked here: a model `.configure()` picks at run time. The adapter
* asks the same declaration on every request and refuses before sending.
*/
import { isDevMode } from 'footprintjs';
import { UnsupportedThinkingError } from '../../thinking/errors.js';
import { thinkingModeFor } from '../../thinking/thinkingModeFor.js';
import { providerTargets } from './providerTargets.js';
/** Models already told that adaptive thinking takes no budget — once per model per process. */
const adaptiveWarned = new Set();
/**
* Refuse `.thinking()` for any reachable model whose provider declares it
* cannot honor thinking for it; dev-warn for one that thinks adaptively (no budget).
*
* @throws UnsupportedThinkingError (`reason: 'no-thinking'`)
* @throws TypeError when a provider's `thinkingMode` declaration is malformed
*/
export function checkThinkingSupport(args) {
for (const target of thinkingTargets(args)) {
const mode = thinkingModeFor(target.provider, target.model);
if (mode === 'none') {
throw new UnsupportedThinkingError({
provider: target.provider.name,
model: target.model,
reason: 'no-thinking',
detail: `this provider cannot honor thinking for this model, so .thinking({ budget: ${args.budget} }) ` +
`cannot be honoured for ${target.site}. Choose a provider/model pair that supports thinking, or drop .thinking().`,
});
}
if (mode === 'adaptive')
warnBudgetNotSent(target, args.budget);
}
}
/** Selected transport pairs, then each brain's — a brain inherits what it leaves out. */
function thinkingTargets(args) {
const targets = [
...providerTargets(args.provider, args.model, args.reliabilityProviders),
];
for (const [skillId, brain] of args.brains?.bySkill ?? []) {
targets.push({
provider: brain.provider ?? args.provider,
model: brain.model ?? args.model,
site: `skill '${skillId}'`,
});
}
const escalation = args.brains?.escalation;
if (escalation !== undefined) {
targets.push({
provider: escalation.provider,
model: escalation.model ?? args.model,
site: 'the escalation brain',
});
}
return targets.filter((t, i) => targets.findIndex((u) => u.provider === t.provider && u.model === t.model) === i);
}
function warnBudgetNotSent(target, budget) {
const key = JSON.stringify([target.provider.name, target.model]);
if (adaptiveWarned.has(key) || !isDevMode())
return;
if (adaptiveWarned.size < 500)
adaptiveWarned.add(key);
// eslint-disable-next-line no-console
console.warn(`agentfootprint Agent: .thinking({ budget: ${budget} }) — '${target.model}' on ` +
`'${target.provider.name}' thinks adaptively, which takes no budget: the model decides ` +
`whether and how much to think, and the budget is not sent. The built-in Anthropic ` +
`adapters still keep max_tokens above it, so a long think has room. They also read an ` +
`id they do not know as adaptive: if '${target.model}' is an alias, a deployment name ` +
`or a profile ARN for a model that needs a budget (Claude 4.5 and earlier), declare it ` +
`with the adapter's thinkingMode option. This warning fires once per model per process.`);
}
//# sourceMappingURL=thinkingSupport.js.map