@indexnetwork/protocol 4.5.0-rc.324.1 → 4.5.0-rc.326.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/agent/agent.tools.js +1 -1
  2. package/dist/agent/agent.tools.js.map +1 -1
  3. package/dist/agent/tests/fakes.d.ts +1 -1
  4. package/dist/agent/tests/fakes.d.ts.map +1 -1
  5. package/dist/agent/tests/fakes.js +2 -2
  6. package/dist/agent/tests/fakes.js.map +1 -1
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/negotiation/negotiation.graph.js +2 -2
  12. package/dist/negotiation/negotiation.graph.js.map +1 -1
  13. package/dist/opportunity/feed/feed.graph.d.ts +17 -1
  14. package/dist/opportunity/feed/feed.graph.d.ts.map +1 -1
  15. package/dist/opportunity/feed/feed.graph.js +101 -4
  16. package/dist/opportunity/feed/feed.graph.js.map +1 -1
  17. package/dist/opportunity/feed/feed.state.d.ts +16 -0
  18. package/dist/opportunity/feed/feed.state.d.ts.map +1 -1
  19. package/dist/opportunity/feed/feed.state.js +10 -0
  20. package/dist/opportunity/feed/feed.state.js.map +1 -1
  21. package/dist/opportunity/opportunity.discover.d.ts +1 -1
  22. package/dist/opportunity/opportunity.discover.d.ts.map +1 -1
  23. package/dist/opportunity/opportunity.discover.js +6 -15
  24. package/dist/opportunity/opportunity.discover.js.map +1 -1
  25. package/dist/opportunity/opportunity.graph.d.ts +1 -1
  26. package/dist/opportunity/opportunity.graph.js +8 -8
  27. package/dist/opportunity/opportunity.graph.js.map +1 -1
  28. package/dist/opportunity/opportunity.tools.d.ts.map +1 -1
  29. package/dist/opportunity/opportunity.tools.js +5 -4
  30. package/dist/opportunity/opportunity.tools.js.map +1 -1
  31. package/dist/opportunity/opportunity.utils.d.ts +4 -1
  32. package/dist/opportunity/opportunity.utils.d.ts.map +1 -1
  33. package/dist/opportunity/opportunity.utils.js +13 -6
  34. package/dist/opportunity/opportunity.utils.js.map +1 -1
  35. package/dist/questioner/questioner.ask.tool.d.ts.map +1 -1
  36. package/dist/questioner/questioner.ask.tool.js +2 -10
  37. package/dist/questioner/questioner.ask.tool.js.map +1 -1
  38. package/dist/questioner/questioner.env.d.ts +50 -0
  39. package/dist/questioner/questioner.env.d.ts.map +1 -0
  40. package/dist/questioner/questioner.env.js +76 -0
  41. package/dist/questioner/questioner.env.js.map +1 -0
  42. package/dist/questioner/questioner.types.d.ts +7 -0
  43. package/dist/questioner/questioner.types.d.ts.map +1 -1
  44. package/dist/questioner/questioner.types.js.map +1 -1
  45. package/dist/shared/agent/tool.runtime.js +1 -1
  46. package/dist/shared/agent/tool.runtime.js.map +1 -1
  47. package/dist/shared/interfaces/agent-dispatcher.interface.d.ts +6 -5
  48. package/dist/shared/interfaces/agent-dispatcher.interface.d.ts.map +1 -1
  49. package/dist/shared/interfaces/agent-dispatcher.interface.js.map +1 -1
  50. package/dist/shared/interfaces/agent.interface.d.ts +3 -2
  51. package/dist/shared/interfaces/agent.interface.d.ts.map +1 -1
  52. package/dist/shared/interfaces/agent.interface.js.map +1 -1
  53. package/package.json +1 -1
@@ -81,7 +81,10 @@ export declare function canUserSeeOpportunity(actors: Array<{
81
81
  * (1) `latent`, no introducer → all actors actionable
82
82
  * (2) `latent`, introducer `approved !== true` → introducer only
83
83
  * (3) `latent`, introducer `approved === true` → all non-introducer actors
84
- * (4) `pending` (any introducer config) → non-introducer actors who have not acted
84
+ * (4) `pending` (any introducer config) → non-introducer actors who have not acted.
85
+ * Acting is per-user, not per-actor-row: re-detection can append duplicate
86
+ * actor rows for the same user without `actedAt`, so any viewer row with
87
+ * `actedAt` means the viewer has already acted.
85
88
  * (5) `accepted`/`rejected`/`expired`/`stalled`/`draft`/`negotiating`
86
89
  * → never actionable
87
90
  *
@@ -1 +1 @@
1
- {"version":3,"file":"opportunity.utils.d.ts","sourceRoot":"/","sources":["opportunity/opportunity.utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"opportunity.utils.d.ts","sourceRoot":"/","sources":["opportunity/opportunity.utils.ts"],"names":[],"mappings":"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"}
@@ -124,7 +124,10 @@ export function canUserSeeOpportunity(actors, status, userId) {
124
124
  * (1) `latent`, no introducer → all actors actionable
125
125
  * (2) `latent`, introducer `approved !== true` → introducer only
126
126
  * (3) `latent`, introducer `approved === true` → all non-introducer actors
127
- * (4) `pending` (any introducer config) → non-introducer actors who have not acted
127
+ * (4) `pending` (any introducer config) → non-introducer actors who have not acted.
128
+ * Acting is per-user, not per-actor-row: re-detection can append duplicate
129
+ * actor rows for the same user without `actedAt`, so any viewer row with
130
+ * `actedAt` means the viewer has already acted.
128
131
  * (5) `accepted`/`rejected`/`expired`/`stalled`/`draft`/`negotiating`
129
132
  * → never actionable
130
133
  *
@@ -137,10 +140,14 @@ export function isActionableForViewer(actors, status, viewerId) {
137
140
  const viewerActors = actors.filter((a) => a.userId === viewerId);
138
141
  if (viewerActors.length === 0)
139
142
  return false;
143
+ // Per-user acted signal: duplicate actor rows appended by re-detection may
144
+ // lack `actedAt` even though the viewer already accepted/rejected — a single
145
+ // stamped row means the viewer has acted on this opportunity.
146
+ const viewerActed = viewerActors.some((a) => !!a.actedAt);
140
147
  const introducer = actors.find((a) => a.role === 'introducer');
141
148
  const hasIntroducer = !!introducer;
142
149
  const introducerApproved = introducer?.approved === true;
143
- return viewerActors.some(({ role, actedAt }) => {
150
+ return viewerActors.some(({ role }) => {
144
151
  if (role === 'introducer') {
145
152
  // Rule 2: introducer sees own latent opp only while not yet approved.
146
153
  return status === 'latent' && !introducerApproved;
@@ -152,10 +159,10 @@ export function isActionableForViewer(actors, status, viewerId) {
152
159
  return !hasIntroducer || introducerApproved;
153
160
  }
154
161
  if (status === 'pending') {
155
- // Rule 4: pending is actionable only for actors who have not already
156
- // acted. Once an actor has `actedAt`, the opportunity is waiting on the
157
- // counterparty and should not appear in that actor's home feed.
158
- return !actedAt;
162
+ // Rule 4: pending is actionable only while the viewer has not acted.
163
+ // Once any of the viewer's actor rows has `actedAt`, the opportunity is
164
+ // waiting on the counterparty and should not appear in the viewer's feed.
165
+ return !viewerActed;
159
166
  }
160
167
  // Rule 5: never actionable at terminal or internal statuses.
161
168
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"opportunity.utils.js","sourceRoot":"/","sources":["opportunity/opportunity.utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Opportunity graph utilities: role derivation from corpus type.\n * Used by the opportunity graph to map lens corpus to opportunity actor roles.\n *\n * With lens-based HyDE, strategy selection is handled automatically by the\n * LensInferrer agent. This file provides corpus-to-role mapping for opportunity actors.\n */\n\nimport type { HydeTargetCorpus } from '../shared/hyde/lens.inferrer.js';\nimport { log } from '../shared/observability/log.js';\n\nconst logger = log.graph.from('SelectByComposition');\nconst dedupeByPersonLog = log.graph.from('DeduplicateByPerson');\nconst digestCandidatesLog = log.graph.from('SelectDigestCandidates');\n\n/** Actor roles in the opportunity model (agent / patient / peer). */\nexport type OpportunityActorRole = 'agent' | 'patient' | 'peer';\n\n/** Result of mapping a corpus to source and candidate roles. */\nexport interface DerivedRoles {\n sourceRole: OpportunityActorRole;\n candidateRole: OpportunityActorRole;\n}\n\n/**\n * Derive actor roles from the corpus type of a lens match.\n *\n * When a candidate is found via:\n * - \"profiles\" corpus → found by who they are → candidate can help → agent\n * - \"intents\" corpus → found by what they need → candidate needs something → patient\n *\n * @param corpus - The target corpus that produced the match ('profiles' | 'intents')\n * @returns Roles for the source (intent owner) and the candidate (matched user/intent)\n */\nexport function deriveRolesFromCorpus(corpus: HydeTargetCorpus): DerivedRoles {\n switch (corpus) {\n case 'profiles':\n // Source seeks someone who can help → source is patient, candidate can help → agent\n return { sourceRole: 'patient', candidateRole: 'agent' };\n case 'intents':\n // Source offers or needs; candidate has complementary goal → source is agent, candidate is patient\n return { sourceRole: 'agent', candidateRole: 'patient' };\n case 'premises':\n // Premise matches are symmetric: two people whose self-descriptions align.\n // Unlike intents (directional roles), premises express stable identity truths.\n return { sourceRole: 'peer', candidateRole: 'peer' };\n default:\n return { sourceRole: 'peer', candidateRole: 'peer' };\n }\n}\n\n/**\n * Validates opportunity actors: if an opportunity has an introducer, it must have\n * one or two non-introducer actors (1 = 1:1 intro e.g. \"I want to connect with X\";\n * 2 = introducer connecting two others).\n *\n * Also rejects self-matches — the same person occupying both sides of a\n * connection. The discovery/persist pipeline trusts the LLM evaluator's actor\n * list, which can collapse onto a single user; downstream readers then garble\n * identity (e.g. a connect link/greeting rendered in one party's voice while the\n * card shows the viewer \"matched with themselves\"). Two degenerate shapes are\n * blocked here, at the single persist chokepoint:\n * - every userId-bearing non-introducer actor collapses to the same user,\n * e.g. `[X(agent), X(patient)]`\n * - an introducer who is also a participant (\"Amina introduced you to Amina\")\n * Only `userId`-bearing actors are checked; role-only actors (legacy/tests) pass.\n * Duplicate rows for one participant are allowed when at least one other distinct\n * participant is present (some callers model multiple intents as multiple actor rows).\n *\n * @param actors - Array of actors with at least a role and optional userId\n * @throws Error when the actor set is invalid\n */\nexport function validateOpportunityActors(actors: Array<{ userId?: string; role: string }>): void {\n const introducerCount = actors.filter((a) => a.role === 'introducer').length;\n const nonIntroducerCount = actors.filter((a) => a.role !== 'introducer').length;\n\n if (introducerCount > 0 && (nonIntroducerCount < 1 || nonIntroducerCount > 2)) {\n throw new Error(\n 'An opportunity with an introducer must have one or two other actors.'\n );\n }\n\n // Self-match guard. Compare only actors that carry a userId so role-only\n // shapes (used by some callers/tests) are unaffected.\n const introducerUserIds = new Set(\n actors.filter((a) => a.role === 'introducer' && a.userId).map((a) => a.userId as string),\n );\n const nonIntroducerUserIds = actors\n .filter((a) => a.role !== 'introducer' && a.userId)\n .map((a) => a.userId as string);\n\n for (const userId of nonIntroducerUserIds) {\n if (introducerUserIds.has(userId)) {\n throw new Error(\n 'An opportunity actor cannot be both the introducer and a participant (self-match).'\n );\n }\n }\n\n const uniqueNonIntroducerUserIds = new Set(nonIntroducerUserIds);\n if (nonIntroducerUserIds.length > 1 && uniqueNonIntroducerUserIds.size === 1) {\n throw new Error('An opportunity cannot match a user with themselves (duplicate participant).');\n }\n}\n\n/**\n * Read-level ACL: whether a user is an actor on the opportunity and may fetch\n * its details. Intentionally broader than `isActionableForViewer` — a user can\n * read an opportunity they are not currently expected to act on (e.g. an agent\n * viewing an accepted opportunity).\n *\n * The feed graph and debug controller chain both predicates: an opportunity only\n * reaches the home feed if it passes `canUserSeeOpportunity` first, then\n * `isActionableForViewer`. For `agent with introducer at pending`,\n * `canUserSeeOpportunity` returns false (read gate blocks it), so the opportunity\n * never surfaces even though `isActionableForViewer` Rule 4 would return true in\n * isolation. This is by design — the agent is not granted read access through the\n * home path until the introducer path completes (negotiation → accepted).\n *\n * Compact Visibility Rule (from lifecycle doc):\n * - Introducer or peer: always see.\n * - Patient or party: see if (status is not latent, or there is no introducer).\n * - Agent: see if (status is accepted/rejected/expired, or (status is not latent and there is no introducer)).\n */\nexport function canUserSeeOpportunity(\n actors: Array<{ userId: string; role: string }>,\n status: string,\n userId: string\n): boolean {\n const hasIntroducer = actors.some((a) => a.role === 'introducer');\n const userRoles = actors.filter((a) => a.userId === userId).map((a) => a.role);\n if (userRoles.length === 0) return false;\n\n return userRoles.some((role) => {\n if (role === 'introducer') return true;\n if (role === 'peer') return true;\n if (role === 'patient' || role === 'party')\n return status !== 'latent' || !hasIntroducer;\n if (role === 'agent')\n return (\n ['accepted', 'rejected', 'expired'].includes(status) ||\n (status !== 'latent' && !hasIntroducer)\n );\n return false;\n });\n}\n\n/**\n * Whether an opportunity should appear on the viewer's home feed (actionable =\n * has a pending action for this user).\n *\n * Rules (see `docs/Latent Opportunity Lifecycle.md` — Role-Visibility Matrix):\n *\n * (1) `latent`, no introducer → all actors actionable\n * (2) `latent`, introducer `approved !== true` → introducer only\n * (3) `latent`, introducer `approved === true` → all non-introducer actors\n * (4) `pending` (any introducer config) → non-introducer actors who have not acted\n * (5) `accepted`/`rejected`/`expired`/`stalled`/`draft`/`negotiating`\n * → never actionable\n *\n * The introducer approval signal is stored on the `introducer`-roled actor's\n * `approved: boolean` field within the opportunity's `actors` JSONB. It flips\n * from `false` to `true` when the introducer approves; status stays `latent`\n * across the flip while a background negotiation runs.\n */\nexport function isActionableForViewer(\n actors: Array<{ userId: string; role: string; approved?: boolean; actedAt?: string | null }>,\n status: string,\n viewerId: string\n): boolean {\n const viewerActors = actors.filter((a) => a.userId === viewerId);\n if (viewerActors.length === 0) return false;\n\n const introducer = actors.find((a) => a.role === 'introducer');\n const hasIntroducer = !!introducer;\n const introducerApproved = introducer?.approved === true;\n\n return viewerActors.some(({ role, actedAt }) => {\n if (role === 'introducer') {\n // Rule 2: introducer sees own latent opp only while not yet approved.\n return status === 'latent' && !introducerApproved;\n }\n\n // Non-introducer actors: patient / party / agent / peer.\n if (status === 'latent') {\n // Rule 1: no introducer → visible.\n // Rule 3: introducer approved → visible.\n return !hasIntroducer || introducerApproved;\n }\n if (status === 'pending') {\n // Rule 4: pending is actionable only for actors who have not already\n // acted. Once an actor has `actedAt`, the opportunity is waiting on the\n // counterparty and should not appear in that actor's home feed.\n return !actedAt;\n }\n // Rule 5: never actionable at terminal or internal statuses.\n return false;\n });\n}\n\n/** Feed category for home composition. */\nexport type FeedCategory = 'connection' | 'connector-flow' | 'expired';\n\n/** Soft targets for home feed composition. */\nexport const FEED_SOFT_TARGETS = {\n connection: 3,\n connectorFlow: 2,\n expired: 2,\n} as const;\n\n/**\n * Classify an actionable opportunity into a feed category.\n * Assumes the opportunity already passed isActionableForViewer or is expired.\n *\n * @param opp - Opportunity with actors and status\n * @param viewerId - The viewing user's ID\n * @returns Feed category\n */\nexport function classifyOpportunity(\n opp: { actors: Array<{ userId: string; role: string }>; status: string },\n viewerId: string\n): FeedCategory {\n if (opp.status === 'expired') return 'expired';\n const viewerIsIntroducer = opp.actors.some((a) => a.userId === viewerId && a.role === 'introducer');\n if (viewerIsIntroducer) return 'connector-flow';\n return 'connection';\n}\n\n/**\n * Select opportunities for the home feed using soft composition targets.\n * Fills each category up to its target, then redistributes unused slots\n * to categories that have more items available. Preserves input order.\n *\n * @param opportunities - Pre-sorted opportunities (by confidence/recency)\n * @param viewerId - The viewing user's ID\n * @returns Composition-balanced subset\n */\nexport function selectByComposition<T extends { actors: Array<{ userId: string; role: string }>; status: string }>(\n opportunities: T[],\n viewerId: string\n): T[] {\n const buckets: Record<FeedCategory, T[]> = {\n connection: [],\n 'connector-flow': [],\n expired: [],\n };\n\n for (const opp of opportunities) {\n const category = classifyOpportunity(opp, viewerId);\n buckets[category].push(opp);\n }\n\n const targets: Record<FeedCategory, number> = {\n connection: FEED_SOFT_TARGETS.connection,\n 'connector-flow': FEED_SOFT_TARGETS.connectorFlow,\n expired: FEED_SOFT_TARGETS.expired,\n };\n\n // First pass: fill each category up to its target\n const selected: Record<FeedCategory, T[]> = {\n connection: buckets.connection.slice(0, targets.connection),\n 'connector-flow': buckets['connector-flow'].slice(0, targets['connector-flow']),\n expired: buckets.expired.slice(0, targets.expired),\n };\n\n // Calculate unused slots and remaining items\n const totalTarget = targets.connection + targets['connector-flow'] + targets.expired;\n const usedSlots = selected.connection.length + selected['connector-flow'].length + selected.expired.length;\n let unusedSlots = totalTarget - usedSlots;\n\n // Second pass: redistribute unused slots to categories with remaining items\n // Priority: connection > connector-flow > expired\n const redistOrder: FeedCategory[] = ['connection', 'connector-flow', 'expired'];\n for (const category of redistOrder) {\n if (unusedSlots <= 0) break;\n const remaining = buckets[category].slice(selected[category].length);\n const take = Math.min(remaining.length, unusedSlots);\n selected[category].push(...remaining.slice(0, take));\n unusedSlots -= take;\n }\n\n // Merge in category priority order: connection > connector-flow > expired\n // Within each category, preserve original input order\n const indexMap = new Map(opportunities.map((opp, i) => [opp, i]));\n const sortByOriginal = (a: T, b: T) => (indexMap.get(a) ?? 0) - (indexMap.get(b) ?? 0);\n selected.connection.sort(sortByOriginal);\n selected['connector-flow'].sort(sortByOriginal);\n selected.expired.sort(sortByOriginal);\n\n logger.info('Selected opportunities by composition', {\n input: opportunities.length,\n buckets: {\n connection: buckets.connection.length,\n connectorFlow: buckets['connector-flow'].length,\n expired: buckets.expired.length,\n },\n selected: {\n connection: selected.connection.length,\n connectorFlow: selected['connector-flow'].length,\n expired: selected.expired.length,\n },\n });\n\n return [\n ...selected.connection,\n ...selected['connector-flow'],\n ...selected.expired,\n ];\n}\n\n/**\n * Deduplicate opportunities so each counterpart appears at most once.\n * Keeps the opportunity with the highest interpretation.confidence per\n * counterpart userId. On ties, the first encountered wins (stable).\n *\n * Counterpart = first actor whose userId !== viewerId and role !== 'introducer'.\n * Opportunities without a derivable counterpart pass through undeduped.\n *\n * @param opportunities - Pre-sorted opportunities (e.g. by confidence/recency)\n * @param viewerId - The viewing user's ID\n * @returns Deduped subset preserving original input order among winners\n */\nexport function deduplicateByPerson<T extends {\n actors: Array<{ userId: string; role: string }>;\n interpretation?: { confidence?: number } | null;\n}>(opportunities: T[], viewerId: string): T[] {\n const bestByCounterpart = new Map<string, { opp: T; index: number }>();\n const noCounterpart: Array<{ opp: T; index: number }> = [];\n\n for (let i = 0; i < opportunities.length; i++) {\n const opp = opportunities[i];\n const counterpart = opp.actors.find(\n (a) => a.userId !== viewerId && a.role !== 'introducer',\n );\n\n if (!counterpart) {\n noCounterpart.push({ opp, index: i });\n continue;\n }\n\n const key = counterpart.userId;\n const existing = bestByCounterpart.get(key);\n\n if (!existing) {\n bestByCounterpart.set(key, { opp, index: i });\n continue;\n }\n\n const newConf = opp.interpretation?.confidence ?? -1;\n const oldConf = existing.opp.interpretation?.confidence ?? -1;\n if (newConf > oldConf) {\n bestByCounterpart.set(key, { opp, index: i });\n }\n }\n\n const all = [...bestByCounterpart.values(), ...noCounterpart];\n all.sort((a, b) => a.index - b.index);\n\n const result = all.map((entry) => entry.opp);\n if (result.length < opportunities.length) {\n dedupeByPersonLog.info('Deduped opportunities by person', {\n input: opportunities.length,\n output: result.length,\n });\n }\n return result;\n}\n\n/**\n * Days a digest-delivered opportunity stays suppressed before it becomes\n * eligible for a \"still open\" reminder re-show (when nothing fresh exists).\n */\nexport const DIGEST_REDELIVERY_COOLDOWN_DAYS = 5;\n\n/** Committed delivery row shape consumed by {@link selectDigestCandidates}. */\nexport interface DigestDeliveredRow {\n opportunityId: string;\n deliveredAtStatus: string;\n deliveredAt: Date;\n}\n\n/**\n * Cross-day digest suppression for scheduled-brief candidates.\n *\n * Three rules, applied in order:\n * 1. **Accepted-counterpart suppression** — a direct-connection candidate whose\n * counterpart the viewer has already connected with (an `accepted`\n * opportunity exists with that person) is dropped permanently. A new\n * discovery run re-minting the same person must not resurface them.\n * Connector-flow candidates (viewer is the introducer) are exempt: being\n * connected with someone doesn't make an intro ask on their behalf stale.\n * 2. **Delivery-ledger dedup** — candidates with a committed delivery row at\n * the same `(opportunityId, status)` key have already been shown. While any\n * fresh (never-shown) candidate exists, shown ones are dropped entirely.\n * 3. **Cooldown re-show** — when *no* fresh candidate survives, already-shown\n * candidates whose latest delivery is at least `cooldownDays` old are\n * returned instead, least-recently-shown first, flagged via\n * `redeliveryIds` so the digest can frame them as reminders.\n *\n * Pure function — callers fetch accepted counterparts and ledger rows.\n *\n * @param candidates - Deduped, confidence-ordered digest candidates.\n * @param opts.viewerId - The digest recipient.\n * @param opts.acceptedCounterpartIds - userIds the viewer already connected with.\n * @param opts.deliveredRows - Committed ledger rows for the candidate ids.\n * @param opts.now - Clock override for tests.\n * @param opts.cooldownDays - Cooldown override (default {@link DIGEST_REDELIVERY_COOLDOWN_DAYS}).\n * @returns Surviving pool plus the set of candidate ids that are cooldown re-shows.\n */\nexport function selectDigestCandidates<T extends {\n id: string;\n status: string;\n actors: Array<{ userId: string; role: string }>;\n}>(\n candidates: T[],\n opts: {\n viewerId: string;\n acceptedCounterpartIds: ReadonlySet<string>;\n deliveredRows: DigestDeliveredRow[];\n now?: Date;\n cooldownDays?: number;\n },\n): { pool: T[]; redeliveryIds: Set<string> } {\n const { viewerId, acceptedCounterpartIds } = opts;\n\n // Rule 1: accepted-counterpart suppression (direct connections only).\n const afterAccepted = candidates.filter((opp) => {\n const viewerIsIntroducer = opp.actors.some(\n (a) => a.role === 'introducer' && a.userId === viewerId,\n );\n if (viewerIsIntroducer) return true;\n const counterpart = opp.actors.find(\n (a) => a.userId !== viewerId && a.role !== 'introducer',\n );\n return !counterpart || !acceptedCounterpartIds.has(counterpart.userId);\n });\n if (afterAccepted.length < candidates.length) {\n digestCandidatesLog.info('Accepted-counterpart suppression dropped candidates', {\n dropped: candidates.length - afterAccepted.length,\n total: candidates.length,\n });\n }\n\n // Rule 2: delivery-ledger dedup keyed (opportunityId, deliveredAtStatus).\n // Keep the LATEST committed delivery per key — cooldown measures time since\n // the user last saw the card, not since they first saw it.\n const lastDeliveredByKey = new Map<string, Date>();\n for (const row of opts.deliveredRows) {\n if (!(row.deliveredAt instanceof Date) || Number.isNaN(row.deliveredAt.getTime())) continue;\n const key = `${row.opportunityId}:${row.deliveredAtStatus}`;\n const existing = lastDeliveredByKey.get(key);\n if (!existing || row.deliveredAt > existing) lastDeliveredByKey.set(key, row.deliveredAt);\n }\n\n const fresh = afterAccepted.filter((opp) => !lastDeliveredByKey.has(`${opp.id}:${opp.status}`));\n if (fresh.length > 0) {\n if (fresh.length < afterAccepted.length) {\n digestCandidatesLog.info('Ledger dedup dropped already-shown candidates', {\n dropped: afterAccepted.length - fresh.length,\n });\n }\n return { pool: fresh, redeliveryIds: new Set<string>() };\n }\n\n // Rule 3: nothing fresh — re-show the least-recently-shown candidates past cooldown.\n const cooldownMs = (opts.cooldownDays ?? DIGEST_REDELIVERY_COOLDOWN_DAYS) * 86_400_000;\n const now = opts.now ?? new Date();\n const cooled = afterAccepted\n .map((opp) => ({ opp, at: lastDeliveredByKey.get(`${opp.id}:${opp.status}`) }))\n .filter((entry): entry is { opp: T; at: Date } =>\n entry.at instanceof Date && now.getTime() - entry.at.getTime() >= cooldownMs,\n )\n .sort((a, b) => a.at.getTime() - b.at.getTime());\n\n if (cooled.length > 0) {\n digestCandidatesLog.info('No fresh candidates — re-showing past-cooldown candidates', {\n count: cooled.length,\n });\n }\n return {\n pool: cooled.map((entry) => entry.opp),\n redeliveryIds: new Set(cooled.map((entry) => entry.opp.id)),\n };\n}\n"]}
1
+ {"version":3,"file":"opportunity.utils.js","sourceRoot":"/","sources":["opportunity/opportunity.utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Opportunity graph utilities: role derivation from corpus type.\n * Used by the opportunity graph to map lens corpus to opportunity actor roles.\n *\n * With lens-based HyDE, strategy selection is handled automatically by the\n * LensInferrer agent. This file provides corpus-to-role mapping for opportunity actors.\n */\n\nimport type { HydeTargetCorpus } from '../shared/hyde/lens.inferrer.js';\nimport { log } from '../shared/observability/log.js';\n\nconst logger = log.graph.from('SelectByComposition');\nconst dedupeByPersonLog = log.graph.from('DeduplicateByPerson');\nconst digestCandidatesLog = log.graph.from('SelectDigestCandidates');\n\n/** Actor roles in the opportunity model (agent / patient / peer). */\nexport type OpportunityActorRole = 'agent' | 'patient' | 'peer';\n\n/** Result of mapping a corpus to source and candidate roles. */\nexport interface DerivedRoles {\n sourceRole: OpportunityActorRole;\n candidateRole: OpportunityActorRole;\n}\n\n/**\n * Derive actor roles from the corpus type of a lens match.\n *\n * When a candidate is found via:\n * - \"profiles\" corpus → found by who they are → candidate can help → agent\n * - \"intents\" corpus → found by what they need → candidate needs something → patient\n *\n * @param corpus - The target corpus that produced the match ('profiles' | 'intents')\n * @returns Roles for the source (intent owner) and the candidate (matched user/intent)\n */\nexport function deriveRolesFromCorpus(corpus: HydeTargetCorpus): DerivedRoles {\n switch (corpus) {\n case 'profiles':\n // Source seeks someone who can help → source is patient, candidate can help → agent\n return { sourceRole: 'patient', candidateRole: 'agent' };\n case 'intents':\n // Source offers or needs; candidate has complementary goal → source is agent, candidate is patient\n return { sourceRole: 'agent', candidateRole: 'patient' };\n case 'premises':\n // Premise matches are symmetric: two people whose self-descriptions align.\n // Unlike intents (directional roles), premises express stable identity truths.\n return { sourceRole: 'peer', candidateRole: 'peer' };\n default:\n return { sourceRole: 'peer', candidateRole: 'peer' };\n }\n}\n\n/**\n * Validates opportunity actors: if an opportunity has an introducer, it must have\n * one or two non-introducer actors (1 = 1:1 intro e.g. \"I want to connect with X\";\n * 2 = introducer connecting two others).\n *\n * Also rejects self-matches — the same person occupying both sides of a\n * connection. The discovery/persist pipeline trusts the LLM evaluator's actor\n * list, which can collapse onto a single user; downstream readers then garble\n * identity (e.g. a connect link/greeting rendered in one party's voice while the\n * card shows the viewer \"matched with themselves\"). Two degenerate shapes are\n * blocked here, at the single persist chokepoint:\n * - every userId-bearing non-introducer actor collapses to the same user,\n * e.g. `[X(agent), X(patient)]`\n * - an introducer who is also a participant (\"Amina introduced you to Amina\")\n * Only `userId`-bearing actors are checked; role-only actors (legacy/tests) pass.\n * Duplicate rows for one participant are allowed when at least one other distinct\n * participant is present (some callers model multiple intents as multiple actor rows).\n *\n * @param actors - Array of actors with at least a role and optional userId\n * @throws Error when the actor set is invalid\n */\nexport function validateOpportunityActors(actors: Array<{ userId?: string; role: string }>): void {\n const introducerCount = actors.filter((a) => a.role === 'introducer').length;\n const nonIntroducerCount = actors.filter((a) => a.role !== 'introducer').length;\n\n if (introducerCount > 0 && (nonIntroducerCount < 1 || nonIntroducerCount > 2)) {\n throw new Error(\n 'An opportunity with an introducer must have one or two other actors.'\n );\n }\n\n // Self-match guard. Compare only actors that carry a userId so role-only\n // shapes (used by some callers/tests) are unaffected.\n const introducerUserIds = new Set(\n actors.filter((a) => a.role === 'introducer' && a.userId).map((a) => a.userId as string),\n );\n const nonIntroducerUserIds = actors\n .filter((a) => a.role !== 'introducer' && a.userId)\n .map((a) => a.userId as string);\n\n for (const userId of nonIntroducerUserIds) {\n if (introducerUserIds.has(userId)) {\n throw new Error(\n 'An opportunity actor cannot be both the introducer and a participant (self-match).'\n );\n }\n }\n\n const uniqueNonIntroducerUserIds = new Set(nonIntroducerUserIds);\n if (nonIntroducerUserIds.length > 1 && uniqueNonIntroducerUserIds.size === 1) {\n throw new Error('An opportunity cannot match a user with themselves (duplicate participant).');\n }\n}\n\n/**\n * Read-level ACL: whether a user is an actor on the opportunity and may fetch\n * its details. Intentionally broader than `isActionableForViewer` — a user can\n * read an opportunity they are not currently expected to act on (e.g. an agent\n * viewing an accepted opportunity).\n *\n * The feed graph and debug controller chain both predicates: an opportunity only\n * reaches the home feed if it passes `canUserSeeOpportunity` first, then\n * `isActionableForViewer`. For `agent with introducer at pending`,\n * `canUserSeeOpportunity` returns false (read gate blocks it), so the opportunity\n * never surfaces even though `isActionableForViewer` Rule 4 would return true in\n * isolation. This is by design — the agent is not granted read access through the\n * home path until the introducer path completes (negotiation → accepted).\n *\n * Compact Visibility Rule (from lifecycle doc):\n * - Introducer or peer: always see.\n * - Patient or party: see if (status is not latent, or there is no introducer).\n * - Agent: see if (status is accepted/rejected/expired, or (status is not latent and there is no introducer)).\n */\nexport function canUserSeeOpportunity(\n actors: Array<{ userId: string; role: string }>,\n status: string,\n userId: string\n): boolean {\n const hasIntroducer = actors.some((a) => a.role === 'introducer');\n const userRoles = actors.filter((a) => a.userId === userId).map((a) => a.role);\n if (userRoles.length === 0) return false;\n\n return userRoles.some((role) => {\n if (role === 'introducer') return true;\n if (role === 'peer') return true;\n if (role === 'patient' || role === 'party')\n return status !== 'latent' || !hasIntroducer;\n if (role === 'agent')\n return (\n ['accepted', 'rejected', 'expired'].includes(status) ||\n (status !== 'latent' && !hasIntroducer)\n );\n return false;\n });\n}\n\n/**\n * Whether an opportunity should appear on the viewer's home feed (actionable =\n * has a pending action for this user).\n *\n * Rules (see `docs/Latent Opportunity Lifecycle.md` — Role-Visibility Matrix):\n *\n * (1) `latent`, no introducer → all actors actionable\n * (2) `latent`, introducer `approved !== true` → introducer only\n * (3) `latent`, introducer `approved === true` → all non-introducer actors\n * (4) `pending` (any introducer config) → non-introducer actors who have not acted.\n * Acting is per-user, not per-actor-row: re-detection can append duplicate\n * actor rows for the same user without `actedAt`, so any viewer row with\n * `actedAt` means the viewer has already acted.\n * (5) `accepted`/`rejected`/`expired`/`stalled`/`draft`/`negotiating`\n * → never actionable\n *\n * The introducer approval signal is stored on the `introducer`-roled actor's\n * `approved: boolean` field within the opportunity's `actors` JSONB. It flips\n * from `false` to `true` when the introducer approves; status stays `latent`\n * across the flip while a background negotiation runs.\n */\nexport function isActionableForViewer(\n actors: Array<{ userId: string; role: string; approved?: boolean; actedAt?: string | null }>,\n status: string,\n viewerId: string\n): boolean {\n const viewerActors = actors.filter((a) => a.userId === viewerId);\n if (viewerActors.length === 0) return false;\n\n // Per-user acted signal: duplicate actor rows appended by re-detection may\n // lack `actedAt` even though the viewer already accepted/rejected — a single\n // stamped row means the viewer has acted on this opportunity.\n const viewerActed = viewerActors.some((a) => !!a.actedAt);\n\n const introducer = actors.find((a) => a.role === 'introducer');\n const hasIntroducer = !!introducer;\n const introducerApproved = introducer?.approved === true;\n\n return viewerActors.some(({ role }) => {\n if (role === 'introducer') {\n // Rule 2: introducer sees own latent opp only while not yet approved.\n return status === 'latent' && !introducerApproved;\n }\n\n // Non-introducer actors: patient / party / agent / peer.\n if (status === 'latent') {\n // Rule 1: no introducer → visible.\n // Rule 3: introducer approved → visible.\n return !hasIntroducer || introducerApproved;\n }\n if (status === 'pending') {\n // Rule 4: pending is actionable only while the viewer has not acted.\n // Once any of the viewer's actor rows has `actedAt`, the opportunity is\n // waiting on the counterparty and should not appear in the viewer's feed.\n return !viewerActed;\n }\n // Rule 5: never actionable at terminal or internal statuses.\n return false;\n });\n}\n\n/** Feed category for home composition. */\nexport type FeedCategory = 'connection' | 'connector-flow' | 'expired';\n\n/** Soft targets for home feed composition. */\nexport const FEED_SOFT_TARGETS = {\n connection: 3,\n connectorFlow: 2,\n expired: 2,\n} as const;\n\n/**\n * Classify an actionable opportunity into a feed category.\n * Assumes the opportunity already passed isActionableForViewer or is expired.\n *\n * @param opp - Opportunity with actors and status\n * @param viewerId - The viewing user's ID\n * @returns Feed category\n */\nexport function classifyOpportunity(\n opp: { actors: Array<{ userId: string; role: string }>; status: string },\n viewerId: string\n): FeedCategory {\n if (opp.status === 'expired') return 'expired';\n const viewerIsIntroducer = opp.actors.some((a) => a.userId === viewerId && a.role === 'introducer');\n if (viewerIsIntroducer) return 'connector-flow';\n return 'connection';\n}\n\n/**\n * Select opportunities for the home feed using soft composition targets.\n * Fills each category up to its target, then redistributes unused slots\n * to categories that have more items available. Preserves input order.\n *\n * @param opportunities - Pre-sorted opportunities (by confidence/recency)\n * @param viewerId - The viewing user's ID\n * @returns Composition-balanced subset\n */\nexport function selectByComposition<T extends { actors: Array<{ userId: string; role: string }>; status: string }>(\n opportunities: T[],\n viewerId: string\n): T[] {\n const buckets: Record<FeedCategory, T[]> = {\n connection: [],\n 'connector-flow': [],\n expired: [],\n };\n\n for (const opp of opportunities) {\n const category = classifyOpportunity(opp, viewerId);\n buckets[category].push(opp);\n }\n\n const targets: Record<FeedCategory, number> = {\n connection: FEED_SOFT_TARGETS.connection,\n 'connector-flow': FEED_SOFT_TARGETS.connectorFlow,\n expired: FEED_SOFT_TARGETS.expired,\n };\n\n // First pass: fill each category up to its target\n const selected: Record<FeedCategory, T[]> = {\n connection: buckets.connection.slice(0, targets.connection),\n 'connector-flow': buckets['connector-flow'].slice(0, targets['connector-flow']),\n expired: buckets.expired.slice(0, targets.expired),\n };\n\n // Calculate unused slots and remaining items\n const totalTarget = targets.connection + targets['connector-flow'] + targets.expired;\n const usedSlots = selected.connection.length + selected['connector-flow'].length + selected.expired.length;\n let unusedSlots = totalTarget - usedSlots;\n\n // Second pass: redistribute unused slots to categories with remaining items\n // Priority: connection > connector-flow > expired\n const redistOrder: FeedCategory[] = ['connection', 'connector-flow', 'expired'];\n for (const category of redistOrder) {\n if (unusedSlots <= 0) break;\n const remaining = buckets[category].slice(selected[category].length);\n const take = Math.min(remaining.length, unusedSlots);\n selected[category].push(...remaining.slice(0, take));\n unusedSlots -= take;\n }\n\n // Merge in category priority order: connection > connector-flow > expired\n // Within each category, preserve original input order\n const indexMap = new Map(opportunities.map((opp, i) => [opp, i]));\n const sortByOriginal = (a: T, b: T) => (indexMap.get(a) ?? 0) - (indexMap.get(b) ?? 0);\n selected.connection.sort(sortByOriginal);\n selected['connector-flow'].sort(sortByOriginal);\n selected.expired.sort(sortByOriginal);\n\n logger.info('Selected opportunities by composition', {\n input: opportunities.length,\n buckets: {\n connection: buckets.connection.length,\n connectorFlow: buckets['connector-flow'].length,\n expired: buckets.expired.length,\n },\n selected: {\n connection: selected.connection.length,\n connectorFlow: selected['connector-flow'].length,\n expired: selected.expired.length,\n },\n });\n\n return [\n ...selected.connection,\n ...selected['connector-flow'],\n ...selected.expired,\n ];\n}\n\n/**\n * Deduplicate opportunities so each counterpart appears at most once.\n * Keeps the opportunity with the highest interpretation.confidence per\n * counterpart userId. On ties, the first encountered wins (stable).\n *\n * Counterpart = first actor whose userId !== viewerId and role !== 'introducer'.\n * Opportunities without a derivable counterpart pass through undeduped.\n *\n * @param opportunities - Pre-sorted opportunities (e.g. by confidence/recency)\n * @param viewerId - The viewing user's ID\n * @returns Deduped subset preserving original input order among winners\n */\nexport function deduplicateByPerson<T extends {\n actors: Array<{ userId: string; role: string }>;\n interpretation?: { confidence?: number } | null;\n}>(opportunities: T[], viewerId: string): T[] {\n const bestByCounterpart = new Map<string, { opp: T; index: number }>();\n const noCounterpart: Array<{ opp: T; index: number }> = [];\n\n for (let i = 0; i < opportunities.length; i++) {\n const opp = opportunities[i];\n const counterpart = opp.actors.find(\n (a) => a.userId !== viewerId && a.role !== 'introducer',\n );\n\n if (!counterpart) {\n noCounterpart.push({ opp, index: i });\n continue;\n }\n\n const key = counterpart.userId;\n const existing = bestByCounterpart.get(key);\n\n if (!existing) {\n bestByCounterpart.set(key, { opp, index: i });\n continue;\n }\n\n const newConf = opp.interpretation?.confidence ?? -1;\n const oldConf = existing.opp.interpretation?.confidence ?? -1;\n if (newConf > oldConf) {\n bestByCounterpart.set(key, { opp, index: i });\n }\n }\n\n const all = [...bestByCounterpart.values(), ...noCounterpart];\n all.sort((a, b) => a.index - b.index);\n\n const result = all.map((entry) => entry.opp);\n if (result.length < opportunities.length) {\n dedupeByPersonLog.info('Deduped opportunities by person', {\n input: opportunities.length,\n output: result.length,\n });\n }\n return result;\n}\n\n/**\n * Days a digest-delivered opportunity stays suppressed before it becomes\n * eligible for a \"still open\" reminder re-show (when nothing fresh exists).\n */\nexport const DIGEST_REDELIVERY_COOLDOWN_DAYS = 5;\n\n/** Committed delivery row shape consumed by {@link selectDigestCandidates}. */\nexport interface DigestDeliveredRow {\n opportunityId: string;\n deliveredAtStatus: string;\n deliveredAt: Date;\n}\n\n/**\n * Cross-day digest suppression for scheduled-brief candidates.\n *\n * Three rules, applied in order:\n * 1. **Accepted-counterpart suppression** — a direct-connection candidate whose\n * counterpart the viewer has already connected with (an `accepted`\n * opportunity exists with that person) is dropped permanently. A new\n * discovery run re-minting the same person must not resurface them.\n * Connector-flow candidates (viewer is the introducer) are exempt: being\n * connected with someone doesn't make an intro ask on their behalf stale.\n * 2. **Delivery-ledger dedup** — candidates with a committed delivery row at\n * the same `(opportunityId, status)` key have already been shown. While any\n * fresh (never-shown) candidate exists, shown ones are dropped entirely.\n * 3. **Cooldown re-show** — when *no* fresh candidate survives, already-shown\n * candidates whose latest delivery is at least `cooldownDays` old are\n * returned instead, least-recently-shown first, flagged via\n * `redeliveryIds` so the digest can frame them as reminders.\n *\n * Pure function — callers fetch accepted counterparts and ledger rows.\n *\n * @param candidates - Deduped, confidence-ordered digest candidates.\n * @param opts.viewerId - The digest recipient.\n * @param opts.acceptedCounterpartIds - userIds the viewer already connected with.\n * @param opts.deliveredRows - Committed ledger rows for the candidate ids.\n * @param opts.now - Clock override for tests.\n * @param opts.cooldownDays - Cooldown override (default {@link DIGEST_REDELIVERY_COOLDOWN_DAYS}).\n * @returns Surviving pool plus the set of candidate ids that are cooldown re-shows.\n */\nexport function selectDigestCandidates<T extends {\n id: string;\n status: string;\n actors: Array<{ userId: string; role: string }>;\n}>(\n candidates: T[],\n opts: {\n viewerId: string;\n acceptedCounterpartIds: ReadonlySet<string>;\n deliveredRows: DigestDeliveredRow[];\n now?: Date;\n cooldownDays?: number;\n },\n): { pool: T[]; redeliveryIds: Set<string> } {\n const { viewerId, acceptedCounterpartIds } = opts;\n\n // Rule 1: accepted-counterpart suppression (direct connections only).\n const afterAccepted = candidates.filter((opp) => {\n const viewerIsIntroducer = opp.actors.some(\n (a) => a.role === 'introducer' && a.userId === viewerId,\n );\n if (viewerIsIntroducer) return true;\n const counterpart = opp.actors.find(\n (a) => a.userId !== viewerId && a.role !== 'introducer',\n );\n return !counterpart || !acceptedCounterpartIds.has(counterpart.userId);\n });\n if (afterAccepted.length < candidates.length) {\n digestCandidatesLog.info('Accepted-counterpart suppression dropped candidates', {\n dropped: candidates.length - afterAccepted.length,\n total: candidates.length,\n });\n }\n\n // Rule 2: delivery-ledger dedup keyed (opportunityId, deliveredAtStatus).\n // Keep the LATEST committed delivery per key — cooldown measures time since\n // the user last saw the card, not since they first saw it.\n const lastDeliveredByKey = new Map<string, Date>();\n for (const row of opts.deliveredRows) {\n if (!(row.deliveredAt instanceof Date) || Number.isNaN(row.deliveredAt.getTime())) continue;\n const key = `${row.opportunityId}:${row.deliveredAtStatus}`;\n const existing = lastDeliveredByKey.get(key);\n if (!existing || row.deliveredAt > existing) lastDeliveredByKey.set(key, row.deliveredAt);\n }\n\n const fresh = afterAccepted.filter((opp) => !lastDeliveredByKey.has(`${opp.id}:${opp.status}`));\n if (fresh.length > 0) {\n if (fresh.length < afterAccepted.length) {\n digestCandidatesLog.info('Ledger dedup dropped already-shown candidates', {\n dropped: afterAccepted.length - fresh.length,\n });\n }\n return { pool: fresh, redeliveryIds: new Set<string>() };\n }\n\n // Rule 3: nothing fresh — re-show the least-recently-shown candidates past cooldown.\n const cooldownMs = (opts.cooldownDays ?? DIGEST_REDELIVERY_COOLDOWN_DAYS) * 86_400_000;\n const now = opts.now ?? new Date();\n const cooled = afterAccepted\n .map((opp) => ({ opp, at: lastDeliveredByKey.get(`${opp.id}:${opp.status}`) }))\n .filter((entry): entry is { opp: T; at: Date } =>\n entry.at instanceof Date && now.getTime() - entry.at.getTime() >= cooldownMs,\n )\n .sort((a, b) => a.at.getTime() - b.at.getTime());\n\n if (cooled.length > 0) {\n digestCandidatesLog.info('No fresh candidates — re-showing past-cooldown candidates', {\n count: cooled.length,\n });\n }\n return {\n pool: cooled.map((entry) => entry.opp),\n redeliveryIds: new Set(cooled.map((entry) => entry.opp.id)),\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"questioner.ask.tool.d.ts","sourceRoot":"/","sources":["questioner/questioner.ask.tool.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAM5E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAsCxD,8EAA8E;AAC9E,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,GAAG,IAAI,CAEhF;AAqCD;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,kBAsMhF"}
1
+ {"version":3,"file":"questioner.ask.tool.d.ts","sourceRoot":"/","sources":["questioner/questioner.ask.tool.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAM5E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AA8BxD,8EAA8E;AAC9E,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,GAAG,IAAI,CAEhF;AAqCD;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,kBAsMhF"}
@@ -29,9 +29,8 @@ import { error, success } from "../shared/agent/tool.helpers.js";
29
29
  import { requestContext } from "../shared/observability/request-context.js";
30
30
  import { protocolLogger } from "../shared/observability/protocol.logger.js";
31
31
  import { QuestionerAgent } from "./questioner.agent.js";
32
+ import { chatQuestionWaitTimeoutMs } from "./questioner.env.js";
32
33
  const logger = protocolLogger("AskUserQuestionTool");
33
- /** Default in-tool wait budget for the user's answer (4 minutes). */
34
- const DEFAULT_WAIT_TIMEOUT_MS = 4 * 60 * 1000;
35
34
  /** Heartbeat interval while blocked, so SSE transports do not idle out. */
36
35
  const WAIT_HEARTBEAT_MS = 15000;
37
36
  /** Messages included in the conversation excerpt fed to the QuestionerAgent. */
@@ -44,13 +43,6 @@ const EXCERPT_MESSAGE_COUNT = 10;
44
43
  const EXCERPT_FETCH_LIMIT = 100;
45
44
  /** Max characters per message inside the excerpt. */
46
45
  const EXCERPT_MESSAGE_CHARS = 400;
47
- function waitTimeoutMs() {
48
- const raw = process.env.CHAT_QUESTION_WAIT_TIMEOUT_MS;
49
- if (!raw)
50
- return DEFAULT_WAIT_TIMEOUT_MS;
51
- const parsed = Number.parseInt(raw, 10);
52
- return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_WAIT_TIMEOUT_MS;
53
- }
54
46
  // Lazy singleton — construction binds the LLM once; invocations are stateless.
55
47
  let questionerAgent = null;
56
48
  function getQuestionerAgent() {
@@ -242,7 +234,7 @@ export function createAskUserQuestionTools(defineTool, deps) {
242
234
  }, WAIT_HEARTBEAT_MS);
243
235
  let outcomes;
244
236
  try {
245
- outcomes = await host.awaitAnswers(persisted.map((q) => q.id), { timeoutMs: waitTimeoutMs(), ...(signal ? { signal } : {}) });
237
+ outcomes = await host.awaitAnswers(persisted.map((q) => q.id), { timeoutMs: chatQuestionWaitTimeoutMs(), ...(signal ? { signal } : {}) });
246
238
  }
247
239
  finally {
248
240
  clearInterval(heartbeat);
@@ -1 +1 @@
1
- {"version":3,"file":"questioner.ask.tool.js","sourceRoot":"/","sources":["questioner/questioner.ask.tool.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ask_user_question — blocking mid-conversation questions for the chat\n * orchestrator (AskUserQuestion-style human-in-the-loop).\n *\n * Flow (hybrid authoring):\n * 1. The orchestrator states what it needs to learn (`purpose`) plus optional\n * draft questions.\n * 2. The QuestionerAgent (mode `chat`) refines that into polished structured\n * questions, grounded in the recent conversation excerpt and the user's\n * global context.\n * 3. Questions are persisted (`questions` table, mode `chat`,\n * `conversationId = sessionId`) via the injected {@link ChatQuestionsHost}.\n * 4. A `user_question` trace event streams the persisted questions to the\n * frontend, which renders them inline while the turn stays open.\n * 5. The tool blocks on `awaitAnswers` until the user answers/dismisses\n * through the questions REST endpoints, the wait budget elapses, or the\n * run is aborted. Answers come back as the tool result so the model\n * continues the SAME turn.\n *\n * On timeout the questions remain `pending`: they survive reloads via the\n * conversation-linked question fetch, and a later answer re-enters the chat\n * as a new user turn (frontend responsibility).\n *\n * Chat-only: not registered in the MCP tool registry. The handler also fails\n * gracefully when the session/stream context or host bridge is missing.\n */\nimport { z } from \"zod\";\n\nimport type { DefineTool, ToolDeps } from \"../shared/agent/tool.helpers.js\";\nimport { error, success } from \"../shared/agent/tool.helpers.js\";\nimport { requestContext } from \"../shared/observability/request-context.js\";\nimport { protocolLogger } from \"../shared/observability/protocol.logger.js\";\nimport type { PersistableQuestion, PersistedQuestion, ChatQuestionAnswerOutcome } from \"../shared/interfaces/questioner.interface.js\";\nimport type { Question, QuestionStrategy } from \"../shared/schemas/question.schema.js\";\nimport { QuestionerAgent } from \"./questioner.agent.js\";\nimport type { ChatContext } from \"./questioner.types.js\";\n\nconst logger = protocolLogger(\"AskUserQuestionTool\");\n\n/** Default in-tool wait budget for the user's answer (4 minutes). */\nconst DEFAULT_WAIT_TIMEOUT_MS = 4 * 60 * 1000;\n\n/** Heartbeat interval while blocked, so SSE transports do not idle out. */\nconst WAIT_HEARTBEAT_MS = 15_000;\n\n/** Messages included in the conversation excerpt fed to the QuestionerAgent. */\nconst EXCERPT_MESSAGE_COUNT = 10;\n\n/**\n * Fetch window for the excerpt. Host adapters return the FIRST N messages\n * (ascending) when a limit is passed, so we fetch a wide window and keep the\n * tail to get the most recent exchange.\n */\nconst EXCERPT_FETCH_LIMIT = 100;\n\n/** Max characters per message inside the excerpt. */\nconst EXCERPT_MESSAGE_CHARS = 400;\n\nfunction waitTimeoutMs(): number {\n const raw = process.env.CHAT_QUESTION_WAIT_TIMEOUT_MS;\n if (!raw) return DEFAULT_WAIT_TIMEOUT_MS;\n const parsed = Number.parseInt(raw, 10);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_WAIT_TIMEOUT_MS;\n}\n\n// Lazy singleton — construction binds the LLM once; invocations are stateless.\nlet questionerAgent: QuestionerAgent | null = null;\nfunction getQuestionerAgent(): QuestionerAgent {\n if (!questionerAgent) questionerAgent = new QuestionerAgent();\n return questionerAgent;\n}\n\n/** Test seam: replace or reset the module-level QuestionerAgent singleton. */\nexport function setQuestionerAgentForTesting(agent: QuestionerAgent | null): void {\n questionerAgent = agent;\n}\n\nconst draftQuestionSchema = z.object({\n prompt: z\n .string()\n .min(5)\n .max(400)\n .describe(\"The question to ask, ending in a question mark. Self-contained plain language.\"),\n options: z\n .array(z.string().min(1).max(120))\n .min(2)\n .max(4)\n .optional()\n .describe(\"2-4 mutually distinct answer options. Omit to let the question generator derive them.\"),\n multiSelect: z\n .boolean()\n .optional()\n .describe(\"True when several options can be picked together (priorities, bundles).\"),\n});\n\n/**\n * Build a fallback Question directly from an orchestrator draft when the\n * QuestionerAgent produced nothing. Requires the draft to carry options.\n */\nfunction questionFromDraft(draft: z.infer<typeof draftQuestionSchema>, index: number): Question | null {\n if (!draft.options || draft.options.length < 2) return null;\n return {\n title: `Question ${index + 1}`,\n prompt: draft.prompt.slice(0, 400),\n options: draft.options.slice(0, 4).map((label) => ({\n label: label.slice(0, 120),\n description: label.slice(0, 280),\n })),\n multiSelect: draft.multiSelect ?? false,\n };\n}\n\n/**\n * Creates the chat-only `ask_user_question` tool. Registered by\n * `createChatTools` only when `deps.chatQuestions` is provided — never part\n * of the MCP tool registry (MCP clients have their own elicitation surface).\n *\n * @param defineTool - Tool factory provided by the composition root.\n * @param deps - Shared tool dependencies; requires `chatQuestions`.\n */\nexport function createAskUserQuestionTools(defineTool: DefineTool, deps: ToolDeps) {\n const askUserQuestion = defineTool({\n name: \"ask_user_question\",\n description:\n \"Ask the user 1-3 structured clarifying questions and WAIT for their answer before continuing. \" +\n \"The conversation pauses: the user sees interactive question cards inline and your turn resumes \" +\n \"with their selections as the tool result.\\n\\n\" +\n \"**Use when** a decision materially changes what you do next — before an expensive operation \" +\n \"(discovery, creating an intent from ambiguous input), when facing meaningfully different \" +\n \"directions, or when one concrete missing detail (timing, scope, budget, format) blocks progress.\\n\\n\" +\n \"**Do not use** for facts already visible in the conversation or profile, procedural \" +\n \"confirmations (\\\"Should I proceed?\\\"), or open-ended questions better asked in your response text.\\n\\n\" +\n \"**Input:** `purpose` states what you need to learn and why. Optionally propose `questions` \" +\n \"drafts (prompt + 2-4 options); a question generator refines wording and option quality.\\n\\n\" +\n \"**Returns:** One entry per question with `status` (`answered`/`dismissed`/`timeout`) and the \" +\n \"user's `selectedOptions`/`freeText`. On `timeout` the questions stay visible in the \" +\n \"conversation — acknowledge briefly and end your turn; do NOT repeat the questions in text.\",\n querySchema: z.object({\n purpose: z\n .string()\n .min(10)\n .max(600)\n .describe(\"What you need to learn from the user and why it changes what you do next.\"),\n questions: z\n .array(draftQuestionSchema)\n .min(1)\n .max(3)\n .optional()\n .describe(\"Draft questions to ask. The question generator polishes them before display.\"),\n }),\n handler: async ({ context, query }) => {\n const host = deps.chatQuestions;\n if (!host) {\n return error(\n \"Interactive questions are not available in this environment. Ask the user directly in your response text instead.\",\n );\n }\n if (context.isMcp || !context.sessionId) {\n return error(\n \"Interactive questions require a live chat session. Ask the user directly in your response text instead.\",\n );\n }\n const store = requestContext.getStore();\n const emit = store?.traceEmitter;\n const signal = store?.abortSignal;\n if (!emit) {\n return error(\n \"Interactive questions require a streaming chat turn. Ask the user directly in your response text instead.\",\n );\n }\n\n const sessionId = context.sessionId;\n\n // ── 1. Gather grounding context ────────────────────────────────────\n const [conversationExcerpt, userContext] = await Promise.all([\n loadConversationExcerpt(deps, sessionId),\n deps.getUserContextText?.(context.userId).catch(() => \"\") ?? Promise.resolve(\"\"),\n ]);\n\n // ── 2. Generate polished questions (hybrid: drafts + QuestionerAgent) ──\n const chatContext: ChatContext = {\n purpose: query.purpose,\n ...(query.questions?.length ? { draftQuestions: query.questions } : {}),\n ...(conversationExcerpt ? { conversationExcerpt } : {}),\n ...(userContext ? { userContext } : {}),\n };\n\n let generated: { questions: Question[]; strategies: QuestionStrategy[] } | null = null;\n try {\n generated = await getQuestionerAgent().invoke(\n {\n mode: \"chat\",\n userId: context.userId,\n sourceType: \"conversation\",\n sourceId: sessionId,\n context: chatContext,\n conversationId: sessionId,\n },\n signal ? { signal } : undefined,\n );\n } catch (err) {\n logger.warn(\"QuestionerAgent invocation failed\", {\n error: err instanceof Error ? err.message : String(err),\n });\n }\n\n let finalQuestions: Question[];\n let strategies: QuestionStrategy[];\n if (generated && generated.questions.length > 0) {\n finalQuestions = generated.questions;\n strategies = generated.strategies;\n } else {\n // Fallback: use the orchestrator's own drafts verbatim (options required).\n const fromDrafts = (query.questions ?? [])\n .map((d, i) => questionFromDraft(d, i))\n .filter((q): q is Question => q !== null);\n if (fromDrafts.length === 0) {\n return error(\n \"Could not prepare structured questions. Ask the user directly in your response text instead.\",\n );\n }\n finalQuestions = fromDrafts;\n strategies = fromDrafts.map(() => \"surface_missing_detail\" as const);\n }\n\n if (signal?.aborted) {\n return error(\"The chat turn was cancelled before the questions could be shown.\");\n }\n\n // ── 3. Persist (mode `chat`, linked to this conversation) ──────────\n const timestamp = new Date().toISOString();\n const batch: PersistableQuestion[] = finalQuestions.map((payload, i) => ({\n detection: {\n mode: \"chat\",\n sourceType: \"conversation\",\n sourceId: sessionId,\n timestamp,\n },\n actors: [{ userId: context.userId, role: \"subject\" as const }],\n payload,\n strategy: strategies[i] ?? \"surface_missing_detail\",\n conversationId: sessionId,\n }));\n\n let persisted: PersistedQuestion[];\n try {\n persisted = await host.persist(batch);\n } catch (err) {\n logger.error(\"Failed to persist chat questions\", {\n error: err instanceof Error ? err.message : String(err),\n });\n return error(\"Could not deliver the questions to the user. Ask directly in your response text instead.\");\n }\n\n // ── 4. Stream the cards to the frontend ────────────────────────────\n emit({\n type: \"user_question\",\n questions: persisted.map((q) => ({\n id: q.id,\n title: q.payload.title,\n prompt: q.payload.prompt,\n options: q.payload.options,\n multiSelect: q.payload.multiSelect,\n })),\n });\n\n // ── 5. Block until answered / dismissed / timeout / abort ──────────\n const heartbeat = setInterval(() => {\n try {\n emit({ type: \"status\", message: \"Waiting for your answer…\" });\n } catch {\n /* stream may be closing; the wait resolves via timeout/abort */\n }\n }, WAIT_HEARTBEAT_MS);\n\n let outcomes: ChatQuestionAnswerOutcome[];\n try {\n outcomes = await host.awaitAnswers(\n persisted.map((q) => q.id),\n { timeoutMs: waitTimeoutMs(), ...(signal ? { signal } : {}) },\n );\n } finally {\n clearInterval(heartbeat);\n }\n\n const byId = new Map(persisted.map((q) => [q.id, q]));\n const results = outcomes.map((o) => {\n const q = byId.get(o.questionId);\n return {\n questionId: o.questionId,\n prompt: q?.payload.prompt ?? \"\",\n status: o.status,\n ...(o.answer\n ? {\n selectedOptions: o.answer.selectedOptions,\n ...(o.answer.freeText ? { freeText: o.answer.freeText } : {}),\n }\n : {}),\n };\n });\n\n const answeredCount = results.filter((r) => r.status === \"answered\").length;\n const timedOut = results.some((r) => r.status === \"timeout\");\n\n return success({\n answers: results,\n summary: `${answeredCount} of ${results.length} question(s) answered`,\n ...(timedOut\n ? {\n guidance:\n \"The user has not answered the remaining question(s) yet. They stay visible in the conversation — acknowledge briefly, do NOT repeat the questions in text, and end your turn.\",\n }\n : {}),\n });\n },\n });\n\n return [askUserQuestion] as const;\n}\n\n/**\n * Load a compact excerpt of the most recent conversation messages for the\n * QuestionerAgent's grounding. Best-effort: returns \"\" on any failure or when\n * no chat session reader is available. Note: the in-flight user message is\n * not yet persisted; the orchestrator's `purpose` carries that context.\n */\nasync function loadConversationExcerpt(deps: ToolDeps, sessionId: string): Promise<string> {\n if (!deps.chatSession) return \"\";\n try {\n const messages = await deps.chatSession.getSessionMessages(sessionId, EXCERPT_FETCH_LIMIT);\n if (!messages || messages.length === 0) return \"\";\n return messages\n .slice(-EXCERPT_MESSAGE_COUNT)\n .map((m) => {\n const role = m.role === \"assistant\" ? \"Assistant\" : \"User\";\n const text = (m.content ?? \"\").replace(/\\s+/g, \" \").trim();\n return `${role}: ${text.slice(0, EXCERPT_MESSAGE_CHARS)}`;\n })\n .join(\"\\n\");\n } catch (err) {\n logger.warn(\"Failed to load conversation excerpt\", {\n error: err instanceof Error ? err.message : String(err),\n });\n return \"\";\n }\n}\n"]}
1
+ {"version":3,"file":"questioner.ask.tool.js","sourceRoot":"/","sources":["questioner/questioner.ask.tool.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ask_user_question — blocking mid-conversation questions for the chat\n * orchestrator (AskUserQuestion-style human-in-the-loop).\n *\n * Flow (hybrid authoring):\n * 1. The orchestrator states what it needs to learn (`purpose`) plus optional\n * draft questions.\n * 2. The QuestionerAgent (mode `chat`) refines that into polished structured\n * questions, grounded in the recent conversation excerpt and the user's\n * global context.\n * 3. Questions are persisted (`questions` table, mode `chat`,\n * `conversationId = sessionId`) via the injected {@link ChatQuestionsHost}.\n * 4. A `user_question` trace event streams the persisted questions to the\n * frontend, which renders them inline while the turn stays open.\n * 5. The tool blocks on `awaitAnswers` until the user answers/dismisses\n * through the questions REST endpoints, the wait budget elapses, or the\n * run is aborted. Answers come back as the tool result so the model\n * continues the SAME turn.\n *\n * On timeout the questions remain `pending`: they survive reloads via the\n * conversation-linked question fetch, and a later answer re-enters the chat\n * as a new user turn (frontend responsibility).\n *\n * Chat-only: not registered in the MCP tool registry. The handler also fails\n * gracefully when the session/stream context or host bridge is missing.\n */\nimport { z } from \"zod\";\n\nimport type { DefineTool, ToolDeps } from \"../shared/agent/tool.helpers.js\";\nimport { error, success } from \"../shared/agent/tool.helpers.js\";\nimport { requestContext } from \"../shared/observability/request-context.js\";\nimport { protocolLogger } from \"../shared/observability/protocol.logger.js\";\nimport type { PersistableQuestion, PersistedQuestion, ChatQuestionAnswerOutcome } from \"../shared/interfaces/questioner.interface.js\";\nimport type { Question, QuestionStrategy } from \"../shared/schemas/question.schema.js\";\nimport { QuestionerAgent } from \"./questioner.agent.js\";\nimport { chatQuestionWaitTimeoutMs } from \"./questioner.env.js\";\nimport type { ChatContext } from \"./questioner.types.js\";\n\nconst logger = protocolLogger(\"AskUserQuestionTool\");\n\n\n/** Heartbeat interval while blocked, so SSE transports do not idle out. */\nconst WAIT_HEARTBEAT_MS = 15_000;\n\n/** Messages included in the conversation excerpt fed to the QuestionerAgent. */\nconst EXCERPT_MESSAGE_COUNT = 10;\n\n/**\n * Fetch window for the excerpt. Host adapters return the FIRST N messages\n * (ascending) when a limit is passed, so we fetch a wide window and keep the\n * tail to get the most recent exchange.\n */\nconst EXCERPT_FETCH_LIMIT = 100;\n\n/** Max characters per message inside the excerpt. */\nconst EXCERPT_MESSAGE_CHARS = 400;\n\n// Lazy singleton — construction binds the LLM once; invocations are stateless.\nlet questionerAgent: QuestionerAgent | null = null;\nfunction getQuestionerAgent(): QuestionerAgent {\n if (!questionerAgent) questionerAgent = new QuestionerAgent();\n return questionerAgent;\n}\n\n/** Test seam: replace or reset the module-level QuestionerAgent singleton. */\nexport function setQuestionerAgentForTesting(agent: QuestionerAgent | null): void {\n questionerAgent = agent;\n}\n\nconst draftQuestionSchema = z.object({\n prompt: z\n .string()\n .min(5)\n .max(400)\n .describe(\"The question to ask, ending in a question mark. Self-contained plain language.\"),\n options: z\n .array(z.string().min(1).max(120))\n .min(2)\n .max(4)\n .optional()\n .describe(\"2-4 mutually distinct answer options. Omit to let the question generator derive them.\"),\n multiSelect: z\n .boolean()\n .optional()\n .describe(\"True when several options can be picked together (priorities, bundles).\"),\n});\n\n/**\n * Build a fallback Question directly from an orchestrator draft when the\n * QuestionerAgent produced nothing. Requires the draft to carry options.\n */\nfunction questionFromDraft(draft: z.infer<typeof draftQuestionSchema>, index: number): Question | null {\n if (!draft.options || draft.options.length < 2) return null;\n return {\n title: `Question ${index + 1}`,\n prompt: draft.prompt.slice(0, 400),\n options: draft.options.slice(0, 4).map((label) => ({\n label: label.slice(0, 120),\n description: label.slice(0, 280),\n })),\n multiSelect: draft.multiSelect ?? false,\n };\n}\n\n/**\n * Creates the chat-only `ask_user_question` tool. Registered by\n * `createChatTools` only when `deps.chatQuestions` is provided — never part\n * of the MCP tool registry (MCP clients have their own elicitation surface).\n *\n * @param defineTool - Tool factory provided by the composition root.\n * @param deps - Shared tool dependencies; requires `chatQuestions`.\n */\nexport function createAskUserQuestionTools(defineTool: DefineTool, deps: ToolDeps) {\n const askUserQuestion = defineTool({\n name: \"ask_user_question\",\n description:\n \"Ask the user 1-3 structured clarifying questions and WAIT for their answer before continuing. \" +\n \"The conversation pauses: the user sees interactive question cards inline and your turn resumes \" +\n \"with their selections as the tool result.\\n\\n\" +\n \"**Use when** a decision materially changes what you do next — before an expensive operation \" +\n \"(discovery, creating an intent from ambiguous input), when facing meaningfully different \" +\n \"directions, or when one concrete missing detail (timing, scope, budget, format) blocks progress.\\n\\n\" +\n \"**Do not use** for facts already visible in the conversation or profile, procedural \" +\n \"confirmations (\\\"Should I proceed?\\\"), or open-ended questions better asked in your response text.\\n\\n\" +\n \"**Input:** `purpose` states what you need to learn and why. Optionally propose `questions` \" +\n \"drafts (prompt + 2-4 options); a question generator refines wording and option quality.\\n\\n\" +\n \"**Returns:** One entry per question with `status` (`answered`/`dismissed`/`timeout`) and the \" +\n \"user's `selectedOptions`/`freeText`. On `timeout` the questions stay visible in the \" +\n \"conversation — acknowledge briefly and end your turn; do NOT repeat the questions in text.\",\n querySchema: z.object({\n purpose: z\n .string()\n .min(10)\n .max(600)\n .describe(\"What you need to learn from the user and why it changes what you do next.\"),\n questions: z\n .array(draftQuestionSchema)\n .min(1)\n .max(3)\n .optional()\n .describe(\"Draft questions to ask. The question generator polishes them before display.\"),\n }),\n handler: async ({ context, query }) => {\n const host = deps.chatQuestions;\n if (!host) {\n return error(\n \"Interactive questions are not available in this environment. Ask the user directly in your response text instead.\",\n );\n }\n if (context.isMcp || !context.sessionId) {\n return error(\n \"Interactive questions require a live chat session. Ask the user directly in your response text instead.\",\n );\n }\n const store = requestContext.getStore();\n const emit = store?.traceEmitter;\n const signal = store?.abortSignal;\n if (!emit) {\n return error(\n \"Interactive questions require a streaming chat turn. Ask the user directly in your response text instead.\",\n );\n }\n\n const sessionId = context.sessionId;\n\n // ── 1. Gather grounding context ────────────────────────────────────\n const [conversationExcerpt, userContext] = await Promise.all([\n loadConversationExcerpt(deps, sessionId),\n deps.getUserContextText?.(context.userId).catch(() => \"\") ?? Promise.resolve(\"\"),\n ]);\n\n // ── 2. Generate polished questions (hybrid: drafts + QuestionerAgent) ──\n const chatContext: ChatContext = {\n purpose: query.purpose,\n ...(query.questions?.length ? { draftQuestions: query.questions } : {}),\n ...(conversationExcerpt ? { conversationExcerpt } : {}),\n ...(userContext ? { userContext } : {}),\n };\n\n let generated: { questions: Question[]; strategies: QuestionStrategy[] } | null = null;\n try {\n generated = await getQuestionerAgent().invoke(\n {\n mode: \"chat\",\n userId: context.userId,\n sourceType: \"conversation\",\n sourceId: sessionId,\n context: chatContext,\n conversationId: sessionId,\n },\n signal ? { signal } : undefined,\n );\n } catch (err) {\n logger.warn(\"QuestionerAgent invocation failed\", {\n error: err instanceof Error ? err.message : String(err),\n });\n }\n\n let finalQuestions: Question[];\n let strategies: QuestionStrategy[];\n if (generated && generated.questions.length > 0) {\n finalQuestions = generated.questions;\n strategies = generated.strategies;\n } else {\n // Fallback: use the orchestrator's own drafts verbatim (options required).\n const fromDrafts = (query.questions ?? [])\n .map((d, i) => questionFromDraft(d, i))\n .filter((q): q is Question => q !== null);\n if (fromDrafts.length === 0) {\n return error(\n \"Could not prepare structured questions. Ask the user directly in your response text instead.\",\n );\n }\n finalQuestions = fromDrafts;\n strategies = fromDrafts.map(() => \"surface_missing_detail\" as const);\n }\n\n if (signal?.aborted) {\n return error(\"The chat turn was cancelled before the questions could be shown.\");\n }\n\n // ── 3. Persist (mode `chat`, linked to this conversation) ──────────\n const timestamp = new Date().toISOString();\n const batch: PersistableQuestion[] = finalQuestions.map((payload, i) => ({\n detection: {\n mode: \"chat\",\n sourceType: \"conversation\",\n sourceId: sessionId,\n timestamp,\n },\n actors: [{ userId: context.userId, role: \"subject\" as const }],\n payload,\n strategy: strategies[i] ?? \"surface_missing_detail\",\n conversationId: sessionId,\n }));\n\n let persisted: PersistedQuestion[];\n try {\n persisted = await host.persist(batch);\n } catch (err) {\n logger.error(\"Failed to persist chat questions\", {\n error: err instanceof Error ? err.message : String(err),\n });\n return error(\"Could not deliver the questions to the user. Ask directly in your response text instead.\");\n }\n\n // ── 4. Stream the cards to the frontend ────────────────────────────\n emit({\n type: \"user_question\",\n questions: persisted.map((q) => ({\n id: q.id,\n title: q.payload.title,\n prompt: q.payload.prompt,\n options: q.payload.options,\n multiSelect: q.payload.multiSelect,\n })),\n });\n\n // ── 5. Block until answered / dismissed / timeout / abort ──────────\n const heartbeat = setInterval(() => {\n try {\n emit({ type: \"status\", message: \"Waiting for your answer…\" });\n } catch {\n /* stream may be closing; the wait resolves via timeout/abort */\n }\n }, WAIT_HEARTBEAT_MS);\n\n let outcomes: ChatQuestionAnswerOutcome[];\n try {\n outcomes = await host.awaitAnswers(\n persisted.map((q) => q.id),\n { timeoutMs: chatQuestionWaitTimeoutMs(), ...(signal ? { signal } : {}) },\n );\n } finally {\n clearInterval(heartbeat);\n }\n\n const byId = new Map(persisted.map((q) => [q.id, q]));\n const results = outcomes.map((o) => {\n const q = byId.get(o.questionId);\n return {\n questionId: o.questionId,\n prompt: q?.payload.prompt ?? \"\",\n status: o.status,\n ...(o.answer\n ? {\n selectedOptions: o.answer.selectedOptions,\n ...(o.answer.freeText ? { freeText: o.answer.freeText } : {}),\n }\n : {}),\n };\n });\n\n const answeredCount = results.filter((r) => r.status === \"answered\").length;\n const timedOut = results.some((r) => r.status === \"timeout\");\n\n return success({\n answers: results,\n summary: `${answeredCount} of ${results.length} question(s) answered`,\n ...(timedOut\n ? {\n guidance:\n \"The user has not answered the remaining question(s) yet. They stay visible in the conversation — acknowledge briefly, do NOT repeat the questions in text, and end your turn.\",\n }\n : {}),\n });\n },\n });\n\n return [askUserQuestion] as const;\n}\n\n/**\n * Load a compact excerpt of the most recent conversation messages for the\n * QuestionerAgent's grounding. Best-effort: returns \"\" on any failure or when\n * no chat session reader is available. Note: the in-flight user message is\n * not yet persisted; the orchestrator's `purpose` carries that context.\n */\nasync function loadConversationExcerpt(deps: ToolDeps, sessionId: string): Promise<string> {\n if (!deps.chatSession) return \"\";\n try {\n const messages = await deps.chatSession.getSessionMessages(sessionId, EXCERPT_FETCH_LIMIT);\n if (!messages || messages.length === 0) return \"\";\n return messages\n .slice(-EXCERPT_MESSAGE_COUNT)\n .map((m) => {\n const role = m.role === \"assistant\" ? \"Assistant\" : \"User\";\n const text = (m.content ?? \"\").replace(/\\s+/g, \" \").trim();\n return `${role}: ${text.slice(0, EXCERPT_MESSAGE_CHARS)}`;\n })\n .join(\"\\n\");\n } catch (err) {\n logger.warn(\"Failed to load conversation excerpt\", {\n error: err instanceof Error ? err.message : String(err),\n });\n return \"\";\n }\n}\n"]}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Centralized accessors for all question-generation environment variables.
3
+ *
4
+ * Naming scheme (one prefix, hierarchical):
5
+ *
6
+ * QUESTIONER_ENABLED master switch — QuestionerQueue worker +
7
+ * enqueue closures at every composition site.
8
+ * QUESTIONER_DISCOVERY_ENABLED per-surface switch — decision questions during
9
+ * chat/MCP opportunity discovery. Only effective
10
+ * when the master switch is on (the discovery
11
+ * question step adds inline summarizer latency,
12
+ * so operators can turn it off independently).
13
+ * QUESTIONER_DISCOVERY_INPUT_MODE 'transcripts' | 'insights' generator input.
14
+ * QUESTIONER_DISCOVERY_TIMEOUT_MS per-call deadline for the discovery-questions
15
+ * LLM step (default 12s).
16
+ * QUESTIONER_CHAT_WAIT_TIMEOUT_MS how long the blocking ask_user_question chat
17
+ * tool waits for an inline answer (default 4 min).
18
+ *
19
+ * All reads go through this module — do not read these variables via
20
+ * `process.env` elsewhere. Values are read on every call (no caching) so tests
21
+ * and long-lived processes observe changes.
22
+ */
23
+ /**
24
+ * Question-generator budget. Sized against Railway's ~60 s edge timeout:
25
+ * the discovery + evaluation + negotiate phases consume ~50 s on the slow
26
+ * path, leaving ~10 s of headroom for the tail. 12 s is the larger end of
27
+ * "fits"; the question step usually completes in 4-8 s, so most legitimate
28
+ * calls finish well inside. Aborted calls return `null` (no questions);
29
+ * the rest of the discovery payload still ships.
30
+ */
31
+ export declare const DISCOVERY_QUESTIONS_TIMEOUT_MS_DEFAULT = 12000;
32
+ export declare const CHAT_QUESTION_WAIT_TIMEOUT_MS_DEFAULT = 240000;
33
+ /** Master switch: is any background question generation enabled? */
34
+ export declare function isQuestionerEnabled(): boolean;
35
+ /**
36
+ * Per-surface switch: should chat/MCP opportunity discovery produce decision
37
+ * questions? Hierarchical — always false when the master switch is off.
38
+ */
39
+ export declare function isDiscoveryQuestionsEnabled(): boolean;
40
+ /**
41
+ * Input mode for the discovery question generator. Only `transcripts` is
42
+ * implemented; any other value falls back to `transcripts` (startup.env.ts
43
+ * warns on invalid values).
44
+ */
45
+ export declare function discoveryQuestionsInputMode(): "transcripts" | "insights";
46
+ /** Per-call deadline for the discovery-questions LLM step. */
47
+ export declare function discoveryQuestionsTimeoutMs(): number;
48
+ /** Wait budget for the blocking ask_user_question chat tool. */
49
+ export declare function chatQuestionWaitTimeoutMs(): number;
50
+ //# sourceMappingURL=questioner.env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"questioner.env.d.ts","sourceRoot":"/","sources":["questioner/questioner.env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,sCAAsC,QAAS,CAAC;AAC7D,eAAO,MAAM,qCAAqC,SAAU,CAAC;AAe7D,oEAAoE;AACpE,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,IAAI,OAAO,CAErD;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,IAAI,aAAa,GAAG,UAAU,CAIxE;AAED,8DAA8D;AAC9D,wBAAgB,2BAA2B,IAAI,MAAM,CAEpD;AAED,gEAAgE;AAChE,wBAAgB,yBAAyB,IAAI,MAAM,CAElD"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Centralized accessors for all question-generation environment variables.
3
+ *
4
+ * Naming scheme (one prefix, hierarchical):
5
+ *
6
+ * QUESTIONER_ENABLED master switch — QuestionerQueue worker +
7
+ * enqueue closures at every composition site.
8
+ * QUESTIONER_DISCOVERY_ENABLED per-surface switch — decision questions during
9
+ * chat/MCP opportunity discovery. Only effective
10
+ * when the master switch is on (the discovery
11
+ * question step adds inline summarizer latency,
12
+ * so operators can turn it off independently).
13
+ * QUESTIONER_DISCOVERY_INPUT_MODE 'transcripts' | 'insights' generator input.
14
+ * QUESTIONER_DISCOVERY_TIMEOUT_MS per-call deadline for the discovery-questions
15
+ * LLM step (default 12s).
16
+ * QUESTIONER_CHAT_WAIT_TIMEOUT_MS how long the blocking ask_user_question chat
17
+ * tool waits for an inline answer (default 4 min).
18
+ *
19
+ * All reads go through this module — do not read these variables via
20
+ * `process.env` elsewhere. Values are read on every call (no caching) so tests
21
+ * and long-lived processes observe changes.
22
+ */
23
+ /**
24
+ * Question-generator budget. Sized against Railway's ~60 s edge timeout:
25
+ * the discovery + evaluation + negotiate phases consume ~50 s on the slow
26
+ * path, leaving ~10 s of headroom for the tail. 12 s is the larger end of
27
+ * "fits"; the question step usually completes in 4-8 s, so most legitimate
28
+ * calls finish well inside. Aborted calls return `null` (no questions);
29
+ * the rest of the discovery payload still ships.
30
+ */
31
+ export const DISCOVERY_QUESTIONS_TIMEOUT_MS_DEFAULT = 12000;
32
+ export const CHAT_QUESTION_WAIT_TIMEOUT_MS_DEFAULT = 240000;
33
+ /**
34
+ * Parse a positive integer env var, clamped to the safe-integer range so a
35
+ * malformed env value cannot crash `AbortSignal.timeout` (which throws on
36
+ * values outside `[0, MAX_SAFE_INTEGER]`).
37
+ */
38
+ function positiveIntEnv(name, fallback) {
39
+ const raw = process.env[name];
40
+ if (!raw)
41
+ return fallback;
42
+ const parsed = Number.parseInt(raw, 10);
43
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > Number.MAX_SAFE_INTEGER)
44
+ return fallback;
45
+ return parsed;
46
+ }
47
+ /** Master switch: is any background question generation enabled? */
48
+ export function isQuestionerEnabled() {
49
+ return process.env.QUESTIONER_ENABLED === "true";
50
+ }
51
+ /**
52
+ * Per-surface switch: should chat/MCP opportunity discovery produce decision
53
+ * questions? Hierarchical — always false when the master switch is off.
54
+ */
55
+ export function isDiscoveryQuestionsEnabled() {
56
+ return isQuestionerEnabled() && process.env.QUESTIONER_DISCOVERY_ENABLED === "true";
57
+ }
58
+ /**
59
+ * Input mode for the discovery question generator. Only `transcripts` is
60
+ * implemented; any other value falls back to `transcripts` (startup.env.ts
61
+ * warns on invalid values).
62
+ */
63
+ export function discoveryQuestionsInputMode() {
64
+ return process.env.QUESTIONER_DISCOVERY_INPUT_MODE?.trim() === "insights"
65
+ ? "insights"
66
+ : "transcripts";
67
+ }
68
+ /** Per-call deadline for the discovery-questions LLM step. */
69
+ export function discoveryQuestionsTimeoutMs() {
70
+ return positiveIntEnv("QUESTIONER_DISCOVERY_TIMEOUT_MS", DISCOVERY_QUESTIONS_TIMEOUT_MS_DEFAULT);
71
+ }
72
+ /** Wait budget for the blocking ask_user_question chat tool. */
73
+ export function chatQuestionWaitTimeoutMs() {
74
+ return positiveIntEnv("QUESTIONER_CHAT_WAIT_TIMEOUT_MS", CHAT_QUESTION_WAIT_TIMEOUT_MS_DEFAULT);
75
+ }
76
+ //# sourceMappingURL=questioner.env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"questioner.env.js","sourceRoot":"/","sources":["questioner/questioner.env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,sCAAsC,GAAG,KAAM,CAAC;AAC7D,MAAM,CAAC,MAAM,qCAAqC,GAAG,MAAO,CAAC;AAE7D;;;;GAIG;AACH,SAAS,cAAc,CAAC,IAAY,EAAE,QAAgB;IACpD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,CAAC,GAAG;QAAE,OAAO,QAAQ,CAAC;IAC1B,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACxC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,IAAI,MAAM,GAAG,MAAM,CAAC,gBAAgB;QAAE,OAAO,QAAQ,CAAC;IACjG,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,mBAAmB;IACjC,OAAO,OAAO,CAAC,GAAG,CAAC,kBAAkB,KAAK,MAAM,CAAC;AACnD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,2BAA2B;IACzC,OAAO,mBAAmB,EAAE,IAAI,OAAO,CAAC,GAAG,CAAC,4BAA4B,KAAK,MAAM,CAAC;AACtF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,2BAA2B;IACzC,OAAO,OAAO,CAAC,GAAG,CAAC,+BAA+B,EAAE,IAAI,EAAE,KAAK,UAAU;QACvE,CAAC,CAAC,UAAU;QACZ,CAAC,CAAC,aAAa,CAAC;AACpB,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,2BAA2B;IACzC,OAAO,cAAc,CAAC,iCAAiC,EAAE,sCAAsC,CAAC,CAAC;AACnG,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,yBAAyB;IACvC,OAAO,cAAc,CAAC,iCAAiC,EAAE,qCAAqC,CAAC,CAAC;AAClG,CAAC","sourcesContent":["/**\n * Centralized accessors for all question-generation environment variables.\n *\n * Naming scheme (one prefix, hierarchical):\n *\n * QUESTIONER_ENABLED master switch — QuestionerQueue worker +\n * enqueue closures at every composition site.\n * QUESTIONER_DISCOVERY_ENABLED per-surface switch — decision questions during\n * chat/MCP opportunity discovery. Only effective\n * when the master switch is on (the discovery\n * question step adds inline summarizer latency,\n * so operators can turn it off independently).\n * QUESTIONER_DISCOVERY_INPUT_MODE 'transcripts' | 'insights' generator input.\n * QUESTIONER_DISCOVERY_TIMEOUT_MS per-call deadline for the discovery-questions\n * LLM step (default 12s).\n * QUESTIONER_CHAT_WAIT_TIMEOUT_MS how long the blocking ask_user_question chat\n * tool waits for an inline answer (default 4 min).\n *\n * All reads go through this module — do not read these variables via\n * `process.env` elsewhere. Values are read on every call (no caching) so tests\n * and long-lived processes observe changes.\n */\n\n/**\n * Question-generator budget. Sized against Railway's ~60 s edge timeout:\n * the discovery + evaluation + negotiate phases consume ~50 s on the slow\n * path, leaving ~10 s of headroom for the tail. 12 s is the larger end of\n * \"fits\"; the question step usually completes in 4-8 s, so most legitimate\n * calls finish well inside. Aborted calls return `null` (no questions);\n * the rest of the discovery payload still ships.\n */\nexport const DISCOVERY_QUESTIONS_TIMEOUT_MS_DEFAULT = 12_000;\nexport const CHAT_QUESTION_WAIT_TIMEOUT_MS_DEFAULT = 240_000;\n\n/**\n * Parse a positive integer env var, clamped to the safe-integer range so a\n * malformed env value cannot crash `AbortSignal.timeout` (which throws on\n * values outside `[0, MAX_SAFE_INTEGER]`).\n */\nfunction positiveIntEnv(name: string, fallback: number): number {\n const raw = process.env[name];\n if (!raw) return fallback;\n const parsed = Number.parseInt(raw, 10);\n if (!Number.isFinite(parsed) || parsed <= 0 || parsed > Number.MAX_SAFE_INTEGER) return fallback;\n return parsed;\n}\n\n/** Master switch: is any background question generation enabled? */\nexport function isQuestionerEnabled(): boolean {\n return process.env.QUESTIONER_ENABLED === \"true\";\n}\n\n/**\n * Per-surface switch: should chat/MCP opportunity discovery produce decision\n * questions? Hierarchical — always false when the master switch is off.\n */\nexport function isDiscoveryQuestionsEnabled(): boolean {\n return isQuestionerEnabled() && process.env.QUESTIONER_DISCOVERY_ENABLED === \"true\";\n}\n\n/**\n * Input mode for the discovery question generator. Only `transcripts` is\n * implemented; any other value falls back to `transcripts` (startup.env.ts\n * warns on invalid values).\n */\nexport function discoveryQuestionsInputMode(): \"transcripts\" | \"insights\" {\n return process.env.QUESTIONER_DISCOVERY_INPUT_MODE?.trim() === \"insights\"\n ? \"insights\"\n : \"transcripts\";\n}\n\n/** Per-call deadline for the discovery-questions LLM step. */\nexport function discoveryQuestionsTimeoutMs(): number {\n return positiveIntEnv(\"QUESTIONER_DISCOVERY_TIMEOUT_MS\", DISCOVERY_QUESTIONS_TIMEOUT_MS_DEFAULT);\n}\n\n/** Wait budget for the blocking ask_user_question chat tool. */\nexport function chatQuestionWaitTimeoutMs(): number {\n return positiveIntEnv(\"QUESTIONER_CHAT_WAIT_TIMEOUT_MS\", CHAT_QUESTION_WAIT_TIMEOUT_MS_DEFAULT);\n}\n"]}
@@ -85,6 +85,13 @@ export interface QuestionerInput {
85
85
  scopeType?: ToolScopeType;
86
86
  /** Scoped question id. When scopeType is `network`, this is the actor networkId. */
87
87
  scopeId?: string;
88
+ /**
89
+ * Intent that triggered the run that generated these questions. Persisted as
90
+ * `detection.triggeredBy` so intent-scoped surfaces (e.g. the intent page)
91
+ * can find them. Independent of `scopeType`/`scopeId`, which may carry a
92
+ * network scope at the same time.
93
+ */
94
+ triggeredByIntentId?: string;
88
95
  /** Conversation ID — set when the question originates from a chat session. Persisted on the question row for frontend filtering. */
89
96
  conversationId?: string;
90
97
  /** Assistant message ID — set when we know which message triggered the question. Stored in detection.messageId for inline anchoring. */
@@ -1 +1 @@
1
- {"version":3,"file":"questioner.types.d.ts","sourceRoot":"/","sources":["questioner/questioner.types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAIzE;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAEtD,0EAA0E;AAC1E,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,oFAAoF;IACpF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,uEAAuE;AACvE,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,iFAAiF;IACjF,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,KAAK,CAAC;QACrB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,WAAW,CAAC,EAAE,OAAO,CAAC;KACvB,CAAC,CAAC;IACH,6EAA6E;IAC7E,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,2DAA2D;AAC3D,MAAM,MAAM,iBAAiB,GACzB,gBAAgB,GAChB,aAAa,GACb,cAAc,GACd,kBAAkB,GAClB,WAAW,CAAC;AAEhB;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG,eAAe,CAAC;AAEvD,gEAAgE;AAChE,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,wBAAwB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAErF,6DAA6D;AAC7D,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,IAAI,EAAE,YAAY,CAAC;IACnB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,OAAO,EAAE,iBAAiB,CAAC;IAC3B,kFAAkF;IAClF,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,oFAAoF;IACpF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oIAAoI;IACpI,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wIAAwI;IACxI,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB"}
1
+ {"version":3,"file":"questioner.types.d.ts","sourceRoot":"/","sources":["questioner/questioner.types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAIzE;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAEtD,0EAA0E;AAC1E,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,oFAAoF;IACpF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,uEAAuE;AACvE,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,iFAAiF;IACjF,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,KAAK,CAAC;QACrB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;QACnB,WAAW,CAAC,EAAE,OAAO,CAAC;KACvB,CAAC,CAAC;IACH,6EAA6E;IAC7E,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,2DAA2D;AAC3D,MAAM,MAAM,iBAAiB,GACzB,gBAAgB,GAChB,aAAa,GACb,cAAc,GACd,kBAAkB,GAClB,WAAW,CAAC;AAEhB;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG,eAAe,CAAC;AAEvD,gEAAgE;AAChE,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,wBAAwB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAErF,6DAA6D;AAC7D,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,IAAI,EAAE,YAAY,CAAC;IACnB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,OAAO,EAAE,iBAAiB,CAAC;IAC3B,kFAAkF;IAClF,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,oFAAoF;IACpF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oIAAoI;IACpI,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wIAAwI;IACxI,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB"}
@@ -1 +1 @@
1
- {"version":3,"file":"questioner.types.js","sourceRoot":"/","sources":["questioner/questioner.types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * QuestionerAgent input types. The `QuestionerInput` envelope carries a `mode`\n * field that selects a preset, plus a polymorphic `context` that varies per mode.\n *\n * Slice 1 defines all four context shapes but only `DiscoveryContext` has a\n * working preset implementation. The others are type stubs for future slices.\n */\nimport type { DiscoveryQuestionInput } from \"../opportunity/question.prompt.js\";\nimport type { ToolScopeType } from \"../shared/agent/tool.scope.js\";\nimport type { QuestionMode } from \"../shared/schemas/question.schema.js\";\n\n// ─── Per-mode context types ─────────────────────────────────────────────────\n\n/**\n * Discovery context — wraps the existing DiscoveryQuestionInput wholesale.\n * The discovery preset's buildPrompt delegates to the migrated builder.\n */\nexport type DiscoveryContext = DiscoveryQuestionInput;\n\n/** Intent context — data needed to generate questions about an intent. */\nexport interface IntentContext {\n intentId: string;\n payload: string;\n summary?: string;\n /** The user's global user_context paragraph (profile-replacing identity text). */\n userContext?: string;\n}\n\n/** Profile context — data needed to generate questions to fill profile gaps. */\nexport interface ProfileContext {\n /** The user's global user_context paragraph (profile-replacing identity text). */\n userContext?: string;\n gaps: string[];\n /** Existing premise texts the user has already stated (e.g. \"I live in Berlin\"). */\n existingPremises?: string[];\n}\n\n/** Negotiation context — data from a stalled or capped negotiation. */\nexport interface NegotiationContext {\n negotiationId: string;\n counterpartyHint: string;\n indexContext: string;\n outcomeReason: \"turn_cap\" | \"timeout\" | \"stalled\";\n keyTake: string;\n /** The user's global user_context paragraph (profile-replacing identity text). */\n userContext?: string;\n}\n\n/**\n * Chat context — data for orchestrator-initiated mid-conversation questions\n * (the `ask_user_question` tool). The orchestrator states what it needs to\n * learn; the QuestionerAgent turns that into polished structured questions,\n * grounded in the recent conversation and the user's identity context.\n */\nexport interface ChatContext {\n /** What the orchestrator needs to learn and why (authored by the chat model). */\n purpose: string;\n /** Draft questions proposed by the orchestrator. The agent refines these. */\n draftQuestions?: Array<{\n prompt: string;\n options?: string[];\n multiSelect?: boolean;\n }>;\n /** Recent conversation excerpt for grounding (most recent messages last). */\n conversationExcerpt?: string;\n /** The user's global user_context paragraph (profile-replacing identity text). */\n userContext?: string;\n}\n\n/** Discriminated union: mode selects the context shape. */\nexport type QuestionerContext =\n | DiscoveryContext\n | IntentContext\n | ProfileContext\n | NegotiationContext\n | ChatContext;\n\n/**\n * Payload shape accepted by the questionerEnqueue callback. Covers all\n * question modes — the composition root bridges this to the concrete\n * QuestionerQueue.\n */\nexport type QuestionerEnqueuePayload = QuestionerInput;\n\n/** Callback signature for async question generation enqueue. */\nexport type QuestionerEnqueueFn = (input: QuestionerEnqueuePayload) => Promise<void>;\n\n/** Top-level input envelope for QuestionerAgent.invoke(). */\nexport interface QuestionerInput {\n /** Selects the preset (system prompt + builder). */\n mode: QuestionMode;\n /** User the questions are generated for. */\n userId: string;\n /** Entity type that triggered this (e.g. \"opportunity\", \"intent\", \"profile\"). */\n sourceType: string;\n /** ID of the triggering entity. */\n sourceId: string;\n /** Mode-specific context. Must align with the selected mode. */\n context: QuestionerContext;\n /** Scoped question context. Network scopes persist as QuestionActor.networkId. */\n scopeType?: ToolScopeType;\n /** Scoped question id. When scopeType is `network`, this is the actor networkId. */\n scopeId?: string;\n /** Conversation ID — set when the question originates from a chat session. Persisted on the question row for frontend filtering. */\n conversationId?: string;\n /** Assistant message ID — set when we know which message triggered the question. Stored in detection.messageId for inline anchoring. */\n messageId?: string;\n}\n"]}
1
+ {"version":3,"file":"questioner.types.js","sourceRoot":"/","sources":["questioner/questioner.types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * QuestionerAgent input types. The `QuestionerInput` envelope carries a `mode`\n * field that selects a preset, plus a polymorphic `context` that varies per mode.\n *\n * Slice 1 defines all four context shapes but only `DiscoveryContext` has a\n * working preset implementation. The others are type stubs for future slices.\n */\nimport type { DiscoveryQuestionInput } from \"../opportunity/question.prompt.js\";\nimport type { ToolScopeType } from \"../shared/agent/tool.scope.js\";\nimport type { QuestionMode } from \"../shared/schemas/question.schema.js\";\n\n// ─── Per-mode context types ─────────────────────────────────────────────────\n\n/**\n * Discovery context — wraps the existing DiscoveryQuestionInput wholesale.\n * The discovery preset's buildPrompt delegates to the migrated builder.\n */\nexport type DiscoveryContext = DiscoveryQuestionInput;\n\n/** Intent context — data needed to generate questions about an intent. */\nexport interface IntentContext {\n intentId: string;\n payload: string;\n summary?: string;\n /** The user's global user_context paragraph (profile-replacing identity text). */\n userContext?: string;\n}\n\n/** Profile context — data needed to generate questions to fill profile gaps. */\nexport interface ProfileContext {\n /** The user's global user_context paragraph (profile-replacing identity text). */\n userContext?: string;\n gaps: string[];\n /** Existing premise texts the user has already stated (e.g. \"I live in Berlin\"). */\n existingPremises?: string[];\n}\n\n/** Negotiation context — data from a stalled or capped negotiation. */\nexport interface NegotiationContext {\n negotiationId: string;\n counterpartyHint: string;\n indexContext: string;\n outcomeReason: \"turn_cap\" | \"timeout\" | \"stalled\";\n keyTake: string;\n /** The user's global user_context paragraph (profile-replacing identity text). */\n userContext?: string;\n}\n\n/**\n * Chat context — data for orchestrator-initiated mid-conversation questions\n * (the `ask_user_question` tool). The orchestrator states what it needs to\n * learn; the QuestionerAgent turns that into polished structured questions,\n * grounded in the recent conversation and the user's identity context.\n */\nexport interface ChatContext {\n /** What the orchestrator needs to learn and why (authored by the chat model). */\n purpose: string;\n /** Draft questions proposed by the orchestrator. The agent refines these. */\n draftQuestions?: Array<{\n prompt: string;\n options?: string[];\n multiSelect?: boolean;\n }>;\n /** Recent conversation excerpt for grounding (most recent messages last). */\n conversationExcerpt?: string;\n /** The user's global user_context paragraph (profile-replacing identity text). */\n userContext?: string;\n}\n\n/** Discriminated union: mode selects the context shape. */\nexport type QuestionerContext =\n | DiscoveryContext\n | IntentContext\n | ProfileContext\n | NegotiationContext\n | ChatContext;\n\n/**\n * Payload shape accepted by the questionerEnqueue callback. Covers all\n * question modes — the composition root bridges this to the concrete\n * QuestionerQueue.\n */\nexport type QuestionerEnqueuePayload = QuestionerInput;\n\n/** Callback signature for async question generation enqueue. */\nexport type QuestionerEnqueueFn = (input: QuestionerEnqueuePayload) => Promise<void>;\n\n/** Top-level input envelope for QuestionerAgent.invoke(). */\nexport interface QuestionerInput {\n /** Selects the preset (system prompt + builder). */\n mode: QuestionMode;\n /** User the questions are generated for. */\n userId: string;\n /** Entity type that triggered this (e.g. \"opportunity\", \"intent\", \"profile\"). */\n sourceType: string;\n /** ID of the triggering entity. */\n sourceId: string;\n /** Mode-specific context. Must align with the selected mode. */\n context: QuestionerContext;\n /** Scoped question context. Network scopes persist as QuestionActor.networkId. */\n scopeType?: ToolScopeType;\n /** Scoped question id. When scopeType is `network`, this is the actor networkId. */\n scopeId?: string;\n /**\n * Intent that triggered the run that generated these questions. Persisted as\n * `detection.triggeredBy` so intent-scoped surfaces (e.g. the intent page)\n * can find them. Independent of `scopeType`/`scopeId`, which may carry a\n * network scope at the same time.\n */\n triggeredByIntentId?: string;\n /** Conversation ID — set when the question originates from a chat session. Persisted on the question row for frontend filtering. */\n conversationId?: string;\n /** Assistant message ID — set when we know which message triggered the question. Stored in detection.messageId for inline anchoring. */\n messageId?: string;\n}\n"]}
@@ -4,7 +4,7 @@ const FAST_TIMEOUT_MS = 10000;
4
4
  const BOUNDED_SLOW_TIMEOUT_MS = 45000;
5
5
  const ASYNC_CANDIDATE_TIMEOUT_MS = 50000;
6
6
  // Interactive tools deliberately block on human input (ask_user_question).
7
- // Must exceed the tool's own internal wait budget (CHAT_QUESTION_WAIT_TIMEOUT_MS,
7
+ // Must exceed the tool's own internal wait budget (QUESTIONER_CHAT_WAIT_TIMEOUT_MS,
8
8
  // default 4 min) so the tool returns a graceful timeout result instead of
9
9
  // being killed by the runtime.
10
10
  const INTERACTIVE_TIMEOUT_MS = 300000;