mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-06 09:10:47 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f89f648d11 | |||
| c28ea79662 | |||
| 08bef1696d | |||
| 6186ae5e43 |
@@ -1,4 +1,4 @@
|
|||||||
import { Deferred, Effect, Layer, Schema, Context } from "effect"
|
import { Cache, Context, Deferred, Duration, Effect, Layer, Schema } from "effect"
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
@@ -75,6 +75,19 @@ export const Reply = Schema.Struct({
|
|||||||
}).annotate({ identifier: "QuestionReply" })
|
}).annotate({ identifier: "QuestionReply" })
|
||||||
export type Reply = Schema.Schema.Type<typeof Reply>
|
export type Reply = Schema.Schema.Type<typeof Reply>
|
||||||
|
|
||||||
|
export const Result = Schema.Union([
|
||||||
|
Schema.Struct({
|
||||||
|
status: Schema.Literal("answered"),
|
||||||
|
answers: Schema.Array(Answer).annotate({
|
||||||
|
description: "User answers in order of questions (each answer is an array of selected labels)",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Schema.Struct({
|
||||||
|
status: Schema.Literal("rejected"),
|
||||||
|
}),
|
||||||
|
]).annotate({ discriminator: "status", identifier: "QuestionResult" })
|
||||||
|
export type Result = Schema.Schema.Type<typeof Result>
|
||||||
|
|
||||||
const Replied = Schema.Struct({
|
const Replied = Schema.Struct({
|
||||||
sessionID: SessionID,
|
sessionID: SessionID,
|
||||||
requestID: QuestionID,
|
requestID: QuestionID,
|
||||||
@@ -104,21 +117,28 @@ export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Que
|
|||||||
|
|
||||||
interface PendingEntry {
|
interface PendingEntry {
|
||||||
info: Request
|
info: Request
|
||||||
deferred: Deferred.Deferred<ReadonlyArray<Answer>, RejectedError>
|
deferred: Deferred.Deferred<Result>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
pending: Map<QuestionID, PendingEntry>
|
pending: Map<QuestionID, PendingEntry>
|
||||||
|
completed: Cache.Cache<QuestionID, Result, NotFoundError>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service
|
// Service
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
|
readonly create: (input: {
|
||||||
|
sessionID: SessionID
|
||||||
|
questions: ReadonlyArray<Info>
|
||||||
|
tool?: Tool
|
||||||
|
}) => Effect.Effect<Request>
|
||||||
readonly ask: (input: {
|
readonly ask: (input: {
|
||||||
sessionID: SessionID
|
sessionID: SessionID
|
||||||
questions: ReadonlyArray<Info>
|
questions: ReadonlyArray<Info>
|
||||||
tool?: Tool
|
tool?: Tool
|
||||||
}) => Effect.Effect<ReadonlyArray<Answer>, RejectedError>
|
}) => Effect.Effect<ReadonlyArray<Answer>, RejectedError>
|
||||||
|
readonly wait: (requestID: QuestionID) => Effect.Effect<Result, NotFoundError>
|
||||||
readonly reply: (input: {
|
readonly reply: (input: {
|
||||||
requestID: QuestionID
|
requestID: QuestionID
|
||||||
answers: ReadonlyArray<Answer>
|
answers: ReadonlyArray<Answer>
|
||||||
@@ -137,12 +157,17 @@ export const layer = Layer.effect(
|
|||||||
Effect.fn("Question.state")(function* () {
|
Effect.fn("Question.state")(function* () {
|
||||||
const state = {
|
const state = {
|
||||||
pending: new Map<QuestionID, PendingEntry>(),
|
pending: new Map<QuestionID, PendingEntry>(),
|
||||||
|
completed: yield* Cache.make<QuestionID, Result, NotFoundError>({
|
||||||
|
capacity: 10_000,
|
||||||
|
timeToLive: Duration.minutes(30),
|
||||||
|
lookup: (requestID) => Effect.fail(new NotFoundError({ requestID })),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
yield* Effect.addFinalizer(() =>
|
yield* Effect.addFinalizer(() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
for (const item of state.pending.values()) {
|
for (const item of state.pending.values()) {
|
||||||
yield* Deferred.fail(item.deferred, new RejectedError())
|
yield* Deferred.succeed(item.deferred, { status: "rejected" })
|
||||||
}
|
}
|
||||||
state.pending.clear()
|
state.pending.clear()
|
||||||
}),
|
}),
|
||||||
@@ -152,7 +177,7 @@ export const layer = Layer.effect(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const ask = Effect.fn("Question.ask")(function* (input: {
|
const create = Effect.fn("Question.create")(function* (input: {
|
||||||
sessionID: SessionID
|
sessionID: SessionID
|
||||||
questions: ReadonlyArray<Info>
|
questions: ReadonlyArray<Info>
|
||||||
tool?: Tool
|
tool?: Tool
|
||||||
@@ -161,7 +186,7 @@ export const layer = Layer.effect(
|
|||||||
const id = QuestionID.ascending()
|
const id = QuestionID.ascending()
|
||||||
log.info("asking", { id, questions: input.questions.length })
|
log.info("asking", { id, questions: input.questions.length })
|
||||||
|
|
||||||
const deferred = yield* Deferred.make<ReadonlyArray<Answer>, RejectedError>()
|
const deferred = yield* Deferred.make<Result>()
|
||||||
const info: Request = {
|
const info: Request = {
|
||||||
id,
|
id,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
@@ -170,49 +195,79 @@ export const layer = Layer.effect(
|
|||||||
}
|
}
|
||||||
pending.set(id, { info, deferred })
|
pending.set(id, { info, deferred })
|
||||||
yield* bus.publish(Event.Asked, info)
|
yield* bus.publish(Event.Asked, info)
|
||||||
|
return info
|
||||||
|
})
|
||||||
|
|
||||||
return yield* Effect.ensuring(
|
const wait = Effect.fn("Question.wait")(function* (requestID: QuestionID) {
|
||||||
Deferred.await(deferred),
|
const info = yield* InstanceState.get(state)
|
||||||
Effect.sync(() => {
|
const pending = info.pending.get(requestID)
|
||||||
pending.delete(id)
|
if (pending) return yield* Deferred.await(pending.deferred)
|
||||||
}),
|
return yield* Cache.get(info.completed, requestID).pipe(
|
||||||
|
Effect.tapError(() => Cache.invalidateWhen(info.completed, requestID, () => true)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const settle = Effect.fn("Question.settle")(function* (requestID: QuestionID, result: Result) {
|
||||||
|
const info = yield* InstanceState.get(state)
|
||||||
|
const existing = info.pending.get(requestID)
|
||||||
|
if (!existing) return false
|
||||||
|
|
||||||
|
yield* Cache.set(info.completed, requestID, result)
|
||||||
|
info.pending.delete(requestID)
|
||||||
|
if (result.status === "answered") {
|
||||||
|
yield* bus.publish(Event.Replied, {
|
||||||
|
sessionID: existing.info.sessionID,
|
||||||
|
requestID: existing.info.id,
|
||||||
|
answers: result.answers,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
yield* bus.publish(Event.Rejected, {
|
||||||
|
sessionID: existing.info.sessionID,
|
||||||
|
requestID: existing.info.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
yield* Deferred.succeed(existing.deferred, result)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
const ask = Effect.fn("Question.ask")(function* (input: {
|
||||||
|
sessionID: SessionID
|
||||||
|
questions: ReadonlyArray<Info>
|
||||||
|
tool?: Tool
|
||||||
|
}) {
|
||||||
|
const request = yield* create(input)
|
||||||
|
|
||||||
|
const result = yield* Effect.ensuring(
|
||||||
|
wait(request.id).pipe(Effect.orDie),
|
||||||
|
settle(request.id, { status: "rejected" }).pipe(Effect.asVoid),
|
||||||
|
)
|
||||||
|
if (result.status === "answered") return result.answers
|
||||||
|
return yield* new RejectedError()
|
||||||
|
})
|
||||||
|
|
||||||
const reply = Effect.fn("Question.reply")(function* (input: {
|
const reply = Effect.fn("Question.reply")(function* (input: {
|
||||||
requestID: QuestionID
|
requestID: QuestionID
|
||||||
answers: ReadonlyArray<Answer>
|
answers: ReadonlyArray<Answer>
|
||||||
}) {
|
}) {
|
||||||
const pending = (yield* InstanceState.get(state)).pending
|
const result: Result = {
|
||||||
const existing = pending.get(input.requestID)
|
status: "answered",
|
||||||
if (!existing) {
|
answers: input.answers.map((a) => [...a]),
|
||||||
|
}
|
||||||
|
const ok = yield* settle(input.requestID, result)
|
||||||
|
if (!ok) {
|
||||||
log.warn("reply for unknown request", { requestID: input.requestID })
|
log.warn("reply for unknown request", { requestID: input.requestID })
|
||||||
return yield* new NotFoundError({ requestID: input.requestID })
|
return yield* new NotFoundError({ requestID: input.requestID })
|
||||||
}
|
}
|
||||||
pending.delete(input.requestID)
|
|
||||||
log.info("replied", { requestID: input.requestID, answers: input.answers })
|
log.info("replied", { requestID: input.requestID, answers: input.answers })
|
||||||
yield* bus.publish(Event.Replied, {
|
|
||||||
sessionID: existing.info.sessionID,
|
|
||||||
requestID: existing.info.id,
|
|
||||||
answers: input.answers.map((a) => [...a]),
|
|
||||||
})
|
|
||||||
yield* Deferred.succeed(existing.deferred, input.answers)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const reject = Effect.fn("Question.reject")(function* (requestID: QuestionID) {
|
const reject = Effect.fn("Question.reject")(function* (requestID: QuestionID) {
|
||||||
const pending = (yield* InstanceState.get(state)).pending
|
const ok = yield* settle(requestID, { status: "rejected" })
|
||||||
const existing = pending.get(requestID)
|
if (!ok) {
|
||||||
if (!existing) {
|
|
||||||
log.warn("reject for unknown request", { requestID })
|
log.warn("reject for unknown request", { requestID })
|
||||||
return yield* new NotFoundError({ requestID })
|
return yield* new NotFoundError({ requestID })
|
||||||
}
|
}
|
||||||
pending.delete(requestID)
|
|
||||||
log.info("rejected", { requestID })
|
log.info("rejected", { requestID })
|
||||||
yield* bus.publish(Event.Rejected, {
|
|
||||||
sessionID: existing.info.sessionID,
|
|
||||||
requestID: existing.info.id,
|
|
||||||
})
|
|
||||||
yield* Deferred.fail(existing.deferred, new RejectedError())
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const list = Effect.fn("Question.list")(function* () {
|
const list = Effect.fn("Question.list")(function* () {
|
||||||
@@ -220,7 +275,7 @@ export const layer = Layer.effect(
|
|||||||
return Array.from(pending.values(), (x) => x.info)
|
return Array.from(pending.values(), (x) => x.info)
|
||||||
})
|
})
|
||||||
|
|
||||||
return Service.of({ ask, reply, reject, list })
|
return Service.of({ create, ask, wait, reply, reject, list })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Question } from "@/question"
|
import { Question } from "@/question"
|
||||||
import { QuestionID } from "@/question/schema"
|
import { QuestionID } from "@/question/schema"
|
||||||
|
import { SessionID } from "@/session/schema"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { QuestionNotFoundError } from "../errors"
|
import { QuestionNotFoundError } from "../errors"
|
||||||
@@ -15,10 +16,32 @@ const ReplyPayload = Schema.Struct({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const AskPayload = Schema.Struct({
|
||||||
|
sessionID: SessionID,
|
||||||
|
questions: Schema.Array(Question.Info).annotate({
|
||||||
|
description: "Questions to ask",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const AskResponse = Schema.Struct({
|
||||||
|
id: QuestionID,
|
||||||
|
})
|
||||||
|
|
||||||
export const QuestionApi = HttpApi.make("question")
|
export const QuestionApi = HttpApi.make("question")
|
||||||
.add(
|
.add(
|
||||||
HttpApiGroup.make("question")
|
HttpApiGroup.make("question")
|
||||||
.add(
|
.add(
|
||||||
|
HttpApiEndpoint.post("ask", root, {
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
|
payload: AskPayload,
|
||||||
|
success: described(AskResponse, "Created question request"),
|
||||||
|
}).annotateMerge(
|
||||||
|
OpenApi.annotations({
|
||||||
|
identifier: "question.ask",
|
||||||
|
summary: "Ask questions",
|
||||||
|
description: "Create a question request and return its id.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
HttpApiEndpoint.get("list", root, {
|
HttpApiEndpoint.get("list", root, {
|
||||||
query: WorkspaceRoutingQuery,
|
query: WorkspaceRoutingQuery,
|
||||||
success: described(Schema.Array(Question.Request), "List of pending questions"),
|
success: described(Schema.Array(Question.Request), "List of pending questions"),
|
||||||
@@ -29,6 +52,18 @@ export const QuestionApi = HttpApi.make("question")
|
|||||||
description: "Get all pending question requests across all sessions.",
|
description: "Get all pending question requests across all sessions.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
HttpApiEndpoint.get("wait", `${root}/:requestID/wait`, {
|
||||||
|
params: { requestID: QuestionID },
|
||||||
|
query: WorkspaceRoutingQuery,
|
||||||
|
success: described(Question.Result, "Question result"),
|
||||||
|
error: [QuestionNotFoundError],
|
||||||
|
}).annotateMerge(
|
||||||
|
OpenApi.annotations({
|
||||||
|
identifier: "question.wait",
|
||||||
|
summary: "Wait for question result",
|
||||||
|
description: "Wait for a question request to be answered or rejected.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
|
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
|
||||||
params: { requestID: QuestionID },
|
params: { requestID: QuestionID },
|
||||||
query: WorkspaceRoutingQuery,
|
query: WorkspaceRoutingQuery,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Effect } from "effect"
|
|||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { InstanceHttpApi } from "../api"
|
import { InstanceHttpApi } from "../api"
|
||||||
import { QuestionNotFoundError } from "../errors"
|
import { QuestionNotFoundError } from "../errors"
|
||||||
|
import { AskPayload } from "../groups/question"
|
||||||
|
|
||||||
export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "question", (handlers) =>
|
export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "question", (handlers) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
@@ -13,6 +14,24 @@ export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "question"
|
|||||||
return yield* svc.list()
|
return yield* svc.list()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const ask = Effect.fn("QuestionHttpApi.ask")(function* (ctx: { payload: typeof AskPayload.Type }) {
|
||||||
|
const request = yield* svc.create(ctx.payload)
|
||||||
|
return { id: request.id }
|
||||||
|
})
|
||||||
|
|
||||||
|
const wait = Effect.fn("QuestionHttpApi.wait")(function* (ctx: { params: { requestID: QuestionID } }) {
|
||||||
|
return yield* svc.wait(ctx.params.requestID).pipe(
|
||||||
|
Effect.catchTag("Question.NotFoundError", (error) =>
|
||||||
|
Effect.fail(
|
||||||
|
new QuestionNotFoundError({
|
||||||
|
requestID: String(error.requestID),
|
||||||
|
message: `Question request not found: ${error.requestID}`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const reply = Effect.fn("QuestionHttpApi.reply")(function* (ctx: {
|
const reply = Effect.fn("QuestionHttpApi.reply")(function* (ctx: {
|
||||||
params: { requestID: QuestionID }
|
params: { requestID: QuestionID }
|
||||||
payload: Question.Reply
|
payload: Question.Reply
|
||||||
@@ -49,6 +68,11 @@ export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "question"
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
return handlers.handle("list", list).handle("reply", reply).handle("reject", reject)
|
return handlers
|
||||||
|
.handle("ask", ask)
|
||||||
|
.handle("list", list)
|
||||||
|
.handle("wait", wait)
|
||||||
|
.handle("reply", reply)
|
||||||
|
.handle("reject", reject)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,6 +23,20 @@ const askEffect = Effect.fn("QuestionTest.ask")(function* (input: {
|
|||||||
return yield* question.ask(input)
|
return yield* question.ask(input)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const createEffect = Effect.fn("QuestionTest.create")(function* (input: {
|
||||||
|
sessionID: SessionID
|
||||||
|
questions: ReadonlyArray<Question.Info>
|
||||||
|
tool?: Question.Tool
|
||||||
|
}) {
|
||||||
|
const question = yield* Question.Service
|
||||||
|
return yield* question.create(input)
|
||||||
|
})
|
||||||
|
|
||||||
|
const waitEffect = Effect.fn("QuestionTest.wait")(function* (requestID: QuestionID) {
|
||||||
|
const question = yield* Question.Service
|
||||||
|
return yield* question.wait(requestID)
|
||||||
|
})
|
||||||
|
|
||||||
const listEffect = Question.Service.use((svc) => svc.list())
|
const listEffect = Question.Service.use((svc) => svc.list())
|
||||||
|
|
||||||
const replyEffect = Effect.fn("QuestionTest.reply")(function* (input: {
|
const replyEffect = Effect.fn("QuestionTest.reply")(function* (input: {
|
||||||
@@ -183,6 +197,64 @@ it.instance(
|
|||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.instance(
|
||||||
|
"wait - returns answers for pending and completed requests",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const request = yield* createEffect({
|
||||||
|
sessionID: SessionID.make("ses_test"),
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
question: "What would you like to do?",
|
||||||
|
header: "Action",
|
||||||
|
options: [
|
||||||
|
{ label: "Option 1", description: "First option" },
|
||||||
|
{ label: "Option 2", description: "Second option" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const fiber = yield* waitEffect(request.id).pipe(Effect.forkScoped)
|
||||||
|
|
||||||
|
yield* replyEffect({
|
||||||
|
requestID: request.id,
|
||||||
|
answers: [["Option 1"]],
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = { status: "answered" as const, answers: [["Option 1"]] }
|
||||||
|
expect(yield* Fiber.join(fiber)).toEqual(result)
|
||||||
|
expect(yield* waitEffect(request.id)).toEqual(result)
|
||||||
|
}),
|
||||||
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
it.instance(
|
||||||
|
"wait - returns rejected when pending ask is interrupted",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const fiber = yield* askEffect({
|
||||||
|
sessionID: SessionID.make("ses_test"),
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
question: "What would you like to do?",
|
||||||
|
header: "Action",
|
||||||
|
options: [{ label: "Option 1", description: "First option" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).pipe(Effect.forkScoped)
|
||||||
|
|
||||||
|
const pending = yield* waitForPending(1)
|
||||||
|
const waiter = yield* waitEffect(pending[0].id).pipe(Effect.forkScoped)
|
||||||
|
|
||||||
|
yield* Fiber.interrupt(fiber)
|
||||||
|
|
||||||
|
expect(yield* Fiber.join(waiter)).toEqual({ status: "rejected" })
|
||||||
|
expect(yield* waitEffect(pending[0].id)).toEqual({ status: "rejected" })
|
||||||
|
}),
|
||||||
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
"reply - fails for unknown requestID",
|
"reply - fails for unknown requestID",
|
||||||
() =>
|
() =>
|
||||||
@@ -259,6 +331,28 @@ it.instance(
|
|||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.instance(
|
||||||
|
"wait - returns rejected for completed rejected requests",
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const request = yield* createEffect({
|
||||||
|
sessionID: SessionID.make("ses_test"),
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
question: "What would you like to do?",
|
||||||
|
header: "Action",
|
||||||
|
options: [{ label: "Option 1", description: "First option" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* rejectEffect(request.id)
|
||||||
|
|
||||||
|
expect(yield* waitEffect(request.id)).toEqual({ status: "rejected" })
|
||||||
|
}),
|
||||||
|
{ git: true },
|
||||||
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
"reject - fails for unknown requestID",
|
"reject - fails for unknown requestID",
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -235,6 +235,34 @@ const scenarios: Scenario[] = [
|
|||||||
}))
|
}))
|
||||||
.json(404, object, "status"),
|
.json(404, object, "status"),
|
||||||
http.protected.get("/question", "question.list").json(200, array),
|
http.protected.get("/question", "question.list").json(200, array),
|
||||||
|
http.protected
|
||||||
|
.post("/question", "question.ask")
|
||||||
|
.mutating()
|
||||||
|
.at((ctx) => ({
|
||||||
|
path: "/question",
|
||||||
|
headers: ctx.headers(),
|
||||||
|
body: {
|
||||||
|
sessionID: "ses_httpapi_question",
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
question: "Proceed?",
|
||||||
|
header: "Proceed",
|
||||||
|
options: [{ label: "Yes", description: "Continue" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.json(200, (body) => {
|
||||||
|
object(body)
|
||||||
|
check(typeof body.id === "string" && body.id.startsWith("que_"), "question ask should return request id")
|
||||||
|
}),
|
||||||
|
http.protected
|
||||||
|
.get("/question/{requestID}/wait", "question.wait.missing")
|
||||||
|
.at((ctx) => ({
|
||||||
|
path: route("/question/{requestID}/wait", { requestID: "que_httpapi_wait" }),
|
||||||
|
headers: ctx.headers(),
|
||||||
|
}))
|
||||||
|
.json(404, object, "status"),
|
||||||
http.protected
|
http.protected
|
||||||
.post("/question/{requestID}/reply", "question.reply.invalid")
|
.post("/question/{requestID}/reply", "question.reply.invalid")
|
||||||
.at((ctx) => ({
|
.at((ctx) => ({
|
||||||
|
|||||||
@@ -151,12 +151,17 @@ import type {
|
|||||||
PtyUpdateErrors,
|
PtyUpdateErrors,
|
||||||
PtyUpdateResponses,
|
PtyUpdateResponses,
|
||||||
QuestionAnswer,
|
QuestionAnswer,
|
||||||
|
QuestionAskErrors,
|
||||||
|
QuestionAskResponses,
|
||||||
|
QuestionInfo,
|
||||||
QuestionListErrors,
|
QuestionListErrors,
|
||||||
QuestionListResponses,
|
QuestionListResponses,
|
||||||
QuestionRejectErrors,
|
QuestionRejectErrors,
|
||||||
QuestionRejectResponses,
|
QuestionRejectResponses,
|
||||||
QuestionReplyErrors,
|
QuestionReplyErrors,
|
||||||
QuestionReplyResponses,
|
QuestionReplyResponses,
|
||||||
|
QuestionWaitErrors,
|
||||||
|
QuestionWaitResponses,
|
||||||
SessionAbortErrors,
|
SessionAbortErrors,
|
||||||
SessionAbortResponses,
|
SessionAbortResponses,
|
||||||
SessionChildrenErrors,
|
SessionChildrenErrors,
|
||||||
@@ -2687,6 +2692,77 @@ export class Question extends HeyApiClient {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask questions
|
||||||
|
*
|
||||||
|
* Create a question request and return its id.
|
||||||
|
*/
|
||||||
|
public ask<ThrowOnError extends boolean = false>(
|
||||||
|
parameters?: {
|
||||||
|
directory?: string
|
||||||
|
workspace?: string
|
||||||
|
sessionID?: string
|
||||||
|
questions?: Array<QuestionInfo>
|
||||||
|
},
|
||||||
|
options?: Options<never, ThrowOnError>,
|
||||||
|
) {
|
||||||
|
const params = buildClientParams(
|
||||||
|
[parameters],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
{ in: "query", key: "directory" },
|
||||||
|
{ in: "query", key: "workspace" },
|
||||||
|
{ in: "body", key: "sessionID" },
|
||||||
|
{ in: "body", key: "questions" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return (options?.client ?? this.client).post<QuestionAskResponses, QuestionAskErrors, ThrowOnError>({
|
||||||
|
url: "/question",
|
||||||
|
...options,
|
||||||
|
...params,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...options?.headers,
|
||||||
|
...params.headers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for question result
|
||||||
|
*
|
||||||
|
* Wait for a question request to be answered or rejected.
|
||||||
|
*/
|
||||||
|
public wait<ThrowOnError extends boolean = false>(
|
||||||
|
parameters: {
|
||||||
|
requestID: string
|
||||||
|
directory?: string
|
||||||
|
workspace?: string
|
||||||
|
},
|
||||||
|
options?: Options<never, ThrowOnError>,
|
||||||
|
) {
|
||||||
|
const params = buildClientParams(
|
||||||
|
[parameters],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
args: [
|
||||||
|
{ in: "path", key: "requestID" },
|
||||||
|
{ in: "query", key: "directory" },
|
||||||
|
{ in: "query", key: "workspace" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return (options?.client ?? this.client).get<QuestionWaitResponses, QuestionWaitErrors, ThrowOnError>({
|
||||||
|
url: "/question/{requestID}/wait",
|
||||||
|
...options,
|
||||||
|
...params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reply to question request
|
* Reply to question request
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1717,6 +1717,18 @@ export type PtyForbiddenError = {
|
|||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QuestionResult =
|
||||||
|
| {
|
||||||
|
status: "answered"
|
||||||
|
/**
|
||||||
|
* User answers in order of questions (each answer is an array of selected labels)
|
||||||
|
*/
|
||||||
|
answers: Array<QuestionAnswer>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: "rejected"
|
||||||
|
}
|
||||||
|
|
||||||
export type QuestionNotFoundError = {
|
export type QuestionNotFoundError = {
|
||||||
_tag: "QuestionNotFoundError"
|
_tag: "QuestionNotFoundError"
|
||||||
requestID: string
|
requestID: string
|
||||||
@@ -5743,6 +5755,76 @@ export type QuestionListResponses = {
|
|||||||
|
|
||||||
export type QuestionListResponse = QuestionListResponses[keyof QuestionListResponses]
|
export type QuestionListResponse = QuestionListResponses[keyof QuestionListResponses]
|
||||||
|
|
||||||
|
export type QuestionAskData = {
|
||||||
|
body?: {
|
||||||
|
sessionID: string
|
||||||
|
/**
|
||||||
|
* Questions to ask
|
||||||
|
*/
|
||||||
|
questions: Array<QuestionInfo>
|
||||||
|
}
|
||||||
|
path?: never
|
||||||
|
query?: {
|
||||||
|
directory?: string
|
||||||
|
workspace?: string
|
||||||
|
}
|
||||||
|
url: "/question"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuestionAskErrors = {
|
||||||
|
/**
|
||||||
|
* Bad request
|
||||||
|
*/
|
||||||
|
400: BadRequestError
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuestionAskError = QuestionAskErrors[keyof QuestionAskErrors]
|
||||||
|
|
||||||
|
export type QuestionAskResponses = {
|
||||||
|
/**
|
||||||
|
* Created question request
|
||||||
|
*/
|
||||||
|
200: {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuestionAskResponse = QuestionAskResponses[keyof QuestionAskResponses]
|
||||||
|
|
||||||
|
export type QuestionWaitData = {
|
||||||
|
body?: never
|
||||||
|
path: {
|
||||||
|
requestID: string
|
||||||
|
}
|
||||||
|
query?: {
|
||||||
|
directory?: string
|
||||||
|
workspace?: string
|
||||||
|
}
|
||||||
|
url: "/question/{requestID}/wait"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuestionWaitErrors = {
|
||||||
|
/**
|
||||||
|
* Bad request
|
||||||
|
*/
|
||||||
|
400: BadRequestError
|
||||||
|
/**
|
||||||
|
* QuestionNotFoundError
|
||||||
|
*/
|
||||||
|
404: QuestionNotFoundError
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuestionWaitError = QuestionWaitErrors[keyof QuestionWaitErrors]
|
||||||
|
|
||||||
|
export type QuestionWaitResponses = {
|
||||||
|
/**
|
||||||
|
* Question result
|
||||||
|
*/
|
||||||
|
200: QuestionResult
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuestionWaitResponse = QuestionWaitResponses[keyof QuestionWaitResponses]
|
||||||
|
|
||||||
export type QuestionReplyData = {
|
export type QuestionReplyData = {
|
||||||
body?: {
|
body?: {
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user