agenthub-multiagent-mcp 1.77.0 → 1.78.0

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 (57) hide show
  1. package/dist/channel.js +26 -4
  2. package/dist/channel.js.map +1 -1
  3. package/dist/client.d.ts +37 -2
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +18 -0
  6. package/dist/client.js.map +1 -1
  7. package/dist/commands/setup-shell.js +11 -11
  8. package/dist/daemon.js +42 -1
  9. package/dist/daemon.js.map +1 -1
  10. package/dist/daemonPolicy.d.ts +23 -0
  11. package/dist/daemonPolicy.d.ts.map +1 -1
  12. package/dist/daemonPolicy.js +22 -0
  13. package/dist/daemonPolicy.js.map +1 -1
  14. package/dist/daemonPolicy.test.js +54 -1
  15. package/dist/daemonPolicy.test.js.map +1 -1
  16. package/dist/inboxHookCli.d.ts +1 -1
  17. package/dist/inboxHookCli.d.ts.map +1 -1
  18. package/dist/inboxHookCli.js +16 -3
  19. package/dist/inboxHookCli.js.map +1 -1
  20. package/dist/inboxHookCli.test.js +23 -0
  21. package/dist/inboxHookCli.test.js.map +1 -1
  22. package/dist/lib/brainstormInvites.d.ts +68 -0
  23. package/dist/lib/brainstormInvites.d.ts.map +1 -0
  24. package/dist/lib/brainstormInvites.js +143 -0
  25. package/dist/lib/brainstormInvites.js.map +1 -0
  26. package/dist/lib/brainstormInvites.test.d.ts +8 -0
  27. package/dist/lib/brainstormInvites.test.d.ts.map +1 -0
  28. package/dist/lib/brainstormInvites.test.js +102 -0
  29. package/dist/lib/brainstormInvites.test.js.map +1 -0
  30. package/dist/lib/pending-items.d.ts +7 -0
  31. package/dist/lib/pending-items.d.ts.map +1 -1
  32. package/dist/lib/pending-items.js +3 -2
  33. package/dist/lib/pending-items.js.map +1 -1
  34. package/dist/skillPackInstaller.d.ts +1 -1
  35. package/dist/skillPackInstaller.d.ts.map +1 -1
  36. package/dist/skillPackInstaller.js +56 -2
  37. package/dist/skillPackInstaller.js.map +1 -1
  38. package/dist/skillPackInstaller.test.js +113 -0
  39. package/dist/skillPackInstaller.test.js.map +1 -1
  40. package/dist/tools/brainLessons.test.d.ts +6 -0
  41. package/dist/tools/brainLessons.test.d.ts.map +1 -0
  42. package/dist/tools/brainLessons.test.js +61 -0
  43. package/dist/tools/brainLessons.test.js.map +1 -0
  44. package/dist/tools/brainstormInvites.test.d.ts +7 -0
  45. package/dist/tools/brainstormInvites.test.d.ts.map +1 -0
  46. package/dist/tools/brainstormInvites.test.js +139 -0
  47. package/dist/tools/brainstormInvites.test.js.map +1 -0
  48. package/dist/tools/index.d.ts.map +1 -1
  49. package/dist/tools/index.js +224 -12
  50. package/dist/tools/index.js.map +1 -1
  51. package/dist/tools/tools.test.js +122 -2
  52. package/dist/tools/tools.test.js.map +1 -1
  53. package/package.json +79 -79
  54. package/skills/catalog.json +6 -4
  55. package/skills/manifest.json +4 -2
  56. package/skills/skills/inbox/SKILL.md +25 -3
  57. package/skills/skills/multiagent-brainstorm/SKILL.md +26 -6
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * MCP tool definitions and handlers
3
3
  */
4
+ import { AgentHubApiError, } from "../client.js";
4
5
  import { resolveAgentIdentity, missingStateGuidance, credentialIsDead, } from "../agentIdentity.js";
5
6
  import { boundInbox, resolveInboxLimit, CHECK_INBOX_DEFAULT_LIMIT, CHECK_INBOX_MAX_LIMIT, } from "./inboxBounds.js";
6
7
  import * as state from "../state.js";
@@ -20,6 +21,7 @@ import { isWhatsAppTool, runWhatsAppTool, whatsappToolDefinitions } from "./what
20
21
  import { installPackFromArchive } from "../skillPackInstaller.js";
21
22
  import { getServerTime, useTimeInjection } from "../lib/server-time.js";
22
23
  import { getPendingItems } from "../lib/pending-items.js";
24
+ import { brainstormAutoAcceptEnabled, inviteGuidance, } from "../lib/brainstormInvites.js";
23
25
  import { ENG_IC_CHARTER } from "../employee.js";
24
26
  import { promises as fsp } from "node:fs";
25
27
  import * as os from "node:os";
@@ -84,6 +86,19 @@ function backgroundSync(client, agentId, workingDir) {
84
86
  }
85
87
  })();
86
88
  }
89
+ /**
90
+ * The invite fields every pending-items block carries (check_inbox and the
91
+ * tool wrapper). The guidance rides along only when there is an invite, so an
92
+ * empty inbox costs no extra text.
93
+ */
94
+ function inviteFields(invites) {
95
+ const list = invites ?? [];
96
+ return {
97
+ pending_brainstorm_invites: list,
98
+ pending_brainstorm_invites_count: list.length,
99
+ ...(list.length > 0 ? { brainstorm_invite_guidance: inviteGuidance(brainstormAutoAcceptEnabled()) } : {}),
100
+ };
101
+ }
87
102
  /**
88
103
  * Wraps a tool response with pending messages and tasks
89
104
  * This enables automatic delivery without extra API calls
@@ -161,6 +176,7 @@ async function wrapWithPendingItems(client, agentId, result, context) {
161
176
  pending_messages_count: messages.length,
162
177
  pending_tasks: tasks,
163
178
  pending_tasks_count: tasks.length,
179
+ ...inviteFields(cachedItems.invites),
164
180
  urgent_action: urgentAction,
165
181
  };
166
182
  if (useTimeInjection()) {
@@ -977,7 +993,7 @@ export function registerTools() {
977
993
  },
978
994
  {
979
995
  name: "skill_pack_install",
980
- description: "Install an opt-in skill pack into ~/.claude/skills/. Hash-safe (never overwrites files you've edited unless force=true). Destructive packs (e.g. cybersecurity, which ships offensive PoC scripts) require the skill-pack:<name> capability attached to this agent. Returns a per-file install report.",
996
+ description: "Install an opt-in skill pack into ~/.claude/skills/. Hash-safe (never overwrites files you've edited unless force=true). On upgrade, files the new version dropped are removed if unedited (removed) and kept if you edited them (kept_modified). Destructive packs (e.g. cybersecurity, which ships offensive PoC scripts) require the skill-pack:<name> capability attached to this agent. Returns a per-file install report.",
981
997
  inputSchema: {
982
998
  type: "object",
983
999
  properties: {
@@ -990,6 +1006,52 @@ export function registerTools() {
990
1006
  required: ["pack"],
991
1007
  },
992
1008
  },
1009
+ // add-product-lifecycle-pack §2.1–2.2 — product-lifecycle hard gates.
1010
+ // 503 lifecycle_gates_disabled while AGENTHUB_LIFECYCLE_GATES_ENABLED is off.
1011
+ {
1012
+ name: "lifecycle_gate_request",
1013
+ description: "Open a product-lifecycle hard gate (stage 3, 6, 9 or dvt) on the stage's gate task. Creates a blocking lifecycle_gate review that only an org owner or admin can sign, on the dashboard (/reviews) — not an agent, an API key or a chat button. Returns the review id and status; poll lifecycle_gate_list until it is decided. A second pending gate on the same task is refused (gate_pending). For a re-gate after a stale or revised one, pass parent_review_id.",
1014
+ inputSchema: {
1015
+ type: "object",
1016
+ properties: {
1017
+ project_id: { type: "string", description: "Project id (defaults to the project this agent is bound to)" },
1018
+ task_id: { type: "string", description: "The stage's gate task id (must be in project_id)" },
1019
+ stage: { type: "string", enum: ["3", "6", "9", "dvt"], description: "Hard-gate stage" },
1020
+ title: { type: "string", description: "Gate title" },
1021
+ content: { type: "string", description: "The gate pack, as markdown" },
1022
+ evidence_hash: { type: "string", description: "sha256 of the evidence, 64 lowercase hex characters" },
1023
+ items: {
1024
+ type: "array",
1025
+ description: "The stage's tasks and their verdicts",
1026
+ items: {
1027
+ type: "object",
1028
+ properties: {
1029
+ task_id: { type: "string" },
1030
+ status: { type: "string" },
1031
+ verdict: { type: "string" },
1032
+ refs: { type: "array", items: { type: "string" }, description: "Evidence refs (PRs, docs, runs)" },
1033
+ },
1034
+ required: ["task_id", "status", "verdict"],
1035
+ },
1036
+ },
1037
+ parent_review_id: { type: "string", description: "Earlier lifecycle_gate on the same task this one supersedes" },
1038
+ },
1039
+ required: ["task_id", "stage", "title", "content", "evidence_hash", "items"],
1040
+ },
1041
+ },
1042
+ {
1043
+ name: "lifecycle_gate_list",
1044
+ description: "List a project's product-lifecycle hard gates, newest first, with their parsed metadata (stage, evidence hash, items) and status (pending/approved/denied/…). Filter by stage, task_id or status.",
1045
+ inputSchema: {
1046
+ type: "object",
1047
+ properties: {
1048
+ project_id: { type: "string", description: "Project id (defaults to the project this agent is bound to)" },
1049
+ stage: { type: "string", enum: ["3", "6", "9", "dvt"], description: "Only this stage" },
1050
+ task_id: { type: "string", description: "Only gates on this task" },
1051
+ status: { type: "string", description: "Only this review status, e.g. pending" },
1052
+ },
1053
+ },
1054
+ },
993
1055
  {
994
1056
  name: "api_search",
995
1057
  description: "Search the API knowledge graph — endpoints (REST/MCP/GraphQL/gRPC) extracted from indexed repos across the org. Use this BEFORE building a new endpoint to check if it already exists or to match a sibling project's contract. Org-wide read.",
@@ -1141,6 +1203,21 @@ export function registerTools() {
1141
1203
  required: ["channel"],
1142
1204
  },
1143
1205
  },
1206
+ {
1207
+ name: "room_history",
1208
+ description: "Read a room's (channel's) earlier posts, oldest first, paged — e.g. after joining late. " +
1209
+ "Pass next_since from the previous page as `since` to read on. Reading marks nothing read. " +
1210
+ "Rooms belong to your organization; another org's room is 'not found'.",
1211
+ inputSchema: {
1212
+ type: "object",
1213
+ properties: {
1214
+ room: { type: "string", description: "Room (channel) name" },
1215
+ since: { type: "number", description: "Return posts after this message seq (default 0: from the start)" },
1216
+ limit: { type: "number", description: "Posts per page (default 50, at most 200)" },
1217
+ },
1218
+ required: ["room"],
1219
+ },
1220
+ },
1144
1221
  {
1145
1222
  name: "leave_channel",
1146
1223
  description: "Unsubscribe from a channel",
@@ -1779,13 +1856,18 @@ export function registerTools() {
1779
1856
  required: ["topic"],
1780
1857
  },
1781
1858
  },
1859
+ {
1860
+ name: "brainstorm_invites",
1861
+ description: "List your pending brainstorm invites (session id, display number, topic, initiator). An invite is not chat: bring it to your human before brainstorm_accept unless they asked for that brainstorm or AGENTHUB_BRAINSTORM_AUTO_ACCEPT=true. The same list rides on check_inbox and every tool response as pending_brainstorm_invites. (requires agent_register first)",
1862
+ inputSchema: { type: "object", properties: {} },
1863
+ },
1782
1864
  {
1783
1865
  name: "brainstorm_accept",
1784
- description: "Accept a brainstorm invite (transition the session to active). Idempotent — a redelivered invite can't double-join. You are the responder. (requires agent_register first)",
1866
+ description: "Accept a brainstorm invite (transition the session to active). Idempotent — a redelivered invite can't double-join. You are the responder. Unless your human asked for this brainstorm or AGENTHUB_BRAINSTORM_AUTO_ACCEPT=true, ask them first. (requires agent_register first)",
1785
1867
  inputSchema: {
1786
1868
  type: "object",
1787
1869
  properties: {
1788
- session_id: { type: "string", description: "The session id from the invite message." },
1870
+ session_id: { type: "string", description: "The session id, from pending_brainstorm_invites / brainstorm_invites (or a legacy invite message)." },
1789
1871
  },
1790
1872
  required: ["session_id"],
1791
1873
  },
@@ -2372,10 +2454,14 @@ export function registerTools() {
2372
2454
  query: { type: "string", description: "Search query" },
2373
2455
  category: {
2374
2456
  type: "string",
2375
- enum: ["decision", "convention", "architecture", "blocker", "alert", "insight"],
2457
+ enum: ["decision", "convention", "architecture", "blocker", "alert", "insight", "lesson"],
2376
2458
  description: "Filter by knowledge category",
2377
2459
  },
2378
2460
  project: { type: "string", description: "Filter by project" },
2461
+ tag: {
2462
+ type: "string",
2463
+ description: "Exact tag filter: matches a row only when one of its tags equals this string (e.g. `stage:6` does not match `stage:60`)",
2464
+ },
2379
2465
  limit: { type: "number", description: "Max results (default 20)" },
2380
2466
  },
2381
2467
  required: ["query"],
@@ -2383,20 +2469,38 @@ export function registerTools() {
2383
2469
  },
2384
2470
  {
2385
2471
  name: "brain_write_knowledge",
2386
- description: "Write an org knowledge fact (decision, convention, architecture insight, blocker, or alert) to the shared org brain. The result carries `check`: `created` for a new row (with its `id`), or `deduped` when an identical active fact already exists — then `canonical_id` names the existing row if it is your organization's, and nothing is named if the match belongs to another organization. A refusal (missing fact, unknown category) is an error.",
2472
+ description: "Write an org knowledge fact (decision, convention, architecture insight, blocker, or alert) to the shared org brain. The result carries `check`: `created` for a new row (with its `id`), or `deduped` when an identical active fact already exists — then `canonical_id` names the existing row. Dedup matches only your organization's own facts: identical text held by another organization is stored as your own new row. A refusal (missing fact, unknown category) is an error.",
2387
2473
  inputSchema: {
2388
2474
  type: "object",
2389
2475
  properties: {
2390
2476
  category: {
2391
2477
  type: "string",
2392
- enum: ["decision", "convention", "architecture", "blocker", "alert", "insight"],
2478
+ enum: ["decision", "convention", "architecture", "blocker", "alert", "insight", "lesson"],
2393
2479
  description: "Knowledge category",
2394
2480
  },
2395
- fact: { type: "string", description: "The fact or decision (concise)" },
2481
+ fact: { type: "string", description: "The fact or decision (concise). Required except for a lesson, whose fact defaults to `LESSON s<stage>: <rule>`." },
2396
2482
  detail: { type: "string", description: "Extended context or rationale" },
2397
2483
  project: { type: "string", description: "Project this applies to (optional)" },
2484
+ tags: {
2485
+ type: "array",
2486
+ items: { type: "string" },
2487
+ description: "Tags for the exact tag filter (e.g. [\"lesson\",\"stage:6\"]). A lesson always also gets lesson, stage:<n>, track:<t>, product:<p>.",
2488
+ },
2489
+ lesson: {
2490
+ type: "object",
2491
+ description: "Required for category=lesson (400 invalid_lesson otherwise). All six fields are non-empty strings.",
2492
+ properties: {
2493
+ stage: { type: "string", description: "\"0\"–\"10\", \"HW-POC\", \"HW-EVT\", \"HW-DVT\", \"HW-PVT\" or \"HW-CERT\"" },
2494
+ product: { type: "string" },
2495
+ track: { type: "string", enum: ["gov", "commercial", "hardware", "any"] },
2496
+ rule: { type: "string", description: "One imperative sentence" },
2497
+ evidence: { type: "string", description: "A verifiable reference" },
2498
+ gate_kind: { type: "string", enum: ["gate item", "template field", "stage instruction"] },
2499
+ },
2500
+ required: ["stage", "product", "track", "rule", "evidence", "gate_kind"],
2501
+ },
2398
2502
  },
2399
- required: ["category", "fact"],
2503
+ required: ["category"],
2400
2504
  },
2401
2505
  },
2402
2506
  {
@@ -3233,6 +3337,23 @@ function brainstormError(tool, result) {
3233
3337
  message: result.message || `${tool} failed: ${result.error} (HTTP ${result.status})`,
3234
3338
  };
3235
3339
  }
3340
+ // lifecycleGateProject resolves a lifecycle gate tool's project: the argument,
3341
+ // else the project this agent is bound to (add-product-lifecycle-pack §2.1).
3342
+ function lifecycleGateProject(args, context) {
3343
+ if (typeof args.project_id === "string" && args.project_id !== "")
3344
+ return args.project_id;
3345
+ return state.loadState(context.getWorkingDir())?.project_id ?? "";
3346
+ }
3347
+ // lifecycleGateError passes a server refusal through with its code and status,
3348
+ // and says in plain words what a 503 lifecycle_gates_disabled means.
3349
+ function lifecycleGateError(err) {
3350
+ if (!(err instanceof AgentHubApiError))
3351
+ throw err;
3352
+ const message = err.code === "lifecycle_gates_disabled"
3353
+ ? "Lifecycle gates are switched off on this AgentHub server (AGENTHUB_LIFECYCLE_GATES_ENABLED is not true). No gate can be opened or listed until an operator turns it on; existing gates stay readable and decidable on /reviews."
3354
+ : err.message;
3355
+ return { ok: false, error: err.code || "request_failed", status: err.status, message };
3356
+ }
3236
3357
  export async function handleToolCall(name, args, client, context) {
3237
3358
  const agentId = context.getCurrentAgentId();
3238
3359
  // Session steering + takeover (add-multiplayer-sessions §4.7, §5.9). Handled
@@ -3972,7 +4093,7 @@ export async function handleToolCall(name, args, client, context) {
3972
4093
  throw new Error("Not registered. Call agent_register first.");
3973
4094
  const inboxLimit = resolveInboxLimit(args.limit);
3974
4095
  const raw = await client.getInbox(agentId, args.unread_only, inboxLimit);
3975
- return boundInbox(raw);
4096
+ return { ...boundInbox(raw), ...inviteFields(raw.pending_brainstorm_invites) };
3976
4097
  }
3977
4098
  case "mark_read": {
3978
4099
  const result = await client.markRead(args.message_id);
@@ -4118,6 +4239,12 @@ export async function handleToolCall(name, args, client, context) {
4118
4239
  const result = await client.joinChannel(args.channel, agentId);
4119
4240
  return wrapWithPendingItems(client, agentId, result, context);
4120
4241
  }
4242
+ case "room_history": {
4243
+ if (!agentId)
4244
+ throw new Error("Not registered. Call agent_register first.");
4245
+ const result = await client.roomHistory(args.room, args.since, args.limit);
4246
+ return wrapWithPendingItems(client, agentId, result, context);
4247
+ }
4121
4248
  case "leave_channel": {
4122
4249
  if (!agentId)
4123
4250
  throw new Error("Not registered. Call agent_register first.");
@@ -4545,6 +4672,55 @@ export async function handleToolCall(name, args, client, context) {
4545
4672
  message: `Filed bug ${result.key} (${result.severity}/${result.bug_state}): ${args.title}`,
4546
4673
  };
4547
4674
  }
4675
+ case "lifecycle_gate_request": {
4676
+ const projectId = lifecycleGateProject(args, context);
4677
+ if (!projectId) {
4678
+ return {
4679
+ ok: false,
4680
+ error: "invalid_args",
4681
+ message: "project_id is required (or bind this agent to a project with agent_register).",
4682
+ };
4683
+ }
4684
+ const body = { project_id: projectId };
4685
+ for (const k of ["task_id", "stage", "title", "content", "evidence_hash", "items", "parent_review_id"]) {
4686
+ if (args[k] !== undefined)
4687
+ body[k] = args[k];
4688
+ }
4689
+ try {
4690
+ const review = await client.post("/lifecycle/gates", body);
4691
+ return {
4692
+ ok: true,
4693
+ review_id: review.id,
4694
+ status: review.status,
4695
+ review,
4696
+ message: `Opened lifecycle gate ${review.id} (stage ${String(args.stage)}, ${review.status}). An org owner or admin signs it on /reviews.`,
4697
+ };
4698
+ }
4699
+ catch (err) {
4700
+ return lifecycleGateError(err);
4701
+ }
4702
+ }
4703
+ case "lifecycle_gate_list": {
4704
+ const projectId = lifecycleGateProject(args, context);
4705
+ if (!projectId) {
4706
+ return {
4707
+ ok: false,
4708
+ error: "invalid_args",
4709
+ message: "project_id is required (or bind this agent to a project with agent_register).",
4710
+ };
4711
+ }
4712
+ const params = new URLSearchParams({ project_id: projectId });
4713
+ for (const k of ["stage", "task_id", "status"]) {
4714
+ if (typeof args[k] === "string" && args[k] !== "")
4715
+ params.set(k, args[k]);
4716
+ }
4717
+ try {
4718
+ return await client.get(`/lifecycle/gates?${params.toString()}`);
4719
+ }
4720
+ catch (err) {
4721
+ return lifecycleGateError(err);
4722
+ }
4723
+ }
4548
4724
  case "skill_pack_list": {
4549
4725
  try {
4550
4726
  const { packs } = await client.listSkillPacks();
@@ -4612,7 +4788,8 @@ export async function handleToolCall(name, args, client, context) {
4612
4788
  const c = report.counts;
4613
4789
  const summary = `${pack} v${manifest.version}: ${c.installed} installed, ${c.updated} updated, ` +
4614
4790
  `${c.skipped_current} unchanged, ${c.skipped_user_modified} skipped (user-modified), ` +
4615
- `${c.overwritten_force} overwritten, ${c.error} errors.`;
4791
+ `${c.overwritten_force} overwritten, ${c.removed} removed (dropped by this version), ` +
4792
+ `${c.kept_modified} kept (dropped but user-modified), ${c.error} errors.`;
4616
4793
  return {
4617
4794
  ok: c.error === 0,
4618
4795
  pack,
@@ -4707,7 +4884,37 @@ export async function handleToolCall(name, args, client, context) {
4707
4884
  invites: result.data.invites,
4708
4885
  mirror_thread_id: result.data.mirror_thread_id,
4709
4886
  mirror_warning: result.data.mirror_warning,
4710
- message: `Brainstorm ${result.data.display_number} opened with ${invitees}. Invite delivered${result.data.invite_delivered === false ? " FAILED" : ""}.`,
4887
+ // separate-agent-chat-from-brainstorms §1.4: event | message |
4888
+ // event+message | none. "none" means recorded but the invitee is not
4889
+ // connected — it will see the invite in pending_brainstorm_invites.
4890
+ invite_delivery: result.data.invite_delivery,
4891
+ warning: result.data.warning,
4892
+ message: `Brainstorm ${result.data.display_number} opened with ${invitees}. ${result.data.invite_delivered === false
4893
+ ? result.data.warning
4894
+ ? "Invite delivery FAILED."
4895
+ : "Invite recorded; the invitee is not connected and will see it in pending_brainstorm_invites."
4896
+ : "Invite delivered."}`,
4897
+ };
4898
+ }
4899
+ case "brainstorm_invites": {
4900
+ // separate-agent-chat-from-brainstorms §2.1. Not wrapped: the wrapper
4901
+ // would repeat this same list as pending_brainstorm_invites.
4902
+ if (!agentId)
4903
+ throw new Error("Not registered. Call agent_register first.");
4904
+ const result = await client.brainstormInvites();
4905
+ if (!result.ok)
4906
+ return brainstormError("brainstorm_invites", result);
4907
+ const invites = Array.isArray(result.data.invites) ? result.data.invites : [];
4908
+ const autoAccept = brainstormAutoAcceptEnabled();
4909
+ return {
4910
+ ok: true,
4911
+ count: invites.length,
4912
+ invites,
4913
+ auto_accept: autoAccept,
4914
+ guidance: inviteGuidance(autoAccept),
4915
+ message: invites.length === 0
4916
+ ? "No pending brainstorm invites."
4917
+ : `${invites.length} pending brainstorm invite(s). ${autoAccept ? "Auto-accept is on." : "Ask your human before accepting any you were not asked to join."}`,
4711
4918
  };
4712
4919
  }
4713
4920
  case "brainstorm_accept": {
@@ -5266,6 +5473,8 @@ export async function handleToolCall(name, args, client, context) {
5266
5473
  brainParams.set("category", args.category);
5267
5474
  if (args.project)
5268
5475
  brainParams.set("project", args.project);
5476
+ if (args.tag)
5477
+ brainParams.set("tag", args.tag);
5269
5478
  if (args.limit)
5270
5479
  brainParams.set("limit", String(args.limit));
5271
5480
  return client.get(`/brain/knowledge?${brainParams.toString()}`);
@@ -5275,10 +5484,13 @@ export async function handleToolCall(name, args, client, context) {
5275
5484
  throw new Error("Not registered. Call agent_register first.");
5276
5485
  return client.createBrainKnowledge({
5277
5486
  category: args.category,
5278
- fact: args.fact,
5487
+ fact: args.fact || "",
5279
5488
  detail: args.detail || "",
5280
5489
  source_agent: agentId,
5281
5490
  project: args.project,
5491
+ // The server stores tags as a JSON array string.
5492
+ ...(Array.isArray(args.tags) ? { tags: JSON.stringify(args.tags) } : {}),
5493
+ ...(args.lesson ? { lesson: args.lesson } : {}),
5282
5494
  });
5283
5495
  }
5284
5496
  case "brain_get_context": {