session.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. import { Permission } from "@/permission"
  2. import { PermissionID } from "@/permission/schema"
  3. import { ModelID, ProviderID } from "@/provider/schema"
  4. import { Session } from "@/session/session"
  5. import { MessageV2 } from "@/session/message-v2"
  6. import { SessionPrompt } from "@/session/prompt"
  7. import { SessionRevert } from "@/session/revert"
  8. import { SessionStatus } from "@/session/status"
  9. import { SessionSummary } from "@/session/summary"
  10. import { Todo } from "@/session/todo"
  11. import { MessageID, PartID, SessionID } from "@/session/schema"
  12. import { Snapshot } from "@/snapshot"
  13. import { Schema, Struct } from "effect"
  14. import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
  15. import { Authorization } from "../middleware/authorization"
  16. import { InstanceContextMiddleware } from "../middleware/instance-context"
  17. import {
  18. WorkspaceRoutingMiddleware,
  19. WorkspaceRoutingQuery,
  20. WorkspaceRoutingQueryFields,
  21. } from "../middleware/workspace-routing"
  22. import { ApiNotFoundError, PermissionNotFoundError, SessionBusyError } from "../errors"
  23. import { described } from "./metadata"
  24. import { QueryBoolean } from "./query"
  25. const root = "/session"
  26. export const ListQuery = Schema.Struct({
  27. ...WorkspaceRoutingQueryFields,
  28. scope: Schema.optional(Schema.Literals(["project"])),
  29. path: Schema.optional(Schema.String),
  30. roots: Schema.optional(QueryBoolean),
  31. start: Schema.optional(Schema.NumberFromString),
  32. search: Schema.optional(Schema.String),
  33. limit: Schema.optional(Schema.NumberFromString),
  34. })
  35. export const DiffQuery = Schema.Struct({
  36. ...WorkspaceRoutingQueryFields,
  37. ...Struct.omit(SessionSummary.DiffInput.fields, ["sessionID"]),
  38. })
  39. export const MessagesQuery = Schema.Struct({
  40. ...WorkspaceRoutingQueryFields,
  41. limit: Schema.optional(Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0))),
  42. before: Schema.optional(Schema.String),
  43. })
  44. export const StatusMap = Schema.Record(Schema.String, SessionStatus.Info)
  45. export const UpdatePayload = Schema.Struct({
  46. title: Schema.optional(Schema.String),
  47. permission: Schema.optional(Permission.Ruleset),
  48. time: Schema.optional(
  49. Schema.Struct({
  50. archived: Schema.optional(Session.ArchivedTimestamp),
  51. }),
  52. ),
  53. })
  54. export const ForkPayload = Schema.Struct(Struct.omit(Session.ForkInput.fields, ["sessionID"]))
  55. export const InitPayload = Schema.Struct({
  56. modelID: ModelID,
  57. providerID: ProviderID,
  58. messageID: MessageID,
  59. })
  60. export const SummarizePayload = Schema.Struct({
  61. providerID: ProviderID,
  62. modelID: ModelID,
  63. auto: Schema.optional(Schema.Boolean),
  64. })
  65. export const PromptPayload = Schema.Struct(Struct.omit(SessionPrompt.PromptInput.fields, ["sessionID"]))
  66. export const CommandPayload = Schema.Struct(Struct.omit(SessionPrompt.CommandInput.fields, ["sessionID"]))
  67. export const ShellPayload = Schema.Struct(Struct.omit(SessionPrompt.ShellInput.fields, ["sessionID"]))
  68. export const RevertPayload = Schema.Struct(Struct.omit(SessionRevert.RevertInput.fields, ["sessionID"]))
  69. export const PermissionResponsePayload = Schema.Struct({
  70. response: Permission.Reply,
  71. })
  72. export const SessionPaths = {
  73. list: root,
  74. status: `${root}/status`,
  75. get: `${root}/:sessionID`,
  76. children: `${root}/:sessionID/children`,
  77. todo: `${root}/:sessionID/todo`,
  78. diff: `${root}/:sessionID/diff`,
  79. messages: `${root}/:sessionID/message`,
  80. message: `${root}/:sessionID/message/:messageID`,
  81. create: root,
  82. remove: `${root}/:sessionID`,
  83. update: `${root}/:sessionID`,
  84. fork: `${root}/:sessionID/fork`,
  85. abort: `${root}/:sessionID/abort`,
  86. share: `${root}/:sessionID/share`,
  87. init: `${root}/:sessionID/init`,
  88. summarize: `${root}/:sessionID/summarize`,
  89. prompt: `${root}/:sessionID/message`,
  90. promptAsync: `${root}/:sessionID/prompt_async`,
  91. command: `${root}/:sessionID/command`,
  92. shell: `${root}/:sessionID/shell`,
  93. revert: `${root}/:sessionID/revert`,
  94. unrevert: `${root}/:sessionID/unrevert`,
  95. permissions: `${root}/:sessionID/permissions/:permissionID`,
  96. deleteMessage: `${root}/:sessionID/message/:messageID`,
  97. deletePart: `${root}/:sessionID/message/:messageID/part/:partID`,
  98. updatePart: `${root}/:sessionID/message/:messageID/part/:partID`,
  99. } as const
  100. export const SessionApi = HttpApi.make("session")
  101. .add(
  102. HttpApiGroup.make("session")
  103. .add(
  104. HttpApiEndpoint.get("list", SessionPaths.list, {
  105. query: ListQuery,
  106. success: described(Schema.Array(Session.Info), "List of sessions"),
  107. }).annotateMerge(
  108. OpenApi.annotations({
  109. identifier: "session.list",
  110. summary: "List sessions",
  111. description: "Get a list of all OpenCode sessions, sorted by most recently updated.",
  112. }),
  113. ),
  114. HttpApiEndpoint.get("status", SessionPaths.status, {
  115. query: WorkspaceRoutingQuery,
  116. success: described(StatusMap, "Get session status"),
  117. error: HttpApiError.BadRequest,
  118. }).annotateMerge(
  119. OpenApi.annotations({
  120. identifier: "session.status",
  121. summary: "Get session status",
  122. description: "Retrieve the current status of all sessions, including active, idle, and completed states.",
  123. }),
  124. ),
  125. HttpApiEndpoint.get("get", SessionPaths.get, {
  126. params: { sessionID: SessionID },
  127. query: WorkspaceRoutingQuery,
  128. success: described(Session.Info, "Get session"),
  129. error: [HttpApiError.BadRequest, ApiNotFoundError],
  130. }).annotateMerge(
  131. OpenApi.annotations({
  132. identifier: "session.get",
  133. summary: "Get session",
  134. description: "Retrieve detailed information about a specific OpenCode session.",
  135. }),
  136. ),
  137. HttpApiEndpoint.get("children", SessionPaths.children, {
  138. params: { sessionID: SessionID },
  139. query: WorkspaceRoutingQuery,
  140. success: described(Schema.Array(Session.Info), "List of children"),
  141. error: [HttpApiError.BadRequest, ApiNotFoundError],
  142. }).annotateMerge(
  143. OpenApi.annotations({
  144. identifier: "session.children",
  145. summary: "Get session children",
  146. description: "Retrieve all child sessions that were forked from the specified parent session.",
  147. }),
  148. ),
  149. HttpApiEndpoint.get("todo", SessionPaths.todo, {
  150. params: { sessionID: SessionID },
  151. query: WorkspaceRoutingQuery,
  152. success: described(Schema.Array(Todo.Info), "Todo list"),
  153. error: [HttpApiError.BadRequest, ApiNotFoundError],
  154. }).annotateMerge(
  155. OpenApi.annotations({
  156. identifier: "session.todo",
  157. summary: "Get session todos",
  158. description: "Retrieve the todo list associated with a specific session, showing tasks and action items.",
  159. }),
  160. ),
  161. HttpApiEndpoint.get("diff", SessionPaths.diff, {
  162. params: { sessionID: SessionID },
  163. query: DiffQuery,
  164. success: described(Schema.Array(Snapshot.FileDiff), "Successfully retrieved diff"),
  165. }).annotateMerge(
  166. OpenApi.annotations({
  167. identifier: "session.diff",
  168. summary: "Get message diff",
  169. description: "Get the file changes (diff) that resulted from a specific user message in the session.",
  170. }),
  171. ),
  172. HttpApiEndpoint.get("messages", SessionPaths.messages, {
  173. params: { sessionID: SessionID },
  174. query: MessagesQuery,
  175. success: described(Schema.Array(MessageV2.WithParts), "List of messages"),
  176. error: [HttpApiError.BadRequest, ApiNotFoundError],
  177. }).annotateMerge(
  178. OpenApi.annotations({
  179. identifier: "session.messages",
  180. summary: "Get session messages",
  181. description: "Retrieve all messages in a session, including user prompts and AI responses.",
  182. }),
  183. ),
  184. HttpApiEndpoint.get("message", SessionPaths.message, {
  185. params: { sessionID: SessionID, messageID: MessageID },
  186. query: WorkspaceRoutingQuery,
  187. success: described(MessageV2.WithParts, "Message"),
  188. error: [HttpApiError.BadRequest, ApiNotFoundError],
  189. }).annotateMerge(
  190. OpenApi.annotations({
  191. identifier: "session.message",
  192. summary: "Get message",
  193. description: "Retrieve a specific message from a session by its message ID.",
  194. }),
  195. ),
  196. HttpApiEndpoint.post("create", SessionPaths.create, {
  197. query: WorkspaceRoutingQuery,
  198. payload: [HttpApiSchema.NoContent, Session.CreateInput],
  199. success: described(Session.Info, "Successfully created session"),
  200. error: HttpApiError.BadRequest,
  201. }).annotateMerge(
  202. OpenApi.annotations({
  203. identifier: "session.create",
  204. summary: "Create session",
  205. description: "Create a new OpenCode session for interacting with AI assistants and managing conversations.",
  206. }),
  207. ),
  208. HttpApiEndpoint.delete("remove", SessionPaths.remove, {
  209. params: { sessionID: SessionID },
  210. query: WorkspaceRoutingQuery,
  211. success: described(Schema.Boolean, "Successfully deleted session"),
  212. error: [HttpApiError.BadRequest, ApiNotFoundError],
  213. }).annotateMerge(
  214. OpenApi.annotations({
  215. identifier: "session.delete",
  216. summary: "Delete session",
  217. description: "Delete a session and permanently remove all associated data, including messages and history.",
  218. }),
  219. ),
  220. HttpApiEndpoint.patch("update", SessionPaths.update, {
  221. params: { sessionID: SessionID },
  222. query: WorkspaceRoutingQuery,
  223. payload: UpdatePayload,
  224. success: described(Session.Info, "Successfully updated session"),
  225. error: [HttpApiError.BadRequest, ApiNotFoundError],
  226. }).annotateMerge(
  227. OpenApi.annotations({
  228. identifier: "session.update",
  229. summary: "Update session",
  230. description: "Update properties of an existing session, such as title or other metadata.",
  231. }),
  232. ),
  233. HttpApiEndpoint.post("fork", SessionPaths.fork, {
  234. params: { sessionID: SessionID },
  235. query: WorkspaceRoutingQuery,
  236. payload: Schema.optional(ForkPayload),
  237. success: described(Session.Info, "200"),
  238. error: [HttpApiError.BadRequest, ApiNotFoundError],
  239. }).annotateMerge(
  240. OpenApi.annotations({
  241. identifier: "session.fork",
  242. summary: "Fork session",
  243. description: "Create a new session by forking an existing session at a specific message point.",
  244. }),
  245. ),
  246. HttpApiEndpoint.post("abort", SessionPaths.abort, {
  247. params: { sessionID: SessionID },
  248. query: WorkspaceRoutingQuery,
  249. success: described(Schema.Boolean, "Aborted session"),
  250. error: HttpApiError.BadRequest,
  251. }).annotateMerge(
  252. OpenApi.annotations({
  253. identifier: "session.abort",
  254. summary: "Abort session",
  255. description: "Abort an active session and stop any ongoing AI processing or command execution.",
  256. }),
  257. ),
  258. HttpApiEndpoint.post("init", SessionPaths.init, {
  259. params: { sessionID: SessionID },
  260. query: WorkspaceRoutingQuery,
  261. payload: InitPayload,
  262. success: described(Schema.Boolean, "200"),
  263. error: [HttpApiError.BadRequest, ApiNotFoundError],
  264. }).annotateMerge(
  265. OpenApi.annotations({
  266. identifier: "session.init",
  267. summary: "Initialize session",
  268. description:
  269. "Analyze the current application and create an AGENTS.md file with project-specific agent configurations.",
  270. }),
  271. ),
  272. HttpApiEndpoint.post("share", SessionPaths.share, {
  273. params: { sessionID: SessionID },
  274. query: WorkspaceRoutingQuery,
  275. success: described(Session.Info, "Successfully shared session"),
  276. error: [HttpApiError.InternalServerError, ApiNotFoundError],
  277. }).annotateMerge(
  278. OpenApi.annotations({
  279. identifier: "session.share",
  280. summary: "Share session",
  281. description: "Create a shareable link for a session, allowing others to view the conversation.",
  282. }),
  283. ),
  284. HttpApiEndpoint.delete("unshare", SessionPaths.share, {
  285. params: { sessionID: SessionID },
  286. query: WorkspaceRoutingQuery,
  287. success: described(Session.Info, "Successfully unshared session"),
  288. error: [HttpApiError.InternalServerError, ApiNotFoundError],
  289. }).annotateMerge(
  290. OpenApi.annotations({
  291. identifier: "session.unshare",
  292. summary: "Unshare session",
  293. description: "Remove the shareable link for a session, making it private again.",
  294. }),
  295. ),
  296. HttpApiEndpoint.post("summarize", SessionPaths.summarize, {
  297. params: { sessionID: SessionID },
  298. query: WorkspaceRoutingQuery,
  299. payload: SummarizePayload,
  300. success: described(Schema.Boolean, "Summarized session"),
  301. error: [HttpApiError.BadRequest, ApiNotFoundError],
  302. }).annotateMerge(
  303. OpenApi.annotations({
  304. identifier: "session.summarize",
  305. summary: "Summarize session",
  306. description: "Generate a concise summary of the session using AI compaction to preserve key information.",
  307. }),
  308. ),
  309. HttpApiEndpoint.post("prompt", SessionPaths.prompt, {
  310. params: { sessionID: SessionID },
  311. query: WorkspaceRoutingQuery,
  312. payload: PromptPayload,
  313. success: described(MessageV2.WithParts, "Created message"),
  314. error: [HttpApiError.BadRequest, ApiNotFoundError],
  315. }).annotateMerge(
  316. OpenApi.annotations({
  317. identifier: "session.prompt",
  318. summary: "Send message",
  319. description: "Create and send a new message to a session, streaming the AI response.",
  320. }),
  321. ),
  322. HttpApiEndpoint.post("promptAsync", SessionPaths.promptAsync, {
  323. params: { sessionID: SessionID },
  324. query: WorkspaceRoutingQuery,
  325. payload: PromptPayload,
  326. success: described(HttpApiSchema.NoContent, "Prompt accepted"),
  327. error: [HttpApiError.BadRequest, ApiNotFoundError],
  328. }).annotateMerge(
  329. OpenApi.annotations({
  330. identifier: "session.prompt_async",
  331. summary: "Send async message",
  332. description:
  333. "Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.",
  334. }),
  335. ),
  336. HttpApiEndpoint.post("command", SessionPaths.command, {
  337. params: { sessionID: SessionID },
  338. query: WorkspaceRoutingQuery,
  339. payload: CommandPayload,
  340. success: described(MessageV2.WithParts, "Created message"),
  341. error: [HttpApiError.BadRequest, ApiNotFoundError],
  342. }).annotateMerge(
  343. OpenApi.annotations({
  344. identifier: "session.command",
  345. summary: "Send command",
  346. description: "Send a new command to a session for execution by the AI assistant.",
  347. }),
  348. ),
  349. HttpApiEndpoint.post("shell", SessionPaths.shell, {
  350. params: { sessionID: SessionID },
  351. query: WorkspaceRoutingQuery,
  352. payload: ShellPayload,
  353. success: described(MessageV2.WithParts, "Created message"),
  354. error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
  355. }).annotateMerge(
  356. OpenApi.annotations({
  357. identifier: "session.shell",
  358. summary: "Run shell command",
  359. description: "Execute a shell command within the session context and return the AI's response.",
  360. }),
  361. ),
  362. HttpApiEndpoint.post("revert", SessionPaths.revert, {
  363. params: { sessionID: SessionID },
  364. query: WorkspaceRoutingQuery,
  365. payload: RevertPayload,
  366. success: described(Session.Info, "Updated session"),
  367. error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
  368. }).annotateMerge(
  369. OpenApi.annotations({
  370. identifier: "session.revert",
  371. summary: "Revert message",
  372. description:
  373. "Revert a specific message in a session, undoing its effects and restoring the previous state.",
  374. }),
  375. ),
  376. HttpApiEndpoint.post("unrevert", SessionPaths.unrevert, {
  377. params: { sessionID: SessionID },
  378. query: WorkspaceRoutingQuery,
  379. success: described(Session.Info, "Updated session"),
  380. error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
  381. }).annotateMerge(
  382. OpenApi.annotations({
  383. identifier: "session.unrevert",
  384. summary: "Restore reverted messages",
  385. description: "Restore all previously reverted messages in a session.",
  386. }),
  387. ),
  388. HttpApiEndpoint.post("permissionRespond", SessionPaths.permissions, {
  389. params: { sessionID: SessionID, permissionID: PermissionID },
  390. query: WorkspaceRoutingQuery,
  391. payload: PermissionResponsePayload,
  392. success: described(Schema.Boolean, "Permission processed successfully"),
  393. error: [HttpApiError.BadRequest, ApiNotFoundError, PermissionNotFoundError],
  394. }).annotateMerge(
  395. OpenApi.annotations({
  396. identifier: "permission.respond",
  397. summary: "Respond to permission",
  398. description: "Approve or deny a permission request from the AI assistant.",
  399. deprecated: true,
  400. }),
  401. ),
  402. HttpApiEndpoint.delete("deleteMessage", SessionPaths.deleteMessage, {
  403. params: { sessionID: SessionID, messageID: MessageID },
  404. query: WorkspaceRoutingQuery,
  405. success: described(Schema.Boolean, "Successfully deleted message"),
  406. error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
  407. }).annotateMerge(
  408. OpenApi.annotations({
  409. identifier: "session.deleteMessage",
  410. summary: "Delete message",
  411. description:
  412. "Permanently delete a specific message and all of its parts from a session without reverting file changes.",
  413. }),
  414. ),
  415. HttpApiEndpoint.delete("deletePart", SessionPaths.deletePart, {
  416. params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
  417. query: WorkspaceRoutingQuery,
  418. success: described(Schema.Boolean, "Successfully deleted part"),
  419. error: [HttpApiError.BadRequest, ApiNotFoundError],
  420. }).annotateMerge(
  421. OpenApi.annotations({
  422. identifier: "part.delete",
  423. description: "Delete a part from a message.",
  424. }),
  425. ),
  426. HttpApiEndpoint.patch("updatePart", SessionPaths.updatePart, {
  427. params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
  428. query: WorkspaceRoutingQuery,
  429. payload: MessageV2.Part,
  430. success: described(MessageV2.Part, "Successfully updated part"),
  431. error: [HttpApiError.BadRequest, ApiNotFoundError],
  432. }).annotateMerge(
  433. OpenApi.annotations({
  434. identifier: "part.update",
  435. description: "Update a part in a message.",
  436. }),
  437. ),
  438. )
  439. .annotateMerge(
  440. OpenApi.annotations({
  441. title: "session",
  442. description: "Experimental HttpApi session routes.",
  443. }),
  444. )
  445. .middleware(InstanceContextMiddleware)
  446. .middleware(WorkspaceRoutingMiddleware)
  447. .middleware(Authorization),
  448. )
  449. .annotateMerge(
  450. OpenApi.annotations({
  451. title: "opencode experimental HttpApi",
  452. version: "0.0.1",
  453. description: "Experimental HttpApi surface for selected instance routes.",
  454. }),
  455. )