/** * The answer a `read_skill` SELF-CALL gets back (9.84.0). * * A self-call is `read_skill("X")` from a cursor already standing on X. It used * to fall through the skill-graph gate's reachability arm — `makeReachableSkills` * filters the cursor out of its own successor set (a MOVE to where you already * are is not a move) and `openSkillIds()` excludes every graph-wired skill, so * the cursor's own id is in neither half of `hops ∪ open`. The model was told: * * read_skill("X") is not reachable from here. Reachable skills: … * * while X's body was in its system prompt and X's tools were in that very call's * tool list. Read as a statement about AVAILABILITY — which is how a model reads * "not reachable" — that is a flat contradiction of the request it arrived in, * and the observed behaviour was to stop and answer that it could not help. * * ── THE LAW THIS FILE IS WRITTEN AGAINST ────────────────────────────────── * * A TOOL RESULT IS NOT READ ONCE. It is composed during iteration N, written * into `history`, and then read by the model on call N+1 AND ON EVERY CALL * AFTER IT for the rest of the turn — including the out-of-budget WRAP-UP, * which `callLLM` serves with an EMPTY tool list under a user message saying * that no tools were offered on it. * * The consequence is the whole design: NO COMPOSE-TIME CHECK CAN MAKE A * FORWARD-LOOKING SENTENCE SAFE. A budget test, a posture test, a cursor test * — each is true at the instant it runs and each is outlived by the sentence * it admitted. Three rounds of this file fixed one clause at a time and each * time a different clause was still false, because every one of them was a * PREDICTION and the fix was always a better prediction. * * So the notice makes no predictions at all. Every clause is a fact about ONE * already-finished event — the call the model made `read_skill` on — and every * clause is anchored to that call by name ("that call"), never by deixis ("the * call you just made", which denotes a different call each time it is re-read). * A past fact about a named call cannot be falsified by anything that happens * afterwards: not by the budget running out, not by a posture refusing a hop, * not by a sibling tool in the same batch moving the cursor. * * ── WHAT WAS DELETED, AND WHY THE NOTICE STILL DOES ITS JOB ─────────────── * * Gone: the exhortation ("Go ahead and act on the request"), the move offer * ("read_skill MOVES you somewhere else — from here that would be: beta"), the * open-skill offer ("These activate without moving you: gamma"), and the * budget clause that was added to gate them. Each was false somewhere: * * • the exhortation, on the wrap-up call, beside a frame reporting that no * tools were offered on it; * • the move offer, on the wrap-up call (a hop is a tool call), under * `strictness: 'rails'` (every model hop refused) and under `'guard'` on a * decisively-routed turn (every hop not on an outstanding menu refused) — * where the sibling posture arm answers a taken-up offer with "read_skill * here reaches only the open skills: gamma", contradicting the notice from * the arm immediately above it, and spending escalation budget to do so; * • the budget clause, because a budget read at compose time is a claim * about a call that has not happened. * * The notice's JOB is to stop a model concluding its capability is gone. It * does that by stating what it held, not by promising what it may do: you were * standing in this skill, its instructions were in that call's prompt, its * tools were on that call's wire, and asking took nothing away. A model that * reads those four facts has no ground left for "I cannot help". * * The forward-looking half had a rightful owner all along, and it is not a * tool result: the `read_skill` DESCRIPTION is recomposed on every single call, * so it can safely speak in the present ("You are in 'beta'."), it is filtered * for per-role visibility at source, and it is absent from the wrap-up because * the whole tool list is. One owner for the present tense, one for the past, * and they can no longer disagree — which is the only property that survived * every round of this bug. */ import type { ActiveInjection } from '../../lib/injection-engine/types.js'; import type { LLMToolSchema } from '../../adapters/types.js'; /** What the current skill's tools did on the call the model made `read_skill` on. */ export interface SelfSkillTools { /** Every tool the skill declares — `[]` for a skill that declares none. */ readonly declared: readonly string[]; /** Those actually on that call's wire. A subset of `declared`. */ readonly served: readonly string[]; } /** * The skill's tools, as of the call the model just made — or `undefined` when * that cannot be established, which is not the same answer and must not be * reported as one. * * `undefined` means the notice says nothing about tools at all, and there are * exactly two ways to get it (9.86.0 — the previous note named a third that * cannot happen). It happens when the cursor's skill is not in the ACTIVE set * for that call, and when the wire key is absent from scope altogether. * * `reactMode: 'classic'` is NOT one of them, which is worth stating because the * old note said it was: seed writes `scope.dynamicToolSchemas` from the * build-time schemas at turn start (`stages/seed.ts`), so under classic the key * is present and holds the list `callLLM` really sends — the notice would name * the build-time list, correctly. (No self-call can arise there anyway: * `.skillGraph()` is refused at build under classic, and only a graph gives the * gate a cursor to compare against.) The grouped chart used to be a real third * way — the Tools slot writes the key inside `sf-llm-call` and this gate reads * it outside — until that boundary began bubbling it out. * * Silence is the only honest output in the two remaining cases: the * alternative is a sentence naming tools the model may not have. * * The out-of-budget wrap-up is NOT one of these cases and must not be modelled * as one. Its empty tool list belongs to a LATER call; the wire read here is * always the one the model was really handed on the call being answered, and * the notice speaks of that call and only that call. */ export declare function selfSkillTools(skillId: string, activeInjections: readonly ActiveInjection[] | undefined, wire: readonly LLMToolSchema[] | undefined): SelfSkillTools | undefined; /** * The self-call's tool result. * * NOT a refusal to the MODEL, and deliberately so. The gate's other two arms * refuse because the model asked for something it may not have; a self-call * asks for a body it already holds, and the useful reply is the fact — where * you stood, and what rode that call — not a decline. It is still counted as a * refusal by the ESCALATION BUDGET (`noteSkillRefusal`, beside the emit), and * those two facts do not fight: the budget is not measuring how the sentence * reads, it is measuring a model that keeps asking the graph where it is * instead of working, which is exactly the stuck run the budget escalates. * * It still OVERWRITES the tool's own result the way every other arm does, * because that overwrite is what puts the authoritative sentence in front of * the governance rules, the refusal cap and the after-tool moment, on both * channels, before the model reads anything. * * The one thing not overwritten is a BODY. The live case is `surfaceMode: * 'both'` — `'tool-only'` cannot arise here, because `skillBodyDelivery` * refuses it at BUILD time for anything but an `llm-activated` skill and only a * graph-wired skill can be the cursor. Under `'both'` the body is in the system * prompt AND rides the tool result, and the second copy is the whole point of * the mode: recency-first delivery, the body at the end of the window rather * than only at the top of the prompt. Dropping it here would quietly demote * `'both'` to `'system-prompt'` on the one call that asked to re-read. * * TENSE IS LOAD-BEARING, AND SO IS DEIXIS. Every clause below is past tense * about a single named call, and every reference to that call is "that call" — * bound by the opening clause, which says which call it was. "The call you just * made" was the earlier wording and it is not safe: re-read four calls later it * denotes call four, and the tools it names rode call one. * * There are exactly two present-tense phrases in the output and both are * timeless rather than temporal: "It declares no tools of its own" (a * declaration is fixed when the skill is defined and cannot vary within a run) * and "Its instructions are repeated below" (the body is appended to this very * string, so the sentence is true wherever the string is read). */ export declare function selfCallNotice(args: { readonly skillId: string; /** `undefined` when the wire could not be established — see `selfSkillTools`. */ readonly tools: SelfSkillTools | undefined; readonly body?: string; }): string;