@indexnetwork/protocol 21.0.0-rc.488.1 → 21.0.0-rc.490.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 (37) hide show
  1. package/dist/index.d.ts +3 -2
  2. package/dist/index.js +1 -1
  3. package/dist/negotiations/negotiation.agent.d.ts +13 -0
  4. package/dist/negotiations/negotiation.agent.js +64 -6
  5. package/dist/negotiations/negotiation.client-dm.d.ts +50 -0
  6. package/dist/negotiations/negotiation.client-dm.js +66 -0
  7. package/dist/negotiations/negotiation.detail-reader.js +4 -1
  8. package/dist/negotiations/negotiation.expected-speaker.d.ts +16 -2
  9. package/dist/negotiations/negotiation.expected-speaker.js +15 -3
  10. package/dist/negotiations/negotiation.graph.d.ts +110 -1
  11. package/dist/negotiations/negotiation.graph.init.js +51 -22
  12. package/dist/negotiations/negotiation.graph.js +2 -1
  13. package/dist/negotiations/negotiation.graph.shared.d.ts +22 -3
  14. package/dist/negotiations/negotiation.graph.shared.js +31 -4
  15. package/dist/negotiations/negotiation.graph.turn.d.ts +27 -0
  16. package/dist/negotiations/negotiation.graph.turn.js +64 -2
  17. package/dist/negotiations/negotiation.module.d.ts +4 -1
  18. package/dist/negotiations/negotiation.module.js +2 -1
  19. package/dist/negotiations/negotiation.protocol.d.ts +498 -0
  20. package/dist/negotiations/negotiation.question-safety.d.ts +28 -0
  21. package/dist/negotiations/negotiation.question-safety.js +65 -0
  22. package/dist/negotiations/negotiation.scope.d.ts +41 -0
  23. package/dist/negotiations/negotiation.scope.js +39 -0
  24. package/dist/negotiations/negotiation.screen.js +7 -3
  25. package/dist/negotiations/negotiation.state.d.ts +110 -0
  26. package/dist/negotiations/negotiation.tools.js +15 -4
  27. package/dist/opportunities/negotiation-context.loader.d.ts +1 -1
  28. package/dist/opportunities/negotiation-context.loader.js +3 -1
  29. package/dist/questions/question.schema.d.ts +13 -31
  30. package/dist/questions/question.schema.js +9 -15
  31. package/dist/shared/interfaces/database.capabilities.d.ts +1 -1
  32. package/dist/shared/interfaces/database.negotiation.d.ts +24 -0
  33. package/dist/shared/schemas/negotiation-state.schema.d.ts +182 -3
  34. package/dist/shared/schemas/negotiation-state.schema.js +23 -3
  35. package/dist/shared/schemas/structured-question.schema.d.ts +66 -0
  36. package/dist/shared/schemas/structured-question.schema.js +31 -0
  37. package/package.json +1 -1
@@ -36,6 +36,71 @@ export function isSafeNegotiationQuestionText(value, options) {
36
36
  }
37
37
  return true;
38
38
  }
39
+ /**
40
+ * Instruction-shaped text that must never survive into a rendered question.
41
+ *
42
+ * Applies ONLY to `isSafeAuthoredNegotiationQuestion` — deliberately not added
43
+ * to `isSafeNegotiationQuestionText`, which guards a live path whose verdicts
44
+ * must not move. The exposure is specific to the authored question: the client
45
+ * reads these strings verbatim on a card, and whatever they select comes BACK
46
+ * into the negotiator's prompt as a user answer. An option labelled "ignore
47
+ * previous instructions" is therefore a round-trip injection with a human
48
+ * clicking the button.
49
+ *
50
+ * Two families. Override phrasings ("ignore the above", "you are now a…"), and
51
+ * forged structure: role headers and the `--- section ---` delimiter this
52
+ * codebase actually uses to fence prompt sections (see
53
+ * `renderNegotiatorClientDmSection`), which is what a forged block would have
54
+ * to imitate to be read as one.
55
+ */
56
+ const PROMPT_INJECTION_PATTERN = /\b(?:ignore|disregard|forget|override|bypass)\b[^.!?\n]{0,40}\b(?:instructions?|prompts?|rules?|directives?|guidelines?|everything\s+above|the\s+above|all\s+of\s+the\s+above)\b|\byou\s+are\s+now\s+(?:an?|the)\b|\bnew\s+instructions?\s*:|^\s*(?:#{1,6}\s*|\*{2}\s*)?(?:system|assistant|developer|human)\s*:|<\|[^|>]{0,40}\|>|\[\/?(?:INST|SYS)\]|^\s*-{3,}[^\n]*-{3,}\s*$/im;
57
+ /**
58
+ * Whole-question gate for a question the NEGOTIATOR authored.
59
+ *
60
+ * The pre-A2H `disclosureSubject` never reached the client as written — the
61
+ * server templated the copy around it. An authored `StructuredQuestion` is
62
+ * rendered verbatim, so every visible field needs the same gate the subject
63
+ * got, and it needs it with the identifiers in hand.
64
+ *
65
+ * That is the capability the api-side `isSafeNegotiationQuestionPayload` lacks
66
+ * and cannot gain: at the DB boundary it holds only generic patterns, so it
67
+ * cannot tell that a perfectly well-formed question is naming THIS counterparty
68
+ * or paraphrasing THIS seed assessment. The turn node has both, which is why
69
+ * this runs here and not there.
70
+ *
71
+ * Fail-closed and non-rewriting, like every guard in this file: a caller that
72
+ * gets `false` drops the question and keeps the enum-only path, rather than
73
+ * shipping a repaired one.
74
+ *
75
+ * @param options.forbiddenIdentifiers Names that must not appear as words —
76
+ * the counterparty's, at the call site.
77
+ * @param options.forbiddenSourceText Private inputs that must not be echoed —
78
+ * the seed assessment's reasoning, at the call site.
79
+ */
80
+ export function isSafeAuthoredNegotiationQuestion(question, options) {
81
+ if (!question || typeof question !== 'object')
82
+ return false;
83
+ // Re-checked rather than trusted from the schema: this gate also runs on
84
+ // turns that arrive from an external agent, and a rendered card with one
85
+ // option (or fifteen) is a broken card regardless of how it validated.
86
+ const questionOptions = question.options;
87
+ if (!Array.isArray(questionOptions) || questionOptions.length < 2 || questionOptions.length > 4)
88
+ return false;
89
+ const fields = [
90
+ question.title,
91
+ question.prompt,
92
+ ...questionOptions.flatMap((option) => [option?.label, option?.description]),
93
+ ];
94
+ for (const field of fields) {
95
+ if (typeof field !== 'string')
96
+ return false;
97
+ if (!isSafeNegotiationQuestionText(field, options))
98
+ return false;
99
+ if (PROMPT_INJECTION_PATTERN.test(field))
100
+ return false;
101
+ }
102
+ return true;
103
+ }
39
104
  /** Validate the only structured fields allowed to enter the inflight Questioner prompt. */
40
105
  export function validateInflightAskUserFields(input) {
41
106
  const disclosureSubject = input.disclosureSubject?.trim();
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The boundary between a negotiation and the conversation it runs in.
3
+ *
4
+ * Two agents share one DM permanently — `getOrCreateDM` keys on the agent pair
5
+ * alone — so that conversation accumulates every negotiation the pair has ever
6
+ * had. A negotiation is a bounded episode about ONE match inside it.
7
+ *
8
+ * Every question about a negotiation's own state (whose turn it is, whether it
9
+ * has opened, how many turns it has run) must be answered from the
10
+ * negotiation's messages. Conversation-wide reads remain valid for CONTEXT —
11
+ * what the pair discussed before — but must never decide state, or a fresh
12
+ * match inherits the turn parity of an unrelated concluded one.
13
+ *
14
+ * Every surface that resolves the floor shares this rule. If the graph and the
15
+ * respond/polling surfaces disagreed about a negotiation's scope, an external
16
+ * agent would be told it is not its turn forever.
17
+ */
18
+ /** Minimal task-metadata shape needed to identify a negotiation. */
19
+ export interface NegotiationScopeMetadata {
20
+ opportunityId?: unknown;
21
+ }
22
+ /**
23
+ * The opportunity a negotiation belongs to, or null when the task carries no
24
+ * opportunity. Keyed on opportunity rather than task because an `ask_user`
25
+ * pause resumes into a successor task: both tasks' turns are one negotiation.
26
+ */
27
+ export declare function negotiationScopeKey(metadata: NegotiationScopeMetadata | null | undefined): string | null;
28
+ /**
29
+ * Reads the messages that constitute one negotiation.
30
+ *
31
+ * A task with no opportunity has no identity separate from its conversation
32
+ * (direct and legacy invocations), so the conversation is its scope — there is
33
+ * no other match it could be confused with.
34
+ */
35
+ export declare function readNegotiationMessages<M>(readers: {
36
+ byNegotiation: (opportunityId: string) => Promise<M[]>;
37
+ byConversation: (conversationId: string) => Promise<M[]>;
38
+ }, scope: {
39
+ conversationId: string;
40
+ metadata: NegotiationScopeMetadata | null | undefined;
41
+ }): Promise<M[]>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The boundary between a negotiation and the conversation it runs in.
3
+ *
4
+ * Two agents share one DM permanently — `getOrCreateDM` keys on the agent pair
5
+ * alone — so that conversation accumulates every negotiation the pair has ever
6
+ * had. A negotiation is a bounded episode about ONE match inside it.
7
+ *
8
+ * Every question about a negotiation's own state (whose turn it is, whether it
9
+ * has opened, how many turns it has run) must be answered from the
10
+ * negotiation's messages. Conversation-wide reads remain valid for CONTEXT —
11
+ * what the pair discussed before — but must never decide state, or a fresh
12
+ * match inherits the turn parity of an unrelated concluded one.
13
+ *
14
+ * Every surface that resolves the floor shares this rule. If the graph and the
15
+ * respond/polling surfaces disagreed about a negotiation's scope, an external
16
+ * agent would be told it is not its turn forever.
17
+ */
18
+ /**
19
+ * The opportunity a negotiation belongs to, or null when the task carries no
20
+ * opportunity. Keyed on opportunity rather than task because an `ask_user`
21
+ * pause resumes into a successor task: both tasks' turns are one negotiation.
22
+ */
23
+ export function negotiationScopeKey(metadata) {
24
+ const id = metadata?.opportunityId;
25
+ return typeof id === 'string' && id.length > 0 ? id : null;
26
+ }
27
+ /**
28
+ * Reads the messages that constitute one negotiation.
29
+ *
30
+ * A task with no opportunity has no identity separate from its conversation
31
+ * (direct and legacy invocations), so the conversation is its scope — there is
32
+ * no other match it could be confused with.
33
+ */
34
+ export async function readNegotiationMessages(readers, scope) {
35
+ const opportunityId = negotiationScopeKey(scope.metadata);
36
+ return opportunityId
37
+ ? readers.byNegotiation(opportunityId)
38
+ : readers.byConversation(scope.conversationId);
39
+ }
@@ -59,10 +59,14 @@ export class NegotiationScreener {
59
59
  };
60
60
  // IND-569: prefer the attributed rendering (labeled per-opportunity blocks)
61
61
  // when the graph supplies it; otherwise fall back to the flat prior-turn list.
62
- const hasAttributed = input.isContinuation
63
- && input.priorDialogueAttributed != null
62
+ //
63
+ // Deliberately NOT gated on `isContinuation`, which means "this negotiation
64
+ // has already spoken". The gate this section exists for is the opposite
65
+ // case — a FRESH signal against a counterparty the client has prior dialogue
66
+ // with (IND-563), which is precisely when duplicate outreach must be caught.
67
+ const hasAttributed = input.priorDialogueAttributed != null
64
68
  && !attributedDialogueIsEmpty(input.priorDialogueAttributed);
65
- const flatPriorDialogue = input.isContinuation && input.priorDialogue && input.priorDialogue.length > 0
69
+ const flatPriorDialogue = input.priorDialogue && input.priorDialogue.length > 0
66
70
  ? input.priorDialogue
67
71
  : [];
68
72
  const priorDialogueBody = hasAttributed
@@ -40,10 +40,75 @@ export declare const NegotiationTurnSchema: z.ZodObject<{
40
40
  /** Present when action is `ask_user` (v2, P3.2). */
41
41
  askUser: z.ZodOptional<z.ZodNullable<z.ZodObject<{
42
42
  reason: z.ZodEnum<["unresolved_owner_constraint", "consequential_disclosure_permission", "repeated_non_convergence", "insufficient_commitment_authority"]>;
43
+ question: z.ZodEffects<z.ZodOptional<z.ZodNullable<z.ZodObject<{
44
+ title: z.ZodString;
45
+ prompt: z.ZodString;
46
+ options: z.ZodArray<z.ZodObject<{
47
+ label: z.ZodString;
48
+ description: z.ZodString;
49
+ }, "strip", z.ZodTypeAny, {
50
+ label: string;
51
+ description: string;
52
+ }, {
53
+ label: string;
54
+ description: string;
55
+ }>, "many">;
56
+ multiSelect: z.ZodBoolean;
57
+ }, "strip", z.ZodTypeAny, {
58
+ prompt: string;
59
+ options: {
60
+ label: string;
61
+ description: string;
62
+ }[];
63
+ title: string;
64
+ multiSelect: boolean;
65
+ }, {
66
+ prompt: string;
67
+ options: {
68
+ label: string;
69
+ description: string;
70
+ }[];
71
+ title: string;
72
+ multiSelect: boolean;
73
+ }>>>, {
74
+ prompt: string;
75
+ options: {
76
+ label: string;
77
+ description: string;
78
+ }[];
79
+ title: string;
80
+ multiSelect: boolean;
81
+ } | undefined, {
82
+ prompt: string;
83
+ options: {
84
+ label: string;
85
+ description: string;
86
+ }[];
87
+ title: string;
88
+ multiSelect: boolean;
89
+ } | null | undefined>;
43
90
  }, "strict", z.ZodTypeAny, {
44
91
  reason: "unresolved_owner_constraint" | "consequential_disclosure_permission" | "repeated_non_convergence" | "insufficient_commitment_authority";
92
+ question?: {
93
+ prompt: string;
94
+ options: {
95
+ label: string;
96
+ description: string;
97
+ }[];
98
+ title: string;
99
+ multiSelect: boolean;
100
+ } | undefined;
45
101
  }, {
46
102
  reason: "unresolved_owner_constraint" | "consequential_disclosure_permission" | "repeated_non_convergence" | "insufficient_commitment_authority";
103
+ question?: {
104
+ prompt: string;
105
+ options: {
106
+ label: string;
107
+ description: string;
108
+ }[];
109
+ title: string;
110
+ multiSelect: boolean;
111
+ } | null | undefined;
47
112
  }>>>;
48
113
  }, "strip", z.ZodTypeAny, {
49
114
  action: "propose" | "accept" | "reject" | "counter" | "question" | "outreach" | "withdraw" | "decline" | "ask_user";
@@ -57,6 +122,15 @@ export declare const NegotiationTurnSchema: z.ZodObject<{
57
122
  message?: string | null | undefined;
58
123
  askUser?: {
59
124
  reason: "unresolved_owner_constraint" | "consequential_disclosure_permission" | "repeated_non_convergence" | "insufficient_commitment_authority";
125
+ question?: {
126
+ prompt: string;
127
+ options: {
128
+ label: string;
129
+ description: string;
130
+ }[];
131
+ title: string;
132
+ multiSelect: boolean;
133
+ } | undefined;
60
134
  } | null | undefined;
61
135
  }, {
62
136
  action: "propose" | "accept" | "reject" | "counter" | "question" | "outreach" | "withdraw" | "decline" | "ask_user";
@@ -70,6 +144,15 @@ export declare const NegotiationTurnSchema: z.ZodObject<{
70
144
  message?: string | null | undefined;
71
145
  askUser?: {
72
146
  reason: "unresolved_owner_constraint" | "consequential_disclosure_permission" | "repeated_non_convergence" | "insufficient_commitment_authority";
147
+ question?: {
148
+ prompt: string;
149
+ options: {
150
+ label: string;
151
+ description: string;
152
+ }[];
153
+ title: string;
154
+ multiSelect: boolean;
155
+ } | null | undefined;
73
156
  } | null | undefined;
74
157
  }>;
75
158
  /** Restricted v1 turn schema for the system agent (no question action). */
@@ -414,6 +497,15 @@ export declare const NegotiationGraphState: import("@langchain/langgraph").Annot
414
497
  message?: string | null | undefined;
415
498
  askUser?: {
416
499
  reason: "unresolved_owner_constraint" | "consequential_disclosure_permission" | "repeated_non_convergence" | "insufficient_commitment_authority";
500
+ question?: {
501
+ prompt: string;
502
+ options: {
503
+ label: string;
504
+ description: string;
505
+ }[];
506
+ title: string;
507
+ multiSelect: boolean;
508
+ } | undefined;
417
509
  } | null | undefined;
418
510
  } | null, {
419
511
  action: "propose" | "accept" | "reject" | "counter" | "question" | "outreach" | "withdraw" | "decline" | "ask_user";
@@ -427,6 +519,15 @@ export declare const NegotiationGraphState: import("@langchain/langgraph").Annot
427
519
  message?: string | null | undefined;
428
520
  askUser?: {
429
521
  reason: "unresolved_owner_constraint" | "consequential_disclosure_permission" | "repeated_non_convergence" | "insufficient_commitment_authority";
522
+ question?: {
523
+ prompt: string;
524
+ options: {
525
+ label: string;
526
+ description: string;
527
+ }[];
528
+ title: string;
529
+ multiSelect: boolean;
530
+ } | undefined;
430
531
  } | null | undefined;
431
532
  } | import("@langchain/langgraph").OverwriteValue<{
432
533
  action: "propose" | "accept" | "reject" | "counter" | "question" | "outreach" | "withdraw" | "decline" | "ask_user";
@@ -440,6 +541,15 @@ export declare const NegotiationGraphState: import("@langchain/langgraph").Annot
440
541
  message?: string | null | undefined;
441
542
  askUser?: {
442
543
  reason: "unresolved_owner_constraint" | "consequential_disclosure_permission" | "repeated_non_convergence" | "insufficient_commitment_authority";
544
+ question?: {
545
+ prompt: string;
546
+ options: {
547
+ label: string;
548
+ description: string;
549
+ }[];
550
+ title: string;
551
+ multiSelect: boolean;
552
+ } | undefined;
443
553
  } | null | undefined;
444
554
  } | null> | null, unknown>;
445
555
  /**
@@ -9,6 +9,7 @@ import { readAuthorizedNegotiationDetail } from './negotiation.detail-reader.js'
9
9
  import { buildLifecycleNarration } from './negotiation.lifecycle-narration.js';
10
10
  import { isNegotiationTurnCapReached } from './negotiation.turn-cap.js';
11
11
  import { expectedNegotiationSpeaker } from './negotiation.expected-speaker.js';
12
+ import { readNegotiationMessages } from './negotiation.scope.js';
12
13
  export { buildLifecycleNarration } from './negotiation.lifecycle-narration.js';
13
14
  const logger = protocolLogger('ChatTools:Negotiation');
14
15
  /**
@@ -150,8 +151,12 @@ export function createNegotiationTools(defineTool, deps) {
150
151
  }
151
152
  const isSource = meta.sourceUserId === context.userId;
152
153
  const counterpartyId = isSource ? meta.candidateUserId : meta.sourceUserId;
153
- // Get messages for preview (and turns when narrative detail requested)
154
- const messages = await negotiationDatabase.getMessagesForConversation(task.conversationId);
154
+ // This negotiation's own turns: the preview, turn count and floor all
155
+ // describe THIS match, and the pair's DM also holds other matches.
156
+ const messages = await readNegotiationMessages({
157
+ byNegotiation: (id) => negotiationDatabase.getNegotiationMessages(id),
158
+ byConversation: (id) => negotiationDatabase.getMessagesForConversation(id),
159
+ }, { conversationId: task.conversationId, metadata: meta });
155
160
  const lastMessage = messages[messages.length - 1];
156
161
  const lastTurnData = lastMessage
157
162
  ? lastMessage.parts?.find(p => p.kind === 'data')?.data
@@ -296,7 +301,10 @@ export function createNegotiationTools(defineTool, deps) {
296
301
  metadata: meta,
297
302
  callerUserId: context.userId,
298
303
  callerRole: isSource ? 'source' : 'candidate',
299
- readMessages: (conversationId) => negotiationDatabase.getMessagesForConversation(conversationId),
304
+ readMessages: (conversationId) => readNegotiationMessages({
305
+ byNegotiation: (id) => negotiationDatabase.getNegotiationMessages(id),
306
+ byConversation: (id) => negotiationDatabase.getMessagesForConversation(id),
307
+ }, { conversationId, metadata: meta }),
300
308
  readArtifacts: (taskId) => negotiationDatabase.getArtifactsForTask(taskId),
301
309
  readLifecycleEvidence: (opportunityIds, ownerUserId) => readOpportunityLifecycles(negotiationDatabase, opportunityIds, ownerUserId),
302
310
  });
@@ -386,7 +394,10 @@ export function createNegotiationTools(defineTool, deps) {
386
394
  if (!allowedActionsFor(protocolVersion, seat).includes(query.action)) {
387
395
  return error(seatViolationMessage(query.action, seat, protocolVersion));
388
396
  }
389
- const messages = await negotiationDatabase.getMessagesForConversation(task.conversationId);
397
+ const messages = await readNegotiationMessages({
398
+ byNegotiation: (id) => negotiationDatabase.getNegotiationMessages(id),
399
+ byConversation: (id) => negotiationDatabase.getMessagesForConversation(id),
400
+ }, { conversationId: task.conversationId, metadata: meta });
390
401
  const turnCount = messages.length;
391
402
  const expectedSpeaker = expectedNegotiationSpeaker(meta, messages);
392
403
  if (expectedSpeaker !== context.userId) {
@@ -24,7 +24,7 @@ import type { NegotiationOutcome, NegotiationTurn } from '../negotiations/negoti
24
24
  * Narrow slice of {@link NegotiationGraphDatabase} required by the loader. Kept
25
25
  * minimal so call sites can opt into a smaller surface.
26
26
  */
27
- export type NegotiationContextDatabase = Pick<NegotiationGraphDatabase, 'getNegotiationTaskForOpportunity' | 'getMessagesForConversation' | 'getArtifactsForTask'>;
27
+ export type NegotiationContextDatabase = Pick<NegotiationGraphDatabase, 'getNegotiationTaskForOpportunity' | 'getNegotiationMessages' | 'getArtifactsForTask'>;
28
28
  /**
29
29
  * Snapshot of a negotiation surfaced to the presenter. `turns` and `outcome`
30
30
  * are only populated for post-negotiation statuses (pending/stalled/
@@ -42,7 +42,9 @@ export async function loadNegotiationContext(db, opportunityId, opportunityStatu
42
42
  return null;
43
43
  }
44
44
  const turnCap = readNumber(task.metadata, 'maxTurns') ?? 0;
45
- const messages = await db.getMessagesForConversation(task.conversationId);
45
+ // Scoped to this opportunity: `turnCount`/`turns` describe THIS negotiation,
46
+ // and the pair's shared DM also holds concluded negotiations for other matches.
47
+ const messages = await db.getNegotiationMessages(opportunityId);
46
48
  const turns = extractTurns(messages);
47
49
  const turnCount = turns.length;
48
50
  if (opportunityStatus === 'negotiating') {
@@ -7,29 +7,21 @@
7
7
  */
8
8
  import { z } from "zod";
9
9
  import { UnderspecificationTypeSchema, type UnderspecificationType } from "../shared/schemas/underspecification.schema.js";
10
+ import { QuestionOptionSchema, StructuredQuestionSchema, type QuestionOption } from "../shared/schemas/structured-question.schema.js";
10
11
  export { UnderspecificationTypeSchema };
11
- export declare const QuestionOptionSchema: z.ZodObject<{
12
- /** Display text. Suffix " (Recommended)" on the safest path; list it first. */
13
- label: z.ZodString;
14
- /** Explains the consequence of choosing this option, not just its definition. */
15
- description: z.ZodString;
16
- }, "strip", z.ZodTypeAny, {
17
- label: string;
18
- description: string;
19
- }, {
20
- label: string;
21
- description: string;
22
- }>;
12
+ /**
13
+ * The renderer-facing quartet (title/prompt/options/multiSelect) lives in
14
+ * `shared/schemas/structured-question.schema.ts` so the negotiator can author a
15
+ * question without `shared/` importing this capability. Re-exported here so
16
+ * `questions/question.schema.js` stays the import site every caller already uses.
17
+ */
18
+ export { QuestionOptionSchema, StructuredQuestionSchema };
19
+ export type { StructuredQuestion } from "../shared/schemas/structured-question.schema.js";
23
20
  export declare const QuestionSchema: z.ZodObject<{
24
- /** ≤12 chars. Noun of the decision domain — e.g. "Stage", "Timing", "Role". */
25
21
  title: z.ZodString;
26
- /** ≤2 sentences, ≤400 chars. Ends in a question mark. */
27
22
  prompt: z.ZodString;
28
- /** 2–4 options. No explicit "Other" — clients provide that automatically. */
29
23
  options: z.ZodArray<z.ZodObject<{
30
- /** Display text. Suffix " (Recommended)" on the safest path; list it first. */
31
24
  label: z.ZodString;
32
- /** Explains the consequence of choosing this option, not just its definition. */
33
25
  description: z.ZodString;
34
26
  }, "strip", z.ZodTypeAny, {
35
27
  label: string;
@@ -38,8 +30,8 @@ export declare const QuestionSchema: z.ZodObject<{
38
30
  label: string;
39
31
  description: string;
40
32
  }>, "many">;
41
- /** True when options are not mutually exclusive (priorities, bundles). */
42
33
  multiSelect: z.ZodBoolean;
34
+ } & {
43
35
  /**
44
36
  * Optional provenance line rendered as a muted chip above the prompt
45
37
  * (e.g. "based on 18 people matching this intent"). Aggregate counts only —
@@ -77,15 +69,10 @@ export declare const QuestionSchema: z.ZodObject<{
77
69
  }>;
78
70
  export declare const QuestionStrategySchema: z.ZodEnum<["refine_intent", "surface_missing_detail", "open_adjacent_thread", "reflective_summary", "surface_emergent_knowledge"]>;
79
71
  export declare const QuestionWithStrategySchema: z.ZodObject<{
80
- /** ≤12 chars. Noun of the decision domain — e.g. "Stage", "Timing", "Role". */
81
72
  title: z.ZodString;
82
- /** ≤2 sentences, ≤400 chars. Ends in a question mark. */
83
73
  prompt: z.ZodString;
84
- /** 2–4 options. No explicit "Other" — clients provide that automatically. */
85
74
  options: z.ZodArray<z.ZodObject<{
86
- /** Display text. Suffix " (Recommended)" on the safest path; list it first. */
87
75
  label: z.ZodString;
88
- /** Explains the consequence of choosing this option, not just its definition. */
89
76
  description: z.ZodString;
90
77
  }, "strip", z.ZodTypeAny, {
91
78
  label: string;
@@ -94,8 +81,8 @@ export declare const QuestionWithStrategySchema: z.ZodObject<{
94
81
  label: string;
95
82
  description: string;
96
83
  }>, "many">;
97
- /** True when options are not mutually exclusive (priorities, bundles). */
98
84
  multiSelect: z.ZodBoolean;
85
+ } & {
99
86
  /**
100
87
  * Optional provenance line rendered as a muted chip above the prompt
101
88
  * (e.g. "based on 18 people matching this intent"). Aggregate counts only —
@@ -141,15 +128,10 @@ export declare const QuestionWithStrategySchema: z.ZodObject<{
141
128
  }>;
142
129
  export declare const QuestionGeneratorResponseSchema: z.ZodObject<{
143
130
  questions: z.ZodArray<z.ZodObject<{
144
- /** ≤12 chars. Noun of the decision domain — e.g. "Stage", "Timing", "Role". */
145
131
  title: z.ZodString;
146
- /** ≤2 sentences, ≤400 chars. Ends in a question mark. */
147
132
  prompt: z.ZodString;
148
- /** 2–4 options. No explicit "Other" — clients provide that automatically. */
149
133
  options: z.ZodArray<z.ZodObject<{
150
- /** Display text. Suffix " (Recommended)" on the safest path; list it first. */
151
134
  label: z.ZodString;
152
- /** Explains the consequence of choosing this option, not just its definition. */
153
135
  description: z.ZodString;
154
136
  }, "strip", z.ZodTypeAny, {
155
137
  label: string;
@@ -158,8 +140,8 @@ export declare const QuestionGeneratorResponseSchema: z.ZodObject<{
158
140
  label: string;
159
141
  description: string;
160
142
  }>, "many">;
161
- /** True when options are not mutually exclusive (priorities, bundles). */
162
143
  multiSelect: z.ZodBoolean;
144
+ } & {
163
145
  /**
164
146
  * Optional provenance line rendered as a muted chip above the prompt
165
147
  * (e.g. "based on 18 people matching this intent"). Aggregate counts only —
@@ -230,7 +212,7 @@ export declare const QuestionGeneratorResponseSchema: z.ZodObject<{
230
212
  evidence?: string | null | undefined;
231
213
  }[];
232
214
  }>;
233
- export type QuestionOption = z.infer<typeof QuestionOptionSchema>;
215
+ export type { QuestionOption };
234
216
  export type Question = z.infer<typeof QuestionSchema>;
235
217
  export type { UnderspecificationType };
236
218
  export type QuestionStrategy = z.infer<typeof QuestionStrategySchema>;
@@ -7,22 +7,16 @@
7
7
  */
8
8
  import { z } from "zod";
9
9
  import { UnderspecificationTypeSchema } from "../shared/schemas/underspecification.schema.js";
10
+ import { QuestionOptionSchema, StructuredQuestionSchema } from "../shared/schemas/structured-question.schema.js";
10
11
  export { UnderspecificationTypeSchema };
11
- export const QuestionOptionSchema = z.object({
12
- /** Display text. Suffix " (Recommended)" on the safest path; list it first. */
13
- label: z.string().min(1).max(120),
14
- /** Explains the consequence of choosing this option, not just its definition. */
15
- description: z.string().min(1).max(280),
16
- });
17
- export const QuestionSchema = z.object({
18
- /** ≤12 chars. Noun of the decision domain — e.g. "Stage", "Timing", "Role". */
19
- title: z.string().min(1).max(12),
20
- /** ≤2 sentences, ≤400 chars. Ends in a question mark. */
21
- prompt: z.string().min(1).max(400),
22
- /** 2–4 options. No explicit "Other" — clients provide that automatically. */
23
- options: z.array(QuestionOptionSchema).min(2).max(4),
24
- /** True when options are not mutually exclusive (priorities, bundles). */
25
- multiSelect: z.boolean(),
12
+ /**
13
+ * The renderer-facing quartet (title/prompt/options/multiSelect) lives in
14
+ * `shared/schemas/structured-question.schema.ts` so the negotiator can author a
15
+ * question without `shared/` importing this capability. Re-exported here so
16
+ * `questions/question.schema.js` stays the import site every caller already uses.
17
+ */
18
+ export { QuestionOptionSchema, StructuredQuestionSchema };
19
+ export const QuestionSchema = StructuredQuestionSchema.extend({
26
20
  /**
27
21
  * Optional provenance line rendered as a muted chip above the prompt
28
22
  * (e.g. "based on 18 people matching this intent"). Aggregate counts only —
@@ -97,4 +97,4 @@ export type HydeGraphDatabase = Pick<Database, 'getHydeDocument' | 'getHydeDocum
97
97
  *
98
98
  * Access layer: UserDatabase (own opportunities and profile)
99
99
  */
100
- export type RadarGraphDatabase = Pick<Database, 'getOpportunitiesForUser' | 'getOpportunity' | 'getProfile' | 'getActiveIntents' | 'getNetwork' | 'getUser'> & Pick<NegotiationGraphDatabase, 'getNegotiationTaskForOpportunity' | 'getMessagesForConversation' | 'getArtifactsForTask'>;
100
+ export type RadarGraphDatabase = Pick<Database, 'getOpportunitiesForUser' | 'getOpportunity' | 'getProfile' | 'getActiveIntents' | 'getNetwork' | 'getUser'> & Pick<NegotiationGraphDatabase, 'getNegotiationTaskForOpportunity' | 'getNegotiationMessages' | 'getArtifactsForTask'>;
@@ -274,6 +274,30 @@ export type NegotiationGraphDatabase = Pick<Database, 'getOrCreateDM' | 'getUser
274
274
  createdAt: Date;
275
275
  taskId?: string | null;
276
276
  }>>;
277
+ /**
278
+ * Gets the messages belonging to ONE negotiation — those written by tasks
279
+ * carrying `type: 'negotiation'` and the given `opportunityId` — ordered by
280
+ * creation time.
281
+ *
282
+ * A negotiation is keyed by opportunity, not by task: an `ask_user` pause
283
+ * parks its task and resumes into a pre-claimed successor, so one negotiation
284
+ * spans several tasks. This is the read behind every question ABOUT a
285
+ * negotiation (whose turn it is, whether it has opened, how many turns it has
286
+ * run). `getMessagesForConversation` remains the read for conversation-wide
287
+ * CONTEXT — prior matches between the same pair — which must never determine
288
+ * a negotiation's own state.
289
+ *
290
+ * Messages with no `taskId`, or whose task carries no opportunityId, are not
291
+ * part of any negotiation and are never returned here.
292
+ */
293
+ getNegotiationMessages(opportunityId: string): Promise<Array<{
294
+ id: string;
295
+ senderId: string;
296
+ role: 'user' | 'agent';
297
+ parts: unknown[];
298
+ createdAt: Date;
299
+ taskId?: string | null;
300
+ }>>;
277
301
  /** Gets artifacts for a task (e.g. negotiation outcome). */
278
302
  getArtifactsForTask(taskId: string): Promise<Array<{
279
303
  id: string;