@indexnetwork/protocol 14.3.2-rc.477.1 → 17.0.0-rc.479.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 +141 -0
- package/IMPLEMENTATION.md +50 -52
- package/STABILITY.md +3 -3
- package/dist/chat/chat.agent.js +2 -3
- package/dist/chat/chat.graph.d.ts +41 -1
- package/dist/chat/chat.graph.js +108 -127
- package/dist/chat/negotiator.persona.d.ts +1 -1
- package/dist/chat/negotiator.persona.js +1 -4
- package/dist/chat/negotiator.prompt.js +1 -2
- package/dist/chat/onboarding.persona.js +0 -1
- package/dist/chat/reporter.persona.js +0 -1
- package/dist/chat/signal.persona.js +0 -1
- package/dist/contacts/application/contact.tools.d.ts +6 -9
- package/dist/contacts/application/contact.tools.js +11 -92
- package/dist/contacts/application/index.d.ts +2 -14
- package/dist/contacts/application/index.js +2 -13
- package/dist/contacts/domain/contact.types.d.ts +0 -23
- package/dist/contacts/domain/index.d.ts +2 -6
- package/dist/contacts/index.d.ts +1 -1
- package/dist/contacts/index.js +1 -1
- package/dist/contacts/ports/contact.repository.port.d.ts +5 -12
- package/dist/contacts/ports/contact.repository.port.js +4 -6
- package/dist/contacts/ports/contact.tools.port.d.ts +1 -3
- package/dist/contacts/ports/contact.tools.port.js +1 -2
- package/dist/contacts/ports/index.d.ts +1 -1
- package/dist/contacts/ports/index.js +1 -1
- package/dist/discovery/hyde.frame.d.ts +4 -4
- package/dist/discovery/hyde.graph.d.ts +71 -12
- package/dist/discovery/hyde.graph.js +337 -335
- package/dist/discovery/lens.inferrer.d.ts +6 -6
- package/dist/enrichment/enrichment.graph.d.ts +127 -9
- package/dist/enrichment/enrichment.graph.js +633 -655
- package/dist/enrichment/enrichment.state.d.ts +1 -1
- package/dist/enrichment/enrichment.tools.context-read.d.ts +10 -0
- package/dist/enrichment/enrichment.tools.context-read.js +353 -0
- package/dist/enrichment/enrichment.tools.context-write.d.ts +9 -0
- package/dist/enrichment/enrichment.tools.context-write.js +417 -0
- package/dist/enrichment/enrichment.tools.d.ts +11 -1
- package/dist/enrichment/enrichment.tools.helpers.d.ts +139 -0
- package/dist/enrichment/enrichment.tools.helpers.js +234 -0
- package/dist/enrichment/enrichment.tools.js +16 -975
- package/dist/index.d.ts +27 -50
- package/dist/index.js +17 -28
- package/dist/intents/application/intent.graph.d.ts +71 -67
- package/dist/intents/application/intent.graph.execute.d.ts +59 -0
- package/dist/intents/application/intent.graph.execute.js +301 -0
- package/dist/intents/application/intent.graph.infer.d.ts +44 -0
- package/dist/intents/application/intent.graph.infer.js +97 -0
- package/dist/intents/application/intent.graph.js +96 -888
- package/dist/intents/application/intent.graph.reconcile.d.ts +71 -0
- package/dist/intents/application/intent.graph.reconcile.js +274 -0
- package/dist/intents/application/intent.graph.shared.d.ts +70 -0
- package/dist/intents/application/intent.graph.shared.js +153 -0
- package/dist/intents/domain/intent.state.d.ts +1 -1
- package/dist/maintenance/maintenance.graph.d.ts +66 -3
- package/dist/maintenance/maintenance.graph.js +155 -156
- package/dist/mcp/mcp.authorization-policy.d.ts +24 -28
- package/dist/mcp/mcp.authorization-policy.js +14 -34
- package/dist/mcp/mcp.server.d.ts +3 -3
- package/dist/mcp/mcp.server.js +14 -20
- package/dist/negotiations/application/negotiation.candidates.d.ts +83 -0
- package/dist/negotiations/application/negotiation.candidates.js +162 -0
- package/dist/negotiations/application/negotiation.graph.d.ts +91 -157
- package/dist/negotiations/application/negotiation.graph.finalize.d.ts +5 -0
- package/dist/negotiations/application/negotiation.graph.finalize.js +280 -0
- package/dist/negotiations/application/negotiation.graph.init.d.ts +54 -0
- package/dist/negotiations/application/negotiation.graph.init.js +227 -0
- package/dist/negotiations/application/negotiation.graph.js +69 -1388
- package/dist/negotiations/application/negotiation.graph.screen.d.ts +23 -0
- package/dist/negotiations/application/negotiation.graph.screen.js +108 -0
- package/dist/negotiations/application/negotiation.graph.shared.d.ts +75 -0
- package/dist/negotiations/application/negotiation.graph.shared.js +125 -0
- package/dist/negotiations/application/negotiation.graph.turn.d.ts +105 -0
- package/dist/negotiations/application/negotiation.graph.turn.js +484 -0
- package/dist/negotiations/domain/negotiation.state.d.ts +1 -1
- package/dist/negotiations/domain/negotiation.state.js +0 -1
- package/dist/networks/application/indexer.graph.d.ts +165 -7
- package/dist/networks/application/indexer.graph.js +339 -388
- package/dist/networks/application/indexer.state.d.ts +1 -1
- package/dist/networks/application/membership.graph.d.ts +83 -5
- package/dist/networks/application/membership.graph.js +177 -207
- package/dist/networks/application/network.graph.d.ts +148 -5
- package/dist/networks/application/network.graph.js +249 -278
- package/dist/networks/domain/membership.state.d.ts +1 -1
- package/dist/networks/domain/network.state.d.ts +1 -1
- package/dist/opportunities/application/delivery-card.cache.d.ts +1 -1
- package/dist/opportunities/application/delivery-card.cache.js +2 -2
- package/dist/opportunities/application/index.d.ts +2 -2
- package/dist/opportunities/application/index.js +2 -2
- package/dist/opportunities/application/opportunity.evaluator.js +1 -1
- package/dist/opportunities/application/opportunity.graph.d.ts +686 -473
- package/dist/opportunities/application/opportunity.graph.discovery-strategies.d.ts +109 -0
- package/dist/opportunities/application/opportunity.graph.discovery-strategies.js +451 -0
- package/dist/opportunities/application/opportunity.graph.discovery.d.ts +69 -0
- package/dist/opportunities/application/opportunity.graph.discovery.js +397 -0
- package/dist/opportunities/application/opportunity.graph.evaluation.d.ts +82 -0
- package/dist/opportunities/application/opportunity.graph.evaluation.js +608 -0
- package/dist/opportunities/application/opportunity.graph.js +120 -3690
- package/dist/opportunities/application/opportunity.graph.modes.d.ts +538 -0
- package/dist/opportunities/application/opportunity.graph.modes.js +538 -0
- package/dist/opportunities/application/opportunity.graph.negotiate.d.ts +109 -0
- package/dist/opportunities/application/opportunity.graph.negotiate.js +392 -0
- package/dist/opportunities/application/opportunity.graph.persist-node.d.ts +93 -0
- package/dist/opportunities/application/opportunity.graph.persist-node.js +765 -0
- package/dist/opportunities/application/opportunity.graph.prep.d.ts +150 -0
- package/dist/opportunities/application/opportunity.graph.prep.js +381 -0
- package/dist/opportunities/application/opportunity.graph.shared.d.ts +173 -0
- package/dist/opportunities/application/opportunity.graph.shared.js +199 -0
- package/dist/opportunities/application/opportunity.presentation.d.ts +399 -0
- package/dist/opportunities/application/{opportunity.presenter.js → opportunity.presentation.js} +735 -9
- package/dist/opportunities/application/opportunity.tools.cards.d.ts +152 -0
- package/dist/opportunities/application/opportunity.tools.cards.js +235 -0
- package/dist/opportunities/application/opportunity.tools.d.ts +8 -114
- package/dist/opportunities/application/opportunity.tools.js +28 -711
- package/dist/opportunities/application/opportunity.tools.list.d.ts +10 -0
- package/dist/opportunities/application/opportunity.tools.list.js +493 -0
- package/dist/opportunities/domain/index.d.ts +3 -3
- package/dist/opportunities/domain/index.js +3 -3
- package/dist/opportunities/domain/opportunity.state.d.ts +19 -19
- package/dist/opportunities/index.d.ts +6 -6
- package/dist/opportunities/index.js +4 -4
- package/dist/opportunities/ports/opportunity.tools.port.d.ts +1 -1
- package/dist/opportunities/radar/radar.graph.d.ts +57 -13
- package/dist/opportunities/radar/radar.graph.js +470 -471
- package/dist/premises/premise.graph.d.ts +88 -20
- package/dist/premises/premise.graph.js +207 -218
- package/dist/questions/domain/question.schema.d.ts +40 -40
- package/dist/shared/agent/tool.factory.js +0 -8
- package/dist/shared/agent/tool.helpers.d.ts +21 -45
- package/dist/shared/agent/tool.helpers.js +1 -2
- package/dist/shared/agent/tool.registry.d.ts +4 -3
- package/dist/shared/agent/tool.registry.js +2 -4
- package/dist/shared/agent/tool.runtime.js +0 -2
- package/dist/shared/agent/utility.tools.js +9 -13
- package/dist/shared/interfaces/database.capabilities.d.ts +100 -0
- package/dist/shared/interfaces/database.capabilities.js +7 -0
- package/dist/shared/interfaces/database.entities.d.ts +533 -0
- package/dist/shared/interfaces/database.entities.js +10 -0
- package/dist/shared/interfaces/database.identity-queries.d.ts +294 -0
- package/dist/shared/interfaces/database.identity-queries.js +4 -0
- package/dist/shared/interfaces/database.interface.d.ts +15 -2277
- package/dist/shared/interfaces/database.interface.js +8 -0
- package/dist/shared/interfaces/database.member-queries.d.ts +221 -0
- package/dist/shared/interfaces/database.member-queries.js +4 -0
- package/dist/shared/interfaces/database.negotiation.d.ts +305 -0
- package/dist/shared/interfaces/database.negotiation.js +26 -0
- package/dist/shared/interfaces/database.network-queries.d.ts +277 -0
- package/dist/shared/interfaces/database.network-queries.js +4 -0
- package/dist/shared/interfaces/database.opportunity-queries.d.ts +285 -0
- package/dist/shared/interfaces/database.opportunity-queries.js +4 -0
- package/dist/shared/interfaces/database.port.d.ts +322 -0
- package/dist/shared/interfaces/database.port.js +29 -0
- package/dist/shared/schemas/discovery-question.schema.d.ts +14 -14
- package/dist/shared/schemas/negotiation-digest.schema.d.ts +2 -2
- package/package.json +2 -15
- package/dist/contacts/application/contact.inviter.d.ts +0 -49
- package/dist/contacts/application/contact.inviter.js +0 -64
- package/dist/integrations/application/index.d.ts +0 -14
- package/dist/integrations/application/index.js +0 -14
- package/dist/integrations/application/integration.tools.d.ts +0 -27
- package/dist/integrations/application/integration.tools.js +0 -98
- package/dist/integrations/domain/index.d.ts +0 -16
- package/dist/integrations/domain/index.js +0 -1
- package/dist/integrations/domain/integration.types.d.ts +0 -51
- package/dist/integrations/domain/integration.types.js +0 -9
- package/dist/integrations/index.d.ts +0 -10
- package/dist/integrations/index.js +0 -8
- package/dist/integrations/ports/index.d.ts +0 -25
- package/dist/integrations/ports/index.js +0 -23
- package/dist/integrations/ports/integration.adapter.port.d.ts +0 -62
- package/dist/integrations/ports/integration.adapter.port.js +0 -15
- package/dist/integrations/ports/integration.importer.port.d.ts +0 -31
- package/dist/integrations/ports/integration.importer.port.js +0 -15
- package/dist/integrations/ports/integration.tools.port.d.ts +0 -25
- package/dist/integrations/ports/integration.tools.port.js +0 -18
- package/dist/opportunities/application/opportunity.card-presentation.d.ts +0 -44
- package/dist/opportunities/application/opportunity.card-presentation.js +0 -93
- package/dist/opportunities/application/opportunity.presenter.d.ts +0 -156
- package/dist/opportunities/domain/opportunity.presentation-cache.d.ts +0 -5
- package/dist/opportunities/domain/opportunity.presentation-cache.js +0 -11
- package/dist/opportunities/domain/opportunity.presentation.d.ts +0 -76
- package/dist/opportunities/domain/opportunity.presentation.js +0 -516
- package/dist/opportunities/domain/opportunity.safe-presentation.d.ts +0 -104
- package/dist/opportunities/domain/opportunity.safe-presentation.js +0 -102
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Negotiation graph, stage 3: one negotiator turn.
|
|
3
|
+
*/
|
|
4
|
+
import { requestContext } from "../../shared/observability/request-context.js";
|
|
5
|
+
import { allowedActionsFor, askUserAnswerWindowMs, configuredAskUserEnabled, fallbackActionFor, rejectActionFor } from "../domain/negotiation.protocol.js";
|
|
6
|
+
import { assessConsultationEligibility, consultationPromptFor, negotiationConsultationPolicyMode } from "../domain/negotiation.consultation-policy.js";
|
|
7
|
+
import { blocksNegotiationBeforeFirstTurn } from "./negotiation.screen.js";
|
|
8
|
+
import { assessDeadlock, configuredDeadlockShiftEnabled, configuredDeadlockThreshold } from "../domain/negotiation.deadlock.js";
|
|
9
|
+
import { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, negotiationQuestionSettlementId } from '../domain/negotiation.question-safety.js';
|
|
10
|
+
import { isNegotiationTurnCapReached } from "../domain/negotiation.turn-cap.js";
|
|
11
|
+
import { buildAttributedDialogue, hasPriorAskUser, memoryQueryText, retrieveMemory, turnLog, turnsFromMessages } from "./negotiation.graph.shared.js";
|
|
12
|
+
export async function turnNode(state, deps) {
|
|
13
|
+
const traceEmitter = requestContext.getStore()?.traceEmitter;
|
|
14
|
+
// Local helper to emit events whose shape is wider than the declared
|
|
15
|
+
// `TraceEmitter` union. The chat agent already casts at its relay sink;
|
|
16
|
+
// here we localize the cast at the callsite so the rest of the body stays typed.
|
|
17
|
+
const emitWide = (event) => traceEmitter?.(event);
|
|
18
|
+
const agentName = "Index negotiator";
|
|
19
|
+
const agentStart = Date.now();
|
|
20
|
+
traceEmitter?.({ type: "agent_start", name: agentName });
|
|
21
|
+
try {
|
|
22
|
+
const history = turnsFromMessages(state.messages);
|
|
23
|
+
const isSource = state.currentSpeaker === "source";
|
|
24
|
+
const ownUser = isSource ? state.sourceUser : state.candidateUser;
|
|
25
|
+
const otherUser = isSource ? state.candidateUser : state.sourceUser;
|
|
26
|
+
const ownIntentId = isSource ? state.sourceIntentId : state.candidateIntentId;
|
|
27
|
+
// Determine if this is the system agent's final allowed turn.
|
|
28
|
+
const isFinalTurn = isNegotiationTurnCapReached(state.turnCount + 1, state.maxTurns);
|
|
29
|
+
// Seat attribution keys on initiatorUserId (rigid v2 stamp), never on
|
|
30
|
+
// parity or source/candidate position — under the conversation-scoped
|
|
31
|
+
// tie-break this run's source may hold the counterparty seat.
|
|
32
|
+
const version = state.protocolVersion ?? 'v1';
|
|
33
|
+
const seat = ownUser.id === (state.initiatorUserId ?? state.sourceUser.id)
|
|
34
|
+
? 'initiator'
|
|
35
|
+
: 'counterparty';
|
|
36
|
+
// Legacy ask_user availability (P3.2): flag on, full pause loop wired
|
|
37
|
+
// (questioner + answer-window timer + an opportunity to resume
|
|
38
|
+
// against), v2 non-final non-opening turn, and this side's one client
|
|
39
|
+
// consultation not yet spent (rationing). Shadow is observational and
|
|
40
|
+
// must preserve this legacy path byte-for-byte except for telemetry.
|
|
41
|
+
const policyMode = negotiationConsultationPolicyMode();
|
|
42
|
+
const askUserAvailable = version === 'v2'
|
|
43
|
+
&& !isFinalTurn
|
|
44
|
+
&& configuredAskUserEnabled()
|
|
45
|
+
&& !!deps.questionerEnqueue
|
|
46
|
+
&& !!deps.timeoutQueue?.enqueueAskUserExpiry
|
|
47
|
+
&& !!state.opportunityId
|
|
48
|
+
&& !!ownIntentId
|
|
49
|
+
&& !!state.indexContext.networkId
|
|
50
|
+
&& !(state.turnCount === 0 && !state.isContinuation)
|
|
51
|
+
&& !hasPriorAskUser(state.messages, ownUser.id);
|
|
52
|
+
// ─── Deadlock detection → persuasion→bargaining stance (IND-428) ──────
|
|
53
|
+
// Deterministic trailing-run inspection of the persisted history — no
|
|
54
|
+
// LLM in the decision. Gated on the strict default-off flag AND v2,
|
|
55
|
+
// checked alongside the protocol-version plumbing so v1 semantics stay
|
|
56
|
+
// untouched. Fail-open: any detection error means "no deadlock" and
|
|
57
|
+
// the legacy path proceeds byte-identically. The shift changes the
|
|
58
|
+
// system agent's drafting stance only — allowedActions, the dispatch
|
|
59
|
+
// payload, and all termination rules are untouched.
|
|
60
|
+
let deadlock = null;
|
|
61
|
+
if (version === 'v2' && configuredDeadlockShiftEnabled()) {
|
|
62
|
+
try {
|
|
63
|
+
deadlock = assessDeadlock(history, configuredDeadlockThreshold());
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
turnLog.warn('Deadlock detection failed; proceeding without mode shift', {
|
|
67
|
+
taskId: state.taskId,
|
|
68
|
+
error: err instanceof Error ? err.message : String(err),
|
|
69
|
+
});
|
|
70
|
+
deadlock = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const bargainingMode = deadlock?.deadlocked === true;
|
|
74
|
+
// P5.3: the speaker's own negotiator memory (cached per side across
|
|
75
|
+
// turns). Injected into both the dispatch payload (the user's own
|
|
76
|
+
// agent — scope-correct) and the system-agent prompt.
|
|
77
|
+
const ownSide = isSource ? "source" : "candidate";
|
|
78
|
+
const ownMemory = state.memoryBySide?.[ownSide]
|
|
79
|
+
?? (deps.memoryRetrieve
|
|
80
|
+
? await retrieveMemory(deps, ownUser.id, otherUser.id, memoryQueryText(state, otherUser), "turn")
|
|
81
|
+
: []);
|
|
82
|
+
const payload = {
|
|
83
|
+
negotiationId: state.taskId,
|
|
84
|
+
ownUser,
|
|
85
|
+
otherUser,
|
|
86
|
+
indexContext: state.indexContext,
|
|
87
|
+
seedAssessment: state.seedAssessment,
|
|
88
|
+
history,
|
|
89
|
+
isFinalTurn,
|
|
90
|
+
isDiscoverer: isSource,
|
|
91
|
+
seat,
|
|
92
|
+
protocolVersion: version,
|
|
93
|
+
allowedActions: [...allowedActionsFor(version, seat, isFinalTurn, { askUser: askUserAvailable })],
|
|
94
|
+
...(state.discoveryQuery && isSource && { discoveryQuery: state.discoveryQuery }),
|
|
95
|
+
...(ownMemory.length > 0 && { negotiatorMemory: ownMemory }),
|
|
96
|
+
...(state.privateConsultation?.recipientUserId === ownUser.id
|
|
97
|
+
? { privateConsultation: state.privateConsultation }
|
|
98
|
+
: {}),
|
|
99
|
+
...(state.continuationExecution
|
|
100
|
+
? {
|
|
101
|
+
timeoutContinuation: {
|
|
102
|
+
priorTaskId: state.continuationExecution.taskId,
|
|
103
|
+
settlementId: state.continuationExecution.settlementId,
|
|
104
|
+
successorTaskId: state.continuationExecution.successorTaskId,
|
|
105
|
+
token: state.continuationExecution.token,
|
|
106
|
+
fence: state.continuationExecution.fence,
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
: {}),
|
|
110
|
+
};
|
|
111
|
+
const scope = { action: 'manage:negotiations', scopeType: 'network', scopeId: state.indexContext.networkId };
|
|
112
|
+
const dispatchResult = await deps.dispatcher.dispatch(ownUser.id, scope, payload, { timeoutMs: state.timeoutMs });
|
|
113
|
+
let turn;
|
|
114
|
+
if (dispatchResult.handled) {
|
|
115
|
+
// Personal agent responded. Under v2, coerce out-of-seat actions to
|
|
116
|
+
// the conservative fallback — the polling/respond surfaces reject
|
|
117
|
+
// these with a 400, but locally-dispatched turns land here directly.
|
|
118
|
+
turn = dispatchResult.turn;
|
|
119
|
+
if (version === 'v2' && !allowedActionsFor(version, seat, isFinalTurn, { askUser: askUserAvailable }).includes(turn.action)) {
|
|
120
|
+
turnLog.warn('Personal agent returned out-of-seat action, coercing to conservative fallback', {
|
|
121
|
+
action: turn.action, seat, isFinalTurn,
|
|
122
|
+
});
|
|
123
|
+
turn = { ...turn, action: fallbackActionFor(version, seat, isFinalTurn) };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else if (dispatchResult.reason === 'waiting') {
|
|
127
|
+
// Long timeout — graph suspends. Persist the full turn context so the
|
|
128
|
+
// polling agent (and MCP consumers via get_negotiation) reconstruct
|
|
129
|
+
// the same view the in-process system agent would see. The view is
|
|
130
|
+
// stored in absolute source/candidate terms; perspective is projected
|
|
131
|
+
// at pickup time using the claiming user's id.
|
|
132
|
+
traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: "waiting_for_agent" });
|
|
133
|
+
await deps.database.setTaskTurnContext(state.taskId, {
|
|
134
|
+
sourceUser: state.sourceUser,
|
|
135
|
+
candidateUser: state.candidateUser,
|
|
136
|
+
indexContext: state.indexContext,
|
|
137
|
+
seedAssessment: state.seedAssessment,
|
|
138
|
+
// Keep discoveryQuery speaker-scoped: include it only when the
|
|
139
|
+
// parked turn belongs to the discoverer (source). Persisting it on
|
|
140
|
+
// candidate-side turns would make the pickup prompt frame the
|
|
141
|
+
// search as "your user searched for X" for the wrong user.
|
|
142
|
+
...(isSource && state.discoveryQuery && { discoveryQuery: state.discoveryQuery }),
|
|
143
|
+
...(state.privateConsultation && state.privateConsultation.recipientUserId === ownUser.id
|
|
144
|
+
? { privateConsultation: state.privateConsultation }
|
|
145
|
+
: {}),
|
|
146
|
+
}, state.continuationExecution);
|
|
147
|
+
await deps.database.updateTaskState(state.taskId, "waiting_for_agent", undefined, state.continuationExecution, dispatchResult.resumeToken);
|
|
148
|
+
return { status: 'waiting_for_agent' };
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// No personal agent or timeout — run system agent
|
|
152
|
+
const agentPriorDialogue = buildAttributedDialogue(state);
|
|
153
|
+
turn = await deps.systemAgent.invoke({
|
|
154
|
+
ownUser,
|
|
155
|
+
otherUser,
|
|
156
|
+
indexContext: state.indexContext,
|
|
157
|
+
seedAssessment: state.seedAssessment,
|
|
158
|
+
history,
|
|
159
|
+
isFinalTurn,
|
|
160
|
+
isDiscoverer: isSource,
|
|
161
|
+
seat,
|
|
162
|
+
protocolVersion: version,
|
|
163
|
+
...(state.discoveryQuery && isSource && { discoveryQuery: state.discoveryQuery }),
|
|
164
|
+
isContinuation: state.isContinuation,
|
|
165
|
+
...(agentPriorDialogue && { priorDialogue: agentPriorDialogue }),
|
|
166
|
+
...(state.userAnswers.length > 0 && { userAnswers: state.userAnswers }),
|
|
167
|
+
...(askUserAvailable && { canAskUser: true }),
|
|
168
|
+
...(bargainingMode && { bargaining: { consecutiveNonConvergent: deadlock.consecutiveNonConvergent } }),
|
|
169
|
+
...(ownMemory.length > 0 && { memory: ownMemory }),
|
|
170
|
+
...(state.privateConsultation?.recipientUserId === ownUser.id
|
|
171
|
+
? { privateConsultation: state.privateConsultation }
|
|
172
|
+
: {}),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: `${turn.action}` });
|
|
176
|
+
// IND-564 / IND-611: the opening-withdraw guard runs BEFORE the turn-0
|
|
177
|
+
// opening force below. Order matters and used to be inverted: the force
|
|
178
|
+
// rewrote a turn-0 `withdraw` into `outreach` first, which (a) made this
|
|
179
|
+
// guard dead code for a v2 initiator on turn 0 and (b) sent an outreach
|
|
180
|
+
// whose surviving `reasoning` argued against the match to the
|
|
181
|
+
// counterparty. An honest turn-0 refusal is now allowed to stand and
|
|
182
|
+
// flows into the existing quiet `screened_out` path — no message is
|
|
183
|
+
// persisted, so the original guard's intent (never retract a message
|
|
184
|
+
// that was never made) is preserved rather than weakened.
|
|
185
|
+
//
|
|
186
|
+
// Exact ask_user resumes are exempt: the successor is the SAME logical
|
|
187
|
+
// negotiation resumed after the client answered, so post-consultation
|
|
188
|
+
// withdraw is legitimate.
|
|
189
|
+
if (turn.action === 'withdraw' && !state.outreachOpened && !state.continuationExecution) {
|
|
190
|
+
turnLog.info('negotiation_opening_withdraw_screened_out', {
|
|
191
|
+
taskId: state.taskId,
|
|
192
|
+
opportunityId: state.opportunityId || undefined,
|
|
193
|
+
seat,
|
|
194
|
+
turnCount: state.turnCount,
|
|
195
|
+
isContinuation: state.isContinuation,
|
|
196
|
+
});
|
|
197
|
+
traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: "screened_out: opening withdraw" });
|
|
198
|
+
return { lastTurn: turn, firstTurnScreenedOut: true };
|
|
199
|
+
}
|
|
200
|
+
// First turn must open the negotiation (unless continuing a prior
|
|
201
|
+
// conversation): v1 → "propose"; v2 initiator → "outreach". A v2 turn-0
|
|
202
|
+
// speaker holding the counterparty seat (tie-break inheritance) is left
|
|
203
|
+
// unforced — it is responding, not opening.
|
|
204
|
+
//
|
|
205
|
+
// A legitimate turn-0 refusal never reaches here: the opening-withdraw
|
|
206
|
+
// guard above already returned. What remains are genuinely malformed
|
|
207
|
+
// openings (a turn-0 `counter`/`question`), which are still coerced.
|
|
208
|
+
if (state.turnCount === 0 && !state.isContinuation) {
|
|
209
|
+
const openingAction = version === 'v2' ? 'outreach' : 'propose';
|
|
210
|
+
if ((version !== 'v2' || seat === 'initiator') && turn.action !== openingAction) {
|
|
211
|
+
turnLog.warn(`Agent returned unexpected action on turn 0, forcing to ${openingAction}`, { action: turn.action });
|
|
212
|
+
turn.action = openingAction;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// IND-508 deterministic admission is evaluated only after the opening
|
|
216
|
+
// guard but before any turn is persisted. It consumes action/role enums
|
|
217
|
+
// only; free-form reasoning, messages, profiles, and evaluator inputs
|
|
218
|
+
// are intentionally unavailable to the policy.
|
|
219
|
+
let consultationPolicyReason;
|
|
220
|
+
const policyEligibility = policyMode === 'off' ? { eligible: false } : assessConsultationEligibility({
|
|
221
|
+
protocolVersion: version,
|
|
222
|
+
seat,
|
|
223
|
+
isOpeningTurn: state.turnCount === 0 && !state.isContinuation,
|
|
224
|
+
isFinalTurn,
|
|
225
|
+
screenedOut: blocksNegotiationBeforeFirstTurn(state.screenDecision, state.turnCount),
|
|
226
|
+
action: turn.action,
|
|
227
|
+
ownSuggestedRole: turn.assessment?.suggestedRoles?.ownUser,
|
|
228
|
+
priorActions: history.map((prior) => prior.action),
|
|
229
|
+
previouslyConsulted: hasPriorAskUser(state.messages, ownUser.id),
|
|
230
|
+
hasExactResumeCoordinate: Boolean(configuredAskUserEnabled()
|
|
231
|
+
&& deps.questionerEnqueue
|
|
232
|
+
&& deps.timeoutQueue?.enqueueAskUserExpiry
|
|
233
|
+
&& state.taskId
|
|
234
|
+
&& state.opportunityId
|
|
235
|
+
&& ownIntentId
|
|
236
|
+
&& state.indexContext.networkId),
|
|
237
|
+
lifecycleValid: Boolean(state.taskId && state.opportunityId && ownIntentId && state.indexContext.networkId),
|
|
238
|
+
});
|
|
239
|
+
const emitConsultationTelemetry = (stage, reason) => {
|
|
240
|
+
turnLog.info('negotiation_consultation_policy', { stage, mode: policyMode, reason });
|
|
241
|
+
emitWide({ type: 'negotiation_consultation_policy', stage, mode: policyMode, reason });
|
|
242
|
+
};
|
|
243
|
+
if (policyEligibility.eligible && policyEligibility.reason) {
|
|
244
|
+
emitConsultationTelemetry('eligible', policyEligibility.reason);
|
|
245
|
+
if (policyMode === 'on') {
|
|
246
|
+
consultationPolicyReason = policyEligibility.reason;
|
|
247
|
+
turn = {
|
|
248
|
+
...turn,
|
|
249
|
+
action: 'ask_user',
|
|
250
|
+
message: null,
|
|
251
|
+
assessment: {
|
|
252
|
+
reasoning: 'Client consultation required.',
|
|
253
|
+
suggestedRoles: turn.assessment.suggestedRoles,
|
|
254
|
+
},
|
|
255
|
+
askUser: { reason: consultationPolicyReason },
|
|
256
|
+
};
|
|
257
|
+
emitConsultationTelemetry('asked', consultationPolicyReason);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Safety net: off/shadow retain legacy behavior. In on, a spontaneous
|
|
261
|
+
// ask_user is admissible only when the deterministic policy just
|
|
262
|
+
// authorized it, so no unbounded pause can enter shared history.
|
|
263
|
+
if (turn.action === 'ask_user' && (!askUserAvailable || (policyMode === 'on' && !consultationPolicyReason))) {
|
|
264
|
+
turnLog.warn('ask_user emitted while unavailable, coercing to conservative fallback', {
|
|
265
|
+
seat, isFinalTurn, taskId: state.taskId,
|
|
266
|
+
});
|
|
267
|
+
turn = { ...turn, action: fallbackActionFor(version, seat, isFinalTurn) };
|
|
268
|
+
}
|
|
269
|
+
// ─── Deadlock shift record (IND-428) ───────────────────────────────
|
|
270
|
+
// Applied-stance analytics: recorded once per session, on the first
|
|
271
|
+
// turn actually drafted in the bargaining stance (the system agent —
|
|
272
|
+
// externally dispatched turns never receive the stance). Internal
|
|
273
|
+
// metadata only: persisted to tasks.metadata.deadlockShift via the
|
|
274
|
+
// optional hook; negotiation API surfaces project specific fields and
|
|
275
|
+
// never return task metadata verbatim. Every step fails open.
|
|
276
|
+
const bargainingApplied = bargainingMode && !dispatchResult.handled;
|
|
277
|
+
let deadlockShiftRecord = null;
|
|
278
|
+
if (bargainingApplied && !state.deadlockShift) {
|
|
279
|
+
deadlockShiftRecord = {
|
|
280
|
+
reason: 'consecutive_non_convergent',
|
|
281
|
+
consecutiveNonConvergent: deadlock.consecutiveNonConvergent,
|
|
282
|
+
threshold: deadlock.threshold,
|
|
283
|
+
shiftedAtTurn: state.turnCount,
|
|
284
|
+
seat,
|
|
285
|
+
detectedAt: new Date().toISOString(),
|
|
286
|
+
};
|
|
287
|
+
await deps.database.setTaskDeadlockShift?.(state.taskId, deadlockShiftRecord, state.continuationExecution).catch((err) => {
|
|
288
|
+
turnLog.error('Failed to persist deadlock shift record', { taskId: state.taskId, error: err });
|
|
289
|
+
});
|
|
290
|
+
turnLog.info('negotiation_deadlock_shift', {
|
|
291
|
+
taskId: state.taskId,
|
|
292
|
+
opportunityId: state.opportunityId || undefined,
|
|
293
|
+
seat,
|
|
294
|
+
consecutiveNonConvergent: deadlockShiftRecord.consecutiveNonConvergent,
|
|
295
|
+
threshold: deadlockShiftRecord.threshold,
|
|
296
|
+
turnIndex: state.turnCount,
|
|
297
|
+
});
|
|
298
|
+
if (state.opportunityId) {
|
|
299
|
+
emitWide({
|
|
300
|
+
type: 'negotiation_deadlock_shift',
|
|
301
|
+
opportunityId: state.opportunityId,
|
|
302
|
+
negotiationConversationId: state.conversationId,
|
|
303
|
+
turnIndex: state.turnCount,
|
|
304
|
+
actor: isSource ? 'source' : 'candidate',
|
|
305
|
+
consecutiveNonConvergent: deadlockShiftRecord.consecutiveNonConvergent,
|
|
306
|
+
threshold: deadlockShiftRecord.threshold,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const parts = [{ kind: "data", data: turn }];
|
|
311
|
+
const message = await deps.database.createMessage({
|
|
312
|
+
conversationId: state.conversationId,
|
|
313
|
+
senderId: `agent:${ownUser.id}`,
|
|
314
|
+
role: "agent",
|
|
315
|
+
parts,
|
|
316
|
+
taskId: state.taskId,
|
|
317
|
+
...(state.continuationExecution ? { continuationExecution: state.continuationExecution } : {}),
|
|
318
|
+
});
|
|
319
|
+
// ─── ask_user pause (P3.2) ────────────────────────────────────────────
|
|
320
|
+
// The negotiator consults its OWN client: persist the turn (done above),
|
|
321
|
+
// park the full turn context, arm the answer-window timer, enqueue the
|
|
322
|
+
// question through the negotiation_inflight preset, then suspend the
|
|
323
|
+
// task as input_required. The graph exits at this turn boundary exactly
|
|
324
|
+
// like the waiting_for_agent suspend; the answer (or window expiry)
|
|
325
|
+
// resumes via the run-existing continuation path.
|
|
326
|
+
if (turn.action === 'ask_user') {
|
|
327
|
+
const consultationReason = turn.askUser?.reason;
|
|
328
|
+
const safeAskUser = consultationReason
|
|
329
|
+
? consultationPromptFor(consultationReason)
|
|
330
|
+
: null;
|
|
331
|
+
const settlementId = negotiationQuestionSettlementId(state.taskId);
|
|
332
|
+
const askUserBinding = await deps.database.captureNegotiationAskUserBinding({
|
|
333
|
+
taskId: state.taskId,
|
|
334
|
+
settlementId,
|
|
335
|
+
recipientUserId: ownUser.id,
|
|
336
|
+
recipientIntentId: ownIntentId,
|
|
337
|
+
opportunityId: state.opportunityId,
|
|
338
|
+
networkId: state.indexContext.networkId,
|
|
339
|
+
turnContext: {
|
|
340
|
+
sourceUser: state.sourceUser,
|
|
341
|
+
candidateUser: state.candidateUser,
|
|
342
|
+
indexContext: state.indexContext,
|
|
343
|
+
seedAssessment: state.seedAssessment,
|
|
344
|
+
...(isSource && state.discoveryQuery && { discoveryQuery: state.discoveryQuery }),
|
|
345
|
+
...(consultationReason && { consultationPolicyReason: consultationReason }),
|
|
346
|
+
},
|
|
347
|
+
...(state.continuationExecution ? { continuationExecution: state.continuationExecution } : {}),
|
|
348
|
+
});
|
|
349
|
+
// Arm the timer BEFORE flipping state: it is the durable recovery
|
|
350
|
+
// trigger even when generation enqueues no job or persists no row.
|
|
351
|
+
const windowMs = askUserAnswerWindowMs();
|
|
352
|
+
await deps.timeoutQueue.enqueueAskUserExpiry(state.taskId, {
|
|
353
|
+
settlementId,
|
|
354
|
+
opportunityId: state.opportunityId,
|
|
355
|
+
userId: ownUser.id,
|
|
356
|
+
recipientIntentId: ownIntentId,
|
|
357
|
+
networkId: state.indexContext.networkId,
|
|
358
|
+
intentFingerprint: askUserBinding.intentFingerprint,
|
|
359
|
+
opportunityStatus: askUserBinding.opportunityStatus,
|
|
360
|
+
opportunityUpdatedAt: askUserBinding.opportunityUpdatedAt,
|
|
361
|
+
counterpartyUserId: askUserBinding.counterpartyUserId,
|
|
362
|
+
counterpartyIntentId: askUserBinding.counterpartyIntentId,
|
|
363
|
+
}, windowMs);
|
|
364
|
+
// Persistence admission requires the exact task to be input_required.
|
|
365
|
+
// Flip before enqueue; if the structured ask_user fields fail the
|
|
366
|
+
// deterministic privacy gate, the timer alone closes the exact task.
|
|
367
|
+
await deps.database.updateTaskState(state.taskId, 'input_required', undefined, state.continuationExecution);
|
|
368
|
+
if (safeAskUser) {
|
|
369
|
+
const userContext = (await deps.database.getUserContext(ownUser.id, null).catch(() => null))?.text ?? '';
|
|
370
|
+
await deps.questionerEnqueue({
|
|
371
|
+
mode: 'negotiation_inflight',
|
|
372
|
+
purpose: 'inflight_consultation',
|
|
373
|
+
userId: ownUser.id,
|
|
374
|
+
sourceType: 'opportunity',
|
|
375
|
+
sourceId: state.opportunityId,
|
|
376
|
+
negotiation: {
|
|
377
|
+
purpose: 'inflight_consultation',
|
|
378
|
+
recipientUserId: ownUser.id,
|
|
379
|
+
recipientIntentId: ownIntentId,
|
|
380
|
+
opportunityId: state.opportunityId,
|
|
381
|
+
taskId: state.taskId,
|
|
382
|
+
networkId: state.indexContext.networkId,
|
|
383
|
+
},
|
|
384
|
+
context: {
|
|
385
|
+
negotiationId: state.taskId,
|
|
386
|
+
counterpartyHint: NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY,
|
|
387
|
+
indexContext: NEGOTIATION_QUESTION_GENERIC_NETWORK,
|
|
388
|
+
consultationPolicyReason: consultationReason,
|
|
389
|
+
...(userContext && { userContext }),
|
|
390
|
+
},
|
|
391
|
+
}).catch((error) => {
|
|
392
|
+
turnLog.error('Failed to enqueue safe ask_user question; timeout recovery remains armed', {
|
|
393
|
+
taskId: state.taskId,
|
|
394
|
+
opportunityId: state.opportunityId,
|
|
395
|
+
error,
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
turnLog.warn('Skipping unsafe or incomplete ask_user question generation', {
|
|
401
|
+
taskId: state.taskId,
|
|
402
|
+
opportunityId: state.opportunityId,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
turnLog.info('negotiation_ask_user_pause', {
|
|
406
|
+
taskId: state.taskId,
|
|
407
|
+
opportunityId: state.opportunityId,
|
|
408
|
+
seat,
|
|
409
|
+
askingUserId: ownUser.id,
|
|
410
|
+
windowMs,
|
|
411
|
+
});
|
|
412
|
+
traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: "ask_user" });
|
|
413
|
+
emitWide({
|
|
414
|
+
type: 'negotiation_ask_user',
|
|
415
|
+
opportunityId: state.opportunityId,
|
|
416
|
+
negotiationConversationId: state.conversationId,
|
|
417
|
+
turnIndex: state.turnCount,
|
|
418
|
+
actor: isSource ? 'source' : 'candidate',
|
|
419
|
+
questionGenerationSafe: Boolean(safeAskUser),
|
|
420
|
+
windowMs,
|
|
421
|
+
});
|
|
422
|
+
return {
|
|
423
|
+
messages: [{
|
|
424
|
+
id: message.id,
|
|
425
|
+
senderId: message.senderId,
|
|
426
|
+
role: "agent",
|
|
427
|
+
parts: message.parts,
|
|
428
|
+
createdAt: message.createdAt,
|
|
429
|
+
}],
|
|
430
|
+
turnCount: state.turnCount + 1,
|
|
431
|
+
lastTurn: turn,
|
|
432
|
+
status: 'input_required',
|
|
433
|
+
...(deadlockShiftRecord && { deadlockShift: deadlockShiftRecord }),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
await deps.database.updateTaskState(state.taskId, "working", undefined, state.continuationExecution);
|
|
437
|
+
if (state.opportunityId) {
|
|
438
|
+
emitWide({
|
|
439
|
+
type: "negotiation_turn",
|
|
440
|
+
opportunityId: state.opportunityId,
|
|
441
|
+
negotiationConversationId: state.conversationId,
|
|
442
|
+
turnIndex: state.turnCount,
|
|
443
|
+
actor: isSource ? "source" : "candidate",
|
|
444
|
+
action: turn.action,
|
|
445
|
+
...(turn.assessment?.reasoning && { reasoning: turn.assessment.reasoning }),
|
|
446
|
+
...(turn.message && { message: turn.message }),
|
|
447
|
+
...(turn.assessment?.suggestedRoles && { suggestedRoles: turn.assessment.suggestedRoles }),
|
|
448
|
+
durationMs: Date.now() - agentStart,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
messages: [{
|
|
453
|
+
id: message.id,
|
|
454
|
+
senderId: message.senderId,
|
|
455
|
+
role: "agent",
|
|
456
|
+
parts: message.parts,
|
|
457
|
+
createdAt: message.createdAt,
|
|
458
|
+
}],
|
|
459
|
+
turnCount: state.turnCount + 1,
|
|
460
|
+
currentSpeaker: (isSource ? "candidate" : "source"),
|
|
461
|
+
lastTurn: turn,
|
|
462
|
+
memoryBySide: { [ownSide]: ownMemory },
|
|
463
|
+
// Record the in-task outreach so a later `withdraw` is legal (IND-564).
|
|
464
|
+
...(turn.action === 'outreach' && { outreachOpened: true }),
|
|
465
|
+
...(deadlockShiftRecord && { deadlockShift: deadlockShiftRecord }),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
470
|
+
turnLog.error("Agent invocation failed", { error: errMsg, stack: err instanceof Error ? err.stack : undefined, turnCount: state.turnCount });
|
|
471
|
+
traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: `error: ${errMsg}` });
|
|
472
|
+
const errorSeat = (state.currentSpeaker === 'source' ? state.sourceUser.id : state.candidateUser.id) === (state.initiatorUserId ?? state.sourceUser.id)
|
|
473
|
+
? 'initiator'
|
|
474
|
+
: 'counterparty';
|
|
475
|
+
return {
|
|
476
|
+
lastTurn: {
|
|
477
|
+
action: rejectActionFor(state.protocolVersion ?? 'v1', errorSeat),
|
|
478
|
+
assessment: { reasoning: `Agent error: ${errMsg}`, suggestedRoles: { ownUser: "peer", otherUser: "peer" } },
|
|
479
|
+
},
|
|
480
|
+
turnCount: state.turnCount + 1,
|
|
481
|
+
error: `Turn failed: ${errMsg}`,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
}
|
|
@@ -450,7 +450,7 @@ export declare const NegotiationGraphState: import("@langchain/langgraph").Annot
|
|
|
450
450
|
* negotiator's own client (answer or 24 h window expiry resumes it)
|
|
451
451
|
* - `completed` — negotiation finalized (accept/reject/turn-cap/timeout)
|
|
452
452
|
*/
|
|
453
|
-
status: import("@langchain/langgraph").BaseChannel<"waiting_for_agent" | "input_required" | "
|
|
453
|
+
status: import("@langchain/langgraph").BaseChannel<"waiting_for_agent" | "input_required" | "completed" | "active", "waiting_for_agent" | "input_required" | "completed" | "active" | import("@langchain/langgraph").OverwriteValue<"waiting_for_agent" | "input_required" | "completed" | "active">, unknown>;
|
|
454
454
|
/** Number of turns present in the conversation before this session started. */
|
|
455
455
|
priorTurnCount: import("@langchain/langgraph").BaseChannel<number, number | import("@langchain/langgraph").OverwriteValue<number>, unknown>;
|
|
456
456
|
/** User answers collected by the questioner between negotiation sessions. */
|
|
@@ -149,7 +149,6 @@ export const NegotiationGraphState = Annotation.Root({
|
|
|
149
149
|
* (attribution module imports NegotiationTurn from this module's shim);
|
|
150
150
|
* callers cast to `SeededAttribution` via the negotiation.attribution module.
|
|
151
151
|
*/
|
|
152
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
153
152
|
priorAttribution: Annotation({
|
|
154
153
|
reducer: (curr, next) => next ?? curr,
|
|
155
154
|
default: () => null,
|