agent-clinch 0.7.7 → 0.7.8

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 (2) hide show
  1. package/cli.js +81 -59
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -202,10 +202,13 @@ function downloadFile(url, dest) {
202
202
  });
203
203
  }
204
204
 
205
+ // Global cache to prevent reloading the 1.1GB model on every chat turn
206
+ let cachedLlamaModel = null;
207
+ let cachedLlamaContext = null;
208
+
205
209
  async function promptAI(systemPrompt, userText, cfg) {
206
210
  if (cfg.engine === 'ollama') {
207
211
  try {
208
- console.log(c.dim(`\n[Agent Q] Dispatching request to local Ollama (${cfg.ollamaModel || 'llama3'})...`));
209
212
  const res = await fetch('http://127.0.0.1:11434/api/chat', {
210
213
  method: 'POST',
211
214
  headers: { 'Content-Type': 'application/json' },
@@ -234,8 +237,6 @@ async function promptAI(systemPrompt, userText, cfg) {
234
237
  await downloadFile(cfg.ggufUrl || "https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q4_k_m.gguf", resolvedPath);
235
238
  }
236
239
 
237
- console.log(c.dim(`\n[Agent Q] Loading GGUF model into memory (this may take a few seconds)...`));
238
-
239
240
  let nodeLlama;
240
241
  try { nodeLlama = await import('node-llama-cpp'); }
241
242
  catch (e) {
@@ -244,17 +245,23 @@ async function promptAI(systemPrompt, userText, cfg) {
244
245
  process.exit(1);
245
246
  }
246
247
 
247
- const llama = await nodeLlama.getLlama();
248
- const model = await llama.loadModel({ modelPath: resolvedPath });
249
- const context = await model.createContext({ contextSize: 2048, threads: Math.max(1, os.cpus().length - 1) });
248
+ if (!cachedLlamaModel) {
249
+ const threads = Math.max(2, os.cpus().length - 1);
250
+ console.log(c.dim(`\n[Agent Q] Loading GGUF model into memory using ${threads} threads (this may take a few seconds)...`));
251
+
252
+ const llama = await nodeLlama.getLlama();
253
+ cachedLlamaModel = await llama.loadModel({ modelPath: resolvedPath });
254
+ cachedLlamaContext = await cachedLlamaModel.createContext({ contextSize: 2048, threads: threads });
255
+
256
+ console.log(c.dim(`[Agent Q] Model loaded. Analyzing...`));
257
+ }
258
+
250
259
  const session = new nodeLlama.LlamaChatSession({
251
- contextSequence: context.getSequence(),
260
+ contextSequence: cachedLlamaContext.getSequence(),
252
261
  systemPrompt: systemPrompt,
253
262
  chatWrapper: new nodeLlama.ChatMLChatWrapper()
254
263
  });
255
264
 
256
- console.log(c.dim(`[Agent Q] Model loaded. Analyzing intent...`));
257
-
258
265
  let responseText = "";
259
266
  await session.prompt(userText, { maxTokens: 1500, onTextChunk: (chunk) => { responseText += chunk; } });
260
267
  return responseText;
@@ -262,18 +269,18 @@ async function promptAI(systemPrompt, userText, cfg) {
262
269
  }
263
270
 
264
271
  async function parseIntentWithLLM(userInput, cfg) {
265
- const systemPrompt = `You are a structured data extractor for a purchasing agent.
266
- Analyze the user's input.
272
+ const systemPrompt = `You are a structured data extractor for a smart network agent.
273
+ Analyze the user's input. They might want to purchase an item, schedule a P2P service, book something, or query a node.
267
274
 
268
- If the user says a greeting (like "hi" or "hello") or does not clearly specify BOTH an item and a budget, output EXACTLY this JSON:
269
- {"error": "Please specify what you want to buy and your maximum budget (e.g. 'Get me a laptop for under $500')."}
275
+ If the user says a greeting (like "hi" or "hello") or their request is too vague to act on, output EXACTLY this JSON:
276
+ {"error": "Please specify what you want to do (e.g. 'Get me a laptop under $500', or 'Schedule a call with @algeru on ginger')."}
270
277
 
271
- If they DO specify a purchase intent, output EXACTLY this JSON schema:
278
+ If they DO specify a clear intent (purchase, scheduling, booking, data retrieval), output EXACTLY this JSON schema:
272
279
  {
273
- "intent": "purchase",
274
- "category": "string (e.g. electronics, domain_names, software, etc)",
275
- "item": "string (the actual item requested)",
276
- "max_budget": number (integer representing the max budget)
280
+ "intent": "string (e.g. purchase, schedule, booking)",
281
+ "category": "string (e.g. electronics, scheduling, p2p_services, domain_names)",
282
+ "item": "string (the actual item, target, or service requested)",
283
+ "max_budget": number (integer representing max budget. If none mentioned, use 0)
277
284
  }
278
285
  Your response MUST be ONLY valid JSON. Do not include conversational text.`;
279
286
 
@@ -283,22 +290,18 @@ Your response MUST be ONLY valid JSON. Do not include conversational text.`;
283
290
 
284
291
  const parsed = JSON.parse(cleanJson);
285
292
 
286
- // Check if the LLM flagged the input as a greeting/unclear
293
+ // Allow the loop to catch errors and reprompt the user interactively
287
294
  if (parsed.error) {
288
- console.log(c.yellow(`\n[Agent Q] ${parsed.error}`));
289
- return null;
295
+ return { error: parsed.error };
290
296
  }
291
297
 
292
- // Fallback validation to ensure it didn't hallucinate missing fields
293
- if (!parsed.item || !parsed.max_budget) {
294
- console.log(c.yellow(`\n[Agent Q] I couldn't quite figure out the item or budget from your request. Please be specific!`));
295
- return null;
298
+ if (!parsed.item) {
299
+ return { error: "I couldn't figure out the exact item or service you want. Please be specific!" };
296
300
  }
297
301
 
298
302
  return parsed;
299
303
  } catch (e) {
300
- console.error(c.red("\n[Agent Q] Failed to parse intent correctly. Falling back to manual entry."));
301
- return null;
304
+ return { error: "Failed to parse intent correctly. Please try formatting your request more simply." };
302
305
  }
303
306
  }
304
307
 
@@ -424,37 +427,51 @@ program
424
427
 
425
428
  cfg = await ensureAIEngine(cfg);
426
429
 
427
- const naturalIntent = await prompt("šŸ‘‰ Describe what you want to negotiate\n" +
430
+ let naturalIntent = await prompt("šŸ‘‰ Describe what you want to negotiate\n" +
428
431
  c.dim(" (e.g., 'Get me the domain cartpost.shop under 80 dollars')\n\nšŸ’¬: "));
429
432
 
430
- if (!naturalIntent) process.exit(1);
433
+ // Conversational loop: keeps asking until a valid intent is parsed
434
+ while (true) {
435
+ if (!naturalIntent) process.exit(1);
431
436
 
432
- const parsed = await parseIntentWithLLM(naturalIntent, cfg);
433
- if (!parsed) {
434
- // We failed to parse correctly (or the user typed hi), gracefully exit rather than crashing
435
- process.exit(1);
436
- }
437
+ const parsed = await parseIntentWithLLM(naturalIntent, cfg);
438
+
439
+ if (!parsed) {
440
+ naturalIntent = await prompt(c.yellow("\n[Agent Q] Something went wrong. Let's try again. What are you looking for?\nšŸ’¬: "));
441
+ continue;
442
+ }
437
443
 
438
- console.log(c.bold("\nšŸ“Š Extracted Intention Context:"));
439
- console.log(` - Category: ${c.cyan(parsed.category)}`);
440
- console.log(` - Target Item: ${c.cyan(parsed.item)}`);
441
- console.log(` - Max Budget: ${c.green("$" + parsed.max_budget)}\n`);
444
+ if (parsed.error) {
445
+ naturalIntent = await prompt(c.yellow(`\n[Agent Q] ${parsed.error}\nšŸ’¬: `));
446
+ continue;
447
+ }
442
448
 
443
- const confirm = await prompt("šŸ‘‰ Is this correct? (Y/n): ");
444
- if (confirm.toLowerCase() === 'n') process.exit(0);
449
+ console.log(c.bold("\nšŸ“Š Extracted Intention Context:"));
450
+ console.log(` - Intent: ${c.cyan(parsed.intent || 'purchase')}`);
451
+ console.log(` - Category: ${c.cyan(parsed.category)}`);
452
+ console.log(` - Target Item: ${c.cyan(parsed.item)}`);
453
+ console.log(` - Max Budget: ${c.green("$" + parsed.max_budget)}\n`);
445
454
 
446
- constraints = parsed;
447
- budget = parsed.max_budget;
455
+ const confirm = await prompt("šŸ‘‰ Is this correct? (Y/n): ");
456
+ if (confirm.toLowerCase() === 'n') {
457
+ naturalIntent = await prompt(c.yellow("\n[Agent Q] Got it. Let's try again. What are you looking for?\nšŸ’¬: "));
458
+ continue;
459
+ }
448
460
 
449
- console.log(c.dim(`\n[Network] Querying registry for category "${parsed.category}"...`));
461
+ constraints = parsed;
462
+ budget = parsed.max_budget;
463
+ break; // Exit loop on confirmation
464
+ }
465
+
466
+ console.log(c.dim(`\n[Network] Querying registry for category "${constraints.category}"...`));
450
467
  const coreDiscovery = getClinchCore(cfg);
451
468
  await coreDiscovery.initialize(cfg.token);
452
- const results = await coreDiscovery.search(parsed.category);
469
+ const results = await coreDiscovery.search(constraints.category);
453
470
  coreDiscovery.disconnect();
454
471
 
455
472
  const sellers = results.results || [];
456
473
  if (sellers.length === 0) {
457
- console.log(c.yellow(`\nNo sellers found for "${parsed.category}".`));
474
+ console.log(c.yellow(`\nNo sellers found for "${constraints.category}".`));
458
475
  targetAddress = await prompt("šŸ‘‰ Enter address manually (e.g. ANP/C.amazon.anp): ");
459
476
  } else {
460
477
  console.log(c.bold(`\nAvailable sellers:`));
@@ -490,12 +507,11 @@ program
490
507
 
491
508
  const incomingMessage = payload.message || JSON.stringify(payload);
492
509
 
493
- // Extract basic price to check constraints
494
510
  const priceMatch = incomingMessage.match(/price\s*:\s*\$?(\d+(?:\.\d{2})?)/i);
495
511
  if (priceMatch) session.lastKnownPrice = parseFloat(priceMatch[1]);
496
512
 
497
513
  if (session.lastKnownPrice > 0 && session.lastKnownPrice <= session.constraints.max_budget) {
498
- console.log(c.green(`\nšŸŽ‰ [Agent Q] Seller met budget conditions! Securing deal.`));
514
+ console.log(c.green(`\nšŸŽ‰ [Agent Q] Target met constraints! Securing deal.`));
499
515
  await core.sendCounter(sessionId, session.lastKnownPrice, "I accept this offer.");
500
516
  return;
501
517
  }
@@ -507,10 +523,12 @@ program
507
523
  }
508
524
 
509
525
  const promptStr = core.buildAgentPrompt(sessionId, incomingMessage);
526
+
527
+ console.log(c.dim(`\n[Agent Q] Evaluating turn ${session.currentTurn}...`));
510
528
  const aiResponse = await promptAI(promptStr, incomingMessage, cfg);
511
529
 
512
530
  let price = null;
513
- let msg = "Counter offer";
531
+ let msg = "Counter offer / Clarification requested";
514
532
 
515
533
  try {
516
534
  const clean = aiResponse.replace(/```json|```/g, "").trim();
@@ -525,8 +543,8 @@ program
525
543
  if (price) {
526
544
  await core.sendCounter(sessionId, Math.min(price, session.constraints.max_budget), msg);
527
545
  } else {
528
- console.log(c.yellow(`[Agent Q] Failed to parse price constraint. Sending safe fallback.`));
529
- await core.sendCounter(sessionId, session.lastKnownPrice * 0.9, "Can you do slightly better?");
546
+ console.log(c.yellow(`[Agent Q] Sending safe fallback response.`));
547
+ await core.sendCounter(sessionId, session.lastKnownPrice * 0.9 || 0, "Can you provide more details?");
530
548
  }
531
549
  });
532
550
  }
@@ -539,20 +557,20 @@ program
539
557
  if (opts.parallel && !opts.squeeze) {
540
558
  maxSellers = parseInt(opts.parallel);
541
559
  strategy = 'parallel';
542
- console.log(c.yellow(`šŸ¤– Parallel Mode: Handshaking concurrently with top ${maxSellers} sellers for "${opts.category}"...\n`));
560
+ console.log(c.yellow(`šŸ¤– Parallel Mode: Handshaking concurrently with top ${maxSellers} nodes for "${opts.category}"...\n`));
543
561
  } else {
544
562
  maxSellers = parseInt(opts.squeeze || '3');
545
563
  strategy = 'sequential';
546
- console.log(c.yellow(`šŸ¤– Squeeze Mode: Sequentially bargaining across top ${maxSellers} sellers for "${opts.category}"...\n`));
564
+ console.log(c.yellow(`šŸ¤– Squeeze Mode: Sequentially communicating across top ${maxSellers} nodes for "${opts.category}"...\n`));
547
565
  }
548
566
 
549
567
  await core.initialize(cfg.token);
550
568
  const bestDeal = await core.negotiateCascade(opts.category, constraints, maxSellers, strategy);
551
569
 
552
570
  if (bestDeal) {
553
- console.log(c.green(c.bold(`\nšŸ† CASCADE COMPLETE: Secured optimal deal with ${bestDeal.sellerId} at $${bestDeal.finalPrice}!`)));
571
+ console.log(c.green(c.bold(`\nšŸ† CASCADE COMPLETE: Secured optimal agreement with ${bestDeal.sellerId} at $${bestDeal.finalPrice}!`)));
554
572
  } else {
555
- console.log(c.red(`\nāœ— Cascade completed without any successful deals.`));
573
+ console.log(c.red(`\nāœ— Cascade completed without any successful agreements.`));
556
574
  }
557
575
  process.exit(0);
558
576
  }
@@ -575,15 +593,15 @@ program
575
593
  if (!runAuto) {
576
594
  core.on('callback_received', ({ sessionId, payload }) => {
577
595
  saveSessionState(sessionId, core);
578
- console.log(c.cyan(`\nšŸ’¬ Seller says:`), payload);
579
- console.log(c.dim(`\nType a price to counter, or "exit" / "accept":`));
596
+ console.log(c.cyan(`\nšŸ’¬ Node says:`), payload);
597
+ console.log(c.dim(`\nType a price/response to counter, or "exit" / "accept":`));
580
598
  });
581
599
  }
582
600
 
583
601
  core.on('session_closed', ({ sessionId, outcome, finalPrice }) => {
584
602
  saveSessionState(sessionId, core);
585
603
  if (outcome === 'deal') {
586
- console.log(c.green(c.bold(`\nšŸŽ‰ DEAL SECURED at $${finalPrice}`)));
604
+ console.log(c.green(c.bold(`\nšŸŽ‰ AGREEMENT SECURED at $${finalPrice}`)));
587
605
  process.exit(0);
588
606
  }
589
607
  });
@@ -598,7 +616,7 @@ program
598
616
  const sessionId = await core.negotiate(targetAddress, constraints);
599
617
 
600
618
  if (!runAuto) {
601
- console.log(c.bold('\nManual mode — await seller response, then type a price to counter, or "exit" / "accept".\n'));
619
+ console.log(c.bold('\nManual mode — await response, then type a counter-offer, or "exit" / "accept".\n'));
602
620
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
603
621
  rl.on('line', async (cmd) => {
604
622
  if (cmd === 'exit') {
@@ -612,6 +630,10 @@ program
612
630
  if (!isNaN(price)) {
613
631
  await core.sendCounter(sessionId, price, 'Counter offer');
614
632
  saveSessionState(sessionId, core);
633
+ } else {
634
+ // Allows sending non-numeric replies if needed
635
+ await core.sendCounter(sessionId, 0, cmd);
636
+ saveSessionState(sessionId, core);
615
637
  }
616
638
  }
617
639
  });
@@ -665,7 +687,7 @@ program
665
687
  core.on('callback_received', ({ id }) => saveSessionState(id, core));
666
688
  core.on('session_closed', ({ outcome, finalPrice }) => {
667
689
  saveSessionState(sessionId, core);
668
- if (outcome === 'deal') console.log(c.green(c.bold(`\nšŸŽ‰ DEAL SECURED at $${finalPrice}`)));
690
+ if (outcome === 'deal') console.log(c.green(c.bold(`\nšŸŽ‰ AGREEMENT SECURED at $${finalPrice}`)));
669
691
  process.exit(0);
670
692
  });
671
693
 
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
 
8
8
  "name": "agent-clinch",
9
- "version": "0.7.7",
9
+ "version": "0.7.8",
10
10
  "description": "Clinch Protocol CLI — agent negotiation from your terminal",
11
11
  "main": "cli.js",
12
12
  "bin": {