refactor(effect): inline session processor interrupt cleanup (#21593)
This commit is contained in:
@@ -46,7 +46,7 @@ export namespace FileTime {
|
|||||||
const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK
|
const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK
|
||||||
|
|
||||||
const stamp = Effect.fnUntraced(function* (file: string) {
|
const stamp = Effect.fnUntraced(function* (file: string) {
|
||||||
const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.void))
|
||||||
return {
|
return {
|
||||||
read: yield* DateTime.nowAsDate,
|
read: yield* DateTime.nowAsDate,
|
||||||
mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined,
|
mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined,
|
||||||
|
|||||||
@@ -501,7 +501,7 @@ export namespace MCP {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = yield* create(key, mcp).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
const result = yield* create(key, mcp).pipe(Effect.catch(() => Effect.void))
|
||||||
if (!result) return
|
if (!result) return
|
||||||
|
|
||||||
s.status[key] = result.status
|
s.status[key] = result.status
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export namespace Project {
|
|||||||
return yield* fs.readFileString(pathSvc.join(dir, "opencode")).pipe(
|
return yield* fs.readFileString(pathSvc.join(dir, "opencode")).pipe(
|
||||||
Effect.map((x) => x.trim()),
|
Effect.map((x) => x.trim()),
|
||||||
Effect.map(ProjectID.make),
|
Effect.map(ProjectID.make),
|
||||||
Effect.catch(() => Effect.succeed(undefined)),
|
Effect.catch(() => Effect.void),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -253,23 +253,21 @@ When constructing the summary, try to stick to this template:
|
|||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
model,
|
model,
|
||||||
})
|
})
|
||||||
const result = yield* processor
|
const result = yield* processor.process({
|
||||||
.process({
|
user: userMessage,
|
||||||
user: userMessage,
|
agent,
|
||||||
agent,
|
sessionID: input.sessionID,
|
||||||
sessionID: input.sessionID,
|
tools: {},
|
||||||
tools: {},
|
system: [],
|
||||||
system: [],
|
messages: [
|
||||||
messages: [
|
...modelMessages,
|
||||||
...modelMessages,
|
{
|
||||||
{
|
role: "user",
|
||||||
role: "user",
|
content: [{ type: "text", text: prompt }],
|
||||||
content: [{ type: "text", text: prompt }],
|
},
|
||||||
},
|
],
|
||||||
],
|
model,
|
||||||
model,
|
})
|
||||||
})
|
|
||||||
.pipe(Effect.onInterrupt(() => processor.abort()))
|
|
||||||
|
|
||||||
if (result === "compact") {
|
if (result === "compact") {
|
||||||
processor.message.error = new MessageV2.ContextOverflowError({
|
processor.message.error = new MessageV2.ContextOverflowError({
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export namespace SessionProcessor {
|
|||||||
export interface Handle {
|
export interface Handle {
|
||||||
readonly message: MessageV2.Assistant
|
readonly message: MessageV2.Assistant
|
||||||
readonly partFromToolCall: (toolCallID: string) => MessageV2.ToolPart | undefined
|
readonly partFromToolCall: (toolCallID: string) => MessageV2.ToolPart | undefined
|
||||||
readonly abort: () => Effect.Effect<void>
|
|
||||||
readonly process: (streamInput: LLM.StreamInput) => Effect.Effect<Result>
|
readonly process: (streamInput: LLM.StreamInput) => Effect.Effect<Result>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,19 +428,6 @@ export namespace SessionProcessor {
|
|||||||
yield* status.set(ctx.sessionID, { type: "idle" })
|
yield* status.set(ctx.sessionID, { type: "idle" })
|
||||||
})
|
})
|
||||||
|
|
||||||
const abort = Effect.fn("SessionProcessor.abort")(() =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
if (!ctx.assistantMessage.error) {
|
|
||||||
yield* halt(new DOMException("Aborted", "AbortError"))
|
|
||||||
}
|
|
||||||
if (!ctx.assistantMessage.time.completed) {
|
|
||||||
yield* cleanup()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
yield* session.updateMessage(ctx.assistantMessage)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) {
|
const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) {
|
||||||
log.info("process")
|
log.info("process")
|
||||||
ctx.needsCompaction = false
|
ctx.needsCompaction = false
|
||||||
@@ -459,7 +445,14 @@ export namespace SessionProcessor {
|
|||||||
Stream.runDrain,
|
Stream.runDrain,
|
||||||
)
|
)
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.onInterrupt(() => Effect.sync(() => void (aborted = true))),
|
Effect.onInterrupt(() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
aborted = true
|
||||||
|
if (!ctx.assistantMessage.error) {
|
||||||
|
yield* halt(new DOMException("Aborted", "AbortError"))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
Effect.catchCauseIf(
|
Effect.catchCauseIf(
|
||||||
(cause) => !Cause.hasInterruptsOnly(cause),
|
(cause) => !Cause.hasInterruptsOnly(cause),
|
||||||
(cause) => Effect.fail(Cause.squash(cause)),
|
(cause) => Effect.fail(Cause.squash(cause)),
|
||||||
@@ -480,13 +473,10 @@ export namespace SessionProcessor {
|
|||||||
Effect.ensuring(cleanup()),
|
Effect.ensuring(cleanup()),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (aborted && !ctx.assistantMessage.error) {
|
|
||||||
yield* abort()
|
|
||||||
}
|
|
||||||
if (ctx.needsCompaction) return "compact"
|
if (ctx.needsCompaction) return "compact"
|
||||||
if (ctx.blocked || ctx.assistantMessage.error || aborted) return "stop"
|
if (ctx.blocked || ctx.assistantMessage.error) return "stop"
|
||||||
return "continue"
|
return "continue"
|
||||||
}).pipe(Effect.onInterrupt(() => abort().pipe(Effect.asVoid)))
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -496,7 +486,6 @@ export namespace SessionProcessor {
|
|||||||
partFromToolCall(toolCallID: string) {
|
partFromToolCall(toolCallID: string) {
|
||||||
return ctx.toolcalls[toolCallID]
|
return ctx.toolcalls[toolCallID]
|
||||||
},
|
},
|
||||||
abort,
|
|
||||||
process,
|
process,
|
||||||
} satisfies Handle
|
} satisfies Handle
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -964,9 +964,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|||||||
const same = ag.model && model.providerID === ag.model.providerID && model.modelID === ag.model.modelID
|
const same = ag.model && model.providerID === ag.model.providerID && model.modelID === ag.model.modelID
|
||||||
const full =
|
const full =
|
||||||
!input.variant && ag.variant && same
|
!input.variant && ag.variant && same
|
||||||
? yield* provider
|
? yield* provider.getModel(model.providerID, model.modelID).pipe(Effect.catchDefect(() => Effect.void))
|
||||||
.getModel(model.providerID, model.modelID)
|
|
||||||
.pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
||||||
: undefined
|
: undefined
|
||||||
const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined)
|
const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined)
|
||||||
|
|
||||||
@@ -986,9 +984,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|||||||
format: input.format,
|
format: input.format,
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* Effect.addFinalizer(() =>
|
yield* Effect.addFinalizer(() => instruction.clear(info.id))
|
||||||
InstanceState.withALS(() => instruction.clear(info.id)).pipe(Effect.flatMap((x) => x)),
|
|
||||||
)
|
|
||||||
|
|
||||||
type Draft<T> = T extends MessageV2.Part ? Omit<T, "id"> & { id?: string } : never
|
type Draft<T> = T extends MessageV2.Part ? Omit<T, "id"> & { id?: string } : never
|
||||||
const assign = (part: Draft<MessageV2.Part>): MessageV2.Part => ({
|
const assign = (part: Draft<MessageV2.Part>): MessageV2.Part => ({
|
||||||
@@ -1459,110 +1455,104 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|||||||
model,
|
model,
|
||||||
})
|
})
|
||||||
|
|
||||||
const outcome: "break" | "continue" = yield* Effect.onExit(
|
const outcome: "break" | "continue" = yield* Effect.gen(function* () {
|
||||||
Effect.gen(function* () {
|
const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
|
||||||
const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
|
const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false
|
||||||
const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false
|
|
||||||
|
|
||||||
const tools = yield* resolveTools({
|
const tools = yield* resolveTools({
|
||||||
agent,
|
agent,
|
||||||
session,
|
session,
|
||||||
model,
|
model,
|
||||||
tools: lastUser.tools,
|
tools: lastUser.tools,
|
||||||
processor: handle,
|
processor: handle,
|
||||||
bypassAgentCheck,
|
bypassAgentCheck,
|
||||||
messages: msgs,
|
messages: msgs,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (lastUser.format?.type === "json_schema") {
|
||||||
|
tools["StructuredOutput"] = createStructuredOutputTool({
|
||||||
|
schema: lastUser.format.schema,
|
||||||
|
onSuccess(output) {
|
||||||
|
structured = output
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (lastUser.format?.type === "json_schema") {
|
if (step === 1) SessionSummary.summarize({ sessionID, messageID: lastUser.id })
|
||||||
tools["StructuredOutput"] = createStructuredOutputTool({
|
|
||||||
schema: lastUser.format.schema,
|
|
||||||
onSuccess(output) {
|
|
||||||
structured = output
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step === 1) SessionSummary.summarize({ sessionID, messageID: lastUser.id })
|
if (step > 1 && lastFinished) {
|
||||||
|
for (const m of msgs) {
|
||||||
if (step > 1 && lastFinished) {
|
if (m.info.role !== "user" || m.info.id <= lastFinished.id) continue
|
||||||
for (const m of msgs) {
|
for (const p of m.parts) {
|
||||||
if (m.info.role !== "user" || m.info.id <= lastFinished.id) continue
|
if (p.type !== "text" || p.ignored || p.synthetic) continue
|
||||||
for (const p of m.parts) {
|
if (!p.text.trim()) continue
|
||||||
if (p.type !== "text" || p.ignored || p.synthetic) continue
|
p.text = [
|
||||||
if (!p.text.trim()) continue
|
"<system-reminder>",
|
||||||
p.text = [
|
"The user sent the following message:",
|
||||||
"<system-reminder>",
|
p.text,
|
||||||
"The user sent the following message:",
|
"",
|
||||||
p.text,
|
"Please address this message and continue with your tasks.",
|
||||||
"",
|
"</system-reminder>",
|
||||||
"Please address this message and continue with your tasks.",
|
].join("\n")
|
||||||
"</system-reminder>",
|
|
||||||
].join("\n")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
|
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
|
||||||
|
|
||||||
const [skills, env, instructions, modelMsgs] = yield* Effect.all([
|
const [skills, env, instructions, modelMsgs] = yield* Effect.all([
|
||||||
Effect.promise(() => SystemPrompt.skills(agent)),
|
Effect.promise(() => SystemPrompt.skills(agent)),
|
||||||
Effect.promise(() => SystemPrompt.environment(model)),
|
Effect.promise(() => SystemPrompt.environment(model)),
|
||||||
instruction.system().pipe(Effect.orDie),
|
instruction.system().pipe(Effect.orDie),
|
||||||
Effect.promise(() => MessageV2.toModelMessages(msgs, model)),
|
Effect.promise(() => MessageV2.toModelMessages(msgs, model)),
|
||||||
])
|
])
|
||||||
const system = [...env, ...(skills ? [skills] : []), ...instructions]
|
const system = [...env, ...(skills ? [skills] : []), ...instructions]
|
||||||
const format = lastUser.format ?? { type: "text" as const }
|
const format = lastUser.format ?? { type: "text" as const }
|
||||||
if (format.type === "json_schema") system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
|
if (format.type === "json_schema") system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
|
||||||
const result = yield* handle.process({
|
const result = yield* handle.process({
|
||||||
user: lastUser,
|
user: lastUser,
|
||||||
agent,
|
agent,
|
||||||
permission: session.permission,
|
permission: session.permission,
|
||||||
sessionID,
|
sessionID,
|
||||||
parentSessionID: session.parentID,
|
parentSessionID: session.parentID,
|
||||||
system,
|
system,
|
||||||
messages: [...modelMsgs, ...(isLastStep ? [{ role: "assistant" as const, content: MAX_STEPS }] : [])],
|
messages: [...modelMsgs, ...(isLastStep ? [{ role: "assistant" as const, content: MAX_STEPS }] : [])],
|
||||||
tools,
|
tools,
|
||||||
model,
|
model,
|
||||||
toolChoice: format.type === "json_schema" ? "required" : undefined,
|
toolChoice: format.type === "json_schema" ? "required" : undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (structured !== undefined) {
|
if (structured !== undefined) {
|
||||||
handle.message.structured = structured
|
handle.message.structured = structured
|
||||||
handle.message.finish = handle.message.finish ?? "stop"
|
handle.message.finish = handle.message.finish ?? "stop"
|
||||||
|
yield* sessions.updateMessage(handle.message)
|
||||||
|
return "break" as const
|
||||||
|
}
|
||||||
|
|
||||||
|
const finished = handle.message.finish && !["tool-calls", "unknown"].includes(handle.message.finish)
|
||||||
|
if (finished && !handle.message.error) {
|
||||||
|
if (format.type === "json_schema") {
|
||||||
|
handle.message.error = new MessageV2.StructuredOutputError({
|
||||||
|
message: "Model did not produce structured output",
|
||||||
|
retries: 0,
|
||||||
|
}).toObject()
|
||||||
yield* sessions.updateMessage(handle.message)
|
yield* sessions.updateMessage(handle.message)
|
||||||
return "break" as const
|
return "break" as const
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const finished = handle.message.finish && !["tool-calls", "unknown"].includes(handle.message.finish)
|
if (result === "stop") return "break" as const
|
||||||
if (finished && !handle.message.error) {
|
if (result === "compact") {
|
||||||
if (format.type === "json_schema") {
|
yield* compaction.create({
|
||||||
handle.message.error = new MessageV2.StructuredOutputError({
|
sessionID,
|
||||||
message: "Model did not produce structured output",
|
agent: lastUser.agent,
|
||||||
retries: 0,
|
model: lastUser.model,
|
||||||
}).toObject()
|
auto: true,
|
||||||
yield* sessions.updateMessage(handle.message)
|
overflow: !handle.message.finish,
|
||||||
return "break" as const
|
})
|
||||||
}
|
}
|
||||||
}
|
return "continue" as const
|
||||||
|
}).pipe(Effect.ensuring(instruction.clear(handle.message.id)))
|
||||||
if (result === "stop") return "break" as const
|
|
||||||
if (result === "compact") {
|
|
||||||
yield* compaction.create({
|
|
||||||
sessionID,
|
|
||||||
agent: lastUser.agent,
|
|
||||||
model: lastUser.model,
|
|
||||||
auto: true,
|
|
||||||
overflow: !handle.message.finish,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return "continue" as const
|
|
||||||
}),
|
|
||||||
Effect.fnUntraced(function* (exit) {
|
|
||||||
if (Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)) yield* handle.abort()
|
|
||||||
yield* InstanceState.withALS(() => instruction.clear(handle.message.id)).pipe(Effect.flatMap((x) => x))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
if (outcome === "break") break
|
if (outcome === "break") break
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,9 +67,7 @@ export const ReadTool = Tool.defineEffect(
|
|||||||
if (item.type === "directory") return item.name + "/"
|
if (item.type === "directory") return item.name + "/"
|
||||||
if (item.type !== "symlink") return item.name
|
if (item.type !== "symlink") return item.name
|
||||||
|
|
||||||
const target = yield* fs
|
const target = yield* fs.stat(path.join(filepath, item.name)).pipe(Effect.catch(() => Effect.void))
|
||||||
.stat(path.join(filepath, item.name))
|
|
||||||
.pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
||||||
if (target?.type === "Directory") return item.name + "/"
|
if (target?.type === "Directory") return item.name + "/"
|
||||||
return item.name
|
return item.name
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ function fake(
|
|||||||
get message() {
|
get message() {
|
||||||
return msg
|
return msg
|
||||||
},
|
},
|
||||||
abort: Effect.fn("TestSessionProcessor.abort")(() => Effect.void),
|
|
||||||
partFromToolCall() {
|
partFromToolCall() {
|
||||||
return {
|
return {
|
||||||
id: PartID.ascending(),
|
id: PartID.ascending(),
|
||||||
|
|||||||
@@ -593,9 +593,6 @@ it.live("session.processor effect tests mark pending tools as aborted on cleanup
|
|||||||
yield* Fiber.interrupt(run)
|
yield* Fiber.interrupt(run)
|
||||||
|
|
||||||
const exit = yield* Fiber.await(run)
|
const exit = yield* Fiber.await(run)
|
||||||
if (Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)) {
|
|
||||||
yield* handle.abort()
|
|
||||||
}
|
|
||||||
const parts = MessageV2.parts(msg.id)
|
const parts = MessageV2.parts(msg.id)
|
||||||
const call = parts.find((part): part is MessageV2.ToolPart => part.type === "tool")
|
const call = parts.find((part): part is MessageV2.ToolPart => part.type === "tool")
|
||||||
|
|
||||||
@@ -665,9 +662,6 @@ it.live("session.processor effect tests record aborted errors and idle state", (
|
|||||||
yield* Fiber.interrupt(run)
|
yield* Fiber.interrupt(run)
|
||||||
|
|
||||||
const exit = yield* Fiber.await(run)
|
const exit = yield* Fiber.await(run)
|
||||||
if (Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)) {
|
|
||||||
yield* handle.abort()
|
|
||||||
}
|
|
||||||
yield* Effect.promise(() => seen.promise)
|
yield* Effect.promise(() => seen.promise)
|
||||||
const stored = MessageV2.get({ sessionID: chat.id, messageID: msg.id })
|
const stored = MessageV2.get({ sessionID: chat.id, messageID: msg.id })
|
||||||
const state = yield* sts.get(chat.id)
|
const state = yield* sts.get(chat.id)
|
||||||
|
|||||||
Reference in New Issue
Block a user