feat(core): expose session switching endpoints
This commit is contained in:
parent
9dadc2455f
commit
35b3fc85d0
@ -129,7 +129,7 @@ export interface Interface {
|
||||
readonly switchAgent: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
agent: string
|
||||
}) => Effect.Effect<void, OperationUnavailableError>
|
||||
}) => Effect.Effect<void, NotFoundError>
|
||||
readonly switchModel: (input: {
|
||||
sessionID: SessionSchema.ID
|
||||
model: ModelV2.Ref
|
||||
@ -376,8 +376,14 @@ export const layer = Layer.effect(
|
||||
skill: Effect.fn("V2Session.skill")(function* () {
|
||||
return yield* new OperationUnavailableError({ operation: "skill" })
|
||||
}),
|
||||
switchAgent: Effect.fn("V2Session.switchAgent")(function* () {
|
||||
return yield* new OperationUnavailableError({ operation: "switchAgent" })
|
||||
switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) {
|
||||
yield* result.get(input.sessionID)
|
||||
yield* events.publish(SessionEvent.AgentSwitched, {
|
||||
sessionID: input.sessionID,
|
||||
messageID: SessionMessage.ID.create(),
|
||||
timestamp: yield* DateTime.now,
|
||||
agent: input.agent,
|
||||
})
|
||||
}),
|
||||
switchModel: Effect.fn("V2Session.switchModel")(function* (input) {
|
||||
yield* result.get(input.sessionID)
|
||||
|
||||
@ -336,7 +336,34 @@ describe("SessionV2.create", () => {
|
||||
|
||||
expect(yield* unavailable(session.shell({ sessionID: created.id, command: "pwd" }))).toBe("shell")
|
||||
expect(yield* unavailable(session.skill({ sessionID: created.id, skill: "review" }))).toBe("skill")
|
||||
expect(yield* unavailable(session.switchAgent({ sessionID: created.id, agent: "build" }))).toBe("switchAgent")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("switches the selected agent through the durable Session event", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* SessionV2.Service
|
||||
const created = yield* session.create({ location })
|
||||
|
||||
yield* session.switchAgent({ sessionID: created.id, agent: "plan" })
|
||||
|
||||
expect(yield* session.get(created.id)).toMatchObject({ agent: "plan" })
|
||||
expect(
|
||||
Array.from(yield* session.events({ sessionID: created.id }).pipe(Stream.take(1), Stream.runCollect)),
|
||||
).toMatchObject([{ type: "session.next.agent.switched", data: { agent: "plan" } }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects an agent switch for a missing Session", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* SessionV2.Service
|
||||
const missing = SessionV2.ID.make("ses_missing_agent_switch")
|
||||
|
||||
expect(
|
||||
yield* session.switchAgent({ sessionID: missing, agent: "plan" }).pipe(
|
||||
Effect.flip,
|
||||
Effect.map((error) => error._tag),
|
||||
),
|
||||
).toBe("Session.NotFoundError")
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@ -955,6 +955,24 @@ const scenarios: Scenario[] = [
|
||||
headers: ctx.headers(),
|
||||
}))
|
||||
.json(200, data(object)),
|
||||
http.protected
|
||||
.post("/api/session/{sessionID}/agent", "v2.session.switchAgent")
|
||||
.seeded((ctx) => ctx.session({ title: "Switch agent" }))
|
||||
.at((ctx) => ({
|
||||
path: route("/api/session/{sessionID}/agent", { sessionID: ctx.state.id }),
|
||||
headers: { ...ctx.headers(), "content-type": "application/json" },
|
||||
body: { agent: "plan" },
|
||||
}))
|
||||
.status(204, undefined, "none"),
|
||||
http.protected
|
||||
.post("/api/session/{sessionID}/model", "v2.session.switchModel")
|
||||
.seeded((ctx) => ctx.session({ title: "Switch model" }))
|
||||
.at((ctx) => ({
|
||||
path: route("/api/session/{sessionID}/model", { sessionID: ctx.state.id }),
|
||||
headers: { ...ctx.headers(), "content-type": "application/json" },
|
||||
body: { model: { providerID: "opencode", id: "big-pickle" } },
|
||||
}))
|
||||
.status(204, undefined, "none"),
|
||||
http.protected
|
||||
.get("/api/session/{sessionID}/context", "v2.session.context")
|
||||
.at((ctx) => ({
|
||||
|
||||
@ -353,6 +353,10 @@ import type {
|
||||
V2SessionQuestionRejectResponses,
|
||||
V2SessionQuestionReplyErrors,
|
||||
V2SessionQuestionReplyResponses,
|
||||
V2SessionSwitchAgentErrors,
|
||||
V2SessionSwitchAgentResponses,
|
||||
V2SessionSwitchModelErrors,
|
||||
V2SessionSwitchModelResponses,
|
||||
V2SessionWaitErrors,
|
||||
V2SessionWaitResponses,
|
||||
V2SkillListErrors,
|
||||
@ -5337,6 +5341,88 @@ export class Session3 extends HeyApiClient {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch session agent
|
||||
*
|
||||
* Switch the agent used by subsequent session activity.
|
||||
*/
|
||||
public switchAgent<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
sessionID: string
|
||||
agent?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "path", key: "sessionID" },
|
||||
{ in: "body", key: "agent" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).post<
|
||||
V2SessionSwitchAgentResponses,
|
||||
V2SessionSwitchAgentErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/api/session/{sessionID}/agent",
|
||||
...options,
|
||||
...params,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
...params.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch session model
|
||||
*
|
||||
* Switch the model used by subsequent session activity.
|
||||
*/
|
||||
public switchModel<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
sessionID: string
|
||||
model?: {
|
||||
id: string
|
||||
providerID: string
|
||||
variant?: string
|
||||
}
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "path", key: "sessionID" },
|
||||
{ in: "body", key: "model" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).post<
|
||||
V2SessionSwitchModelResponses,
|
||||
V2SessionSwitchModelErrors,
|
||||
ThrowOnError
|
||||
>({
|
||||
url: "/api/session/{sessionID}/model",
|
||||
...options,
|
||||
...params,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...options?.headers,
|
||||
...params.headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message
|
||||
*
|
||||
|
||||
@ -9521,6 +9521,84 @@ export type V2SessionGetResponses = {
|
||||
|
||||
export type V2SessionGetResponse = V2SessionGetResponses[keyof V2SessionGetResponses]
|
||||
|
||||
export type V2SessionSwitchAgentData = {
|
||||
body: {
|
||||
agent: string
|
||||
}
|
||||
path: {
|
||||
sessionID: string
|
||||
}
|
||||
query?: never
|
||||
url: "/api/session/{sessionID}/agent"
|
||||
}
|
||||
|
||||
export type V2SessionSwitchAgentErrors = {
|
||||
/**
|
||||
* InvalidRequestError
|
||||
*/
|
||||
400: InvalidRequestError
|
||||
/**
|
||||
* UnauthorizedError
|
||||
*/
|
||||
401: UnauthorizedError
|
||||
/**
|
||||
* SessionNotFoundError
|
||||
*/
|
||||
404: SessionNotFoundError
|
||||
}
|
||||
|
||||
export type V2SessionSwitchAgentError = V2SessionSwitchAgentErrors[keyof V2SessionSwitchAgentErrors]
|
||||
|
||||
export type V2SessionSwitchAgentResponses = {
|
||||
/**
|
||||
* <No Content>
|
||||
*/
|
||||
204: void
|
||||
}
|
||||
|
||||
export type V2SessionSwitchAgentResponse = V2SessionSwitchAgentResponses[keyof V2SessionSwitchAgentResponses]
|
||||
|
||||
export type V2SessionSwitchModelData = {
|
||||
body: {
|
||||
model: {
|
||||
id: string
|
||||
providerID: string
|
||||
variant?: string
|
||||
}
|
||||
}
|
||||
path: {
|
||||
sessionID: string
|
||||
}
|
||||
query?: never
|
||||
url: "/api/session/{sessionID}/model"
|
||||
}
|
||||
|
||||
export type V2SessionSwitchModelErrors = {
|
||||
/**
|
||||
* InvalidRequestError
|
||||
*/
|
||||
400: InvalidRequestError
|
||||
/**
|
||||
* UnauthorizedError
|
||||
*/
|
||||
401: UnauthorizedError
|
||||
/**
|
||||
* SessionNotFoundError
|
||||
*/
|
||||
404: SessionNotFoundError
|
||||
}
|
||||
|
||||
export type V2SessionSwitchModelError = V2SessionSwitchModelErrors[keyof V2SessionSwitchModelErrors]
|
||||
|
||||
export type V2SessionSwitchModelResponses = {
|
||||
/**
|
||||
* <No Content>
|
||||
*/
|
||||
204: void
|
||||
}
|
||||
|
||||
export type V2SessionSwitchModelResponse = V2SessionSwitchModelResponses[keyof V2SessionSwitchModelResponses]
|
||||
|
||||
export type V2SessionPromptData = {
|
||||
body: {
|
||||
id?: string
|
||||
|
||||
@ -140,6 +140,38 @@ export const SessionGroup = HttpApiGroup.make("server.session")
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.switchAgent", "/api/session/:sessionID/agent", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
payload: Schema.Struct({ agent: AgentV2.ID }),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: SessionNotFoundError,
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.switchAgent",
|
||||
summary: "Switch session agent",
|
||||
description: "Switch the agent used by subsequent session activity.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.switchModel", "/api/session/:sessionID/model", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
payload: Schema.Struct({ model: ModelV2.Ref }),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: SessionNotFoundError,
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.switchModel",
|
||||
summary: "Switch session model",
|
||||
description: "Switch the model used by subsequent session activity.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.prompt", "/api/session/:sessionID/prompt", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
|
||||
@ -92,6 +92,38 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
}
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.switchAgent",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* session.switchAgent({ sessionID: ctx.params.sessionID, agent: ctx.payload.agent }).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
Effect.fail(
|
||||
new SessionNotFoundError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.switchModel",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* session.switchModel({ sessionID: ctx.params.sessionID, model: ctx.payload.model }).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
Effect.fail(
|
||||
new SessionNotFoundError({
|
||||
sessionID: error.sessionID,
|
||||
message: `Session not found: ${error.sessionID}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.prompt",
|
||||
Effect.fn(function* (ctx) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user