Skip to content

Commit 35265e5

Browse files
Apply PR #19483: refactor(session): effectify SessionPrompt service
2 parents 0722f29 + 8b04ddc commit 35265e5

11 files changed

Lines changed: 2936 additions & 1910 deletions

File tree

packages/opencode/src/server/routes/session.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ export const SessionRoutes = lazy(() =>
381381
}),
382382
),
383383
async (c) => {
384-
SessionPrompt.cancel(c.req.valid("param").sessionID)
384+
await SessionPrompt.cancel(c.req.valid("param").sessionID)
385385
return c.json(true)
386386
},
387387
)
@@ -699,7 +699,7 @@ export const SessionRoutes = lazy(() =>
699699
),
700700
async (c) => {
701701
const params = c.req.valid("param")
702-
SessionPrompt.assertNotBusy(params.sessionID)
702+
await SessionPrompt.assertNotBusy(params.sessionID)
703703
await Session.removeMessage({
704704
sessionID: params.sessionID,
705705
messageID: params.messageID,

packages/opencode/src/session/compaction.ts

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ export namespace SessionCompaction {
4545
parentID: MessageID
4646
messages: MessageV2.WithParts[]
4747
sessionID: SessionID
48-
abort: AbortSignal
4948
auto: boolean
5049
overflow?: boolean
5150
}) => Effect.Effect<"continue" | "stop">
@@ -135,7 +134,6 @@ export namespace SessionCompaction {
135134
parentID: MessageID
136135
messages: MessageV2.WithParts[]
137136
sessionID: SessionID
138-
abort: AbortSignal
139137
auto: boolean
140138
overflow?: boolean
141139
}) {
@@ -235,20 +233,11 @@ When constructing the summary, try to stick to this template:
235233
assistantMessage: msg,
236234
sessionID: input.sessionID,
237235
model,
238-
abort: input.abort,
239-
})
240-
const cancel = Effect.fn("SessionCompaction.cancel")(function* () {
241-
if (!input.abort.aborted || msg.time.completed) return
242-
msg.error = msg.error ?? new MessageV2.AbortedError({ message: "Aborted" }).toObject()
243-
msg.finish = msg.finish ?? "error"
244-
msg.time.completed = Date.now()
245-
yield* session.updateMessage(msg)
246236
})
247237
const result = yield* processor
248238
.process({
249239
user: userMessage,
250240
agent,
251-
abort: input.abort,
252241
sessionID: input.sessionID,
253242
tools: {},
254243
system: [],
@@ -261,7 +250,7 @@ When constructing the summary, try to stick to this template:
261250
],
262251
model,
263252
})
264-
.pipe(Effect.ensuring(cancel()))
253+
.pipe(Effect.onInterrupt(() => processor.abort()))
265254

266255
if (result === "compact") {
267256
processor.message.error = new MessageV2.ContextOverflowError({
@@ -385,7 +374,7 @@ When constructing the summary, try to stick to this template:
385374
),
386375
)
387376

388-
const { runPromise, runPromiseExit } = makeRuntime(Service, defaultLayer)
377+
const { runPromise } = makeRuntime(Service, defaultLayer)
389378

390379
export async function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: Provider.Model }) {
391380
return runPromise((svc) => svc.isOverflow(input))
@@ -395,21 +384,16 @@ When constructing the summary, try to stick to this template:
395384
return runPromise((svc) => svc.prune(input))
396385
}
397386

398-
export async function process(input: {
399-
parentID: MessageID
400-
messages: MessageV2.WithParts[]
401-
sessionID: SessionID
402-
abort: AbortSignal
403-
auto: boolean
404-
overflow?: boolean
405-
}) {
406-
const exit = await runPromiseExit((svc) => svc.process(input), { signal: input.abort })
407-
if (Exit.isFailure(exit)) {
408-
if (Cause.hasInterrupts(exit.cause) && input.abort.aborted) return "stop"
409-
throw Cause.squash(exit.cause)
410-
}
411-
return exit.value
412-
}
387+
export const process = fn(
388+
z.object({
389+
parentID: MessageID.zod,
390+
messages: z.custom<MessageV2.WithParts[]>(),
391+
sessionID: SessionID.zod,
392+
auto: z.boolean(),
393+
overflow: z.boolean().optional(),
394+
}),
395+
(input) => runPromise((svc) => svc.process(input)),
396+
)
413397

414398
export const create = fn(
415399
z.object({

packages/opencode/src/session/index.ts

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -334,14 +334,14 @@ export namespace Session {
334334
readonly messages: (input: { sessionID: SessionID; limit?: number }) => Effect.Effect<MessageV2.WithParts[]>
335335
readonly children: (parentID: SessionID) => Effect.Effect<Info[]>
336336
readonly remove: (sessionID: SessionID) => Effect.Effect<void>
337-
readonly updateMessage: (msg: MessageV2.Info) => Effect.Effect<MessageV2.Info>
337+
readonly updateMessage: <T extends MessageV2.Info>(msg: T) => Effect.Effect<T>
338338
readonly removeMessage: (input: { sessionID: SessionID; messageID: MessageID }) => Effect.Effect<MessageID>
339339
readonly removePart: (input: {
340340
sessionID: SessionID
341341
messageID: MessageID
342342
partID: PartID
343343
}) => Effect.Effect<PartID>
344-
readonly updatePart: (part: MessageV2.Part) => Effect.Effect<MessageV2.Part>
344+
readonly updatePart: <T extends MessageV2.Part>(part: T) => Effect.Effect<T>
345345
readonly updatePartDelta: (input: {
346346
sessionID: SessionID
347347
messageID: MessageID
@@ -469,26 +469,23 @@ export namespace Session {
469469
}
470470
})
471471

472-
const updateMessage = Effect.fn("Session.updateMessage")(function* (msg: MessageV2.Info) {
473-
yield* Effect.sync(() =>
474-
SyncEvent.run(MessageV2.Event.Updated, {
475-
sessionID: msg.sessionID,
476-
info: msg,
477-
}),
478-
)
479-
return msg
480-
})
481-
482-
const updatePart = Effect.fn("Session.updatePart")(function* (part: MessageV2.Part) {
483-
yield* Effect.sync(() =>
484-
SyncEvent.run(MessageV2.Event.PartUpdated, {
485-
sessionID: part.sessionID,
486-
part: structuredClone(part),
487-
time: Date.now(),
488-
}),
489-
)
490-
return part
491-
})
472+
const updateMessage = <T extends MessageV2.Info>(msg: T): Effect.Effect<T> =>
473+
Effect.gen(function* () {
474+
yield* Effect.sync(() => SyncEvent.run(MessageV2.Event.Updated, { sessionID: msg.sessionID, info: msg }))
475+
return msg
476+
}).pipe(Effect.withSpan("Session.updateMessage"))
477+
478+
const updatePart = <T extends MessageV2.Part>(part: T): Effect.Effect<T> =>
479+
Effect.gen(function* () {
480+
yield* Effect.sync(() =>
481+
SyncEvent.run(MessageV2.Event.PartUpdated, {
482+
sessionID: part.sessionID,
483+
part: structuredClone(part),
484+
time: Date.now(),
485+
}),
486+
)
487+
return part
488+
}).pipe(Effect.withSpan("Session.updatePart"))
492489

493490
const create = Effect.fn("Session.create")(function* (input?: {
494491
parentID?: SessionID
@@ -851,7 +848,17 @@ export namespace Session {
851848

852849
export const children = fn(SessionID.zod, (id) => runPromise((svc) => svc.children(id)))
853850
export const remove = fn(SessionID.zod, (id) => runPromise((svc) => svc.remove(id)))
854-
export const updateMessage = fn(MessageV2.Info, (msg) => runPromise((svc) => svc.updateMessage(msg)))
851+
export const updateMessage = Object.assign(
852+
async function updateMessage<T extends MessageV2.Info>(msg: T): Promise<T> {
853+
return runPromise((svc) => svc.updateMessage(MessageV2.Info.parse(msg) as T))
854+
},
855+
{
856+
schema: MessageV2.Info,
857+
force<T extends MessageV2.Info>(msg: T): Promise<T> {
858+
return runPromise((svc) => svc.updateMessage(msg))
859+
},
860+
},
861+
)
855862

856863
export const removeMessage = fn(z.object({ sessionID: SessionID.zod, messageID: MessageID.zod }), (input) =>
857864
runPromise((svc) => svc.removeMessage(input)),
@@ -862,7 +869,17 @@ export namespace Session {
862869
(input) => runPromise((svc) => svc.removePart(input)),
863870
)
864871

865-
export const updatePart = fn(MessageV2.Part, (part) => runPromise((svc) => svc.updatePart(part)))
872+
export const updatePart = Object.assign(
873+
async function updatePart<T extends MessageV2.Part>(part: T): Promise<T> {
874+
return runPromise((svc) => svc.updatePart(MessageV2.Part.parse(part) as T))
875+
},
876+
{
877+
schema: MessageV2.Part,
878+
force<T extends MessageV2.Part>(part: T): Promise<T> {
879+
return runPromise((svc) => svc.updatePart(part))
880+
},
881+
},
882+
)
866883

867884
export const updatePartDelta = fn(
868885
z.object({

packages/opencode/src/session/llm.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Provider } from "@/provider/provider"
22
import { Log } from "@/util/log"
3-
import { Effect, Layer, ServiceMap } from "effect"
3+
import { Effect, Layer, Record, ServiceMap } from "effect"
44
import * as Stream from "effect/Stream"
55
import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai"
66
import { mergeDeep, pipe } from "remeda"
@@ -28,14 +28,17 @@ export namespace LLM {
2828
agent: Agent.Info
2929
permission?: Permission.Ruleset
3030
system: string[]
31-
abort: AbortSignal
3231
messages: ModelMessage[]
3332
small?: boolean
3433
tools: Record<string, Tool>
3534
retries?: number
3635
toolChoice?: "auto" | "required" | "none"
3736
}
3837

38+
export type StreamRequest = StreamInput & {
39+
abort: AbortSignal
40+
}
41+
3942
export type Event = Awaited<ReturnType<typeof stream>>["fullStream"] extends AsyncIterable<infer T> ? T : never
4043

4144
export interface Interface {
@@ -50,7 +53,7 @@ export namespace LLM {
5053
return Service.of({
5154
stream(input) {
5255
return Stream.unwrap(
53-
Effect.promise(() => LLM.stream(input)).pipe(
56+
Effect.promise((signal) => LLM.stream({ ...input, abort: signal })).pipe(
5457
Effect.map((result) =>
5558
Stream.fromAsyncIterable(result.fullStream, (err) => err).pipe(
5659
Stream.mapEffect((event) => Effect.succeed(event)),
@@ -65,7 +68,7 @@ export namespace LLM {
6568

6669
export const defaultLayer = layer
6770

68-
export async function stream(input: StreamInput) {
71+
export async function stream(input: StreamRequest) {
6972
const l = log
7073
.clone()
7174
.tag("providerID", input.model.providerID)
@@ -314,17 +317,12 @@ export namespace LLM {
314317
})
315318
}
316319

317-
async function resolveTools(input: Pick<StreamInput, "tools" | "agent" | "permission" | "user">) {
320+
function resolveTools(input: Pick<StreamInput, "tools" | "agent" | "permission" | "user">) {
318321
const disabled = Permission.disabled(
319322
Object.keys(input.tools),
320323
Permission.merge(input.agent.permission, input.permission ?? []),
321324
)
322-
for (const tool of Object.keys(input.tools)) {
323-
if (input.user.tools?.[tool] === false || disabled.has(tool)) {
324-
delete input.tools[tool]
325-
}
326-
}
327-
return input.tools
325+
return Record.filter(input.tools, (_, k) => input.user.tools?.[k] !== false && !disabled.has(k))
328326
}
329327

330328
// Check if messages contain any tool-call content

packages/opencode/src/session/processor.ts

Lines changed: 10 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Cause, Effect, Exit, Layer, ServiceMap } from "effect"
22
import * as Stream from "effect/Stream"
33
import { Agent } from "@/agent/agent"
44
import { Bus } from "@/bus"
5-
import { makeRuntime } from "@/effect/run-service"
65
import { Config } from "@/config/config"
76
import { Permission } from "@/permission"
87
import { Plugin } from "@/plugin"
@@ -35,17 +34,10 @@ export namespace SessionProcessor {
3534
readonly process: (streamInput: LLM.StreamInput) => Effect.Effect<Result>
3635
}
3736

38-
export interface Info {
39-
readonly message: MessageV2.Assistant
40-
readonly partFromToolCall: (toolCallID: string) => MessageV2.ToolPart | undefined
41-
readonly process: (streamInput: LLM.StreamInput) => Promise<Result>
42-
}
43-
4437
type Input = {
4538
assistantMessage: MessageV2.Assistant
4639
sessionID: SessionID
4740
model: Provider.Model
48-
abort: AbortSignal
4941
}
5042

5143
export interface Interface {
@@ -96,7 +88,6 @@ export namespace SessionProcessor {
9688
assistantMessage: input.assistantMessage,
9789
sessionID: input.sessionID,
9890
model: input.model,
99-
abort: input.abort,
10091
toolcalls: {},
10192
shouldBreak: false,
10293
snapshot: undefined,
@@ -105,11 +96,12 @@ export namespace SessionProcessor {
10596
currentText: undefined,
10697
reasoningMap: {},
10798
}
99+
let aborted = false
108100

109101
const parse = (e: unknown) =>
110102
MessageV2.fromError(e, {
111103
providerID: input.model.providerID,
112-
aborted: input.abort.aborted,
104+
aborted,
113105
})
114106

115107
const handleEvent = Effect.fn("SessionProcessor.handleEvent")(function* (value: StreamEvent) {
@@ -440,16 +432,12 @@ export namespace SessionProcessor {
440432
const stream = llm.stream(streamInput)
441433

442434
yield* stream.pipe(
443-
Stream.tap((event) =>
444-
Effect.gen(function* () {
445-
input.abort.throwIfAborted()
446-
yield* handleEvent(event)
447-
}),
448-
),
435+
Stream.tap((event) => handleEvent(event)),
449436
Stream.takeUntil(() => ctx.needsCompaction),
450437
Stream.runDrain,
451438
)
452439
}).pipe(
440+
Effect.onInterrupt(() => Effect.sync(() => void (aborted = true))),
453441
Effect.catchCauseIf(
454442
(cause) => !Cause.hasInterruptsOnly(cause),
455443
(cause) => Effect.fail(Cause.squash(cause)),
@@ -468,17 +456,20 @@ export namespace SessionProcessor {
468456
),
469457
Effect.catchCause((cause) =>
470458
Cause.hasInterruptsOnly(cause)
471-
? halt(new DOMException("Aborted", "AbortError"))
459+
? Effect.gen(function* () {
460+
aborted = true
461+
yield* halt(new DOMException("Aborted", "AbortError"))
462+
})
472463
: halt(Cause.squash(cause)),
473464
),
474465
Effect.ensuring(cleanup()),
475466
)
476467

477-
if (input.abort.aborted && !ctx.assistantMessage.error) {
468+
if (aborted && !ctx.assistantMessage.error) {
478469
yield* abort()
479470
}
480471
if (ctx.needsCompaction) return "compact"
481-
if (ctx.blocked || ctx.assistantMessage.error || input.abort.aborted) return "stop"
472+
if (ctx.blocked || ctx.assistantMessage.error || aborted) return "stop"
482473
return "continue"
483474
})
484475

@@ -526,29 +517,4 @@ export namespace SessionProcessor {
526517
),
527518
),
528519
)
529-
530-
const { runPromise } = makeRuntime(Service, defaultLayer)
531-
532-
export async function create(input: Input): Promise<Info> {
533-
const hit = await runPromise((svc) => svc.create(input))
534-
return {
535-
get message() {
536-
return hit.message
537-
},
538-
partFromToolCall(toolCallID: string) {
539-
return hit.partFromToolCall(toolCallID)
540-
},
541-
async process(streamInput: LLM.StreamInput) {
542-
const exit = await Effect.runPromiseExit(hit.process(streamInput), { signal: input.abort })
543-
if (Exit.isFailure(exit)) {
544-
if (Cause.hasInterrupts(exit.cause) && input.abort.aborted) {
545-
await Effect.runPromise(hit.abort())
546-
return "stop"
547-
}
548-
throw Cause.squash(exit.cause)
549-
}
550-
return exit.value
551-
},
552-
}
553-
}
554520
}

0 commit comments

Comments
 (0)