/** * questions/application/question.input — QuestionerAgent input envelope. * * Defines per-mode context types (DiscoveryContext, IntentContext, …), * the discriminated QuestionerInput union, and the runtime validation guard * `isValidQuestionerInputContract`. * * Lives in the application layer (not domain) because `isValidQuestionerInputContract` * consumes the negotiation question-safety port * (`capabilities/negotiation.questions.facade`) to enforce privacy invariants * on counterparty hints and indexContext labels. * * Foreground and ambient adapter entry points inject a `QuestionerEnqueueFn` * (defined here as a port alias) to schedule async generation without * importing the queue implementation. * * IND-547: canonical home — previously questioner/questioner.types.ts. * Legacy path is a thin compatibility shim pointing here. */ import type { DiscoveryQuestionInput } from "../../shared/schemas/discovery-question.schema.js"; import type { ToolScopeType } from "../../shared/agent/tool.scope.js"; import type { NegotiationQuestionCandidate, QuestionMode, QuestionPoolDiscriminator } from "../domain/question.schema.js"; import type { NegotiationConsultationReason } from "../../capabilities/negotiation.questions.facade.js"; /** * Discovery context — wraps the existing DiscoveryQuestionInput wholesale. * The discovery preset's buildPrompt delegates to `question.discovery.prompt.ts`. */ export type DiscoveryContext = DiscoveryQuestionInput; /** Intent context — data needed to generate questions about an intent. */ export interface IntentContext { intentId: string; payload: string; summary?: string; /** The user's global user_context paragraph (profile-replacing identity text). */ userContext?: string; } /** Recovery-only intent context after a successful discovery completion. */ export interface RecoveryIntentContext extends IntentContext { purpose: "recovery"; /** Privacy-safe aggregate signal; raw negotiation evidence is never provided. */ rejectedNegotiationCount?: number; } /** Profile context — data needed to generate questions to fill profile gaps. */ export interface ProfileContext { /** The user's global user_context paragraph (profile-replacing identity text). */ userContext?: string; gaps: string[]; /** Existing premise texts the user has already stated (e.g. "I live in Berlin"). */ existingPremises?: string[]; } /** Shared context fields for negotiation-mode questions. */ interface NegotiationContextBase { negotiationId: string; /** Privacy-reviewed generic description; never raw counterparty identity/profile. */ counterpartyHint: string; /** Source-safe network label, never an internal prompt or identifier. */ indexContext: string; /** The user's global user_context paragraph (profile-replacing identity text). */ userContext?: string; } /** Post-stall negotiation context. Preserves the existing source shape. */ export interface PostStallNegotiationContext extends NegotiationContextBase { purpose?: undefined; outcomeReason: "turn_cap" | "timeout" | "stalled"; /** The recipient's own exact opportunity-bound signal, never evaluator reasoning. */ recipientIntent: string; } /** Pre-accept uptake context targeting a counterparty's preparatory conditions. */ export interface UptakeNegotiationContext extends NegotiationContextBase { purpose: "uptake"; /** Plain-language activity or commitment whose feasibility needs clarification. */ proposedActivity: string; } /** Negotiation context discriminated by internal question purpose. */ export type NegotiationContext = PostStallNegotiationContext | UptakeNegotiationContext; /** * Negotiation-inflight context — a negotiator mid-negotiation wants to ask its * OWN client a question before continuing (the `ask_user` action, P3.2). The * negotiator supplies only a closed category; the QuestionerAgent receives * server-owned fixed copy and never agent-authored instruction text. * Distinct from {@link NegotiationContext}, which covers post-stall questions. */ export interface NegotiationInflightContext { negotiationId: string; /** Anonymized counterparty description (attributes, never identity). */ counterpartyHint: string; /** Community / index context the negotiation runs in. */ indexContext: string; /** Closed server-owned category selecting fixed Questioner copy. */ consultationPolicyReason: NegotiationConsultationReason; /** The user's global user_context paragraph (profile-replacing identity text). */ userContext?: string; } /** * Chat context — data for orchestrator-initiated mid-conversation questions * (the `ask_user_question` tool). The orchestrator states what it needs to * learn; the QuestionerAgent turns that into polished structured questions, * grounded in the recent conversation and the user's identity context. */ export interface ChatContext { /** What the orchestrator needs to learn and why (authored by the chat model). */ purpose: string; /** Draft questions proposed by the orchestrator. The agent refines these. */ draftQuestions?: Array<{ prompt: string; options?: string[]; multiSelect?: boolean; }>; /** Recent conversation excerpt for grounding (most recent messages last). */ conversationExcerpt?: string; /** The user's global user_context paragraph (profile-replacing identity text). */ userContext?: string; } /** * Pool-discovery context — mined discriminators from a discovery-run pool * (IND-418). No generator LLM runs for this mode: the QuestionerQueue * synthesizes the question deterministically from the top discriminator and * stashes the rest as interview-mode alternates. */ export interface PoolDiscoveryContext { intentId: string; /** Truncated intent payload (+ summary) display snippet. */ intentText: string; /** Stable hash of the full normalized payload + summary used for freshness. */ intentFingerprint?: string; poolSize: number; /** Exact bounded candidate opportunity IDs supplied to synthesis. */ opportunityIds: string[]; /** Discovery run that produced the pool. */ runId?: string; /** Eligible discriminators, VoI-descending (asked + chain alternates). */ discriminators: QuestionPoolDiscriminator[]; /** ISO-8601 timestamp of the mining pass. */ minedAt: string; } /** Discriminated union: mode selects the context shape. */ export type QuestionerContext = DiscoveryContext | IntentContext | RecoveryIntentContext | ProfileContext | NegotiationContext | NegotiationInflightContext | ChatContext | PoolDiscoveryContext; /** * Payload shape accepted by the questionerEnqueue callback. Covers all * question modes — the composition root bridges this to the concrete * QuestionerQueue. */ export type QuestionerEnqueuePayload = QuestionerInput; /** * Callback signature for async question generation enqueue. * * Ambient adapter port: recovery, pool, uptake, inflight, and push generation * each inject this callback from the composition root; they never import the * queue implementation directly. */ export type QuestionerEnqueueFn = (input: QuestionerEnqueuePayload) => Promise; /** Top-level input envelope for QuestionerAgent.invoke(). */ interface QuestionerInputBase { /** Selects the preset (system prompt + builder). */ mode: QuestionMode; /** User the questions are generated for. */ userId: string; /** Entity type that triggered this (e.g. "opportunity", "intent", "profile"). */ sourceType: string; /** ID of the triggering entity. */ sourceId: string; /** Scoped question context. Network scopes persist as QuestionActor.networkId. */ scopeType?: ToolScopeType; /** Scoped question id. When scopeType is `network`, this is the actor networkId. */ scopeId?: string; /** * Intent that triggered the run that generated these questions. Persisted as * `detection.triggeredBy` so intent-scoped surfaces (e.g. the intent page) * can find them. Independent of `scopeType`/`scopeId`, which may carry a * network scope at the same time. */ triggeredByIntentId?: string; /** Conversation ID — set when the question originates from a chat session. Persisted on the question row for frontend filtering. */ conversationId?: string; /** Assistant message ID — set when we know which message triggered the question. Stored in detection.messageId for inline anchoring. */ messageId?: string; /** * Candidate exact binding for negotiation-family jobs. The API/DB must * authoritatively re-resolve it before generation and again before insert. */ negotiation?: NegotiationQuestionCandidate; } /** Non-negotiation modes cannot smuggle negotiation purpose/provenance. */ interface StandardQuestionerInput extends QuestionerInputBase { mode: Exclude; purpose?: never; negotiation?: never; context: Exclude; } /** Ordinary post-stall generation is task-backed and uses only the ordinary preset. */ export interface PostStallQuestionerInput extends QuestionerInputBase { mode: "negotiation"; purpose: "stalled_followup"; negotiation: NegotiationQuestionCandidate & { purpose: "stalled_followup"; taskId: string; }; context: PostStallNegotiationContext; } /** Mid-negotiation consultation is task-backed and uses only structured ask_user fields. */ export interface InflightQuestionerInput extends QuestionerInputBase { mode: "negotiation_inflight"; purpose: "inflight_consultation"; negotiation: NegotiationQuestionCandidate & { purpose: "inflight_consultation"; taskId: string; }; context: NegotiationInflightContext; } /** Negotiation-mode uptake generation input. */ export interface UptakeQuestionerInput extends QuestionerInputBase { mode: "negotiation"; purpose: "uptake"; negotiation: NegotiationQuestionCandidate & { purpose: "uptake"; taskId?: undefined; }; context: UptakeNegotiationContext; } /** Intent-mode post-discovery recovery generation input. */ export interface RecoveryQuestionerInput extends QuestionerInputBase { mode: "intent"; purpose: "recovery"; sourceType: "intent"; triggeredByIntentId: string; negotiation?: never; context: RecoveryIntentContext; } /** Top-level input discriminated by both mode and internal purpose. */ export type QuestionerInput = StandardQuestionerInput | PostStallQuestionerInput | InflightQuestionerInput | UptakeQuestionerInput | RecoveryQuestionerInput; /** Runtime mirror of the mode/purpose/context discriminant used at queue boundaries. */ export declare function isValidQuestionerInputContract(input: QuestionerInput): boolean; export {};