mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-04 16:06:23 +00:00
Compare commits
5
Commits
v2
...
context-kind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b1dcbb45c | ||
|
|
542e79efd5 | ||
|
|
7e228666e9 | ||
|
|
a52c7ab089 | ||
|
|
986ee8c060 |
@@ -11,7 +11,6 @@ import {
|
||||
Message,
|
||||
type ContentPart,
|
||||
} from "@opencode-ai/ai"
|
||||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { Context, Effect, Layer, Stream } from "effect"
|
||||
import { Bus } from "../bus.js"
|
||||
@@ -399,8 +398,7 @@ export const layer = Layer.effect(
|
||||
kind: "compaction",
|
||||
scope: {
|
||||
session: context.session,
|
||||
agentID: Agent.ID.make("compaction"),
|
||||
contextAgentID: context.agent.id,
|
||||
agentID: context.agent.id,
|
||||
model: context.model,
|
||||
tools: context.tools,
|
||||
},
|
||||
@@ -486,7 +484,7 @@ export const layer = Layer.effect(
|
||||
const decision = yield* retry({
|
||||
cause,
|
||||
error: toSessionError(cause),
|
||||
agent: Agent.ID.make("compaction"),
|
||||
agent: context.agent.id,
|
||||
model: context.model.ref,
|
||||
hook: prepared.retry,
|
||||
retry: SessionRunnerRetry.isRetryable(cause),
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
export * as SessionModelRequest from "./model-request.js"
|
||||
|
||||
import { HttpOptions, LanguageModel, LLM, LLMRequest, Message, SystemPart } from "@opencode-ai/ai"
|
||||
import {
|
||||
GenerationOptions,
|
||||
type GenerationOptionsFields,
|
||||
HttpOptions,
|
||||
LanguageModel,
|
||||
LLM,
|
||||
LLMRequest,
|
||||
Message,
|
||||
SystemPart,
|
||||
} from "@opencode-ai/ai"
|
||||
import type { StreamOptions } from "@opencode-ai/ai/route"
|
||||
import type { SessionRequestKind } from "@opencode-ai/plugin/effect/session"
|
||||
import type { SessionRequestKind, SessionRequestOptions } from "@opencode-ai/plugin/effect/session"
|
||||
import type { Agent } from "@opencode-ai/schema/agent"
|
||||
import type { Model } from "@opencode-ai/schema/model"
|
||||
import type { Content } from "@opencode-ai/schema/tool"
|
||||
@@ -26,6 +35,8 @@ const IMAGE_BYTES_TARGET = 15 * 1024 * 1024 // 15 MiB
|
||||
const IMAGE_REMOVED =
|
||||
"[This image was removed to reduce the request size and is no longer visible. Do not make claims about its contents from memory. If needed, retrieve it again with an available tool or ask the user to attach it again.]"
|
||||
|
||||
const GENERATION_KEYS = new Set(Object.keys(GenerationOptions.fields))
|
||||
|
||||
const responsesWebSocketFlag = (providerID: string) =>
|
||||
`OPENCODE_EXPERIMENTAL_${providerID.replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase()}_RESPONSES_WEBSOCKET`
|
||||
|
||||
@@ -49,7 +60,10 @@ const declineDefect = (cause: Cause.Cause<Tool.Error>) => {
|
||||
export interface Prepared {
|
||||
readonly request: LLMRequest
|
||||
readonly options: StreamOptions
|
||||
readonly retry: (event: PluginHooks.Domains["session"]["retry"]) => Effect.Effect<void>
|
||||
/** Runs retry hooks with this request's kind; the returned event carries the hooked decision. */
|
||||
readonly retry: (
|
||||
event: Omit<PluginHooks.Domains["session"]["retry"], "kind">,
|
||||
) => Effect.Effect<PluginHooks.Domains["session"]["retry"]>
|
||||
/**
|
||||
* One request-scoped execution operation. Unknown and hook-removed calls
|
||||
* fail individually through the same seam.
|
||||
@@ -65,8 +79,6 @@ interface PrepareInput {
|
||||
readonly scope: {
|
||||
readonly session: SessionSchema.Info
|
||||
readonly agentID: Agent.ID
|
||||
/** Agent whose context an auxiliary request reuses, without changing its request-hook identity. */
|
||||
readonly contextAgentID?: Agent.ID
|
||||
readonly model: SessionRunnerModel.Resolved
|
||||
/** Omitted for requests that carry no tool definitions, such as titles. */
|
||||
readonly tools?: Tool.Snapshot
|
||||
@@ -76,11 +88,6 @@ interface PrepareInput {
|
||||
readonly messages: Array<Message>
|
||||
}
|
||||
readonly toolChoice?: LLM.RequestInput["toolChoice"]
|
||||
/**
|
||||
* Session context hooks shape the agent conversation. Standalone requests
|
||||
* such as titles opt out; compaction uses the selected Session context.
|
||||
*/
|
||||
readonly contextHooks?: false
|
||||
/** Stateful Session WebSocket channels require an explicit durable-runner opt-in. */
|
||||
readonly webSocket?: "session"
|
||||
}
|
||||
@@ -305,17 +312,26 @@ export const layer = Layer.effect(
|
||||
)
|
||||
// Hooks mutate this record in place: edit descriptions and schemas, rename, or remove.
|
||||
const definitions = Object.fromEntries(Array.from(given, ([definition, tool]) => [tool.name, definition]))
|
||||
const context: PluginHooks.Domains["session"]["context"] = {
|
||||
const draft = {
|
||||
sessionID: session.id,
|
||||
agent: input.scope.contextAgentID ?? input.scope.agentID,
|
||||
agent: input.scope.agentID,
|
||||
model: resolved.ref,
|
||||
system: input.transcript.system,
|
||||
messages: input.transcript.messages,
|
||||
tools: definitions,
|
||||
generation: {},
|
||||
providerOptions: {},
|
||||
options: {} as SessionRequestOptions,
|
||||
}
|
||||
if (input.contextHooks !== false) yield* hooks.trigger("session", "context", context)
|
||||
// Titles are not part of the agent conversation and skip context hooks.
|
||||
const context =
|
||||
input.kind === "title" ? draft : yield* hooks.trigger("session", "context", { ...draft, kind: input.kind })
|
||||
// Typed generation keys and provider-semantic keys share one bag in the hook;
|
||||
// the request keeps them apart.
|
||||
const generation = Object.fromEntries(
|
||||
Object.entries(context.options).filter(([key]) => GENERATION_KEYS.has(key)),
|
||||
) as GenerationOptionsFields
|
||||
const providerOptions = Object.fromEntries(
|
||||
Object.entries(context.options).filter(([key]) => !GENERATION_KEYS.has(key)),
|
||||
)
|
||||
// Match each surviving entry back to its tool, by recognizing a moved definition or
|
||||
// by key. Identity wins so a definition moved onto another tool's name still executes
|
||||
// the tool it describes. Entries matching neither were invented by a hook and dropped.
|
||||
@@ -341,8 +357,8 @@ export const layer = Layer.effect(
|
||||
messages: boundImages(unsupportedParts(context.messages, resolved.capabilities)),
|
||||
tools: Array.from(hooked, ([name, tool]) => ({ ...tool, name })),
|
||||
toolChoice: input.toolChoice,
|
||||
generation: Object.keys(context.generation).length === 0 ? undefined : context.generation,
|
||||
providerOptions: Object.keys(context.providerOptions).length === 0 ? undefined : context.providerOptions,
|
||||
generation: Object.keys(generation).length === 0 ? undefined : generation,
|
||||
providerOptions: Object.keys(providerOptions).length === 0 ? undefined : providerOptions,
|
||||
}),
|
||||
)
|
||||
const hasHttpHooks =
|
||||
@@ -373,7 +389,7 @@ export const layer = Layer.effect(
|
||||
tools
|
||||
.execute({ ...input, definitions: hooked })
|
||||
.pipe(Effect.catchCauseFilter(declineDefect, (decline) => Effect.fail(decline)))
|
||||
const retry: Prepared["retry"] = (event) => hooks.trigger("session", "retry", event).pipe(Effect.asVoid)
|
||||
const retry: Prepared["retry"] = (event) => hooks.trigger("session", "retry", { ...event, kind: input.kind })
|
||||
return {
|
||||
request,
|
||||
options,
|
||||
|
||||
@@ -6,8 +6,8 @@ import { Model } from "@opencode-ai/schema/model"
|
||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { Clock, Duration, Effect, Pull, Schedule } from "effect"
|
||||
import { Bus } from "../../bus.js"
|
||||
import type { PluginHooks } from "../../plugin/hooks.js"
|
||||
import { SessionEvent } from "../event.js"
|
||||
import type { SessionModelRequest } from "../model-request.js"
|
||||
import { SessionMessage } from "../message.js"
|
||||
import { SessionSchema } from "../schema.js"
|
||||
|
||||
@@ -16,7 +16,7 @@ interface Input {
|
||||
readonly error: SessionError.Error
|
||||
readonly agent: Agent.ID
|
||||
readonly model: Model.Ref
|
||||
readonly hook: (event: PluginHooks.Domains["session"]["retry"]) => Effect.Effect<void>
|
||||
readonly hook: SessionModelRequest.Prepared["retry"]
|
||||
readonly retry: boolean
|
||||
}
|
||||
|
||||
@@ -90,15 +90,14 @@ export const policy = (sessionID: SessionSchema.ID) =>
|
||||
const [, duration] = next
|
||||
attempt++
|
||||
const delay = Math.ceil(Duration.toMillis(duration))
|
||||
const event: PluginHooks.Domains["session"]["retry"] = {
|
||||
const event = yield* input.hook({
|
||||
sessionID,
|
||||
agent: input.agent,
|
||||
model: input.model,
|
||||
error: input.error,
|
||||
attempt,
|
||||
decision: input.retry ? { retry: true, delay } : { retry: false },
|
||||
}
|
||||
yield* input.hook(event)
|
||||
})
|
||||
if (!event.decision.retry) return event.decision
|
||||
const normalized =
|
||||
Number.isFinite(event.decision.delay) && event.decision.delay >= 0 ? Math.ceil(event.decision.delay) : delay
|
||||
|
||||
@@ -70,7 +70,6 @@ export const layer = Layer.effect(
|
||||
system: input.agent.system ? [SystemPart.make(input.agent.system)] : [],
|
||||
messages: [Message.user(input.text)],
|
||||
},
|
||||
contextHooks: false,
|
||||
})
|
||||
yield* llm.stream(prepared.request, prepared.options).pipe(
|
||||
Stream.runForEach((event) => {
|
||||
|
||||
@@ -121,11 +121,11 @@ const request = (agent: Agent.ID, messages: Array<Message>): SessionContext => (
|
||||
sessionID,
|
||||
agent,
|
||||
model: { id: Model.ID.make("test-model"), providerID: Provider.ID.make("test") },
|
||||
kind: "primary",
|
||||
system: [],
|
||||
messages,
|
||||
tools: {},
|
||||
generation: {},
|
||||
providerOptions: {},
|
||||
options: {},
|
||||
})
|
||||
|
||||
type ToolErrorEvent = Extract<ToolHooks["execute.after"], { readonly status: "error" }>
|
||||
|
||||
@@ -29,11 +29,11 @@ const context = (id: string, system = fallback): SessionHooks["context"] => ({
|
||||
sessionID: Session.ID.make("ses_system_prompt"),
|
||||
agent: Agent.ID.make("build"),
|
||||
model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make(id) }),
|
||||
kind: "primary",
|
||||
system: [SystemPart.make(system)],
|
||||
messages: [],
|
||||
tools: {},
|
||||
generation: {},
|
||||
providerOptions: {},
|
||||
options: {},
|
||||
})
|
||||
|
||||
describe("SystemPromptPlugin", () => {
|
||||
|
||||
@@ -2124,19 +2124,19 @@ describe("SessionRunnerLLM", () => {
|
||||
sessionID,
|
||||
model: { id: ID.make(s.currentModel.id), providerID: Provider.ID.make(s.currentModel.provider), variant },
|
||||
})
|
||||
const requestAgents: Agent.ID[] = []
|
||||
const hookRequests: Array<{ agent: Agent.ID; kind: string }> = []
|
||||
yield* hooks.register("session", "context", (event) =>
|
||||
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.generation.maxTokens = 4_000
|
||||
event.options.maxTokens = 4_000
|
||||
}),
|
||||
)
|
||||
yield* hooks.register("session", "model.request", (event) =>
|
||||
Effect.sync(() => {
|
||||
requestAgents.push(event.agent)
|
||||
hookRequests.push({ agent: event.agent, kind: event.kind })
|
||||
}),
|
||||
)
|
||||
yield* s.llm.push(
|
||||
@@ -2182,7 +2182,7 @@ describe("SessionRunnerLLM", () => {
|
||||
expect(compact[field]).toEqual(normal[field])
|
||||
expect(compact.toolChoice).toBeUndefined()
|
||||
expect(compact.system.map((part) => part.text)).toContain("Review the project carefully.")
|
||||
expect(requestAgents[2]).toBe(Agent.ID.make("compaction"))
|
||||
expect(hookRequests[2]).toEqual({ agent: agentID, kind: "compaction" })
|
||||
expect(s.executions).toEqual(["x".repeat(4_000)])
|
||||
expect((yield* s.messages).find((message) => message.type === "compaction")).toMatchObject({
|
||||
model: { id: s.currentModel.id, providerID: s.currentModel.provider, variant },
|
||||
@@ -2297,7 +2297,7 @@ describe("SessionRunnerLLM", () => {
|
||||
expect(s.requests).toHaveLength(5)
|
||||
for (const request of s.requests) expect(request).toEqual(s.requests[0])
|
||||
expect(retries.map((event) => event.attempt)).toEqual([2, 3, 4, 5])
|
||||
expect(retries.every((event) => event.sessionID === sessionID && event.agent === "compaction")).toBe(true)
|
||||
expect(retries.every((event) => event.sessionID === sessionID && event.kind === "compaction")).toBe(true)
|
||||
expect(retries[3].decision).toEqual({ retry: true, delay: 60_000 })
|
||||
expect((yield* s.messages).find((message) => message.id === compaction.id)).toMatchObject({
|
||||
status: "completed",
|
||||
|
||||
@@ -105,7 +105,7 @@ for (const fixture of [
|
||||
agent: Agent.defaultID,
|
||||
model,
|
||||
prepared: {
|
||||
retry: () => Effect.void,
|
||||
retry: (event) => Effect.succeed({ ...event, kind: "primary" as const }),
|
||||
request: LLM.request({ model: model.model, prompt: "Run one tool", toolChoice: fixture.toolChoice }),
|
||||
options: {},
|
||||
executeTool: () =>
|
||||
|
||||
@@ -232,6 +232,26 @@ it.effect("generates a title from the sole user message and renames the session"
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("does not run context hooks for title requests", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* enableTitleAgent
|
||||
const sessionID = Session.ID.make("ses_title_context_hook")
|
||||
yield* insertSession(sessionID)
|
||||
yield* prompt(sessionID, "Help me debug the failing build")
|
||||
|
||||
const hooks = yield* PluginHooks.Service
|
||||
let calls = 0
|
||||
yield* hooks.register("session", "context", () => Effect.sync(() => calls++))
|
||||
yield* hooks.register("session", "model.request", (event) => Effect.sync(() => expect(event.kind).toBe("title")))
|
||||
|
||||
const title = yield* SessionTitle.Service
|
||||
yield* title.generate(sessionID)
|
||||
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(calls).toBe(0)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("uses a small model from the primary provider", () =>
|
||||
Effect.gen(function* () {
|
||||
selectedSmall = small
|
||||
|
||||
@@ -18,24 +18,32 @@ export interface SessionPrompt {
|
||||
delivery: SessionInbox.Delivery
|
||||
}
|
||||
|
||||
export interface SessionContext {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly model: Model.Ref
|
||||
system: Array<SystemPart>
|
||||
messages: Array<Message>
|
||||
tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
|
||||
/** Request overrides; unset fields retain route and model defaults. */
|
||||
generation: Types.DeepMutable<GenerationOptionsFields>
|
||||
providerOptions: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Why a Session request is being made. Auxiliary requests share the Session's
|
||||
* hook identity but need to be told apart from the agent loop.
|
||||
*/
|
||||
export type SessionRequestKind = "primary" | "compaction" | "title" | "generate"
|
||||
|
||||
/**
|
||||
* Request overrides. Typed keys are the protocol-neutral generation settings;
|
||||
* any other key is passed to the selected protocol as a provider option under its
|
||||
* semantic name, such as `reasoningEffort` for OpenAI Responses. Unset fields
|
||||
* retain route and model defaults.
|
||||
*/
|
||||
export type SessionRequestOptions = Types.DeepMutable<GenerationOptionsFields> & Record<string, unknown>
|
||||
|
||||
export interface SessionContext {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly model: Model.Ref
|
||||
/** Titles do not run context hooks; they will get a dedicated hook. */
|
||||
readonly kind: Exclude<SessionRequestKind, "title">
|
||||
system: Array<SystemPart>
|
||||
messages: Array<Message>
|
||||
tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
|
||||
options: SessionRequestOptions
|
||||
}
|
||||
|
||||
export interface SessionModelRequest {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
@@ -68,6 +76,7 @@ export interface SessionRetry {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly model: Model.Ref
|
||||
readonly kind: SessionRequestKind
|
||||
readonly error: SessionError.Error
|
||||
readonly attempt: number
|
||||
decision: SessionRetryDecision
|
||||
|
||||
@@ -18,24 +18,32 @@ export interface SessionPrompt {
|
||||
delivery: SessionInbox.Delivery
|
||||
}
|
||||
|
||||
export interface SessionContext {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly model: Model.Ref
|
||||
system: Array<SystemPart>
|
||||
messages: Array<Message>
|
||||
tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
|
||||
/** Request overrides; unset fields retain route and model defaults. */
|
||||
generation: Types.DeepMutable<GenerationOptionsFields>
|
||||
providerOptions: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Why a Session request is being made. Auxiliary requests share the Session's
|
||||
* hook identity but need to be told apart from the agent loop.
|
||||
*/
|
||||
export type SessionRequestKind = "primary" | "compaction" | "title" | "generate"
|
||||
|
||||
/**
|
||||
* Request overrides. Typed keys are the protocol-neutral generation settings;
|
||||
* any other key is passed to the selected protocol as a provider option under its
|
||||
* semantic name, such as `reasoningEffort` for OpenAI Responses. Unset fields
|
||||
* retain route and model defaults.
|
||||
*/
|
||||
export type SessionRequestOptions = Types.DeepMutable<GenerationOptionsFields> & Record<string, unknown>
|
||||
|
||||
export interface SessionContext {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly model: Model.Ref
|
||||
/** Titles do not run context hooks; they will get a dedicated hook. */
|
||||
readonly kind: Exclude<SessionRequestKind, "title">
|
||||
system: Array<SystemPart>
|
||||
messages: Array<Message>
|
||||
tools: Record<string, { description: string; input: JsonSchema.JsonSchema }>
|
||||
options: SessionRequestOptions
|
||||
}
|
||||
|
||||
export interface SessionModelRequest {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
@@ -68,6 +76,7 @@ export interface SessionRetry {
|
||||
readonly sessionID: Session.ID
|
||||
readonly agent: Agent.ID
|
||||
readonly model: Model.Ref
|
||||
readonly kind: SessionRequestKind
|
||||
readonly error: SessionError.Error
|
||||
readonly attempt: number
|
||||
decision: SessionRetryDecision
|
||||
|
||||
@@ -69,7 +69,7 @@ it.live(
|
||||
)
|
||||
yield* ctx.session.hook("context", (event) =>
|
||||
Effect.sync(() => {
|
||||
event.generation.temperature = 0.25
|
||||
event.options.temperature = 0.25
|
||||
}),
|
||||
)
|
||||
yield* ctx.tool.transform((editor) =>
|
||||
|
||||
@@ -94,7 +94,7 @@ it.live(
|
||||
)
|
||||
yield* ctx.session.hook("context", (event) =>
|
||||
Effect.sync(() => {
|
||||
event.generation.temperature = config.temperature
|
||||
event.options.temperature = config.temperature
|
||||
}),
|
||||
)
|
||||
yield* ctx.permission.hook("evaluate", (event) =>
|
||||
|
||||
@@ -1089,7 +1089,10 @@ effect: (ctx) =>
|
||||
|
||||
### Sessions
|
||||
|
||||
Modify assembled system instructions, messages, or tools immediately before model dispatch.
|
||||
Modify assembled system instructions, messages, tools, or request options immediately before model dispatch. The hook
|
||||
runs for every request that carries the session conversation; `event.kind` is `"primary"`, `"compaction"`, or
|
||||
`"generate"`. Title requests do not run context hooks.
|
||||
Typed `options` keys are generation settings; any other key is passed to the protocol as a provider option.
|
||||
|
||||
```ts
|
||||
effect: (ctx) =>
|
||||
@@ -1097,8 +1100,10 @@ effect: (ctx) =>
|
||||
const session = ctx.session
|
||||
yield* session.hook("context", (event) =>
|
||||
Effect.sync(() => {
|
||||
if (event.kind === "compaction") return
|
||||
event.system.push({ text: "Keep the review focused on correctness." })
|
||||
delete event.tools.write
|
||||
event.options.maxTokens = 8_000
|
||||
}),
|
||||
)
|
||||
}),
|
||||
@@ -1186,10 +1191,13 @@ interface SessionHooks {
|
||||
|
||||
type RetryDecision = { retry: false } | { retry: true; delay: number }
|
||||
|
||||
type SessionRequestKind = "primary" | "compaction" | "title" | "generate"
|
||||
|
||||
interface SessionRetry {
|
||||
readonly sessionID: string
|
||||
readonly agent: string
|
||||
readonly model: { providerID: string; id: string; variant?: string }
|
||||
readonly kind: SessionRequestKind
|
||||
readonly error: { type: string; message: string; status?: number }
|
||||
readonly attempt: number
|
||||
decision: RetryDecision
|
||||
|
||||
@@ -1089,28 +1089,34 @@ Keep prompt hooks retry-safe. They are not an exactly-once side-effect boundary:
|
||||
|
||||
#### Model context
|
||||
|
||||
Modify assembled system instructions, messages, tools, generation settings, or provider options immediately before model
|
||||
dispatch.
|
||||
Modify assembled system instructions, messages, tools, or request options immediately before model dispatch.
|
||||
|
||||
```ts
|
||||
await ctx.session.hook("context", (event) => {
|
||||
event.system.push({ text: "Keep the review focused on correctness." })
|
||||
delete event.tools.write
|
||||
event.generation.temperature = 0.2
|
||||
event.generation.maxTokens = 8_000
|
||||
event.options.temperature = 0.2
|
||||
event.options.maxTokens = 8_000
|
||||
})
|
||||
```
|
||||
|
||||
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.
|
||||
Context changes affect only the outgoing model call, not persisted history or configuration. The hook runs for every
|
||||
request that carries the session conversation; `event.kind` says which flow issued it: `"primary"` for the agent loop,
|
||||
`"compaction"` for checkpoint summaries, and `"generate"` for transient `ctx.session.generate` calls. Title requests do
|
||||
not run context hooks. `event.agent` is always the session's selected agent.
|
||||
|
||||
Compaction context hooks receive the selected session agent. Its model-request
|
||||
and HTTP hooks retain the `compaction` agent identity for provider-specific handling.
|
||||
```ts
|
||||
await ctx.session.hook("context", (event) => {
|
||||
if (event.kind === "compaction") return
|
||||
event.messages.push({ role: "user", content: [{ type: "text", text: "Reminder: prefer small diffs." }] })
|
||||
})
|
||||
```
|
||||
|
||||
Request overrides follow these rules:
|
||||
Request options follow these rules:
|
||||
|
||||
- `generation` and `providerOptions` start empty for each model call; they do not contain resolved model settings.
|
||||
- `options` starts empty for each model call; it does not contain resolved model settings.
|
||||
- Typed keys (`maxTokens`, `temperature`, `topP`, `topK`, `frequencyPenalty`, `presencePenalty`, `seed`, `stop`) are the
|
||||
protocol-neutral generation settings. Any other key is passed to the selected protocol as a provider option.
|
||||
- Hooks run in registration order and see overrides made by earlier hooks.
|
||||
- Request overrides take precedence over model defaults, which take precedence over route defaults.
|
||||
- Provider option objects merge recursively; arrays and scalar values replace earlier values.
|
||||
@@ -1126,7 +1132,7 @@ settings to the matching provider. For example, OpenAI Responses uses `reasoning
|
||||
await ctx.session.hook(
|
||||
"context",
|
||||
(event) => {
|
||||
event.providerOptions.reasoningEffort = "high"
|
||||
event.options.reasoningEffort = "high"
|
||||
},
|
||||
{ providerID: "openai" },
|
||||
)
|
||||
@@ -1222,33 +1228,38 @@ interface SessionHooks {
|
||||
|
||||
type RetryDecision = { retry: false } | { retry: true; delay: number }
|
||||
|
||||
type SessionRequestKind = "primary" | "compaction" | "title" | "generate"
|
||||
|
||||
interface SessionRetryHook {
|
||||
readonly sessionID: string
|
||||
readonly agent: string
|
||||
readonly model: { providerID: string; id: string; variant?: string }
|
||||
readonly kind: SessionRequestKind
|
||||
readonly error: { type: string; message: string; status?: number }
|
||||
readonly attempt: number
|
||||
decision: RetryDecision
|
||||
}
|
||||
|
||||
type SessionRequestOptions = {
|
||||
maxTokens?: number
|
||||
temperature?: number
|
||||
topP?: number
|
||||
topK?: number
|
||||
frequencyPenalty?: number
|
||||
presencePenalty?: number
|
||||
seed?: number
|
||||
stop?: string[]
|
||||
} & Record<string, unknown>
|
||||
|
||||
interface SessionContextHook {
|
||||
readonly sessionID: string
|
||||
readonly agent: string
|
||||
readonly model: { providerID: string; id: string; variant?: string }
|
||||
readonly kind: Exclude<SessionRequestKind, "title">
|
||||
system: SystemPart[]
|
||||
messages: Message[]
|
||||
tools: Record<string, { description: string; input: JsonSchema }>
|
||||
generation: {
|
||||
maxTokens?: number
|
||||
temperature?: number
|
||||
topP?: number
|
||||
topK?: number
|
||||
frequencyPenalty?: number
|
||||
presencePenalty?: number
|
||||
seed?: number
|
||||
stop?: string[]
|
||||
}
|
||||
providerOptions: Record<string, unknown>
|
||||
options: SessionRequestOptions
|
||||
}
|
||||
|
||||
interface SessionHookContext {
|
||||
|
||||
@@ -80,7 +80,8 @@ preserves more recent detail but leaves less room for future work. Larger
|
||||
V2 uses the session's selected agent, model, and variant to generate the summary.
|
||||
The request reuses the normal instructions, tool definitions, and structured
|
||||
history prefix, then appends a user message requesting a checkpoint. Context
|
||||
hooks run as they do for normal session requests.
|
||||
hooks run as they do for normal session requests, with `kind` set to
|
||||
`"compaction"`.
|
||||
|
||||
Compaction does not dispatch local tool calls or override tool choice. The
|
||||
summary must contain at least one heading from the requested template, such as
|
||||
|
||||
Reference in New Issue
Block a user