Skip to content

Commit 0bdddde

Browse files
fix(opencode): increase question tool header max length
Fixes #8650 The header field max length was 12 characters, which is too restrictive for the AI model to generate meaningful labels. Increased to 30 chars. Error was: 'Too big: expected string to have <=12 characters'
1 parent 4038a55 commit 0bdddde

File tree

1 file changed

+171
-0
lines changed

1 file changed

+171
-0
lines changed
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { Bus } from "@/bus"
2+
import { BusEvent } from "@/bus/bus-event"
3+
import { Identifier } from "@/id/id"
4+
import { Instance } from "@/project/instance"
5+
import { Log } from "@/util/log"
6+
import z from "zod"
7+
8+
export namespace Question {
9+
const log = Log.create({ service: "question" })
10+
11+
export const Option = z
12+
.object({
13+
label: z.string().describe("Display text (1-5 words, concise)"),
14+
description: z.string().describe("Explanation of choice"),
15+
})
16+
.meta({
17+
ref: "QuestionOption",
18+
})
19+
export type Option = z.infer<typeof Option>
20+
21+
export const Info = z
22+
.object({
23+
question: z.string().describe("Complete question"),
24+
header: z.string().max(30).describe("Short label (max 30 chars)"),
25+
options: z.array(Option).describe("Available choices"),
26+
multiple: z.boolean().optional().describe("Allow selecting multiple choices"),
27+
custom: z.boolean().optional().describe("Allow typing a custom answer (default: true)"),
28+
})
29+
.meta({
30+
ref: "QuestionInfo",
31+
})
32+
export type Info = z.infer<typeof Info>
33+
34+
export const Request = z
35+
.object({
36+
id: Identifier.schema("question"),
37+
sessionID: Identifier.schema("session"),
38+
questions: z.array(Info).describe("Questions to ask"),
39+
tool: z
40+
.object({
41+
messageID: z.string(),
42+
callID: z.string(),
43+
})
44+
.optional(),
45+
})
46+
.meta({
47+
ref: "QuestionRequest",
48+
})
49+
export type Request = z.infer<typeof Request>
50+
51+
export const Answer = z.array(z.string()).meta({
52+
ref: "QuestionAnswer",
53+
})
54+
export type Answer = z.infer<typeof Answer>
55+
56+
export const Reply = z.object({
57+
answers: z
58+
.array(Answer)
59+
.describe("User answers in order of questions (each answer is an array of selected labels)"),
60+
})
61+
export type Reply = z.infer<typeof Reply>
62+
63+
export const Event = {
64+
Asked: BusEvent.define("question.asked", Request),
65+
Replied: BusEvent.define(
66+
"question.replied",
67+
z.object({
68+
sessionID: z.string(),
69+
requestID: z.string(),
70+
answers: z.array(Answer),
71+
}),
72+
),
73+
Rejected: BusEvent.define(
74+
"question.rejected",
75+
z.object({
76+
sessionID: z.string(),
77+
requestID: z.string(),
78+
}),
79+
),
80+
}
81+
82+
const state = Instance.state(async () => {
83+
const pending: Record<
84+
string,
85+
{
86+
info: Request
87+
resolve: (answers: Answer[]) => void
88+
reject: (e: any) => void
89+
}
90+
> = {}
91+
92+
return {
93+
pending,
94+
}
95+
})
96+
97+
export async function ask(input: {
98+
sessionID: string
99+
questions: Info[]
100+
tool?: { messageID: string; callID: string }
101+
}): Promise<Answer[]> {
102+
const s = await state()
103+
const id = Identifier.ascending("question")
104+
105+
log.info("asking", { id, questions: input.questions.length })
106+
107+
return new Promise<Answer[]>((resolve, reject) => {
108+
const info: Request = {
109+
id,
110+
sessionID: input.sessionID,
111+
questions: input.questions,
112+
tool: input.tool,
113+
}
114+
s.pending[id] = {
115+
info,
116+
resolve,
117+
reject,
118+
}
119+
Bus.publish(Event.Asked, info)
120+
})
121+
}
122+
123+
export async function reply(input: { requestID: string; answers: Answer[] }): Promise<void> {
124+
const s = await state()
125+
const existing = s.pending[input.requestID]
126+
if (!existing) {
127+
log.warn("reply for unknown request", { requestID: input.requestID })
128+
return
129+
}
130+
delete s.pending[input.requestID]
131+
132+
log.info("replied", { requestID: input.requestID, answers: input.answers })
133+
134+
Bus.publish(Event.Replied, {
135+
sessionID: existing.info.sessionID,
136+
requestID: existing.info.id,
137+
answers: input.answers,
138+
})
139+
140+
existing.resolve(input.answers)
141+
}
142+
143+
export async function reject(requestID: string): Promise<void> {
144+
const s = await state()
145+
const existing = s.pending[requestID]
146+
if (!existing) {
147+
log.warn("reject for unknown request", { requestID })
148+
return
149+
}
150+
delete s.pending[requestID]
151+
152+
log.info("rejected", { requestID })
153+
154+
Bus.publish(Event.Rejected, {
155+
sessionID: existing.info.sessionID,
156+
requestID: existing.info.id,
157+
})
158+
159+
existing.reject(new RejectedError())
160+
}
161+
162+
export class RejectedError extends Error {
163+
constructor() {
164+
super("The user dismissed this question")
165+
}
166+
}
167+
168+
export async function list() {
169+
return state().then((x) => Object.values(x.pending).map((x) => x.info))
170+
}
171+
}

0 commit comments

Comments
 (0)