refactor(core): extract session model request preparation (#37210)
This commit is contained in:
@@ -34,13 +34,19 @@ export interface Loaded {
|
|||||||
readonly messages: ReadonlyArray<SessionMessage.Info>
|
readonly messages: ReadonlyArray<SessionMessage.Info>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves model-request state in two phases: `select` fixes the Session,
|
||||||
|
* agent, and instruction sources; `load` adds the model and active history for
|
||||||
|
* that selection. This module does not build or execute the model request.
|
||||||
|
*/
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
/** Resolves the Session, selected agent, and current instruction sources before durable Step preparation. */
|
/** Selects the Session, agent, and instruction sources used by subsequent work. */
|
||||||
readonly select: (sessionID: SessionSchema.ID) => Effect.Effect<Selection, AgentNotFoundError>
|
readonly select: (sessionID: SessionSchema.ID) => Effect.Effect<Selection, AgentNotFoundError>
|
||||||
/** Loads the selected model and active history after instruction sync and pending-input promotion. */
|
/** Resolves the model and active history for that selection. */
|
||||||
readonly load: (selection: Selection) => Effect.Effect<Loaded, SessionRunnerModel.Error>
|
readonly load: (selection: Selection) => Effect.Effect<Loaded, SessionRunnerModel.Error>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Location-scoped model-context loader for durable Session Steps. */
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionContext") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionContext") {}
|
||||||
|
|
||||||
const layer = Layer.effect(
|
const layer = Layer.effect(
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
export * as SessionModelRequest from "./model-request"
|
||||||
|
|
||||||
|
import { LLM, Message, SystemPart, type LLMRequest } from "@opencode-ai/ai"
|
||||||
|
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||||
|
import { Context, Effect, Layer } from "effect"
|
||||||
|
import { makeLocationNode } from "../effect/app-node"
|
||||||
|
import { PluginHooks } from "../plugin/hooks"
|
||||||
|
import { ToolRegistry } from "../tool/registry"
|
||||||
|
import { SessionContext } from "./context"
|
||||||
|
import { SessionModelHeaders } from "./model-headers"
|
||||||
|
import { MAX_STEPS_PROMPT } from "./runner/max-steps"
|
||||||
|
import PROMPT_DEFAULT from "./runner/prompt/base.txt"
|
||||||
|
import { toLLMMessages } from "./runner/to-llm-message"
|
||||||
|
|
||||||
|
type ToolCallResolution =
|
||||||
|
| { readonly type: "reject"; readonly error: SessionError.Error }
|
||||||
|
| { readonly type: "settle"; readonly settle: ToolRegistry.Materialization["settle"] }
|
||||||
|
|
||||||
|
interface Prepared {
|
||||||
|
readonly request: LLMRequest
|
||||||
|
readonly resolveToolCall: (name: string) => ToolCallResolution
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PrepareInput {
|
||||||
|
readonly context: SessionContext.Loaded
|
||||||
|
readonly step: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an outbound model request and captures the tool-call capability that
|
||||||
|
* must remain paired with it. It does not execute the request or mutate
|
||||||
|
* Session state.
|
||||||
|
*/
|
||||||
|
export interface Interface {
|
||||||
|
/** Builds one outbound model request and its matching tool-call capability. */
|
||||||
|
readonly prepare: (input: PrepareInput) => Effect.Effect<Prepared>
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Location-scoped outbound model-request preparation. */
|
||||||
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SessionModelRequest") {}
|
||||||
|
|
||||||
|
const layer = Layer.effect(
|
||||||
|
Service,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const hooks = yield* PluginHooks.Service
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
|
||||||
|
const prepare = Effect.fn("SessionModelRequest.prepare")(function* (input: PrepareInput) {
|
||||||
|
const session = input.context.session
|
||||||
|
const agent = input.context.agent
|
||||||
|
const resolved = input.context.model
|
||||||
|
const model = resolved.model
|
||||||
|
const providerMetadataKey = model.route.providerMetadataKey ?? model.provider
|
||||||
|
const stepLimitReached = agent.info.steps !== undefined && input.step >= agent.info.steps
|
||||||
|
const executableTools = stepLimitReached ? undefined : yield* registry.materialize(agent.info.permissions)
|
||||||
|
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id
|
||||||
|
const system = [agent.info.system ? agent.info.system : PROMPT_DEFAULT, input.context.initial]
|
||||||
|
.filter((part) => part.length > 0)
|
||||||
|
.map(SystemPart.make)
|
||||||
|
const history = toLLMMessages(input.context.messages, resolved.ref, providerMetadataKey)
|
||||||
|
const messages = stepLimitReached ? [...history, Message.assistant(MAX_STEPS_PROMPT)] : history
|
||||||
|
const toolDefinitions = executableTools?.definitions ?? []
|
||||||
|
const toolsByName = new Map(toolDefinitions.map((tool) => [tool.name, tool]))
|
||||||
|
// Hooks may reshape available definitions but cannot advertise tools omitted by permissions or the Step limit.
|
||||||
|
const contextEvent = yield* hooks.trigger("session", "context", {
|
||||||
|
sessionID: session.id,
|
||||||
|
agent: agent.id,
|
||||||
|
model: resolved.ref,
|
||||||
|
system,
|
||||||
|
messages,
|
||||||
|
tools: Object.fromEntries(
|
||||||
|
toolDefinitions.map((tool) => [tool.name, { description: tool.description, input: { ...tool.inputSchema } }]),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
const hookedTools = Object.entries(contextEvent.tools).flatMap(([name, tool]) => {
|
||||||
|
const registered = toolsByName.get(name)
|
||||||
|
return registered
|
||||||
|
? [Object.assign({}, registered, { description: tool.description, inputSchema: tool.input })]
|
||||||
|
: []
|
||||||
|
})
|
||||||
|
const request = LLM.request({
|
||||||
|
model,
|
||||||
|
http: {
|
||||||
|
headers: SessionModelHeaders.make(session),
|
||||||
|
},
|
||||||
|
providerOptions: { openai: { promptCacheKey } },
|
||||||
|
system: contextEvent.system,
|
||||||
|
messages: contextEvent.messages,
|
||||||
|
tools: hookedTools,
|
||||||
|
toolChoice: stepLimitReached ? "none" : undefined,
|
||||||
|
})
|
||||||
|
const resolveToolCall = (name: string): ToolCallResolution => {
|
||||||
|
if (!executableTools)
|
||||||
|
return {
|
||||||
|
type: "reject",
|
||||||
|
error: { type: "tool.execution", message: "Tools are disabled after the maximum agent steps" },
|
||||||
|
}
|
||||||
|
if (toolsByName.has(name) && !Object.hasOwn(contextEvent.tools, name))
|
||||||
|
return {
|
||||||
|
type: "reject",
|
||||||
|
error: { type: "tool.execution", message: `Tool is not available for this request: ${name}` },
|
||||||
|
}
|
||||||
|
return { type: "settle", settle: executableTools.settle }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
request,
|
||||||
|
resolveToolCall,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Service.of({ prepare })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const node = makeLocationNode({
|
||||||
|
service: Service,
|
||||||
|
layer,
|
||||||
|
deps: [PluginHooks.node, ToolRegistry.node],
|
||||||
|
})
|
||||||
@@ -1,16 +1,6 @@
|
|||||||
export * as SessionRunnerLLM from "./llm"
|
export * as SessionRunnerLLM from "./llm"
|
||||||
|
|
||||||
import {
|
import { LLMClient, LLMError, LLMEvent, isContextOverflowFailure, type ProviderErrorEvent } from "@opencode-ai/ai"
|
||||||
LLM,
|
|
||||||
LLMClient,
|
|
||||||
LLMError,
|
|
||||||
LLMEvent,
|
|
||||||
Message,
|
|
||||||
SystemPart,
|
|
||||||
isContextOverflowFailure,
|
|
||||||
type ProviderErrorEvent,
|
|
||||||
} from "@opencode-ai/ai"
|
|
||||||
import type { SessionHooks } from "@opencode-ai/plugin/v2/effect/session"
|
|
||||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||||
import { Money } from "@opencode-ai/schema/money"
|
import { Money } from "@opencode-ai/schema/money"
|
||||||
import { Cause, Effect, Exit, Fiber, FiberSet, Layer, Option, Semaphore, Stream } from "effect"
|
import { Cause, Effect, Exit, Fiber, FiberSet, Layer, Option, Semaphore, Stream } from "effect"
|
||||||
@@ -19,30 +9,25 @@ import { EventV2 } from "../../event"
|
|||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
import { PermissionV2 } from "../../permission"
|
import { PermissionV2 } from "../../permission"
|
||||||
import { QuestionTool } from "../../tool/question"
|
import { QuestionTool } from "../../tool/question"
|
||||||
import { ToolRegistry } from "../../tool/registry"
|
|
||||||
import { ToolOutputStore } from "../../tool-output-store"
|
import { ToolOutputStore } from "../../tool-output-store"
|
||||||
import { InstructionState } from "../instruction-state"
|
import { InstructionState } from "../instruction-state"
|
||||||
import { SessionCompaction } from "../compaction"
|
import { SessionCompaction } from "../compaction"
|
||||||
import { SessionContext } from "../context"
|
import { SessionContext } from "../context"
|
||||||
import { SessionEvent } from "../event"
|
import { SessionEvent } from "../event"
|
||||||
import { SessionPending } from "../pending"
|
import { SessionPending } from "../pending"
|
||||||
|
import { SessionModelRequest } from "../model-request"
|
||||||
import { SessionMessage } from "../message"
|
import { SessionMessage } from "../message"
|
||||||
import { SessionSchema } from "../schema"
|
import { SessionSchema } from "../schema"
|
||||||
import { SessionStore } from "../store"
|
import { SessionStore } from "../store"
|
||||||
import { SessionTitle } from "../title"
|
import { SessionTitle } from "../title"
|
||||||
import { Service } from "./index"
|
import { Service } from "./index"
|
||||||
import { createLLMEventPublisher } from "./publish-llm-event"
|
import { createLLMEventPublisher } from "./publish-llm-event"
|
||||||
import { toLLMMessages } from "./to-llm-message"
|
|
||||||
import { MAX_STEPS_PROMPT } from "./max-steps"
|
|
||||||
import PROMPT_DEFAULT from "./prompt/base.txt"
|
|
||||||
import { Snapshot } from "../../snapshot"
|
import { Snapshot } from "../../snapshot"
|
||||||
import { makeLocationNode } from "../../effect/app-node"
|
import { makeLocationNode } from "../../effect/app-node"
|
||||||
import { llmClient } from "../../effect/app-node-platform"
|
import { llmClient } from "../../effect/app-node-platform"
|
||||||
import { StepFailedError } from "../error"
|
import { StepFailedError } from "../error"
|
||||||
import { toSessionError } from "../to-session-error"
|
import { toSessionError } from "../to-session-error"
|
||||||
import { SessionRunnerRetry } from "./retry"
|
import { SessionRunnerRetry } from "./retry"
|
||||||
import { PluginHooks } from "../../plugin/hooks"
|
|
||||||
import { SessionModelHeaders } from "../model-headers"
|
|
||||||
|
|
||||||
type StepTokens = {
|
type StepTokens = {
|
||||||
readonly input: number
|
readonly input: number
|
||||||
@@ -68,20 +53,14 @@ export function calculateCost(costs: ModelV2.Info["cost"], tokens: StepTokens) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs one durable coding-agent Session until it settles. Each step reloads projected history,
|
|
||||||
* materializes tools, makes one model request, and settles local calls before continuation.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const layer = Layer.effect(
|
const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const llm = yield* LLMClient.Service
|
const llm = yield* LLMClient.Service
|
||||||
const tools = yield* ToolRegistry.Service
|
|
||||||
const hooks = yield* PluginHooks.Service
|
|
||||||
const store = yield* SessionStore.Service
|
const store = yield* SessionStore.Service
|
||||||
const context = yield* SessionContext.Service
|
const context = yield* SessionContext.Service
|
||||||
|
const modelRequests = yield* SessionModelRequest.Service
|
||||||
const snapshots = yield* Snapshot.Service
|
const snapshots = yield* Snapshot.Service
|
||||||
const db = (yield* Database.Service).db
|
const db = (yield* Database.Service).db
|
||||||
const compaction = yield* SessionCompaction.Service
|
const compaction = yield* SessionCompaction.Service
|
||||||
@@ -146,59 +125,18 @@ const layer = Layer.effect(
|
|||||||
const loaded = yield* context.load(selected)
|
const loaded = yield* context.load(selected)
|
||||||
const session = loaded.session
|
const session = loaded.session
|
||||||
const agent = loaded.agent
|
const agent = loaded.agent
|
||||||
const agentInfo = agent.info
|
|
||||||
const resolved = loaded.model
|
const resolved = loaded.model
|
||||||
const model = resolved.model
|
const model = resolved.model
|
||||||
const providerMetadataKey = model.route.providerMetadataKey ?? model.provider
|
|
||||||
const compactionInput = { session, messages: loaded.messages, model }
|
const compactionInput = { session, messages: loaded.messages, model }
|
||||||
if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) {
|
if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) {
|
||||||
const compacted = yield* compaction.compact(compactionInput)
|
const compacted = yield* compaction.compact(compactionInput)
|
||||||
if (compacted.status === "completed") return { _tag: "RestartAfterCompaction", step: currentStep } as const
|
if (compacted.status === "completed") return { _tag: "RestartAfterCompaction", step: currentStep } as const
|
||||||
return yield* new StepFailedError({ error: compacted.error })
|
return yield* new StepFailedError({ error: compacted.error })
|
||||||
}
|
}
|
||||||
const isLastStep = agentInfo.steps !== undefined && currentStep >= agentInfo.steps
|
const prepared = yield* modelRequests.prepare({
|
||||||
const toolMaterialization = isLastStep ? undefined : yield* tools.materialize(agentInfo.permissions)
|
context: loaded,
|
||||||
const promptCacheKey = /^ses_[0-9a-f]{64}$/.test(session.id) ? session.id.slice(4) : session.id
|
step: currentStep,
|
||||||
const request = LLM.request({
|
|
||||||
model,
|
|
||||||
http: {
|
|
||||||
headers: SessionModelHeaders.make(session),
|
|
||||||
},
|
|
||||||
providerOptions: { openai: { promptCacheKey } },
|
|
||||||
system: [agentInfo.system ? agentInfo.system : PROMPT_DEFAULT, loaded.initial]
|
|
||||||
.filter((part): part is string => part !== undefined && part.length > 0)
|
|
||||||
.map(SystemPart.make),
|
|
||||||
messages: [
|
|
||||||
...toLLMMessages(loaded.messages, resolved.ref, providerMetadataKey),
|
|
||||||
...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : []),
|
|
||||||
],
|
|
||||||
tools: toolMaterialization?.definitions ?? [],
|
|
||||||
toolChoice: isLastStep ? "none" : undefined,
|
|
||||||
})
|
})
|
||||||
const availableTools = new Map(request.tools.map((tool) => [tool.name, tool]))
|
|
||||||
const contextEvent: SessionHooks["context"] = {
|
|
||||||
sessionID: session.id,
|
|
||||||
agent: agent.id,
|
|
||||||
model: resolved.ref,
|
|
||||||
system: [...request.system],
|
|
||||||
messages: [...request.messages],
|
|
||||||
tools: Object.fromEntries(
|
|
||||||
request.tools.map((tool) => [tool.name, { description: tool.description, input: { ...tool.inputSchema } }]),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
// Plugins may reshape the draft but cannot advertise tools excluded by
|
|
||||||
// permissions, registration state, or the selected agent's step limit.
|
|
||||||
yield* hooks.trigger("session", "context", contextEvent)
|
|
||||||
const hookedRequest = LLM.updateRequest(request, {
|
|
||||||
system: contextEvent.system,
|
|
||||||
messages: contextEvent.messages,
|
|
||||||
tools: Object.entries(contextEvent.tools).flatMap(([name, tool]) => {
|
|
||||||
const registered = availableTools.get(name)
|
|
||||||
if (!registered) return []
|
|
||||||
return [{ ...registered, description: tool.description, inputSchema: tool.input }]
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
const advertisedTools = new Set(hookedRequest.tools.map((tool) => tool.name))
|
|
||||||
const toolFibers = yield* FiberSet.make<void, ToolOutputStore.Error>()
|
const toolFibers = yield* FiberSet.make<void, ToolOutputStore.Error>()
|
||||||
const ownedToolFibers: Array<Fiber.Fiber<void, ToolOutputStore.Error>> = []
|
const ownedToolFibers: Array<Fiber.Fiber<void, ToolOutputStore.Error>> = []
|
||||||
let needsContinuation = false
|
let needsContinuation = false
|
||||||
@@ -209,7 +147,7 @@ const layer = Layer.effect(
|
|||||||
// The selected catalog identity, not model.id: route-level ids are provider API
|
// The selected catalog identity, not model.id: route-level ids are provider API
|
||||||
// model ids (for example gpt-5.5-fast resolves to api id gpt-5.5).
|
// model ids (for example gpt-5.5-fast resolves to api id gpt-5.5).
|
||||||
model: resolved.ref,
|
model: resolved.ref,
|
||||||
providerMetadataKey,
|
providerMetadataKey: model.route.providerMetadataKey ?? model.provider,
|
||||||
snapshot: startSnapshot,
|
snapshot: startSnapshot,
|
||||||
assistantMessageID,
|
assistantMessageID,
|
||||||
})
|
})
|
||||||
@@ -219,7 +157,7 @@ const layer = Layer.effect(
|
|||||||
const serialized = <A, E, R>(effect: Effect.Effect<A, E, R>) => publication.withPermit(effect)
|
const serialized = <A, E, R>(effect: Effect.Effect<A, E, R>) => publication.withPermit(effect)
|
||||||
const publish = (event: LLMEvent, error?: SessionError.Error) => serialized(publisher.publish(event, error))
|
const publish = (event: LLMEvent, error?: SessionError.Error) => serialized(publisher.publish(event, error))
|
||||||
let overflowFailure: ProviderErrorEvent | undefined
|
let overflowFailure: ProviderErrorEvent | undefined
|
||||||
const providerStream = llm.stream(hookedRequest).pipe(
|
const providerStream = llm.stream(prepared.request).pipe(
|
||||||
Stream.runForEach((event) =>
|
Stream.runForEach((event) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
if (overflowFailure || publisher.hasProviderError()) return
|
if (overflowFailure || publisher.hasProviderError()) return
|
||||||
@@ -231,15 +169,9 @@ const layer = Layer.effect(
|
|||||||
}
|
}
|
||||||
yield* publish(event)
|
yield* publish(event)
|
||||||
if (event.type !== "tool-call" || event.providerExecuted) return
|
if (event.type !== "tool-call" || event.providerExecuted) return
|
||||||
if (!toolMaterialization || (availableTools.has(event.name) && !advertisedTools.has(event.name))) {
|
const tool = prepared.resolveToolCall(event.name)
|
||||||
yield* serialized(
|
if (tool.type === "reject") {
|
||||||
publisher.failUnsettledTools({
|
yield* serialized(publisher.failUnsettledTools(tool.error))
|
||||||
type: "tool.execution",
|
|
||||||
message: toolMaterialization
|
|
||||||
? `Tool is not available for this request: ${event.name}`
|
|
||||||
: "Tools are disabled after the maximum agent steps",
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
needsContinuation = true
|
needsContinuation = true
|
||||||
@@ -247,7 +179,7 @@ const layer = Layer.effect(
|
|||||||
ownedToolFibers.push(
|
ownedToolFibers.push(
|
||||||
yield* Effect.uninterruptibleMask((restore) =>
|
yield* Effect.uninterruptibleMask((restore) =>
|
||||||
restore(
|
restore(
|
||||||
toolMaterialization.settle({
|
tool.settle({
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
agent: agent.id,
|
agent: agent.id,
|
||||||
messageID: assistantMessageID,
|
messageID: assistantMessageID,
|
||||||
@@ -337,10 +269,7 @@ const layer = Layer.effect(
|
|||||||
const llmFailure = streamFailure instanceof LLMError ? streamFailure : undefined
|
const llmFailure = streamFailure instanceof LLMError ? streamFailure : undefined
|
||||||
if (llmFailure && !publisher.hasProviderError()) {
|
if (llmFailure && !publisher.hasProviderError()) {
|
||||||
const error = toSessionError(llmFailure)
|
const error = toSessionError(llmFailure)
|
||||||
if (
|
if (SessionRunnerRetry.isRetryable(llmFailure) && !publisher.hasRetryEvidence()) {
|
||||||
SessionRunnerRetry.isRetryable(llmFailure) &&
|
|
||||||
!publisher.hasRetryEvidence()
|
|
||||||
) {
|
|
||||||
return yield* new SessionRunnerRetry.RetryableFailure({
|
return yield* new SessionRunnerRetry.RetryableFailure({
|
||||||
cause: llmFailure,
|
cause: llmFailure,
|
||||||
assistantMessageID: yield* publisher.startAssistant(),
|
assistantMessageID: yield* publisher.startAssistant(),
|
||||||
@@ -570,9 +499,8 @@ export const node = makeLocationNode({
|
|||||||
deps: [
|
deps: [
|
||||||
EventV2.node,
|
EventV2.node,
|
||||||
llmClient,
|
llmClient,
|
||||||
ToolRegistry.node,
|
|
||||||
PluginHooks.node,
|
|
||||||
SessionContext.node,
|
SessionContext.node,
|
||||||
|
SessionModelRequest.node,
|
||||||
SessionStore.node,
|
SessionStore.node,
|
||||||
SessionCompaction.node,
|
SessionCompaction.node,
|
||||||
SessionTitle.node,
|
SessionTitle.node,
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ session.prompt
|
|||||||
-> SessionAdmission
|
-> SessionAdmission
|
||||||
-> SessionExecution
|
-> SessionExecution
|
||||||
-> SessionContext.snapshot
|
-> SessionContext.snapshot
|
||||||
-> SessionRequest.prepare
|
-> SessionModelRequest.prepare
|
||||||
-> LLMClient.stream
|
-> LLMClient.stream
|
||||||
-> SessionSettlement
|
-> SessionSettlement
|
||||||
|
|
||||||
session.generate
|
session.generate
|
||||||
-> SessionContext.snapshot
|
-> SessionContext.snapshot
|
||||||
-> SessionRequest.prepare
|
-> SessionModelRequest.prepare
|
||||||
-> LLMClient.generate
|
-> LLMClient.generate
|
||||||
-> return text
|
-> return text
|
||||||
```
|
```
|
||||||
@@ -54,7 +54,7 @@ The modules have distinct jobs:
|
|||||||
|
|
||||||
- `SessionAdmission` records and promotes durable input.
|
- `SessionAdmission` records and promotes durable input.
|
||||||
- `SessionContext` resolves a read-only, internally consistent view of what the selected agent would see.
|
- `SessionContext` resolves a read-only, internally consistent view of what the selected agent would see.
|
||||||
- `SessionRequest` converts that view into the provider request used by a Physical Attempt.
|
- `SessionModelRequest` converts that view into the provider request used by a Physical Attempt.
|
||||||
- `LLMClient` executes one provider request without deciding what becomes durable.
|
- `LLMClient` executes one provider request without deciding what becomes durable.
|
||||||
- `SessionSettlement` gives streamed provider events their durable Session meaning, executes local tools, records usage, and decides continuation.
|
- `SessionSettlement` gives streamed provider events their durable Session meaning, executes local tools, records usage, and decides continuation.
|
||||||
- `SessionCompaction` replaces oversized active history and remains a durable Session operation.
|
- `SessionCompaction` replaces oversized active history and remains a durable Session operation.
|
||||||
@@ -66,16 +66,16 @@ Durability becomes a property of admission and settlement, not request preparati
|
|||||||
The first extraction should be one internal Location-scoped module with a small interface. Names are provisional; behavior is not.
|
The first extraction should be one internal Location-scoped module with a small interface. Names are provisional; behavior is not.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
interface SessionRequest {
|
interface SessionModelRequest {
|
||||||
readonly prepare: (input: {
|
readonly prepare: (input: {
|
||||||
snapshot: SessionContext.Snapshot
|
snapshot: SessionContext.Snapshot
|
||||||
operation: { type: "step"; current: number; maximum?: number } | { type: "generate"; prompt: Message.User }
|
operation: { type: "step"; current: number; maximum?: number } | { type: "generate"; prompt: Message.User }
|
||||||
}) => Effect<PreparedSessionRequest, SessionRequestError>
|
}) => Effect<PreparedSessionModelRequest, SessionModelRequestError>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
type PreparedSessionRequest = {
|
type PreparedSessionModelRequest = {
|
||||||
request: LLM.Request
|
request: LLM.Request
|
||||||
snapshot: SessionContext.Snapshot
|
snapshot: SessionContext.Snapshot
|
||||||
executableTools?: MaterializedTools
|
executableTools?: MaterializedTools
|
||||||
@@ -154,7 +154,7 @@ The prepared result carries the captured revision internally. A later recap inte
|
|||||||
The existing Session context hook should run because it participates in normal request preparation. Its event should eventually identify the operation:
|
The existing Session context hook should run because it participates in normal request preparation. Its event should eventually identify the operation:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
type SessionRequestOperation = { type: "step"; step: number } | { type: "generate" } | { type: "compaction" }
|
type SessionModelRequestOperation = { type: "step"; step: number } | { type: "generate" } | { type: "compaction" }
|
||||||
```
|
```
|
||||||
|
|
||||||
Request preparation and observation hooks run for `session.generate`. Admission, projection, Session settlement, and tool-execution hooks do not run because those stages do not occur.
|
Request preparation and observation hooks run for `session.generate`. Admission, projection, Session settlement, and tool-execution hooks do not run because those stages do not occur.
|
||||||
@@ -211,9 +211,9 @@ Move instruction source loading and read-only assembly behind one internal inter
|
|||||||
|
|
||||||
This commit should not add `session.generate`.
|
This commit should not add `session.generate`.
|
||||||
|
|
||||||
### 3. Extract Session Request Preparation
|
### 3. Extract Session Model Request Preparation
|
||||||
|
|
||||||
Move model resolution, history selection, request construction, tool definitions, cache identity, and context hooks into the Location-scoped `SessionRequest` module. Make the durable runner its only caller first.
|
Move model resolution, history selection, request construction, tool definitions, cache identity, and context hooks into the Location-scoped `SessionModelRequest` module. Make the durable runner its only caller first.
|
||||||
|
|
||||||
Verify the recorded normal request before and after extraction. Keep compaction detection and pending promotion outside the new module if putting them inside would make preparation mutate state.
|
Verify the recorded normal request before and after extraction. Keep compaction detection and pending promotion outside the new module if putting them inside would make preparation mutate state.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user