@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.
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/negotiations/negotiation.agent.d.ts +13 -0
- package/dist/negotiations/negotiation.agent.js +64 -6
- package/dist/negotiations/negotiation.client-dm.d.ts +50 -0
- package/dist/negotiations/negotiation.client-dm.js +66 -0
- package/dist/negotiations/negotiation.detail-reader.js +4 -1
- package/dist/negotiations/negotiation.expected-speaker.d.ts +16 -2
- package/dist/negotiations/negotiation.expected-speaker.js +15 -3
- package/dist/negotiations/negotiation.graph.d.ts +110 -1
- package/dist/negotiations/negotiation.graph.init.js +51 -22
- package/dist/negotiations/negotiation.graph.js +2 -1
- package/dist/negotiations/negotiation.graph.shared.d.ts +22 -3
- package/dist/negotiations/negotiation.graph.shared.js +31 -4
- package/dist/negotiations/negotiation.graph.turn.d.ts +27 -0
- package/dist/negotiations/negotiation.graph.turn.js +64 -2
- package/dist/negotiations/negotiation.module.d.ts +4 -1
- package/dist/negotiations/negotiation.module.js +2 -1
- package/dist/negotiations/negotiation.protocol.d.ts +498 -0
- package/dist/negotiations/negotiation.question-safety.d.ts +28 -0
- package/dist/negotiations/negotiation.question-safety.js +65 -0
- package/dist/negotiations/negotiation.scope.d.ts +41 -0
- package/dist/negotiations/negotiation.scope.js +39 -0
- package/dist/negotiations/negotiation.screen.js +7 -3
- package/dist/negotiations/negotiation.state.d.ts +110 -0
- package/dist/negotiations/negotiation.tools.js +15 -4
- package/dist/opportunities/negotiation-context.loader.d.ts +1 -1
- package/dist/opportunities/negotiation-context.loader.js +3 -1
- package/dist/questions/question.schema.d.ts +13 -31
- package/dist/questions/question.schema.js +9 -15
- package/dist/shared/interfaces/database.capabilities.d.ts +1 -1
- package/dist/shared/interfaces/database.negotiation.d.ts +24 -0
- package/dist/shared/schemas/negotiation-state.schema.d.ts +182 -3
- package/dist/shared/schemas/negotiation-state.schema.js +23 -3
- package/dist/shared/schemas/structured-question.schema.d.ts +66 -0
- package/dist/shared/schemas/structured-question.schema.js +31 -0
- 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
|
-
|
|
63
|
-
|
|
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.
|
|
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
|
-
//
|
|
154
|
-
|
|
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) =>
|
|
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
|
|
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' | '
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export
|
|
18
|
-
|
|
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' | '
|
|
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;
|