@indexnetwork/protocol 4.5.0-rc.331.1 → 4.5.0-rc.333.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.
Files changed (49) hide show
  1. package/dist/chat/chat-streaming.types.d.ts +3 -1
  2. package/dist/chat/chat-streaming.types.d.ts.map +1 -1
  3. package/dist/chat/chat-streaming.types.js.map +1 -1
  4. package/dist/chat/chat.agent.d.ts +1 -1
  5. package/dist/chat/chat.agent.d.ts.map +1 -1
  6. package/dist/chat/chat.agent.js.map +1 -1
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +3 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/negotiation/negotiation.agent.d.ts +21 -0
  12. package/dist/negotiation/negotiation.agent.d.ts.map +1 -1
  13. package/dist/negotiation/negotiation.agent.js +79 -11
  14. package/dist/negotiation/negotiation.agent.js.map +1 -1
  15. package/dist/negotiation/negotiation.graph.d.ts +55 -24
  16. package/dist/negotiation/negotiation.graph.d.ts.map +1 -1
  17. package/dist/negotiation/negotiation.graph.js +106 -27
  18. package/dist/negotiation/negotiation.graph.js.map +1 -1
  19. package/dist/negotiation/negotiation.protocol.d.ts +287 -0
  20. package/dist/negotiation/negotiation.protocol.d.ts.map +1 -0
  21. package/dist/negotiation/negotiation.protocol.js +152 -0
  22. package/dist/negotiation/negotiation.protocol.js.map +1 -0
  23. package/dist/negotiation/negotiation.state.d.ts +34 -9
  24. package/dist/negotiation/negotiation.state.d.ts.map +1 -1
  25. package/dist/negotiation/negotiation.state.js +28 -4
  26. package/dist/negotiation/negotiation.state.js.map +1 -1
  27. package/dist/negotiation/negotiation.tools.d.ts.map +1 -1
  28. package/dist/negotiation/negotiation.tools.js +86 -31
  29. package/dist/negotiation/negotiation.tools.js.map +1 -1
  30. package/dist/opportunity/opportunity.graph.d.ts.map +1 -1
  31. package/dist/opportunity/opportunity.graph.js +9 -0
  32. package/dist/opportunity/opportunity.graph.js.map +1 -1
  33. package/dist/opportunity/question.prompt.d.ts +1 -1
  34. package/dist/opportunity/question.prompt.d.ts.map +1 -1
  35. package/dist/opportunity/question.prompt.js.map +1 -1
  36. package/dist/shared/interfaces/agent-dispatcher.interface.d.ts +6 -0
  37. package/dist/shared/interfaces/agent-dispatcher.interface.d.ts.map +1 -1
  38. package/dist/shared/interfaces/agent-dispatcher.interface.js.map +1 -1
  39. package/dist/shared/interfaces/database.interface.d.ts +17 -0
  40. package/dist/shared/interfaces/database.interface.d.ts.map +1 -1
  41. package/dist/shared/interfaces/database.interface.js.map +1 -1
  42. package/dist/shared/schemas/discovery-question.schema.d.ts +8 -8
  43. package/dist/shared/schemas/discovery-question.schema.js +1 -1
  44. package/dist/shared/schemas/discovery-question.schema.js.map +1 -1
  45. package/dist/shared/schemas/negotiation-state.schema.d.ts +19 -3
  46. package/dist/shared/schemas/negotiation-state.schema.d.ts.map +1 -1
  47. package/dist/shared/schemas/negotiation-state.schema.js +15 -1
  48. package/dist/shared/schemas/negotiation-state.schema.js.map +1 -1
  49. package/package.json +1 -1
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { success, error } from '../shared/agent/tool.helpers.js';
3
3
  import { IndexNegotiator } from './negotiation.agent.js';
4
+ import { allowedActionsFor, isTerminalAction, readProtocolVersion, rejectActionFor, resolveSeat, seatViolationMessage } from './negotiation.protocol.js';
5
+ import { NEGOTIATION_ACTIONS } from '../shared/schemas/negotiation-state.schema.js';
4
6
  import { protocolLogger } from '../shared/observability/protocol.logger.js';
5
7
  import { focusedNetworkId } from '../shared/agent/tool.scope.js';
6
8
  const logger = protocolLogger('ChatTools:Negotiation');
@@ -107,9 +109,14 @@ export function createNegotiationTools(defineTool, deps) {
107
109
  const lastTurnData = lastMessage
108
110
  ? lastMessage.parts?.find(p => p.kind === 'data')?.data
109
111
  : undefined;
110
- // Determine whose turn it is based on message count (alternating source/candidate)
112
+ // Determine whose turn it is from the last message's sender not
113
+ // parity, which misattributes across continuation sessions. Rows
114
+ // without senderId (legacy) fall back to parity.
111
115
  const turnCount = messages.length;
112
- const currentSpeaker = turnCount % 2 === 0 ? 'source' : 'candidate';
116
+ const lastSenderId = turnCount > 0 ? messages[turnCount - 1].senderId : null;
117
+ const currentSpeaker = lastSenderId
118
+ ? (lastSenderId === `agent:${meta.sourceUserId}` ? 'candidate' : 'source')
119
+ : (turnCount % 2 === 0 ? 'source' : 'candidate');
113
120
  // Map task state to tool status
114
121
  const status = task.state === 'working' ? 'active'
115
122
  : task.state === 'waiting_for_agent' ? 'waiting_for_agent'
@@ -138,7 +145,9 @@ export function createNegotiationTools(defineTool, deps) {
138
145
  const recentMessages = messages.slice(-RECENT_TURNS_LIMIT);
139
146
  const recentTurns = recentMessages.map((m, sliceIdx) => {
140
147
  const absoluteIdx = messages.length - recentMessages.length + sliceIdx;
141
- const speaker = absoluteIdx % 2 === 0 ? 'source' : 'candidate';
148
+ const speaker = m.senderId
149
+ ? (m.senderId === `agent:${meta.sourceUserId}` ? 'source' : 'candidate')
150
+ : (absoluteIdx % 2 === 0 ? 'source' : 'candidate');
142
151
  const td = m.parts?.find(p => p.kind === 'data')?.data;
143
152
  return {
144
153
  turnNumber: absoluteIdx + 1,
@@ -256,12 +265,15 @@ export function createNegotiationTools(defineTool, deps) {
256
265
  negotiationDatabase.getMessagesForConversation(task.conversationId),
257
266
  negotiationDatabase.getArtifactsForTask(task.id),
258
267
  ]);
259
- // Parse turns from messages
268
+ // Parse turns from messages (speaker from senderId, not parity —
269
+ // continuations can start with either side speaking first)
260
270
  const turns = messages.map((m, idx) => {
261
271
  const dataPart = m.parts?.find(p => p.kind === 'data');
262
272
  const turnData = dataPart?.data;
263
273
  const turnNumber = idx + 1;
264
- const speaker = turnNumber % 2 === 1 ? 'source' : 'candidate';
274
+ const speaker = m.senderId
275
+ ? (m.senderId === `agent:${meta.sourceUserId}` ? 'source' : 'candidate')
276
+ : (turnNumber % 2 === 1 ? 'source' : 'candidate');
265
277
  return {
266
278
  turnNumber,
267
279
  speaker,
@@ -278,9 +290,13 @@ export function createNegotiationTools(defineTool, deps) {
278
290
  const outcome = outcomeArtifact
279
291
  ? outcomeArtifact.parts?.find(p => p.kind === 'data')?.data
280
292
  : null;
281
- // Determine whose turn it is
293
+ // Determine whose turn it is (last sender's counterpart, not parity;
294
+ // rows without senderId fall back to parity)
282
295
  const turnCount = messages.length;
283
- const currentSpeaker = turnCount % 2 === 0 ? 'source' : 'candidate';
296
+ const lastSenderId = turnCount > 0 ? messages[turnCount - 1].senderId : null;
297
+ const currentSpeaker = lastSenderId
298
+ ? (lastSenderId === `agent:${meta.sourceUserId}` ? 'candidate' : 'source')
299
+ : (turnCount % 2 === 0 ? 'source' : 'candidate');
284
300
  const status = task.state === 'working' ? 'active'
285
301
  : task.state === 'waiting_for_agent' ? 'waiting_for_agent'
286
302
  : task.state === 'completed' ? 'completed'
@@ -289,11 +305,18 @@ export function createNegotiationTools(defineTool, deps) {
289
305
  ((isSource && currentSpeaker === 'source') || (!isSource && currentSpeaker === 'candidate'));
290
306
  const isContinuation = meta.isContinuation ?? false;
291
307
  const priorTurnCount = meta.priorTurnCount ?? 0;
308
+ // Seat + protocol version (v2 client-advocate): announce the caller's
309
+ // seat and the actions it may submit so agents don't guess.
310
+ const protocolVersion = readProtocolVersion(meta) ?? 'v1';
311
+ const seat = resolveSeat(context.userId, meta);
292
312
  return success({
293
313
  id: task.id,
294
314
  conversationId: task.conversationId,
295
315
  status,
296
316
  role: isSource ? 'source' : 'candidate',
317
+ seat,
318
+ protocolVersion,
319
+ allowedActions: allowedActionsFor(protocolVersion, seat),
297
320
  counterpartyId: counterpartyId ?? 'unknown',
298
321
  turnCount,
299
322
  isUsersTurn,
@@ -319,22 +342,25 @@ export function createNegotiationTools(defineTool, deps) {
319
342
  'by accepting, rejecting, countering, or asking a clarifying question.\n\n' +
320
343
  '**Turn-based model:** Negotiations alternate between source and candidate agents. When the graph yields with ' +
321
344
  '`waiting_for_agent` status, the user whose turn it is can respond.\n\n' +
322
- '**Valid actions:**\n' +
323
- '- `accept` Accept the current proposal. The negotiation will be finalized as an opportunity.\n' +
324
- '- `reject` — Reject the current proposal. The negotiation will end without creating an opportunity.\n' +
325
- '- `counter` — Counter the proposal with a message (message is required). The negotiation will continue.\n' +
326
- '- `question` Ask the counterparty a clarifying question (message is required). The negotiation will continue.\n\n' +
327
- '**What happens after:** Accept/reject finalizes the negotiation immediately. Counter/question continues the negotiation ' +
328
- 'if the counterparty has an agent, the negotiation yields again; otherwise the AI agent responds inline.\n\n' +
345
+ '**Valid actions depend on the negotiation protocol version and your seat** — call get_negotiation first: ' +
346
+ 'its `seat`, `protocolVersion`, and `allowedActions` fields tell you exactly what you may submit.\n\n' +
347
+ '**v1 negotiations (legacy):** `propose | accept | reject | counter | question` — on the first turn the action MUST be `propose`.\n\n' +
348
+ '**v2 negotiations (client-advocate seat rules):**\n' +
349
+ '- Initiator seat (`outreach | counter | question | withdraw`): you reached out you can NEVER accept. ' +
350
+ '`outreach` opens the negotiation; `withdraw` ends it without an opportunity.\n' +
351
+ '- Counterparty seat (`accept | decline | counter | question`): only your seat can `accept` (finalizes an opportunity); ' +
352
+ '`decline` ends the negotiation without one.\n\n' +
353
+ '- `counter` — Counter with a message (message is required). The negotiation continues.\n' +
354
+ '- `question` — Ask the other side a clarifying question (message is required). The negotiation continues.\n\n' +
355
+ '**What happens after:** Terminal actions (accept/reject/withdraw/decline) finalize the negotiation immediately. ' +
356
+ 'Counter/question continues — if the counterparty has an agent, the negotiation yields again; otherwise the AI agent responds inline.\n\n' +
329
357
  '**Silent-subagent response contract.** In negotiation-turn mode, submit exactly ONE call to this tool ' +
330
- 'per dispatch with the action (propose | counter | accept | reject | question) and the assessment ' +
331
- '(reasoning + suggestedRoles). If the decision is ambiguous, pick the most conservative action — usually ' +
332
- '`counter` with specific objections, or `reject` with clear reasoning. On the first turn of a negotiation ' +
333
- '(turnCount === 0) the action MUST be `propose`. Do not ask the user clarifying questions; you are ' +
334
- 'authorized to act on their behalf within the scope granted to your agent.',
358
+ 'per dispatch with an action from your seat\'s allowed set and the assessment (reasoning + suggestedRoles). ' +
359
+ 'If the decision is ambiguous, pick the most conservative action — usually `counter` with specific objections. ' +
360
+ 'Do not ask the user clarifying questions; you are authorized to act on their behalf within the scope granted to your agent.',
335
361
  querySchema: z.object({
336
362
  negotiationId: z.string().describe('The negotiation task ID to respond to.'),
337
- action: z.enum(['propose', 'accept', 'reject', 'counter', 'question']).describe('The response action. On the first turn (turnCount === 0) this MUST be "propose".'),
363
+ action: z.enum(NEGOTIATION_ACTIONS).describe('The response action. Must be within your seat\'s allowedActions (see get_negotiation). v1 first turn MUST be "propose"; v2 initiator first turn MUST be "outreach".'),
338
364
  reasoning: z.string().describe('Why you are taking this action — your assessment of the opportunity.'),
339
365
  suggestedRoles: z.object({
340
366
  ownUser: z.enum(['agent', 'patient', 'peer']).describe('Suggested role for your user in this opportunity.'),
@@ -371,14 +397,29 @@ export function createNegotiationTools(defineTool, deps) {
371
397
  if (!isSource && !isCandidate) {
372
398
  return error('Access denied: you are not a party to this negotiation.');
373
399
  }
374
- // Determine whose turn it is
400
+ // Seat + version validation (v2 client-advocate): the submitted action
401
+ // must be within the caller's seat vocabulary. v1 tasks accept the
402
+ // legacy vocabulary unchanged (grandfathered).
403
+ const protocolVersion = readProtocolVersion(meta) ?? 'v1';
404
+ const seat = resolveSeat(context.userId, meta);
405
+ if (!allowedActionsFor(protocolVersion, seat).includes(query.action)) {
406
+ return error(seatViolationMessage(query.action, seat, protocolVersion));
407
+ }
408
+ // Determine whose turn it is from the last message's sender — not
409
+ // parity, which misattributes across continuation sessions. Rows
410
+ // without senderId (legacy) fall back to the parity heuristic.
375
411
  const messages = await negotiationDatabase.getMessagesForConversation(task.conversationId);
376
412
  const turnCount = messages.length;
377
- const currentSpeaker = turnCount % 2 === 0 ? 'source' : 'candidate';
378
- const isUsersTurn = (isSource && currentSpeaker === 'source') || (!isSource && currentSpeaker === 'candidate');
413
+ const lastSenderId = turnCount > 0 ? messages[turnCount - 1].senderId : null;
414
+ const paritySpeaker = turnCount % 2 === 0 ? 'source' : 'candidate';
415
+ const isUsersTurn = lastSenderId
416
+ ? lastSenderId !== `agent:${context.userId}`
417
+ : ((isSource && paritySpeaker === 'source') || (!isSource && paritySpeaker === 'candidate'));
379
418
  if (!isUsersTurn) {
380
419
  return error('It is not your turn to respond in this negotiation.');
381
420
  }
421
+ // The caller is the current speaker (verified above).
422
+ const currentSpeaker = isSource ? 'source' : 'candidate';
382
423
  // Validate counter/question has a message
383
424
  if ((query.action === 'counter' || query.action === 'question') && !query.message?.trim()) {
384
425
  return error(`A message is required when using "${query.action}". Explain what you want to change or clarify.`);
@@ -405,8 +446,8 @@ export function createNegotiationTools(defineTool, deps) {
405
446
  taskId: task.id,
406
447
  });
407
448
  const newTurnCount = turnCount + 1;
408
- // ── Handle accept/reject: finalize immediately ──
409
- if (query.action === 'accept' || query.action === 'reject') {
449
+ // ── Handle terminal actions (accept / reject / withdraw / decline): finalize immediately ──
450
+ if (isTerminalAction(query.action)) {
410
451
  const allMessages = [...messages, { id: turnMessage.id, senderId: turnMessage.senderId, role: turnMessage.role, parts: turnMessage.parts, createdAt: turnMessage.createdAt }];
411
452
  const history = turnsFromMessages(allMessages);
412
453
  const nextSpeaker = currentSpeaker === 'source' ? 'candidate' : 'source';
@@ -421,7 +462,11 @@ export function createNegotiationTools(defineTool, deps) {
421
462
  return success({
422
463
  message: query.action === 'accept'
423
464
  ? 'Negotiation accepted. An opportunity has been created.'
424
- : 'Negotiation rejected.',
465
+ : query.action === 'withdraw'
466
+ ? 'Negotiation withdrawn.'
467
+ : query.action === 'decline'
468
+ ? 'Negotiation declined.'
469
+ : 'Negotiation rejected.',
425
470
  negotiationId: task.id,
426
471
  action: query.action,
427
472
  turnNumber: newTurnCount,
@@ -454,6 +499,7 @@ export function createNegotiationTools(defineTool, deps) {
454
499
  // ── Counter/question under max turns: dispatch to counterparty's agent ──
455
500
  const counterpartyUserId = isSource ? meta.candidateUserId : meta.sourceUserId;
456
501
  const counterpartySpeaker = isSource ? 'candidate' : 'source';
502
+ const counterpartySeat = resolveSeat(counterpartyUserId, meta);
457
503
  // Build the current turn history for dispatcher payload
458
504
  const allMessagesWithTurn = [...messages, { id: turnMessage.id, senderId: turnMessage.senderId, role: turnMessage.role, parts: turnMessage.parts, createdAt: turnMessage.createdAt }];
459
505
  const historyForDispatch = turnsFromMessages(allMessagesWithTurn);
@@ -470,6 +516,9 @@ export function createNegotiationTools(defineTool, deps) {
470
516
  history: historyForDispatch,
471
517
  isFinalTurn,
472
518
  isDiscoverer: false,
519
+ seat: counterpartySeat,
520
+ protocolVersion,
521
+ allowedActions: [...allowedActionsFor(protocolVersion, counterpartySeat, isFinalTurn)],
473
522
  };
474
523
  const scope = { action: 'negotiation.respond', scopeType: 'negotiation', scopeId: task.id };
475
524
  const timeoutMs = AMBIENT_PARK_WINDOW_MS;
@@ -481,7 +530,7 @@ export function createNegotiationTools(defineTool, deps) {
481
530
  await deps.negotiationTimeoutQueue.enqueueTimeout(task.id, newTurnCount, timeoutMs);
482
531
  }
483
532
  return success({
484
- message: `${query.action === 'question' ? 'Question' : query.action === 'propose' ? 'Proposal' : 'Counter-proposal'} submitted. Waiting for counterparty response.`,
533
+ message: `${query.action === 'question' ? 'Question' : query.action === 'propose' ? 'Proposal' : query.action === 'outreach' ? 'Outreach' : 'Counter-proposal'} submitted. Waiting for counterparty response.`,
485
534
  negotiationId: task.id,
486
535
  action: query.action,
487
536
  turnNumber: newTurnCount,
@@ -510,6 +559,8 @@ export function createNegotiationTools(defineTool, deps) {
510
559
  seedAssessment,
511
560
  history: historyForDispatch,
512
561
  isFinalTurn,
562
+ seat: counterpartySeat,
563
+ protocolVersion,
513
564
  });
514
565
  }
515
566
  catch (err) {
@@ -528,7 +579,7 @@ export function createNegotiationTools(defineTool, deps) {
528
579
  error: errMsg,
529
580
  });
530
581
  aiTurn = {
531
- action: 'reject',
582
+ action: rejectActionFor(protocolVersion, counterpartySeat),
532
583
  assessment: {
533
584
  reasoning: isTimeout
534
585
  ? 'Negotiator response timed out.'
@@ -549,7 +600,7 @@ export function createNegotiationTools(defineTool, deps) {
549
600
  });
550
601
  const finalTurnCount = newTurnCount + 1;
551
602
  // Evaluate response
552
- if (aiTurn.action === 'accept' || aiTurn.action === 'reject') {
603
+ if (isTerminalAction(aiTurn.action)) {
553
604
  const fullHistory = [...historyForDispatch, aiTurn];
554
605
  const outcome = buildNegotiationOutcome(fullHistory, finalTurnCount, aiTurn.action, meta.sourceUserId, meta.candidateUserId, counterpartySpeaker === 'source' ? 'candidate' : 'source');
555
606
  await negotiationDatabase.updateTaskState(task.id, 'completed');
@@ -598,6 +649,9 @@ export function createNegotiationTools(defineTool, deps) {
598
649
  history: [...historyForDispatch, aiTurn],
599
650
  isFinalTurn: finalTurnCount + 1 >= maxTurns,
600
651
  isDiscoverer: true,
652
+ seat,
653
+ protocolVersion,
654
+ allowedActions: [...allowedActionsFor(protocolVersion, seat, finalTurnCount + 1 >= maxTurns)],
601
655
  };
602
656
  const userDispatchResult = await deps.agentDispatcher?.dispatch(context.userId, scope, userDispatchPayload, { timeoutMs });
603
657
  if (!userDispatchResult || (userDispatchResult.handled === false && userDispatchResult.reason === 'no_agent')) {
@@ -642,7 +696,7 @@ export function createNegotiationTools(defineTool, deps) {
642
696
  taskId: task.id,
643
697
  });
644
698
  const userTurnCount = finalTurnCount + 1;
645
- if (userAgentTurn.action === 'accept' || userAgentTurn.action === 'reject') {
699
+ if (isTerminalAction(userAgentTurn.action)) {
646
700
  const fullHistory = [...historyForDispatch, aiTurn, userAgentTurn];
647
701
  const userSpeaker = isSource ? 'source' : 'candidate';
648
702
  const outcome = buildNegotiationOutcome(fullHistory, userTurnCount, userAgentTurn.action, meta.sourceUserId, meta.candidateUserId, userSpeaker === 'source' ? 'candidate' : 'source');
@@ -726,7 +780,8 @@ export function createNegotiationTools(defineTool, deps) {
726
780
  */
727
781
  function buildNegotiationOutcome(history, turnCount, lastAction, sourceUserId, candidateUserId, currentSpeaker) {
728
782
  const hasOpportunity = lastAction === 'accept';
729
- const atCap = lastAction === 'counter';
783
+ // Non-terminal last action at finalization means the turn cap was hit.
784
+ const atCap = !isTerminalAction(lastAction);
730
785
  let agreedRoles = [];
731
786
  if (hasOpportunity && history.length >= 2) {
732
787
  const acceptTurn = history[history.length - 1];