33 *
44 * **Service interface** (`IQuestionService`): Reverse-RPC one-shot broker
55 * role — routes `QuestionRequest`s coming out of `PythinkerCore` to a waiter
6- * (web client over WS, mock handler in tests) and resolves the
7- * promise when the response arrives — or `dismiss()`-es it if the user
8- * closes the panel (SCHEMAS.md §6.3).
6+ * and resolves the promise when the response arrives or the user dismisses it.
97 *
108 * Role: one-shot broker — see `packages/services/AGENTS.md`. Kept under the
119 * `Service` suffix per the package-wide convention; the broker semantics
1210 * lives in the interface shape (`request` + `resolve` + `dismiss`) and the
1311 * docstring, not in the type name.
1412 *
1513 * **Shape note:** the service returns the in-process
16- * `QuestionResult = null | QuestionAnswers | QuestionResponse` (see
17- * `packages/agent-core/src/rpc/sdk-api.ts:48`). SCHEMAS.md §6.2/§6.4 defines
18- * a protocol-level `QuestionResponse` with a 5-kind discriminated union
19- * (`single` / `multi` / `other` / `multi_with_other` / `skipped`); the
20- * protocol↔in-process adapter lives at the daemon boundary, NOT inside the
21- * service interface. This keeps the SDK side of the adapter untouched and
22- * confines protocol shape decisions to one place.
14+ * `QuestionResult = null | QuestionAnswers | QuestionResponse`. The
15+ * protocol↔in-process adapter lives at the daemon boundary, not inside the
16+ * service interface.
2317 *
2418 * **Adapter** (`toBrokerRequest` / `toAgentCoreResponse` / `dismissedResult`):
25- * Bridges two representations of the same question interaction:
19+ * Bridges two representations of the same question interaction. Protocol ids
20+ * go in; question text and option labels that the user saw come out. This is
21+ * the only protocol↔SDK translation site for questions:
2622 *
2723 * 1. **In-process SDK shape** (agent-core, camelCase) — what
28- * `BridgeClientAPI` sees from `PythinkerCore.requestQuestion(...)`. See
29- * `packages/agent-core/src/rpc/sdk-api.ts:50-54`:
24+ * `BridgeClientAPI` sees from `PythinkerCore.requestQuestion(...)`:
3025 * `QuestionRequest { turnId?, toolCallId?, questions: QuestionItem[] }`
3126 * where `QuestionItem` has `question, header?, body?, options[],
3227 * multiSelect?, allowOther?, otherLabel?, otherDescription?`.
3328 * `QuestionResult = null | QuestionAnswers | QuestionResponse`,
3429 * `QuestionAnswers = Record<string, string | true>`.
3530 *
36- * 2. **Protocol wire shape** (snake_case, with daemon-allocated metadata) —
37- * defined in `packages/protocol/src/question.ts`. 5-kind discriminated
38- * union for answers: `single | multi | other | multi_with_other | skipped`.
31+ * 2. **Protocol wire shape** (snake_case, with daemon-allocated metadata).
3932 *
4033 * **Synthesizing stable ids** (SDK has no per-item / per-option `id`):
4134 * - `QuestionItem.id` ← `q_<index>` (e.g. `q_0`, `q_1`, ...)
4235 * - `QuestionOption.id` ← `opt_<parent_idx>_<option_idx>` (e.g. `opt_0_0`)
4336 *
44- * **Anti-corruption**: this is the ONLY place protocol↔SDK shape translation
45- * happens for question.
4637 */
4738
4839import { createDecorator } from '../../di' ;
@@ -104,14 +95,14 @@ export interface QuestionToBrokerRequestParams {
10495 readonly sessionId : string ;
10596 /** `createdAt` ISO string; broker passes `new Date().toISOString()`. */
10697 readonly createdAt : string ;
107- /** `expiresAt` ISO string; broker computes `createdAt + 60s` . */
98+ /** `expiresAt` ISO string; broker computes the lease deadline . */
10899 readonly expiresAt : string ;
109100}
110101
111102/**
112103 * Build a protocol option from an SDK option. SDK has only `label?:string` +
113104 * `description?:string`; we synthesize `id` from parent and child indices so
114- * `toAgentCoreAnswers` can map back through `Record<qid, string>` .
105+ * the response adapter can map answers back to the question text and labels .
115106 */
116107function buildOption (
117108 opt : {
@@ -134,7 +125,7 @@ function buildOption(
134125
135126/**
136127 * Build a protocol question item from an SDK item + its position. The
137- * synthesized `id` (`q_<parentIdx>`) is the key the SDK answers Record uses .
128+ * synthesized `id` (`q_<parentIdx>`) identifies the matching response item .
138129 */
139130function buildItem (
140131 item : InProcessQuestionItem ,
@@ -178,36 +169,42 @@ export function toBrokerRequest(
178169}
179170
180171/**
181- * Protocol REST response body → in-process SDK `QuestionResponse` (with
182- * `answers` flattened to `Record<string, string | true>`) .
172+ * Protocol response ids + the original request → in-process SDK
173+ * `QuestionResponse` with answers flattened to `Record<string, string | true>`.
183174 *
184- * Normalization rules from SCHEMAS §6.4:
185- * - single → option_id
186- * - multi → option_ids.join(',')
175+ * The original request is the lookup for the text and labels displayed to the
176+ * user:
177+ * - single → option label
178+ * - multi → option labels joined with `, `
187179 * - other → text
188- * - multi_with_other → [...option_ids, other_text].join(',')
180+ * - multi_with_other → option labels and text joined with `, `
189181 * - skipped → OMIT entry
190182 */
191183export function toAgentCoreResponse (
192184 resp : ProtocolQuestionResponse ,
185+ request : ProtocolQuestionRequest ,
193186) : InProcessQuestionResponse {
194187 const flattened : InProcessQuestionAnswers = { } ;
195188 for ( const [ qid , ans ] of Object . entries ( resp . answers ) ) {
189+ const item = request . questions . find ( ( question ) => question . id === qid ) ;
190+ const question = item ?. question ?? qid ;
191+ const optionLabel = ( id : string ) : string =>
192+ item ?. options . find ( ( option ) => option . id === id ) ?. label ?? id ;
196193 switch ( ans . kind ) {
197194 case 'single' :
198- flattened [ qid ] = ans . option_id ;
195+ flattened [ question ] = optionLabel ( ans . option_id ) ;
199196 break ;
200197 case 'multi' :
201- flattened [ qid ] = ans . option_ids . join ( ',' ) ;
198+ flattened [ question ] = ans . option_ids . map ( optionLabel ) . join ( ', ' ) ;
202199 break ;
203200 case 'other' :
204- flattened [ qid ] = ans . text ;
201+ flattened [ question ] = ans . text ;
205202 break ;
206203 case 'multi_with_other' :
207- flattened [ qid ] = [ ...ans . option_ids , ans . other_text ] . join ( ',' ) ;
204+ flattened [ question ] = [ ...ans . option_ids . map ( optionLabel ) , ans . other_text ] . join ( ', ' ) ;
208205 break ;
209206 case 'skipped' :
210- // Omitted from the record — matches SCHEMAS §6.4 ("if skipped continue") .
207+ // Omitted from the record.
211208 break ;
212209 default : {
213210 // Defensive: never-reached if Zod schema is the SOT, but TS narrowing
@@ -219,7 +216,7 @@ export function toAgentCoreResponse(
219216 }
220217 const out : InProcessQuestionResponse = { answers : flattened } ;
221218 if ( resp . method !== undefined ) {
222- // SCHEMAS §6.2 protocol allows 'click' as a method; agent-core's in-process
219+ // Protocol allows 'click' as a method; agent-core's in-process
223220 // `QuestionAnswerMethod` is `'enter' | 'space' | 'number_key'` (NO 'click').
224221 // Drop 'click' on the in-process side to preserve type safety; the wire
225222 // form keeps it for clients that want to surface the affordance used.
0 commit comments