Preserve workspace context in session HTTP routes (#25136)
This commit is contained in:
parent
f384675c01
commit
feeebbe7d4
4 changed files with 154 additions and 248 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import * as InstanceState from "@/effect/instance-state"
|
import * as InstanceState from "@/effect/instance-state"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { EffectBridge } from "@/effect/bridge"
|
||||||
import { Agent } from "@/agent/agent"
|
import { Agent } from "@/agent/agent"
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { Command } from "@/command"
|
import { Command } from "@/command"
|
||||||
|
|
@ -53,6 +53,13 @@ const mapNotFound = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
||||||
export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", (handlers) =>
|
export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session", (handlers) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const session = yield* Session.Service
|
const session = yield* Session.Service
|
||||||
|
const shareSvc = yield* SessionShare.Service
|
||||||
|
const promptSvc = yield* SessionPrompt.Service
|
||||||
|
const revertSvc = yield* SessionRevert.Service
|
||||||
|
const compactSvc = yield* SessionCompaction.Service
|
||||||
|
const runState = yield* SessionRunState.Service
|
||||||
|
const agentSvc = yield* Agent.Service
|
||||||
|
const permissionSvc = yield* Permission.Service
|
||||||
const statusSvc = yield* SessionStatus.Service
|
const statusSvc = yield* SessionStatus.Service
|
||||||
const todoSvc = yield* Todo.Service
|
const todoSvc = yield* Todo.Service
|
||||||
const summary = yield* SessionSummary.Service
|
const summary = yield* SessionSummary.Service
|
||||||
|
|
@ -148,14 +155,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||||
})
|
})
|
||||||
|
|
||||||
const create = Effect.fn("SessionHttpApi.create")(function* (ctx: { payload?: Session.CreateInput }) {
|
const create = Effect.fn("SessionHttpApi.create")(function* (ctx: { payload?: Session.CreateInput }) {
|
||||||
const instance = yield* InstanceState.context
|
return yield* shareSvc.create(ctx.payload)
|
||||||
return yield* Effect.promise(() =>
|
|
||||||
Instance.restore(instance, () =>
|
|
||||||
AppRuntime.runPromise(
|
|
||||||
SessionShare.Service.use((svc) => svc.create(ctx.payload)).pipe(Effect.provide(SessionShare.defaultLayer)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const createRaw = Effect.fn("SessionHttpApi.createRaw")(function* (ctx: {
|
const createRaw = Effect.fn("SessionHttpApi.createRaw")(function* (ctx: {
|
||||||
|
|
@ -175,14 +175,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||||
})
|
})
|
||||||
|
|
||||||
const remove = Effect.fn("SessionHttpApi.remove")(function* (ctx: { params: { sessionID: SessionID } }) {
|
const remove = Effect.fn("SessionHttpApi.remove")(function* (ctx: { params: { sessionID: SessionID } }) {
|
||||||
const instance = yield* InstanceState.context
|
yield* session.remove(ctx.params.sessionID)
|
||||||
yield* Effect.promise(() =>
|
|
||||||
Instance.restore(instance, () =>
|
|
||||||
AppRuntime.runPromise(
|
|
||||||
Session.Service.use((svc) => svc.remove(ctx.params.sessionID)).pipe(Effect.provide(Session.defaultLayer)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -190,60 +183,31 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
payload: typeof UpdatePayload.Type
|
payload: typeof UpdatePayload.Type
|
||||||
}) {
|
}) {
|
||||||
const instance = yield* InstanceState.context
|
const current = yield* session.get(ctx.params.sessionID)
|
||||||
return yield* Effect.promise(() =>
|
if (ctx.payload.title !== undefined) {
|
||||||
Instance.restore(instance, () =>
|
yield* session.setTitle({ sessionID: ctx.params.sessionID, title: ctx.payload.title })
|
||||||
AppRuntime.runPromise(
|
}
|
||||||
Session.Service.use((svc) =>
|
if (ctx.payload.permission !== undefined) {
|
||||||
Effect.gen(function* () {
|
yield* session.setPermission({
|
||||||
const current = yield* svc.get(ctx.params.sessionID)
|
sessionID: ctx.params.sessionID,
|
||||||
if (ctx.payload.title !== undefined) {
|
permission: Permission.merge(current.permission ?? [], ctx.payload.permission),
|
||||||
yield* svc.setTitle({ sessionID: ctx.params.sessionID, title: ctx.payload.title })
|
})
|
||||||
}
|
}
|
||||||
if (ctx.payload.permission !== undefined) {
|
if (ctx.payload.time?.archived !== undefined) {
|
||||||
yield* svc.setPermission({
|
yield* session.setArchived({ sessionID: ctx.params.sessionID, time: ctx.payload.time.archived })
|
||||||
sessionID: ctx.params.sessionID,
|
}
|
||||||
permission: Permission.merge(current.permission ?? [], ctx.payload.permission),
|
return yield* session.get(ctx.params.sessionID)
|
||||||
})
|
|
||||||
}
|
|
||||||
if (ctx.payload.time?.archived !== undefined) {
|
|
||||||
yield* svc.setArchived({ sessionID: ctx.params.sessionID, time: ctx.payload.time.archived })
|
|
||||||
}
|
|
||||||
return yield* svc.get(ctx.params.sessionID)
|
|
||||||
}),
|
|
||||||
).pipe(Effect.provide(Session.defaultLayer)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const fork = Effect.fn("SessionHttpApi.fork")(function* (ctx: {
|
const fork = Effect.fn("SessionHttpApi.fork")(function* (ctx: {
|
||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
payload: typeof ForkPayload.Type
|
payload: typeof ForkPayload.Type
|
||||||
}) {
|
}) {
|
||||||
const instance = yield* InstanceState.context
|
return yield* session.fork({ sessionID: ctx.params.sessionID, messageID: ctx.payload.messageID })
|
||||||
return yield* Effect.promise(() =>
|
|
||||||
Instance.restore(instance, () =>
|
|
||||||
AppRuntime.runPromise(
|
|
||||||
Session.Service.use((svc) =>
|
|
||||||
svc.fork({ sessionID: ctx.params.sessionID, messageID: ctx.payload.messageID }),
|
|
||||||
).pipe(Effect.provide(Session.defaultLayer)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const abort = Effect.fn("SessionHttpApi.abort")(function* (ctx: { params: { sessionID: SessionID } }) {
|
const abort = Effect.fn("SessionHttpApi.abort")(function* (ctx: { params: { sessionID: SessionID } }) {
|
||||||
const instance = yield* InstanceState.context
|
yield* promptSvc.cancel(ctx.params.sessionID)
|
||||||
yield* Effect.promise(() =>
|
|
||||||
Instance.restore(instance, () =>
|
|
||||||
AppRuntime.runPromise(
|
|
||||||
SessionPrompt.Service.use((svc) => svc.cancel(ctx.params.sessionID)).pipe(
|
|
||||||
Effect.provide(SessionPrompt.defaultLayer),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -251,98 +215,45 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
payload: typeof InitPayload.Type
|
payload: typeof InitPayload.Type
|
||||||
}) {
|
}) {
|
||||||
const instance = yield* InstanceState.context
|
yield* promptSvc.command({
|
||||||
yield* Effect.promise(() =>
|
sessionID: ctx.params.sessionID,
|
||||||
Instance.restore(instance, () =>
|
messageID: ctx.payload.messageID,
|
||||||
AppRuntime.runPromise(
|
model: `${ctx.payload.providerID}/${ctx.payload.modelID}`,
|
||||||
SessionPrompt.Service.use((svc) =>
|
command: Command.Default.INIT,
|
||||||
svc.command({
|
arguments: "",
|
||||||
sessionID: ctx.params.sessionID,
|
})
|
||||||
messageID: ctx.payload.messageID,
|
|
||||||
model: `${ctx.payload.providerID}/${ctx.payload.modelID}`,
|
|
||||||
command: Command.Default.INIT,
|
|
||||||
arguments: "",
|
|
||||||
}),
|
|
||||||
).pipe(Effect.provide(SessionPrompt.defaultLayer)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
const share = Effect.fn("SessionHttpApi.share")(function* (ctx: { params: { sessionID: SessionID } }) {
|
const share = Effect.fn("SessionHttpApi.share")(function* (ctx: { params: { sessionID: SessionID } }) {
|
||||||
const instance = yield* InstanceState.context
|
yield* shareSvc.share(ctx.params.sessionID).pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
|
||||||
return yield* Effect.promise(() =>
|
return yield* session.get(ctx.params.sessionID)
|
||||||
Instance.restore(instance, () =>
|
|
||||||
AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const share = yield* SessionShare.Service
|
|
||||||
const session = yield* Session.Service
|
|
||||||
yield* share.share(ctx.params.sessionID)
|
|
||||||
return yield* session.get(ctx.params.sessionID)
|
|
||||||
}).pipe(Effect.provide(SessionShare.defaultLayer)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const unshare = Effect.fn("SessionHttpApi.unshare")(function* (ctx: { params: { sessionID: SessionID } }) {
|
const unshare = Effect.fn("SessionHttpApi.unshare")(function* (ctx: { params: { sessionID: SessionID } }) {
|
||||||
const instance = yield* InstanceState.context
|
yield* shareSvc.unshare(ctx.params.sessionID).pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
|
||||||
return yield* Effect.promise(() =>
|
return yield* session.get(ctx.params.sessionID)
|
||||||
Instance.restore(instance, () =>
|
|
||||||
AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const share = yield* SessionShare.Service
|
|
||||||
const session = yield* Session.Service
|
|
||||||
yield* share.unshare(ctx.params.sessionID)
|
|
||||||
return yield* session.get(ctx.params.sessionID)
|
|
||||||
}).pipe(Effect.provide(SessionShare.defaultLayer)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const summarize = Effect.fn("SessionHttpApi.summarize")(function* (ctx: {
|
const summarize = Effect.fn("SessionHttpApi.summarize")(function* (ctx: {
|
||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
payload: typeof SummarizePayload.Type
|
payload: typeof SummarizePayload.Type
|
||||||
}) {
|
}) {
|
||||||
const instance = yield* InstanceState.context
|
yield* revertSvc.cleanup(yield* session.get(ctx.params.sessionID))
|
||||||
yield* Effect.promise(() =>
|
const messages = yield* session.messages({ sessionID: ctx.params.sessionID })
|
||||||
Instance.restore(instance, () =>
|
const defaultAgent = yield* agentSvc.defaultAgent()
|
||||||
AppRuntime.runPromise(
|
const currentAgent = messages.findLast((message) => message.info.role === "user")?.info.agent ?? defaultAgent
|
||||||
Effect.gen(function* () {
|
|
||||||
const session = yield* Session.Service
|
|
||||||
const revert = yield* SessionRevert.Service
|
|
||||||
const compact = yield* SessionCompaction.Service
|
|
||||||
const prompt = yield* SessionPrompt.Service
|
|
||||||
const agent = yield* Agent.Service
|
|
||||||
|
|
||||||
yield* revert.cleanup(yield* session.get(ctx.params.sessionID))
|
yield* compactSvc.create({
|
||||||
const messages = yield* session.messages({ sessionID: ctx.params.sessionID })
|
sessionID: ctx.params.sessionID,
|
||||||
const defaultAgent = yield* agent.defaultAgent()
|
agent: currentAgent,
|
||||||
const currentAgent =
|
model: {
|
||||||
messages.findLast((message) => message.info.role === "user")?.info.agent ?? defaultAgent
|
providerID: ctx.payload.providerID,
|
||||||
|
modelID: ctx.payload.modelID,
|
||||||
yield* compact.create({
|
},
|
||||||
sessionID: ctx.params.sessionID,
|
auto: ctx.payload.auto ?? false,
|
||||||
agent: currentAgent,
|
})
|
||||||
model: {
|
yield* promptSvc.loop({ sessionID: ctx.params.sessionID })
|
||||||
providerID: ctx.payload.providerID,
|
|
||||||
modelID: ctx.payload.modelID,
|
|
||||||
},
|
|
||||||
auto: ctx.payload.auto ?? false,
|
|
||||||
})
|
|
||||||
yield* prompt.loop({ sessionID: ctx.params.sessionID })
|
|
||||||
}).pipe(
|
|
||||||
Effect.provide(SessionRevert.defaultLayer),
|
|
||||||
Effect.provide(SessionCompaction.defaultLayer),
|
|
||||||
Effect.provide(SessionPrompt.defaultLayer),
|
|
||||||
Effect.provide(Agent.defaultLayer),
|
|
||||||
Effect.provide(Session.defaultLayer),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -350,19 +261,15 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
payload: typeof PromptPayload.Type
|
payload: typeof PromptPayload.Type
|
||||||
}) {
|
}) {
|
||||||
const instance = yield* InstanceState.context
|
const bridge = yield* EffectBridge.make()
|
||||||
return HttpServerResponse.stream(
|
return HttpServerResponse.stream(
|
||||||
Stream.fromEffect(
|
Stream.fromEffect(
|
||||||
Effect.promise(() =>
|
Effect.promise(() =>
|
||||||
Instance.restore(instance, () =>
|
bridge.promise(
|
||||||
AppRuntime.runPromise(
|
promptSvc.prompt({
|
||||||
SessionPrompt.Service.use((svc) =>
|
...ctx.payload,
|
||||||
svc.prompt({
|
sessionID: ctx.params.sessionID,
|
||||||
...ctx.payload,
|
} as unknown as SessionPrompt.PromptInput),
|
||||||
sessionID: ctx.params.sessionID,
|
|
||||||
} as unknown as SessionPrompt.PromptInput),
|
|
||||||
).pipe(Effect.provide(SessionPrompt.defaultLayer)),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).pipe(
|
).pipe(
|
||||||
|
|
@ -377,23 +284,23 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
payload: typeof PromptPayload.Type
|
payload: typeof PromptPayload.Type
|
||||||
}) {
|
}) {
|
||||||
const instance = yield* InstanceState.context
|
const bridge = yield* EffectBridge.make()
|
||||||
yield* Effect.sync(() => {
|
yield* Effect.sync(() => {
|
||||||
Instance.restore(instance, () => {
|
bridge.fork(
|
||||||
void AppRuntime.runPromise(
|
promptSvc
|
||||||
SessionPrompt.Service.use((svc) =>
|
.prompt({ ...ctx.payload, sessionID: ctx.params.sessionID } as unknown as SessionPrompt.PromptInput)
|
||||||
svc.prompt({ ...ctx.payload, sessionID: ctx.params.sessionID } as unknown as SessionPrompt.PromptInput),
|
.pipe(
|
||||||
).pipe(Effect.provide(SessionPrompt.defaultLayer)),
|
Effect.catchCause((error) =>
|
||||||
).catch((error) => {
|
Effect.sync(() => {
|
||||||
log.error("prompt_async failed", { sessionID: ctx.params.sessionID, error })
|
log.error("prompt_async failed", { sessionID: ctx.params.sessionID, error })
|
||||||
void Bus.publish(Session.Event.Error, {
|
void Bus.publish(Session.Event.Error, {
|
||||||
sessionID: ctx.params.sessionID,
|
sessionID: ctx.params.sessionID,
|
||||||
error: new NamedError.Unknown({
|
error: new NamedError.Unknown({ message: String(error) }).toObject(),
|
||||||
message: error instanceof Error ? error.message : String(error),
|
})
|
||||||
}).toObject(),
|
}),
|
||||||
})
|
),
|
||||||
})
|
),
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
return HttpApiSchema.NoContent.make()
|
return HttpApiSchema.NoContent.make()
|
||||||
})
|
})
|
||||||
|
|
@ -402,111 +309,47 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
payload: typeof CommandPayload.Type
|
payload: typeof CommandPayload.Type
|
||||||
}) {
|
}) {
|
||||||
const instance = yield* InstanceState.context
|
return yield* promptSvc.command({ ...ctx.payload, sessionID: ctx.params.sessionID } as SessionPrompt.CommandInput)
|
||||||
return yield* Effect.promise(() =>
|
|
||||||
Instance.restore(instance, () =>
|
|
||||||
AppRuntime.runPromise(
|
|
||||||
SessionPrompt.Service.use((svc) =>
|
|
||||||
svc.command({ ...ctx.payload, sessionID: ctx.params.sessionID } as SessionPrompt.CommandInput),
|
|
||||||
).pipe(Effect.provide(SessionPrompt.defaultLayer)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const shell = Effect.fn("SessionHttpApi.shell")(function* (ctx: {
|
const shell = Effect.fn("SessionHttpApi.shell")(function* (ctx: {
|
||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
payload: typeof ShellPayload.Type
|
payload: typeof ShellPayload.Type
|
||||||
}) {
|
}) {
|
||||||
const instance = yield* InstanceState.context
|
return yield* promptSvc.shell({ ...ctx.payload, sessionID: ctx.params.sessionID } as SessionPrompt.ShellInput)
|
||||||
return yield* Effect.promise(() =>
|
|
||||||
Instance.restore(instance, () =>
|
|
||||||
AppRuntime.runPromise(
|
|
||||||
SessionPrompt.Service.use((svc) =>
|
|
||||||
svc.shell({ ...ctx.payload, sessionID: ctx.params.sessionID } as SessionPrompt.ShellInput),
|
|
||||||
).pipe(Effect.provide(SessionPrompt.defaultLayer)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const revert = Effect.fn("SessionHttpApi.revert")(function* (ctx: {
|
const revert = Effect.fn("SessionHttpApi.revert")(function* (ctx: {
|
||||||
params: { sessionID: SessionID }
|
params: { sessionID: SessionID }
|
||||||
payload: typeof RevertPayload.Type
|
payload: typeof RevertPayload.Type
|
||||||
}) {
|
}) {
|
||||||
const instance = yield* InstanceState.context
|
return yield* revertSvc.revert({ sessionID: ctx.params.sessionID, ...ctx.payload })
|
||||||
log.info("revert", ctx.payload)
|
|
||||||
return yield* Effect.promise(() =>
|
|
||||||
Instance.restore(instance, () =>
|
|
||||||
AppRuntime.runPromise(
|
|
||||||
SessionRevert.Service.use((svc) => svc.revert({ sessionID: ctx.params.sessionID, ...ctx.payload })).pipe(
|
|
||||||
Effect.provide(SessionRevert.defaultLayer),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const unrevert = Effect.fn("SessionHttpApi.unrevert")(function* (ctx: { params: { sessionID: SessionID } }) {
|
const unrevert = Effect.fn("SessionHttpApi.unrevert")(function* (ctx: { params: { sessionID: SessionID } }) {
|
||||||
const instance = yield* InstanceState.context
|
return yield* revertSvc.unrevert({ sessionID: ctx.params.sessionID })
|
||||||
return yield* Effect.promise(() =>
|
|
||||||
Instance.restore(instance, () =>
|
|
||||||
AppRuntime.runPromise(
|
|
||||||
SessionRevert.Service.use((svc) => svc.unrevert({ sessionID: ctx.params.sessionID })).pipe(
|
|
||||||
Effect.provide(SessionRevert.defaultLayer),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const permissionRespond = Effect.fn("SessionHttpApi.permissionRespond")(function* (ctx: {
|
const permissionRespond = Effect.fn("SessionHttpApi.permissionRespond")(function* (ctx: {
|
||||||
params: { permissionID: PermissionID }
|
params: { permissionID: PermissionID }
|
||||||
payload: typeof PermissionResponsePayload.Type
|
payload: typeof PermissionResponsePayload.Type
|
||||||
}) {
|
}) {
|
||||||
const instance = yield* InstanceState.context
|
yield* permissionSvc.reply({ requestID: ctx.params.permissionID, reply: ctx.payload.response })
|
||||||
yield* Effect.promise(() =>
|
|
||||||
Instance.restore(instance, () =>
|
|
||||||
AppRuntime.runPromise(
|
|
||||||
Permission.Service.use((svc) =>
|
|
||||||
svc.reply({ requestID: ctx.params.permissionID, reply: ctx.payload.response }),
|
|
||||||
).pipe(Effect.provide(Permission.defaultLayer)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteMessage = Effect.fn("SessionHttpApi.deleteMessage")(function* (ctx: {
|
const deleteMessage = Effect.fn("SessionHttpApi.deleteMessage")(function* (ctx: {
|
||||||
params: { sessionID: SessionID; messageID: MessageID }
|
params: { sessionID: SessionID; messageID: MessageID }
|
||||||
}) {
|
}) {
|
||||||
const instance = yield* InstanceState.context
|
yield* runState.assertNotBusy(ctx.params.sessionID)
|
||||||
yield* Effect.promise(() =>
|
yield* session.removeMessage(ctx.params)
|
||||||
Instance.restore(instance, () =>
|
|
||||||
AppRuntime.runPromise(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const state = yield* SessionRunState.Service
|
|
||||||
const session = yield* Session.Service
|
|
||||||
yield* state.assertNotBusy(ctx.params.sessionID)
|
|
||||||
yield* session.removeMessage(ctx.params)
|
|
||||||
}).pipe(Effect.provide(SessionRunState.defaultLayer), Effect.provide(Session.defaultLayer)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
const deletePart = Effect.fn("SessionHttpApi.deletePart")(function* (ctx: {
|
const deletePart = Effect.fn("SessionHttpApi.deletePart")(function* (ctx: {
|
||||||
params: { sessionID: SessionID; messageID: MessageID; partID: PartID }
|
params: { sessionID: SessionID; messageID: MessageID; partID: PartID }
|
||||||
}) {
|
}) {
|
||||||
const instance = yield* InstanceState.context
|
yield* session.removePart(ctx.params)
|
||||||
yield* Effect.promise(() =>
|
|
||||||
Instance.restore(instance, () =>
|
|
||||||
AppRuntime.runPromise(
|
|
||||||
Session.Service.use((svc) => svc.removePart(ctx.params)).pipe(Effect.provide(Session.defaultLayer)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -524,14 +367,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
|
||||||
`Part mismatch: body.id='${payload.id}' vs partID='${ctx.params.partID}', body.messageID='${payload.messageID}' vs messageID='${ctx.params.messageID}', body.sessionID='${payload.sessionID}' vs sessionID='${ctx.params.sessionID}'`,
|
`Part mismatch: body.id='${payload.id}' vs partID='${ctx.params.partID}', body.messageID='${payload.messageID}' vs messageID='${ctx.params.messageID}', body.sessionID='${payload.sessionID}' vs sessionID='${ctx.params.sessionID}'`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const instance = yield* InstanceState.context
|
return yield* session.updatePart(payload)
|
||||||
return yield* Effect.promise(() =>
|
|
||||||
Instance.restore(instance, () =>
|
|
||||||
AppRuntime.runPromise(
|
|
||||||
Session.Service.use((svc) => svc.updatePart(payload)).pipe(Effect.provide(Session.defaultLayer)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return handlers
|
return handlers
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,14 @@ import { Provider } from "@/provider/provider"
|
||||||
import { Pty } from "@/pty"
|
import { Pty } from "@/pty"
|
||||||
import { Question } from "@/question"
|
import { Question } from "@/question"
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
|
import { SessionCompaction } from "@/session/compaction"
|
||||||
|
import { SessionPrompt } from "@/session/prompt"
|
||||||
|
import { SessionRevert } from "@/session/revert"
|
||||||
import { SessionRunState } from "@/session/run-state"
|
import { SessionRunState } from "@/session/run-state"
|
||||||
import { SessionStatus } from "@/session/status"
|
import { SessionStatus } from "@/session/status"
|
||||||
import { SessionSummary } from "@/session/summary"
|
import { SessionSummary } from "@/session/summary"
|
||||||
import { Todo } from "@/session/todo"
|
import { Todo } from "@/session/todo"
|
||||||
|
import { SessionShare } from "@/share/session"
|
||||||
import { Skill } from "@/skill"
|
import { Skill } from "@/skill"
|
||||||
import { ToolRegistry } from "@/tool/registry"
|
import { ToolRegistry } from "@/tool/registry"
|
||||||
import { lazy } from "@/util/lazy"
|
import { lazy } from "@/util/lazy"
|
||||||
|
|
@ -134,6 +138,10 @@ export const routes = Layer.mergeAll(rootApiRoutes, instanceRoutes).pipe(
|
||||||
Question.defaultLayer,
|
Question.defaultLayer,
|
||||||
Ripgrep.defaultLayer,
|
Ripgrep.defaultLayer,
|
||||||
Session.defaultLayer,
|
Session.defaultLayer,
|
||||||
|
SessionCompaction.defaultLayer,
|
||||||
|
SessionPrompt.defaultLayer,
|
||||||
|
SessionRevert.defaultLayer,
|
||||||
|
SessionShare.defaultLayer,
|
||||||
SessionRunState.defaultLayer,
|
SessionRunState.defaultLayer,
|
||||||
SessionStatus.defaultLayer,
|
SessionStatus.defaultLayer,
|
||||||
SessionSummary.defaultLayer,
|
SessionSummary.defaultLayer,
|
||||||
|
|
|
||||||
|
|
@ -589,7 +589,7 @@ export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service> =
|
||||||
path: sessionPath(ctx.worktree, ctx.directory),
|
path: sessionPath(ctx.worktree, ctx.directory),
|
||||||
title: input?.title,
|
title: input?.title,
|
||||||
permission: input?.permission,
|
permission: input?.permission,
|
||||||
workspaceID: workspace,
|
workspaceID: input?.workspaceID ?? workspace,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,13 @@ import { mkdir } from "node:fs/promises"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||||
|
import { registerAdaptor } from "../../src/control-plane/adaptors"
|
||||||
|
import type { WorkspaceAdaptor } from "../../src/control-plane/types"
|
||||||
|
import { Workspace } from "../../src/control-plane/workspace"
|
||||||
import { PermissionID } from "../../src/permission/schema"
|
import { PermissionID } from "../../src/permission/schema"
|
||||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
|
import { Project } from "../../src/project/project"
|
||||||
import { Server } from "../../src/server/server"
|
import { Server } from "../../src/server/server"
|
||||||
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
|
import { SessionPaths } from "../../src/server/routes/instance/httpapi/groups/session"
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
|
|
@ -22,6 +26,7 @@ import { it } from "../lib/effect"
|
||||||
void Log.init({ print: false })
|
void Log.init({ print: false })
|
||||||
|
|
||||||
const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
|
||||||
|
const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
||||||
|
|
||||||
function app(experimental = true) {
|
function app(experimental = true) {
|
||||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
|
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = experimental
|
||||||
|
|
@ -77,6 +82,28 @@ function createTextMessage(directory: string, sessionID: SessionID, text: string
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const localAdaptor = (directory: string): WorkspaceAdaptor => ({
|
||||||
|
name: "Local Test",
|
||||||
|
description: "Create a local test workspace",
|
||||||
|
configure: (info) => ({ ...info, name: "local-test", directory }),
|
||||||
|
create: async () => {
|
||||||
|
await mkdir(directory, { recursive: true })
|
||||||
|
},
|
||||||
|
async remove() {},
|
||||||
|
target: () => ({ type: "local" as const, directory }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: string; directory: string }) =>
|
||||||
|
Effect.promise(async () => {
|
||||||
|
registerAdaptor(input.projectID, input.type, localAdaptor(input.directory))
|
||||||
|
return Workspace.create({
|
||||||
|
type: input.type,
|
||||||
|
branch: null,
|
||||||
|
extra: null,
|
||||||
|
projectID: input.projectID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
function request(path: string, init?: RequestInit) {
|
function request(path: string, init?: RequestInit) {
|
||||||
return Effect.promise(async () => app().request(path, init))
|
return Effect.promise(async () => app().request(path, init))
|
||||||
}
|
}
|
||||||
|
|
@ -108,6 +135,7 @@ function withTmp<A, E, R>(
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
|
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original
|
||||||
|
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = originalWorkspaces
|
||||||
await Instance.disposeAll()
|
await Instance.disposeAll()
|
||||||
await resetDatabase()
|
await resetDatabase()
|
||||||
})
|
})
|
||||||
|
|
@ -226,6 +254,40 @@ describe("session HttpApi", () => {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.live(
|
||||||
|
"persists selected workspace id when creating a session",
|
||||||
|
withTmp({ git: true, config: { formatter: false, lsp: false, share: "disabled" } }, (tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
|
||||||
|
const project = yield* Project.use.fromDirectory(tmp.path).pipe(Effect.provide(Project.defaultLayer))
|
||||||
|
const workspace = yield* createLocalWorkspace({
|
||||||
|
projectID: project.project.id,
|
||||||
|
type: "session-create-workspace",
|
||||||
|
directory: path.join(tmp.path, ".workspace-local"),
|
||||||
|
})
|
||||||
|
|
||||||
|
const created = yield* requestJson<Session.Info>(`${SessionPaths.create}?workspace=${workspace.id}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "x-opencode-directory": tmp.path, "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ title: "workspace session" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(created).toMatchObject({ id: created.id, workspaceID: workspace.id })
|
||||||
|
expect(
|
||||||
|
yield* Effect.sync(() =>
|
||||||
|
Database.use((db) =>
|
||||||
|
db
|
||||||
|
.select({ workspaceID: SessionTable.workspace_id })
|
||||||
|
.from(SessionTable)
|
||||||
|
.where(eq(SessionTable.id, created.id))
|
||||||
|
.get(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).toEqual({ workspaceID: workspace.id })
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
it.live(
|
it.live(
|
||||||
"matches legacy archived timestamp validation",
|
"matches legacy archived timestamp validation",
|
||||||
withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
|
withTmp({ git: true, config: { formatter: false, lsp: false } }, (tmp) =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue