/** * The v2 session-request layer (goal #4 and #6 of the product). * * Two things that v1 could only approximate, because 1.18.x gave a plugin one * mutable slot before execution and no way to touch an assembled request: * * 1. the tool surface. v1's whitelist was enforced by permission DENY, which * stops a call but leaves the tool's description and schema in EVERY request * — the model pays ~9.5K tokens for thirteen governed tools plus the native * catalog it is forbidden to use. `session.hook("context")` can `delete * event.tools.`, so the whitelist finally decides what the model sees. * 2. the blackboard root. v1 appended the resolved path to the team prompt at * config time; a v2 agent is a config FILE, which cannot carry a * per-workspace path, so it goes on the request. * * Everything here is additive and reversible: each hook returns a disposable * registration, and nothing is claimed that the host did not hand us. */ import type { DynamicSink } from "./v2-tail.js"; import type { V2Registration, V2Context } from "./v2-types.js"; /** * Context lines the host's compaction summarizer must carry forward. On v2 the * seam is `session.hook("compaction")` and the lines ride `event.system[]` as * text parts; we ADD and never replace, so the summarizer stays the host's and * whatever the host itself learns to preserve is not voided. (v1 pushed the * same list through `experimental.session.compacting` → `output.context[]`; * the list moved here when the v1 adapter layer was cut, and its content is * unchanged — it is the only survivor of `src/host-hooks.ts`.) * * Each line corresponds to a thing that, once summarized away, cannot be * re-derived without spending a round to rediscover it: * - the reply skeleton — prose instead of STATUS:/CHANGES/… makes the * lead's machine check fail for the rest of the session; * - offload handles (ref/access_token/expire_at) — they are the ONLY * window onto a payload that never entered context; * - sub-agent child session ids (host `task`, leftovers of the old * running work, and after a summary it looks indistinguishably like done * work; * - provenance (file:line / URL + confidence) — without it a finding * degrades into model memory, the one thing this project refuses; * - the todo list and board paths — the state lives there, not in chat. */ export declare const COMPACTION_CONTEXT: string[]; /** Sentinel for "this role may not browse at all" — it removes the host's whole * browser catalog (45 `browser_*` tools on 2.0.16), not just our own door. * Leaving them in would hand back, at the request layer, exactly the surface the * whitelist exists to withhold. */ export declare const BROWSER_CATALOG = "browser_*"; /** * #49 feature 4 — task splitting (拆大化小), the cheap mechanical half. * * The prompt discipline (`## Task splitting` in the lead prompt) says a dispatch * owns ONE independently verifiable deliverable. This is the trigger that makes * the discipline visible at the moment it is being broken: a `subagent` brief * whose text exceeds `TM_SPLIT_BRIEF_TOKENS` arms ONE advice line, injected into * the lead's NEXT request. It only ever ADVISES — nothing is rejected, no input * is rewritten — because the dupe-guard lesson is that a model can ignore a * directive, and a hard gate here would block legitimate large-but-single work. * * The brief text is read at `tool.hook("execute.before")`, the only seam that * sees the `subagent` call's own `input` (the host's field is `input`, and it IS * the argument object — measured in v2-subagent.ts). `session.hook("context")` * cannot see it: its `messages` are the parent's assembled history, not the * dispatch brief. So the trigger lives on `execute.before` and the injection on * `context`, both owned by this layer. */ export declare const SPLIT_MARKER = "## Team Split"; export interface SplitConfig { enabled: boolean; /** A brief above this many tokens arms the advice. */ briefTokens: number; /** The criteria count the prompt names as "split past this". */ maxCriteria: number; } export declare const SPLIT_DEFAULTS: SplitConfig; /** `splitAdvice: "off"` disables; the two numbers are clamped by `resolveConfig`. */ export declare function resolveSplitConfig(config?: { splitAdvice: string; splitBriefTokens: number; splitMaxCriteria: number; }): SplitConfig; /** The brief text of a `subagent` dispatch, or "" when the input is not one. * `prompt` is the brief; `description` is the fallback the host also carries. */ export declare function briefTextOf(input: unknown): string; /** The ONE advice line. It names the measured size, the split rule, and the * escape hatch — a directive the user can turn off is not a silent behaviour. */ export declare function splitAdviceText(briefTokens: number, briefTokensLimit: number, maxCriteria: number): string; /** * The tools one agent must never be offered. Only a literal `deny` removes: * the `{ "*": "ask" }` object form means "callable, gated", and deleting such a * tool would silently drop a capability the matrix grants with a dialog. * * `read`/`grep`/`glob` are exempted by name (`V2_LADDER_ACTIONS`), which is the * other half of the `tm_read`/`tm_grep`/`tm_bash` retirement: v1 denied the * native file tools because the governed aliases existed, and on v2 those * aliases are not registered, so honouring the deny here would leave a role with * no way to open a file at all. Nothing is lost by letting them ride — the path * scope P2 enforced in code is the host's own `external_directory` action, which * was observed live answering `effect:"ask"` with a real `permission.asked` * behind it, i.e. a dialog instead of a hard throw. */ export declare function toolsToRemove(permission: Record | undefined | null): string[]; /** Built once per agent id so the hook body stays a set lookup on a hot path. * #38: keys are normalized (lower case) so a host reporting `team` and a config * declaring `Team` resolve to the same plan entry. */ export declare function removalPlan(agents: Record; }>): Map>; export interface SessionLayerInput { /** The resolved blackboard addendum, or "" when there is nothing to say. */ note: string; /** Which agent ids receive the note — v1 gave it to the lead only. */ noteAgents: string[]; plan: Map>; /** Team-scope isolation (#22). `session.context` fires for EVERY agent on the * host, so the board note and the compaction survival list are * writes to somebody else's request unless the agent is one of ours. The tool * trim is already keyed by agent (a foreign role is not in the plan, so nothing * is deleted); this closes the other three. */ scope?: import("./v2-scope.js").TeamScope; /** #49 feature 4: the split-advice knobs. Absent = the defaults (on). */ split?: SplitConfig; /** * 易变注入的尾部通道 (`dynamicTail: "on"`). When handed in, the split advice * goes to the request TAIL instead of `event.system` — the board note and the * compaction survival list stay in `system`, because those are STABLE content * (they are the prefix we are trying to keep cacheable). */ sink?: DynamicSink; /** Fired for each observable split outcome so the personality can write the * `step_id:"v2-split"` trajectory line — the layer owns no store. */ onSplit?: (row: Record) => void; } export interface SessionLayerReport { removed: Record; notePushed: boolean; compactionLines: number; /** Did a `tm_*` name ever appear in an assembled request's own tool surface? * This is the only evidence that direct delivery happened: `options.codemode:false` * is what WE send, and a live 2.0.16 session proved the host still put every * `tm_*` in the Code Mode catalog ("They cannot be called directly…") while the * model's own callable list was nine native tools. Reporting the sent flag as if it * were the outcome is the overstated claim this product exists to refuse. */ tmInRequestSurface: boolean; /** #49 feature 4: `subagent` briefs measured on a Team session. */ splitSeen: number; /** …of those, how many armed the advice AND had it injected into a request. */ splitAdvised: number; /** A throw inside the split hooks is swallowed and counted, never propagated. */ splitThrew: number; } /** * Register the request-layer hooks. Returns the registrations (so the caller * can dispose them) and a live report object — the counts are read off what the * hooks actually did, not off what the matrix intends, because "we trimmed the * surface" is a claim the user can only check against the request. */ export declare function applyV2SessionLayer(ctx: V2Context, input: SessionLayerInput): Promise<{ registrations: V2Registration[]; report: SessionLayerReport; }>; //# sourceMappingURL=v2-session.d.ts.map