@indexnetwork/protocol 0.4.0-rc.13.1 → 0.4.0-rc.15.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.agent.d.ts.map +1 -1
- package/dist/chat/chat.agent.js +7 -6
- package/dist/chat/chat.agent.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/negotiation/negotiation.agent.d.ts +30 -0
- package/dist/negotiation/negotiation.agent.d.ts.map +1 -0
- package/dist/negotiation/negotiation.agent.js +92 -0
- package/dist/negotiation/negotiation.agent.js.map +1 -0
- package/dist/negotiation/negotiation.graph.d.ts +138 -166
- package/dist/negotiation/negotiation.graph.d.ts.map +1 -1
- package/dist/negotiation/negotiation.graph.js +85 -80
- package/dist/negotiation/negotiation.graph.js.map +1 -1
- package/dist/negotiation/negotiation.state.d.ts +128 -34
- package/dist/negotiation/negotiation.state.d.ts.map +1 -1
- package/dist/negotiation/negotiation.state.js +45 -14
- 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 +183 -172
- package/dist/negotiation/negotiation.tools.js.map +1 -1
- package/dist/opportunity/opportunity.evaluator.d.ts.map +1 -1
- package/dist/opportunity/opportunity.evaluator.js +35 -10
- package/dist/opportunity/opportunity.evaluator.js.map +1 -1
- package/dist/opportunity/opportunity.graph.d.ts +1 -2
- package/dist/opportunity/opportunity.graph.d.ts.map +1 -1
- package/dist/opportunity/opportunity.graph.js +4 -10
- package/dist/opportunity/opportunity.graph.js.map +1 -1
- package/dist/shared/agent/model.config.d.ts +1 -4
- package/dist/shared/agent/model.config.d.ts.map +1 -1
- package/dist/shared/agent/model.config.js +1 -2
- package/dist/shared/agent/model.config.js.map +1 -1
- package/dist/shared/agent/tool.factory.d.ts.map +1 -1
- package/dist/shared/agent/tool.factory.js +7 -6
- package/dist/shared/agent/tool.factory.js.map +1 -1
- package/dist/shared/agent/tool.helpers.d.ts +6 -9
- package/dist/shared/agent/tool.helpers.d.ts.map +1 -1
- package/dist/shared/agent/tool.helpers.js.map +1 -1
- package/dist/shared/interfaces/agent-dispatcher.interface.d.ts +68 -0
- package/dist/shared/interfaces/agent-dispatcher.interface.d.ts.map +1 -0
- package/dist/shared/interfaces/agent-dispatcher.interface.js +9 -0
- package/dist/shared/interfaces/agent-dispatcher.interface.js.map +1 -0
- package/dist/shared/interfaces/database.interface.d.ts +1 -1
- package/dist/shared/interfaces/database.interface.d.ts.map +1 -1
- package/dist/shared/interfaces/negotiation-events.interface.d.ts +2 -34
- package/dist/shared/interfaces/negotiation-events.interface.d.ts.map +1 -1
- package/dist/shared/interfaces/negotiation-events.interface.js +2 -2
- package/package.json +1 -1
- package/dist/negotiation/negotiation.proposer.d.ts +0 -26
- package/dist/negotiation/negotiation.proposer.d.ts.map +0 -1
- package/dist/negotiation/negotiation.proposer.js +0 -67
- package/dist/negotiation/negotiation.proposer.js.map +0 -1
- package/dist/negotiation/negotiation.responder.d.ts +0 -26
- package/dist/negotiation/negotiation.responder.d.ts.map +0 -1
- package/dist/negotiation/negotiation.responder.js +0 -71
- 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 {
|
|
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
|
-
'- `
|
|
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', '
|
|
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 === '
|
|
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 === '
|
|
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)
|
|
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 === '
|
|
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
|
|
170
|
-
'by accepting, rejecting, or
|
|
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
|
-
'`
|
|
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
|
|
177
|
-
'
|
|
178
|
-
'
|
|
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,
|
|
182
|
-
message: z.string().optional().describe('Required for "counter"
|
|
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
|
|
196
|
-
if (task.state !== '
|
|
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(
|
|
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; //
|
|
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:
|
|
286
|
+
action: query.action,
|
|
322
287
|
turnNumber: newTurnCount,
|
|
323
288
|
outcome,
|
|
324
289
|
});
|
|
325
290
|
}
|
|
326
|
-
// ── Counter under max turns:
|
|
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
|
-
//
|
|
330
|
-
const
|
|
331
|
-
|
|
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
|
-
|
|
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 = {
|
|
373
|
-
const
|
|
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: ''
|
|
308
|
+
indexContext: { networkId: '' },
|
|
377
309
|
seedAssessment,
|
|
378
|
-
history,
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
359
|
+
// Evaluate response
|
|
391
360
|
if (aiTurn.action === 'accept' || aiTurn.action === 'reject') {
|
|
392
|
-
const fullHistory = [...
|
|
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:
|
|
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
|
-
|
|
375
|
+
counterpartyResponse: { action: aiTurn.action, reasoning: aiTurn.assessment.reasoning, message: aiTurn.message ?? null },
|
|
424
376
|
outcome,
|
|
425
377
|
});
|
|
426
378
|
}
|
|
427
|
-
//
|
|
379
|
+
// Counterparty countered/questioned — check if max turns reached
|
|
428
380
|
if (finalTurnCount >= maxTurns) {
|
|
429
|
-
const fullHistory = [...
|
|
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: '
|
|
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
|
-
|
|
395
|
+
counterpartyResponse: { action: aiTurn.action, reasoning: aiTurn.assessment.reasoning, message: aiTurn.message ?? null },
|
|
460
396
|
outcome,
|
|
461
397
|
});
|
|
462
398
|
}
|
|
463
|
-
//
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
:
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
474
|
-
turnNumber:
|
|
475
|
-
|
|
476
|
-
|
|
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,
|
|
495
|
+
await deps.negotiationTimeoutQueue.enqueueTimeout(task.id, userTurnCount, timeoutMs);
|
|
482
496
|
}
|
|
483
497
|
return success({
|
|
484
|
-
message:
|
|
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
|
-
|
|
489
|
-
|
|
502
|
+
counterpartyResponse: { action: aiTurn.action, reasoning: aiTurn.assessment.reasoning, message: aiTurn.message ?? null },
|
|
503
|
+
waitingForAgent: true,
|
|
490
504
|
});
|
|
491
505
|
}
|
|
492
|
-
// No
|
|
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.
|
|
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
|
-
|
|
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,
|