@indexnetwork/protocol 0.4.0 → 0.5.0-rc.16.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 (85) hide show
  1. package/README.md +8 -3
  2. package/dist/agent/agent.tools.d.ts +3 -0
  3. package/dist/agent/agent.tools.d.ts.map +1 -0
  4. package/dist/agent/agent.tools.js +330 -0
  5. package/dist/agent/agent.tools.js.map +1 -0
  6. package/dist/chat/chat.agent.d.ts.map +1 -1
  7. package/dist/chat/chat.agent.js +7 -6
  8. package/dist/chat/chat.agent.js.map +1 -1
  9. package/dist/index.d.ts +6 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +3 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp/mcp.server.d.ts +1 -1
  14. package/dist/mcp/mcp.server.d.ts.map +1 -1
  15. package/dist/mcp/mcp.server.js +6 -3
  16. package/dist/mcp/mcp.server.js.map +1 -1
  17. package/dist/negotiation/negotiation.agent.d.ts +30 -0
  18. package/dist/negotiation/negotiation.agent.d.ts.map +1 -0
  19. package/dist/negotiation/negotiation.agent.js +92 -0
  20. package/dist/negotiation/negotiation.agent.js.map +1 -0
  21. package/dist/negotiation/negotiation.graph.d.ts +138 -166
  22. package/dist/negotiation/negotiation.graph.d.ts.map +1 -1
  23. package/dist/negotiation/negotiation.graph.js +85 -80
  24. package/dist/negotiation/negotiation.graph.js.map +1 -1
  25. package/dist/negotiation/negotiation.state.d.ts +128 -34
  26. package/dist/negotiation/negotiation.state.d.ts.map +1 -1
  27. package/dist/negotiation/negotiation.state.js +45 -14
  28. package/dist/negotiation/negotiation.state.js.map +1 -1
  29. package/dist/negotiation/negotiation.tools.d.ts.map +1 -1
  30. package/dist/negotiation/negotiation.tools.js +183 -172
  31. package/dist/negotiation/negotiation.tools.js.map +1 -1
  32. package/dist/opportunity/opportunity.evaluator.d.ts.map +1 -1
  33. package/dist/opportunity/opportunity.evaluator.js +35 -10
  34. package/dist/opportunity/opportunity.evaluator.js.map +1 -1
  35. package/dist/opportunity/opportunity.graph.d.ts +30 -0
  36. package/dist/opportunity/opportunity.graph.d.ts.map +1 -1
  37. package/dist/opportunity/opportunity.graph.js +45 -8
  38. package/dist/opportunity/opportunity.graph.js.map +1 -1
  39. package/dist/opportunity/opportunity.tools.d.ts.map +1 -1
  40. package/dist/opportunity/opportunity.tools.js +12 -4
  41. package/dist/opportunity/opportunity.tools.js.map +1 -1
  42. package/dist/profile/profile.graph.d.ts +12 -12
  43. package/dist/profile/profile.graph.d.ts.map +1 -1
  44. package/dist/profile/profile.graph.js +22 -15
  45. package/dist/profile/profile.graph.js.map +1 -1
  46. package/dist/profile/profile.tools.d.ts.map +1 -1
  47. package/dist/profile/profile.tools.js +12 -1
  48. package/dist/profile/profile.tools.js.map +1 -1
  49. package/dist/shared/agent/model.config.d.ts +1 -4
  50. package/dist/shared/agent/model.config.d.ts.map +1 -1
  51. package/dist/shared/agent/model.config.js +1 -2
  52. package/dist/shared/agent/model.config.js.map +1 -1
  53. package/dist/shared/agent/tool.factory.d.ts.map +1 -1
  54. package/dist/shared/agent/tool.factory.js +12 -6
  55. package/dist/shared/agent/tool.factory.js.map +1 -1
  56. package/dist/shared/agent/tool.helpers.d.ts +17 -9
  57. package/dist/shared/agent/tool.helpers.d.ts.map +1 -1
  58. package/dist/shared/agent/tool.helpers.js.map +1 -1
  59. package/dist/shared/agent/tool.registry.d.ts.map +1 -1
  60. package/dist/shared/agent/tool.registry.js +2 -0
  61. package/dist/shared/agent/tool.registry.js.map +1 -1
  62. package/dist/shared/interfaces/agent-dispatcher.interface.d.ts +68 -0
  63. package/dist/shared/interfaces/agent-dispatcher.interface.d.ts.map +1 -0
  64. package/dist/shared/interfaces/agent-dispatcher.interface.js +9 -0
  65. package/dist/shared/interfaces/agent-dispatcher.interface.js.map +1 -0
  66. package/dist/shared/interfaces/agent.interface.d.ts +176 -0
  67. package/dist/shared/interfaces/agent.interface.d.ts.map +1 -0
  68. package/dist/shared/interfaces/agent.interface.js +15 -0
  69. package/dist/shared/interfaces/agent.interface.js.map +1 -0
  70. package/dist/shared/interfaces/auth.interface.d.ts +10 -3
  71. package/dist/shared/interfaces/auth.interface.d.ts.map +1 -1
  72. package/dist/shared/interfaces/database.interface.d.ts +1 -1
  73. package/dist/shared/interfaces/database.interface.d.ts.map +1 -1
  74. package/dist/shared/interfaces/negotiation-events.interface.d.ts +2 -34
  75. package/dist/shared/interfaces/negotiation-events.interface.d.ts.map +1 -1
  76. package/dist/shared/interfaces/negotiation-events.interface.js +2 -2
  77. package/package.json +4 -2
  78. package/dist/negotiation/negotiation.proposer.d.ts +0 -26
  79. package/dist/negotiation/negotiation.proposer.d.ts.map +0 -1
  80. package/dist/negotiation/negotiation.proposer.js +0 -67
  81. package/dist/negotiation/negotiation.proposer.js.map +0 -1
  82. package/dist/negotiation/negotiation.responder.d.ts +0 -26
  83. package/dist/negotiation/negotiation.responder.d.ts.map +0 -1
  84. package/dist/negotiation/negotiation.responder.js +0 -71
  85. package/dist/negotiation/negotiation.responder.js.map +0 -1
@@ -1,7 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { success, error } from '../shared/agent/tool.helpers.js';
3
- import { NegotiationProposer } from './negotiation.proposer.js';
4
- import { NegotiationResponder } from './negotiation.responder.js';
3
+ import { IndexNegotiator } from './negotiation.agent.js';
5
4
  import { protocolLogger } from '../shared/observability/protocol.logger.js';
6
5
  const logger = protocolLogger('NegotiationTools');
7
6
  /**
@@ -17,12 +16,12 @@ export function createNegotiationTools(defineTool, deps) {
17
16
  'mutual opportunity for collaboration.\n\n' +
18
17
  '**Statuses:**\n' +
19
18
  '- `active` — Negotiation is in progress, agents are exchanging turns.\n' +
20
- '- `waiting_for_external` — The graph has yielded and is waiting for an external response (e.g. from the user via respond_to_negotiation) or a timeout.\n' +
19
+ '- `waiting_for_agent` — The graph has yielded and is waiting for an agent response (e.g. from the user via respond_to_negotiation) or a timeout.\n' +
21
20
  '- `completed` — Negotiation has concluded (accepted, rejected, or reached turn cap).\n\n' +
22
21
  '**When to use:** To see ongoing and past negotiations, check which negotiations need attention, ' +
23
22
  'or find a negotiation ID for get_negotiation or respond_to_negotiation.',
24
23
  querySchema: z.object({
25
- status: z.enum(['active', 'waiting_for_external', 'completed', 'all']).optional()
24
+ status: z.enum(['active', 'waiting_for_agent', 'completed', 'all']).optional()
26
25
  .describe('Filter by negotiation status. Omit or use "all" to return all negotiations.'),
27
26
  }),
28
27
  handler: async ({ context, query }) => {
@@ -31,7 +30,7 @@ export function createNegotiationTools(defineTool, deps) {
31
30
  const stateFilter = query.status && query.status !== 'all' ? query.status : undefined;
32
31
  // For 'active', query 'working' state tasks
33
32
  const dbState = stateFilter === 'active' ? 'working'
34
- : stateFilter === 'waiting_for_external' ? 'waiting_for_external'
33
+ : stateFilter === 'waiting_for_agent' ? 'waiting_for_agent'
35
34
  : stateFilter === 'completed' ? 'completed'
36
35
  : undefined;
37
36
  const tasks = await negotiationDatabase.getTasksForUser(context.userId, dbState ? { state: dbState } : undefined);
@@ -53,7 +52,7 @@ export function createNegotiationTools(defineTool, deps) {
53
52
  const isUsersTurn = (isSource && currentSpeaker === 'source') || (!isSource && currentSpeaker === 'candidate');
54
53
  // Map task state to tool status
55
54
  const status = task.state === 'working' ? 'active'
56
- : task.state === 'waiting_for_external' ? 'waiting_for_external'
55
+ : task.state === 'waiting_for_agent' ? 'waiting_for_agent'
57
56
  : task.state === 'completed' ? 'completed'
58
57
  : task.state;
59
58
  return {
@@ -86,8 +85,9 @@ export function createNegotiationTools(defineTool, deps) {
86
85
  name: 'get_negotiation',
87
86
  description: 'Get the full details of a specific negotiation, including all turns, messages, counterparty info, and current state. ' +
88
87
  'Negotiations are bilateral exchanges where two AI agents negotiate on behalf of users. Each turn contains an action ' +
89
- '(propose, accept, reject, counter) and an assessment with a fit score, reasoning, and suggested roles.\n\n' +
88
+ '(propose, accept, reject, counter, question), an assessment with reasoning and suggested roles, and an optional message.\n\n' +
90
89
  '**Access control:** You must be a party to the negotiation (source or candidate) to view it.\n\n' +
90
+ '**Statuses:** `active` — in progress. `waiting_for_agent` — waiting for an agent response or timeout. `completed` — concluded.\n\n' +
91
91
  '**When to use:** To review the full negotiation history before responding, to understand why a negotiation was ' +
92
92
  'accepted or rejected, or to see the current state of an active negotiation.',
93
93
  querySchema: z.object({
@@ -126,9 +126,9 @@ export function createNegotiationTools(defineTool, deps) {
126
126
  speaker,
127
127
  senderId: m.senderId,
128
128
  action: turnData?.action ?? 'unknown',
129
- fitScore: turnData?.assessment?.fitScore ?? null,
130
129
  reasoning: turnData?.assessment?.reasoning ?? null,
131
130
  suggestedRoles: turnData?.assessment?.suggestedRoles ?? null,
131
+ message: turnData?.message ?? null,
132
132
  createdAt: m.createdAt,
133
133
  };
134
134
  });
@@ -142,7 +142,7 @@ export function createNegotiationTools(defineTool, deps) {
142
142
  const currentSpeaker = turnCount % 2 === 0 ? 'source' : 'candidate';
143
143
  const isUsersTurn = (isSource && currentSpeaker === 'source') || (!isSource && currentSpeaker === 'candidate');
144
144
  const status = task.state === 'working' ? 'active'
145
- : task.state === 'waiting_for_external' ? 'waiting_for_external'
145
+ : task.state === 'waiting_for_agent' ? 'waiting_for_agent'
146
146
  : task.state === 'completed' ? 'completed'
147
147
  : task.state;
148
148
  return success({
@@ -166,21 +166,21 @@ export function createNegotiationTools(defineTool, deps) {
166
166
  });
167
167
  const respond_to_negotiation = defineTool({
168
168
  name: 'respond_to_negotiation',
169
- description: 'Respond to a negotiation that is waiting for external input. This tool allows users to influence the negotiation ' +
170
- 'by accepting, rejecting, or countering the current proposal.\n\n' +
169
+ description: 'Respond to a negotiation that is waiting for agent input. This tool allows users to influence the negotiation ' +
170
+ 'by accepting, rejecting, countering, or asking a clarifying question.\n\n' +
171
171
  '**Turn-based model:** Negotiations alternate between source and candidate agents. When the graph yields with ' +
172
- '`waiting_for_external` status, the user whose turn it is can respond.\n\n' +
172
+ '`waiting_for_agent` status, the user whose turn it is can respond.\n\n' +
173
173
  '**Valid actions:**\n' +
174
174
  '- `accept` — Accept the current proposal. The negotiation will be finalized as an opportunity.\n' +
175
175
  '- `reject` — Reject the current proposal. The negotiation will end without creating an opportunity.\n' +
176
- '- `counter` — Counter the proposal with a message (message is required for counter). The negotiation will continue.\n\n' +
177
- '**What happens after:** Accept/reject finalizes the negotiation immediately. Counter continues the negotiation ' +
178
- 'if the counterparty has an external agent, the negotiation yields again; otherwise the AI agent responds inline.',
176
+ '- `counter` — Counter the proposal with a message (message is required). The negotiation will continue.\n' +
177
+ '- `question` Ask the counterparty a clarifying question (message is required). The negotiation will continue.\n\n' +
178
+ '**What happens after:** Accept/reject finalizes the negotiation immediately. Counter/question continues the negotiation ' +
179
+ 'if the counterparty has an agent, the negotiation yields again; otherwise the AI agent responds inline.',
179
180
  querySchema: z.object({
180
181
  negotiationId: z.string().describe('The negotiation task ID to respond to.'),
181
- action: z.enum(['accept', 'reject', 'counter']).describe('The response action: accept the proposal, reject it, or counter with a new message.'),
182
- message: z.string().optional().describe('Required for "counter" action. Your counter-proposal message explaining what you want to change.'),
183
- fitScore: z.number().min(0).max(100).optional().describe('Optional fit score (0-100) for your assessment. Defaults to 100 for accept, 0 for reject, 50 for counter.'),
182
+ action: z.enum(['accept', 'reject', 'counter', 'question']).describe('The response action: accept the proposal, reject it, counter with a new message, or ask a clarifying question.'),
183
+ message: z.string().optional().describe('Required for "counter" and "question" actions. Your message explaining what you want to change or clarify.'),
184
184
  }),
185
185
  handler: async ({ context, query }) => {
186
186
  try {
@@ -192,8 +192,8 @@ export function createNegotiationTools(defineTool, deps) {
192
192
  if (meta?.type !== 'negotiation') {
193
193
  return error('Negotiation not found.');
194
194
  }
195
- // Validate negotiation is waiting for external input
196
- if (task.state !== 'waiting_for_external') {
195
+ // Validate negotiation is waiting for agent input
196
+ if (task.state !== 'waiting_for_agent') {
197
197
  return error(`Negotiation is not waiting for a response. Current status: ${task.state}`);
198
198
  }
199
199
  // Access control: user must be a party
@@ -210,23 +210,22 @@ export function createNegotiationTools(defineTool, deps) {
210
210
  if (!isUsersTurn) {
211
211
  return error('It is not your turn to respond in this negotiation.');
212
212
  }
213
- // Validate counter has a message
214
- if (query.action === 'counter' && !query.message?.trim()) {
215
- return error('A message is required when countering a proposal. Explain what you want to change.');
213
+ // Validate counter/question has a message
214
+ if ((query.action === 'counter' || query.action === 'question') && !query.message?.trim()) {
215
+ return error(`A message is required when using "${query.action}". Explain what you want to change or clarify.`);
216
216
  }
217
217
  // ── Cancel pending timeout ──
218
218
  if (deps.negotiationTimeoutQueue) {
219
219
  await deps.negotiationTimeoutQueue.cancelTimeout(task.id);
220
220
  }
221
221
  // ── Build and persist the external agent's turn ──
222
- const defaultFitScore = query.action === 'accept' ? 100 : query.action === 'reject' ? 0 : 50;
223
222
  const turnData = {
224
223
  action: query.action,
225
224
  assessment: {
226
- fitScore: query.fitScore ?? defaultFitScore,
227
225
  reasoning: query.message ?? `User ${query.action}ed the proposal.`,
228
226
  suggestedRoles: { ownUser: 'peer', otherUser: 'peer' },
229
227
  },
228
+ ...(query.message ? { message: query.message } : {}),
230
229
  };
231
230
  const senderId = `agent:${context.userId}`;
232
231
  const turnMessage = await negotiationDatabase.createMessage({
@@ -253,24 +252,6 @@ export function createNegotiationTools(defineTool, deps) {
253
252
  parts: [{ kind: 'data', data: outcome }],
254
253
  metadata: { hasOpportunity: outcome.hasOpportunity, turnCount: newTurnCount },
255
254
  });
256
- // Emit completed event for both parties
257
- if (deps.negotiationEvents) {
258
- const outcomeStr = query.action === 'accept' ? 'accepted' : 'rejected';
259
- deps.negotiationEvents.emitCompleted({
260
- negotiationId: task.id,
261
- userId: meta.sourceUserId,
262
- outcome: outcomeStr,
263
- finalScore: outcome.finalScore,
264
- turnCount: newTurnCount,
265
- });
266
- deps.negotiationEvents.emitCompleted({
267
- negotiationId: task.id,
268
- userId: meta.candidateUserId,
269
- outcome: outcomeStr,
270
- finalScore: outcome.finalScore,
271
- turnCount: newTurnCount,
272
- });
273
- }
274
255
  return success({
275
256
  message: query.action === 'accept'
276
257
  ? 'Negotiation accepted. An opportunity has been created.'
@@ -281,8 +262,8 @@ export function createNegotiationTools(defineTool, deps) {
281
262
  outcome,
282
263
  });
283
264
  }
284
- // ── Handle counter: check if under max turns ──
285
- const maxTurns = 6; // Default max turns (matches graph default)
265
+ // ── Handle counter/question: check if under max turns ──
266
+ const maxTurns = meta.maxTurns ?? 6; // Read from task metadata; fallback to system default
286
267
  if (newTurnCount >= maxTurns) {
287
268
  // Max turns reached — finalize with turn_cap
288
269
  const allMessages = [...messages, { id: turnMessage.id, senderId: turnMessage.senderId, role: turnMessage.role, parts: turnMessage.parts, createdAt: turnMessage.createdAt }];
@@ -299,85 +280,73 @@ export function createNegotiationTools(defineTool, deps) {
299
280
  parts: [{ kind: 'data', data: outcome }],
300
281
  metadata: { hasOpportunity: false, turnCount: newTurnCount },
301
282
  });
302
- if (deps.negotiationEvents) {
303
- deps.negotiationEvents.emitCompleted({
304
- negotiationId: task.id,
305
- userId: meta.sourceUserId,
306
- outcome: 'turn_cap',
307
- finalScore: 0,
308
- turnCount: newTurnCount,
309
- });
310
- deps.negotiationEvents.emitCompleted({
311
- negotiationId: task.id,
312
- userId: meta.candidateUserId,
313
- outcome: 'turn_cap',
314
- finalScore: 0,
315
- turnCount: newTurnCount,
316
- });
317
- }
318
283
  return success({
319
284
  message: 'Maximum turns reached. Negotiation finalized without opportunity.',
320
285
  negotiationId: task.id,
321
- action: 'counter',
286
+ action: query.action,
322
287
  turnNumber: newTurnCount,
323
288
  outcome,
324
289
  });
325
290
  }
326
- // ── Counter under max turns: determine counterparty's next turn ──
291
+ // ── Counter/question under max turns: dispatch to counterparty's agent ──
327
292
  const counterpartyUserId = isSource ? meta.candidateUserId : meta.sourceUserId;
328
293
  const counterpartySpeaker = isSource ? 'candidate' : 'source';
329
- // Check if counterparty has an external agent
330
- const counterpartyHasWebhook = deps.webhookLookup
331
- ? await deps.webhookLookup.hasWebhookForEvent(counterpartyUserId, 'negotiation.turn_received')
332
- : false;
333
- if (counterpartyHasWebhook) {
334
- // Yield again for the counterparty's external agent
335
- await negotiationDatabase.updateTaskState(task.id, 'waiting_for_external');
336
- const deadline = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
337
- if (deps.negotiationEvents) {
338
- deps.negotiationEvents.emitTurnReceived({
339
- negotiationId: task.id,
340
- userId: counterpartyUserId,
341
- turnNumber: newTurnCount + 1,
342
- counterpartyAction: 'counter',
343
- counterpartyMessage: query.message,
344
- deadline,
345
- });
346
- }
347
- if (deps.negotiationTimeoutQueue) {
348
- await deps.negotiationTimeoutQueue.enqueueTimeout(task.id, newTurnCount, 24 * 60 * 60 * 1000);
349
- }
350
- return success({
351
- message: 'Counter-proposal submitted. Waiting for counterparty response.',
352
- negotiationId: task.id,
353
- action: 'counter',
354
- turnNumber: newTurnCount,
355
- waitingForExternal: true,
356
- });
357
- }
358
- // ── No external agent for counterparty: run AI agent inline ──
359
- await negotiationDatabase.updateTaskState(task.id, 'working');
360
- const allMessages = [...messages, { id: turnMessage.id, senderId: turnMessage.senderId, role: turnMessage.role, parts: turnMessage.parts, createdAt: turnMessage.createdAt }];
361
- const history = allMessages.map(m => {
294
+ // Build the current turn history for dispatcher payload
295
+ const allMessagesWithTurn = [...messages, { id: turnMessage.id, senderId: turnMessage.senderId, role: turnMessage.role, parts: turnMessage.parts, createdAt: turnMessage.createdAt }];
296
+ const historyForDispatch = allMessagesWithTurn.map(m => {
362
297
  const dp = m.parts?.find(p => p.kind === 'data');
363
298
  return dp?.data;
364
299
  }).filter(Boolean);
365
- // Build user contexts from task metadata for AI agent invocation
366
- // Note: we use minimal context here since we don't have full user profiles in the tool.
367
- // The AI agent will work with the history which contains all the reasoning.
368
- const counterpartyIsSource = counterpartySpeaker === 'source';
369
- const agent = counterpartyIsSource ? new NegotiationProposer() : new NegotiationResponder();
300
+ const isFinalTurn = newTurnCount + 1 >= maxTurns;
370
301
  const ownUserCtx = { id: counterpartyUserId, intents: [], profile: {} };
371
302
  const otherUserCtx = { id: context.userId, intents: [], profile: {} };
372
- const seedAssessment = { score: 50, reasoning: 'Continued negotiation', valencyRole: 'peer' };
373
- const aiTurn = await agent.invoke({
303
+ const seedAssessment = { reasoning: 'Continued negotiation', valencyRole: 'peer' };
304
+ const dispatchPayload = {
305
+ negotiationId: task.id,
374
306
  ownUser: ownUserCtx,
375
307
  otherUser: otherUserCtx,
376
- indexContext: { networkId: '', prompt: '' },
308
+ indexContext: { networkId: '' },
377
309
  seedAssessment,
378
- history,
379
- });
380
- // Persist AI turn
310
+ history: historyForDispatch,
311
+ isFinalTurn,
312
+ isDiscoverer: false,
313
+ };
314
+ const scope = { action: 'negotiation.respond', scopeType: 'negotiation', scopeId: task.id };
315
+ const timeoutMs = 24 * 60 * 60 * 1000;
316
+ const dispatchResult = await deps.agentDispatcher?.dispatch(counterpartyUserId, scope, dispatchPayload, { timeoutMs });
317
+ if (dispatchResult?.handled === false && dispatchResult.reason === 'waiting') {
318
+ // Counterparty's agent acknowledged — yield and wait
319
+ await negotiationDatabase.updateTaskState(task.id, 'waiting_for_agent');
320
+ if (deps.negotiationTimeoutQueue) {
321
+ await deps.negotiationTimeoutQueue.enqueueTimeout(task.id, newTurnCount, timeoutMs);
322
+ }
323
+ return success({
324
+ message: `${query.action === 'question' ? 'Question' : 'Counter-proposal'} submitted. Waiting for counterparty response.`,
325
+ negotiationId: task.id,
326
+ action: query.action,
327
+ turnNumber: newTurnCount,
328
+ waitingForAgent: true,
329
+ });
330
+ }
331
+ let aiTurn;
332
+ if (dispatchResult?.handled === true) {
333
+ // Dispatcher returned an agent turn directly
334
+ aiTurn = dispatchResult.turn;
335
+ }
336
+ else {
337
+ // No agent or timeout — run the system AI agent inline
338
+ await negotiationDatabase.updateTaskState(task.id, 'working');
339
+ const agent = new IndexNegotiator();
340
+ aiTurn = await agent.invoke({
341
+ ownUser: ownUserCtx,
342
+ otherUser: otherUserCtx,
343
+ indexContext: { networkId: '' },
344
+ seedAssessment,
345
+ history: historyForDispatch,
346
+ isFinalTurn,
347
+ });
348
+ }
349
+ // Persist the counterparty's turn (from dispatcher or inline AI)
381
350
  const aiSenderId = `agent:${counterpartyUserId}`;
382
351
  await negotiationDatabase.createMessage({
383
352
  conversationId: task.conversationId,
@@ -387,9 +356,9 @@ export function createNegotiationTools(defineTool, deps) {
387
356
  taskId: task.id,
388
357
  });
389
358
  const finalTurnCount = newTurnCount + 1;
390
- // Evaluate AI response
359
+ // Evaluate response
391
360
  if (aiTurn.action === 'accept' || aiTurn.action === 'reject') {
392
- const fullHistory = [...history, aiTurn];
361
+ const fullHistory = [...historyForDispatch, aiTurn];
393
362
  const outcome = buildNegotiationOutcome(fullHistory, finalTurnCount, aiTurn.action, meta.sourceUserId, meta.candidateUserId, counterpartySpeaker === 'source' ? 'candidate' : 'source');
394
363
  await negotiationDatabase.updateTaskState(task.id, 'completed');
395
364
  await negotiationDatabase.createArtifact({
@@ -398,35 +367,18 @@ export function createNegotiationTools(defineTool, deps) {
398
367
  parts: [{ kind: 'data', data: outcome }],
399
368
  metadata: { hasOpportunity: outcome.hasOpportunity, turnCount: finalTurnCount },
400
369
  });
401
- if (deps.negotiationEvents) {
402
- const outcomeStr = aiTurn.action === 'accept' ? 'accepted' : 'rejected';
403
- deps.negotiationEvents.emitCompleted({
404
- negotiationId: task.id,
405
- userId: meta.sourceUserId,
406
- outcome: outcomeStr,
407
- finalScore: outcome.finalScore,
408
- turnCount: finalTurnCount,
409
- });
410
- deps.negotiationEvents.emitCompleted({
411
- negotiationId: task.id,
412
- userId: meta.candidateUserId,
413
- outcome: outcomeStr,
414
- finalScore: outcome.finalScore,
415
- turnCount: finalTurnCount,
416
- });
417
- }
418
370
  return success({
419
- message: `Counter submitted. AI agent responded with ${aiTurn.action}.`,
371
+ message: `${query.action === 'question' ? 'Question' : 'Counter'} submitted. Counterparty responded with ${aiTurn.action}.`,
420
372
  negotiationId: task.id,
421
373
  action: query.action,
422
374
  turnNumber: newTurnCount,
423
- aiResponse: { action: aiTurn.action, fitScore: aiTurn.assessment.fitScore, reasoning: aiTurn.assessment.reasoning },
375
+ counterpartyResponse: { action: aiTurn.action, reasoning: aiTurn.assessment.reasoning, message: aiTurn.message ?? null },
424
376
  outcome,
425
377
  });
426
378
  }
427
- // AI countered — check if max turns reached
379
+ // Counterparty countered/questioned — check if max turns reached
428
380
  if (finalTurnCount >= maxTurns) {
429
- const fullHistory = [...history, aiTurn];
381
+ const fullHistory = [...historyForDispatch, aiTurn];
430
382
  const outcome = buildNegotiationOutcome(fullHistory, finalTurnCount, 'counter', meta.sourceUserId, meta.candidateUserId, counterpartySpeaker === 'source' ? 'candidate' : 'source');
431
383
  await negotiationDatabase.updateTaskState(task.id, 'completed');
432
384
  await negotiationDatabase.createArtifact({
@@ -435,68 +387,130 @@ export function createNegotiationTools(defineTool, deps) {
435
387
  parts: [{ kind: 'data', data: outcome }],
436
388
  metadata: { hasOpportunity: false, turnCount: finalTurnCount },
437
389
  });
438
- if (deps.negotiationEvents) {
439
- deps.negotiationEvents.emitCompleted({
440
- negotiationId: task.id,
441
- userId: meta.sourceUserId,
442
- outcome: 'turn_cap',
443
- finalScore: 0,
444
- turnCount: finalTurnCount,
445
- });
446
- deps.negotiationEvents.emitCompleted({
447
- negotiationId: task.id,
448
- userId: meta.candidateUserId,
449
- outcome: 'turn_cap',
450
- finalScore: 0,
451
- turnCount: finalTurnCount,
452
- });
453
- }
454
390
  return success({
455
- message: 'AI agent countered but max turns reached. Negotiation finalized.',
391
+ message: 'Counterparty responded but max turns reached. Negotiation finalized.',
456
392
  negotiationId: task.id,
457
393
  action: query.action,
458
394
  turnNumber: newTurnCount,
459
- aiResponse: { action: aiTurn.action, fitScore: aiTurn.assessment.fitScore, reasoning: aiTurn.assessment.reasoning },
395
+ counterpartyResponse: { action: aiTurn.action, reasoning: aiTurn.assessment.reasoning, message: aiTurn.message ?? null },
460
396
  outcome,
461
397
  });
462
398
  }
463
- // AI countered, user's turn again — check if user has webhook to yield
464
- const userHasWebhook = deps.webhookLookup
465
- ? await deps.webhookLookup.hasWebhookForEvent(context.userId, 'negotiation.turn_received')
466
- : false;
467
- if (userHasWebhook) {
468
- await negotiationDatabase.updateTaskState(task.id, 'waiting_for_external');
469
- const deadline = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
470
- if (deps.negotiationEvents) {
471
- deps.negotiationEvents.emitTurnReceived({
399
+ // Counterparty countered/questioned, now user's turn again — dispatch to user's agent
400
+ const userDispatchPayload = {
401
+ negotiationId: task.id,
402
+ ownUser: { id: context.userId, intents: [], profile: {} },
403
+ otherUser: { id: counterpartyUserId, intents: [], profile: {} },
404
+ indexContext: { networkId: '' },
405
+ seedAssessment,
406
+ history: [...historyForDispatch, aiTurn],
407
+ isFinalTurn: finalTurnCount + 1 >= maxTurns,
408
+ isDiscoverer: true,
409
+ };
410
+ const userDispatchResult = await deps.agentDispatcher?.dispatch(context.userId, scope, userDispatchPayload, { timeoutMs });
411
+ if (!userDispatchResult || (userDispatchResult.handled === false && userDispatchResult.reason === 'no_agent')) {
412
+ // No agent for user — set back to waiting_for_agent so they can use respond_to_negotiation
413
+ await negotiationDatabase.updateTaskState(task.id, 'waiting_for_agent');
414
+ if (deps.negotiationTimeoutQueue) {
415
+ await deps.negotiationTimeoutQueue.enqueueTimeout(task.id, finalTurnCount, timeoutMs);
416
+ }
417
+ return success({
418
+ message: `${query.action === 'question' ? 'Question' : 'Counter'} submitted. Counterparty responded. Your turn to respond.`,
419
+ negotiationId: task.id,
420
+ action: query.action,
421
+ turnNumber: newTurnCount,
422
+ counterpartyResponse: { action: aiTurn.action, reasoning: aiTurn.assessment.reasoning, message: aiTurn.message ?? null },
423
+ waitingForAgent: true,
424
+ });
425
+ }
426
+ if (userDispatchResult.handled === false && userDispatchResult.reason === 'waiting') {
427
+ // User's agent acknowledged — yield and wait
428
+ await negotiationDatabase.updateTaskState(task.id, 'waiting_for_agent');
429
+ if (deps.negotiationTimeoutQueue) {
430
+ await deps.negotiationTimeoutQueue.enqueueTimeout(task.id, finalTurnCount, timeoutMs);
431
+ }
432
+ return success({
433
+ message: `${query.action === 'question' ? 'Question' : 'Counter'} submitted. Counterparty countered back. Waiting for your agent's response.`,
434
+ negotiationId: task.id,
435
+ action: query.action,
436
+ turnNumber: newTurnCount,
437
+ counterpartyResponse: { action: aiTurn.action, reasoning: aiTurn.assessment.reasoning, message: aiTurn.message ?? null },
438
+ waitingForAgent: true,
439
+ });
440
+ }
441
+ if (userDispatchResult.handled === true) {
442
+ // User's agent returned a turn directly — persist, evaluate, and continue
443
+ const userAgentTurn = userDispatchResult.turn;
444
+ const userAgentSenderId = `agent:${context.userId}`;
445
+ await negotiationDatabase.createMessage({
446
+ conversationId: task.conversationId,
447
+ senderId: userAgentSenderId,
448
+ role: 'agent',
449
+ parts: [{ kind: 'data', data: userAgentTurn }],
450
+ taskId: task.id,
451
+ });
452
+ const userTurnCount = finalTurnCount + 1;
453
+ if (userAgentTurn.action === 'accept' || userAgentTurn.action === 'reject') {
454
+ const fullHistory = [...historyForDispatch, aiTurn, userAgentTurn];
455
+ const userSpeaker = isSource ? 'source' : 'candidate';
456
+ const outcome = buildNegotiationOutcome(fullHistory, userTurnCount, userAgentTurn.action, meta.sourceUserId, meta.candidateUserId, userSpeaker === 'source' ? 'candidate' : 'source');
457
+ await negotiationDatabase.updateTaskState(task.id, 'completed');
458
+ await negotiationDatabase.createArtifact({
459
+ taskId: task.id,
460
+ name: 'negotiation-outcome',
461
+ parts: [{ kind: 'data', data: outcome }],
462
+ metadata: { hasOpportunity: outcome.hasOpportunity, turnCount: userTurnCount },
463
+ });
464
+ return success({
465
+ message: `Your agent ${userAgentTurn.action}ed the counterparty's response.`,
466
+ negotiationId: task.id,
467
+ action: query.action,
468
+ turnNumber: newTurnCount,
469
+ counterpartyResponse: { action: aiTurn.action, reasoning: aiTurn.assessment.reasoning, message: aiTurn.message ?? null },
470
+ outcome,
471
+ });
472
+ }
473
+ if (userTurnCount >= maxTurns) {
474
+ const fullHistory = [...historyForDispatch, aiTurn, userAgentTurn];
475
+ const outcome = buildNegotiationOutcome(fullHistory, userTurnCount, 'counter', meta.sourceUserId, meta.candidateUserId, isSource ? 'candidate' : 'source');
476
+ await negotiationDatabase.updateTaskState(task.id, 'completed');
477
+ await negotiationDatabase.createArtifact({
478
+ taskId: task.id,
479
+ name: 'negotiation-outcome',
480
+ parts: [{ kind: 'data', data: outcome }],
481
+ metadata: { hasOpportunity: false, turnCount: userTurnCount },
482
+ });
483
+ return success({
484
+ message: 'Your agent responded but max turns reached. Negotiation finalized.',
472
485
  negotiationId: task.id,
473
- userId: context.userId,
474
- turnNumber: finalTurnCount + 1,
475
- counterpartyAction: aiTurn.action,
476
- counterpartyMessage: aiTurn.assessment.reasoning,
477
- deadline,
486
+ action: query.action,
487
+ turnNumber: newTurnCount,
488
+ counterpartyResponse: { action: aiTurn.action, reasoning: aiTurn.assessment.reasoning, message: aiTurn.message ?? null },
489
+ outcome,
478
490
  });
479
491
  }
492
+ // User's agent countered/questioned — arm timeout for counterparty's next turn
493
+ await negotiationDatabase.updateTaskState(task.id, 'waiting_for_agent');
480
494
  if (deps.negotiationTimeoutQueue) {
481
- await deps.negotiationTimeoutQueue.enqueueTimeout(task.id, finalTurnCount, 24 * 60 * 60 * 1000);
495
+ await deps.negotiationTimeoutQueue.enqueueTimeout(task.id, userTurnCount, timeoutMs);
482
496
  }
483
497
  return success({
484
- message: 'Counter submitted. AI agent countered back. Waiting for your next response.',
498
+ message: `Your agent responded with ${userAgentTurn.action}. Waiting for counterparty.`,
485
499
  negotiationId: task.id,
486
500
  action: query.action,
487
501
  turnNumber: newTurnCount,
488
- aiResponse: { action: aiTurn.action, fitScore: aiTurn.assessment.fitScore, reasoning: aiTurn.assessment.reasoning },
489
- waitingForExternal: true,
502
+ counterpartyResponse: { action: aiTurn.action, reasoning: aiTurn.assessment.reasoning, message: aiTurn.message ?? null },
503
+ waitingForAgent: true,
490
504
  });
491
505
  }
492
- // No webhook for user either — set back to working so graph can continue
506
+ // No agent / timeout — set back to working so graph can continue
493
507
  await negotiationDatabase.updateTaskState(task.id, 'working');
494
508
  return success({
495
- message: 'Counter submitted. AI agent countered back. Negotiation continues.',
509
+ message: `${query.action === 'question' ? 'Question' : 'Counter'} submitted. Counterparty countered back. Negotiation continues.`,
496
510
  negotiationId: task.id,
497
511
  action: query.action,
498
512
  turnNumber: newTurnCount,
499
- aiResponse: { action: aiTurn.action, fitScore: aiTurn.assessment.fitScore, reasoning: aiTurn.assessment.reasoning },
513
+ counterpartyResponse: { action: aiTurn.action, reasoning: aiTurn.assessment.reasoning, message: aiTurn.message ?? null },
500
514
  });
501
515
  }
502
516
  catch (err) {
@@ -520,8 +534,6 @@ export function createNegotiationTools(defineTool, deps) {
520
534
  function buildNegotiationOutcome(history, turnCount, lastAction, sourceUserId, candidateUserId, currentSpeaker) {
521
535
  const hasOpportunity = lastAction === 'accept';
522
536
  const atCap = lastAction === 'counter';
523
- const scores = history.map(t => t.assessment.fitScore);
524
- const avgScore = scores.length > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 0;
525
537
  let agreedRoles = [];
526
538
  if (hasOpportunity && history.length >= 2) {
527
539
  const acceptTurn = history[history.length - 1];
@@ -537,7 +549,6 @@ function buildNegotiationOutcome(history, turnCount, lastAction, sourceUserId, c
537
549
  }
538
550
  return {
539
551
  hasOpportunity,
540
- finalScore: hasOpportunity ? avgScore : 0,
541
552
  agreedRoles,
542
553
  reasoning: history[history.length - 1]?.assessment.reasoning ?? '',
543
554
  turnCount,