agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
71 lines • 3.22 kB
JavaScript
/**
* defineFact — sugar for context-style Injections (data, not behavior).
*
* Use for developer-supplied facts the LLM should see in addition to
* user messages and tool results. Examples: user profile, env info,
* computed conversation summary, cached config, current time. Distinct
* from Skills (LLM-activated guidance) and Steering (always-on rules)
* in INTENT — they share the engine.
*
* Produces an `Injection` with:
* - flavor: `'fact'`
* - trigger: configurable (default `'always'`)
* - inject: `systemPrompt` (default), or `messages` with a declared role
*
* @example
* const userProfile = defineFact({
* id: 'user-profile',
* data: `Name: ${user.name}, Plan: ${user.plan}, Joined: ${user.joinedAt}`,
* });
*
* const turnTime = defineFact({
* id: 'turn-time',
* data: `Current time: ${new Date().toISOString()}`,
* });
*/
import { messagesToolRoleRefusal } from '../messagesSlotRefusal.js';
import { cachePolicyMetadata } from '../../cache/applyCachePolicy.js';
export function defineFact(opts) {
if (!opts.id || opts.id.trim().length === 0) {
throw new Error('defineFact: `id` is required and must be non-empty.');
}
if (!opts.data || opts.data.length === 0) {
throw new Error(`defineFact(${opts.id}): \`data\` is required.`);
}
// The runtime guards read a WIDENED view on purpose. The declared type
// already pairs `slot` with `role` correctly, and TypeScript narrows so well
// that `opts.role` is provably present inside a `slot === 'messages'` branch
// — which makes the checks below unreachable *to the compiler* and therefore
// invisible to the JavaScript callers and `as never` casts they exist for. A
// refusal that only holds at compile time is not a refusal.
const raw = opts;
const toMessages = raw.slot === 'messages';
if (toMessages && !raw.role) {
throw new Error(`defineFact('${opts.id}'): \`slot: 'messages'\` requires a \`role\` — ` +
`'user', 'assistant', or 'system' on a provider that carries it. There is no ` +
`default: who appears to speak is a meaning your app owns, not one the library picks.`);
}
if (toMessages && raw.role === 'tool') {
throw new Error(messagesToolRoleRefusal(`defineFact('${opts.id}')`));
}
const trigger = opts.activeWhen
? { kind: 'rule', activeWhen: opts.activeWhen }
: { kind: 'always' };
const inject = toMessages
? { messages: [{ role: raw.role, content: opts.data }] }
: { systemPrompt: opts.data };
const cacheMetadata = cachePolicyMetadata('fact', opts.cache);
// Two-stage cast (`as unknown as Injection`) is required because
// `flavor: 'fact'` narrows tighter than `ContextSource`. Both stages
// are type-safe at the call site — `flavor` IS a valid `ContextSource`
// member; TypeScript just can't narrow back through the freeze.
return Object.freeze({
id: opts.id,
...(opts.description && { description: opts.description }),
flavor: 'fact',
trigger,
inject,
metadata: Object.freeze(cacheMetadata),
});
}
//# sourceMappingURL=defineFact.js.map