/** * v2 setup orchestration. * * Returns the `setup(ctx)` function v2 calls via `default.setup`. The setup * wraps the existing v1 factory (reusing ALL build logic) and translates the * returned v1 `Hooks` into v2 registrations: agent/tool/command transforms, * a single session context hook (system/messages transforms, chat.message * tracking, and interview + generic command marker dispatch), the native * `session.prompt` hook (once-per-admission chat.message fidelity, with a * context-hook fallback on older hosts), the native `session.model.request` * hook (v1 chat.headers — Copilot initiator header), tool execute hooks, * and the event stream. Each bridge is independently try/catch-guarded. */ import { type SyntheticPartCacheHint } from '../hooks/cache-safe-injection'; import type { McpConfig } from '../mcp/types'; import { type V2CommandSubmit } from './session-submit'; import type { V2Cleanup, V2CommandDraft, V2Context, V2PermissionRule, V2SessionCompactionEvent, V2SessionContextEvent, V2SessionModelRequestEvent, V2SessionPromptEvent } from './types'; /** v1 `command.execute.before` hook shape (see src/index.ts wiring). */ export type V1CommandBeforeHook = (input: { command: string; sessionID: string; arguments: string; }, output: { parts: Array<{ type: string; text?: string; synthetic?: boolean; metadata?: Record; }>; }) => Promise; /** Wrap slash-command arguments in the generic v2 command marker. v2 command * drafts are add-only (no `template`), so `execute` submits this marker as a * plain user prompt and the session context hook recovers it below. */ export declare function wrapCommandMarker(name: string, args: string): string; export interface ParsedCommandMarker { name: string; args: string; } /** Parse the generic command marker from a message text, if present. */ export declare function parseCommandMarker(text: string): ParsedCommandMarker | undefined; /** Strip the marker tags from marker-only `text`, leaving the raw args. */ export declare function stripCommandMarker(text: string): string; /** Register one v1 synth command on a v2 command draft. Uses `add` when * present; callers wrap per-command in try/catch so a throwing `draft.add` * only skips that command. */ export declare function createCommandRegistration(draft: V2CommandDraft, name: string, cmd: { description?: string; }, submit: V2CommandSubmit): void; /** Register the v1 synth commands on a v2 command draft. `interview` is * owned by the interview bridge's own registration (whose context hook owns * the interview marker), so it is skipped here — a duplicate `draft.add` * would break `/interview` on host builds that are first-wins or throw on * duplicates. */ export declare function registerSynthCommands(draft: V2CommandDraft, entries: Array<[string, { description?: string; }]>, submit: V2CommandSubmit): void; /** Dispatch a generic command marker found in the trailing user message to * the v1 `command.execute.before` hook, then replace that message's content * with the hook-produced parts. Mirrors the interview bridge mutation * semantics: only the trailing message is touched so earlier messages stay * byte-for-byte identical (provider prompt-cache prefix reuse). */ export declare function applyCommandMarkerToContext(event: V2SessionContextEvent, commandBefore: V1CommandBeforeHook): Promise; /** Payload the v1 `chat.message` bridge feeds its consumers (a subset of * the real v1 hook input — see src/index.ts wiring). */ export type V1ChatMessageInput = { sessionID: string; agent?: string; model?: { providerID: string; modelID: string; variant?: string; }; messageID?: string; parts?: unknown[]; }; /** Deps injected into the single session context hook. */ export interface V2SessionContextHandlerDeps { /** Interview bridge handleContext (transcript projection + /interview * marker dispatch). */ interviewHandleContext: (event: V2SessionContextEvent) => Promise; /** v1 `command.execute.before` hook (generic command marker dispatch). */ commandBefore?: V1CommandBeforeHook; /** v1 `chat.message` hook (per-request context emulation). Omitted when * the native v2 `session.prompt` hook owns message-scoped delivery. */ chatMessage?: (input: V1ChatMessageInput, output: unknown) => Promise; /** Native prompt-hook mode: records per-session agent/model from * context events and forwards newly learned state to the v1 * `chat.message` hook (see createSessionPromptBridge). */ observeContextAgent?: (event: V2SessionContextEvent) => Promise; /** v1 `chat.headers` support: records the trailing user message * identity + internal-initiator state per session from context events * (context fires before every `model.request` — see * createChatHeadersBridge). */ observeChatHeaders?: (event: V2SessionContextEvent) => void; /** Agent known for a session, from the agent-learned state the * session-prompt bridge / context events maintain. Used to enrich * transcript user messages the v1 injection gates key on when the * context event itself carries no agent. */ knownAgentForSession?: (sessionID: string) => string | undefined; /** v1 `experimental.chat.system.transform` hook. */ systemTransform?: (input: unknown, output: { system: string[]; }) => Promise; /** v1 `experimental.chat.messages.transform` hook. */ messagesTransform?: (input: unknown, output: { messages: Array<{ info: { role: string; }; parts: unknown[]; }>; }) => Promise; /** CacheHint stamped on parts injected while the bridged messages * transform runs (v2 ContentPart.cache; v1 bytes never change — see * cache-safe-injection). */ syntheticPartCacheHint?: SyntheticPartCacheHint; } /** Build the single `ctx.session.hook("context")` handler: interview marker * bridge, generic command marker dispatch, chat.message agent tracking, and * the v1 system/messages transforms — each independently try/catch-guarded. */ export declare function createSessionContextHandler(deps: V2SessionContextHandlerDeps): (event: V2SessionContextEvent) => Promise; export interface V2SessionPromptBridge { /** `ctx.session.hook("prompt")` handler — one v1 chat.message delivery * per admitted input (dedupe by messageID). The FIRST admission per * session is deferred until the agent is learned (see * `observeContext`) so it is delivered with parts + agent together. */ handlePrompt(event: V2SessionPromptEvent): Promise; /** Record per-session agent/model from context events; forward NEWLY * learned state to the v1 chat.message hook, flushing any deferred * first admission with the agent attached. */ observeContext(event: V2SessionContextEvent): Promise; /** Latest agent known for a session from the learned state above (the * identity source for transcript user-message enrichment). */ agentForSession(sessionID: string): string | undefined; } /** Trailing-user-message internal-initiator state per session, learned * from context events by `observeChatHeaderState` and consumed by * `createChatHeadersBridge`. Bounded via `pruneSessionMap`. */ export interface ChatHeaderSessionState { messageID?: string; internal: boolean; } export type ChatHeaderSessionStates = Map; /** Record the current trailing user message identity and whether it is an * internal-initiator admission (plugin-driven wake/fallback prompt). The * v1 chat.headers hook answered this per request by fetching the message's * parts; on v2 the marker is visible in-band — prompt `metadata` persisted * onto the transcript user message (spread onto the LLM Message envelope * the context event carries) or the admission tracker for synthetic * admissions — so no per-request transcript fetch is needed. Overwrites * per context event: each event is the current request's view. */ export declare function observeChatHeaderState(states: ChatHeaderSessionStates, event: V2SessionContextEvent): void; export declare function __resetChatHeadersOrderingTripwireForTesting(): void; /** * v1 `chat.headers` → v2 `session.model.request` bridge. * * The v1 hook sets `x-initiator: agent` on GitHub Copilot provider requests * whose user message is an internal-initiator admission, so Copilot's * backend does not account plugin-driven turns (orchestrator wake prompts) * as user activity. v2 exposes the same transport-level surface via * `session.hook("model.request")` with a mutable `headers` record the host * merges into the outgoing HTTP request. * * Translation notes (deliberate deviations, both verified against the v2 * host source): * - The v1 `model.api.npm === '@ai-sdk/github-copilot'` exclusion is not * reproducible (v2 Model.Ref carries no npm package) and not desirable: * v2's built-in Copilot provider hook leaves `x-initiator` unset exactly * for primary requests in root sessions, and the native fetch layer only * escalates (`x-initiator` pre-set to `agent` is honored, never reset to * `user`) — so this bridge composes with the built-in instead of * conflicting. * - Auxiliary kinds (compaction/title/generate) are skipped: v2's built-in * Copilot hook already marks those (`conversation-background` / * `conversation-compaction` → `x-initiator: agent`). * - The decision constants and provider gate come from * `src/hooks/chat-headers.ts` so both hosts stamp the same header. * - Escalation-only writes: an already-present `x-initiator: agent` * (e.g. set by the built-in or another plugin hook) is never rewritten, * mirroring the upstream fetch-layer contract. * * Headers are transport-level only — no payload content is read or mutated * (prompt-cache safety is unaffected). * * @param onOrderingDrift invoked (once per process — module-global latch) * when a primary request arrives for a session with no context-event * observation; injectable so tests can observe the tripwire without * mocking the logger. */ export declare function createChatHeadersBridge(states: ChatHeaderSessionStates, onOrderingDrift?: () => void): (event: V2SessionModelRequestEvent) => Promise; /** * Native `session.compaction` hook bridge (v2.0.0+). * * The host's session summarizer fires `compaction` with the request's * message list; without this bridge the summary would bake the plugin's * volatile injected content (background job boards, phase reminders) * into the compacted transcript permanently. The callback strips ONLY * tagged synthetic parts, reusing `stripTaggedContent` from * cache-safe-injection (the same helper every injection strips with) — * user text, command markers, untagged synthetic parts, and message * order are untouched; messages consisting solely of tagged parts (the * volatile trailing-message shape) are dropped. * * Deliberately read-only on the rest of the event: `system` is never * rewritten (open host bug: the compaction system prompt may be absent — * adding one would corrupt the request) and `result` is host-owned. * Fail-soft like every other bridge. */ export declare function createSessionCompactionBridge(metadataKeys?: readonly string[]): (event: V2SessionCompactionEvent) => Promise; /** * Derive exact-match v2 permission rules from a v1 agent permission map * (the child agent's task-policy — the same map `adaptPermissions` * consumes for static agent registration). * * Only entries that can be expressed WITHOUT wildcards survive: * - the string shorthand and whole-tool string effects (e.g. * `edit: 'deny'`) apply to every resource, so emitting them would * require a `'*'` resource — skipped; * - the `'*'` catch-all key is skipped by the action gate; * - nested `{tool: {pattern: effect}}` entries emit * `{action, resource: pattern, effect}` only when `pattern` is * wildcard-free (e.g. `skill: {codemap: 'allow'}`, * `bash: {'git push': 'ask'}`). * * The result is defense-in-depth: the child's static agent-level * permissions (from `applyAgentToDraft`) keep governing everything the * exact-match ruleset cannot express. */ export declare function deriveExactPermissionRules(perm: unknown): V2PermissionRule[]; export declare function __resetPermissionRulesWarningForTesting(): void; /** Deps for the per-session permission rules bridge. */ export interface V2PermissionRulesOptions { /** Task-policy lookup: the v1 permission map governing a child agent * (from the resolved agent configs). */ permissionForAgent: (agent: string) => unknown; /** Plugin-defined agent ids — the plugin-managed child gate. A child * whose agent is not in this set was not spawned by the plugin's task * pipeline and must never have its session rules replaced. */ pluginAgents: ReadonlySet; /** Injectable degradation sink (tests observe the one-time warning * without mocking the logger). */ onUnavailable?: () => void; } /** * Per-session permission rules bridge (`ctx.permission.rules`, v2.0.0+ * #48351; capability-probed, fail-soft). * * v2 children inherit their parent's session-scoped rules at creation * and were previously governed ONLY by the static agent-level permission * list mapped at agent-transform time (`adaptPermissions`). This bridge * observes the RAW v2 `session.created` event from the setup event pump * and, for each plugin-managed child session (parentID present AND the * child's agent is plugin-defined — the v2-local equivalent of the * event-router's `shouldManageSession(parent)` gate, since session agent * metadata lives inside the v1 factory), installs the child agent's * task-policy as session-scoped exact-match rules exactly once per * sessionID (duplicate event delivery is idempotent). * * Because `rules` REPLACES the whole session-scoped list, root sessions * and foreign-agent children are never touched. Hosts without the * capability degrade with the one-time warning above. Failures are * logged, never thrown into the event pump. */ export declare function createPermissionRulesBridge(permission: V2Context['permission'], options: V2PermissionRulesOptions): { /** Observe one raw v2 event; applies rules when it is a * plugin-managed child `session.created`. Never throws. */ observeSessionCreated(event: Record): Promise; }; /** * Native `session.prompt` hook → v1 `chat.message` bridge. * * v2's prompt hook fires ONCE per admitted input — endpoint prompts AND * subagent-tool child prompts (synthetic/shell/compaction inputs skip * it) — with the eventual inbox User `messageID`, the exact identity the * v1 chat.message consumers key on (task-session-manager + * orchestrator-wake `observeChatMessage`, toolLoopGuard * `observeNewUserMessage`). The context-hook emulation cannot provide * this: it fires per LLM request and has no prompt parts, so * `observeChatMessage`'s non-synthetic-part gate never passed on v2. * * The prompt payload carries NO agent/model, so `observeContext` learns * them from the (immediately following) context events and forwards * first-seen/changed state — preserving the v1 timing where the session * agent is known before the first tool call of a turn. * * First-admission deferral: the v1 chat.message handler only registers * the session agent (sessionMetadata.setAgent) when a delivery carries * one, and its consumers gate on that registration * (shouldManageSession → getAgent === 'orchestrator'). Forwarding the * FIRST admitted prompt before any agent was learned would therefore be * dropped by every consumer, and the follow-up agent-only forward (no * parts) is dropped by the parts gate — the first external message's * state effects (input-wait latch clearing, wake-progress rearm) would * be lost. The bridge instead latches that first prompt per session and * flushes it once the first agent-bearing context event arrives (parts + * agent delivered together, mirroring v1's single chat.message). Bounded * fallbacks keep delivery from being lost outright when no agent is ever * learned: the next admitted prompt for the session flushes a * still-pending one best-known, and so does a context event whose * trailing user message shows the conversation has moved past it. * * Child-session filtering: none, deliberately — the context-hook * emulation never filtered child sessions either, and every consumer * gates itself (e.g. `shouldManageSession`). */ export declare function createSessionPromptBridge(chatMessage: (input: V1ChatMessageInput, output: unknown) => Promise): V2SessionPromptBridge; /** The v2→v1 tool.execute bridge pair produced by * `createToolExecuteBridges`. */ export interface V2ToolBridgeEvents { beforeBridge: (event: Record & { input: unknown; }) => Promise; afterBridge: (event: Record & { result?: unknown; }) => Promise; } /** Build the tool.execute.before/after v2→v1 bridges, including the * `subagent`→`task` delegation normalization. Exported for tests. */ export declare function createToolExecuteBridges(before: ((i: { tool: string; sessionID: string; callID: string; }, o: { args: unknown; }) => Promise) | undefined, after: ((i: unknown, o: unknown) => Promise) | undefined): V2ToolBridgeEvents; /** v1 McpConfig → v2 Mcp.ServerConfig(字段几乎同构;仅剔除 undefined)。 */ export declare function adaptMcpServer(v1: McpConfig): Record; export declare function createV2Setup(): (ctx: V2Context) => Promise;