feat(sdk): restore session runtime operations (#33777)

This commit is contained in:
Kit Langton 2026-06-25 20:23:01 +02:00 committed by GitHub
commit f44423609b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1099 additions and 92 deletions

View file

@ -1,7 +1,7 @@
import { expect, test } from "bun:test"
import { DateTime, Effect } from "effect"
import { DateTime, Effect, Stream } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
import { AbsolutePath, Agent, Location, Model, OpenCode, Prompt, Session } from "../src/effect"
import { AbsolutePath, Agent, Location, Model, OpenCode, Prompt, Session, SessionMessage } from "../src/effect"
test("sessions.get returns the decoded Effect projection", async () => {
const httpClient = HttpClient.make((request) =>
@ -18,12 +18,25 @@ test("sessions.get returns the decoded Effect projection", async () => {
test("session methods retain decoded Effect inputs and outputs", async () => {
const httpClient = HttpClient.make((request) => {
const url = request.url
if (url.includes("/event")) {
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`, {
headers: { "content-type": "text/event-stream" },
}),
),
)
}
if (url.includes("/prompt")) {
return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(admission)))
}
if (url.includes("/context")) {
return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ data: [] })))
}
if (url.includes("/message/")) {
return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ data: modelSwitchedMessage })))
}
if (request.method === "POST" && url.endsWith("/api/session")) {
return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(session)))
}
@ -53,7 +66,15 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
yield* client.sessions.compact({ sessionID: Session.ID.make("ses_test") })
yield* client.sessions.wait({ sessionID: Session.ID.make("ses_test") })
const context = yield* client.sessions.context({ sessionID: Session.ID.make("ses_test") })
return { page, created, admitted, context }
const events = yield* client.sessions
.events({ sessionID: Session.ID.make("ses_test"), after: 0 })
.pipe(Stream.runCollect)
yield* client.sessions.interrupt({ sessionID: Session.ID.make("ses_test") })
const message = yield* client.sessions.message({
sessionID: Session.ID.make("ses_test"),
messageID: SessionMessage.ID.make("msg_model"),
})
return { page, created, admitted, context, events, message }
}).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
expect(DateTime.toEpochMillis(result.page.data[0].time.created)).toBe(1_717_171_717_000)
@ -64,6 +85,8 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
expect(Object.getPrototypeOf(result.admitted.prompt)).toBe(Object.prototype)
expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
expect(result.context).toEqual([])
expect(DateTime.toEpochMillis(result.events[0].data.timestamp)).toBe(1_717_171_717_000)
expect(result.message).toEqual(expect.objectContaining({ id: "msg_model", type: "model-switched" }))
})
const session = {
@ -96,3 +119,22 @@ const admission = {
timeCreated: 1_717_171_717_000,
},
}
const modelSwitchedMessage = {
id: "msg_model",
type: "model-switched",
time: { created: 1_717_171_717_000 },
model: { id: "claude", providerID: "anthropic" },
}
const modelSwitchedEvent = {
id: "evt_model",
type: "session.next.model.switched",
durable: { aggregateID: "ses_test", seq: 1, version: 1 },
data: {
timestamp: 1_717_171_717_000,
sessionID: "ses_test",
messageID: "msg_model",
model: { id: "claude", providerID: "anthropic" },
},
}

View file

@ -24,8 +24,14 @@ test("session methods use the public HTTP contract", async () => {
fetch: async (input, init) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
requests.push({ url, init })
if (url.includes("/event")) {
return new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`, {
headers: { "content-type": "text/event-stream" },
})
}
if (url.includes("/prompt")) return Response.json(admission)
if (url.includes("/context")) return Response.json({ data: [] })
if (url.includes("/message/")) return Response.json({ data: modelSwitchedMessage })
if (init?.method === "POST" && url.endsWith("/api/session")) return Response.json(session)
if (init?.method === "POST") return new Response(null, { status: 204 })
return Response.json({ data: [session.data], cursor: { next: "next" } })
@ -47,11 +53,17 @@ test("session methods use the public HTTP contract", async () => {
await client.sessions.compact({ sessionID: "ses_test" })
await client.sessions.wait({ sessionID: "ses_test" })
const context = await client.sessions.context({ sessionID: "ses_test" })
const events = []
for await (const event of client.sessions.events({ sessionID: "ses_test", after: "0" })) events.push(event)
await client.sessions.interrupt({ sessionID: "ses_test" })
const message = await client.sessions.message({ sessionID: "ses_test", messageID: "msg_model" })
expect(page.cursor.next).toBe("next")
expect(created.id).toBe("ses_test")
expect(admitted.id).toBe("msg_test")
expect(context).toEqual([])
expect(events).toEqual([modelSwitchedEvent])
expect(message).toEqual(modelSwitchedMessage)
expect(requests.map((request) => [request.init?.method, request.url])).toEqual([
["GET", "http://localhost:3000/api/session?limit=10&order=desc"],
["POST", "http://localhost:3000/api/session"],
@ -61,6 +73,9 @@ test("session methods use the public HTTP contract", async () => {
["POST", "http://localhost:3000/api/session/ses_test/compact"],
["POST", "http://localhost:3000/api/session/ses_test/wait"],
["GET", "http://localhost:3000/api/session/ses_test/context"],
["GET", "http://localhost:3000/api/session/ses_test/event?after=0"],
["POST", "http://localhost:3000/api/session/ses_test/interrupt"],
["GET", "http://localhost:3000/api/session/ses_test/message/msg_model"],
])
const body = requests[4]?.init?.body
if (typeof body !== "string") throw new Error("Expected JSON request body")
@ -115,3 +130,22 @@ const admission = {
timeCreated: 1_717_171_717_000,
},
}
const modelSwitchedMessage = {
id: "msg_model",
type: "model-switched",
time: { created: 1_717_171_717_000 },
model: { id: "claude", providerID: "anthropic" },
}
const modelSwitchedEvent = {
id: "evt_model",
type: "session.next.model.switched",
durable: { aggregateID: "ses_test", seq: 1, version: 1 },
data: {
timestamp: 1_717_171_717_000,
sessionID: "ses_test",
messageID: "msg_model",
model: { id: "claude", providerID: "anthropic" },
},
}