fix(core): harden compaction recovery
This commit is contained in:
parent
c6364f4b2e
commit
71751ac339
2 changed files with 49 additions and 20 deletions
|
|
@ -61,7 +61,7 @@ type Dependencies = {
|
||||||
readonly llm: {
|
readonly llm: {
|
||||||
readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError>
|
readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMError>
|
||||||
}
|
}
|
||||||
readonly config: readonly Config.Entry[]
|
readonly config: Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AutoInput = {
|
export type AutoInput = {
|
||||||
|
|
@ -193,7 +193,7 @@ export const buildPrompt = (input: { readonly previousSummary?: string; readonly
|
||||||
].join("\n\n")
|
].join("\n\n")
|
||||||
|
|
||||||
const make = (dependencies: Dependencies) => {
|
const make = (dependencies: Dependencies) => {
|
||||||
const config = settings(dependencies.config)
|
const config = dependencies.config
|
||||||
const compact = Effect.fn("SessionCompaction.compact")(function* (input: {
|
const compact = Effect.fn("SessionCompaction.compact")(function* (input: {
|
||||||
readonly sessionID: SessionSchema.ID
|
readonly sessionID: SessionSchema.ID
|
||||||
readonly model: Model
|
readonly model: Model
|
||||||
|
|
@ -232,6 +232,8 @@ const make = (dependencies: Dependencies) => {
|
||||||
type: event.classification === "context-overflow" ? "provider.invalid-request" : "provider.error",
|
type: event.classification === "context-overflow" ? "provider.invalid-request" : "provider.error",
|
||||||
message: event.message,
|
message: event.message,
|
||||||
}
|
}
|
||||||
|
if (LLMEvent.is.finish(event) && event.reason === "length")
|
||||||
|
failure = { type: "compaction.failed", message: "Compaction reached the model output limit" }
|
||||||
if (LLMEvent.is.textDelta(event)) {
|
if (LLMEvent.is.textDelta(event)) {
|
||||||
chunks.push(event.text)
|
chunks.push(event.text)
|
||||||
return dependencies.events.publish(SessionEvent.Compaction.Delta, {
|
return dependencies.events.publish(SessionEvent.Compaction.Delta, {
|
||||||
|
|
@ -284,16 +286,7 @@ const make = (dependencies: Dependencies) => {
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const selected = select(input.messages, config.tokens)
|
const selected = select(input.messages, config.tokens)
|
||||||
if (!selected) {
|
if (!selected) return false
|
||||||
if (input.inputID === undefined) return false
|
|
||||||
yield* dependencies.events.publish(SessionEvent.Compaction.Failed, {
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
reason: input.reason,
|
|
||||||
error: { type: "compaction.unavailable", message: "Nothing to compact yet" },
|
|
||||||
inputID: input.inputID,
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const previousSummary = input.messages.find(
|
const previousSummary = input.messages.find(
|
||||||
(message) => message.type === "compaction" && message.status === "completed",
|
(message) => message.type === "compaction" && message.status === "completed",
|
||||||
)
|
)
|
||||||
|
|
@ -318,7 +311,6 @@ const make = (dependencies: Dependencies) => {
|
||||||
messages: input.messages,
|
messages: input.messages,
|
||||||
model: input.request.model,
|
model: input.request.model,
|
||||||
reason: "auto",
|
reason: "auto",
|
||||||
output: input.request.generation?.maxTokens ?? input.request.model.route.defaults.limits?.output ?? 0,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const compactManual = Effect.fn("SessionCompaction.compactManual")(function* (input: CompactInput) {
|
const compactManual = Effect.fn("SessionCompaction.compactManual")(function* (input: CompactInput) {
|
||||||
|
|
@ -377,12 +369,22 @@ export const layer = Layer.effect(
|
||||||
const llm = yield* LLMClient.Service
|
const llm = yield* LLMClient.Service
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const models = yield* SessionRunnerModel.Service
|
const models = yield* SessionRunnerModel.Service
|
||||||
const compaction = make({ events, llm, config: yield* config.entries() })
|
const configured = settings(yield* config.entries())
|
||||||
|
const compaction = make({ events, llm, config: configured })
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
compactIfNeeded: compaction.compactIfNeeded,
|
compactIfNeeded: compaction.compactIfNeeded,
|
||||||
compactAfterOverflow: compaction.compactAfterOverflow,
|
compactAfterOverflow: compaction.compactAfterOverflow,
|
||||||
compactManual: Effect.fn("SessionCompaction.compactManual")(function* (input) {
|
compactManual: Effect.fn("SessionCompaction.compactManual")(function* (input) {
|
||||||
|
if (!select(input.messages, configured.tokens)) {
|
||||||
|
yield* events.publish(SessionEvent.Compaction.Failed, {
|
||||||
|
sessionID: input.session.id,
|
||||||
|
reason: "manual",
|
||||||
|
error: { type: "compaction.unavailable", message: "Nothing to compact yet" },
|
||||||
|
inputID: input.inputID,
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
const resolved = yield* models.resolve(input.session).pipe(
|
const resolved = yield* models.resolve(input.session).pipe(
|
||||||
Effect.catch((error) =>
|
Effect.catch((error) =>
|
||||||
events
|
events
|
||||||
|
|
|
||||||
|
|
@ -128,10 +128,10 @@ const compactModel = Model.make({
|
||||||
provider: "fake",
|
provider: "fake",
|
||||||
route: OpenAIChat.route.with({ limits: { context: 4_000, output: 50 } }),
|
route: OpenAIChat.route.with({ limits: { context: 4_000, output: 50 } }),
|
||||||
})
|
})
|
||||||
const undersizedModel = Model.make({
|
const undersizedContextModel = Model.make({
|
||||||
id: "undersized",
|
id: "undersized-context",
|
||||||
provider: "fake",
|
provider: "fake",
|
||||||
route: OpenAIChat.route.with({ limits: { context: 1, output: 1 } }),
|
route: OpenAIChat.route.with({ limits: { context: 1, output: 1_000 } }),
|
||||||
})
|
})
|
||||||
const recoveryModel = Model.make({
|
const recoveryModel = Model.make({
|
||||||
id: "recovery",
|
id: "recovery",
|
||||||
|
|
@ -1544,6 +1544,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
yield* setup
|
yield* setup
|
||||||
const session = yield* SessionV2.Service
|
const session = yield* SessionV2.Service
|
||||||
const compaction = yield* session.compact({ sessionID })
|
const compaction = yield* session.compact({ sessionID })
|
||||||
|
modelResolveHook = Effect.die("model resolution should not run")
|
||||||
|
|
||||||
yield* session.resume(sessionID)
|
yield* session.resume(sessionID)
|
||||||
|
|
||||||
|
|
@ -1622,10 +1623,35 @@ describe("SessionRunnerLLM", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("rejects a manual compaction truncated by the model output limit", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const session = yield* setup
|
||||||
|
response = reply.text("Earlier answer", "text-manual-length-history")
|
||||||
|
yield* admit(session, "Earlier question")
|
||||||
|
yield* session.resume(sessionID)
|
||||||
|
|
||||||
|
response = [
|
||||||
|
LLMEvent.textDelta({ id: "summary", text: "Partial summary" }),
|
||||||
|
LLMEvent.finish({ reason: "length" }),
|
||||||
|
]
|
||||||
|
const compaction = yield* session.compact({ sessionID })
|
||||||
|
yield* session.resume(sessionID)
|
||||||
|
|
||||||
|
expect((yield* session.messages({ sessionID })).find((message) => message.id === compaction.id)).toMatchObject({
|
||||||
|
type: "compaction",
|
||||||
|
status: "failed",
|
||||||
|
error: { type: "compaction.failed", message: "Compaction reached the model output limit" },
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("settles an admitted manual compaction when pre-start resolution throws", () =>
|
it.effect("settles an admitted manual compaction when pre-start resolution throws", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* setup
|
const session = yield* setup
|
||||||
const session = yield* SessionV2.Service
|
response = reply.text("Earlier answer", "text-manual-resolution-history")
|
||||||
|
yield* admit(session, "Earlier question")
|
||||||
|
yield* session.resume(sessionID)
|
||||||
|
|
||||||
const compaction = yield* session.compact({ sessionID })
|
const compaction = yield* session.compact({ sessionID })
|
||||||
modelResolveHook = Effect.die("model resolution failed")
|
modelResolveHook = Effect.die("model resolution failed")
|
||||||
|
|
||||||
|
|
@ -1747,7 +1773,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
it.effect("recovers from provider context overflow despite an undersized configured limit", () =>
|
it.effect("recovers from provider context overflow despite an undersized configured limit", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const session = yield* setupOverflowRecovery
|
const session = yield* setupOverflowRecovery
|
||||||
currentModel = undersizedModel
|
currentModel = undersizedContextModel
|
||||||
responses = [
|
responses = [
|
||||||
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })],
|
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })],
|
||||||
reply.text("## Objective\n- Recover undersized limit", "text-summary-undersized-limit"),
|
reply.text("## Objective\n- Recover undersized limit", "text-summary-undersized-limit"),
|
||||||
|
|
@ -1757,6 +1783,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
yield* session.resume(sessionID)
|
yield* session.resume(sessionID)
|
||||||
|
|
||||||
expect(requests).toHaveLength(3)
|
expect(requests).toHaveLength(3)
|
||||||
|
expect(requests[1].generation?.maxTokens).toBe(1_000)
|
||||||
expect(yield* session.context(sessionID)).toMatchObject([
|
expect(yield* session.context(sessionID)).toMatchObject([
|
||||||
{ type: "compaction", summary: "## Objective\n- Recover undersized limit" },
|
{ type: "compaction", summary: "## Objective\n- Recover undersized limit" },
|
||||||
{ type: "assistant", finish: "stop" },
|
{ type: "assistant", finish: "stop" },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue