@indexnetwork/protocol 11.0.2-rc.456.1 → 11.2.0-rc.458.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/CHANGELOG.md +10 -0
- package/dist/capabilities/negotiation.facade.d.ts +4 -1
- package/dist/capabilities/negotiation.facade.js +2 -1
- package/dist/capabilities/negotiation.questions.facade.d.ts +1 -1
- package/dist/capabilities/negotiation.questions.facade.js +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +5 -1
- package/dist/mcp/mcp.authorization-policy.d.ts +10 -4
- package/dist/mcp/mcp.authorization-policy.js +64 -5
- package/dist/negotiation/application/negotiation.agent.d.ts +7 -1
- package/dist/negotiation/application/negotiation.agent.js +6 -4
- package/dist/negotiation/application/negotiation.detail-reader.js +3 -6
- package/dist/negotiation/application/negotiation.graph.d.ts +17 -29
- package/dist/negotiation/application/negotiation.graph.js +35 -49
- package/dist/negotiation/application/negotiation.tools.js +40 -43
- package/dist/negotiation/domain/index.d.ts +5 -0
- package/dist/negotiation/domain/index.js +4 -0
- package/dist/negotiation/domain/negotiation.consultation-policy.d.ts +2 -3
- package/dist/negotiation/domain/negotiation.expected-speaker.d.ts +19 -0
- package/dist/negotiation/domain/negotiation.expected-speaker.js +47 -0
- package/dist/negotiation/domain/negotiation.hermes-contract.d.ts +31 -0
- package/dist/negotiation/domain/negotiation.hermes-contract.js +72 -0
- package/dist/negotiation/domain/negotiation.protocol.d.ts +36 -66
- package/dist/negotiation/domain/negotiation.state.d.ts +10 -19
- package/dist/negotiation/domain/negotiation.turn-cap.d.ts +9 -0
- package/dist/negotiation/domain/negotiation.turn-cap.js +12 -0
- package/dist/negotiation/public/index.d.ts +4 -1
- package/dist/negotiation/public/index.js +3 -1
- package/dist/questions/application/question.input.d.ts +4 -8
- package/dist/questions/application/question.input.js +3 -8
- package/dist/questions/application/question.presets.js +10 -11
- package/dist/shared/agent/model-signal.d.ts +6 -1
- package/dist/shared/agent/model-signal.js +5 -2
- package/dist/shared/interfaces/agent-dispatcher.interface.d.ts +3 -0
- package/dist/shared/interfaces/database.interface.d.ts +1 -1
- package/dist/shared/interfaces/negotiation-events.interface.d.ts +12 -2
- package/dist/shared/schemas/mcp-auth.schema.d.ts +6 -0
- package/dist/shared/schemas/mcp-auth.schema.js +9 -0
- package/dist/shared/schemas/negotiation-state.schema.d.ts +17 -23
- package/dist/shared/schemas/negotiation-state.schema.js +13 -8
- package/package.json +1 -1
|
@@ -9,9 +9,11 @@ import { blocksNegotiationBeforeFirstTurn, NegotiationScreener } from "./negotia
|
|
|
9
9
|
import { configuredScreenMode } from "../domain/negotiation.screen.contracts.js";
|
|
10
10
|
import { assessDeadlock, configuredDeadlockShiftEnabled, configuredDeadlockThreshold } from "../domain/negotiation.deadlock.js";
|
|
11
11
|
import { protocolLogger } from "../../shared/observability/protocol.logger.js";
|
|
12
|
-
import { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, negotiationQuestionSettlementId
|
|
12
|
+
import { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, negotiationQuestionSettlementId } from '../domain/negotiation.question-safety.js';
|
|
13
13
|
import { buildIntentSnapshots } from "../domain/negotiation.intent-snapshot-provenance.js";
|
|
14
14
|
import { holdsNegotiationConversationLock } from "../domain/negotiation.task-lock-policy.js";
|
|
15
|
+
import { isNegotiationTurnCapReached } from "../domain/negotiation.turn-cap.js";
|
|
16
|
+
import { expectedNegotiationSpeaker } from "../domain/negotiation.expected-speaker.js";
|
|
15
17
|
import { attributedDialogueIsEmpty, buildSeededAttribution, combineAttributedDialogue } from '../negotiation.attribution.js';
|
|
16
18
|
const logger = protocolLogger("NegotiationGraph");
|
|
17
19
|
const initLog = protocolLogger("NegotiationGraph:Init");
|
|
@@ -196,22 +198,15 @@ export class NegotiationGraphFactory {
|
|
|
196
198
|
// --- Load prior messages and determine continuation ---
|
|
197
199
|
const priorTurns = turnsFromMessages(priorMessages);
|
|
198
200
|
const isContinuation = priorTurns.length > 0;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (lastAction === 'ask_user') {
|
|
209
|
-
currentSpeaker = lastMessage.senderId === agentIdA ? 'source' : 'candidate';
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
currentSpeaker = lastMessage.senderId === agentIdA ? 'candidate' : 'source';
|
|
213
|
-
}
|
|
214
|
-
}
|
|
201
|
+
const expectedSpeaker = expectedNegotiationSpeaker({
|
|
202
|
+
sourceUserId: state.sourceUser.id,
|
|
203
|
+
candidateUserId: state.candidateUser.id,
|
|
204
|
+
}, priorMessages);
|
|
205
|
+
if (!expectedSpeaker)
|
|
206
|
+
return { error: 'invalid negotiation participants' };
|
|
207
|
+
const currentSpeaker = expectedSpeaker === state.sourceUser.id
|
|
208
|
+
? 'source'
|
|
209
|
+
: 'candidate';
|
|
215
210
|
// Determine scenario-based maxTurns
|
|
216
211
|
const scope = { action: 'manage:negotiations', scopeType: 'network', scopeId: state.indexContext.networkId };
|
|
217
212
|
const [sourceHasAgent, candidateHasAgent] = await Promise.all([
|
|
@@ -485,9 +480,8 @@ export class NegotiationGraphFactory {
|
|
|
485
480
|
const ownUser = isSource ? state.sourceUser : state.candidateUser;
|
|
486
481
|
const otherUser = isSource ? state.candidateUser : state.sourceUser;
|
|
487
482
|
const ownIntentId = isSource ? state.sourceIntentId : state.candidateIntentId;
|
|
488
|
-
// Determine if this is the system agent's final allowed turn
|
|
489
|
-
const
|
|
490
|
-
const isFinalTurn = maxTurns > 0 && (state.turnCount + 1) >= maxTurns;
|
|
483
|
+
// Determine if this is the system agent's final allowed turn.
|
|
484
|
+
const isFinalTurn = isNegotiationTurnCapReached(state.turnCount + 1, state.maxTurns);
|
|
491
485
|
// Seat attribution keys on initiatorUserId (rigid v2 stamp), never on
|
|
492
486
|
// parity or source/candidate position — under the conversation-scoped
|
|
493
487
|
// tie-break this run's source may hold the counterparty seat.
|
|
@@ -558,6 +552,17 @@ export class NegotiationGraphFactory {
|
|
|
558
552
|
...(state.privateConsultation?.recipientUserId === ownUser.id
|
|
559
553
|
? { privateConsultation: state.privateConsultation }
|
|
560
554
|
: {}),
|
|
555
|
+
...(state.continuationExecution
|
|
556
|
+
? {
|
|
557
|
+
timeoutContinuation: {
|
|
558
|
+
priorTaskId: state.continuationExecution.taskId,
|
|
559
|
+
settlementId: state.continuationExecution.settlementId,
|
|
560
|
+
successorTaskId: state.continuationExecution.successorTaskId,
|
|
561
|
+
token: state.continuationExecution.token,
|
|
562
|
+
fence: state.continuationExecution.fence,
|
|
563
|
+
},
|
|
564
|
+
}
|
|
565
|
+
: {}),
|
|
561
566
|
};
|
|
562
567
|
const scope = { action: 'manage:negotiations', scopeType: 'network', scopeId: state.indexContext.networkId };
|
|
563
568
|
const dispatchResult = await dispatcher.dispatch(ownUser.id, scope, payload, { timeoutMs: state.timeoutMs });
|
|
@@ -595,7 +600,7 @@ export class NegotiationGraphFactory {
|
|
|
595
600
|
? { privateConsultation: state.privateConsultation }
|
|
596
601
|
: {}),
|
|
597
602
|
}, state.continuationExecution);
|
|
598
|
-
await database.updateTaskState(state.taskId, "waiting_for_agent", undefined, state.continuationExecution);
|
|
603
|
+
await database.updateTaskState(state.taskId, "waiting_for_agent", undefined, state.continuationExecution, dispatchResult.resumeToken);
|
|
599
604
|
return { status: 'waiting_for_agent' };
|
|
600
605
|
}
|
|
601
606
|
else {
|
|
@@ -703,7 +708,7 @@ export class NegotiationGraphFactory {
|
|
|
703
708
|
reasoning: 'Client consultation required.',
|
|
704
709
|
suggestedRoles: turn.assessment.suggestedRoles,
|
|
705
710
|
},
|
|
706
|
-
askUser:
|
|
711
|
+
askUser: { reason: consultationPolicyReason },
|
|
707
712
|
};
|
|
708
713
|
emitConsultationTelemetry('asked', consultationPolicyReason);
|
|
709
714
|
}
|
|
@@ -775,27 +780,10 @@ export class NegotiationGraphFactory {
|
|
|
775
780
|
// like the waiting_for_agent suspend; the answer (or window expiry)
|
|
776
781
|
// resumes via the run-existing continuation path.
|
|
777
782
|
if (turn.action === 'ask_user') {
|
|
778
|
-
const
|
|
779
|
-
const safeAskUser =
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
forbiddenIdentifiers: [
|
|
783
|
-
counterparty.id,
|
|
784
|
-
counterparty.profile.name ?? '',
|
|
785
|
-
state.opportunityId,
|
|
786
|
-
state.taskId,
|
|
787
|
-
state.indexContext.networkId,
|
|
788
|
-
isSource ? state.candidateIntentId ?? '' : state.sourceIntentId ?? '',
|
|
789
|
-
],
|
|
790
|
-
forbiddenSourceText: [
|
|
791
|
-
counterparty.profile.bio ?? '',
|
|
792
|
-
counterparty.profile.location ?? '',
|
|
793
|
-
...counterparty.intents.flatMap((intent) => [intent.title, intent.description]),
|
|
794
|
-
state.seedAssessment.reasoning,
|
|
795
|
-
state.indexContext.prompt,
|
|
796
|
-
state.discoveryQuery ?? '',
|
|
797
|
-
],
|
|
798
|
-
});
|
|
783
|
+
const consultationReason = turn.askUser?.reason;
|
|
784
|
+
const safeAskUser = consultationReason
|
|
785
|
+
? consultationPromptFor(consultationReason)
|
|
786
|
+
: null;
|
|
799
787
|
const settlementId = negotiationQuestionSettlementId(state.taskId);
|
|
800
788
|
const askUserBinding = await database.captureNegotiationAskUserBinding({
|
|
801
789
|
taskId: state.taskId,
|
|
@@ -810,7 +798,7 @@ export class NegotiationGraphFactory {
|
|
|
810
798
|
indexContext: state.indexContext,
|
|
811
799
|
seedAssessment: state.seedAssessment,
|
|
812
800
|
...(isSource && state.discoveryQuery && { discoveryQuery: state.discoveryQuery }),
|
|
813
|
-
...(
|
|
801
|
+
...(consultationReason && { consultationPolicyReason: consultationReason }),
|
|
814
802
|
},
|
|
815
803
|
...(state.continuationExecution ? { continuationExecution: state.continuationExecution } : {}),
|
|
816
804
|
});
|
|
@@ -852,10 +840,8 @@ export class NegotiationGraphFactory {
|
|
|
852
840
|
context: {
|
|
853
841
|
negotiationId: state.taskId,
|
|
854
842
|
counterpartyHint: NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY,
|
|
855
|
-
disclosureSubject: safeAskUser.disclosureSubject,
|
|
856
|
-
...(safeAskUser.draftQuestion && { draftQuestion: safeAskUser.draftQuestion }),
|
|
857
843
|
indexContext: NEGOTIATION_QUESTION_GENERIC_NETWORK,
|
|
858
|
-
|
|
844
|
+
consultationPolicyReason: consultationReason,
|
|
859
845
|
...(userContext && { userContext }),
|
|
860
846
|
},
|
|
861
847
|
}).catch((error) => {
|
|
@@ -965,7 +951,7 @@ export class NegotiationGraphFactory {
|
|
|
965
951
|
if (isTerminalAction(state.lastTurn.action))
|
|
966
952
|
return "finalize";
|
|
967
953
|
// question routes same as counter — next turn
|
|
968
|
-
if ((state.
|
|
954
|
+
if (isNegotiationTurnCapReached(state.turnCount, state.maxTurns))
|
|
969
955
|
return "finalize";
|
|
970
956
|
return "turn";
|
|
971
957
|
};
|
|
@@ -1038,7 +1024,7 @@ export class NegotiationGraphFactory {
|
|
|
1038
1024
|
const blockedByScreenNode = blocksNegotiationBeforeFirstTurn(state.screenDecision, state.turnCount);
|
|
1039
1025
|
const refusedAtOpeningTurn = state.firstTurnScreenedOut === true;
|
|
1040
1026
|
const screenedOut = blockedByScreenNode || refusedAtOpeningTurn;
|
|
1041
|
-
const atCap = !screenedOut && (state.
|
|
1027
|
+
const atCap = !screenedOut && isNegotiationTurnCapReached(state.turnCount, state.maxTurns) && !isTerminalAction(lastTurn?.action);
|
|
1042
1028
|
let agreedRoles = [];
|
|
1043
1029
|
if (hasOpportunity && history.length >= 2) {
|
|
1044
1030
|
const acceptTurn = history[history.length - 1];
|
|
@@ -7,6 +7,8 @@ import { protocolLogger } from '../../shared/observability/protocol.logger.js';
|
|
|
7
7
|
import { focusedIntentId, focusedNetworkId } from '../../shared/agent/tool.scope.js';
|
|
8
8
|
import { readAuthorizedNegotiationDetail } from './negotiation.detail-reader.js';
|
|
9
9
|
import { buildLifecycleNarration } from '../domain/negotiation.lifecycle-narration.js';
|
|
10
|
+
import { isNegotiationTurnCapReached } from '../domain/negotiation.turn-cap.js';
|
|
11
|
+
import { expectedNegotiationSpeaker } from '../domain/negotiation.expected-speaker.js';
|
|
10
12
|
export { buildLifecycleNarration } from '../domain/negotiation.lifecycle-narration.js';
|
|
11
13
|
const logger = protocolLogger('ChatTools:Negotiation');
|
|
12
14
|
/**
|
|
@@ -154,21 +156,14 @@ export function createNegotiationTools(defineTool, deps) {
|
|
|
154
156
|
const lastTurnData = lastMessage
|
|
155
157
|
? lastMessage.parts?.find(p => p.kind === 'data')?.data
|
|
156
158
|
: undefined;
|
|
157
|
-
// Determine whose turn it is from the last message's sender — not
|
|
158
|
-
// parity, which misattributes across continuation sessions. Rows
|
|
159
|
-
// without senderId (legacy) fall back to parity.
|
|
160
159
|
const turnCount = messages.length;
|
|
161
|
-
const
|
|
162
|
-
const currentSpeaker = lastSenderId
|
|
163
|
-
? (lastSenderId === `agent:${meta.sourceUserId}` ? 'candidate' : 'source')
|
|
164
|
-
: (turnCount % 2 === 0 ? 'source' : 'candidate');
|
|
160
|
+
const expectedSpeaker = expectedNegotiationSpeaker(meta, messages);
|
|
165
161
|
// Map task state to tool status
|
|
166
162
|
const status = task.state === 'working' ? 'active'
|
|
167
163
|
: task.state === 'waiting_for_agent' ? 'waiting_for_agent'
|
|
168
164
|
: task.state === 'completed' ? 'completed'
|
|
169
165
|
: task.state;
|
|
170
|
-
const isUsersTurn = status !== 'completed' &&
|
|
171
|
-
((isSource && currentSpeaker === 'source') || (!isSource && currentSpeaker === 'candidate'));
|
|
166
|
+
const isUsersTurn = status !== 'completed' && expectedSpeaker === context.userId;
|
|
172
167
|
const base = {
|
|
173
168
|
id: task.id,
|
|
174
169
|
counterpartyId: counterpartyId ?? 'unknown',
|
|
@@ -355,6 +350,15 @@ export function createNegotiationTools(defineTool, deps) {
|
|
|
355
350
|
if (meta?.type !== 'negotiation') {
|
|
356
351
|
return error('Negotiation not found.');
|
|
357
352
|
}
|
|
353
|
+
const timeoutContinuation = meta.continuationExecution
|
|
354
|
+
? {
|
|
355
|
+
priorTaskId: meta.continuationExecution.priorTaskId,
|
|
356
|
+
settlementId: meta.continuationExecution.settlementId,
|
|
357
|
+
successorTaskId: meta.continuationExecution.successorTaskId,
|
|
358
|
+
token: meta.continuationExecution.token,
|
|
359
|
+
fence: meta.continuationExecution.fence,
|
|
360
|
+
}
|
|
361
|
+
: undefined;
|
|
358
362
|
// Network-scope check (mirrors get_negotiation): a network-bound agent
|
|
359
363
|
// must not act on negotiations outside its bound network.
|
|
360
364
|
const scopedNetworkId = focusedNetworkId(context);
|
|
@@ -382,17 +386,10 @@ export function createNegotiationTools(defineTool, deps) {
|
|
|
382
386
|
if (!allowedActionsFor(protocolVersion, seat).includes(query.action)) {
|
|
383
387
|
return error(seatViolationMessage(query.action, seat, protocolVersion));
|
|
384
388
|
}
|
|
385
|
-
// Determine whose turn it is from the last message's sender — not
|
|
386
|
-
// parity, which misattributes across continuation sessions. Rows
|
|
387
|
-
// without senderId (legacy) fall back to the parity heuristic.
|
|
388
389
|
const messages = await negotiationDatabase.getMessagesForConversation(task.conversationId);
|
|
389
390
|
const turnCount = messages.length;
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
const isUsersTurn = lastSenderId
|
|
393
|
-
? lastSenderId !== `agent:${context.userId}`
|
|
394
|
-
: ((isSource && paritySpeaker === 'source') || (!isSource && paritySpeaker === 'candidate'));
|
|
395
|
-
if (!isUsersTurn) {
|
|
391
|
+
const expectedSpeaker = expectedNegotiationSpeaker(meta, messages);
|
|
392
|
+
if (expectedSpeaker !== context.userId) {
|
|
396
393
|
return error('It is not your turn to respond in this negotiation.');
|
|
397
394
|
}
|
|
398
395
|
// The caller is the current speaker (verified above).
|
|
@@ -402,8 +399,8 @@ export function createNegotiationTools(defineTool, deps) {
|
|
|
402
399
|
return error(`A message is required when using "${query.action}". Explain what you want to change or clarify.`);
|
|
403
400
|
}
|
|
404
401
|
// ── Cancel pending timeout ──
|
|
405
|
-
if (deps.negotiationTimeoutQueue) {
|
|
406
|
-
await deps.negotiationTimeoutQueue.cancelTimeout(task.id);
|
|
402
|
+
if (deps.negotiationTimeoutQueue && meta.negotiationParkGeneration) {
|
|
403
|
+
await deps.negotiationTimeoutQueue.cancelTimeout(task.id, meta.negotiationParkGeneration);
|
|
407
404
|
}
|
|
408
405
|
// ── Build and persist the external agent's turn ──
|
|
409
406
|
const turnData = {
|
|
@@ -451,8 +448,7 @@ export function createNegotiationTools(defineTool, deps) {
|
|
|
451
448
|
});
|
|
452
449
|
}
|
|
453
450
|
// ── Handle counter/question: check if under max turns ──
|
|
454
|
-
|
|
455
|
-
if (newTurnCount >= maxTurns) {
|
|
451
|
+
if (isNegotiationTurnCapReached(newTurnCount, meta.maxTurns)) {
|
|
456
452
|
// Max turns reached — finalize with turn_cap
|
|
457
453
|
const allMessages = [...messages, { id: turnMessage.id, senderId: turnMessage.senderId, role: turnMessage.role, parts: turnMessage.parts, createdAt: turnMessage.createdAt }];
|
|
458
454
|
const history = turnsFromMessages(allMessages);
|
|
@@ -480,7 +476,7 @@ export function createNegotiationTools(defineTool, deps) {
|
|
|
480
476
|
// Build the current turn history for dispatcher payload
|
|
481
477
|
const allMessagesWithTurn = [...messages, { id: turnMessage.id, senderId: turnMessage.senderId, role: turnMessage.role, parts: turnMessage.parts, createdAt: turnMessage.createdAt }];
|
|
482
478
|
const historyForDispatch = turnsFromMessages(allMessagesWithTurn);
|
|
483
|
-
const isFinalTurn = newTurnCount + 1
|
|
479
|
+
const isFinalTurn = isNegotiationTurnCapReached(newTurnCount + 1, meta.maxTurns);
|
|
484
480
|
const ownUserCtx = { id: counterpartyUserId, intents: [], profile: {} };
|
|
485
481
|
const otherUserCtx = { id: context.userId, intents: [], profile: {} };
|
|
486
482
|
const seedAssessment = { reasoning: 'Continued negotiation', valencyRole: 'peer' };
|
|
@@ -496,16 +492,15 @@ export function createNegotiationTools(defineTool, deps) {
|
|
|
496
492
|
seat: counterpartySeat,
|
|
497
493
|
protocolVersion,
|
|
498
494
|
allowedActions: [...allowedActionsFor(protocolVersion, counterpartySeat, isFinalTurn)],
|
|
495
|
+
...(timeoutContinuation ? { timeoutContinuation } : {}),
|
|
499
496
|
};
|
|
500
497
|
const scope = { action: 'negotiation.respond', scopeType: 'negotiation', scopeId: task.id };
|
|
501
498
|
const timeoutMs = AMBIENT_PARK_WINDOW_MS;
|
|
502
499
|
const dispatchResult = await deps.agentDispatcher?.dispatch(counterpartyUserId, scope, dispatchPayload, { timeoutMs });
|
|
503
500
|
if (dispatchResult?.handled === false && dispatchResult.reason === 'waiting') {
|
|
504
|
-
//
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
await deps.negotiationTimeoutQueue.enqueueTimeout(task.id, newTurnCount, timeoutMs);
|
|
508
|
-
}
|
|
501
|
+
// The dispatcher armed this exact generation before acknowledging;
|
|
502
|
+
// persist that same token with the waiting state.
|
|
503
|
+
await negotiationDatabase.updateTaskState(task.id, 'waiting_for_agent', undefined, undefined, dispatchResult.resumeToken);
|
|
509
504
|
return success({
|
|
510
505
|
message: `${query.action === 'question' ? 'Question' : query.action === 'propose' ? 'Proposal' : query.action === 'outreach' ? 'Outreach' : 'Counter-proposal'} submitted. Waiting for counterparty response.`,
|
|
511
506
|
negotiationId: task.id,
|
|
@@ -597,7 +592,7 @@ export function createNegotiationTools(defineTool, deps) {
|
|
|
597
592
|
});
|
|
598
593
|
}
|
|
599
594
|
// Counterparty countered/questioned — check if max turns reached
|
|
600
|
-
if (finalTurnCount
|
|
595
|
+
if (isNegotiationTurnCapReached(finalTurnCount, meta.maxTurns)) {
|
|
601
596
|
const fullHistory = [...historyForDispatch, aiTurn];
|
|
602
597
|
const outcome = buildNegotiationOutcome(fullHistory, finalTurnCount, 'counter', meta.sourceUserId, meta.candidateUserId, counterpartySpeaker === 'source' ? 'candidate' : 'source');
|
|
603
598
|
await negotiationDatabase.updateTaskState(task.id, 'completed');
|
|
@@ -624,19 +619,22 @@ export function createNegotiationTools(defineTool, deps) {
|
|
|
624
619
|
indexContext: { networkId: '' },
|
|
625
620
|
seedAssessment,
|
|
626
621
|
history: [...historyForDispatch, aiTurn],
|
|
627
|
-
isFinalTurn: finalTurnCount + 1
|
|
622
|
+
isFinalTurn: isNegotiationTurnCapReached(finalTurnCount + 1, meta.maxTurns),
|
|
628
623
|
isDiscoverer: true,
|
|
629
624
|
seat,
|
|
630
625
|
protocolVersion,
|
|
631
|
-
allowedActions: [...allowedActionsFor(protocolVersion, seat, finalTurnCount + 1
|
|
626
|
+
allowedActions: [...allowedActionsFor(protocolVersion, seat, isNegotiationTurnCapReached(finalTurnCount + 1, meta.maxTurns))],
|
|
627
|
+
...(timeoutContinuation ? { timeoutContinuation } : {}),
|
|
632
628
|
};
|
|
633
629
|
const userDispatchResult = await deps.agentDispatcher?.dispatch(context.userId, scope, userDispatchPayload, { timeoutMs });
|
|
634
630
|
if (!userDispatchResult || (userDispatchResult.handled === false && userDispatchResult.reason === 'no_agent')) {
|
|
635
|
-
// No agent for user —
|
|
636
|
-
|
|
631
|
+
// No agent for user — arm and persist one exact generation so they
|
|
632
|
+
// can still use respond_to_negotiation while fallback is bounded.
|
|
633
|
+
const parkGeneration = crypto.randomUUID();
|
|
637
634
|
if (deps.negotiationTimeoutQueue) {
|
|
638
|
-
await deps.negotiationTimeoutQueue.enqueueTimeout(task.id, finalTurnCount, timeoutMs);
|
|
635
|
+
await deps.negotiationTimeoutQueue.enqueueTimeout(task.id, finalTurnCount, timeoutMs, parkGeneration, timeoutContinuation);
|
|
639
636
|
}
|
|
637
|
+
await negotiationDatabase.updateTaskState(task.id, 'waiting_for_agent', undefined, undefined, parkGeneration);
|
|
640
638
|
return success({
|
|
641
639
|
message: `${query.action === 'question' ? 'Question' : 'Counter'} submitted. Counterparty responded. Your turn to respond.`,
|
|
642
640
|
negotiationId: task.id,
|
|
@@ -647,11 +645,8 @@ export function createNegotiationTools(defineTool, deps) {
|
|
|
647
645
|
});
|
|
648
646
|
}
|
|
649
647
|
if (userDispatchResult.handled === false && userDispatchResult.reason === 'waiting') {
|
|
650
|
-
//
|
|
651
|
-
await negotiationDatabase.updateTaskState(task.id, 'waiting_for_agent');
|
|
652
|
-
if (deps.negotiationTimeoutQueue) {
|
|
653
|
-
await deps.negotiationTimeoutQueue.enqueueTimeout(task.id, finalTurnCount, timeoutMs);
|
|
654
|
-
}
|
|
648
|
+
// The dispatcher armed this exact generation before acknowledging.
|
|
649
|
+
await negotiationDatabase.updateTaskState(task.id, 'waiting_for_agent', undefined, undefined, userDispatchResult.resumeToken);
|
|
655
650
|
return success({
|
|
656
651
|
message: `${query.action === 'question' ? 'Question' : 'Counter'} submitted. Counterparty countered back. Waiting for your agent's response.`,
|
|
657
652
|
negotiationId: task.id,
|
|
@@ -693,7 +688,7 @@ export function createNegotiationTools(defineTool, deps) {
|
|
|
693
688
|
outcome,
|
|
694
689
|
});
|
|
695
690
|
}
|
|
696
|
-
if (userTurnCount
|
|
691
|
+
if (isNegotiationTurnCapReached(userTurnCount, meta.maxTurns)) {
|
|
697
692
|
const fullHistory = [...historyForDispatch, aiTurn, userAgentTurn];
|
|
698
693
|
const outcome = buildNegotiationOutcome(fullHistory, userTurnCount, 'counter', meta.sourceUserId, meta.candidateUserId, isSource ? 'candidate' : 'source');
|
|
699
694
|
await negotiationDatabase.updateTaskState(task.id, 'completed');
|
|
@@ -712,11 +707,13 @@ export function createNegotiationTools(defineTool, deps) {
|
|
|
712
707
|
outcome,
|
|
713
708
|
});
|
|
714
709
|
}
|
|
715
|
-
// User's agent countered/questioned — arm
|
|
716
|
-
|
|
710
|
+
// User's agent countered/questioned — arm one exact generation for
|
|
711
|
+
// the counterparty's next turn.
|
|
712
|
+
const parkGeneration = crypto.randomUUID();
|
|
717
713
|
if (deps.negotiationTimeoutQueue) {
|
|
718
|
-
await deps.negotiationTimeoutQueue.enqueueTimeout(task.id, userTurnCount, timeoutMs);
|
|
714
|
+
await deps.negotiationTimeoutQueue.enqueueTimeout(task.id, userTurnCount, timeoutMs, parkGeneration, timeoutContinuation);
|
|
719
715
|
}
|
|
716
|
+
await negotiationDatabase.updateTaskState(task.id, 'waiting_for_agent', undefined, undefined, parkGeneration);
|
|
720
717
|
return success({
|
|
721
718
|
message: `Your agent responded with ${userAgentTurn.action}. Waiting for counterparty.`,
|
|
722
719
|
negotiationId: task.id,
|
|
@@ -35,7 +35,12 @@ export { NEGOTIATOR_STANCES, DEFAULT_NEGOTIATOR_STANCE, configuredNegotiatorStan
|
|
|
35
35
|
export type { NegotiatorStance } from "./negotiation.stance.contracts.js";
|
|
36
36
|
export { NegotiationTurnSchema, SystemNegotiationTurnSchema, FinalNegotiationTurnSchema, NegotiationOutcomeSchema, NegotiationGraphState, } from "./negotiation.state.js";
|
|
37
37
|
export type { NegotiationTurn, NegotiationOutcome, UserNegotiationContext, SeedAssessment, NegotiationGraphLike, NegotiationMessage, } from "./negotiation.state.js";
|
|
38
|
+
export { DEFAULT_NEGOTIATION_MAX_TURNS, isNegotiationTurnCapReached, } from "./negotiation.turn-cap.js";
|
|
39
|
+
export { expectedNegotiationSpeaker } from "./negotiation.expected-speaker.js";
|
|
40
|
+
export type { NegotiationSpeakerParticipants, NegotiationSpeakerMessage, } from "./negotiation.expected-speaker.js";
|
|
38
41
|
export { InitiatorTurnSchema, CounterpartyTurnSchema, FinalInitiatorTurnSchema, FinalCounterpartyTurnSchema, InitiatorAskUserTurnSchema, CounterpartyAskUserTurnSchema, allowedActionsFor, turnSchemaFor, isTerminalAction, isRejectLikeAction, fallbackActionFor, rejectActionFor, readProtocolVersion, configuredProtocolVersion, configuredAskUserEnabled, askUserAnswerWindowMs, ASK_USER_LOCK_SLACK_MS, DEFAULT_ASK_USER_WINDOW_MS, resolveSeat, seatViolationMessage, } from "./negotiation.protocol.js";
|
|
42
|
+
export { HERMES_OWNER_DIRECTIVE, HERMES_SHARED_MESSAGE_TEMPLATES, HermesNegotiationActionSchema, HermesNegotiationResponseSchema, HermesOwnerDirectiveSchema, HermesRoleAlignmentSchema, allowedHermesActionsFor, buildHermesNegotiationTurn, } from "./negotiation.hermes-contract.js";
|
|
43
|
+
export type { HermesNegotiationAction, HermesNegotiationResponse, HermesOwnerDirective, HermesRoleAlignment, } from "./negotiation.hermes-contract.js";
|
|
39
44
|
export { configuredDeadlockShiftEnabled, configuredDeadlockThreshold, assessDeadlock, renderBargainingShiftSection, DEFAULT_DEADLOCK_THRESHOLD, MIN_DEADLOCK_THRESHOLD, } from "./negotiation.deadlock.js";
|
|
40
45
|
export type { DeadlockAssessment } from "./negotiation.deadlock.js";
|
|
41
46
|
export type { DeadlockShiftRecord } from "./negotiation.deadlock.contracts.js";
|
|
@@ -36,7 +36,11 @@ export { NEGOTIATOR_STANCES, DEFAULT_NEGOTIATOR_STANCE, configuredNegotiatorStan
|
|
|
36
36
|
// ── Graph state and DTOs ──────────────────────────────────────────────────────
|
|
37
37
|
export { NegotiationTurnSchema, SystemNegotiationTurnSchema, FinalNegotiationTurnSchema, NegotiationOutcomeSchema, NegotiationGraphState, } from "./negotiation.state.js";
|
|
38
38
|
// ── Protocol rules ────────────────────────────────────────────────────────────
|
|
39
|
+
export { DEFAULT_NEGOTIATION_MAX_TURNS, isNegotiationTurnCapReached, } from "./negotiation.turn-cap.js";
|
|
40
|
+
export { expectedNegotiationSpeaker } from "./negotiation.expected-speaker.js";
|
|
39
41
|
export { InitiatorTurnSchema, CounterpartyTurnSchema, FinalInitiatorTurnSchema, FinalCounterpartyTurnSchema, InitiatorAskUserTurnSchema, CounterpartyAskUserTurnSchema, allowedActionsFor, turnSchemaFor, isTerminalAction, isRejectLikeAction, fallbackActionFor, rejectActionFor, readProtocolVersion, configuredProtocolVersion, configuredAskUserEnabled, askUserAnswerWindowMs, ASK_USER_LOCK_SLACK_MS, DEFAULT_ASK_USER_WINDOW_MS, resolveSeat, seatViolationMessage, } from "./negotiation.protocol.js";
|
|
42
|
+
// ── Hermes structural response contract ──────────────────────────────────────
|
|
43
|
+
export { HERMES_OWNER_DIRECTIVE, HERMES_SHARED_MESSAGE_TEMPLATES, HermesNegotiationActionSchema, HermesNegotiationResponseSchema, HermesOwnerDirectiveSchema, HermesRoleAlignmentSchema, allowedHermesActionsFor, buildHermesNegotiationTurn, } from "./negotiation.hermes-contract.js";
|
|
40
44
|
// ── Deadlock detection ────────────────────────────────────────────────────────
|
|
41
45
|
export { configuredDeadlockShiftEnabled, configuredDeadlockThreshold, assessDeadlock, renderBargainingShiftSection, DEFAULT_DEADLOCK_THRESHOLD, MIN_DEADLOCK_THRESHOLD, } from "./negotiation.deadlock.js";
|
|
42
46
|
// ── Lifecycle narration ───────────────────────────────────────────────────────
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type { NegotiationAction, NegotiationProtocolVersion, NegotiationSeat } from "../../shared/schemas/negotiation-state.schema.js";
|
|
1
|
+
import type { NegotiationAction, NegotiationConsultationReason, NegotiationProtocolVersion, NegotiationSeat } from "../../shared/schemas/negotiation-state.schema.js";
|
|
2
|
+
export type { NegotiationConsultationReason } from "../../shared/schemas/negotiation-state.schema.js";
|
|
2
3
|
/** Independent rollout modes for IND-508's deterministic consultation policy. */
|
|
3
4
|
export type NegotiationConsultationPolicyMode = "off" | "shadow" | "on";
|
|
4
|
-
/** Stable, content-free categories emitted by the consultation funnel. */
|
|
5
|
-
export type NegotiationConsultationReason = "unresolved_owner_constraint" | "consequential_disclosure_permission" | "repeated_non_convergence" | "insufficient_commitment_authority";
|
|
6
5
|
/** The only data the policy may inspect: action/role enums and routing coordinates. */
|
|
7
6
|
export interface ConsultationEligibilityInput {
|
|
8
7
|
protocolVersion: NegotiationProtocolVersion;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface NegotiationSpeakerParticipants {
|
|
2
|
+
sourceUserId?: unknown;
|
|
3
|
+
candidateUserId?: unknown;
|
|
4
|
+
}
|
|
5
|
+
export interface NegotiationSpeakerMessage {
|
|
6
|
+
senderId?: unknown;
|
|
7
|
+
parts?: unknown;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolves the participant whose agent owns the next canonical bilateral turn.
|
|
11
|
+
*
|
|
12
|
+
* Participant identities must be nonempty and distinct. Unrelated agent,
|
|
13
|
+
* system, and owner-settlement messages are ignored while finding the latest
|
|
14
|
+
* source/candidate message. An ordinary canonical message passes the floor to
|
|
15
|
+
* the other participant; `ask_user` retains it for the consulting sender's
|
|
16
|
+
* exact successor. A valid conversation with no canonical history starts with
|
|
17
|
+
* the source participant. Invalid participant metadata always fails closed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function expectedNegotiationSpeaker(participants: NegotiationSpeakerParticipants, messages: readonly NegotiationSpeakerMessage[]): string | null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
function canonicalAction(message) {
|
|
2
|
+
if (!Array.isArray(message.parts))
|
|
3
|
+
return null;
|
|
4
|
+
for (const part of message.parts) {
|
|
5
|
+
if (!part || typeof part !== 'object' || Array.isArray(part))
|
|
6
|
+
continue;
|
|
7
|
+
const partRecord = part;
|
|
8
|
+
if (partRecord.kind !== 'data' || !partRecord.data || typeof partRecord.data !== 'object' || Array.isArray(partRecord.data))
|
|
9
|
+
continue;
|
|
10
|
+
const action = partRecord.data.action;
|
|
11
|
+
return typeof action === 'string' ? action : null;
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
function participantId(value) {
|
|
16
|
+
return typeof value === 'string' && value.trim().length > 0 ? value : null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolves the participant whose agent owns the next canonical bilateral turn.
|
|
20
|
+
*
|
|
21
|
+
* Participant identities must be nonempty and distinct. Unrelated agent,
|
|
22
|
+
* system, and owner-settlement messages are ignored while finding the latest
|
|
23
|
+
* source/candidate message. An ordinary canonical message passes the floor to
|
|
24
|
+
* the other participant; `ask_user` retains it for the consulting sender's
|
|
25
|
+
* exact successor. A valid conversation with no canonical history starts with
|
|
26
|
+
* the source participant. Invalid participant metadata always fails closed.
|
|
27
|
+
*/
|
|
28
|
+
export function expectedNegotiationSpeaker(participants, messages) {
|
|
29
|
+
const source = participantId(participants.sourceUserId);
|
|
30
|
+
const candidate = participantId(participants.candidateUserId);
|
|
31
|
+
if (!source || !candidate || source === candidate)
|
|
32
|
+
return null;
|
|
33
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
34
|
+
const message = messages[index];
|
|
35
|
+
const sender = message?.senderId === `agent:${source}`
|
|
36
|
+
? source
|
|
37
|
+
: message?.senderId === `agent:${candidate}`
|
|
38
|
+
? candidate
|
|
39
|
+
: null;
|
|
40
|
+
if (!sender)
|
|
41
|
+
continue;
|
|
42
|
+
return canonicalAction(message) === 'ask_user'
|
|
43
|
+
? sender
|
|
44
|
+
: sender === source ? candidate : source;
|
|
45
|
+
}
|
|
46
|
+
return source;
|
|
47
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { NegotiationAction, NegotiationTurn } from '../../shared/schemas/negotiation-state.schema.js';
|
|
3
|
+
/** Model-visible directives for the dedicated Hermes negotiation bridge. */
|
|
4
|
+
export declare const HermesNegotiationActionSchema: z.ZodEnum<["accept", "decline", "request_time", "continue"]>;
|
|
5
|
+
export type HermesNegotiationAction = z.infer<typeof HermesNegotiationActionSchema>;
|
|
6
|
+
export declare const HermesRoleAlignmentSchema: z.ZodEnum<["peers", "owner_leads", "counterparty_leads"]>;
|
|
7
|
+
export type HermesRoleAlignment = z.infer<typeof HermesRoleAlignmentSchema>;
|
|
8
|
+
/**
|
|
9
|
+
* No model-authored prose is accepted at this boundary. Strict parsing also
|
|
10
|
+
* prevents model-selected run IDs or capabilities from entering tool arguments.
|
|
11
|
+
*/
|
|
12
|
+
export declare const HermesNegotiationResponseSchema: z.ZodObject<{
|
|
13
|
+
action: z.ZodEnum<["accept", "decline", "request_time", "continue"]>;
|
|
14
|
+
roleAlignment: z.ZodEnum<["peers", "owner_leads", "counterparty_leads"]>;
|
|
15
|
+
}, "strict", z.ZodTypeAny, {
|
|
16
|
+
action: "accept" | "decline" | "request_time" | "continue";
|
|
17
|
+
roleAlignment: "peers" | "owner_leads" | "counterparty_leads";
|
|
18
|
+
}, {
|
|
19
|
+
action: "accept" | "decline" | "request_time" | "continue";
|
|
20
|
+
roleAlignment: "peers" | "owner_leads" | "counterparty_leads";
|
|
21
|
+
}>;
|
|
22
|
+
export type HermesNegotiationResponse = z.infer<typeof HermesNegotiationResponseSchema>;
|
|
23
|
+
/** Only privacy-reviewed server prose can enter the shared transcript. */
|
|
24
|
+
export declare const HERMES_SHARED_MESSAGE_TEMPLATES: Readonly<Record<HermesNegotiationAction, string>>;
|
|
25
|
+
export declare const HermesOwnerDirectiveSchema: z.ZodEnum<["protect_private_context"]>;
|
|
26
|
+
export type HermesOwnerDirective = z.infer<typeof HermesOwnerDirectiveSchema>;
|
|
27
|
+
export declare const HERMES_OWNER_DIRECTIVE: HermesOwnerDirective;
|
|
28
|
+
/** Project exact seat/version actions into the closed Hermes vocabulary. */
|
|
29
|
+
export declare function allowedHermesActionsFor(allowedActions: readonly NegotiationAction[]): HermesNegotiationAction[];
|
|
30
|
+
/** Build the complete persisted turn without using any model-authored prose. */
|
|
31
|
+
export declare function buildHermesNegotiationTurn(input: HermesNegotiationResponse, allowedActions: readonly NegotiationAction[]): NegotiationTurn | null;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Model-visible directives for the dedicated Hermes negotiation bridge. */
|
|
3
|
+
export const HermesNegotiationActionSchema = z.enum([
|
|
4
|
+
'accept',
|
|
5
|
+
'decline',
|
|
6
|
+
'request_time',
|
|
7
|
+
'continue',
|
|
8
|
+
]);
|
|
9
|
+
export const HermesRoleAlignmentSchema = z.enum([
|
|
10
|
+
'peers',
|
|
11
|
+
'owner_leads',
|
|
12
|
+
'counterparty_leads',
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* No model-authored prose is accepted at this boundary. Strict parsing also
|
|
16
|
+
* prevents model-selected run IDs or capabilities from entering tool arguments.
|
|
17
|
+
*/
|
|
18
|
+
export const HermesNegotiationResponseSchema = z.object({
|
|
19
|
+
action: HermesNegotiationActionSchema,
|
|
20
|
+
roleAlignment: HermesRoleAlignmentSchema,
|
|
21
|
+
}).strict();
|
|
22
|
+
/** Only privacy-reviewed server prose can enter the shared transcript. */
|
|
23
|
+
export const HERMES_SHARED_MESSAGE_TEMPLATES = Object.freeze({
|
|
24
|
+
accept: 'I am ready to proceed with this opportunity.',
|
|
25
|
+
decline: 'I am going to decline this opportunity.',
|
|
26
|
+
request_time: 'I need more time before deciding.',
|
|
27
|
+
continue: 'I am open to continuing within the current scope.',
|
|
28
|
+
});
|
|
29
|
+
export const HermesOwnerDirectiveSchema = z.enum(['protect_private_context']);
|
|
30
|
+
export const HERMES_OWNER_DIRECTIVE = 'protect_private_context';
|
|
31
|
+
const ACTION_CANDIDATES = Object.freeze({
|
|
32
|
+
accept: ['accept'],
|
|
33
|
+
decline: ['decline', 'withdraw', 'reject'],
|
|
34
|
+
request_time: ['counter', 'outreach', 'propose'],
|
|
35
|
+
continue: ['question', 'outreach', 'propose', 'counter'],
|
|
36
|
+
});
|
|
37
|
+
const ACTION_ORDER = [
|
|
38
|
+
'accept',
|
|
39
|
+
'decline',
|
|
40
|
+
'request_time',
|
|
41
|
+
'continue',
|
|
42
|
+
];
|
|
43
|
+
/** Project exact seat/version actions into the closed Hermes vocabulary. */
|
|
44
|
+
export function allowedHermesActionsFor(allowedActions) {
|
|
45
|
+
const allowed = new Set(allowedActions);
|
|
46
|
+
return ACTION_ORDER.filter((action) => ACTION_CANDIDATES[action].some((candidate) => allowed.has(candidate)));
|
|
47
|
+
}
|
|
48
|
+
function protocolActionFor(action, allowedActions) {
|
|
49
|
+
const allowed = new Set(allowedActions);
|
|
50
|
+
return ACTION_CANDIDATES[action].find((candidate) => allowed.has(candidate)) ?? null;
|
|
51
|
+
}
|
|
52
|
+
function suggestedRolesFor(alignment) {
|
|
53
|
+
if (alignment === 'owner_leads')
|
|
54
|
+
return { ownUser: 'agent', otherUser: 'patient' };
|
|
55
|
+
if (alignment === 'counterparty_leads')
|
|
56
|
+
return { ownUser: 'patient', otherUser: 'agent' };
|
|
57
|
+
return { ownUser: 'peer', otherUser: 'peer' };
|
|
58
|
+
}
|
|
59
|
+
/** Build the complete persisted turn without using any model-authored prose. */
|
|
60
|
+
export function buildHermesNegotiationTurn(input, allowedActions) {
|
|
61
|
+
const action = protocolActionFor(input.action, allowedActions);
|
|
62
|
+
if (!action)
|
|
63
|
+
return null;
|
|
64
|
+
return {
|
|
65
|
+
action,
|
|
66
|
+
message: HERMES_SHARED_MESSAGE_TEMPLATES[input.action],
|
|
67
|
+
assessment: {
|
|
68
|
+
reasoning: `Hermes selected the closed ${input.action} directive.`,
|
|
69
|
+
suggestedRoles: suggestedRolesFor(input.roleAlignment),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|