@indexnetwork/protocol 6.11.1-rc.402.1 → 6.12.1-rc.404.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 +1 -0
- package/dist/negotiation/negotiation.agent.d.ts +10 -0
- package/dist/negotiation/negotiation.agent.d.ts.map +1 -1
- package/dist/negotiation/negotiation.agent.js +26 -8
- package/dist/negotiation/negotiation.agent.js.map +1 -1
- package/dist/negotiation/negotiation.attribution.d.ts +92 -0
- package/dist/negotiation/negotiation.attribution.d.ts.map +1 -0
- package/dist/negotiation/negotiation.attribution.js +110 -0
- package/dist/negotiation/negotiation.attribution.js.map +1 -0
- package/dist/negotiation/negotiation.graph.d.ts +19 -0
- package/dist/negotiation/negotiation.graph.d.ts.map +1 -1
- package/dist/negotiation/negotiation.graph.js +128 -6
- package/dist/negotiation/negotiation.graph.js.map +1 -1
- package/dist/negotiation/negotiation.screen.d.ts +24 -0
- package/dist/negotiation/negotiation.screen.d.ts.map +1 -1
- package/dist/negotiation/negotiation.screen.js +20 -1
- package/dist/negotiation/negotiation.screen.js.map +1 -1
- package/dist/negotiation/negotiation.state.d.ts +33 -0
- package/dist/negotiation/negotiation.state.d.ts.map +1 -1
- package/dist/negotiation/negotiation.state.js +33 -0
- package/dist/negotiation/negotiation.state.js.map +1 -1
- package/dist/opportunity/opportunity.evidence.d.ts.map +1 -1
- package/dist/opportunity/opportunity.evidence.js +8 -1
- package/dist/opportunity/opportunity.evidence.js.map +1 -1
- package/dist/opportunity/opportunity.graph.d.ts.map +1 -1
- package/dist/opportunity/opportunity.graph.js +91 -4
- package/dist/opportunity/opportunity.graph.js.map +1 -1
- package/dist/shared/interfaces/agent-dispatcher.interface.d.ts +8 -0
- package/dist/shared/interfaces/agent-dispatcher.interface.d.ts.map +1 -1
- package/dist/shared/interfaces/agent-dispatcher.interface.js.map +1 -1
- package/dist/shared/interfaces/database.interface.d.ts +27 -2
- package/dist/shared/interfaces/database.interface.d.ts.map +1 -1
- package/dist/shared/interfaces/database.interface.js.map +1 -1
- package/dist/shared/schemas/question.schema.d.ts +37 -7
- package/dist/shared/schemas/question.schema.d.ts.map +1 -1
- package/dist/shared/schemas/question.schema.js +11 -1
- package/dist/shared/schemas/question.schema.js.map +1 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ import { NegotiationScreener, configuredScreenMode } from "./negotiation.screen.
|
|
|
9
9
|
import { assessDeadlock, configuredDeadlockShiftEnabled, configuredDeadlockThreshold } from "./negotiation.deadlock.js";
|
|
10
10
|
import { protocolLogger } from "../shared/observability/protocol.logger.js";
|
|
11
11
|
import { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, negotiationQuestionSettlementId, validateInflightAskUserFields } from './negotiation.question-safety.js';
|
|
12
|
+
import { attributedDialogueIsEmpty, buildSeededAttribution, combineAttributedDialogue } from './negotiation.attribution.js';
|
|
12
13
|
const logger = protocolLogger("NegotiationGraph");
|
|
13
14
|
const initLog = protocolLogger("NegotiationGraph:Init");
|
|
14
15
|
const screenNodeLog = protocolLogger("NegotiationGraph:Screen");
|
|
@@ -100,6 +101,61 @@ export class NegotiationGraphFactory {
|
|
|
100
101
|
return [];
|
|
101
102
|
}
|
|
102
103
|
};
|
|
104
|
+
/**
|
|
105
|
+
* IND-569: resolve a prior negotiation task's attribution metadata for the
|
|
106
|
+
* per-opportunity prior-dialogue labels. Never throws — any failure returns
|
|
107
|
+
* null so the turns degrade to the unattributed block instead of leaking
|
|
108
|
+
* into the current opportunity's context.
|
|
109
|
+
*/
|
|
110
|
+
const resolveTaskAttribution = async (taskId) => {
|
|
111
|
+
try {
|
|
112
|
+
const task = await database.getTask(taskId);
|
|
113
|
+
if (!task)
|
|
114
|
+
return null;
|
|
115
|
+
const md = (task.metadata ?? {});
|
|
116
|
+
const opportunityId = typeof md.opportunityId === 'string' && md.opportunityId.length > 0 ? md.opportunityId : null;
|
|
117
|
+
const snapshots = Array.isArray(md.intentSnapshots) ? md.intentSnapshots : [];
|
|
118
|
+
const sourceIntentId = typeof md.sourceIntentId === 'string' ? md.sourceIntentId : null;
|
|
119
|
+
const snap = snapshots.find((s) => s && s.intentId === sourceIntentId) ?? snapshots[0];
|
|
120
|
+
const opportunityTitle = snap && typeof snap.title === 'string' && snap.title.trim().length > 0 ? snap.title : null;
|
|
121
|
+
let outcome = null;
|
|
122
|
+
try {
|
|
123
|
+
const artifacts = await database.getArtifactsForTask(taskId);
|
|
124
|
+
const outArtifact = artifacts.find((a) => a.name === 'negotiation-outcome');
|
|
125
|
+
if (outArtifact) {
|
|
126
|
+
const firstPart = Array.isArray(outArtifact.parts) ? outArtifact.parts[0] : undefined;
|
|
127
|
+
const data = firstPart?.data;
|
|
128
|
+
if (data && typeof data.hasOpportunity === 'boolean') {
|
|
129
|
+
outcome = data.hasOpportunity ? 'accepted' : (data.reason === 'screened_out' ? 'not pursued' : 'declined');
|
|
130
|
+
}
|
|
131
|
+
else if (typeof outArtifact.metadata?.continuationOutcome === 'string') {
|
|
132
|
+
outcome = outArtifact.metadata.continuationOutcome;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch { /* outcome stays null; header degrades to "outcome unknown" */ }
|
|
137
|
+
const concludedAt = task.updatedAt instanceof Date
|
|
138
|
+
? task.updatedAt.toISOString()
|
|
139
|
+
: (task.updatedAt ? new Date(task.updatedAt).toISOString() : null);
|
|
140
|
+
return { opportunityId, opportunityTitle, outcome, concludedAt };
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* IND-569: build the attributed prior dialogue passed to the screener and
|
|
148
|
+
* turn prompts. Combines the immutable seeded attribution (earlier + legacy
|
|
149
|
+
* unattributed blocks, resolved once in init) with this session's own turns
|
|
150
|
+
* (task-id-matched). Null when there is no seeded attribution.
|
|
151
|
+
*/
|
|
152
|
+
const buildAttributedDialogue = (state) => {
|
|
153
|
+
if (!state.isContinuation || !state.priorAttribution)
|
|
154
|
+
return null;
|
|
155
|
+
const currentSessionTurns = turnsFromMessages(state.messages.filter((m) => m.taskId === state.taskId));
|
|
156
|
+
const dialogue = combineAttributedDialogue(state.priorAttribution, currentSessionTurns);
|
|
157
|
+
return attributedDialogueIsEmpty(dialogue) ? null : dialogue;
|
|
158
|
+
};
|
|
103
159
|
/** Similarity query text: seed reasoning + counterparty context. */
|
|
104
160
|
const memoryQueryText = (state, counterparty) => [
|
|
105
161
|
state.discoveryQuery ? `Search: ${state.discoveryQuery}` : "",
|
|
@@ -301,14 +357,25 @@ export class NegotiationGraphFactory {
|
|
|
301
357
|
return [];
|
|
302
358
|
})
|
|
303
359
|
: [];
|
|
304
|
-
// Seed messages with prior turns (additive reducer appends new turns on top)
|
|
360
|
+
// Seed messages with prior turns (additive reducer appends new turns on top).
|
|
361
|
+
// taskId is preserved so the turn/screen nodes can separate this
|
|
362
|
+
// session's turns from seeded prior-task turns (IND-569).
|
|
305
363
|
const seedMessages = isContinuation ? priorMessages.map((m) => ({
|
|
306
364
|
id: m.id,
|
|
307
365
|
senderId: m.senderId,
|
|
308
366
|
role: 'agent',
|
|
309
367
|
parts: m.parts,
|
|
310
368
|
createdAt: m.createdAt,
|
|
369
|
+
taskId: m.taskId ?? null,
|
|
311
370
|
})) : [];
|
|
371
|
+
// IND-569: attribute seeded prior turns to their originating opportunity
|
|
372
|
+
// once, up front. Earlier-opportunity and legacy unattributed blocks are
|
|
373
|
+
// immutable for the session; the current block is composed per turn.
|
|
374
|
+
const priorAttribution = isContinuation
|
|
375
|
+
? await buildSeededAttribution(priorMessages
|
|
376
|
+
.map((m) => ({ taskId: m.taskId ?? null, turn: turnsFromMessages([m])[0] }))
|
|
377
|
+
.filter((e) => Boolean(e.turn)), state.opportunityId, resolveTaskAttribution)
|
|
378
|
+
: null;
|
|
312
379
|
return {
|
|
313
380
|
conversationId: conversation.id,
|
|
314
381
|
taskId: task.id,
|
|
@@ -319,6 +386,7 @@ export class NegotiationGraphFactory {
|
|
|
319
386
|
initiatorUserId,
|
|
320
387
|
protocolVersion,
|
|
321
388
|
priorTurnCount: priorTurns.length,
|
|
389
|
+
...(priorAttribution && { priorAttribution }),
|
|
322
390
|
...(userAnswers.length > 0 && { userAnswers }),
|
|
323
391
|
...(exactContinuation?.execution.consultation
|
|
324
392
|
? { privateConsultation: exactContinuation.execution.consultation }
|
|
@@ -369,6 +437,14 @@ export class NegotiationGraphFactory {
|
|
|
369
437
|
let decision;
|
|
370
438
|
let failedOpen = false;
|
|
371
439
|
let screenError;
|
|
440
|
+
// On continuations the seeded prior turns are the dialogue this pair
|
|
441
|
+
// already had. Pass them so the gate evaluates the NEW signal
|
|
442
|
+
// (discoveryQuery / seedAssessment) on its own merits with that context
|
|
443
|
+
// available — mirroring the continuation policy in negotiation.agent.ts
|
|
444
|
+
// ("if materially different, evaluate on its own merits").
|
|
445
|
+
const priorDialogue = state.isContinuation ? turnsFromMessages(state.messages) : [];
|
|
446
|
+
// IND-569: labeled per-opportunity attribution of that prior dialogue.
|
|
447
|
+
const attributedPrior = buildAttributedDialogue(state);
|
|
372
448
|
try {
|
|
373
449
|
const counterpartyContext = (await database.getUserContext(counterpartyUser.id, null).catch(() => null))?.text ?? "";
|
|
374
450
|
decision = await screener.invoke({
|
|
@@ -381,6 +457,8 @@ export class NegotiationGraphFactory {
|
|
|
381
457
|
...(clientIsSource && state.discoveryQuery && { discoveryQuery: state.discoveryQuery }),
|
|
382
458
|
seedAssessment: state.seedAssessment,
|
|
383
459
|
indexContext: state.indexContext,
|
|
460
|
+
...(priorDialogue.length > 0 && { isContinuation: true, priorDialogue }),
|
|
461
|
+
...(attributedPrior && { isContinuation: true, priorDialogueAttributed: attributedPrior }),
|
|
384
462
|
});
|
|
385
463
|
}
|
|
386
464
|
catch (err) {
|
|
@@ -453,6 +531,9 @@ export class NegotiationGraphFactory {
|
|
|
453
531
|
traceEmitter?.({ type: "agent_start", name: agentName });
|
|
454
532
|
try {
|
|
455
533
|
const history = turnsFromMessages(state.messages);
|
|
534
|
+
// IND-569: labeled per-opportunity attribution of prior dialogue on a
|
|
535
|
+
// continuation. Null on fresh runs (history renders flat, unchanged).
|
|
536
|
+
const attributedPrior = buildAttributedDialogue(state);
|
|
456
537
|
const isSource = state.currentSpeaker === "source";
|
|
457
538
|
const ownUser = isSource ? state.sourceUser : state.candidateUser;
|
|
458
539
|
const otherUser = isSource ? state.candidateUser : state.sourceUser;
|
|
@@ -526,6 +607,7 @@ export class NegotiationGraphFactory {
|
|
|
526
607
|
protocolVersion: version,
|
|
527
608
|
allowedActions: [...allowedActionsFor(version, seat, isFinalTurn, { askUser: askUserAvailable })],
|
|
528
609
|
...(state.discoveryQuery && isSource && { discoveryQuery: state.discoveryQuery }),
|
|
610
|
+
...(attributedPrior && { priorDialogue: attributedPrior }),
|
|
529
611
|
...(ownMemory.length > 0 && { negotiatorMemory: ownMemory }),
|
|
530
612
|
...(state.privateConsultation?.recipientUserId === ownUser.id
|
|
531
613
|
? { privateConsultation: state.privateConsultation }
|
|
@@ -584,6 +666,7 @@ export class NegotiationGraphFactory {
|
|
|
584
666
|
protocolVersion: version,
|
|
585
667
|
...(state.discoveryQuery && isSource && { discoveryQuery: state.discoveryQuery }),
|
|
586
668
|
isContinuation: state.isContinuation,
|
|
669
|
+
...(attributedPrior && { priorDialogue: attributedPrior }),
|
|
587
670
|
...(state.userAnswers.length > 0 && { userAnswers: state.userAnswers }),
|
|
588
671
|
...(askUserAvailable && { canAskUser: true }),
|
|
589
672
|
...(bargainingMode && { bargaining: { consecutiveNonConvergent: deadlock.consecutiveNonConvergent } }),
|
|
@@ -700,6 +783,33 @@ export class NegotiationGraphFactory {
|
|
|
700
783
|
});
|
|
701
784
|
}
|
|
702
785
|
}
|
|
786
|
+
// ─── IND-564: `withdraw` requires a prior in-task outreach ────────────
|
|
787
|
+
// `withdraw` semantically retracts an outreach the initiator made. In a
|
|
788
|
+
// continuation whose first initiator move is `withdraw` (or any withdraw
|
|
789
|
+
// before this task opened outreach), there is nothing to retract —
|
|
790
|
+
// persisting it would drop a spurious "connection withdrawn" message
|
|
791
|
+
// into the shared dm_pair thread as if an accepted connection were
|
|
792
|
+
// pulled. Seeded prior-task turns (state.messages) do NOT count as an
|
|
793
|
+
// in-task outreach. Map it to the quiet screen-out outcome instead: no
|
|
794
|
+
// message persisted, turnCount unchanged, opportunity quietly rejected
|
|
795
|
+
// in finalize. This is the backstop for whatever the screen gate (IND-
|
|
796
|
+
// 563) does not catch (screen off/shadow, or a fail-open reach_out).
|
|
797
|
+
//
|
|
798
|
+
// Exact ask_user resumes (continuationExecution) are exempt: the
|
|
799
|
+
// successor task is the SAME logical negotiation resumed after the
|
|
800
|
+
// client answered, so a post-consultation `withdraw` is a legitimate
|
|
801
|
+
// terminal decision, not an opening move.
|
|
802
|
+
if (turn.action === 'withdraw' && !state.outreachOpened && !state.continuationExecution) {
|
|
803
|
+
turnLog.info('negotiation_opening_withdraw_screened_out', {
|
|
804
|
+
taskId: state.taskId,
|
|
805
|
+
opportunityId: state.opportunityId || undefined,
|
|
806
|
+
seat,
|
|
807
|
+
turnCount: state.turnCount,
|
|
808
|
+
isContinuation: state.isContinuation,
|
|
809
|
+
});
|
|
810
|
+
traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: "screened_out: opening withdraw" });
|
|
811
|
+
return { lastTurn: turn, firstTurnScreenedOut: true };
|
|
812
|
+
}
|
|
703
813
|
const parts = [{ kind: "data", data: turn }];
|
|
704
814
|
const message = await database.createMessage({
|
|
705
815
|
conversationId: state.conversationId,
|
|
@@ -838,6 +948,7 @@ export class NegotiationGraphFactory {
|
|
|
838
948
|
role: "agent",
|
|
839
949
|
parts: message.parts,
|
|
840
950
|
createdAt: message.createdAt,
|
|
951
|
+
taskId: state.taskId,
|
|
841
952
|
}],
|
|
842
953
|
turnCount: state.turnCount + 1,
|
|
843
954
|
lastTurn: turn,
|
|
@@ -867,11 +978,14 @@ export class NegotiationGraphFactory {
|
|
|
867
978
|
role: "agent",
|
|
868
979
|
parts: message.parts,
|
|
869
980
|
createdAt: message.createdAt,
|
|
981
|
+
taskId: state.taskId,
|
|
870
982
|
}],
|
|
871
983
|
turnCount: state.turnCount + 1,
|
|
872
984
|
currentSpeaker: (isSource ? "candidate" : "source"),
|
|
873
985
|
lastTurn: turn,
|
|
874
986
|
memoryBySide: { [ownSide]: ownMemory },
|
|
987
|
+
// Record the in-task outreach so a later `withdraw` is legal (IND-564).
|
|
988
|
+
...(turn.action === 'outreach' && { outreachOpened: true }),
|
|
875
989
|
...(deadlockShiftRecord && { deadlockShift: deadlockShiftRecord }),
|
|
876
990
|
};
|
|
877
991
|
}
|
|
@@ -964,7 +1078,10 @@ export class NegotiationGraphFactory {
|
|
|
964
1078
|
const hasOpportunity = lastTurn?.action === "accept";
|
|
965
1079
|
// P2.2: the client's own outreach gate declined before any turn — the
|
|
966
1080
|
// negotiation never happened from the counterparty's perspective.
|
|
967
|
-
|
|
1081
|
+
// IND-564: an opening-move `withdraw` blocked before any message was
|
|
1082
|
+
// persisted is the same quiet screen-out outcome (no in-task outreach to
|
|
1083
|
+
// retract), reached from the turn node rather than the screen node.
|
|
1084
|
+
const screenedOut = isScreenBlocked(state) || state.firstTurnScreenedOut === true;
|
|
968
1085
|
const atCap = !screenedOut && (state.maxTurns ?? 0) > 0 && state.turnCount >= state.maxTurns && !isTerminalAction(lastTurn?.action);
|
|
969
1086
|
let agreedRoles = [];
|
|
970
1087
|
if (hasOpportunity && history.length >= 2) {
|
|
@@ -983,7 +1100,7 @@ export class NegotiationGraphFactory {
|
|
|
983
1100
|
hasOpportunity,
|
|
984
1101
|
agreedRoles,
|
|
985
1102
|
reasoning: screenedOut
|
|
986
|
-
? (state.screenDecision?.reasoning ?? "")
|
|
1103
|
+
? (state.screenDecision?.reasoning ?? lastTurn?.assessment?.reasoning ?? "")
|
|
987
1104
|
: (lastTurn?.assessment.reasoning ?? ""),
|
|
988
1105
|
turnCount: state.turnCount,
|
|
989
1106
|
...(screenedOut
|
|
@@ -1169,9 +1286,14 @@ export class NegotiationGraphFactory {
|
|
|
1169
1286
|
.addConditionalEdges("init", (state) => {
|
|
1170
1287
|
if (state.error)
|
|
1171
1288
|
return "finalize";
|
|
1172
|
-
// Screen gate
|
|
1173
|
-
//
|
|
1174
|
-
|
|
1289
|
+
// Screen gate (P2.1). Runs on fresh negotiations AND regular
|
|
1290
|
+
// continuations (IND-563): a new opportunity/intent reusing an existing
|
|
1291
|
+
// conversation must still pass the outreach gate before entering the
|
|
1292
|
+
// shared thread — a bad continuation match should be quietly screened
|
|
1293
|
+
// out, never dropped into the dm_pair. Exact ask_user resumes
|
|
1294
|
+
// (continuationExecution) are mid-flight and must never be re-screened.
|
|
1295
|
+
// `off` disables the node entirely.
|
|
1296
|
+
if (configuredScreenMode() !== "off" && !state.continuationExecution)
|
|
1175
1297
|
return "screen";
|
|
1176
1298
|
return "turn";
|
|
1177
1299
|
}, { screen: "screen", turn: "turn", finalize: "finalize" })
|