Skip to content

Commit 53f212c

Browse files
committed
core: expose v2 session controls
1 parent 9979e8f commit 53f212c

2 files changed

Lines changed: 116 additions & 22 deletions

File tree

packages/opencode/src/server/routes/instance/httpapi/v2.ts

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { SessionID } from "@/session/schema"
22
import { SessionMessage } from "@/v2/session-message"
3+
import { Prompt } from "@/v2/session-prompt"
34
import { SessionV2 } from "@/v2/session"
45
import { Effect, Layer, Schema } from "effect"
5-
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
6+
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
67
import { Authorization } from "./auth"
78

89
export const V2Api = HttpApi.make("v2")
@@ -20,6 +21,46 @@ export const V2Api = HttpApi.make("v2")
2021
}),
2122
),
2223
)
24+
.add(
25+
HttpApiEndpoint.post("prompt", "/api/session/:sessionID/prompt", {
26+
params: { sessionID: SessionID },
27+
payload: Schema.Struct({
28+
prompt: Prompt,
29+
delivery: SessionV2.Delivery.pipe(Schema.optional),
30+
}),
31+
success: SessionMessage.Message,
32+
}).annotateMerge(
33+
OpenApi.annotations({
34+
identifier: "v2.session.prompt",
35+
summary: "Send v2 message",
36+
description: "Create a v2 session message and queue it for the agent loop.",
37+
}),
38+
),
39+
)
40+
.add(
41+
HttpApiEndpoint.post("compact", "/api/session/:sessionID/compact", {
42+
params: { sessionID: SessionID },
43+
success: HttpApiSchema.NoContent,
44+
}).annotateMerge(
45+
OpenApi.annotations({
46+
identifier: "v2.session.compact",
47+
summary: "Compact v2 session",
48+
description: "Compact a v2 session conversation.",
49+
}),
50+
),
51+
)
52+
.add(
53+
HttpApiEndpoint.post("wait", "/api/session/:sessionID/wait", {
54+
params: { sessionID: SessionID },
55+
success: HttpApiSchema.NoContent,
56+
}).annotateMerge(
57+
OpenApi.annotations({
58+
identifier: "v2.session.wait",
59+
summary: "Wait for v2 session",
60+
description: "Wait for a v2 session agent loop to become idle.",
61+
}),
62+
),
63+
)
2364
.annotateMerge(
2465
OpenApi.annotations({
2566
title: "v2",
@@ -39,12 +80,37 @@ export const V2Api = HttpApi.make("v2")
3980
export const v2Handlers = HttpApiBuilder.group(V2Api, "v2", (handlers) =>
4081
Effect.gen(function* () {
4182
const session = yield* SessionV2.Service
42-
return handlers.handle(
43-
"messages",
44-
Effect.fn(function* (ctx) {
45-
return yield* session.messages(ctx.params.sessionID)
46-
}),
47-
)
83+
return handlers
84+
.handle(
85+
"messages",
86+
Effect.fn(function* (ctx) {
87+
return yield* session.messages(ctx.params.sessionID)
88+
}),
89+
)
90+
.handle(
91+
"prompt",
92+
Effect.fn(function* (ctx) {
93+
return yield* session.prompt({
94+
sessionID: ctx.params.sessionID,
95+
prompt: ctx.payload.prompt,
96+
delivery: ctx.payload.delivery ?? SessionV2.DefaultDelivery,
97+
})
98+
}),
99+
)
100+
.handle(
101+
"compact",
102+
Effect.fn(function* (ctx) {
103+
yield* session.compact(ctx.params.sessionID)
104+
return HttpApiSchema.NoContent.make()
105+
}),
106+
)
107+
.handle(
108+
"wait",
109+
Effect.fn(function* (ctx) {
110+
yield* session.wait(ctx.params.sessionID)
111+
return HttpApiSchema.NoContent.make()
112+
}),
113+
)
48114
}),
49115
).pipe(Layer.provide(SessionV2.defaultLayer))
50116

packages/opencode/src/v2/session.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,64 @@ import { asc, eq } from "@/storage/db"
44
import * as Database from "@/storage/db"
55
import { Context, Effect, Layer, Schema } from "effect"
66
import { SessionMessage } from "./session-message"
7+
import type { Prompt } from "./session-prompt"
8+
import { Session } from "@/session/session"
9+
import { SessionPrompt } from "@/session/prompt"
10+
import type { Event } from "./event"
11+
12+
export const Delivery = Schema.Union([Schema.Literal("immediate"), Schema.Literal("deferred")]).annotate({
13+
identifier: "Session.Delivery",
14+
})
15+
export type Delivery = Schema.Schema.Type<typeof Delivery>
16+
17+
export const DefaultDelivery = "immediate" satisfies Delivery
718

819
export interface Interface {
920
readonly messages: (sessionID: SessionID) => Effect.Effect<SessionMessage.Message[], never>
21+
readonly prompt: (input: {
22+
id?: Event.ID
23+
sessionID: SessionID
24+
prompt: Prompt
25+
delivery?: Delivery
26+
}) => Effect.Effect<SessionMessage.User, never>
27+
readonly compact: (sessionID: SessionID) => Effect.Effect<void, never>
28+
readonly wait: (sessionID: SessionID) => Effect.Effect<void, never>
1029
}
1130

1231
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Session") {}
1332

14-
export const layer: Layer.Layer<Service> = Layer.effect(
33+
export const layer = Layer.effect(
1534
Service,
1635
Effect.gen(function* () {
36+
const prompt = yield* SessionPrompt.Service
1737
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message)
1838
const decode = (row: typeof SessionMessageTable.$inferSelect) =>
1939
decodeMessage({ ...row.data, id: row.id, type: row.type })
2040

21-
const messages = Effect.fn("V2Session.messages")(function* (sessionID: SessionID) {
22-
return Database.use((db) =>
23-
db
24-
.select()
25-
.from(SessionMessageTable)
26-
.where(eq(SessionMessageTable.session_id, sessionID))
27-
.orderBy(asc(SessionMessageTable.time_created), asc(SessionMessageTable.id))
28-
.all()
29-
.map((row) => decode(row)),
30-
)
31-
})
32-
33-
return Service.of({ messages })
41+
const result: Interface = {
42+
messages: Effect.fn("V2Session.messages")(function* (sessionID) {
43+
return Database.use((db) =>
44+
db
45+
.select()
46+
.from(SessionMessageTable)
47+
.where(eq(SessionMessageTable.session_id, sessionID))
48+
.orderBy(asc(SessionMessageTable.time_created), asc(SessionMessageTable.id))
49+
.all()
50+
.map((row) => decode(row)),
51+
)
52+
}),
53+
prompt: Effect.fn("V2Session.prompt")(function* (input) {
54+
const delivery = input.delivery ?? DefaultDelivery
55+
return {} as any
56+
}),
57+
compact: Effect.fn("V2Session.compact")(function* (sessionID) {}),
58+
wait: Effect.fn("V2Session.wait")(function* (sessionID) {}),
59+
}
60+
61+
return Service.of(result)
3462
}),
3563
)
3664

37-
export const defaultLayer = layer
65+
export const defaultLayer = layer.pipe(Layer.provide(SessionPrompt.defaultLayer))
3866

3967
export * as SessionV2 from "./session"

0 commit comments

Comments
 (0)