import type { OperatorScope } from "../gateway/operator-scopes.js"; import type { AgentEventPayload, AgentEventStream } from "../infra/agent-events.js"; import type { PluginHookAgentContext, PluginHookBeforeToolCallEvent, PluginHookBeforeToolCallResult, PluginHookToolContext, } from "./hook-types.js"; import type { PluginJsonValue } from "./host-hook-json.js"; import type { PluginAgentTurnPrepareResult, PluginNextTurnInjectionPlacement, PluginNextTurnInjectionRecord, } from "./host-hook-turn-types.js"; export { isPluginJsonValue } from "./host-hook-json.js"; export type { PluginJsonPrimitive, PluginJsonValue } from "./host-hook-json.js"; export type { PluginAgentTurnPrepareEvent, PluginAgentTurnPrepareResult, PluginHeartbeatPromptContributionEvent, PluginHeartbeatPromptContributionResult, PluginNextTurnInjection, PluginNextTurnInjectionEnqueueResult, PluginNextTurnInjectionPlacement, PluginNextTurnInjectionRecord, } from "./host-hook-turn-types.js"; export type PluginHostCleanupReason = "disable" | "reset" | "delete" | "restart"; export type PluginSessionExtensionProjectionContext = { sessionKey: string; sessionId?: string; state: PluginJsonValue | undefined; }; export type PluginSessionExtensionRegistration = { namespace: string; description: string; project?: (ctx: PluginSessionExtensionProjectionContext) => PluginJsonValue | undefined; cleanup?: (ctx: { reason: PluginHostCleanupReason; sessionKey?: string }) => void | Promise; /** * When set, after every successful `patchSessionExtension` the projected * value is mirrored to `SessionEntry[]` so non-plugin readers * can consume the typed slot without reaching into * `pluginExtensions[pluginId][namespace]`. * * The slot is a read-only mirror: writes always go through * `patchSessionExtension`; the host overwrites the slot value on every * subsequent patch. */ sessionEntrySlotKey?: string; /** * Optional JSON-compatible schema describing the projected slot value. * Purely informational at this layer; clients may use it to validate the * mirrored slot against a contract. */ sessionEntrySlotSchema?: PluginJsonValue; }; export type PluginSessionExtensionProjection = { pluginId: string; namespace: string; value: PluginJsonValue; }; export type PluginSessionExtensionPatchParams = { key: string; pluginId: string; namespace: string; value?: PluginJsonValue; unset?: boolean; }; export type PluginToolPolicyDecision = | PluginHookBeforeToolCallResult | { allow?: boolean; reason?: string; }; export type PluginTrustedToolPolicyRegistration = { id: string; description: string; evaluate: ( event: PluginHookBeforeToolCallEvent, ctx: PluginHookToolContext, ) => PluginToolPolicyDecision | void | Promise; }; export type PluginToolMetadataRegistration = { toolName: string; displayName?: string; description?: string; risk?: "low" | "medium" | "high"; tags?: string[]; }; export type PluginCommandContinuation = { continueAgent?: boolean; }; export type PluginControlUiDescriptor = { id: string; surface: "session" | "tool" | "run" | "settings"; label: string; description?: string; placement?: string; schema?: PluginJsonValue; requiredScopes?: OperatorScope[]; }; export type PluginSessionActionContext = { pluginId: string; actionId: string; sessionKey?: string; payload?: PluginJsonValue; client?: { connId?: string; scopes: string[]; }; }; export type PluginSessionActionResult = | { ok?: true; result?: PluginJsonValue; reply?: PluginJsonValue; continueAgent?: boolean; } | { ok: false; error: string; code?: string; details?: PluginJsonValue; }; export type PluginSessionActionRegistration = { id: string; description?: string; schema?: PluginJsonValue; requiredScopes?: OperatorScope[]; handler: ( ctx: PluginSessionActionContext, ) => PluginSessionActionResult | void | Promise; }; export type PluginRuntimeLifecycleRegistration = { id: string; description?: string; cleanup?: (ctx: { reason: PluginHostCleanupReason; sessionKey?: string; runId?: string; }) => void | Promise; }; export type PluginAgentEventSubscriptionRegistration = { id: string; description?: string; streams?: AgentEventStream[]; handle: ( event: AgentEventPayload, ctx: { // oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Run-context JSON reads are caller-typed by namespace. getRunContext: ( namespace: string, ) => T | undefined; setRunContext: (namespace: string, value: PluginJsonValue) => void; clearRunContext: (namespace?: string) => void; }, ) => void | Promise; }; export type PluginAgentEventEmitParams = { runId: string; stream: AgentEventStream; data: PluginJsonValue; sessionKey?: string; }; export type PluginAgentEventEmitResult = | { emitted: true; stream: AgentEventStream } | { emitted: false; reason: string }; export type PluginRunContextPatch = { runId: string; namespace: string; value?: PluginJsonValue; unset?: boolean; }; export type PluginRunContextGetParams = { runId: string; namespace: string; }; export type PluginSessionSchedulerJobRegistration = { id: string; sessionKey: string; kind: string; description?: string; cleanup?: (ctx: { reason: PluginHostCleanupReason; sessionKey: string; jobId: string; }) => void | Promise; }; export type PluginSessionSchedulerJobHandle = { id: string; pluginId: string; sessionKey: string; kind: string; }; export type PluginSessionAttachmentFile = { path: string; }; export type PluginAttachmentChannelHints = { telegram?: { parseMode?: "HTML"; disableNotification?: boolean; /** * Require host-side detection to match this MIME before forcing document delivery. * Mismatched files are rejected before the outbound adapter is called. */ forceDocumentMime?: string; }; slack?: { threadTs?: string; }; }; export type PluginSessionAttachmentCaptionFormat = "plain" | "html" | "markdown"; export type PluginSessionAttachmentParams = { sessionKey: string; files: PluginSessionAttachmentFile[]; text?: string; threadId?: string | number; forceDocument?: boolean; maxBytes?: number; captionFormat?: PluginSessionAttachmentCaptionFormat; channelHints?: PluginAttachmentChannelHints; }; export type PluginSessionAttachmentResult = | { ok: true; channel: string; deliveredTo: string; count: number; } | { ok: false; error: string }; export type PluginSessionTurnSchedule = | { at: string | number | Date } | { delayMs: number } | { cron: string; tz?: string }; type PluginSessionTurnScheduleCommonParams = { sessionKey: string; message: string; agentId?: string; deliveryMode?: "none" | "announce"; name?: string; /** Optional cleanup tag. Reserved cron-name delimiters like `:` are rejected. */ tag?: string; }; export type PluginSessionTurnScheduleParams = | ({ at: string | number | Date; deleteAfterRun?: boolean; } & PluginSessionTurnScheduleCommonParams) | ({ delayMs: number; deleteAfterRun?: boolean; } & PluginSessionTurnScheduleCommonParams) | ({ cron: string; tz?: string; deleteAfterRun?: false; } & PluginSessionTurnScheduleCommonParams); export type PluginSessionTurnUnscheduleByTagParams = { sessionKey: string; tag: string; }; export type PluginSessionTurnUnscheduleByTagResult = { removed: number; failed: number; }; export function normalizePluginHostHookId(value: string | undefined): string { return (value ?? "").trim(); } function normalizeQueuedInjectionText( entry: PluginNextTurnInjectionRecord, placement: PluginNextTurnInjectionPlacement, ): string | undefined { const candidate = entry as { placement?: unknown; text?: unknown; }; if (candidate.placement !== placement || typeof candidate.text !== "string") { return undefined; } const text = candidate.text.trim(); return text || undefined; } export function buildPluginAgentTurnPrepareContext(params: { queuedInjections: PluginNextTurnInjectionRecord[]; }): PluginAgentTurnPrepareResult { const prepend = params.queuedInjections .map((entry) => normalizeQueuedInjectionText(entry, "prepend_context")) .filter(Boolean); const append = params.queuedInjections .map((entry) => normalizeQueuedInjectionText(entry, "append_context")) .filter(Boolean); return { ...(prepend.length > 0 ? { prependContext: prepend.join("\n\n") } : {}), ...(append.length > 0 ? { appendContext: append.join("\n\n") } : {}), }; } export type PluginHostHookRunContext = PluginHookAgentContext;