@indexnetwork/protocol 4.5.0-rc.332.1 → 4.5.0-rc.334.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat/chat-streaming.types.d.ts +1 -1
- package/dist/chat/chat-streaming.types.d.ts.map +1 -1
- package/dist/chat/chat-streaming.types.js.map +1 -1
- package/dist/chat/chat.agent.d.ts +1 -1
- package/dist/chat/chat.agent.d.ts.map +1 -1
- package/dist/chat/chat.agent.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/negotiation/negotiation.agent.d.ts +21 -0
- package/dist/negotiation/negotiation.agent.d.ts.map +1 -1
- package/dist/negotiation/negotiation.agent.js +79 -11
- package/dist/negotiation/negotiation.agent.js.map +1 -1
- package/dist/negotiation/negotiation.graph.d.ts +56 -26
- package/dist/negotiation/negotiation.graph.d.ts.map +1 -1
- package/dist/negotiation/negotiation.graph.js +174 -19
- package/dist/negotiation/negotiation.graph.js.map +1 -1
- package/dist/negotiation/negotiation.protocol.d.ts +287 -0
- package/dist/negotiation/negotiation.protocol.d.ts.map +1 -0
- package/dist/negotiation/negotiation.protocol.js +152 -0
- package/dist/negotiation/negotiation.protocol.js.map +1 -0
- package/dist/negotiation/negotiation.screen.d.ts +128 -0
- package/dist/negotiation/negotiation.screen.d.ts.map +1 -0
- package/dist/negotiation/negotiation.screen.js +131 -0
- package/dist/negotiation/negotiation.screen.js.map +1 -0
- package/dist/negotiation/negotiation.state.d.ts +28 -9
- package/dist/negotiation/negotiation.state.d.ts.map +1 -1
- package/dist/negotiation/negotiation.state.js +28 -4
- package/dist/negotiation/negotiation.state.js.map +1 -1
- package/dist/negotiation/negotiation.tools.d.ts.map +1 -1
- package/dist/negotiation/negotiation.tools.js +86 -31
- package/dist/negotiation/negotiation.tools.js.map +1 -1
- package/dist/opportunity/question.prompt.d.ts +1 -1
- package/dist/opportunity/question.prompt.d.ts.map +1 -1
- package/dist/opportunity/question.prompt.js.map +1 -1
- package/dist/shared/agent/model.config.d.ts +5 -0
- package/dist/shared/agent/model.config.d.ts.map +1 -1
- package/dist/shared/agent/model.config.js +1 -0
- package/dist/shared/agent/model.config.js.map +1 -1
- package/dist/shared/interfaces/agent-dispatcher.interface.d.ts +6 -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 +9 -0
- 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/discovery-question.schema.d.ts +8 -8
- package/dist/shared/schemas/discovery-question.schema.js +1 -1
- package/dist/shared/schemas/discovery-question.schema.js.map +1 -1
- package/dist/shared/schemas/negotiation-state.schema.d.ts +19 -3
- package/dist/shared/schemas/negotiation-state.schema.d.ts.map +1 -1
- package/dist/shared/schemas/negotiation-state.schema.js +15 -1
- package/dist/shared/schemas/negotiation-state.schema.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,9 +3,12 @@ import { invokeWithAbortSignal } from "../shared/agent/model-signal.js";
|
|
|
3
3
|
import { requestContext } from "../shared/observability/request-context.js";
|
|
4
4
|
import { NegotiationGraphState } from "./negotiation.state.js";
|
|
5
5
|
import { IndexNegotiator } from "./negotiation.agent.js";
|
|
6
|
+
import { allowedActionsFor, configuredProtocolVersion, fallbackActionFor, isRejectLikeAction, isTerminalAction, readProtocolVersion, rejectActionFor } from "./negotiation.protocol.js";
|
|
7
|
+
import { NegotiationScreener, configuredScreenMode } from "./negotiation.screen.js";
|
|
6
8
|
import { protocolLogger } from "../shared/observability/protocol.logger.js";
|
|
7
9
|
const logger = protocolLogger("NegotiationGraph");
|
|
8
10
|
const initLog = protocolLogger("NegotiationGraph:Init");
|
|
11
|
+
const screenNodeLog = protocolLogger("NegotiationGraph:Screen");
|
|
9
12
|
const turnLog = protocolLogger("NegotiationGraph:Turn");
|
|
10
13
|
const finalizeLog = protocolLogger("NegotiationGraph:Finalize");
|
|
11
14
|
const negotiateCandidatesLog = protocolLogger("NegotiationGraph:negotiateCandidates");
|
|
@@ -32,6 +35,7 @@ export class NegotiationGraphFactory {
|
|
|
32
35
|
createGraph() {
|
|
33
36
|
const { database, dispatcher, timeoutQueue, questionerEnqueue } = this;
|
|
34
37
|
const systemAgent = new IndexNegotiator();
|
|
38
|
+
const screener = new NegotiationScreener();
|
|
35
39
|
const initNode = async (state) => {
|
|
36
40
|
try {
|
|
37
41
|
// Find-or-create the DM conversation for this agent pair (same as user DMs)
|
|
@@ -87,9 +91,13 @@ export class NegotiationGraphFactory {
|
|
|
87
91
|
return typeof v === 'string' && v.length > 0 ? v : null;
|
|
88
92
|
};
|
|
89
93
|
let initiatorUserId = readInitiator(priorTask?.metadata) ?? state.initiatorUserId ?? state.sourceUser.id;
|
|
94
|
+
// Conversation-scoped prior task: reused for both the initiator tie-break
|
|
95
|
+
// (only when active+fresh) and protocol-version inheritance (any prior
|
|
96
|
+
// task on the conversation pins the version).
|
|
97
|
+
const convTask = (!readInitiator(priorTask?.metadata) || !readProtocolVersion(priorTask?.metadata))
|
|
98
|
+
? await database.getLatestNegotiationTaskForConversation?.(conversation.id).catch(() => null)
|
|
99
|
+
: null;
|
|
90
100
|
if (!readInitiator(priorTask?.metadata)) {
|
|
91
|
-
const convTask = await database.getLatestNegotiationTaskForConversation?.(conversation.id)
|
|
92
|
-
.catch(() => null);
|
|
93
101
|
if (convTask && convTask.id !== priorTask?.id && isActiveAndFresh(convTask)) {
|
|
94
102
|
const convInitiator = readInitiator(convTask.metadata);
|
|
95
103
|
if (convInitiator) {
|
|
@@ -102,10 +110,29 @@ export class NegotiationGraphFactory {
|
|
|
102
110
|
}
|
|
103
111
|
}
|
|
104
112
|
}
|
|
113
|
+
// --- Protocol version: inherited, never re-stamped ---
|
|
114
|
+
// Every session (including continuations) creates a new task row, so a
|
|
115
|
+
// naïve "stamp from env at init" would flip a v1 conversation to v2
|
|
116
|
+
// mid-flight. Rule: any prior negotiation task on this conversation pins
|
|
117
|
+
// the version (absent field on a genuine prior = pre-v2 task = v1);
|
|
118
|
+
// prior turns without a readable task also grandfather to v1; only
|
|
119
|
+
// genuinely fresh negotiations stamp from NEGOTIATION_PROTOCOL_VERSION.
|
|
120
|
+
let protocolVersion;
|
|
121
|
+
const priorVersionSource = priorTask ?? convTask;
|
|
122
|
+
if (priorVersionSource) {
|
|
123
|
+
protocolVersion = readProtocolVersion(priorVersionSource.metadata) ?? 'v1';
|
|
124
|
+
}
|
|
125
|
+
else if (isContinuation) {
|
|
126
|
+
protocolVersion = 'v1';
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
protocolVersion = configuredProtocolVersion();
|
|
130
|
+
}
|
|
105
131
|
const task = await database.createTask(conversation.id, {
|
|
106
132
|
type: 'negotiation',
|
|
107
133
|
sourceUserId: state.sourceUser.id,
|
|
108
134
|
initiatorUserId,
|
|
135
|
+
protocolVersion,
|
|
109
136
|
candidateUserId: state.candidateUser.id,
|
|
110
137
|
networkId: state.indexContext.networkId,
|
|
111
138
|
...(state.opportunityId && { opportunityId: state.opportunityId }),
|
|
@@ -141,6 +168,7 @@ export class NegotiationGraphFactory {
|
|
|
141
168
|
maxTurns,
|
|
142
169
|
isContinuation,
|
|
143
170
|
initiatorUserId,
|
|
171
|
+
protocolVersion,
|
|
144
172
|
priorTurnCount: priorTurns.length,
|
|
145
173
|
...(userAnswers.length > 0 && { userAnswers }),
|
|
146
174
|
...(seedMessages.length > 0 && { messages: seedMessages }),
|
|
@@ -150,6 +178,99 @@ export class NegotiationGraphFactory {
|
|
|
150
178
|
return { error: `Init failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
151
179
|
}
|
|
152
180
|
};
|
|
181
|
+
/**
|
|
182
|
+
* Screen node (P2.1) — the outreach gate. Runs between init and the first
|
|
183
|
+
* turn on FRESH negotiations only (routing skips it on continuations and
|
|
184
|
+
* when NEGOTIATION_SCREEN_MODE=off). The reaching client's negotiator
|
|
185
|
+
* decides whether the match is worth its client's name; in shadow mode the
|
|
186
|
+
* decision is recorded (task metadata + trace event + log line) but never
|
|
187
|
+
* blocks — the negotiation always proceeds to the first turn. `enforce`
|
|
188
|
+
* runs identically until P2.2 lands enforcement (by the time this node
|
|
189
|
+
* runs, init has already created the task and flipped the opportunity to
|
|
190
|
+
* `negotiating` — P2.2 owns making a blocking `pass` quiet/correct).
|
|
191
|
+
*/
|
|
192
|
+
const screenNode = async (state) => {
|
|
193
|
+
const traceEmitter = requestContext.getStore()?.traceEmitter;
|
|
194
|
+
const emitWide = (event) => traceEmitter?.(event);
|
|
195
|
+
const mode = configuredScreenMode();
|
|
196
|
+
if (mode === "enforce") {
|
|
197
|
+
screenNodeLog.warn("NEGOTIATION_SCREEN_MODE=enforce requested but enforcement lands in P2.2; running shadow", {
|
|
198
|
+
taskId: state.taskId,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const start = Date.now();
|
|
202
|
+
// The client is the initiator seat's user — the side whose negotiator is
|
|
203
|
+
// reaching out. Fresh runs stamp initiatorUserId in init; fall back to
|
|
204
|
+
// sourceUser (what the stamp defaults to anyway).
|
|
205
|
+
const initiatorId = state.initiatorUserId ?? state.sourceUser.id;
|
|
206
|
+
const clientIsSource = initiatorId !== state.candidateUser.id;
|
|
207
|
+
const clientUser = clientIsSource ? state.sourceUser : state.candidateUser;
|
|
208
|
+
const counterpartyUser = clientIsSource ? state.candidateUser : state.sourceUser;
|
|
209
|
+
let decision;
|
|
210
|
+
let failedOpen = false;
|
|
211
|
+
let screenError;
|
|
212
|
+
try {
|
|
213
|
+
const counterpartyContext = (await database.getUserContext(counterpartyUser.id, null).catch(() => null))?.text ?? "";
|
|
214
|
+
decision = await screener.invoke({
|
|
215
|
+
clientUser,
|
|
216
|
+
counterpartyUser,
|
|
217
|
+
...(counterpartyContext && { counterpartyContext }),
|
|
218
|
+
// discoveryQuery belongs to the discovery session's source user; only
|
|
219
|
+
// meaningful for the client when the client holds the source side.
|
|
220
|
+
...(clientIsSource && state.discoveryQuery && { discoveryQuery: state.discoveryQuery }),
|
|
221
|
+
seedAssessment: state.seedAssessment,
|
|
222
|
+
indexContext: state.indexContext,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
// Fail open: a screen failure must never block a negotiation.
|
|
227
|
+
failedOpen = true;
|
|
228
|
+
screenError = err instanceof Error ? err.message : String(err);
|
|
229
|
+
screenNodeLog.warn("Screen failed; proceeding open (reach_out)", {
|
|
230
|
+
taskId: state.taskId,
|
|
231
|
+
opportunityId: state.opportunityId || undefined,
|
|
232
|
+
error: screenError,
|
|
233
|
+
});
|
|
234
|
+
decision = {
|
|
235
|
+
decision: "reach_out",
|
|
236
|
+
reasoning: `screen_error: ${screenError}`,
|
|
237
|
+
evidence: { counterpartyPremiseFit: "", intentAlignment: "" },
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const durationMs = Date.now() - start;
|
|
241
|
+
const record = {
|
|
242
|
+
...decision,
|
|
243
|
+
mode,
|
|
244
|
+
...(failedOpen && { failedOpen, error: screenError }),
|
|
245
|
+
screenedAt: new Date().toISOString(),
|
|
246
|
+
durationMs,
|
|
247
|
+
};
|
|
248
|
+
await database.setTaskScreenDecision?.(state.taskId, record).catch((err) => {
|
|
249
|
+
screenNodeLog.error("Failed to persist screen decision", { taskId: state.taskId, error: err });
|
|
250
|
+
});
|
|
251
|
+
screenNodeLog.info("negotiation_screen", {
|
|
252
|
+
taskId: state.taskId,
|
|
253
|
+
opportunityId: state.opportunityId || undefined,
|
|
254
|
+
decision: decision.decision,
|
|
255
|
+
mode,
|
|
256
|
+
failedOpen,
|
|
257
|
+
durationMs,
|
|
258
|
+
});
|
|
259
|
+
if (state.opportunityId) {
|
|
260
|
+
emitWide({
|
|
261
|
+
type: "negotiation_screen",
|
|
262
|
+
opportunityId: state.opportunityId,
|
|
263
|
+
negotiationConversationId: state.conversationId,
|
|
264
|
+
decision: decision.decision,
|
|
265
|
+
reasoning: decision.reasoning,
|
|
266
|
+
mode,
|
|
267
|
+
failedOpen,
|
|
268
|
+
durationMs,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
// Shadow (and pre-P2.2 enforce): always proceed to the first turn.
|
|
272
|
+
return { screenDecision: record };
|
|
273
|
+
};
|
|
153
274
|
const turnNode = async (state) => {
|
|
154
275
|
const traceEmitter = requestContext.getStore()?.traceEmitter;
|
|
155
276
|
// Local helper to emit events whose shape is wider than the declared
|
|
@@ -167,6 +288,13 @@ export class NegotiationGraphFactory {
|
|
|
167
288
|
// Determine if this is the system agent's final allowed turn
|
|
168
289
|
const maxTurns = state.maxTurns ?? 0;
|
|
169
290
|
const isFinalTurn = maxTurns > 0 && (state.turnCount + 1) >= maxTurns;
|
|
291
|
+
// Seat attribution keys on initiatorUserId (rigid v2 stamp), never on
|
|
292
|
+
// parity or source/candidate position — under the conversation-scoped
|
|
293
|
+
// tie-break this run's source may hold the counterparty seat.
|
|
294
|
+
const version = state.protocolVersion ?? 'v1';
|
|
295
|
+
const seat = ownUser.id === (state.initiatorUserId ?? state.sourceUser.id)
|
|
296
|
+
? 'initiator'
|
|
297
|
+
: 'counterparty';
|
|
170
298
|
const payload = {
|
|
171
299
|
negotiationId: state.taskId,
|
|
172
300
|
ownUser,
|
|
@@ -176,14 +304,25 @@ export class NegotiationGraphFactory {
|
|
|
176
304
|
history,
|
|
177
305
|
isFinalTurn,
|
|
178
306
|
isDiscoverer: isSource,
|
|
307
|
+
seat,
|
|
308
|
+
protocolVersion: version,
|
|
309
|
+
allowedActions: [...allowedActionsFor(version, seat, isFinalTurn)],
|
|
179
310
|
...(state.discoveryQuery && isSource && { discoveryQuery: state.discoveryQuery }),
|
|
180
311
|
};
|
|
181
312
|
const scope = { action: 'manage:negotiations', scopeType: 'network', scopeId: state.indexContext.networkId };
|
|
182
313
|
const dispatchResult = await dispatcher.dispatch(ownUser.id, scope, payload, { timeoutMs: state.timeoutMs });
|
|
183
314
|
let turn;
|
|
184
315
|
if (dispatchResult.handled) {
|
|
185
|
-
// Personal agent responded
|
|
316
|
+
// Personal agent responded. Under v2, coerce out-of-seat actions to
|
|
317
|
+
// the conservative fallback — the polling/respond surfaces reject
|
|
318
|
+
// these with a 400, but locally-dispatched turns land here directly.
|
|
186
319
|
turn = dispatchResult.turn;
|
|
320
|
+
if (version === 'v2' && !allowedActionsFor(version, seat, isFinalTurn).includes(turn.action)) {
|
|
321
|
+
turnLog.warn('Personal agent returned out-of-seat action, coercing to conservative fallback', {
|
|
322
|
+
action: turn.action, seat, isFinalTurn,
|
|
323
|
+
});
|
|
324
|
+
turn = { ...turn, action: fallbackActionFor(version, seat, isFinalTurn) };
|
|
325
|
+
}
|
|
187
326
|
}
|
|
188
327
|
else if (dispatchResult.reason === 'waiting') {
|
|
189
328
|
// Long timeout — graph suspends. Persist the full turn context so the
|
|
@@ -216,16 +355,24 @@ export class NegotiationGraphFactory {
|
|
|
216
355
|
history,
|
|
217
356
|
isFinalTurn,
|
|
218
357
|
isDiscoverer: isSource,
|
|
358
|
+
seat,
|
|
359
|
+
protocolVersion: version,
|
|
219
360
|
...(state.discoveryQuery && isSource && { discoveryQuery: state.discoveryQuery }),
|
|
220
361
|
isContinuation: state.isContinuation,
|
|
221
362
|
...(state.userAnswers.length > 0 && { userAnswers: state.userAnswers }),
|
|
222
363
|
});
|
|
223
364
|
}
|
|
224
365
|
traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: `${turn.action}` });
|
|
225
|
-
// First turn must
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
366
|
+
// First turn must open the negotiation (unless continuing a prior
|
|
367
|
+
// conversation): v1 → "propose"; v2 initiator → "outreach". A v2 turn-0
|
|
368
|
+
// speaker holding the counterparty seat (tie-break inheritance) is left
|
|
369
|
+
// unforced — it is responding, not opening.
|
|
370
|
+
if (state.turnCount === 0 && !state.isContinuation) {
|
|
371
|
+
const openingAction = version === 'v2' ? 'outreach' : 'propose';
|
|
372
|
+
if ((version !== 'v2' || seat === 'initiator') && turn.action !== openingAction) {
|
|
373
|
+
turnLog.warn(`Agent returned unexpected action on turn 0, forcing to ${openingAction}`, { action: turn.action });
|
|
374
|
+
turn.action = openingAction;
|
|
375
|
+
}
|
|
229
376
|
}
|
|
230
377
|
const parts = [{ kind: "data", data: turn }];
|
|
231
378
|
const message = await database.createMessage({
|
|
@@ -267,9 +414,12 @@ export class NegotiationGraphFactory {
|
|
|
267
414
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
268
415
|
turnLog.error("Agent invocation failed", { error: errMsg, stack: err instanceof Error ? err.stack : undefined, turnCount: state.turnCount });
|
|
269
416
|
traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: `error: ${errMsg}` });
|
|
417
|
+
const errorSeat = (state.currentSpeaker === 'source' ? state.sourceUser.id : state.candidateUser.id) === (state.initiatorUserId ?? state.sourceUser.id)
|
|
418
|
+
? 'initiator'
|
|
419
|
+
: 'counterparty';
|
|
270
420
|
return {
|
|
271
421
|
lastTurn: {
|
|
272
|
-
action:
|
|
422
|
+
action: rejectActionFor(state.protocolVersion ?? 'v1', errorSeat),
|
|
273
423
|
assessment: { reasoning: `Agent error: ${errMsg}`, suggestedRoles: { ownUser: "peer", otherUser: "peer" } },
|
|
274
424
|
},
|
|
275
425
|
turnCount: state.turnCount + 1,
|
|
@@ -284,9 +434,8 @@ export class NegotiationGraphFactory {
|
|
|
284
434
|
return "finalize";
|
|
285
435
|
if (!state.lastTurn)
|
|
286
436
|
return "finalize";
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (state.lastTurn.action === "reject")
|
|
437
|
+
// Terminal actions: accept (v1+v2), reject (v1), withdraw/decline (v2)
|
|
438
|
+
if (isTerminalAction(state.lastTurn.action))
|
|
290
439
|
return "finalize";
|
|
291
440
|
// question routes same as counter — next turn
|
|
292
441
|
if ((state.maxTurns ?? 0) > 0 && state.turnCount >= state.maxTurns)
|
|
@@ -311,7 +460,7 @@ export class NegotiationGraphFactory {
|
|
|
311
460
|
const history = turnsFromMessages(state.messages);
|
|
312
461
|
const lastTurn = state.lastTurn;
|
|
313
462
|
const hasOpportunity = lastTurn?.action === "accept";
|
|
314
|
-
const atCap = (state.maxTurns ?? 0) > 0 && state.turnCount >= state.maxTurns && lastTurn?.action
|
|
463
|
+
const atCap = (state.maxTurns ?? 0) > 0 && state.turnCount >= state.maxTurns && !isTerminalAction(lastTurn?.action);
|
|
315
464
|
let agreedRoles = [];
|
|
316
465
|
if (hasOpportunity && history.length >= 2) {
|
|
317
466
|
const acceptTurn = history[history.length - 1];
|
|
@@ -352,7 +501,7 @@ export class NegotiationGraphFactory {
|
|
|
352
501
|
if (state.opportunityId) {
|
|
353
502
|
const nextStatus = lastTurn?.action === 'accept'
|
|
354
503
|
? 'pending'
|
|
355
|
-
: lastTurn?.action
|
|
504
|
+
: isRejectLikeAction(lastTurn?.action)
|
|
356
505
|
? 'rejected'
|
|
357
506
|
: 'stalled';
|
|
358
507
|
await database.updateOpportunityStatus(state.opportunityId, nextStatus).catch((err) => {
|
|
@@ -370,9 +519,7 @@ export class NegotiationGraphFactory {
|
|
|
370
519
|
? "turn_cap"
|
|
371
520
|
: state.error && /timeout/i.test(state.error)
|
|
372
521
|
? "timed_out"
|
|
373
|
-
:
|
|
374
|
-
? "rejected_stalled"
|
|
375
|
-
: "rejected_stalled";
|
|
522
|
+
: "rejected_stalled";
|
|
376
523
|
emitWide({
|
|
377
524
|
type: "negotiation_outcome",
|
|
378
525
|
opportunityId: state.opportunityId,
|
|
@@ -392,7 +539,7 @@ export class NegotiationGraphFactory {
|
|
|
392
539
|
}
|
|
393
540
|
// Enqueue question generation for stalled/capped negotiations (not accepted or explicitly rejected).
|
|
394
541
|
// Require turnCount > 0 so early init/turn errors don't enqueue with empty context.
|
|
395
|
-
if (!hasOpportunity && lastTurn?.action
|
|
542
|
+
if (!hasOpportunity && !isRejectLikeAction(lastTurn?.action) && state.turnCount > 0 && state.opportunityId && questionerEnqueue) {
|
|
396
543
|
const stallReason = atCap
|
|
397
544
|
? 'turn_cap'
|
|
398
545
|
: (state.error && /timeout/i.test(state.error))
|
|
@@ -421,6 +568,7 @@ export class NegotiationGraphFactory {
|
|
|
421
568
|
};
|
|
422
569
|
const workflow = new StateGraph(NegotiationGraphState)
|
|
423
570
|
.addNode("init", initNode)
|
|
571
|
+
.addNode("screen", screenNode)
|
|
424
572
|
.addNode("turn", turnNode)
|
|
425
573
|
.addNode("finalize", finalizeNode)
|
|
426
574
|
.addConditionalEdges("turn", evaluateNode, {
|
|
@@ -428,8 +576,15 @@ export class NegotiationGraphFactory {
|
|
|
428
576
|
finalize: "finalize",
|
|
429
577
|
})
|
|
430
578
|
.addConditionalEdges("init", (state) => {
|
|
431
|
-
|
|
432
|
-
|
|
579
|
+
if (state.error)
|
|
580
|
+
return "finalize";
|
|
581
|
+
// Screen gate: fresh negotiations only (continuations already passed
|
|
582
|
+
// the gate when the dialogue opened); off disables the node entirely.
|
|
583
|
+
if (!state.isContinuation && configuredScreenMode() !== "off")
|
|
584
|
+
return "screen";
|
|
585
|
+
return "turn";
|
|
586
|
+
}, { screen: "screen", turn: "turn", finalize: "finalize" })
|
|
587
|
+
.addEdge("screen", "turn")
|
|
433
588
|
.addEdge("__start__", "init")
|
|
434
589
|
.addEdge("finalize", "__end__");
|
|
435
590
|
return workflow.compile();
|