@indexnetwork/protocol 21.0.0-rc.488.1 → 21.0.0-rc.489.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 +2 -2
- package/dist/index.js +1 -1
- package/dist/negotiations/negotiation.agent.js +12 -4
- 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.init.js +51 -22
- package/dist/negotiations/negotiation.graph.shared.d.ts +5 -3
- package/dist/negotiations/negotiation.graph.shared.js +8 -4
- package/dist/negotiations/negotiation.module.d.ts +2 -0
- package/dist/negotiations/negotiation.module.js +1 -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.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/shared/interfaces/database.capabilities.d.ts +1 -1
- package/dist/shared/interfaces/database.negotiation.d.ts +24 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -125,8 +125,8 @@ export type { NegotiationAction, NegotiationConsultationReason, NegotiationSeat,
|
|
|
125
125
|
export type { NegotiationGraphLike } from "./negotiations/negotiation.module.js";
|
|
126
126
|
export { HERMES_OWNER_DIRECTIVE, HermesNegotiationResponseSchema, allowedHermesActionsFor, buildHermesNegotiationTurn, } from "./negotiations/negotiation.module.js";
|
|
127
127
|
export type { HermesNegotiationAction, HermesNegotiationResponse, } from "./negotiations/negotiation.module.js";
|
|
128
|
-
export { isNegotiationTurnCapReached, expectedNegotiationSpeaker, allowedActionsFor, askUserAnswerWindowMs, configuredAskUserEnabled, isTerminalAction, isRejectLikeAction, readProtocolVersion, resolveSeat, seatViolationMessage } from "./negotiations/negotiation.module.js";
|
|
129
|
-
export type { NegotiationSpeakerParticipants, NegotiationSpeakerMessage } from "./negotiations/negotiation.module.js";
|
|
128
|
+
export { isNegotiationTurnCapReached, expectedNegotiationSpeaker, negotiationScopeKey, readNegotiationMessages, allowedActionsFor, askUserAnswerWindowMs, configuredAskUserEnabled, isTerminalAction, isRejectLikeAction, readProtocolVersion, resolveSeat, seatViolationMessage } from "./negotiations/negotiation.module.js";
|
|
129
|
+
export type { NegotiationSpeakerParticipants, NegotiationSpeakerMessage, NegotiationScopeMetadata } from "./negotiations/negotiation.module.js";
|
|
130
130
|
export { assessConsultationEligibility, consultationPromptFor, negotiationConsultationPolicyMode } from "./negotiations/negotiation.module.js";
|
|
131
131
|
export type { ConsultationEligibility, ConsultationEligibilityInput, NegotiationConsultationPolicyMode } from "./negotiations/negotiation.module.js";
|
|
132
132
|
export { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, NEGOTIATION_QUESTION_GENERIC_UPTAKE_ACTIVITY, negotiationQuestionSettlementId, } from "./negotiations/negotiation.module.js";
|
package/dist/index.js
CHANGED
|
@@ -112,6 +112,6 @@ export { CANONICAL_MCP_CAPABILITY_POLICY_OPTIONS } from "./mcp/mcp.authorization
|
|
|
112
112
|
export { NegotiationConsultationReasonSchema } from "./shared/schemas/negotiation-state.schema.js";
|
|
113
113
|
export { HERMES_OWNER_DIRECTIVE, HermesNegotiationResponseSchema, allowedHermesActionsFor, buildHermesNegotiationTurn, } from "./negotiations/negotiation.module.js";
|
|
114
114
|
// ─── Negotiation seat rules (v2 client-advocate protocol) ───────────────────
|
|
115
|
-
export { isNegotiationTurnCapReached, expectedNegotiationSpeaker, allowedActionsFor, askUserAnswerWindowMs, configuredAskUserEnabled, isTerminalAction, isRejectLikeAction, readProtocolVersion, resolveSeat, seatViolationMessage } from "./negotiations/negotiation.module.js";
|
|
115
|
+
export { isNegotiationTurnCapReached, expectedNegotiationSpeaker, negotiationScopeKey, readNegotiationMessages, allowedActionsFor, askUserAnswerWindowMs, configuredAskUserEnabled, isTerminalAction, isRejectLikeAction, readProtocolVersion, resolveSeat, seatViolationMessage } from "./negotiations/negotiation.module.js";
|
|
116
116
|
export { assessConsultationEligibility, consultationPromptFor, negotiationConsultationPolicyMode } from "./negotiations/negotiation.module.js";
|
|
117
117
|
export { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, NEGOTIATION_QUESTION_GENERIC_UPTAKE_ACTIVITY, negotiationQuestionSettlementId, } from "./negotiations/negotiation.module.js";
|
|
@@ -187,11 +187,19 @@ ${stanceQuerySatisfiedRule(stance, otherName, userName)}`
|
|
|
187
187
|
const attributionPolicy = hasAttributedDialogue
|
|
188
188
|
? ' Prior turns from OTHER opportunities are background only — do not treat their conclusions as decisions about this opportunity.'
|
|
189
189
|
: '';
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
// The pair's shared DM carries every negotiation they have ever had. It is
|
|
191
|
+
// rendered whenever it exists — NOT only on continuations — because a fresh
|
|
192
|
+
// match in a long-running DM is exactly the case where that context is
|
|
193
|
+
// worth having. What it may not do is stand in for this negotiation's own
|
|
194
|
+
// exchange, so the policy line differs by whether this one has opened.
|
|
195
|
+
const hasPriorDialogue = hasAttributedDialogue || input.history.length > 0;
|
|
196
|
+
const priorDialoguePolicy = input.isContinuation
|
|
197
|
+
? 'Policy: You are continuing a prior dialogue. If this signal is materially the same as one you previously evaluated, you may resolve quickly. If materially different, evaluate on its own merits.'
|
|
198
|
+
: 'Policy: This signal is NEW — you have not negotiated it before. The dialogue above concluded on other signals and is background only. Evaluate this one on its own merits and make your own case for it.';
|
|
199
|
+
const priorDialogueContext = hasPriorDialogue
|
|
192
200
|
? `\n\n--- Prior dialogue with this counterparty ---\n${attributionPreamble}${priorDialogueBody}\n\n--- New signal under evaluation ---\n${input.discoveryQuery
|
|
193
201
|
? `Discovery query: "${input.discoveryQuery}"`
|
|
194
|
-
: `Seed assessment: ${input.seedAssessment.reasoning}`}\n\
|
|
202
|
+
: `Seed assessment: ${input.seedAssessment.reasoning}`}\n\n${priorDialoguePolicy}${attributionPolicy}`
|
|
195
203
|
: '';
|
|
196
204
|
const userAnswersContext = input.userAnswers && input.userAnswers.length > 0
|
|
197
205
|
? `\n\n--- ${userName}'s additional context (provided between sessions) ---\n${input.userAnswers.map((a) => {
|
|
@@ -222,7 +230,7 @@ Skills: ${input.otherUser.profile.skills?.join(", ") ?? "N/A"}
|
|
|
222
230
|
Intents:
|
|
223
231
|
${input.otherUser.intents.map((i) => `- ${i.title}: ${i.description}`).join("\n")}
|
|
224
232
|
|
|
225
|
-
Why this match was suggested: ${input.seedAssessment.reasoning}${
|
|
233
|
+
Why this match was suggested: ${input.seedAssessment.reasoning}${hasPriorDialogue ? priorDialogueContext : historyText}${userAnswersContext}${privateConsultationContext}
|
|
226
234
|
${discoveryQueryReminder}
|
|
227
235
|
${input.history.length === 0 && !input.isContinuation ? (version === "v2" && seat === "initiator" ? "This is the opening turn. Make the outreach case." : "This is the opening turn. Propose the connection case.") : "Evaluate the latest arguments and respond."}`;
|
|
228
236
|
const chatMessages = [
|
|
@@ -77,7 +77,10 @@ export async function readAuthorizedNegotiationDetail(input) {
|
|
|
77
77
|
isUsersTurn,
|
|
78
78
|
isContinuation: metadata.isContinuation ?? false,
|
|
79
79
|
priorTurnCount,
|
|
80
|
-
|
|
80
|
+
// Clamped: `turnCount` is match-scoped, but a task stamped before that
|
|
81
|
+
// change carries a conversation-wide `priorTurnCount`, which would other-
|
|
82
|
+
// wise report a negative delta for negotiations in flight across the deploy.
|
|
83
|
+
turnsAdded: Math.max(0, turnCount - priorTurnCount),
|
|
81
84
|
turns,
|
|
82
85
|
outcome,
|
|
83
86
|
lifecycle: buildLifecycleNarration(status, lifecycleOpportunityId ? opportunityLifecycles[lifecycleOpportunityId] : undefined),
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export interface NegotiationSpeakerParticipants {
|
|
2
2
|
sourceUserId?: unknown;
|
|
3
3
|
candidateUserId?: unknown;
|
|
4
|
+
/**
|
|
5
|
+
* The seat that opens this negotiation (v2 stamp). An unopened negotiation
|
|
6
|
+
* starts with its initiator; `sourceUserId` is the pre-stamp fallback, which
|
|
7
|
+
* is what the stamp defaults to anyway.
|
|
8
|
+
*/
|
|
9
|
+
initiatorUserId?: unknown;
|
|
4
10
|
}
|
|
5
11
|
export interface NegotiationSpeakerMessage {
|
|
6
12
|
senderId?: unknown;
|
|
@@ -9,11 +15,19 @@ export interface NegotiationSpeakerMessage {
|
|
|
9
15
|
/**
|
|
10
16
|
* Resolves the participant whose agent owns the next canonical bilateral turn.
|
|
11
17
|
*
|
|
18
|
+
* `messages` must be THIS negotiation's messages (see
|
|
19
|
+
* `getNegotiationMessages`), never the whole conversation. Two agents share one
|
|
20
|
+
* DM across every match they are ever paired on, so conversation-scoped parity
|
|
21
|
+
* makes a fresh negotiation inherit the turn order of an unrelated, concluded
|
|
22
|
+
* one — handing the floor to the counterparty, who may `accept` immediately and
|
|
23
|
+
* conclude the match before its initiator has spoken.
|
|
24
|
+
*
|
|
12
25
|
* Participant identities must be nonempty and distinct. Unrelated agent,
|
|
13
26
|
* system, and owner-settlement messages are ignored while finding the latest
|
|
14
27
|
* source/candidate message. An ordinary canonical message passes the floor to
|
|
15
28
|
* the other participant; `ask_user` retains it for the consulting sender's
|
|
16
|
-
* exact successor. A
|
|
17
|
-
*
|
|
29
|
+
* exact successor. A negotiation with no canonical history of its own has not
|
|
30
|
+
* opened yet, so it starts with its initiator. Invalid participant metadata
|
|
31
|
+
* always fails closed.
|
|
18
32
|
*/
|
|
19
33
|
export declare function expectedNegotiationSpeaker(participants: NegotiationSpeakerParticipants, messages: readonly NegotiationSpeakerMessage[]): string | null;
|
|
@@ -18,12 +18,20 @@ function participantId(value) {
|
|
|
18
18
|
/**
|
|
19
19
|
* Resolves the participant whose agent owns the next canonical bilateral turn.
|
|
20
20
|
*
|
|
21
|
+
* `messages` must be THIS negotiation's messages (see
|
|
22
|
+
* `getNegotiationMessages`), never the whole conversation. Two agents share one
|
|
23
|
+
* DM across every match they are ever paired on, so conversation-scoped parity
|
|
24
|
+
* makes a fresh negotiation inherit the turn order of an unrelated, concluded
|
|
25
|
+
* one — handing the floor to the counterparty, who may `accept` immediately and
|
|
26
|
+
* conclude the match before its initiator has spoken.
|
|
27
|
+
*
|
|
21
28
|
* Participant identities must be nonempty and distinct. Unrelated agent,
|
|
22
29
|
* system, and owner-settlement messages are ignored while finding the latest
|
|
23
30
|
* source/candidate message. An ordinary canonical message passes the floor to
|
|
24
31
|
* the other participant; `ask_user` retains it for the consulting sender's
|
|
25
|
-
* exact successor. A
|
|
26
|
-
*
|
|
32
|
+
* exact successor. A negotiation with no canonical history of its own has not
|
|
33
|
+
* opened yet, so it starts with its initiator. Invalid participant metadata
|
|
34
|
+
* always fails closed.
|
|
27
35
|
*/
|
|
28
36
|
export function expectedNegotiationSpeaker(participants, messages) {
|
|
29
37
|
const source = participantId(participants.sourceUserId);
|
|
@@ -43,5 +51,9 @@ export function expectedNegotiationSpeaker(participants, messages) {
|
|
|
43
51
|
? sender
|
|
44
52
|
: sender === source ? candidate : source;
|
|
45
53
|
}
|
|
46
|
-
|
|
54
|
+
// Unopened: the initiator seat speaks first. Mirrors the
|
|
55
|
+
// `initiatorUserId ?? sourceUserId` precedence in `resolveSeat`, and falls
|
|
56
|
+
// back to source when the stamp is absent or names a non-participant.
|
|
57
|
+
const initiator = participantId(participants.initiatorUserId);
|
|
58
|
+
return initiator === candidate ? candidate : source;
|
|
47
59
|
}
|
|
@@ -5,6 +5,7 @@ import { configuredProtocolVersion, readProtocolVersion } from "./negotiation.pr
|
|
|
5
5
|
import { buildIntentSnapshots } from "./negotiation.intent-snapshot-provenance.js";
|
|
6
6
|
import { holdsNegotiationConversationLock } from "./negotiation.task-lock-policy.js";
|
|
7
7
|
import { expectedNegotiationSpeaker } from "./negotiation.expected-speaker.js";
|
|
8
|
+
import { readNegotiationMessages } from "./negotiation.scope.js";
|
|
8
9
|
import { buildSeededAttribution } from './negotiation.attribution.js';
|
|
9
10
|
import { initLog, resolveTaskAttribution, turnsFromMessages } from "./negotiation.graph.shared.js";
|
|
10
11
|
export async function initNode(state, deps) {
|
|
@@ -18,7 +19,12 @@ export async function initNode(state, deps) {
|
|
|
18
19
|
? { id: execution.conversationId }
|
|
19
20
|
: await deps.database.getOrCreateDM(agentIdA, agentIdB, 'agent');
|
|
20
21
|
// --- Lock gate: check for an active task on this conversation ---
|
|
21
|
-
|
|
22
|
+
// Two reads with two jobs. `conversationMessages` is the pair's whole shared
|
|
23
|
+
// DM — CONTEXT, including negotiations for other matches, which reaches the
|
|
24
|
+
// agent only as labelled prior dialogue. `negotiationMessages` is THIS
|
|
25
|
+
// match's own turns, and is the sole input to this negotiation's state:
|
|
26
|
+
// whether it has opened, whose turn it is, how far it has run.
|
|
27
|
+
const conversationMessages = await deps.database.getMessagesForConversation(conversation.id);
|
|
22
28
|
if (Boolean(state.resumeFromTaskId) !== Boolean(state.continuationSettlementId)
|
|
23
29
|
|| Boolean(state.resumeFromTaskId) !== Boolean(execution))
|
|
24
30
|
return { error: 'invalid continuation correlation' };
|
|
@@ -57,18 +63,24 @@ export async function initNode(state, deps) {
|
|
|
57
63
|
});
|
|
58
64
|
return { error: 'busy' };
|
|
59
65
|
}
|
|
60
|
-
// --- Load
|
|
61
|
-
|
|
66
|
+
// --- Load this negotiation's own prior turns ---
|
|
67
|
+
// Resolved through the shared scope rule, not a local copy of it: the graph
|
|
68
|
+
// and the respond/polling surfaces must agree on what "this negotiation's
|
|
69
|
+
// messages" means, or an external agent can be told it is not its turn
|
|
70
|
+
// forever. That rule also owns the unkeyed case — a run with no opportunity
|
|
71
|
+
// has no identity apart from its conversation.
|
|
72
|
+
const negotiationMessages = await readNegotiationMessages({
|
|
73
|
+
byNegotiation: (id) => deps.database.getNegotiationMessages(id),
|
|
74
|
+
byConversation: async () => conversationMessages,
|
|
75
|
+
}, {
|
|
76
|
+
conversationId: conversation.id,
|
|
77
|
+
metadata: { opportunityId: state.opportunityId },
|
|
78
|
+
});
|
|
79
|
+
const priorTurns = turnsFromMessages(negotiationMessages);
|
|
80
|
+
// `isContinuation` means THIS negotiation has already spoken — not that the
|
|
81
|
+
// pair has history. A fresh match in a long-running DM is not a
|
|
82
|
+
// continuation, and must still open.
|
|
62
83
|
const isContinuation = priorTurns.length > 0;
|
|
63
|
-
const expectedSpeaker = expectedNegotiationSpeaker({
|
|
64
|
-
sourceUserId: state.sourceUser.id,
|
|
65
|
-
candidateUserId: state.candidateUser.id,
|
|
66
|
-
}, priorMessages);
|
|
67
|
-
if (!expectedSpeaker)
|
|
68
|
-
return { error: 'invalid negotiation participants' };
|
|
69
|
-
const currentSpeaker = expectedSpeaker === state.sourceUser.id
|
|
70
|
-
? 'source'
|
|
71
|
-
: 'candidate';
|
|
72
84
|
// Determine scenario-based maxTurns
|
|
73
85
|
const scope = { action: 'manage:negotiations', scopeType: 'network', scopeId: state.indexContext.networkId };
|
|
74
86
|
const [sourceHasAgent, candidateHasAgent] = await Promise.all([
|
|
@@ -112,6 +124,19 @@ export async function initNode(state, deps) {
|
|
|
112
124
|
}
|
|
113
125
|
}
|
|
114
126
|
}
|
|
127
|
+
// --- Floor: derived from this negotiation's turns, after the seat is known ---
|
|
128
|
+
// Resolved here rather than above because an unopened negotiation starts
|
|
129
|
+
// with its initiator, which the tie-break may only just have settled.
|
|
130
|
+
const expectedSpeaker = expectedNegotiationSpeaker({
|
|
131
|
+
sourceUserId: state.sourceUser.id,
|
|
132
|
+
candidateUserId: state.candidateUser.id,
|
|
133
|
+
initiatorUserId,
|
|
134
|
+
}, negotiationMessages);
|
|
135
|
+
if (!expectedSpeaker)
|
|
136
|
+
return { error: 'invalid negotiation participants' };
|
|
137
|
+
const currentSpeaker = expectedSpeaker === state.sourceUser.id
|
|
138
|
+
? 'source'
|
|
139
|
+
: 'candidate';
|
|
115
140
|
// --- Protocol version: pinned per negotiation, re-stamped per match ---
|
|
116
141
|
// A prior task for this same negotiation (exact continuation resume or
|
|
117
142
|
// a re-run of the same opportunity) pins the version, so one
|
|
@@ -179,22 +204,26 @@ export async function initNode(state, deps) {
|
|
|
179
204
|
return [];
|
|
180
205
|
})
|
|
181
206
|
: [];
|
|
182
|
-
// Seed messages with prior turns (additive reducer
|
|
183
|
-
// taskId is preserved so the turn/screen nodes
|
|
184
|
-
// session's turns from
|
|
185
|
-
|
|
207
|
+
// Seed messages with THIS negotiation's prior turns (additive reducer
|
|
208
|
+
// appends new turns on top). taskId is preserved so the turn/screen nodes
|
|
209
|
+
// can separate this session's turns from earlier sessions of the same
|
|
210
|
+
// negotiation (IND-569).
|
|
211
|
+
const seedMessages = negotiationMessages.map((m) => ({
|
|
186
212
|
id: m.id,
|
|
187
213
|
senderId: m.senderId,
|
|
188
214
|
role: 'agent',
|
|
189
215
|
parts: m.parts,
|
|
190
216
|
createdAt: m.createdAt,
|
|
191
217
|
taskId: m.taskId ?? null,
|
|
192
|
-
}))
|
|
193
|
-
// IND-569: attribute
|
|
194
|
-
// once, up front. Earlier-
|
|
195
|
-
// immutable for the session; the current block is composed per
|
|
196
|
-
|
|
197
|
-
|
|
218
|
+
}));
|
|
219
|
+
// IND-569: attribute the pair's whole shared DM to its originating
|
|
220
|
+
// negotiations, once, up front. Earlier-match and legacy unattributed
|
|
221
|
+
// blocks are immutable for the session; the current block is composed per
|
|
222
|
+
// turn. Keyed on the conversation, not on `isContinuation`: a fresh match
|
|
223
|
+
// has no turns of its own but the pair's history is exactly the context
|
|
224
|
+
// worth carrying into it.
|
|
225
|
+
const priorAttribution = conversationMessages.length > 0
|
|
226
|
+
? await buildSeededAttribution(conversationMessages
|
|
198
227
|
.map((m) => ({ taskId: m.taskId ?? null, turn: turnsFromMessages([m])[0] }))
|
|
199
228
|
.filter((e) => Boolean(e.turn)), state.opportunityId, (taskId) => resolveTaskAttribution(deps, taskId))
|
|
200
229
|
: null;
|
|
@@ -43,9 +43,11 @@ export declare function turnsFromMessages(messages: Array<{
|
|
|
43
43
|
}>): NegotiationTurn[];
|
|
44
44
|
/**
|
|
45
45
|
* Whether `userId`'s side has already spent its one `ask_user` client
|
|
46
|
-
* consultation in
|
|
47
|
-
*
|
|
48
|
-
*
|
|
46
|
+
* consultation in THIS negotiation (P3.2 rationing: max one per negotiation per
|
|
47
|
+
* side). Callers pass `state.messages`, which carries this negotiation's turns
|
|
48
|
+
* across all of its sessions — so an earlier session of the same negotiation
|
|
49
|
+
* counts, while a consultation spent on a different match with the same
|
|
50
|
+
* counterparty does not.
|
|
49
51
|
*/
|
|
50
52
|
export declare function hasPriorAskUser(messages: Array<{
|
|
51
53
|
senderId: string;
|
|
@@ -25,9 +25,11 @@ export function turnsFromMessages(messages) {
|
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Whether `userId`'s side has already spent its one `ask_user` client
|
|
28
|
-
* consultation in
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* consultation in THIS negotiation (P3.2 rationing: max one per negotiation per
|
|
29
|
+
* side). Callers pass `state.messages`, which carries this negotiation's turns
|
|
30
|
+
* across all of its sessions — so an earlier session of the same negotiation
|
|
31
|
+
* counts, while a consultation spent on a different match with the same
|
|
32
|
+
* counterparty does not.
|
|
31
33
|
*/
|
|
32
34
|
export function hasPriorAskUser(messages, userId) {
|
|
33
35
|
const sender = `agent:${userId}`;
|
|
@@ -117,7 +119,9 @@ export async function resolveTaskAttribution(deps, taskId) {
|
|
|
117
119
|
* (task-id-matched). Null when there is no seeded attribution.
|
|
118
120
|
*/
|
|
119
121
|
export function buildAttributedDialogue(state) {
|
|
120
|
-
|
|
122
|
+
// Not gated on `isContinuation`: that now means "this negotiation has spoken",
|
|
123
|
+
// and the pair's earlier matches are context worth carrying into a fresh one.
|
|
124
|
+
if (!state.priorAttribution)
|
|
121
125
|
return null;
|
|
122
126
|
const currentSessionTurns = turnsFromMessages(state.messages.filter((m) => m.taskId === state.taskId));
|
|
123
127
|
const dialogue = combineAttributedDialogue(state.priorAttribution, currentSessionTurns);
|
|
@@ -17,6 +17,7 @@ export { ASK_USER_LOCK_SLACK_MS, allowedActionsFor, askUserAnswerWindowMs, confi
|
|
|
17
17
|
export { HERMES_OWNER_DIRECTIVE, HERMES_SHARED_MESSAGE_TEMPLATES, HermesNegotiationActionSchema, HermesNegotiationResponseSchema, HermesOwnerDirectiveSchema, HermesRoleAlignmentSchema, allowedHermesActionsFor, buildHermesNegotiationTurn, } from "./negotiation.hermes-contract.js";
|
|
18
18
|
export { DEFAULT_NEGOTIATION_MAX_TURNS, isNegotiationTurnCapReached } from "./negotiation.turn-cap.js";
|
|
19
19
|
export { expectedNegotiationSpeaker } from "./negotiation.expected-speaker.js";
|
|
20
|
+
export { negotiationScopeKey, readNegotiationMessages } from "./negotiation.scope.js";
|
|
20
21
|
export { assessConsultationEligibility, consultationPromptFor, negotiationConsultationPolicyMode, } from "./negotiation.consultation-policy.js";
|
|
21
22
|
export { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, NEGOTIATION_QUESTION_GENERIC_UPTAKE_ACTIVITY, isSafeNegotiationQuestionText, negotiationQuestionSettlementId, validateInflightAskUserFields, } from "./negotiation.question-safety.js";
|
|
22
23
|
export { renderNegotiatorChatMemorySection } from "./negotiation.memory.js";
|
|
@@ -24,5 +25,6 @@ export type { ConsultationEligibility, ConsultationEligibilityInput, Negotiation
|
|
|
24
25
|
export type { HermesNegotiationAction, HermesNegotiationResponse, HermesOwnerDirective, HermesRoleAlignment, } from "./negotiation.hermes-contract.js";
|
|
25
26
|
export type { NegotiationGraphLike, NegotiationOutcome, NegotiationTurn, UserNegotiationContext, } from "./negotiation.state.js";
|
|
26
27
|
export type { NegotiationSpeakerMessage, NegotiationSpeakerParticipants } from "./negotiation.expected-speaker.js";
|
|
28
|
+
export type { NegotiationScopeMetadata } from "./negotiation.scope.js";
|
|
27
29
|
export type { NegotiatorMemoryEntry } from "./negotiation.memory.js";
|
|
28
30
|
export type { NegotiationToolDeps } from "./negotiation.tools.port.js";
|
|
@@ -14,6 +14,7 @@ export { ASK_USER_LOCK_SLACK_MS, allowedActionsFor, askUserAnswerWindowMs, confi
|
|
|
14
14
|
export { HERMES_OWNER_DIRECTIVE, HERMES_SHARED_MESSAGE_TEMPLATES, HermesNegotiationActionSchema, HermesNegotiationResponseSchema, HermesOwnerDirectiveSchema, HermesRoleAlignmentSchema, allowedHermesActionsFor, buildHermesNegotiationTurn, } from "./negotiation.hermes-contract.js";
|
|
15
15
|
export { DEFAULT_NEGOTIATION_MAX_TURNS, isNegotiationTurnCapReached } from "./negotiation.turn-cap.js";
|
|
16
16
|
export { expectedNegotiationSpeaker } from "./negotiation.expected-speaker.js";
|
|
17
|
+
export { negotiationScopeKey, readNegotiationMessages } from "./negotiation.scope.js";
|
|
17
18
|
export { assessConsultationEligibility, consultationPromptFor, negotiationConsultationPolicyMode, } from "./negotiation.consultation-policy.js";
|
|
18
19
|
export { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, NEGOTIATION_QUESTION_GENERIC_UPTAKE_ACTIVITY, isSafeNegotiationQuestionText, negotiationQuestionSettlementId, validateInflightAskUserFields, } from "./negotiation.question-safety.js";
|
|
19
20
|
export { renderNegotiatorChatMemorySection } from "./negotiation.memory.js";
|
|
@@ -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
|
|
@@ -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') {
|
|
@@ -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;
|