nodal-agents 0.3.7 → 0.3.9

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 (100) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/runner.js +72 -3
  4. package/web/.next/BUILD_ID +1 -1
  5. package/web/.next/app-path-routes-manifest.json +1 -1
  6. package/web/.next/build-manifest.json +2 -2
  7. package/web/.next/server/app/(dashboard)/agents/[id]/edit/page.js +2 -2
  8. package/web/.next/server/app/(dashboard)/agents/[id]/edit/page.js.nft.json +1 -1
  9. package/web/.next/server/app/(dashboard)/agents/[id]/edit/page_client-reference-manifest.js +1 -1
  10. package/web/.next/server/app/(dashboard)/agents/[id]/telegram/page.js +1 -1
  11. package/web/.next/server/app/(dashboard)/agents/[id]/telegram/page_client-reference-manifest.js +1 -1
  12. package/web/.next/server/app/(dashboard)/agents/page.js +1 -1
  13. package/web/.next/server/app/(dashboard)/agents/page_client-reference-manifest.js +1 -1
  14. package/web/.next/server/app/(dashboard)/approvals/page.js +1 -1
  15. package/web/.next/server/app/(dashboard)/approvals/page.js.nft.json +1 -1
  16. package/web/.next/server/app/(dashboard)/approvals/page_client-reference-manifest.js +1 -1
  17. package/web/.next/server/app/(dashboard)/automations/page.js +1 -1
  18. package/web/.next/server/app/(dashboard)/automations/page_client-reference-manifest.js +1 -1
  19. package/web/.next/server/app/(dashboard)/billing/page.js +1 -1
  20. package/web/.next/server/app/(dashboard)/billing/page_client-reference-manifest.js +1 -1
  21. package/web/.next/server/app/(dashboard)/connectors/page.js +1 -1
  22. package/web/.next/server/app/(dashboard)/connectors/page.js.nft.json +1 -1
  23. package/web/.next/server/app/(dashboard)/connectors/page_client-reference-manifest.js +1 -1
  24. package/web/.next/server/app/(dashboard)/credentials/page.js +1 -1
  25. package/web/.next/server/app/(dashboard)/credentials/page.js.nft.json +1 -1
  26. package/web/.next/server/app/(dashboard)/credentials/page_client-reference-manifest.js +1 -1
  27. package/web/.next/server/app/(dashboard)/jobs/[id]/page.js +1 -1
  28. package/web/.next/server/app/(dashboard)/jobs/[id]/page_client-reference-manifest.js +1 -1
  29. package/web/.next/server/app/(dashboard)/jobs/page.js +1 -1
  30. package/web/.next/server/app/(dashboard)/jobs/page.js.nft.json +1 -1
  31. package/web/.next/server/app/(dashboard)/jobs/page_client-reference-manifest.js +1 -1
  32. package/web/.next/server/app/(dashboard)/llm-providers/page.js +2 -2
  33. package/web/.next/server/app/(dashboard)/llm-providers/page_client-reference-manifest.js +1 -1
  34. package/web/.next/server/app/(dashboard)/logs/page.js +2 -2
  35. package/web/.next/server/app/(dashboard)/logs/page_client-reference-manifest.js +1 -1
  36. package/web/.next/server/app/(dashboard)/mcp/page.js +1 -1
  37. package/web/.next/server/app/(dashboard)/mcp/page_client-reference-manifest.js +1 -1
  38. package/web/.next/server/app/(dashboard)/memories/page.js +2 -2
  39. package/web/.next/server/app/(dashboard)/memories/page_client-reference-manifest.js +1 -1
  40. package/web/.next/server/app/(dashboard)/page.js +1 -1
  41. package/web/.next/server/app/(dashboard)/page.js.nft.json +1 -1
  42. package/web/.next/server/app/(dashboard)/page_client-reference-manifest.js +1 -1
  43. package/web/.next/server/app/(dashboard)/settings/page.js +1 -1
  44. package/web/.next/server/app/(dashboard)/settings/page_client-reference-manifest.js +1 -1
  45. package/web/.next/server/app/(dashboard)/skills/[id]/edit/page.js +1 -1
  46. package/web/.next/server/app/(dashboard)/skills/[id]/edit/page.js.nft.json +1 -1
  47. package/web/.next/server/app/(dashboard)/skills/[id]/edit/page_client-reference-manifest.js +1 -1
  48. package/web/.next/server/app/(dashboard)/skills/new/page.js +1 -1
  49. package/web/.next/server/app/(dashboard)/skills/new/page.js.nft.json +1 -1
  50. package/web/.next/server/app/(dashboard)/skills/new/page_client-reference-manifest.js +1 -1
  51. package/web/.next/server/app/(dashboard)/skills/page.js +2 -2
  52. package/web/.next/server/app/(dashboard)/skills/page_client-reference-manifest.js +1 -1
  53. package/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  54. package/web/.next/server/app/_global-error.html +1 -1
  55. package/web/.next/server/app/_global-error.rsc +1 -1
  56. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  57. package/web/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  58. package/web/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  59. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  60. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  61. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  62. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  63. package/web/.next/server/app/_not-found.html +1 -1
  64. package/web/.next/server/app/_not-found.rsc +1 -1
  65. package/web/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  66. package/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  67. package/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  68. package/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  69. package/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  70. package/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  71. package/web/.next/server/app/login/page_client-reference-manifest.js +1 -1
  72. package/web/.next/server/app/onboarding/page_client-reference-manifest.js +1 -1
  73. package/web/.next/server/app/onboarding.html +1 -1
  74. package/web/.next/server/app/onboarding.rsc +1 -1
  75. package/web/.next/server/app/onboarding.segments/_full.segment.rsc +1 -1
  76. package/web/.next/server/app/onboarding.segments/_head.segment.rsc +1 -1
  77. package/web/.next/server/app/onboarding.segments/_index.segment.rsc +1 -1
  78. package/web/.next/server/app/onboarding.segments/_tree.segment.rsc +1 -1
  79. package/web/.next/server/app/onboarding.segments/onboarding/__PAGE__.segment.rsc +1 -1
  80. package/web/.next/server/app/onboarding.segments/onboarding.segment.rsc +1 -1
  81. package/web/.next/server/app-paths-manifest.json +1 -1
  82. package/web/.next/server/chunks/1248.js +1 -0
  83. package/web/.next/server/chunks/3362.js +1 -0
  84. package/web/.next/server/chunks/5527.js +1 -0
  85. package/web/.next/server/chunks/7741.js +2 -2
  86. package/web/.next/server/chunks/8193.js +1 -0
  87. package/web/.next/server/middleware-build-manifest.js +1 -1
  88. package/web/.next/server/pages/404.html +1 -1
  89. package/web/.next/server/pages/500.html +1 -1
  90. package/web/.next/server/server-reference-manifest.js +1 -1
  91. package/web/.next/server/server-reference-manifest.json +1 -1
  92. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/edit/{page-daa833e779c2b465.js → page-d3724fbf38b71806.js} +1 -1
  93. package/web/.next/static/chunks/app/(dashboard)/mcp/page-3fa9d4448a31b696.js +1 -0
  94. package/web/.next/server/chunks/319.js +0 -1
  95. package/web/.next/server/chunks/3786.js +0 -1
  96. package/web/.next/server/chunks/6184.js +0 -1
  97. package/web/.next/server/chunks/9977.js +0 -1
  98. package/web/.next/static/chunks/app/(dashboard)/mcp/page-efb99104821983ce.js +0 -1
  99. /package/web/.next/static/{0Ja3V81w-79oY4P3asE4L → 8MmUGL73Wv8U4wr-eDBt8}/_buildManifest.js +0 -0
  100. /package/web/.next/static/{0Ja3V81w-79oY4P3asE4L → 8MmUGL73Wv8U4wr-eDBt8}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -25,7 +25,7 @@ no cloud roundtrip.** Runs on any machine with Node 22+ — Mac, PC, Linux.
25
25
  | 🗂️  **Workspaces** | Multiple isolated workspaces on one install (personal vs work) — each with its own agents, skills, connectors, jobs and memory. Switch from the sidebar. |
26
26
  | 🤖  **Self-extending (ROOT agent)** | Designate an orchestrator as ROOT and let it create skills/agents and assign them on your behalf — gated by per-grant toggles and an autonomy level (propose-confirm → fully-autonomous). |
27
27
  | 📄  **Office files** | Read + edit Excel in place, create Word & PowerPoint, inside the agent's workspace — gated behind the office-editing skill. |
28
- | 📡  **MCP support** | Connect MCP servers over Streamable HTTP *and* stdio (local subprocess) — per-job tool discovery, tool whitelisting, multi-instance. |
28
+ | 📡  **MCP support** | Connect MCP servers over Streamable HTTP *and* stdio (local subprocess) — a growing catalogue (Stripe, n8n, Supabase, Airtable, Notion…) plus add *and edit* your own custom servers from the UI. Per-job tool discovery, tool whitelisting, multi-instance. |
29
29
  | 💬 &nbsp;**Telegram out of the box** | Long-polling, multi-agent routing (`/ask <slug>`), group-chat filters, conversation continuity, delegation gracefulness on Telegram. |
30
30
  | ⚙️ &nbsp;**Real engineering** | TypeScript strict, dependency-cruiser-enforced architecture, full unit + integration suite, Playwright e2e, idempotent migrations, encryption at rest for keys. |
31
31
 
@@ -134,7 +134,7 @@ Delegations create child jobs that resume the parent on completion.
134
134
  | `/agents` | Create, edit, assign skills + connectors + MCP servers to agents. |
135
135
  | `/jobs` | Live job stream — task, agent, status, full transcript, tool I/O. |
136
136
  | `/connectors` | Active connector instances + Marketplace (multi-instance, OAuth or API-key). |
137
- | `/mcp` | Active MCP servers + Marketplace (HTTP transport). |
137
+ | `/mcp` | Active MCP servers + Marketplace HTTP & stdio, a growing catalogue, plus add/edit your own custom servers. |
138
138
  | `/memories` | Persistent facts per entity — search, edit, archive. |
139
139
  | `/skills` | Assigned / Custom / Built-in Library tabs — reusable instructions appended to an agent's prompt; create your own or customise built-ins. |
140
140
  | `/logs` | Tool-call audit — input/output JSON per call, filterable by tool name. |
@@ -204,7 +204,7 @@ pnpm deps:check # runs locally and in CI before every release
204
204
 
205
205
  ## Status
206
206
 
207
- **Current release:** `0.3.7` on npm `latest`. Used daily by the
207
+ **Current release:** `0.3.9` on npm `latest`. Used daily by the
208
208
  maintainer, stable enough for personal production. Pre-1.0 — breaking
209
209
  changes are still possible between minors.
210
210
 
@@ -218,13 +218,15 @@ changes are still possible between minors.
218
218
  caps
219
219
  - Multi-instance connectors with OAuth (Gmail, Drive, Sheets, Docs, Notion,
220
220
  Airtable) and API-key (Notion, Airtable, Apify, Firecrawl, Tavily)
221
- - MCP catalog — Streamable HTTP *and* stdio (local subprocess) servers, API-key auth
221
+ - MCP catalog — Streamable HTTP *and* stdio (local subprocess) servers, API-key auth; a growing catalogue (Stripe, n8n, Supabase, Airtable, Notion…) with a "test pending" badge on entries not yet verified live, plus add *and edit* your own custom HTTP/stdio servers from the dashboard
222
222
  - Top-level workspaces — multiple isolated entities (agents/skills/connectors/jobs/memory per workspace), switch in the sidebar
223
223
  - ROOT agent — designate an orchestrator that can create skills/agents and assign them, gated by per-grant toggles + an autonomy/approval level
224
224
  - Office file editing — Excel in-place edit, Word/PowerPoint create, in the agent workspace (office-editing skill)
225
225
  - Multiple filesystem folders per agent (sandboxed `file_*` tools)
226
226
  - Telegram delivery (long-poll, group filters, multi-agent routing,
227
- delegation gracefulness)
227
+ delegation gracefulness) — exactly-once delivery contract: anti-spam guard
228
+ against runaway message loops + a guard that re-prompts (then fails loud)
229
+ rather than completing a job without ever replying
228
230
  - Approval gates for risky tools (execute-the-approved-action on resume)
229
231
  - Cron scheduling
230
232
  - `nodal-agents update` — one-command upgrade + boot version notice
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodal-agents",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Local-first AI agent platform with a web dashboard — install in one command.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/runner.js CHANGED
@@ -13282,6 +13282,12 @@ Wrong (4 LLM round-trips for the same outcome):
13282
13282
  response 3: [{ telegram_send_message: part3 }]
13283
13283
  response 4: [{ return_result: ... }]
13284
13284
 
13285
+ **Stop when you're done**: once you have sent your reply, call \`return_result\`
13286
+ to end your turn. Do NOT keep sending standalone acknowledgements, follow-ups, or
13287
+ emoji-only messages turn after turn \u2014 the user did not ask for them and the
13288
+ platform will cut you off for spamming if you send on several turns in a row
13289
+ without finishing.
13290
+
13285
13291
  Fail conditions:
13286
13292
  - If no chatId is provided and the current job has no origin chat, the tool throws
13287
13293
  \`telegram_no_recipient\`. This is intentional \u2014 do not guess a chat ID.
@@ -25721,7 +25727,12 @@ function mcpToolToToolDefinition(client, mcpTool, slug) {
25721
25727
  const detail = extractText(result.content);
25722
25728
  throw new Error(`MCP tool ${originalName} failed: ${detail || "unknown error"}`);
25723
25729
  }
25724
- return result.content;
25730
+ if (result.structuredContent != null) return result.structuredContent;
25731
+ const content = result.content ?? [];
25732
+ if (Array.isArray(content) && content.length > 0 && content.every((c) => c.type === "text")) {
25733
+ return extractText(content);
25734
+ }
25735
+ return content;
25725
25736
  }
25726
25737
  };
25727
25738
  }
@@ -25792,8 +25803,9 @@ var DEFAULT_LIMITS = {
25792
25803
  maxChains: 15,
25793
25804
  maxToolCallsPerTurn: 50,
25794
25805
  maxDelegationDepth: 3,
25795
- maxTurns: 50
25806
+ maxTurns: 50,
25796
25807
  // matches Hermes Agent's per-subagent iteration budget; cumulative cap across resumes
25808
+ maxConsecutiveDeliveryTurns: 3
25797
25809
  };
25798
25810
  var ChainCounters = class _ChainCounters {
25799
25811
  constructor(limits = DEFAULT_LIMITS) {
@@ -26803,6 +26815,8 @@ function totalChars(blocks) {
26803
26815
 
26804
26816
  // src/job/execute.ts
26805
26817
  var MAX_TOOL_RESULT_CHARS = 5e4;
26818
+ var DELIVERY_TOOL_NAMES = /* @__PURE__ */ new Set(["telegram_send_message"]);
26819
+ var TOOL_ONLY_DELIVERY_CHANNELS = /* @__PURE__ */ new Set(["telegram"]);
26806
26820
  function truncateForContext(value) {
26807
26821
  if (value.length <= MAX_TOOL_RESULT_CHARS) return value;
26808
26822
  const dropped = value.length - MAX_TOOL_RESULT_CHARS;
@@ -27204,6 +27218,12 @@ async function executeJob(jobId, deps, _runnerEnv) {
27204
27218
  }
27205
27219
  const MAX_EMPTY_TURN_RETRIES = 2;
27206
27220
  let emptyTurnRetries = 0;
27221
+ let consecutiveDeliveryOnlyTurns = 0;
27222
+ const requiresToolDelivery = TOOL_ONLY_DELIVERY_CHANNELS.has(job.channel ?? "");
27223
+ const MAX_TELEGRAM_REDELIVERY_NUDGES = 2;
27224
+ let telegramRedeliveryNudges = 0;
27225
+ let telegramDelivered = false;
27226
+ const deliveryNudge = "[syst\xE8me] Tu es sur Telegram. Tu n'as pas encore livr\xE9 ta r\xE9ponse \xE0 l'utilisateur. Appelle `telegram_send_message` avec ta r\xE9ponse, PUIS `return_result`. Ne r\xE9ponds pas en texte simple \u2014 sur Telegram, seul un message envoy\xE9 via `telegram_send_message` est visible par l'utilisateur.";
27207
27227
  try {
27208
27228
  while (true) {
27209
27229
  turn += 1;
@@ -27243,6 +27263,13 @@ async function executeJob(jobId, deps, _runnerEnv) {
27243
27263
  textLen: (response.text ?? "").length,
27244
27264
  usage: { in: promptT, out: completionT }
27245
27265
  });
27266
+ const isDeliveryOnlyTurn = rawToolCalls.length > 0 && rawToolCalls.every((tc) => DELIVERY_TOOL_NAMES.has(tc.toolName));
27267
+ consecutiveDeliveryOnlyTurns = isDeliveryOnlyTurn ? consecutiveDeliveryOnlyTurns + 1 : 0;
27268
+ if (consecutiveDeliveryOnlyTurns > DEFAULT_LIMITS.maxConsecutiveDeliveryTurns) {
27269
+ trace("delivery_spam_guard", { turn, consecutiveDeliveryOnlyTurns });
27270
+ await failJob(db, jobId, "delivery_spam_guard", runStats());
27271
+ return { status: "failed", error: "delivery_spam_guard" };
27272
+ }
27246
27273
  const assistantMsg = {
27247
27274
  role: "assistant",
27248
27275
  content: rawToolCalls.length > 0 ? rawToolCalls.map((tc) => ({
@@ -27258,6 +27285,21 @@ async function executeJob(jobId, deps, _runnerEnv) {
27258
27285
  trace("no_tool_calls_branch", { turn, hasText: Boolean(response.text) });
27259
27286
  const textContent = response.text ?? "";
27260
27287
  if (textContent) {
27288
+ if (requiresToolDelivery && !telegramDelivered) {
27289
+ if (telegramRedeliveryNudges < MAX_TELEGRAM_REDELIVERY_NUDGES) {
27290
+ telegramRedeliveryNudges += 1;
27291
+ trace("telegram_redelivery_nudge", {
27292
+ turn,
27293
+ attempt: telegramRedeliveryNudges,
27294
+ via: "text_branch"
27295
+ });
27296
+ messages = [...messages, { role: "user", content: deliveryNudge }];
27297
+ continue;
27298
+ }
27299
+ trace("telegram_not_delivered", { turn, via: "text_branch" });
27300
+ await failJob(db, jobId, "telegram_not_delivered", runStats());
27301
+ return { status: "failed", error: "telegram_not_delivered" };
27302
+ }
27261
27303
  await completeJob(db, jobId, textContent, toolsUsed, runStats(), messages);
27262
27304
  return { status: "completed", result: textContent };
27263
27305
  }
@@ -27450,6 +27492,9 @@ async function executeJob(jobId, deps, _runnerEnv) {
27450
27492
  awaitingApproval = true;
27451
27493
  continue;
27452
27494
  }
27495
+ if (toolResult.outcome === "success" && DELIVERY_TOOL_NAMES.has(call.name)) {
27496
+ telegramDelivered = true;
27497
+ }
27453
27498
  toolResultBlocks.push({
27454
27499
  type: "tool-result",
27455
27500
  toolCallId: call.id,
@@ -27490,6 +27535,31 @@ async function executeJob(jobId, deps, _runnerEnv) {
27490
27535
  }
27491
27536
  if (returnResultCall) {
27492
27537
  trace("return_result_branch", { turn });
27538
+ const taskRows = await db.select({ id: agentTasks.id }).from(agentTasks).where(eq4(agentTasks.rootJobId, jobId));
27539
+ if (requiresToolDelivery && !telegramDelivered && taskRows.length === 0) {
27540
+ if (telegramRedeliveryNudges < MAX_TELEGRAM_REDELIVERY_NUDGES) {
27541
+ telegramRedeliveryNudges += 1;
27542
+ trace("telegram_redelivery_nudge", {
27543
+ turn,
27544
+ attempt: telegramRedeliveryNudges,
27545
+ via: "return_result_branch"
27546
+ });
27547
+ toolResultBlocks.push({
27548
+ type: "tool-result",
27549
+ toolCallId: returnResultCall.toolCallId,
27550
+ toolName: "return_result",
27551
+ output: toResultOutput({
27552
+ error: "deferred: tu n'as pas encore livr\xE9 ta r\xE9ponse via telegram_send_message \u2014 fais-le avant de terminer"
27553
+ })
27554
+ });
27555
+ messages = [...messages, { role: "tool", content: toolResultBlocks }];
27556
+ messages = [...messages, { role: "user", content: deliveryNudge }];
27557
+ continue;
27558
+ }
27559
+ trace("telegram_not_delivered", { turn, via: "return_result_branch" });
27560
+ await failJob(db, jobId, "telegram_not_delivered", runStats());
27561
+ return { status: "failed", error: "telegram_not_delivered" };
27562
+ }
27493
27563
  const finalResult = "";
27494
27564
  toolsUsed = [.../* @__PURE__ */ new Set([...toolsUsed, "return_result"])];
27495
27565
  toolResultBlocks.push({
@@ -27499,7 +27569,6 @@ async function executeJob(jobId, deps, _runnerEnv) {
27499
27569
  output: toResultOutput({ acknowledged: true })
27500
27570
  });
27501
27571
  messages = [...messages, { role: "tool", content: toolResultBlocks }];
27502
- const taskRows = await db.select({ id: agentTasks.id }).from(agentTasks).where(eq4(agentTasks.rootJobId, jobId));
27503
27572
  if (taskRows.length > 0) {
27504
27573
  trace("return_result_with_tasks", { taskCount: taskRows.length });
27505
27574
  await saveCheckpoint(db, jobId, {
@@ -1 +1 @@
1
- 0Ja3V81w-79oY4P3asE4L
1
+ 8MmUGL73Wv8U4wr-eDBt8
@@ -13,8 +13,8 @@
13
13
  "/(dashboard)/agents/[id]/edit/page": "/agents/[id]/edit",
14
14
  "/(dashboard)/agents/[id]/telegram/page": "/agents/[id]/telegram",
15
15
  "/(dashboard)/agents/page": "/agents",
16
- "/(dashboard)/approvals/page": "/approvals",
17
16
  "/(dashboard)/automations/page": "/automations",
17
+ "/(dashboard)/approvals/page": "/approvals",
18
18
  "/(dashboard)/connectors/page": "/connectors",
19
19
  "/(dashboard)/credentials/page": "/credentials",
20
20
  "/(dashboard)/jobs/[id]/page": "/jobs/[id]",
@@ -4,8 +4,8 @@
4
4
  ],
5
5
  "devFiles": [],
6
6
  "lowPriorityFiles": [
7
- "static/0Ja3V81w-79oY4P3asE4L/_buildManifest.js",
8
- "static/0Ja3V81w-79oY4P3asE4L/_ssgManifest.js"
7
+ "static/8MmUGL73Wv8U4wr-eDBt8/_buildManifest.js",
8
+ "static/8MmUGL73Wv8U4wr-eDBt8/_ssgManifest.js"
9
9
  ],
10
10
  "rootMainFiles": [
11
11
  "static/chunks/webpack-3741bf0a7636d65e.js",