From fba752a5016a93ad7ea54890cf444de02a89a0f8 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Wed, 15 Apr 2026 04:45:25 +0530 Subject: feat(server): extract question httpapi contract --- packages/server/src/definition/api.ts | 16 +++-- packages/server/src/definition/index.ts | 1 + packages/server/src/definition/question.ts | 94 ++++++++++++++++++++++++++++++ packages/server/src/index.ts | 1 + packages/server/src/openapi.ts | 13 +---- packages/server/src/types.ts | 17 ++---- 6 files changed, 113 insertions(+), 29 deletions(-) create mode 100644 packages/server/src/definition/question.ts (limited to 'packages/server/src') diff --git a/packages/server/src/definition/api.ts b/packages/server/src/definition/api.ts index 6eda4090e..e2f70196d 100644 --- a/packages/server/src/definition/api.ts +++ b/packages/server/src/definition/api.ts @@ -1,6 +1,12 @@ -import type { ServerApi } from "../types.js" +import { HttpApi, OpenApi } from "effect/unstable/httpapi" +import { questionApi } from "./question.js" -export const api: ServerApi = { - name: "opencode", - groups: [], -} +export const api = HttpApi.make("opencode") + .addHttpApi(questionApi) + .annotateMerge( + OpenApi.annotations({ + title: "opencode experimental HttpApi", + version: "0.0.1", + description: "Experimental HttpApi surface for selected instance routes.", + }), + ) diff --git a/packages/server/src/definition/index.ts b/packages/server/src/definition/index.ts index 39cab2446..e9a52dc93 100644 --- a/packages/server/src/definition/index.ts +++ b/packages/server/src/definition/index.ts @@ -1 +1,2 @@ export { api } from "./api.js" +export { questionApi, QuestionReply, QuestionRequest } from "./question.js" diff --git a/packages/server/src/definition/question.ts b/packages/server/src/definition/question.ts new file mode 100644 index 000000000..0d161e013 --- /dev/null +++ b/packages/server/src/definition/question.ts @@ -0,0 +1,94 @@ +import { Schema } from "effect" +import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" + +const root = "/experimental/httpapi/question" + +// Temporary transport-local schemas until canonical question schemas move into packages/core. +export const QuestionID = Schema.String.annotate({ identifier: "QuestionID" }) +export const SessionID = Schema.String.annotate({ identifier: "SessionID" }) +export const MessageID = Schema.String.annotate({ identifier: "MessageID" }) + +export class QuestionOption extends Schema.Class("QuestionOption")({ + label: Schema.String.annotate({ + description: "Display text (1-5 words, concise)", + }), + description: Schema.String.annotate({ + description: "Explanation of choice", + }), +}) {} + +const base = { + question: Schema.String.annotate({ + description: "Complete question", + }), + header: Schema.String.annotate({ + description: "Very short label (max 30 chars)", + }), + options: Schema.Array(QuestionOption).annotate({ + description: "Available choices", + }), + multiple: Schema.optional(Schema.Boolean).annotate({ + description: "Allow selecting multiple choices", + }), +} + +export class QuestionInfo extends Schema.Class("QuestionInfo")({ + ...base, + custom: Schema.optional(Schema.Boolean).annotate({ + description: "Allow typing a custom answer (default: true)", + }), +}) {} + +export class QuestionTool extends Schema.Class("QuestionTool")({ + messageID: MessageID, + callID: Schema.String, +}) {} + +export class QuestionRequest extends Schema.Class("QuestionRequest")({ + id: QuestionID, + sessionID: SessionID, + questions: Schema.Array(QuestionInfo).annotate({ + description: "Questions to ask", + }), + tool: Schema.optional(QuestionTool), +}) {} + +export const QuestionAnswer = Schema.Array(Schema.String).annotate({ identifier: "QuestionAnswer" }) + +export class QuestionReply extends Schema.Class("QuestionReply")({ + answers: Schema.Array(QuestionAnswer).annotate({ + description: "User answers in order of questions (each answer is an array of selected labels)", + }), +}) {} + +export const questionApi = HttpApi.make("question").add( + HttpApiGroup.make("question") + .add( + HttpApiEndpoint.get("list", root, { + success: Schema.Array(QuestionRequest), + }).annotateMerge( + OpenApi.annotations({ + identifier: "question.list", + summary: "List pending questions", + description: "Get all pending question requests across all sessions.", + }), + ), + HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, { + params: { requestID: QuestionID }, + payload: QuestionReply, + success: Schema.Boolean, + }).annotateMerge( + OpenApi.annotations({ + identifier: "question.reply", + summary: "Reply to question request", + description: "Provide answers to a question request from the AI assistant.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "question", + description: "Experimental HttpApi question routes.", + }), + ), +) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 2fbe31a0d..d5bdb6c8d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,3 +1,4 @@ export { openapi } from "./openapi.js" export { api } from "./definition/api.js" +export { questionApi, QuestionReply, QuestionRequest } from "./definition/question.js" export type { OpenApiSpec, ServerApi } from "./types.js" diff --git a/packages/server/src/openapi.ts b/packages/server/src/openapi.ts index c4ac95300..dda870d2b 100644 --- a/packages/server/src/openapi.ts +++ b/packages/server/src/openapi.ts @@ -1,14 +1,5 @@ +import { OpenApi } from "effect/unstable/httpapi" import { api } from "./definition/api.js" import type { OpenApiSpec } from "./types.js" -export function openapi(): OpenApiSpec { - return { - openapi: "3.1.1", - info: { - title: api.name, - version: "0.0.0", - description: "Contract-first server package scaffold.", - }, - paths: {}, - } -} +export const openapi = (): OpenApiSpec => OpenApi.fromApi(api) diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 8d337be42..9e89fe74c 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -1,14 +1,5 @@ -export interface ServerApi { - readonly name: string - readonly groups: readonly string[] -} +import type { HttpApi, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" -export interface OpenApiSpec { - readonly openapi: string - readonly info: { - readonly title: string - readonly version: string - readonly description: string - } - readonly paths: Record -} +export type ServerApi = HttpApi.HttpApi + +export type OpenApiSpec = OpenApi.OpenAPISpec -- cgit v1.2.3