nothumanallowed 15.1.14 → 15.1.17
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/package.json +1 -1
- package/src/cli.mjs +3 -1
- package/src/constants.mjs +1 -1
- package/src/server/routes/studio.mjs +39 -7
- package/src/services/message-responder.mjs +34 -4
- package/src/services/tool-executor.mjs +51 -6
- package/src/ui-dist/assets/{index-D1_FWACq.js → index-e_9WzUAL.js} +51 -51
- package/src/ui-dist/index.html +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "15.1.
|
|
3
|
+
"version": "15.1.17",
|
|
4
4
|
"description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/cli.mjs
CHANGED
|
@@ -55,12 +55,14 @@ export async function main(argv) {
|
|
|
55
55
|
// npm version check (non-blocking). The one-liner uses --prefer-online to
|
|
56
56
|
// bypass npm's metadata cache, which is the #1 reason `npm install -g`
|
|
57
57
|
// appears to "do nothing" — it had stale "latest" in the local cache.
|
|
58
|
+
// Every command separated by `&&` so users can copy-paste the whole line.
|
|
58
59
|
checkNpmVersion().then(result => {
|
|
59
60
|
if (result?.updateAvailable) {
|
|
60
61
|
console.log('');
|
|
61
62
|
warn(`New NHA version available: ${result.current} → ${result.latest}`);
|
|
62
63
|
info(`Run "nha update" (recommended — auto-installs npm + agents)`);
|
|
63
|
-
info(`Or
|
|
64
|
+
info(`Or copy-paste this ENTIRE line (note: --prefer-online, not --pref-online):`);
|
|
65
|
+
info(` npm cache clean --force && npm install -g nothumanallowed@${result.latest} --prefer-online && hash -r && nha version`);
|
|
64
66
|
}
|
|
65
67
|
}).catch(() => {});
|
|
66
68
|
}
|
package/src/constants.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
7
|
|
|
8
|
-
export const VERSION = '15.1.
|
|
8
|
+
export const VERSION = '15.1.17';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -759,16 +759,35 @@ ${okBlock}`;
|
|
|
759
759
|
|
|
760
760
|
tok('[Parlamento — Round 2: Cross-Reading & Refinamento] ');
|
|
761
761
|
const r2Results = [];
|
|
762
|
+
// Per-agent timeout — if any single agent stalls (network, LLM hung)
|
|
763
|
+
// the loop must not block the whole deliberation. After this budget,
|
|
764
|
+
// fall back to the agent's Round 1 output and continue.
|
|
765
|
+
const PER_AGENT_TIMEOUT_MS = 90_000;
|
|
762
766
|
for (const proposal of eligible) {
|
|
763
767
|
tok(`[Round 2: ${proposal.label || proposal.agent}] `);
|
|
764
768
|
const r2Sys = `You are ${proposal.agent}, a specialist AI agent in NHA Studio Parliament. Today is ${today}. Respond entirely in ${language}.\n\n## WORKFLOW GOAL: ${task}\n\n## YOUR ROUND 1 RESPONSE:\n${proposal.output.slice(0,3000)}\n\n## OTHER AGENTS' ROUND 1 PROPOSALS:\n${crossCtx(proposal.agent)}\n\nDELIBERATION ROUND 2 — REFINEMENT:\n1. Review the other agents' proposals carefully\n2. Incorporate valid points where you AGREE — mark with [AGREE]\n3. Flag genuine disagreements with [CONTRADICTION] and explain your reasoning with evidence\n4. Produce your COMPLETE REFINED response — thorough and exhaustive\n5. Keep analysis focused on: ${task}\n\nBe THOROUGH. Minimum 600 words of substantive refined analysis.`;
|
|
765
769
|
let r2Out = '';
|
|
770
|
+
let fellBack = false;
|
|
766
771
|
try {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
+
// Race the LLM stream against a hard timeout.
|
|
773
|
+
await Promise.race([
|
|
774
|
+
callLLMStream(config, r2Sys, 'Produce your refined Round 2 response. Write complete content under every heading — never leave a section title without body text.',
|
|
775
|
+
(t) => { r2Out += t; }, { max_tokens: 16384 }),
|
|
776
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('R2_AGENT_TIMEOUT')), PER_AGENT_TIMEOUT_MS)),
|
|
777
|
+
]);
|
|
778
|
+
} catch (err) {
|
|
779
|
+
// Fallback: use Round 1 output so the rest of deliberation can proceed.
|
|
780
|
+
fellBack = true;
|
|
781
|
+
r2Out = proposal.output;
|
|
782
|
+
tok(`[Round 2 ${proposal.label || proposal.agent}: timeout — uso Round 1 come fallback] `);
|
|
783
|
+
}
|
|
784
|
+
// If the stream returned but produced no text (LLM hiccup), also fall back.
|
|
785
|
+
if (!r2Out || r2Out.trim().length < 50) {
|
|
786
|
+
fellBack = true;
|
|
787
|
+
r2Out = proposal.output;
|
|
788
|
+
}
|
|
789
|
+
r2Results.push({ agent: proposal.agent, label: proposal.label, icon: proposal.icon, output: r2Out, fellBack });
|
|
790
|
+
sse({ deliberation_r2: { agent: proposal.agent, label: proposal.label, icon: proposal.icon, output: r2Out, fellBack } });
|
|
772
791
|
}
|
|
773
792
|
|
|
774
793
|
const r2Conv = measureConvergence(r2Results.map(r => r.output));
|
|
@@ -792,9 +811,22 @@ ${okBlock}`;
|
|
|
792
811
|
const medSys = `You are HERALD, the Parliament Mediator in NHA Studio. Today is ${today}. Respond entirely in ${language}.\n\n## WORKFLOW GOAL: ${task}\n\n## ALL AGENTS' REFINED POSITIONS (Round 2):\n${allR2Ctx}${contBlock}\n\n${medTask}\n\nCRITICAL: NEVER write a heading without immediately writing full content below it. Every section MUST have at least 5-8 concrete bullet points or detailed paragraphs. Be EXHAUSTIVE.`;
|
|
793
812
|
|
|
794
813
|
let mediationOutput = '';
|
|
814
|
+
const HERALD_TIMEOUT_MS = 120_000;
|
|
795
815
|
try {
|
|
796
|
-
await
|
|
797
|
-
|
|
816
|
+
await Promise.race([
|
|
817
|
+
callLLMStream(config, medSys, 'Produce the Parliament final synthesis. Be thorough and complete.', (t) => { mediationOutput += t; }, { max_tokens: 16384 }),
|
|
818
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('HERALD_TIMEOUT')), HERALD_TIMEOUT_MS)),
|
|
819
|
+
]);
|
|
820
|
+
} catch (err) {
|
|
821
|
+
// HERALD timed out — emit whatever we got so the client isn't stuck.
|
|
822
|
+
tok(`[HERALD mediation: timeout dopo ${HERALD_TIMEOUT_MS/1000}s — restituisco contenuto parziale] `);
|
|
823
|
+
}
|
|
824
|
+
// If absolutely nothing came back, fall back to a concatenation of the
|
|
825
|
+
// r2 outputs so the user has SOMETHING to read instead of a blank panel.
|
|
826
|
+
if (!mediationOutput || mediationOutput.trim().length < 50) {
|
|
827
|
+
mediationOutput = `# Sintesi automatica (HERALD non disponibile)\n\n` +
|
|
828
|
+
r2Results.map(r => `## ${r.label || r.agent}\n${r.output.slice(0, 2000)}`).join('\n\n---\n\n');
|
|
829
|
+
}
|
|
798
830
|
sse({ deliberation_r3: { output: mediationOutput, converged } });
|
|
799
831
|
|
|
800
832
|
clearInterval(keepalive);
|
|
@@ -278,6 +278,35 @@ async function callAgentWithTools(config, agentName, userMessage, languageOverri
|
|
|
278
278
|
|
|
279
279
|
if (actions.length === 0) {
|
|
280
280
|
finalText = textParts.join('\n').trim();
|
|
281
|
+
|
|
282
|
+
// ── HALLUCINATED SUCCESS DETECTION ────────────────────────────────
|
|
283
|
+
// The model sometimes writes "X è stato cancellato/creato/inviato con
|
|
284
|
+
// successo" WITHOUT actually emitting a tool block — pure hallucination.
|
|
285
|
+
// If that happens and we're early in the round budget, force ONE retry
|
|
286
|
+
// with an explicit instruction to emit the tool JSON. If it still
|
|
287
|
+
// refuses, replace the fake success with a warning so the user knows
|
|
288
|
+
// the action did NOT actually happen.
|
|
289
|
+
const claimsSuccess = isCompletedAction(finalText);
|
|
290
|
+
const hasReferenceToTool = /\b(eventid|event id|id evento)\b/i.test(finalText);
|
|
291
|
+
const looksFakeSuccess = claimsSuccess || (hasReferenceToTool && round === 0);
|
|
292
|
+
if (looksFakeSuccess && round < 1) {
|
|
293
|
+
// Push the bad response as history so the model sees what it just did,
|
|
294
|
+
// then ask it to actually emit the tool.
|
|
295
|
+
history.push({ role: 'assistant', content: response });
|
|
296
|
+
userMessage =
|
|
297
|
+
`STOP. Hai dichiarato che un'azione è stata completata ma NON hai emesso nessun blocco tool JSON. ` +
|
|
298
|
+
`Senza il blocco tool, NESSUNA azione viene eseguita davvero. ` +
|
|
299
|
+
`Ora emetti il blocco JSON corretto (\`\`\`json ... \`\`\`) per l'azione richiesta dall'utente, usando i parametri corretti. ` +
|
|
300
|
+
`Se ti serve un eventId, chiama prima calendar_find o calendar_date. Non scrivere altro testo finché non hai eseguito davvero il tool.`;
|
|
301
|
+
continue; // restart the loop with the corrective user message
|
|
302
|
+
}
|
|
303
|
+
if (looksFakeSuccess && round >= 1) {
|
|
304
|
+
// Second attempt also lied — refuse to forward the fake success.
|
|
305
|
+
finalText =
|
|
306
|
+
`Non sono riuscito a eseguire l'azione automaticamente (il modello ha dichiarato un successo senza eseguire il tool). ` +
|
|
307
|
+
`Riprova riformulando la richiesta, oppure dimmi esattamente cosa vuoi che faccia.`;
|
|
308
|
+
}
|
|
309
|
+
|
|
281
310
|
break;
|
|
282
311
|
}
|
|
283
312
|
|
|
@@ -547,8 +576,9 @@ class TelegramResponder {
|
|
|
547
576
|
const msg =
|
|
548
577
|
`🆕 NHA v${latest} disponibile!\n\n` +
|
|
549
578
|
`Una nuova versione di NotHumanAllowed è stata pubblicata.\n\n` +
|
|
550
|
-
`Aggiorna con
|
|
551
|
-
`
|
|
579
|
+
`Aggiorna con UN SOLO comando (copia e incolla tutto):\n` +
|
|
580
|
+
`npm cache clean --force && npm install -g nothumanallowed@latest --prefer-online && nha ops stop && nha ops start\n\n` +
|
|
581
|
+
`Importante: usa esattamente "--prefer-online" (NON "--pref-online") e tieni tutti i "&&" tra i comandi.`;
|
|
552
582
|
|
|
553
583
|
this.log(`[Telegram] Broadcasting update notification v${latest} to ${chatIds.length} users`);
|
|
554
584
|
|
|
@@ -695,7 +725,7 @@ class TelegramResponder {
|
|
|
695
725
|
return d.text || '';
|
|
696
726
|
}
|
|
697
727
|
|
|
698
|
-
throw new Error('
|
|
728
|
+
throw new Error('servizio di trascrizione vocale momentaneamente non disponibile (NHA proxy irraggiungibile)');
|
|
699
729
|
}
|
|
700
730
|
|
|
701
731
|
async _handleMessage(message) {
|
|
@@ -729,7 +759,7 @@ class TelegramResponder {
|
|
|
729
759
|
this.log(`[Telegram] Voice transcription failed: ${err.message}`);
|
|
730
760
|
await this._telegramCall('sendMessage', {
|
|
731
761
|
chat_id: chatId,
|
|
732
|
-
text: `Non riesco a trascrivere il vocale
|
|
762
|
+
text: `Non riesco a trascrivere il vocale (${err.message}).\n\nPer abilitare la trascrizione vocale gratuita, dal computer esegui:\nnha config set groqKey TUA_CHIAVE_GROQ\n\nLa chiave si ottiene gratis su https://console.groq.com/keys`,
|
|
733
763
|
});
|
|
734
764
|
return;
|
|
735
765
|
}
|
|
@@ -687,9 +687,13 @@ Never output a JSON block as a suggestion — every block executes immediately.
|
|
|
687
687
|
## ABSOLUTE RULES (violate these and the user loses trust)
|
|
688
688
|
|
|
689
689
|
1. NEVER invent tool output. If you need data (an event ID, a price, a search result), you MUST emit a tool JSON block and wait for its real response. Do NOT write fake "(eventId: 123456789)" or fake results.
|
|
690
|
-
2. NEVER
|
|
691
|
-
3. NEVER
|
|
692
|
-
4.
|
|
690
|
+
2. NEVER claim an action was completed unless you have just received a SUCCESS tool result for that action in this same turn. NEVER write "cancellato con successo", "creato con successo", "inviato" etc. without a real tool execution behind it. If you didn't emit the tool block, the action didn't happen.
|
|
691
|
+
3. NEVER duplicate. If the user says "correggi/modifica/sposta/aggiorna" referring to an item you just created in this conversation, use the corresponding *_update / *_move tool with the eventId/taskId from your previous tool response. Do NOT create a second item.
|
|
692
|
+
4. NEVER put the TITLE in the description field. The "summary" / "title" field is the SHORT name (what shows on a calendar grid). The "description" field is ONLY for extra notes.
|
|
693
|
+
5. NEVER invent times. If the user did not specify a time, ASK them ("A che ora?"). Do not default to 10:00 or any other time silently. Same for missing date, duration, attendees.
|
|
694
|
+
6. When the user confirms ("procedi", "sì", "fallo", "ok", "vai") AND there is a pending action from the IMMEDIATELY PREVIOUS assistant turn (you proposed something like "Posso cancellare X. Procedo?"), EXECUTE that exact pending action with the same parameters — do NOT search again, do NOT propose again, do NOT ask again. Emit the tool block for the action you just proposed.
|
|
695
|
+
7. When in doubt, ASK ONE concise question — do not invent details.
|
|
696
|
+
8. Tool JSON blocks MUST be wrapped in \`\`\`json ... \`\`\` fences. If you emit raw JSON without fences, the system can sometimes still parse it but it's unreliable — always use the fences.
|
|
693
697
|
|
|
694
698
|
## TOOL SIGNATURES (parameters and how to use them)
|
|
695
699
|
|
|
@@ -814,12 +818,53 @@ export function parseActions(text) {
|
|
|
814
818
|
const trailing = normalized.slice(lastIndex).trim();
|
|
815
819
|
if (trailing) textParts.push(trailing);
|
|
816
820
|
|
|
817
|
-
// Fallback: if no fenced blocks found, scan for bare {"action": ...} objects
|
|
821
|
+
// Fallback: if no fenced blocks found, scan for bare {"action": ...} objects
|
|
822
|
+
// in the text. The previous regex was naive and stopped at the first `}`,
|
|
823
|
+
// breaking on nested params like {"action":"X","params":{"summary":"Y"}}.
|
|
824
|
+
// Now we do proper brace balancing so we always capture the FULL object.
|
|
818
825
|
if (actions.length === 0) {
|
|
826
|
+
const consumed = new Set();
|
|
827
|
+
const findBareActions = (src) => {
|
|
828
|
+
let i = 0;
|
|
829
|
+
while (i < src.length) {
|
|
830
|
+
// Quick filter: only consider candidates that start with {"action"
|
|
831
|
+
const idx = src.indexOf('{"action"', i);
|
|
832
|
+
if (idx < 0) break;
|
|
833
|
+
// Walk forward balancing braces, respecting strings
|
|
834
|
+
let depth = 0;
|
|
835
|
+
let inStr = false;
|
|
836
|
+
let escape = false;
|
|
837
|
+
let end = -1;
|
|
838
|
+
for (let j = idx; j < src.length; j++) {
|
|
839
|
+
const c = src[j];
|
|
840
|
+
if (escape) { escape = false; continue; }
|
|
841
|
+
if (c === '\\' && inStr) { escape = true; continue; }
|
|
842
|
+
if (c === '"' && !escape) { inStr = !inStr; continue; }
|
|
843
|
+
if (inStr) continue;
|
|
844
|
+
if (c === '{') depth++;
|
|
845
|
+
else if (c === '}') {
|
|
846
|
+
depth--;
|
|
847
|
+
if (depth === 0) { end = j; break; }
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (end < 0) break;
|
|
851
|
+
const candidate = src.slice(idx, end + 1);
|
|
852
|
+
try {
|
|
853
|
+
const parsed = JSON.parse(candidate);
|
|
854
|
+
if (parsed.action && typeof parsed.action === 'string' && !consumed.has(candidate)) {
|
|
855
|
+
actions.push({ action: parsed.action, params: parsed.params || {} });
|
|
856
|
+
consumed.add(candidate);
|
|
857
|
+
}
|
|
858
|
+
} catch { /* not valid JSON, skip */ }
|
|
859
|
+
i = end + 1;
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
findBareActions(text);
|
|
863
|
+
// Legacy fallback kept for very loose JSON shapes — only fires if the
|
|
864
|
+
// brace scanner found nothing.
|
|
819
865
|
const bareRegex = /\{[\s\S]*?"action"\s*:\s*"[^"]+[\s\S]*?\}/g;
|
|
820
866
|
let bareMatch;
|
|
821
|
-
|
|
822
|
-
while ((bareMatch = bareRegex.exec(text)) !== null) {
|
|
867
|
+
while (actions.length === 0 && (bareMatch = bareRegex.exec(text)) !== null) {
|
|
823
868
|
try {
|
|
824
869
|
const parsed = JSON.parse(bareMatch[0]);
|
|
825
870
|
if (parsed.action && typeof parsed.action === 'string' && !consumed.has(bareMatch[0])) {
|