feat(core): expose session switching endpoints
This commit is contained in:
@@ -129,7 +129,7 @@ export interface Interface {
|
|||||||
readonly switchAgent: (input: {
|
readonly switchAgent: (input: {
|
||||||
sessionID: SessionSchema.ID
|
sessionID: SessionSchema.ID
|
||||||
agent: string
|
agent: string
|
||||||
}) => Effect.Effect<void, OperationUnavailableError>
|
}) => Effect.Effect<void, NotFoundError>
|
||||||
readonly switchModel: (input: {
|
readonly switchModel: (input: {
|
||||||
sessionID: SessionSchema.ID
|
sessionID: SessionSchema.ID
|
||||||
model: ModelV2.Ref
|
model: ModelV2.Ref
|
||||||
@@ -376,8 +376,14 @@ export const layer = Layer.effect(
|
|||||||
skill: Effect.fn("V2Session.skill")(function* () {
|
skill: Effect.fn("V2Session.skill")(function* () {
|
||||||
return yield* new OperationUnavailableError({ operation: "skill" })
|
return yield* new OperationUnavailableError({ operation: "skill" })
|
||||||
}),
|
}),
|
||||||
switchAgent: Effect.fn("V2Session.switchAgent")(function* () {
|
switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) {
|
||||||
return yield* new OperationUnavailableError({ operation: "switchAgent" })
|
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) {
|
switchModel: Effect.fn("V2Session.switchModel")(function* (input) {
|
||||||
yield* result.get(input.sessionID)
|
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.shell({ sessionID: created.id, command: "pwd" }))).toBe("shell")
|
||||||
expect(yield* unavailable(session.skill({ sessionID: created.id, skill: "review" }))).toBe("skill")
|
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(),
|
headers: ctx.headers(),
|
||||||
}))
|
}))
|
||||||
.json(200, data(object)),
|
.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
|
http.protected
|
||||||
.get("/api/session/{sessionID}/context", "v2.session.context")
|
.get("/api/session/{sessionID}/context", "v2.session.context")
|
||||||
.at((ctx) => ({
|
.at((ctx) => ({
|
||||||
|
|||||||
@@ -353,6 +353,10 @@ import type {
|
|||||||
V2SessionQuestionRejectResponses,
|
V2SessionQuestionRejectResponses,
|
||||||
V2SessionQuestionReplyErrors,
|
V2SessionQuestionReplyErrors,
|
||||||
V2SessionQuestionReplyResponses,
|
V2SessionQuestionReplyResponses,
|
||||||
|
V2SessionSwitchAgentErrors,
|
||||||
|
V2SessionSwitchAgentResponses,
|
||||||
|
V2SessionSwitchModelErrors,
|
||||||
|
V2SessionSwitchModelResponses,
|
||||||
V2SessionWaitErrors,
|
V2SessionWaitErrors,
|
||||||
V2SessionWaitResponses,
|
V2SessionWaitResponses,
|
||||||
V2SkillListErrors,
|
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
|
* Send message
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -9521,6 +9521,84 @@ export type V2SessionGetResponses = {
|
|||||||
|
|
||||||
export type V2SessionGetResponse = V2SessionGetResponses[keyof 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 = {
|
export type V2SessionPromptData = {
|
||||||
body: {
|
body: {
|
||||||
id?: string
|
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(
|
.add(
|
||||||
HttpApiEndpoint.post("session.prompt", "/api/session/:sessionID/prompt", {
|
HttpApiEndpoint.post("session.prompt", "/api/session/:sessionID/prompt", {
|
||||||
params: { sessionID: SessionV2.ID },
|
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(
|
.handle(
|
||||||
"session.prompt",
|
"session.prompt",
|
||||||
Effect.fn(function* (ctx) {
|
Effect.fn(function* (ctx) {
|
||||||
|
|||||||
Reference in New Issue
Block a user