UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

116 lines • 5.21 kB
/** * runInput — the one door every runner's `run()` input goes through. * * Pattern: normalize-or-refuse at a boundary (the shape `toolArgsValidation` * uses for a model's arguments, applied to a caller's). * Role: core/ layer. `Agent`, `LLMCall` and the compositions all take the * same `{ message }` bag, and until 8.18.0 none of them looked at it. * A caller who wrote `agent.run('go')` — the shape every chat SDK * takes — reached `buildMessagesSlot` with `content: undefined` and * got `TypeError: Cannot read properties of undefined (reading * 'length')` five frames inside the engine. `LLMCall.run('go')` was * worse: it called the model with ZERO messages and returned an * answer, so the mistake looked like it worked. * Emits: N/A — this runs before any chart does. * * ## Two answers, and the rule that picks between them * * A bare string is **adapted**: `run('go')` means `run({ message: 'go' })`. * `AgentInput` has one required field, so a lone string has exactly one * possible reading, and refusing it would be a speed bump rather than a * teaching. The adaptation is stated here, in the types, and in the docs — * an adaptation nobody can see is indistinguishable from a guess. * * Everything else is **refused, by name**. `{}`, `{ message: undefined }`, * `{ message: 42 }`, `null`, `''` are not one sane meaning each; they are a * caller who believes they passed a message and did not. The refusal says * what arrived, and spells the two accepted forms. It fires before the run * starts, so nothing is billed and no half-run has to be explained. * * An empty (or whitespace-only) message is refused for the same reason it * cannot work: `Agent` used to send a `content: ''` turn and `LLMCall` used * to send no turn at all — the two runners disagreed, and both shapes are a * 400 on a real provider wire. "Run with only the system prompt" is a real * intention, and the refusal names how to say it: put the instruction in * the message. */ /** * Thrown by `run()` (and `runTyped()`) when the input is not a message. * * `received` is a short, non-leaking description of what arrived — the type * and, for an object, its keys. Never the value: an input that was refused * for being the wrong shape is still the caller's data. */ export class InvalidRunInputError extends Error { code = 'ERR_INVALID_RUN_INPUT'; /** Short description of what was passed, e.g. `'object with keys: text'`. */ received; /** Which runner refused, e.g. `'Agent.run'`. */ runner; constructor(ctx) { super(`${ctx.runner}: ${ctx.hint} (received ${ctx.received}).`); this.name = 'InvalidRunInputError'; this.received = ctx.received; this.runner = ctx.runner; } } /** Describe a value for the refusal without quoting it. */ function describe(value) { if (value === null) return 'null'; if (value === undefined) return 'undefined'; if (Array.isArray(value)) return `an array of ${value.length}`; if (typeof value === 'object') { const keys = Object.keys(value); return keys.length === 0 ? 'an object with no keys' : `an object with keys: ${keys.slice(0, 6).join(', ')}`; } return `a ${typeof value}`; } /** The two spellings, quoted the same way in every refusal. */ const SPELLINGS = "pass a message: run('your message') or run({ message: 'your message' })"; /** * Normalize a runner input to `{ message, ...rest }`, or refuse. * * `runner` names the door in the error (`'Agent.run'`, `'LLMCall.run'`, …). * Extra fields on an object input are preserved untouched — `identity` and * anything a subclass adds ride through. */ export function normalizeRunInput(input, runner) { if (typeof input === 'string') { return assertMessage({ message: input }, runner); } if (typeof input !== 'object' || input === null || Array.isArray(input)) { throw new InvalidRunInputError({ runner, received: describe(input), hint: SPELLINGS, }); } return assertMessage(input, runner); } /** The message field itself, judged once for both entry shapes. */ function assertMessage(input, runner) { const { message } = input; if (typeof message !== 'string') { throw new InvalidRunInputError({ runner, received: `${describe(input)}, whose \`message\` is ${describe(message)}`, hint: `\`message\` must be a string — ${SPELLINGS}`, }); } if (message.trim() === '') { throw new InvalidRunInputError({ runner, received: message === '' ? 'an empty message' : 'a whitespace-only message', hint: 'a run needs something to answer. An empty message is not a shorter question — it ' + 'reaches the provider as a turn with no content, which real wires reject. To run on ' + 'the system prompt alone, say so in the message (e.g. run({ message: "begin" }))', }); } return input; } //# sourceMappingURL=runInput.js.map