diff --git a/packages/core/src/plugin/optimize.ts b/packages/core/src/plugin/optimize.ts index 6dc8dedf5e1..cb8e29ba7b3 100644 --- a/packages/core/src/plugin/optimize.ts +++ b/packages/core/src/plugin/optimize.ts @@ -54,7 +54,7 @@ function make( return define({ id, effect: Effect.fn(`OptimizePlugin.${id}`)(function* (ctx) { - yield* ctx.session.hook("context", (event) => + const hook = (event: SessionHooks["context"]) => Effect.gen(function* () { const model = (yield* ctx.catalog.model.list()).data.find( @@ -67,8 +67,10 @@ function make( const system = event.system[0] if (!system) return event.system[0] = { ...system, text: SessionSystemPrompt.render(template, Object.keys(event.tools)) } - }).pipe(Effect.catch(() => Effect.void)), - ) + }).pipe(Effect.catch(() => Effect.void)) + yield* ctx.session.hook("context", hook) + yield* ctx.session.hook("compaction", hook) + yield* ctx.session.hook("generate", hook) }), }) } diff --git a/packages/core/src/plugin/warming.ts b/packages/core/src/plugin/warming.ts index afc1465e83d..2c0698de00a 100644 --- a/packages/core/src/plugin/warming.ts +++ b/packages/core/src/plugin/warming.ts @@ -1,6 +1,7 @@ export * as WarmingPlugin from "./warming.js" import { define } from "@opencode/plugin/effect/plugin" +import type { SessionHooks } from "@opencode/plugin/effect/session" import type { Session } from "@opencode/schema/session" import { Clock, Duration, Effect, Scope } from "effect" import { Config } from "../config.js" @@ -54,7 +55,7 @@ export const Plugin = define({ }, ) - yield* ctx.session.hook("context", (event) => + const hook = (event: SessionHooks["context"]) => Effect.gen(function* () { const active = sessions.get(event.sessionID) const settings = yield* loadSettings() @@ -95,7 +96,9 @@ export const Plugin = define({ ), Effect.forkIn(scope), ) - }), - ) + }) + yield* ctx.session.hook("context", hook) + yield* ctx.session.hook("compaction", hook) + yield* ctx.session.hook("generate", hook) }), }) diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index 98488776aa6..d08e182d2dd 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -11,6 +11,7 @@ import { Message, type ContentPart, } from "@opencode/ai" +import type { SessionCompactionResult } from "@opencode/plugin/effect/session" import { SessionError } from "@opencode/schema/session-error" import { Context, Effect, Layer, Stream } from "effect" import { Bus } from "../bus.js" @@ -402,6 +403,36 @@ export const layer = Layer.effect( recent, inputID: input.inputID, }) + const supplied = Effect.fn("SessionCompaction.supplied")(function* ( + input: ExecuteInput, + result: SessionCompactionResult, + recent: string, + ) { + const context = input.context + const usage = result.tokens + ? { tokens: result.tokens, cost: SessionUsage.calculateCost(context.model.cost, result.tokens) } + : undefined + if (usage) + yield* bus.publish(SessionEvent.UsageRecorded, { + sessionID: context.session.id, + source: "compaction", + ...usage, + }) + yield* bus.publish( + SessionEvent.Compaction.Ended, + { + sessionID: context.session.id, + reason: input.reason, + model: context.model.ref, + providerState: result.providerState, + text: result.summary, + recent, + ...usage, + }, + { metadata: result.metadata }, + ) + return { status: "completed" as const } + }) // Manual controls settle through the inbox; only automatic work needs a durable interruption record. const interrupted = (input: ExecuteInput) => input.reason === "auto" @@ -456,6 +487,10 @@ export const layer = Layer.effect( error: { type: "provider.unsupported-operation", message }, }) const prepared = yield* compactionRequest(input, context.messages, [], "session") + if (prepared.event.result) { + yield* started(input, "") + return yield* supplied(input, prepared.event.result, "") + } const request = prepared.request const provenance = SessionProviderContext.provenance(context.model) if (!provenance) return yield* reject("Provider compaction requires a stable, configured endpoint") @@ -571,6 +606,7 @@ export const layer = Layer.effect( const prepared = yield* compactionRequest(input, history.messages, [ Message.user(buildPrompt(previous !== undefined, legacy)), ]) + if (prepared.event.result) return yield* supplied(input, prepared.event.result, history.recent) // Both requests share the retry allowance; rejected output never enters the reminder request. const transient = SessionRunnerRetry.transient(yield* SessionRunnerRetry.policy(context.session.id), { agent: context.agent.id, diff --git a/packages/core/src/session/message-updater.ts b/packages/core/src/session/message-updater.ts index 0177c095a84..eaf5ecaca12 100644 --- a/packages/core/src/session/message-updater.ts +++ b/packages/core/src/session/message-updater.ts @@ -409,6 +409,7 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) { yield* adapter.updateCompaction({ ...current, status: "completed", + metadata: event.metadata ? { ...current.metadata, ...event.metadata } : current.metadata, reason: event.data.reason, model: event.data.model, providerState: event.data.providerState, diff --git a/packages/core/src/session/model-request.ts b/packages/core/src/session/model-request.ts index a55f0b62a7c..428a5c3e47f 100644 --- a/packages/core/src/session/model-request.ts +++ b/packages/core/src/session/model-request.ts @@ -11,7 +11,14 @@ import { SystemPart, } from "@opencode/ai" import type { StreamOptions } from "@opencode/ai/route" -import type { SessionContext, SessionRequest, SessionRequestKind, SessionTitle } from "@opencode/plugin/effect/session" +import type { + SessionCompaction, + SessionContext, + SessionGenerate, + SessionRequest, + SessionRequestKind, + SessionTitle, +} from "@opencode/plugin/effect/session" import type { Agent } from "@opencode/schema/agent" import type { Model } from "@opencode/schema/model" import type { Content } from "@opencode/schema/tool" @@ -178,8 +185,8 @@ type Definitions = PluginHooks.Domains["session"]["context"]["tools"] /** Builds the model request for each session flow. Each entry runs its own plugin hook. */ export interface Interface { readonly primary: (input: Input) => Effect.Effect> - readonly compaction: (input: Input) => Effect.Effect> - readonly generate: (input: Input) => Effect.Effect> + readonly compaction: (input: Input) => Effect.Effect> + readonly generate: (input: Input) => Effect.Effect> readonly title: (input: Input) => Effect.Effect> } @@ -342,13 +349,14 @@ export const layer = Layer.effect( } }) - const context = (agent: Agent.ID) => (draft: SessionRequest, tools: Definitions) => - hooks.trigger("session", "context", { ...draft, agent, tools }) + const agentHook = + (name: "context" | "compaction" | "generate", agent: Agent.ID) => (draft: SessionRequest, tools: Definitions) => + hooks.trigger("session", name, { ...draft, agent, tools }) return Service.of({ - primary: (input) => prepare("primary", input, context(input.agent)), - generate: (input) => prepare("generate", input, context(input.agent)), - compaction: (input) => prepare("compaction", input, context(input.agent)), + primary: (input) => prepare("primary", input, agentHook("context", input.agent)), + compaction: (input) => prepare("compaction", input, agentHook("compaction", input.agent)), + generate: (input) => prepare("generate", input, agentHook("generate", input.agent)), title: (input) => prepare("title", input, (draft) => hooks.trigger("session", "title", draft)), }) }), diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index 45d09756c43..9c03fc65399 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -11,6 +11,7 @@ import { SessionCompaction } from "@opencode/core/session/compaction" import { SessionEvent } from "@opencode/core/session/event" import { SessionMessage } from "@opencode/core/session/message" import { SessionModelRequest } from "@opencode/core/session/model-request" +import { PluginHooks } from "@opencode/core/plugin/hooks" import { SessionProjector } from "@opencode/core/session/projector" import { SessionRunnerModel } from "@opencode/core/session/runner/model" import { SessionTable } from "@opencode/core/session/sql" @@ -85,6 +86,7 @@ const it = testEffect( SessionStore.node, SessionCompaction.node, SessionModelRequest.node, + PluginHooks.node, ]), [Bus.node.replace(Bus.configured({ persist: true })), llmClient.replace(client)], ), @@ -440,6 +442,65 @@ it.effect("manual compaction summarizes short context instead of no-op", () => }), ) +it.effect("compaction hooks can supply the summary instead of the model", () => + Effect.gen(function* () { + requests = [] + const db = (yield* Database.Service).db + const compaction = yield* SessionCompaction.Service + const hooks = yield* PluginHooks.Service + const store = yield* SessionStore.Service + const sessionID = Session.ID.make("ses_hooked_compaction") + const session = yield* insertSession(sessionID) + const modelRequests = yield* SessionModelRequest.Service + const messages = [ + { + id: SessionMessage.ID.create(), + type: "user" as const, + text: "Hooked compaction should see this conversation.", + time: { created: DateTime.makeUnsafe(0) }, + }, + ] + let contexts = 0 + yield* hooks.register("session", "context", () => Effect.sync(() => contexts++)) + yield* hooks.register("session", "compaction", (event) => + Effect.sync(() => { + expect(event.sessionID).toBe(sessionID) + expect(event.agent).toBe(Agent.defaultID) + expect(JSON.stringify(event.messages)).toContain("Hooked compaction should see this conversation.") + event.result = { summary: "## Objective\n- hooked summary" } + }), + ) + + expect( + yield* compaction.compactManual({ + session, + resolveContext: () => Effect.succeed(loaded(session, messages)), + prepare: modelRequests.compaction, + messages, + inputID: SessionMessage.ID.make("msg_hooked_compaction"), + }), + ).toEqual({ status: "completed" }) + + expect(contexts).toBe(0) + expect(requests).toEqual([]) + expect(yield* store.context(sessionID)).toMatchObject([ + { type: "compaction", reason: "manual", summary: "## Objective\n- hooked summary", recent: "" }, + ]) + expect( + yield* db + .select({ type: EventTable.type }) + .from(EventTable) + .where(eq(EventTable.aggregate_id, sessionID)) + .orderBy(asc(EventTable.seq)) + .all() + .pipe(Effect.orDie), + ).toEqual([ + { type: Bus.versionedType(SessionEvent.Compaction.Started.type, 1) }, + { type: Bus.versionedType(SessionEvent.Compaction.Ended.type, 1) }, + ]) + }), +) + it.effect("manual compaction records model resolution failures without calling the model", () => Effect.gen(function* () { requests = [] diff --git a/packages/core/test/session-generate.test.ts b/packages/core/test/session-generate.test.ts index 573390728f9..3b10c3ef875 100644 --- a/packages/core/test/session-generate.test.ts +++ b/packages/core/test/session-generate.test.ts @@ -1,5 +1,14 @@ import { expect } from "bun:test" -import { LLMClient, LLMEvent, LLMResponse, LanguageModel, ToolDefinition, type LLMRequest } from "@opencode/ai" +import { + LLMClient, + LLMEvent, + LLMResponse, + LanguageModel, + Message, + SystemPart, + ToolDefinition, + type LLMRequest, +} from "@opencode/ai" import { OpenAIChat } from "@opencode/ai/protocols" import type { StreamOptions } from "@opencode/ai/route" import { Agent } from "@opencode/core/agent" @@ -38,6 +47,7 @@ import { import { SessionStore } from "@opencode/core/session/store" import { SkillInstructions } from "@opencode/core/skill/instructions" import { Plugin } from "@opencode/core/plugin" +import { PluginHooks } from "@opencode/core/plugin/hooks" import { PluginSupervisor } from "@opencode/core/plugin/supervisor" import { Tool } from "@opencode/core/tool" import { asc, eq } from "drizzle-orm" @@ -134,6 +144,7 @@ const it = testEffect( Agent.node, InstructionBuiltIns.node, SessionContext.node, + PluginHooks.node, llmClient, ]), [ @@ -344,6 +355,43 @@ it.effect( { timeout: 15_000 }, ) +it.effect( + "runs generate hooks instead of context hooks", + () => + Effect.gen(function* () { + requests.length = 0 + instruction = "Initial context" + const { db, bus, instructions, session, instances } = yield* setup + yield* InstructionState.prepare(db, bus, instructions, sessionID) + const hooks = yield* PluginHooks.Service + let contexts = 0 + yield* hooks.register("session", "context", () => Effect.sync(() => contexts++)) + yield* hooks.register("session", "generate", (event) => + Effect.sync(() => { + expect(event.sessionID).toBe(sessionID) + expect(event.agent).toBe(Agent.ID.make("build")) + expect(Object.keys(event.tools)).toEqual(["lookup"]) + event.system.push(SystemPart.make("Answer briefly.")) + event.messages = [Message.user("[redacted]")] + event.options.maxTokens = 32 + event.options.reasoningEffort = "low" + }), + ) + + yield* SessionGenerate.generate({ session, prompt: "Summarize privately" }).pipe( + Effect.provideService(Instance.Service, instances), + ) + + expect(contexts).toBe(0) + expect(requests).toHaveLength(1) + expect(requests[0]?.system.map((part) => part.text)).toContain("Answer briefly.") + expect(userTexts(requests[0])).toEqual(["[redacted]"]) + expect(requests[0]?.generation).toEqual(expect.objectContaining({ maxTokens: 32 })) + expect(requests[0]?.providerOptions).toEqual({ reasoningEffort: "low" }) + }), + { timeout: 15_000 }, +) + it.effect( "blocks unavailable initial instructions before generation", () => diff --git a/packages/core/test/session-native-compaction.test.ts b/packages/core/test/session-native-compaction.test.ts index 18cd21a0c84..127bfc77852 100644 --- a/packages/core/test/session-native-compaction.test.ts +++ b/packages/core/test/session-native-compaction.test.ts @@ -400,6 +400,34 @@ it.live("only known automatic native overflow falls back locally and failed reco }), ) +it.live("compaction hooks supply the summary instead of provider compaction", () => + Effect.gen(function* () { + const fixture = yield* setup() + yield* fixture.prompt("Original user") + yield* fixture.hooks.register("session", "compaction", (event) => + Effect.sync(() => { + event.result = { + summary: "## Objective\n- hooked summary", + providerState: { responseId: "plugin" }, + metadata: { plugin: "custom" }, + tokens: { input: 10, output: 5, reasoning: 0, cache: { read: 0, write: 0 } }, + } + }), + ) + expect(yield* fixture.compact).toEqual({ status: "completed" }) + expect(fixture.state.calls).toBe(0) + expect((yield* fixture.load).messages.at(-1)).toMatchObject({ + type: "compaction", + status: "completed", + summary: "## Objective\n- hooked summary", + recent: "", + providerState: { responseId: "plugin" }, + metadata: { plugin: "custom" }, + tokens: { input: 10, output: 5 }, + }) + }), +) + it.live("rejects request-hook route rewrites before provider compaction", () => Effect.gen(function* () { const fixture = yield* setup() diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index ded6033757b..5906d6214fe 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -20,6 +20,7 @@ import { OpenAIChat } from "@opencode/ai/protocols/openai-chat" import { AnthropicMessages, OpenAIResponses } from "@opencode/ai/protocols" import { compileRequest } from "@opencode/ai/route/client" import { TestLLM } from "@opencode/ai/testing" +import type { SessionHooks } from "@opencode/plugin/effect/session" import { Catalog } from "@opencode/core/catalog" import { Database } from "@opencode/core/database/database" import { makeLocationNode } from "@opencode/util/effect/app-node" @@ -2411,15 +2412,16 @@ describe("SessionRunnerLLM", () => { model: { id: ID.make(s.currentModel.id), providerID: Provider.ID.make(s.currentModel.provider), variant }, }) const requestAgents: Agent.ID[] = [] - yield* hooks.register("session", "context", (event) => + const hook = (event: SessionHooks["context"]) => Effect.sync(() => { expect(event.agent).toBe(agentID) expect(event.model.variant).toBe(variant) event.system.push(SystemPart.make("Hook-provided instructions")) event.tools.echo.description = "Hook-provided tool description" event.options.maxTokens = 4_000 - }), - ) + }) + yield* hooks.register("session", "context", hook) + yield* hooks.register("session", "compaction", hook) yield* hooks.register("session", "model.request", (event) => Effect.sync(() => { requestAgents.push(event.agent) diff --git a/packages/plugin/src/effect/session.ts b/packages/plugin/src/effect/session.ts index a9cee6c76ca..0dd3ce1f2f5 100644 --- a/packages/plugin/src/effect/session.ts +++ b/packages/plugin/src/effect/session.ts @@ -7,6 +7,7 @@ import type { Session } from "@opencode/schema/session" import type { SessionInbox } from "@opencode/schema/session-inbox" import type { SessionError } from "@opencode/schema/session-error" import type { SessionMessage } from "@opencode/schema/session-message" +import type { TokenUsage } from "@opencode/schema/token-usage" import type { JsonSchema, Types } from "effect" import type { ModelHooks } from "./registration.js" @@ -34,6 +35,20 @@ export interface SessionContext extends SessionRequest { tools: Record } +export interface SessionCompactionResult { + summary: string + providerState?: SessionMessage.ProviderState + metadata?: Record + tokens?: TokenUsage.Info +} + +export interface SessionCompaction extends SessionContext { + /** Set to use this compaction and skip the model request. */ + result?: SessionCompactionResult +} + +export interface SessionGenerate extends SessionContext {} + export interface SessionTitle extends SessionRequest { /** Set to use this title and skip the model request. */ result?: string @@ -85,6 +100,8 @@ export interface SessionRetry { export interface SessionHooks { readonly prompt: SessionPrompt readonly context: SessionContext + readonly compaction: SessionCompaction + readonly generate: SessionGenerate readonly title: SessionTitle readonly "model.request": SessionModelRequest readonly "http.request": SessionHttpRequest diff --git a/packages/plugin/src/promise/session.ts b/packages/plugin/src/promise/session.ts index fc2a35f459c..c32b5cd2288 100644 --- a/packages/plugin/src/promise/session.ts +++ b/packages/plugin/src/promise/session.ts @@ -7,6 +7,7 @@ import type { Session } from "@opencode/schema/session" import type { SessionInbox } from "@opencode/schema/session-inbox" import type { SessionError } from "@opencode/schema/session-error" import type { SessionMessage } from "@opencode/schema/session-message" +import type { TokenUsage } from "@opencode/schema/token-usage" import type { JsonSchema, Types } from "effect" import type { ModelHooks } from "./registration.js" @@ -34,6 +35,20 @@ export interface SessionContext extends SessionRequest { tools: Record } +export interface SessionCompactionResult { + summary: string + providerState?: SessionMessage.ProviderState + metadata?: Record + tokens?: TokenUsage.Info +} + +export interface SessionCompaction extends SessionContext { + /** Set to use this compaction and skip the model request. */ + result?: SessionCompactionResult +} + +export interface SessionGenerate extends SessionContext {} + export interface SessionTitle extends SessionRequest { /** Set to use this title and skip the model request. */ result?: string @@ -85,6 +100,8 @@ export interface SessionRetry { export interface SessionHooks { readonly prompt: SessionPrompt readonly context: SessionContext + readonly compaction: SessionCompaction + readonly generate: SessionGenerate readonly title: SessionTitle readonly "model.request": SessionModelRequest readonly "http.request": SessionHttpRequest diff --git a/packages/server/test/session-instances.test.ts b/packages/server/test/session-instances.test.ts index 9bdd1360707..60f21771feb 100644 --- a/packages/server/test/session-instances.test.ts +++ b/packages/server/test/session-instances.test.ts @@ -13,6 +13,7 @@ import { Plugin } from "@opencode/core/plugin" import { Session } from "@opencode/core/session" import { SessionRunnerModel } from "@opencode/core/session/runner/model" import { define } from "@opencode/plugin/effect/plugin" +import type { SessionHooks } from "@opencode/plugin/effect/session" import { Agent } from "@opencode/schema/agent" import { Location } from "@opencode/schema/location" import { AbsolutePath } from "@opencode/schema/schema" @@ -92,11 +93,12 @@ it.live( event.prompt.text += ` [${config.tool}]` }), ) - yield* ctx.session.hook("context", (event) => + const tune = (event: SessionHooks["context"]) => Effect.sync(() => { event.options.temperature = config.temperature - }), - ) + }) + yield* ctx.session.hook("context", tune) + yield* ctx.session.hook("generate", tune) yield* ctx.permission.hook("evaluate", (event) => Effect.sync(() => { event.effect = event.action === "instance-test" ? "ask" : "allow" diff --git a/services/www/src/docs/content/build/plugins/effect.mdx b/services/www/src/docs/content/build/plugins/effect.mdx index 317fc15a784..2f04f24726e 100644 --- a/services/www/src/docs/content/build/plugins/effect.mdx +++ b/services/www/src/docs/content/build/plugins/effect.mdx @@ -1089,7 +1089,9 @@ effect: (ctx) => ### Sessions -Modify assembled system instructions, messages, or tools immediately before model dispatch. +Modify assembled system instructions, messages, or tools immediately before model dispatch. `context` runs for the +agent loop; `compaction`, `generate`, and `title` run for those auxiliary requests. `compaction` and `title` accept a +`result` that skips the model call. ```ts effect: (ctx) => @@ -1101,6 +1103,11 @@ effect: (ctx) => delete event.tools.write }), ) + yield* session.hook("compaction", (event) => + Effect.map(summarize(event.messages), (summary) => { + event.result = { summary } + }), + ) }), ``` @@ -1178,6 +1185,9 @@ Context-overflow recovery remains separate because it compacts the conversation ```ts interface SessionHooks { readonly context: SessionContext + readonly compaction: SessionCompaction + readonly generate: SessionGenerate + readonly title: SessionTitle readonly "model.request": SessionModelRequest readonly "http.request": SessionHttpRequest readonly "http.response": SessionHttpResponse diff --git a/services/www/src/docs/content/build/plugins/index.mdx b/services/www/src/docs/content/build/plugins/index.mdx index e5beaef18c4..c6b170be21d 100644 --- a/services/www/src/docs/content/build/plugins/index.mdx +++ b/services/www/src/docs/content/build/plugins/index.mdx @@ -1197,9 +1197,17 @@ Keep prompt hooks retry-safe. They are not an exactly-once side-effect boundary: - Concurrent submissions can run hooks more than once, but only the first successful admission wins. - Prompt hooks transform input and do not expose a typed rejection API. -#### Model context +#### Model requests -Modify assembled system instructions, messages, tools, or request options immediately before model dispatch. +Modify assembled system instructions, messages, tools, or request options immediately before model dispatch. Each +kind of request a session issues has its own hook, so a plugin can treat the agent loop and auxiliary requests +differently: + +- `context` runs for the agent loop, including tool-driven continuations. +- `compaction` runs for checkpoint summaries. Set `result` to record the compaction yourself and skip the model + call; it takes the same fields as a completed compaction message. +- `generate` runs for transient `ctx.session.generate` calls. +- `title` runs for title generation. It has no `agent` or `tools`. Set `result` to supply the title yourself. ```ts await ctx.session.hook("context", (event) => { @@ -1208,11 +1216,14 @@ await ctx.session.hook("context", (event) => { event.options.temperature = 0.2 event.options.maxTokens = 8_000 }) + +await ctx.session.hook("compaction", async (event) => { + event.result = { summary: await summarize(event.messages) } +}) ``` -Context changes affect only the outgoing model call, not persisted history or -configuration. The hook runs again for subsequent calls such as tool-driven -continuations, transient session generation, and compaction, but not for title requests. +Changes affect only the outgoing model call, not persisted history or configuration. A hook that should apply to +every request must register for each kind. Request overrides follow these rules: @@ -1321,6 +1332,9 @@ import type { SessionPrompt } from "@opencode/plugin/promise/session" interface SessionHooks { prompt: SessionPrompt context: SessionContextHook + compaction: SessionContextHook & { result?: SessionCompactionResult } + generate: SessionContextHook + title: SessionRequestHook & { result?: string } "model.request": SessionModelRequestHook "http.request": SessionHttpRequestHook "http.response": SessionHttpResponseHook @@ -1338,13 +1352,11 @@ interface SessionRetryHook { decision: RetryDecision } -interface SessionContextHook { +interface SessionRequestHook { readonly sessionID: string - readonly agent: string readonly model: { providerID: string; id: string; variant?: string } system: SystemPart[] messages: Message[] - tools: Record options: { maxTokens?: number temperature?: number @@ -1357,6 +1369,18 @@ interface SessionContextHook { } & Record } +interface SessionContextHook extends SessionRequestHook { + readonly agent: string + tools: Record +} + +interface SessionCompactionResult { + summary: string + providerState?: Record + metadata?: Record + tokens?: TokenUsage +} + interface SessionHookContext { hook( name: Name,