@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
|
@@ -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;
|
|
@@ -22,7 +22,7 @@ export { negotiateCandidates } from "./negotiation.candidates.js";
|
|
|
22
22
|
* @remarks Accepts an AgentDispatcher for per-turn agent resolution.
|
|
23
23
|
*/
|
|
24
24
|
export class NegotiationGraphFactory {
|
|
25
|
-
constructor(database, dispatcher, timeoutQueue, questionerEnqueue, reflectEnqueue, memoryRetrieve) {
|
|
25
|
+
constructor(database, dispatcher, timeoutQueue, questionerEnqueue, reflectEnqueue, memoryRetrieve, clientDmRetrieve) {
|
|
26
26
|
this.deps = {
|
|
27
27
|
database,
|
|
28
28
|
dispatcher,
|
|
@@ -30,6 +30,7 @@ export class NegotiationGraphFactory {
|
|
|
30
30
|
questionerEnqueue,
|
|
31
31
|
reflectEnqueue,
|
|
32
32
|
memoryRetrieve,
|
|
33
|
+
clientDmRetrieve,
|
|
33
34
|
systemAgent: new IndexNegotiator(),
|
|
34
35
|
screener: new NegotiationScreener(),
|
|
35
36
|
};
|
|
@@ -15,6 +15,7 @@ import { NegotiationScreener } from "./negotiation.screen.js";
|
|
|
15
15
|
import type { QuestionerEnqueueFn } from "../questions/question.module.js";
|
|
16
16
|
import type { ReflectEnqueueFn } from "./negotiation.reflect.js";
|
|
17
17
|
import type { NegotiatorMemoryEntry, NegotiatorMemoryRetrieveFn, NegotiatorMemoryScope } from "./negotiation.memory.js";
|
|
18
|
+
import type { NegotiatorClientDmMessage, NegotiatorClientDmRetrieveFn } from "./negotiation.client-dm.js";
|
|
18
19
|
import { type AttributedPriorDialogue, type TaskAttribution } from './negotiation.attribution.js';
|
|
19
20
|
/** The graph's channel state, as every node sees it. */
|
|
20
21
|
export type NegotiationState = typeof NegotiationGraphState.State;
|
|
@@ -26,6 +27,12 @@ export interface NegotiationGraphDeps {
|
|
|
26
27
|
questionerEnqueue?: QuestionerEnqueueFn;
|
|
27
28
|
reflectEnqueue?: ReflectEnqueueFn;
|
|
28
29
|
memoryRetrieve?: NegotiatorMemoryRetrieveFn;
|
|
30
|
+
/**
|
|
31
|
+
* A2H read path: the acting user's own negotiator DM for this signal.
|
|
32
|
+
* System-agent grounding only — see `negotiation.client-dm.ts`; this must
|
|
33
|
+
* never be forwarded to an external seat via `NegotiationTurnPayload`.
|
|
34
|
+
*/
|
|
35
|
+
clientDmRetrieve?: NegotiatorClientDmRetrieveFn;
|
|
29
36
|
/** In-process negotiator used when no personal agent answers. */
|
|
30
37
|
systemAgent: IndexNegotiator;
|
|
31
38
|
/** Outreach gate for fresh negotiations. */
|
|
@@ -43,9 +50,11 @@ export declare function turnsFromMessages(messages: Array<{
|
|
|
43
50
|
}>): NegotiationTurn[];
|
|
44
51
|
/**
|
|
45
52
|
* Whether `userId`'s side has already spent its one `ask_user` client
|
|
46
|
-
* consultation in
|
|
47
|
-
*
|
|
48
|
-
*
|
|
53
|
+
* consultation in THIS negotiation (P3.2 rationing: max one per negotiation per
|
|
54
|
+
* side). Callers pass `state.messages`, which carries this negotiation's turns
|
|
55
|
+
* across all of its sessions — so an earlier session of the same negotiation
|
|
56
|
+
* counts, while a consultation spent on a different match with the same
|
|
57
|
+
* counterparty does not.
|
|
49
58
|
*/
|
|
50
59
|
export declare function hasPriorAskUser(messages: Array<{
|
|
51
60
|
senderId: string;
|
|
@@ -57,6 +66,16 @@ export declare function hasPriorAskUser(messages: Array<{
|
|
|
57
66
|
* this wrapper adds the graph-side failure guard.
|
|
58
67
|
*/
|
|
59
68
|
export declare function retrieveMemory(deps: NegotiationGraphDeps, userId: string, counterpartyUserId: string, queryText: string, scope: NegotiatorMemoryScope): Promise<NegotiatorMemoryEntry[]>;
|
|
69
|
+
/**
|
|
70
|
+
* A2H client-DM retrieval — never throws, never blocks a negotiation. The
|
|
71
|
+
* injected fn already resolves [] when the flag is off or the user has no
|
|
72
|
+
* negotiator DM for this signal; this wrapper adds the graph-side failure
|
|
73
|
+
* guard, exactly as `retrieveMemory` does for the memory seam.
|
|
74
|
+
*
|
|
75
|
+
* `userId` is always the ACTING user's — the seam has no counterparty field,
|
|
76
|
+
* so the counterparty's DM cannot be requested from here.
|
|
77
|
+
*/
|
|
78
|
+
export declare function retrieveClientDm(deps: NegotiationGraphDeps, userId: string, intentId: string): Promise<NegotiatorClientDmMessage[]>;
|
|
60
79
|
/** Similarity query text: seed reasoning + counterparty context. */
|
|
61
80
|
export declare function memoryQueryText(state: NegotiationState, counterparty: UserNegotiationContext): string;
|
|
62
81
|
/**
|
|
@@ -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}`;
|
|
@@ -58,6 +60,29 @@ export async function retrieveMemory(deps, userId, counterpartyUserId, queryText
|
|
|
58
60
|
return [];
|
|
59
61
|
}
|
|
60
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* A2H client-DM retrieval — never throws, never blocks a negotiation. The
|
|
65
|
+
* injected fn already resolves [] when the flag is off or the user has no
|
|
66
|
+
* negotiator DM for this signal; this wrapper adds the graph-side failure
|
|
67
|
+
* guard, exactly as `retrieveMemory` does for the memory seam.
|
|
68
|
+
*
|
|
69
|
+
* `userId` is always the ACTING user's — the seam has no counterparty field,
|
|
70
|
+
* so the counterparty's DM cannot be requested from here.
|
|
71
|
+
*/
|
|
72
|
+
export async function retrieveClientDm(deps, userId, intentId) {
|
|
73
|
+
if (!deps.clientDmRetrieve)
|
|
74
|
+
return [];
|
|
75
|
+
try {
|
|
76
|
+
return await deps.clientDmRetrieve({ userId, intentId });
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
logger.warn("Negotiator client DM retrieval failed; proceeding without it", {
|
|
80
|
+
userId,
|
|
81
|
+
error: err instanceof Error ? err.message : String(err),
|
|
82
|
+
});
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
61
86
|
/** Similarity query text: seed reasoning + counterparty context. */
|
|
62
87
|
export function memoryQueryText(state, counterparty) {
|
|
63
88
|
return [
|
|
@@ -117,7 +142,9 @@ export async function resolveTaskAttribution(deps, taskId) {
|
|
|
117
142
|
* (task-id-matched). Null when there is no seeded attribution.
|
|
118
143
|
*/
|
|
119
144
|
export function buildAttributedDialogue(state) {
|
|
120
|
-
|
|
145
|
+
// Not gated on `isContinuation`: that now means "this negotiation has spoken",
|
|
146
|
+
// and the pair's earlier matches are context worth carrying into a fresh one.
|
|
147
|
+
if (!state.priorAttribution)
|
|
121
148
|
return null;
|
|
122
149
|
const currentSessionTurns = turnsFromMessages(state.messages.filter((m) => m.taskId === state.taskId));
|
|
123
150
|
const dialogue = combineAttributedDialogue(state.priorAttribution, currentSessionTurns);
|
|
@@ -22,6 +22,15 @@ export declare function turnNode(state: NegotiationState, deps: NegotiationGraph
|
|
|
22
22
|
message?: string | null | undefined;
|
|
23
23
|
askUser?: {
|
|
24
24
|
reason: "unresolved_owner_constraint" | "consequential_disclosure_permission" | "repeated_non_convergence" | "insufficient_commitment_authority";
|
|
25
|
+
question?: {
|
|
26
|
+
prompt: string;
|
|
27
|
+
options: {
|
|
28
|
+
label: string;
|
|
29
|
+
description: string;
|
|
30
|
+
}[];
|
|
31
|
+
title: string;
|
|
32
|
+
multiSelect: boolean;
|
|
33
|
+
} | undefined;
|
|
25
34
|
} | null | undefined;
|
|
26
35
|
};
|
|
27
36
|
firstTurnScreenedOut: boolean;
|
|
@@ -50,6 +59,15 @@ export declare function turnNode(state: NegotiationState, deps: NegotiationGraph
|
|
|
50
59
|
message?: string | null | undefined;
|
|
51
60
|
askUser?: {
|
|
52
61
|
reason: "unresolved_owner_constraint" | "consequential_disclosure_permission" | "repeated_non_convergence" | "insufficient_commitment_authority";
|
|
62
|
+
question?: {
|
|
63
|
+
prompt: string;
|
|
64
|
+
options: {
|
|
65
|
+
label: string;
|
|
66
|
+
description: string;
|
|
67
|
+
}[];
|
|
68
|
+
title: string;
|
|
69
|
+
multiSelect: boolean;
|
|
70
|
+
} | undefined;
|
|
53
71
|
} | null | undefined;
|
|
54
72
|
};
|
|
55
73
|
status: "input_required";
|
|
@@ -79,6 +97,15 @@ export declare function turnNode(state: NegotiationState, deps: NegotiationGraph
|
|
|
79
97
|
message?: string | null | undefined;
|
|
80
98
|
askUser?: {
|
|
81
99
|
reason: "unresolved_owner_constraint" | "consequential_disclosure_permission" | "repeated_non_convergence" | "insufficient_commitment_authority";
|
|
100
|
+
question?: {
|
|
101
|
+
prompt: string;
|
|
102
|
+
options: {
|
|
103
|
+
label: string;
|
|
104
|
+
description: string;
|
|
105
|
+
}[];
|
|
106
|
+
title: string;
|
|
107
|
+
multiSelect: boolean;
|
|
108
|
+
} | undefined;
|
|
82
109
|
} | null | undefined;
|
|
83
110
|
};
|
|
84
111
|
memoryBySide: {
|
|
@@ -6,9 +6,9 @@ import { allowedActionsFor, askUserAnswerWindowMs, configuredAskUserEnabled, fal
|
|
|
6
6
|
import { assessConsultationEligibility, consultationPromptFor, negotiationConsultationPolicyMode } from "./negotiation.consultation-policy.js";
|
|
7
7
|
import { blocksNegotiationBeforeFirstTurn } from "./negotiation.screen.js";
|
|
8
8
|
import { assessDeadlock, configuredDeadlockShiftEnabled, configuredDeadlockThreshold } from "./negotiation.deadlock.js";
|
|
9
|
-
import { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, negotiationQuestionSettlementId } from './negotiation.question-safety.js';
|
|
9
|
+
import { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, isSafeAuthoredNegotiationQuestion, negotiationQuestionSettlementId } from './negotiation.question-safety.js';
|
|
10
10
|
import { isNegotiationTurnCapReached } from "./negotiation.turn-cap.js";
|
|
11
|
-
import { buildAttributedDialogue, hasPriorAskUser, memoryQueryText, retrieveMemory, turnLog, turnsFromMessages } from "./negotiation.graph.shared.js";
|
|
11
|
+
import { buildAttributedDialogue, hasPriorAskUser, memoryQueryText, retrieveClientDm, retrieveMemory, turnLog, turnsFromMessages } from "./negotiation.graph.shared.js";
|
|
12
12
|
export async function turnNode(state, deps) {
|
|
13
13
|
const traceEmitter = requestContext.getStore()?.traceEmitter;
|
|
14
14
|
// Local helper to emit events whose shape is wider than the declared
|
|
@@ -150,6 +150,27 @@ export async function turnNode(state, deps) {
|
|
|
150
150
|
else {
|
|
151
151
|
// No personal agent or timeout — run system agent
|
|
152
152
|
const agentPriorDialogue = buildAttributedDialogue(state);
|
|
153
|
+
// ─── A2H: the acting user's own negotiator DM for this signal ──────
|
|
154
|
+
// Retrieved HERE, inside the system-agent branch, rather than beside
|
|
155
|
+
// `ownMemory` above. Two reasons, and the first is the constraint:
|
|
156
|
+
//
|
|
157
|
+
// 1. `payload` is built and dispatched before this point, so the
|
|
158
|
+
// excerpt cannot reach an external agent by a later edit — the
|
|
159
|
+
// value does not exist in that scope. Memory is safe to forward
|
|
160
|
+
// (distilled standing rules); a verbatim excerpt of the client's
|
|
161
|
+
// private thread with their own negotiator is not, and an external
|
|
162
|
+
// registered agent can hold the personal-agent seat.
|
|
163
|
+
// 2. A dispatched turn never reads it, so it never pays for the query.
|
|
164
|
+
//
|
|
165
|
+
// Gated on `askUserAvailable`: the grant is settled before the model
|
|
166
|
+
// runs, so the DM is present on exactly the turns where the agent may
|
|
167
|
+
// consult its client — the turns where knowing what they already said
|
|
168
|
+
// changes what it asks. Fetching it on every turn would move the
|
|
169
|
+
// prompt for every negotiation, not just the consulting ones.
|
|
170
|
+
// `askUserAvailable` already requires a non-empty `ownIntentId`.
|
|
171
|
+
const clientDm = askUserAvailable
|
|
172
|
+
? await retrieveClientDm(deps, ownUser.id, ownIntentId)
|
|
173
|
+
: [];
|
|
153
174
|
turn = await deps.systemAgent.invoke({
|
|
154
175
|
ownUser,
|
|
155
176
|
otherUser,
|
|
@@ -167,6 +188,7 @@ export async function turnNode(state, deps) {
|
|
|
167
188
|
...(askUserAvailable && { canAskUser: true }),
|
|
168
189
|
...(bargainingMode && { bargaining: { consecutiveNonConvergent: deadlock.consecutiveNonConvergent } }),
|
|
169
190
|
...(ownMemory.length > 0 && { memory: ownMemory }),
|
|
191
|
+
...(clientDm.length > 0 && { clientDm }),
|
|
170
192
|
...(state.privateConsultation?.recipientUserId === ownUser.id
|
|
171
193
|
? { privateConsultation: state.privateConsultation }
|
|
172
194
|
: {}),
|
|
@@ -307,6 +329,46 @@ export async function turnNode(state, deps) {
|
|
|
307
329
|
});
|
|
308
330
|
}
|
|
309
331
|
}
|
|
332
|
+
// ─── Authored ask_user question: identifier-aware safety gate ─────────
|
|
333
|
+
// The agent now writes the question its client reads verbatim (the
|
|
334
|
+
// pre-A2H `disclosureSubject` was only an input to server-templated copy),
|
|
335
|
+
// and an external registered agent can hold the personal-agent seat — so
|
|
336
|
+
// this runs on every turn, dispatched or system, not just our own.
|
|
337
|
+
//
|
|
338
|
+
// Placed BEFORE persistence deliberately. The turn is about to become a
|
|
339
|
+
// message in the shared conversation, so a rejected question must not
|
|
340
|
+
// survive there either; and issue 6 reads the field back off the persisted
|
|
341
|
+
// turn, which means dropping it here is what makes that read safe by
|
|
342
|
+
// construction rather than by remembering to re-check.
|
|
343
|
+
//
|
|
344
|
+
// Rejection is a DOWNGRADE, never a failure: the turn, its action, and
|
|
345
|
+
// `askUser.reason` all stand, so the consultation proceeds on today's
|
|
346
|
+
// enum-only path — the same shape a v1 turn or an older agent produces.
|
|
347
|
+
// A guard that could fail a turn would let malformed model output stall a
|
|
348
|
+
// negotiation, which is strictly worse than asking a generic question.
|
|
349
|
+
//
|
|
350
|
+
// The two inputs are exactly what the api-side payload guard can never
|
|
351
|
+
// have: it sees the question at the DB boundary with no idea who the
|
|
352
|
+
// counterparty is or what the evaluator wrote, so it cannot tell that a
|
|
353
|
+
// well-formed question is naming them or paraphrasing it.
|
|
354
|
+
if (turn.askUser?.question) {
|
|
355
|
+
const counterpartyName = otherUser.profile?.name?.trim();
|
|
356
|
+
const seedReasoning = state.seedAssessment?.reasoning?.trim();
|
|
357
|
+
const safeAuthoredQuestion = isSafeAuthoredNegotiationQuestion(turn.askUser.question, {
|
|
358
|
+
...(counterpartyName ? { forbiddenIdentifiers: [counterpartyName] } : {}),
|
|
359
|
+
...(seedReasoning ? { forbiddenSourceText: [seedReasoning] } : {}),
|
|
360
|
+
});
|
|
361
|
+
if (!safeAuthoredQuestion) {
|
|
362
|
+
turnLog.warn('Dropping unsafe authored ask_user question; consultation continues without it', {
|
|
363
|
+
taskId: state.taskId,
|
|
364
|
+
opportunityId: state.opportunityId || undefined,
|
|
365
|
+
seat,
|
|
366
|
+
handledExternally: dispatchResult.handled,
|
|
367
|
+
});
|
|
368
|
+
const { question: _rejected, ...askUserWithoutQuestion } = turn.askUser;
|
|
369
|
+
turn = { ...turn, askUser: askUserWithoutQuestion };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
310
372
|
const parts = [{ kind: "data", data: turn }];
|
|
311
373
|
const message = await deps.database.createMessage({
|
|
312
374
|
conversationId: state.conversationId,
|
|
@@ -17,12 +17,15 @@ 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
|
-
export { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, NEGOTIATION_QUESTION_GENERIC_UPTAKE_ACTIVITY, isSafeNegotiationQuestionText, negotiationQuestionSettlementId, validateInflightAskUserFields, } from "./negotiation.question-safety.js";
|
|
22
|
+
export { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, NEGOTIATION_QUESTION_GENERIC_UPTAKE_ACTIVITY, isSafeAuthoredNegotiationQuestion, isSafeNegotiationQuestionText, negotiationQuestionSettlementId, validateInflightAskUserFields, } from "./negotiation.question-safety.js";
|
|
22
23
|
export { renderNegotiatorChatMemorySection } from "./negotiation.memory.js";
|
|
23
24
|
export type { ConsultationEligibility, ConsultationEligibilityInput, NegotiationConsultationPolicyMode, NegotiationConsultationReason, } from "./negotiation.consultation-policy.js";
|
|
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";
|
|
30
|
+
export type { NegotiatorClientDmMessage, NegotiatorClientDmQuery, NegotiatorClientDmRetrieveFn, } from "./negotiation.client-dm.js";
|
|
28
31
|
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
|
-
export { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, NEGOTIATION_QUESTION_GENERIC_UPTAKE_ACTIVITY, isSafeNegotiationQuestionText, negotiationQuestionSettlementId, validateInflightAskUserFields, } from "./negotiation.question-safety.js";
|
|
19
|
+
export { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, NEGOTIATION_QUESTION_GENERIC_UPTAKE_ACTIVITY, isSafeAuthoredNegotiationQuestion, isSafeNegotiationQuestionText, negotiationQuestionSettlementId, validateInflightAskUserFields, } from "./negotiation.question-safety.js";
|
|
19
20
|
export { renderNegotiatorChatMemorySection } from "./negotiation.memory.js";
|