@indemn/cli 1.6.0 → 1.7.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 (311) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +9 -4
  3. package/.github/workflows/ci.yml +10 -0
  4. package/.github/workflows/publish.yml +2 -2
  5. package/.mcp.json +1 -4
  6. package/CONTRIBUTING.md +3 -1
  7. package/README.md +69 -176
  8. package/dist/cli/agents.d.ts.map +1 -1
  9. package/dist/cli/agents.js +226 -18
  10. package/dist/cli/agents.js.map +1 -1
  11. package/dist/cli/analytics.d.ts +3 -0
  12. package/dist/cli/analytics.d.ts.map +1 -0
  13. package/dist/cli/analytics.js +133 -0
  14. package/dist/cli/analytics.js.map +1 -0
  15. package/dist/cli/auth.d.ts.map +1 -1
  16. package/dist/cli/auth.js +62 -17
  17. package/dist/cli/auth.js.map +1 -1
  18. package/dist/cli/billing.d.ts +3 -0
  19. package/dist/cli/billing.d.ts.map +1 -0
  20. package/dist/cli/billing.js +23 -0
  21. package/dist/cli/billing.js.map +1 -0
  22. package/dist/cli/campaigns.d.ts +3 -0
  23. package/dist/cli/campaigns.d.ts.map +1 -0
  24. package/dist/cli/campaigns.js +34 -0
  25. package/dist/cli/campaigns.js.map +1 -0
  26. package/dist/cli/chat.d.ts.map +1 -1
  27. package/dist/cli/chat.js +69 -102
  28. package/dist/cli/chat.js.map +1 -1
  29. package/dist/cli/config.d.ts.map +1 -1
  30. package/dist/cli/config.js +71 -27
  31. package/dist/cli/config.js.map +1 -1
  32. package/dist/cli/conversations.d.ts +3 -0
  33. package/dist/cli/conversations.d.ts.map +1 -0
  34. package/dist/cli/conversations.js +129 -0
  35. package/dist/cli/conversations.js.map +1 -0
  36. package/dist/cli/distribution.d.ts +3 -0
  37. package/dist/cli/distribution.d.ts.map +1 -0
  38. package/dist/cli/distribution.js +122 -0
  39. package/dist/cli/distribution.js.map +1 -0
  40. package/dist/cli/eval.d.ts.map +1 -1
  41. package/dist/cli/eval.js +315 -71
  42. package/dist/cli/eval.js.map +1 -1
  43. package/dist/cli/functions.d.ts.map +1 -1
  44. package/dist/cli/functions.js +109 -38
  45. package/dist/cli/functions.js.map +1 -1
  46. package/dist/cli/index.js +63 -54
  47. package/dist/cli/index.js.map +1 -1
  48. package/dist/cli/init.d.ts +27 -0
  49. package/dist/cli/init.d.ts.map +1 -0
  50. package/dist/cli/init.js +152 -0
  51. package/dist/cli/init.js.map +1 -0
  52. package/dist/cli/kb.d.ts.map +1 -1
  53. package/dist/cli/kb.js +141 -15
  54. package/dist/cli/kb.js.map +1 -1
  55. package/dist/cli/link.d.ts +21 -0
  56. package/dist/cli/link.d.ts.map +1 -0
  57. package/dist/cli/link.js +112 -0
  58. package/dist/cli/link.js.map +1 -0
  59. package/dist/cli/org.d.ts.map +1 -1
  60. package/dist/cli/org.js +79 -67
  61. package/dist/cli/org.js.map +1 -1
  62. package/dist/cli/platform.d.ts +3 -0
  63. package/dist/cli/platform.d.ts.map +1 -0
  64. package/dist/cli/platform.js +133 -0
  65. package/dist/cli/platform.js.map +1 -0
  66. package/dist/cli/tickets.d.ts +3 -0
  67. package/dist/cli/tickets.d.ts.map +1 -0
  68. package/dist/cli/tickets.js +125 -0
  69. package/dist/cli/tickets.js.map +1 -0
  70. package/dist/cli/utils/output.d.ts +15 -9
  71. package/dist/cli/utils/output.d.ts.map +1 -1
  72. package/dist/cli/utils/output.js +53 -46
  73. package/dist/cli/utils/output.js.map +1 -1
  74. package/dist/cli/voice.d.ts +3 -0
  75. package/dist/cli/voice.d.ts.map +1 -0
  76. package/dist/cli/voice.js +206 -0
  77. package/dist/cli/voice.js.map +1 -0
  78. package/dist/core/diff.d.ts +16 -0
  79. package/dist/core/diff.d.ts.map +1 -0
  80. package/dist/core/diff.js +57 -0
  81. package/dist/core/diff.js.map +1 -0
  82. package/dist/core/org-resolver.d.ts +37 -0
  83. package/dist/core/org-resolver.d.ts.map +1 -0
  84. package/dist/core/org-resolver.js +73 -0
  85. package/dist/core/org-resolver.js.map +1 -0
  86. package/dist/core/runtime.d.ts +36 -0
  87. package/dist/core/runtime.d.ts.map +1 -0
  88. package/dist/core/runtime.js +62 -0
  89. package/dist/core/runtime.js.map +1 -0
  90. package/dist/core/scope.d.ts +33 -0
  91. package/dist/core/scope.d.ts.map +1 -0
  92. package/dist/core/scope.js +37 -0
  93. package/dist/core/scope.js.map +1 -0
  94. package/dist/mcp/registry.d.ts +67 -0
  95. package/dist/mcp/registry.d.ts.map +1 -0
  96. package/dist/mcp/registry.js +195 -0
  97. package/dist/mcp/registry.js.map +1 -0
  98. package/dist/mcp/server.d.ts +9 -0
  99. package/dist/mcp/server.d.ts.map +1 -1
  100. package/dist/mcp/server.js +87 -2
  101. package/dist/mcp/server.js.map +1 -1
  102. package/dist/mcp/tool-meta.d.ts +21 -0
  103. package/dist/mcp/tool-meta.d.ts.map +1 -0
  104. package/dist/mcp/tool-meta.js +203 -0
  105. package/dist/mcp/tool-meta.js.map +1 -0
  106. package/dist/mcp/tools.d.ts +2 -1
  107. package/dist/mcp/tools.d.ts.map +1 -1
  108. package/dist/mcp/tools.js +1462 -215
  109. package/dist/mcp/tools.js.map +1 -1
  110. package/dist/sdk/agent-bundle.d.ts +99 -0
  111. package/dist/sdk/agent-bundle.d.ts.map +1 -0
  112. package/dist/sdk/agent-bundle.js +211 -0
  113. package/dist/sdk/agent-bundle.js.map +1 -0
  114. package/dist/sdk/agents.d.ts +54 -1
  115. package/dist/sdk/agents.d.ts.map +1 -1
  116. package/dist/sdk/agents.js +59 -5
  117. package/dist/sdk/agents.js.map +1 -1
  118. package/dist/sdk/analytics.d.ts +158 -0
  119. package/dist/sdk/analytics.d.ts.map +1 -0
  120. package/dist/sdk/analytics.js +185 -0
  121. package/dist/sdk/analytics.js.map +1 -0
  122. package/dist/sdk/auth.d.ts +34 -12
  123. package/dist/sdk/auth.d.ts.map +1 -1
  124. package/dist/sdk/auth.js +60 -77
  125. package/dist/sdk/auth.js.map +1 -1
  126. package/dist/sdk/billing.d.ts +25 -0
  127. package/dist/sdk/billing.d.ts.map +1 -0
  128. package/dist/sdk/billing.js +61 -0
  129. package/dist/sdk/billing.js.map +1 -0
  130. package/dist/sdk/campaigns.d.ts +35 -0
  131. package/dist/sdk/campaigns.d.ts.map +1 -0
  132. package/dist/sdk/campaigns.js +59 -0
  133. package/dist/sdk/campaigns.js.map +1 -0
  134. package/dist/sdk/chat.d.ts +25 -2
  135. package/dist/sdk/chat.d.ts.map +1 -1
  136. package/dist/sdk/chat.js +37 -10
  137. package/dist/sdk/chat.js.map +1 -1
  138. package/dist/sdk/client.d.ts +93 -5
  139. package/dist/sdk/client.d.ts.map +1 -1
  140. package/dist/sdk/client.js +362 -100
  141. package/dist/sdk/client.js.map +1 -1
  142. package/dist/sdk/config-change.d.ts +74 -0
  143. package/dist/sdk/config-change.d.ts.map +1 -0
  144. package/dist/sdk/config-change.js +117 -0
  145. package/dist/sdk/config-change.js.map +1 -0
  146. package/dist/sdk/config.d.ts.map +1 -1
  147. package/dist/sdk/config.js +2 -1
  148. package/dist/sdk/config.js.map +1 -1
  149. package/dist/sdk/conversations.d.ts +101 -0
  150. package/dist/sdk/conversations.d.ts.map +1 -0
  151. package/dist/sdk/conversations.js +155 -0
  152. package/dist/sdk/conversations.js.map +1 -0
  153. package/dist/sdk/credentials.d.ts +25 -0
  154. package/dist/sdk/credentials.d.ts.map +1 -0
  155. package/dist/sdk/credentials.js +44 -0
  156. package/dist/sdk/credentials.js.map +1 -0
  157. package/dist/sdk/distribution.d.ts +94 -0
  158. package/dist/sdk/distribution.d.ts.map +1 -0
  159. package/dist/sdk/distribution.js +100 -0
  160. package/dist/sdk/distribution.js.map +1 -0
  161. package/dist/sdk/eval.d.ts +81 -0
  162. package/dist/sdk/eval.d.ts.map +1 -1
  163. package/dist/sdk/eval.js +117 -0
  164. package/dist/sdk/eval.js.map +1 -1
  165. package/dist/sdk/functions.d.ts +14 -1
  166. package/dist/sdk/functions.d.ts.map +1 -1
  167. package/dist/sdk/functions.js +48 -5
  168. package/dist/sdk/functions.js.map +1 -1
  169. package/dist/sdk/index.d.ts +2 -0
  170. package/dist/sdk/index.d.ts.map +1 -1
  171. package/dist/sdk/index.js +2 -0
  172. package/dist/sdk/index.js.map +1 -1
  173. package/dist/sdk/jarvis-chat.d.ts +28 -0
  174. package/dist/sdk/jarvis-chat.d.ts.map +1 -0
  175. package/dist/sdk/jarvis-chat.js +117 -0
  176. package/dist/sdk/jarvis-chat.js.map +1 -0
  177. package/dist/sdk/kb.d.ts +69 -1
  178. package/dist/sdk/kb.d.ts.map +1 -1
  179. package/dist/sdk/kb.js +111 -24
  180. package/dist/sdk/kb.js.map +1 -1
  181. package/dist/sdk/orgs.d.ts +13 -0
  182. package/dist/sdk/orgs.d.ts.map +1 -0
  183. package/dist/sdk/orgs.js +59 -0
  184. package/dist/sdk/orgs.js.map +1 -0
  185. package/dist/sdk/platform.d.ts +62 -0
  186. package/dist/sdk/platform.d.ts.map +1 -0
  187. package/dist/sdk/platform.js +95 -0
  188. package/dist/sdk/platform.js.map +1 -0
  189. package/dist/sdk/session.d.ts +25 -0
  190. package/dist/sdk/session.d.ts.map +1 -0
  191. package/dist/sdk/session.js +69 -0
  192. package/dist/sdk/session.js.map +1 -0
  193. package/dist/sdk/skills.d.ts +46 -0
  194. package/dist/sdk/skills.d.ts.map +1 -0
  195. package/dist/sdk/skills.js +203 -0
  196. package/dist/sdk/skills.js.map +1 -0
  197. package/dist/sdk/test-chat.d.ts +52 -0
  198. package/dist/sdk/test-chat.d.ts.map +1 -0
  199. package/dist/sdk/test-chat.js +134 -0
  200. package/dist/sdk/test-chat.js.map +1 -0
  201. package/dist/sdk/tickets.d.ts +98 -0
  202. package/dist/sdk/tickets.d.ts.map +1 -0
  203. package/dist/sdk/tickets.js +91 -0
  204. package/dist/sdk/tickets.js.map +1 -0
  205. package/dist/sdk/types.d.ts +91 -6
  206. package/dist/sdk/types.d.ts.map +1 -1
  207. package/dist/sdk/voice.d.ts +147 -0
  208. package/dist/sdk/voice.d.ts.map +1 -0
  209. package/dist/sdk/voice.js +236 -0
  210. package/dist/sdk/voice.js.map +1 -0
  211. package/package.json +22 -3
  212. package/scripts/check-skills.mjs +25 -0
  213. package/skills/agent-builder/SKILL.md +89 -252
  214. package/skills/evaluations/SKILL.md +23 -234
  215. package/skills/evaluations/references/analysis.md +95 -0
  216. package/skills/evaluations/references/cli-reference.md +28 -0
  217. package/skills/evaluations/references/json-formats.md +100 -0
  218. package/src/cli/agents.ts +202 -22
  219. package/src/cli/analytics.ts +138 -0
  220. package/src/cli/auth.ts +56 -18
  221. package/src/cli/billing.ts +25 -0
  222. package/src/cli/campaigns.ts +33 -0
  223. package/src/cli/chat.ts +66 -115
  224. package/src/cli/config.ts +65 -31
  225. package/src/cli/conversations.ts +133 -0
  226. package/src/cli/distribution.ts +116 -0
  227. package/src/cli/eval.ts +254 -70
  228. package/src/cli/functions.ts +105 -39
  229. package/src/cli/index.ts +58 -55
  230. package/src/cli/init.ts +154 -0
  231. package/src/cli/kb.ts +120 -17
  232. package/src/cli/link.ts +117 -0
  233. package/src/cli/org.ts +81 -75
  234. package/src/cli/platform.ts +128 -0
  235. package/src/cli/tickets.ts +130 -0
  236. package/src/cli/utils/output.ts +51 -42
  237. package/src/cli/voice.ts +204 -0
  238. package/src/core/diff.ts +52 -0
  239. package/src/core/org-resolver.ts +113 -0
  240. package/src/core/runtime.ts +73 -0
  241. package/src/core/scope.ts +64 -0
  242. package/src/mcp/registry.ts +236 -0
  243. package/src/mcp/server.ts +84 -2
  244. package/src/mcp/tool-meta.ts +241 -0
  245. package/src/mcp/tools.ts +1529 -265
  246. package/src/sdk/agent-bundle.ts +245 -0
  247. package/src/sdk/agents.ts +74 -5
  248. package/src/sdk/analytics.ts +220 -0
  249. package/src/sdk/auth.ts +87 -107
  250. package/src/sdk/billing.ts +59 -0
  251. package/src/sdk/campaigns.ts +60 -0
  252. package/src/sdk/chat.ts +44 -10
  253. package/src/sdk/client.ts +373 -102
  254. package/src/sdk/config-change.ts +148 -0
  255. package/src/sdk/config.ts +2 -1
  256. package/src/sdk/conversations.ts +199 -0
  257. package/src/sdk/credentials.ts +53 -0
  258. package/src/sdk/distribution.ts +132 -0
  259. package/src/sdk/eval.ts +140 -0
  260. package/src/sdk/functions.ts +52 -6
  261. package/src/sdk/index.ts +2 -0
  262. package/src/sdk/jarvis-chat.ts +112 -0
  263. package/src/sdk/kb.ts +116 -29
  264. package/src/sdk/orgs.ts +62 -0
  265. package/src/sdk/platform.ts +115 -0
  266. package/src/sdk/session.ts +90 -0
  267. package/src/sdk/skills.ts +188 -0
  268. package/src/sdk/test-chat.ts +151 -0
  269. package/src/sdk/tickets.ts +120 -0
  270. package/src/sdk/types.ts +88 -6
  271. package/src/sdk/voice.ts +262 -0
  272. package/test/cli/entry.test.ts +34 -0
  273. package/test/cli/init.test.ts +66 -0
  274. package/test/cli/link.test.ts +57 -0
  275. package/test/core/org-resolver.test.ts +92 -0
  276. package/test/core/runtime.test.ts +30 -0
  277. package/test/core/scope.test.ts +93 -0
  278. package/test/harness.test.ts +24 -0
  279. package/test/helpers/config-home.ts +22 -0
  280. package/test/helpers/fake-middleware.ts +56 -0
  281. package/test/helpers/fetch-recorder.ts +75 -0
  282. package/test/helpers/jwt.ts +9 -0
  283. package/test/helpers/mcp-pair.ts +38 -0
  284. package/test/mcp/__snapshots__/tools-snapshot.test.ts.snap +145 -0
  285. package/test/mcp/askers.test.ts +60 -0
  286. package/test/mcp/elicit-timeout.test.ts +29 -0
  287. package/test/mcp/registry.test.ts +169 -0
  288. package/test/mcp/shaping.test.ts +41 -0
  289. package/test/mcp/tools-snapshot.test.ts +42 -0
  290. package/test/sdk/agents.test.ts +232 -0
  291. package/test/sdk/analytics.test.ts +89 -0
  292. package/test/sdk/auth.test.ts +70 -0
  293. package/test/sdk/billing.test.ts +47 -0
  294. package/test/sdk/campaigns.test.ts +43 -0
  295. package/test/sdk/client.test.ts +80 -0
  296. package/test/sdk/config-change.test.ts +142 -0
  297. package/test/sdk/conversations.test.ts +80 -0
  298. package/test/sdk/credentials.test.ts +33 -0
  299. package/test/sdk/distribution.test.ts +55 -0
  300. package/test/sdk/eval.test.ts +155 -0
  301. package/test/sdk/kb-upload.test.ts +50 -0
  302. package/test/sdk/kb.test.ts +95 -0
  303. package/test/sdk/orgs.test.ts +48 -0
  304. package/test/sdk/platform.test.ts +89 -0
  305. package/test/sdk/renewal.test.ts +115 -0
  306. package/test/sdk/session.test.ts +60 -0
  307. package/test/sdk/skills.test.ts +235 -0
  308. package/test/sdk/test-chat.test.ts +112 -0
  309. package/test/sdk/tickets.test.ts +42 -0
  310. package/test/sdk/voice.test.ts +147 -0
  311. package/vitest.config.ts +11 -0
package/dist/mcp/tools.js CHANGED
@@ -1,66 +1,327 @@
1
1
  import { z } from 'zod/v3';
2
- import { AgentsSDK } from '../sdk/agents.js';
2
+ import { AgentInsightsSDK, AgentsSDK } from '../sdk/agents.js';
3
+ import { createAgentFromSkeleton, exportAgent, importAgent, missingSkeletonFields, parseModel, } from '../sdk/agent-bundle.js';
4
+ import { getAskers } from '../core/scope.js';
5
+ import { proposedSkills, skillConfiguration } from '../sdk/skills.js';
6
+ import { VoiceSDK } from '../sdk/voice.js';
7
+ import { DistributionSDK } from '../sdk/distribution.js';
8
+ import { AnalyticsSDK, AuditSDK, OrgSettingsSDK } from '../sdk/analytics.js';
9
+ import { TicketsSDK } from '../sdk/tickets.js';
10
+ import { BillingSDK } from '../sdk/billing.js';
11
+ import { CampaignsSDK } from '../sdk/campaigns.js';
12
+ import { PlatformSDK } from '../sdk/platform.js';
13
+ import { jarvisChat } from '../sdk/jarvis-chat.js';
14
+ import { applyConfigChange, describePlan, liveText, mayBeLive, modelArg, planConfigChange } from '../sdk/config-change.js';
3
15
  import { ConfigSDK } from '../sdk/config.js';
4
16
  import { FunctionsSDK } from '../sdk/functions.js';
5
17
  import { KBSDK } from '../sdk/kb.js';
6
18
  import { RubricsSDK } from '../sdk/rubrics.js';
7
19
  import { TestSetsSDK } from '../sdk/test-sets.js';
8
- import { EvalSDK } from '../sdk/eval.js';
9
- import { ChatSession } from '../sdk/chat.js';
20
+ import { EvalSDK, gateVerdict } from '../sdk/eval.js';
21
+ import { exchange, openTestChat } from '../sdk/test-chat.js';
22
+ import { ConversationsSDK, conversationToTestSet, conversationUrl, isCliConversation, isTestConversation, projectOfAgent } from '../sdk/conversations.js';
10
23
  import { exportEvalMarkdown, exportEvalReport, exportAgentCard } from '../report/export.js';
24
+ import { createRegistry } from './registry.js';
25
+ import { OrgsSDK } from '../sdk/orgs.js';
26
+ import { whoami } from '../sdk/auth.js';
27
+ import { NeedsInputError, SessionExpiredError } from '../core/runtime.js';
28
+ // A REST skill's authentication as flat fields: for a nested object Gemini writes calls Google rejects.
29
+ const skillAuthFields = {
30
+ auth_type: z.enum(['no-auth', 'api-key', 'basic-auth', 'bearer-token']).optional().describe('rest_api: how the API is called (default no-auth)'),
31
+ auth_token: z.string().optional().describe('bearer-token: the token'),
32
+ auth_key: z.string().optional().describe('api-key: the header name, e.g. x-api-key'),
33
+ auth_value: z.string().optional().describe('api-key: the key'),
34
+ auth_username: z.string().optional().describe('basic-auth: user name'),
35
+ auth_password: z.string().optional().describe('basic-auth: password'),
36
+ };
37
+ // Helper: the flat auth fields as the dashboard's skill form saves them (configurationSchema.authorization)
38
+ function withAuthorization(args) {
39
+ if (!args.auth_type)
40
+ return args;
41
+ const fields = { 'api-key': ['key', 'value'], 'basic-auth': ['username', 'password'], 'bearer-token': ['token'], 'no-auth': [] };
42
+ const authorization = { type: args.auth_type };
43
+ for (const k of fields[args.auth_type] ?? []) {
44
+ const v = args[`auth_${k}`];
45
+ if (typeof v === 'string' && v)
46
+ authorization[k] = v;
47
+ }
48
+ return { ...args, configuration: { ...(args.configuration ?? {}), authorization } };
49
+ }
11
50
  // Helper: format a successful MCP tool result
12
51
  function success(data) {
13
52
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
14
53
  }
15
54
  // Helper: format an error MCP tool result
16
55
  function error(err) {
56
+ if (err instanceof NeedsInputError) {
57
+ return { content: [{ type: 'text', text: JSON.stringify({ needs_input: true, message: err.message, choices: err.choices, to_proceed: 'Ask the user which organization to use, then call again with org_id set to its id.' }, null, 2) }] };
58
+ }
17
59
  const message = err instanceof Error ? err.message : String(err);
18
60
  return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
19
61
  }
62
+ // Helper: the org's pages in the dashboard, so the person gets a link instead of an id
63
+ async function orgPages(client) {
64
+ const base = client.getHosts().copilot.replace(/\/api\/?$/, '');
65
+ return `${base}/#/organization/${await client.currentOrgId()}`;
66
+ }
67
+ // Test items and rubric rules as the evaluations service defines them (indemn_evals models).
68
+ // Typed fields, not open records: models such as Gemini cannot fill an object with no
69
+ // declared fields. Other fields the service accepts still pass through.
70
+ // Test items and rubric rules with flat fields (a nested object makes Gemini write calls Google rejects);
71
+ // mapped to the evaluations service's shape (inputs / expected, evaluation_criteria) before saving.
72
+ const testItemSchema = z.object({
73
+ type: z.enum(['single_turn', 'scenario', 'voice_simulation', 'transcript']).describe('single_turn: one message; scenario: a simulated chat; voice_simulation: a simulated phone call'),
74
+ name: z.string().describe('Short name of what is tested'),
75
+ message: z.string().optional().describe('single_turn: the message sent to the agent'),
76
+ persona: z.string().optional().describe('scenario / voice_simulation: who the simulated customer is'),
77
+ initial_message: z.string().optional().describe('scenario / voice_simulation: their first message'),
78
+ goal: z.string().optional().describe('voice_simulation: what the caller wants'),
79
+ max_turns: z.number().int().optional().describe('scenario / voice_simulation: turns before stopping (default 5)'),
80
+ success_criteria: z.array(z.string()).optional().describe('Plain-language checks that must all pass (needed)'),
81
+ expected_outcome: z.string().optional(),
82
+ tool_present: z.array(z.string()).optional().describe('Skills that must be used'),
83
+ tool_absent: z.array(z.string()).optional().describe('Skills that must not be used'),
84
+ tags: z.array(z.string()).optional(),
85
+ priority: z.string().optional(),
86
+ }).passthrough();
87
+ const rubricRuleSchema = z.object({
88
+ id: z.string().describe('Short rule id, e.g. STAYS_ON_TOPIC'),
89
+ name: z.string(),
90
+ severity: z.string().describe('high, medium or low'),
91
+ category: z.string().describe('e.g. response_quality, accuracy, safety'),
92
+ description: z.string(),
93
+ pass_conditions: z.array(z.string()).optional().describe('What passing looks like'),
94
+ fail_conditions: z.array(z.string()).optional().describe('What failing looks like'),
95
+ }).passthrough();
96
+ const defined = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
97
+ // Helper: a flat test item in the service's shape; inputs / expected given as objects (other clients) are kept.
98
+ function toTestItem(item) {
99
+ const { message, persona, initial_message, goal, max_turns, success_criteria, expected_outcome, tool_present, tool_absent, inputs, expected, ...rest } = item;
100
+ return {
101
+ ...rest,
102
+ inputs: { ...(inputs ?? {}), ...defined({ message, persona, initial_message, goal, max_turns }) },
103
+ expected: { ...(expected ?? {}), ...defined({ success_criteria, expected_outcome, tool_present, tool_absent }) },
104
+ };
105
+ }
106
+ // Helper: a flat rubric rule in the service's shape (evaluation_criteria).
107
+ function toRubricRule(rule) {
108
+ const { pass_conditions, fail_conditions, evaluation_criteria, ...rest } = rule;
109
+ const criteria = { ...(evaluation_criteria ?? {}), ...defined({ pass_conditions, fail_conditions }) };
110
+ return Object.keys(criteria).length ? { ...rest, evaluation_criteria: criteria } : rest;
111
+ }
112
+ // Objects whose fields a model cannot see (Gemini) arrive as JSON text: accept both.
113
+ const parseJsonText = (v) => {
114
+ if (typeof v !== 'string')
115
+ return v;
116
+ try {
117
+ return JSON.parse(v);
118
+ }
119
+ catch {
120
+ return v;
121
+ }
122
+ };
123
+ const jsonObject = () => z.preprocess(parseJsonText, z.record(z.any()));
124
+ // A skill for bulk_create_skills with flat fields (the configuration is built as create_function builds it);
125
+ // a proposal from generate_skills_from_doc keeps its own configurationSchema.
126
+ const skillDefinition = z.preprocess(parseJsonText, z.object({
127
+ name: z.string(),
128
+ description: z.string().optional(),
129
+ type: z.string().describe('e.g. rest_api, transfer_call'),
130
+ url: z.string().optional().describe('rest_api: endpoint URL; {{parameter}} placeholders allowed'),
131
+ method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).optional().describe('rest_api'),
132
+ payload: z.string().optional().describe('rest_api: request body as JSON text with {{parameter}} placeholders'),
133
+ ...skillAuthFields,
134
+ response_field: z.string().optional().describe('rest_api: dot-path of the answer to use'),
135
+ output_instructions: z.string().optional().describe('What the agent says with the answer'),
136
+ transfer_to: z.string().optional().describe('transfer_call: phone number (E.164) or SIP address'),
137
+ parameters: z.array(z.object({
138
+ name: z.string(), type: z.string().describe('string, number, boolean or list'),
139
+ description: z.string().optional(), required: z.boolean().optional(),
140
+ }).passthrough()).optional(),
141
+ }).passthrough());
20
142
  export function registerTools(server, client) {
143
+ const reg = createRegistry(server, {
144
+ context: () => client.getResultContext(),
145
+ // Each call decides its own org; an answer given once is not reused for the next call.
146
+ resolveOrg: (explicit) => client.resolveOrg(explicit, { cache: false }),
147
+ // The person approves a change by names: look up the ids the call names.
148
+ // An id that names nothing gets no card: the registry answers "not found" instead.
149
+ displayNames: async (args, orgId) => {
150
+ const names = {};
151
+ const missing = [];
152
+ const notFound = /not found|is not on agent|404/i;
153
+ const add = (id, what, lookup) => typeof id === 'string' && id
154
+ ? lookup(id).then((x) => {
155
+ if (typeof x?.name === 'string' && x.name)
156
+ names[id] = x.name;
157
+ else if (what && !x)
158
+ missing.push(`${what} ${id}`);
159
+ }, (err) => { if (what && notFound.test(err instanceof Error ? err.message : String(err)))
160
+ missing.push(`${what} ${id}`); })
161
+ : undefined;
162
+ await Promise.all([
163
+ add(args.agent_id ?? args.bot_id, 'agent', (id) => agents.getAgent(id)),
164
+ typeof args.agent_id === 'string' ? add(args.function_id, 'skill', (id) => skillOf(String(args.agent_id), id)) : undefined,
165
+ add(args.kb_id, 'knowledge base', (id) => kb.getKB(id)),
166
+ add(args.test_set_id, 'test set', (id) => testSets.getTestSet(id)),
167
+ add(args.rubric_id, 'rubric', (id) => rubrics.getRubric(id)),
168
+ add(orgId, null, (id) => client.copilotGet(`/organization/${id}`).then((r) => r.organization)),
169
+ ]);
170
+ return { names, missing };
171
+ },
172
+ });
173
+ const orgs = new OrgsSDK(client);
174
+ // ---------------------------------------------------------------------------
175
+ // Login and organization tools
176
+ // ---------------------------------------------------------------------------
177
+ reg.tool('whoami', 'Show the platform (dev or production), the logged-in user, the session expiry, the orgs you belong to, and whether this folder is linked to one org. Call this first when unsure which org to use.', {}, async () => {
178
+ try {
179
+ const info = whoami();
180
+ if (!info)
181
+ throw new SessionExpiredError('Not logged in. Run `indemn login` in a terminal.');
182
+ const link = client.getLink();
183
+ return success({ ...info, linked_folder: link ? { org_id: link.org_id, org_name: link.org_name, agents: link.agents, path: link.path } : null });
184
+ }
185
+ catch (err) {
186
+ return error(err);
187
+ }
188
+ });
189
+ reg.tool('list_organizations', 'List the organizations you belong to on this platform. Pass one of these ids as org_id to other tools.', {}, async () => {
190
+ try {
191
+ return success(await orgs.listOrganizations());
192
+ }
193
+ catch (err) {
194
+ return error(err);
195
+ }
196
+ });
197
+ reg.tool('list_org_members', 'List the owners, admins and agents of an organization, with their roles.', {}, async () => {
198
+ try {
199
+ return success(await orgs.listMembers());
200
+ }
201
+ catch (err) {
202
+ return error(err);
203
+ }
204
+ });
205
+ reg.tool('list_channels', 'List the channels an agent can be published on, such as WEB or VOICE.', {}, async () => {
206
+ try {
207
+ return success(await orgs.listChannels());
208
+ }
209
+ catch (err) {
210
+ return error(err);
211
+ }
212
+ });
21
213
  const agents = new AgentsSDK(client);
22
214
  const config = new ConfigSDK(client);
23
215
  const functions = new FunctionsSDK(client);
216
+ // A skill of an agent by its id.
217
+ const skillOf = async (agentId, id) => {
218
+ const skill = (await functions.listFunctions(agentId)).find((f) => f._id === id);
219
+ if (!skill)
220
+ throw new Error(`Skill ${id} is not on agent ${agentId}.`);
221
+ return skill;
222
+ };
24
223
  const kb = new KBSDK(client);
25
224
  const rubrics = new RubricsSDK(client);
26
225
  const testSets = new TestSetsSDK(client);
27
226
  const evals = new EvalSDK(client);
227
+ // The evaluations service checks nothing about orgs: touch a rubric or test set only when its agent is in this org.
228
+ const rubricInOrg = async (id) => {
229
+ const r = await rubrics.getRubric(id);
230
+ if (r.agent_id)
231
+ await evals.assertAgentInOrg(String(r.agent_id));
232
+ return r;
233
+ };
234
+ const testSetInOrg = async (id) => {
235
+ const t = await testSets.getTestSet(id);
236
+ if (t.agent_id)
237
+ await evals.assertAgentInOrg(String(t.agent_id));
238
+ return t;
239
+ };
28
240
  // ---------------------------------------------------------------------------
29
241
  // Agent tools
30
242
  // ---------------------------------------------------------------------------
31
- server.tool('list_agents', 'List all agents in the organization', { limit: z.number().optional().describe('Maximum number of agents to return') }, async (args) => {
243
+ reg.tool('list_agents', 'List the agents (bots) of the organization: id, name, channels. Start here to find an agent id.', { channel: z.string().optional().describe('Only agents on this channel, e.g. web, voice, email') }, async (args) => {
32
244
  try {
33
- let result = await agents.listAgents();
34
- if (args.limit)
35
- result = result.slice(0, args.limit);
36
- return success(result);
245
+ const pages = await orgPages(client);
246
+ const wanted = args.channel?.trim().toUpperCase();
247
+ // What a person needs, not the stored record (a page of full records ran to 73k characters).
248
+ const rows = (await agents.listAgents()).map((a) => ({
249
+ _id: a._id,
250
+ name: a.name,
251
+ ...(a.description ? { description: a.description } : {}),
252
+ // The list returns channel records, not the names the type declares.
253
+ channels: (a.channels ?? []).map((c) => (typeof c === 'string' ? c : c?.name ?? '')).filter(Boolean),
254
+ ...(a.connected_phone ? { phone: a.connected_phone } : {}),
255
+ updated_at: a.updatedAt,
256
+ url: `${pages}/ai-studio/bot/${a._id}/configurations`,
257
+ }));
258
+ return success(wanted ? rows.filter((a) => a.channels.some((c) => c.toUpperCase() === wanted)) : rows);
37
259
  }
38
260
  catch (err) {
39
261
  return error(err);
40
262
  }
41
263
  });
42
- server.tool('get_agent', 'Get details of a specific agent', { agent_id: z.string().describe('The agent ID') }, async (args) => {
264
+ reg.tool('get_agent', 'Get one agent: name, description, channels and project. For what it does, use agent_summary; for its prompt and model, get_config.', { agent_id: z.string().describe('The agent ID') }, async (args) => {
43
265
  try {
44
266
  const result = await agents.getAgent(args.agent_id);
45
- return success(result);
267
+ return success({ ...result, url: `${await orgPages(client)}/ai-studio/bot/${args.agent_id}/configurations` });
46
268
  }
47
269
  catch (err) {
48
270
  return error(err);
49
271
  }
50
272
  });
51
- server.tool('create_agent', 'Create a new agent', {
52
- name: z.string().describe('Agent name'),
53
- channels: z.array(z.string()).optional().describe('Channels (e.g. ["WEB"])'),
273
+ reg.tool('create_agent', 'Create an agent with its basics: name, channels, what it is for, system prompt and model. Missing basics are asked from the user when the host can ask; otherwise the result lists them (pass minimal: true to create with only a name). Refuses a name that already exists in the org.', {
274
+ name: z.string().optional().describe('Agent name'),
275
+ channels: z.array(z.string()).optional().describe('Channels, e.g. ["WEB"] (see list_channels)'),
276
+ description: z.string().optional().describe('What the agent is for'),
277
+ system_prompt: z.string().optional().describe('System prompt'),
278
+ model: z.string().optional().describe('provider/model, e.g. openai/gpt-4.1 (see list_llm_models)'),
279
+ allow_duplicate: z.boolean().optional().describe('Create even if an agent with this name exists'),
280
+ minimal: z.boolean().optional().describe('Create with only the fields given, without asking for the rest'),
54
281
  }, async (args) => {
55
282
  try {
56
- const result = await agents.createAgent({ name: args.name, channels: args.channels });
57
- return success(result);
283
+ const skeleton = {
284
+ name: args.name ?? '',
285
+ channels: args.channels,
286
+ description: args.description,
287
+ system_prompt: args.system_prompt,
288
+ model: args.model ? parseModel(args.model) : undefined,
289
+ };
290
+ let missing = missingSkeletonFields(skeleton);
291
+ if (missing.length && !args.minimal) {
292
+ const titles = { name: 'Agent name', description: 'What is this agent for?', system_prompt: 'System prompt' };
293
+ const answers = await getAskers().form?.('Fill in the basics for the new agent.', missing.map((key) => ({ key, title: titles[key] ?? key, required: key === 'name' })));
294
+ if (answers) {
295
+ if (answers.name)
296
+ skeleton.name = answers.name;
297
+ if (answers.description)
298
+ skeleton.description = answers.description;
299
+ if (answers.system_prompt)
300
+ skeleton.system_prompt = answers.system_prompt;
301
+ }
302
+ missing = missingSkeletonFields(skeleton);
303
+ if (missing.includes('name') || (missing.length && !answers)) {
304
+ throw new NeedsInputError(`Before creating the agent, provide: ${missing.join(', ')}. Or pass minimal: true to create it with only a name.`, missing.map((m) => ({ id: m, label: titles[m] ?? m })));
305
+ }
306
+ }
307
+ const result = await createAgentFromSkeleton(client, skeleton, { allowDuplicate: args.allow_duplicate });
308
+ return success({ agent: result.agent, set: result.set, not_set: missingSkeletonFields(skeleton) });
58
309
  }
59
310
  catch (err) {
60
311
  return error(err);
61
312
  }
62
313
  });
63
- server.tool('update_agent', 'Update an existing agent', {
314
+ // Lifecycle lives on the V2 platform; "unknown" when it cannot be read (treated as published: mayBeLive).
315
+ const lifecycleOf = (id) => new AgentInsightsSDK(client).detail(id).then((d) => d.lifecycle ?? 'prototype', () => 'unknown');
316
+ // A change to an agent that may be live asks first; the approval names the fields, never their values (skill auth).
317
+ const liveChange = (what, skip) => ({
318
+ guardWhen: async (a) => mayBeLive(await lifecycleOf(String(a.agent_id))),
319
+ preview: async (a) => {
320
+ const fields = Object.keys(a).filter((k) => a[k] !== undefined && !['agent_id', 'confirm', 'org_id', ...skip].includes(k));
321
+ return `This will change ${what} of agent ${a.agent_id}: ${fields.join(', ') || 'nothing'}. The agent ${liveText(await lifecycleOf(String(a.agent_id)))}.`;
322
+ },
323
+ });
324
+ reg.tool('update_agent', 'Change an agent\'s name, description or webhook. For the prompt, greeting or model use set_config.', {
64
325
  agent_id: z.string().describe('The agent ID'),
65
326
  name: z.string().optional().describe('New agent name'),
66
327
  description: z.string().optional().describe('New agent description'),
@@ -81,20 +342,98 @@ export function registerTools(server, client) {
81
342
  catch (err) {
82
343
  return error(err);
83
344
  }
84
- });
85
- server.tool('delete_agent', 'Delete an agent', { agent_id: z.string().describe('The agent ID to delete') }, async (args) => {
345
+ }, liveChange('the name, description or webhook', []));
346
+ reg.tool('delete_agent', 'Delete an agent. It is removed from the dashboard and its knowledge base links are dropped.', {
347
+ agent_id: z.string().describe('The agent ID to delete'),
348
+ confirm_published: z.boolean().optional().describe('Required with confirm when the agent is published and the user explicitly agreed'),
349
+ }, async (args, meta) => {
86
350
  try {
351
+ const agent = await agents.getAgent(args.agent_id);
352
+ const lifecycle = await lifecycleOf(args.agent_id);
353
+ if (mayBeLive(lifecycle) && !meta.confirmedByPerson && args.confirm_published !== true) {
354
+ throw new Error(`"${agent.name}" ${liveText(lifecycle)}. Show the user; only if they explicitly agree, call again with confirm: true and confirm_published: true.`);
355
+ }
87
356
  await agents.deleteAgent(args.agent_id);
88
- return success({ deleted: true, agent_id: args.agent_id });
357
+ return success({ deleted: true, agent_id: args.agent_id, name: agent.name });
89
358
  }
90
359
  catch (err) {
91
360
  return error(err);
92
361
  }
362
+ }, {
363
+ preview: async (a) => {
364
+ const agent = await agents.getAgent(String(a.agent_id));
365
+ const lifecycle = await lifecycleOf(String(a.agent_id));
366
+ return `This will delete agent "${agent.name}"${mayBeLive(lifecycle) ? `, which ${liveText(lifecycle)}` : ''}. It cannot be restored from the CLI.`;
367
+ },
93
368
  });
94
- server.tool('clone_agent', 'Clone an existing agent', { agent_id: z.string().describe('The agent ID to clone') }, async (args) => {
369
+ const insights = new AgentInsightsSDK(client);
370
+ reg.tool('agent_summary', 'What an agent does: one paragraph (default) or its skills and knowledge sources (full: true). Read this before changing an agent.', { agent_id: z.string().describe('The agent ID'), full: z.boolean().optional().describe('List skills and knowledge sources') }, async (args) => {
95
371
  try {
96
- const result = await agents.cloneAgent(args.agent_id);
97
- return success(result);
372
+ return success(await insights.summary(args.agent_id, { text: !args.full }));
373
+ }
374
+ catch (err) {
375
+ return error(err);
376
+ }
377
+ });
378
+ reg.tool('agent_stats', 'Conversation metrics for one agent (test conversations excluded). Dates are YYYY-MM-DD; default is the last 30 days.', {
379
+ agent_id: z.string().describe('The agent ID'),
380
+ date_from: z.string().optional().describe('YYYY-MM-DD'),
381
+ date_to: z.string().optional().describe('YYYY-MM-DD'),
382
+ channel: z.enum(['web', 'voice', 'all']).optional(),
383
+ }, async (args) => {
384
+ try {
385
+ return success(await insights.stats(args.agent_id, { from: args.date_from, to: args.date_to, channel: args.channel }));
386
+ }
387
+ catch (err) {
388
+ return error(err);
389
+ }
390
+ });
391
+ reg.tool('set_agent_lifecycle', 'Move an agent between prototype, beta and published. Only the owner or an org admin may; the V2 platform enforces it.', { agent_id: z.string().describe('The agent ID'), lifecycle: z.enum(['prototype', 'beta', 'published']) }, async (args) => {
392
+ try {
393
+ return success(await insights.setLifecycle(args.agent_id, args.lifecycle));
394
+ }
395
+ catch (err) {
396
+ return error(err);
397
+ }
398
+ });
399
+ reg.tool('transfer_agent_owner', 'Make another user the owner of an agent. Only the current owner or an org admin may; the V2 platform enforces it.', { agent_id: z.string().describe('The agent ID'), owner_id: z.string().describe('User id of the new owner (see list_org_members)') }, async (args) => {
400
+ try {
401
+ return success(await insights.transferOwner(args.agent_id, args.owner_id));
402
+ }
403
+ catch (err) {
404
+ return error(err);
405
+ }
406
+ });
407
+ reg.tool('clone_agent', 'Copy an agent of this org into a new agent in the same org.', {
408
+ agent_id: z.string().describe('The agent ID to copy'),
409
+ name: z.string().optional().describe('Name of the copy'),
410
+ channels: z.array(z.string()).optional().describe('Channels of the copy'),
411
+ }, async (args) => {
412
+ try {
413
+ return success(await agents.cloneAgent(args.agent_id, { name: args.name, channels: args.channels }));
414
+ }
415
+ catch (err) {
416
+ return error(err);
417
+ }
418
+ });
419
+ reg.tool('export_agent', 'Export an agent as a bundle: prompt, greeting, model, skills with parameters, linked knowledge base ids. Secrets in skill settings are redacted unless include_secrets is true.', { agent_id: z.string().describe('The agent ID'), include_secrets: z.boolean().optional() }, async (args) => {
420
+ try {
421
+ return success(await exportAgent(client, args.agent_id, { includeSecrets: args.include_secrets }));
422
+ }
423
+ catch (err) {
424
+ return error(err);
425
+ }
426
+ });
427
+ reg.tool('import_agent', 'Create a new agent in this org from a bundle made by export_agent (possibly from another org or platform). Lists skills that failed and secrets to fill in.', {
428
+ bundle: z.record(z.unknown()).describe('The bundle returned by export_agent'),
429
+ name: z.string().optional().describe('Name for the new agent (default: the bundle name)'),
430
+ link_knowledge_bases: z.boolean().optional().describe('Link the same knowledge base ids if they exist in this org'),
431
+ allow_duplicate: z.boolean().optional(),
432
+ }, async (args) => {
433
+ try {
434
+ return success(await importAgent(client, args.bundle, {
435
+ name: args.name, linkKnowledgeBases: args.link_knowledge_bases, allowDuplicate: args.allow_duplicate,
436
+ }));
98
437
  }
99
438
  catch (err) {
100
439
  return error(err);
@@ -103,7 +442,7 @@ export function registerTools(server, client) {
103
442
  // ---------------------------------------------------------------------------
104
443
  // Config tools
105
444
  // ---------------------------------------------------------------------------
106
- server.tool('get_config', 'Get the full configuration for an agent', { agent_id: z.string().describe('The agent ID') }, async (args) => {
445
+ reg.tool('get_config', 'An agent\'s configuration: system prompt, greeting (first message), LLM model and knowledge settings.', { agent_id: z.string().describe('The agent ID') }, async (args) => {
107
446
  try {
108
447
  const result = await config.getConfig(args.agent_id);
109
448
  return success(result);
@@ -112,25 +451,37 @@ export function registerTools(server, client) {
112
451
  return error(err);
113
452
  }
114
453
  });
115
- server.tool('set_config', 'Update configuration for an agent', {
454
+ const applyChange = async (agentId, change, confirmedByPerson, confirmPublished) => {
455
+ const plan = await planConfigChange(client, agentId, change);
456
+ if (plan.changes.length === 0)
457
+ return success({ changed: [], message: 'Nothing to change: the agent already has these values.' });
458
+ if (mayBeLive(plan.lifecycle) && !confirmedByPerson && confirmPublished !== true) {
459
+ throw new NeedsInputError(`Agent ${agentId} ${liveText(plan.lifecycle)}. Show the user the change; only if they explicitly agree, call again with confirm: true and confirm_published: true.`);
460
+ }
461
+ const result = await applyConfigChange(client, plan, change);
462
+ return success({ changed: result.applied, not_saved: result.mismatched, lifecycle: plan.lifecycle });
463
+ };
464
+ const previewChange = (argsToChange) => async (a) => describePlan(await planConfigChange(client, String(a.agent_id), argsToChange(a)));
465
+ const configArgs = (a) => ({
466
+ system_prompt: a.system_prompt,
467
+ greeting: a.first_message,
468
+ model: a.model ? modelArg(String(a.model)) : undefined,
469
+ });
470
+ reg.tool('set_config', 'Change an agent\'s prompt, greeting and/or model. The confirmation shows the prompt diff and whether the agent is published; published agents need confirm_published too. The result says what was saved (read back from the platform).', {
116
471
  agent_id: z.string().describe('The agent ID'),
117
- system_prompt: z.string().optional().describe('System prompt text'),
118
- first_message: z.string().optional().describe('First message text shown to users'),
119
- }, async (args) => {
472
+ system_prompt: z.string().optional().describe('New system prompt (whole text)'),
473
+ first_message: z.string().optional().describe('New greeting shown to users'),
474
+ model: z.string().optional().describe('provider/model, e.g. openai/gpt-4.1 (see list_llm_models)'),
475
+ confirm_published: z.boolean().optional().describe('Required with confirm when the agent is published and the user explicitly agreed'),
476
+ }, async (args, meta) => {
120
477
  try {
121
- const input = {};
122
- if (args.system_prompt !== undefined)
123
- input.system_prompt = args.system_prompt;
124
- if (args.first_message !== undefined)
125
- input.first_message = { text: args.first_message };
126
- const result = await config.setConfig(args.agent_id, input);
127
- return success(result);
478
+ return await applyChange(args.agent_id, configArgs(args), meta.confirmedByPerson, args.confirm_published);
128
479
  }
129
480
  catch (err) {
130
481
  return error(err);
131
482
  }
132
- });
133
- server.tool('get_prompt', 'Get the system prompt for an agent', { agent_id: z.string().describe('The agent ID') }, async (args) => {
483
+ }, { preview: previewChange(configArgs) });
484
+ reg.tool('get_prompt', 'An agent\'s system prompt (the instructions it follows).', { agent_id: z.string().describe('The agent ID') }, async (args) => {
134
485
  try {
135
486
  const result = await config.getPrompt(args.agent_id);
136
487
  return success({ agent_id: args.agent_id, system_prompt: result ?? null });
@@ -139,22 +490,22 @@ export function registerTools(server, client) {
139
490
  return error(err);
140
491
  }
141
492
  });
142
- server.tool('set_prompt', 'Set the system prompt for an agent', {
493
+ reg.tool('set_prompt', 'Replace an agent\'s system prompt. The confirmation shows the diff; published agents need confirm_published too.', {
143
494
  agent_id: z.string().describe('The agent ID'),
144
- prompt: z.string().describe('The system prompt text'),
145
- }, async (args) => {
495
+ prompt: z.string().describe('The whole new system prompt'),
496
+ confirm_published: z.boolean().optional().describe('Required with confirm when the agent is published and the user explicitly agreed'),
497
+ }, async (args, meta) => {
146
498
  try {
147
- const result = await config.setPrompt(args.agent_id, args.prompt);
148
- return success(result);
499
+ return await applyChange(args.agent_id, { system_prompt: args.prompt }, meta.confirmedByPerson, args.confirm_published);
149
500
  }
150
501
  catch (err) {
151
502
  return error(err);
152
503
  }
153
- });
504
+ }, { preview: previewChange((a) => ({ system_prompt: String(a.prompt) })) });
154
505
  // ---------------------------------------------------------------------------
155
506
  // Function tools
156
507
  // ---------------------------------------------------------------------------
157
- server.tool('list_functions', 'List all functions (tools) for an agent', { agent_id: z.string().describe('The agent ID') }, async (args) => {
508
+ reg.tool('list_functions', 'List an agent\'s skills (the actions it can take: REST calls, handoff, FAQ retriever, transfer call, ...), with their parameters.', { agent_id: z.string().describe('The agent ID') }, async (args) => {
158
509
  try {
159
510
  const result = await functions.listFunctions(args.agent_id);
160
511
  return success(result);
@@ -163,66 +514,130 @@ export function registerTools(server, client) {
163
514
  return error(err);
164
515
  }
165
516
  });
166
- server.tool('create_function', 'Create a new function (tool) for an agent', {
517
+ reg.tool('create_function', 'Add a skill to an agent. rest_api skills need a configurationSchema (endpoint, method, auth, payload). Names too close to the built-in FAQ retriever are refused.', {
167
518
  agent_id: z.string().describe('The agent ID'),
168
519
  type: z.string().describe('Function type (e.g. "rest_api")'),
169
520
  description: z.string().optional().describe('What the function does'),
170
521
  name: z.string().optional().describe('Function name'),
171
- url: z.string().optional().describe('Endpoint URL'),
172
- method: z.string().optional().describe('HTTP method (GET, POST, etc.)'),
522
+ url: z.string().optional().describe('rest_api: endpoint URL'),
523
+ method: z.string().optional().describe('rest_api: HTTP method (GET, POST, etc.)'),
524
+ payload: z.union([z.string(), z.record(z.any())]).optional().describe('rest_api: request body (JSON); {{parameter}} placeholders are filled from the skill parameters'),
525
+ response_field: z.string().optional().describe('rest_api: dot-path of the response to use (e.g. data.result)'),
526
+ output_instructions: z.string().optional().describe('What the agent says with the answer; {{response.field}} reads the API answer'),
527
+ craft_response: z.boolean().optional().describe('true: the agent phrases the answer itself (default for rest_api without output_instructions); false: the answer, or output_instructions, goes to the customer as is'),
528
+ ...skillAuthFields,
529
+ configuration: jsonObject().optional().describe('Other configurationSchema fields (an object, or JSON text); the fields above win'),
530
+ transfer_to: z.string().optional().describe('transfer_call: phone number (E.164, e.g. +15551234567) or SIP address (sip:...) calls are transferred to'),
173
531
  }, async (args) => {
174
532
  try {
175
- const { agent_id, ...input } = args;
176
- const result = await functions.createFunction(agent_id, input);
177
- return success(result);
533
+ const { agent_id, type, name, description } = args;
534
+ const configurationSchema = skillConfiguration(type, withAuthorization(args));
535
+ // copilot sends a REST answer straight to the customer unless craftResponse is on; with nothing to say
536
+ // how to present it, let the agent phrase it.
537
+ if (type === 'rest_api' && configurationSchema && configurationSchema.craftResponse === undefined && !configurationSchema.outputInstructions) {
538
+ configurationSchema.craftResponse = true;
539
+ }
540
+ if (type === 'rest_api' && !(configurationSchema?.endpoint && configurationSchema?.method)) {
541
+ throw new Error('A rest_api skill needs url and method.');
542
+ }
543
+ if (type === 'transfer_call' && !(configurationSchema?.phoneNumber || configurationSchema?.sipUri)) {
544
+ throw new Error('A transfer_call skill needs transfer_to: a phone number (E.164, e.g. +15551234567) or a SIP address (sip:...).');
545
+ }
546
+ // A retry must not leave copies: one skill per name on an agent.
547
+ const same = name ? (await functions.listFunctions(agent_id)).find((f) => (f.name ?? '').trim().toLowerCase() === name.trim().toLowerCase()) : undefined;
548
+ if (same)
549
+ throw new Error(`The agent already has a skill named "${name}" (function_id ${same._id}). Change it with update_function instead of creating another.`);
550
+ // copilot turns configurationSchema into the executable form only on update: create, then configure.
551
+ const created = await functions.createFunction(agent_id, { type, name, description });
552
+ if (!configurationSchema)
553
+ return success(created);
554
+ try {
555
+ return success(await functions.updateFunction(agent_id, created._id, { type, description: description ?? '', configurationSchema }));
556
+ }
557
+ catch (err) {
558
+ // Leave no half-made skill behind: remove the empty one, then say why nothing was saved.
559
+ const why = err instanceof Error ? err.message : String(err);
560
+ try {
561
+ await functions.deleteFunction(agent_id, created._id);
562
+ }
563
+ catch {
564
+ throw new Error(`Skill ${created._id} was created, but its configuration was not saved (${why}). Remove it with delete_function, or fix it with update_function.`);
565
+ }
566
+ throw new Error(`Nothing was saved: the skill's configuration was refused (${why}). Fix it and call create_function again.`);
567
+ }
178
568
  }
179
569
  catch (err) {
180
570
  return error(err);
181
571
  }
182
572
  });
183
- server.tool('update_function', 'Update an existing function (tool) for an agent', {
573
+ reg.tool('update_function', 'Change a skill of an agent (name, description, configurationSchema).', {
184
574
  agent_id: z.string().describe('The agent ID'),
185
575
  function_id: z.string().describe('The function ID'),
186
576
  description: z.string().optional().describe('Updated description'),
187
577
  name: z.string().optional().describe('Updated function name'),
188
- url: z.string().optional().describe('Updated endpoint URL'),
189
- method: z.string().optional().describe('Updated HTTP method'),
578
+ url: z.string().optional().describe('rest_api: new endpoint URL'),
579
+ method: z.string().optional().describe('rest_api: new HTTP method'),
580
+ payload: z.union([z.string(), z.record(z.any())]).optional().describe('rest_api: new request body (JSON) with {{parameter}} placeholders'),
581
+ response_field: z.string().optional().describe('rest_api: new response dot-path'),
582
+ output_instructions: z.string().optional().describe('New text the agent says with the answer; {{response.field}} reads the API answer'),
583
+ craft_response: z.boolean().optional().describe('true: the agent phrases the answer; false: it goes to the customer as is'),
584
+ ...skillAuthFields,
585
+ configuration: jsonObject().optional().describe('Other configurationSchema fields to change (an object, or JSON text); the rest of the saved configuration is kept'),
586
+ transfer_to: z.string().optional().describe('transfer_call: new phone number (E.164) or SIP address; replaces the saved destination'),
190
587
  }, async (args) => {
191
588
  try {
192
- const { agent_id, function_id, ...input } = args;
193
- const result = await functions.updateFunction(agent_id, function_id, input);
194
- return success(result);
589
+ const { agent_id } = args;
590
+ // copilot needs type and description on every update and replaces the configuration whole.
591
+ const current = await skillOf(agent_id, args.function_id);
592
+ const function_id = current._id;
593
+ const input = {
594
+ type: current.type, description: args.description ?? current.description ?? '',
595
+ };
596
+ if (args.name)
597
+ input.name = args.name;
598
+ const configurationSchema = skillConfiguration(current.type, withAuthorization(args), current.configurationSchema ?? {});
599
+ if (configurationSchema)
600
+ input.configurationSchema = configurationSchema;
601
+ return success(await functions.updateFunction(agent_id, function_id, input));
195
602
  }
196
603
  catch (err) {
197
604
  return error(err);
198
605
  }
199
- });
200
- server.tool('delete_function', 'Delete a function (tool) from an agent', {
606
+ }, liveChange('a skill', ['function_id']));
607
+ reg.tool('delete_function', 'Remove a skill from an agent.', {
201
608
  agent_id: z.string().describe('The agent ID'),
202
609
  function_id: z.string().describe('The function ID to delete'),
203
610
  }, async (args) => {
204
611
  try {
205
- await functions.deleteFunction(args.agent_id, args.function_id);
206
- return success({ deleted: true, agent_id: args.agent_id, function_id: args.function_id });
612
+ const skill = await skillOf(args.agent_id, args.function_id);
613
+ await functions.deleteFunction(args.agent_id, skill._id);
614
+ return success({ deleted: true, agent_id: args.agent_id, function_id: skill._id, name: skill.name });
207
615
  }
208
616
  catch (err) {
209
617
  return error(err);
210
618
  }
211
619
  });
212
- server.tool('test_function', 'Test a function (tool) with optional parameters', {
620
+ reg.tool('test_function', 'Run a skill once with sample parameters and return what its API answered. Calls the real endpoint.', {
213
621
  agent_id: z.string().describe('The agent ID'),
214
622
  function_id: z.string().describe('The function ID to test'),
215
- params: z.record(z.unknown()).optional().describe('Test parameters to send'),
623
+ params: jsonObject().optional().describe('Sample values for the skill\'s {{parameter}} placeholders (an object, or JSON text)'),
216
624
  }, async (args) => {
217
625
  try {
218
- const result = await functions.testFunction(args.agent_id, args.function_id, args.params);
219
- return success(result);
626
+ // copilot's test route runs the configuration it is sent (not the saved one) and sends the body as is:
627
+ // send the saved configuration with the sample values in place of its placeholders.
628
+ const current = await skillOf(args.agent_id, args.function_id);
629
+ const params = (args.params ?? {});
630
+ const fill = (v) => (typeof v === 'string'
631
+ ? v.replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (m, k) => (k in params ? String(params[k]) : m))
632
+ : v);
633
+ const config = Object.fromEntries(Object.entries(current.configurationSchema ?? {}).map(([k, v]) => [k, fill(v)]));
634
+ return success(await functions.testFunction(args.agent_id, current._id, config));
220
635
  }
221
636
  catch (err) {
222
637
  return error(err);
223
638
  }
224
639
  });
225
- server.tool('export_functions', 'Export all functions from an agent', { agent_id: z.string().describe('The agent ID') }, async (args) => {
640
+ reg.tool('export_functions', 'Export every skill of an agent as a JSON file (to copy them or keep a backup before import_functions).', { agent_id: z.string().describe('The agent ID') }, async (args) => {
226
641
  try {
227
642
  const result = await functions.exportFunctions(args.agent_id);
228
643
  return success(result);
@@ -231,7 +646,7 @@ export function registerTools(server, client) {
231
646
  return error(err);
232
647
  }
233
648
  });
234
- server.tool('import_functions', 'Import functions into an agent from a URL (e.g. OpenAPI spec)', {
649
+ reg.tool('import_functions', 'Import functions into an agent from a URL (e.g. OpenAPI spec)', {
235
650
  agent_id: z.string().describe('The agent ID'),
236
651
  url: z.string().describe('URL to import functions from'),
237
652
  }, async (args) => {
@@ -243,36 +658,73 @@ export function registerTools(server, client) {
243
658
  return error(err);
244
659
  }
245
660
  });
246
- server.tool('list_function_parameters', 'List parameters for a function', {
661
+ reg.tool('generate_skills_from_doc', 'Propose skills from an API document (file path on this machine) or pasted text. Creates nothing: review the proposal, then call bulk_create_skills.', {
662
+ agent_id: z.string().describe('The agent ID'),
663
+ file_path: z.string().optional().describe('Path of a document on this machine'),
664
+ text: z.string().optional().describe('Text to read instead of a file'),
665
+ }, async (args) => {
666
+ try {
667
+ if (!args.file_path && !args.text)
668
+ throw new Error('Give file_path or text.');
669
+ const proposal = await functions.generateFromDoc(args.agent_id, { filePath: args.file_path, text: args.text });
670
+ return success({ proposed: proposedSkills(proposal), raw: proposedSkills(proposal) ? undefined : proposal });
671
+ }
672
+ catch (err) {
673
+ return error(err);
674
+ }
675
+ });
676
+ reg.tool('bulk_create_skills', 'Create several skills on an agent at once, for example the proposal from generate_skills_from_doc. Names are checked first; a failure part-way can leave some created.', {
677
+ agent_id: z.string().describe('The agent ID'),
678
+ functions: z.array(skillDefinition).min(1).describe('Skill definitions'),
679
+ }, async (args) => {
680
+ try {
681
+ const skills = args.functions.map((f) => {
682
+ const { url, method, payload, response_field, output_instructions, transfer_to, auth_type, auth_token, auth_key, auth_value, auth_username, auth_password, ...rest } = f;
683
+ const built = skillConfiguration(String(f.type), withAuthorization({
684
+ url: url, method: method, payload, response_field: response_field,
685
+ output_instructions: output_instructions, transfer_to: transfer_to,
686
+ auth_type, auth_token, auth_key, auth_value, auth_username, auth_password,
687
+ configuration: rest.configurationSchema,
688
+ }));
689
+ return built ? { ...rest, configurationSchema: built } : rest;
690
+ });
691
+ return success(await functions.bulkCreate(args.agent_id, skills));
692
+ }
693
+ catch (err) {
694
+ return error(err);
695
+ }
696
+ });
697
+ reg.tool('list_function_parameters', 'The parameters a skill collects from the customer before it runs.', {
247
698
  agent_id: z.string().describe('The agent ID'),
248
699
  function_id: z.string().describe('The function ID'),
249
700
  }, async (args) => {
250
701
  try {
251
- const result = await functions.listParameters(args.agent_id, args.function_id);
702
+ const result = await functions.listParameters(args.agent_id, (await skillOf(args.agent_id, args.function_id))._id);
252
703
  return success(result);
253
704
  }
254
705
  catch (err) {
255
706
  return error(err);
256
707
  }
257
708
  });
258
- server.tool('add_function_parameter', 'Add a parameter to a function', {
709
+ reg.tool('add_function_parameter', 'Add a parameter the skill should collect (name, type, description, required).', {
259
710
  agent_id: z.string().describe('The agent ID'),
260
711
  function_id: z.string().describe('The function ID'),
261
712
  name: z.string().describe('Parameter name'),
262
713
  type: z.string().describe('Parameter type (string, number, boolean)'),
263
714
  description: z.string().optional().describe('Parameter description'),
264
715
  required: z.boolean().optional().describe('Whether the parameter is required'),
716
+ allowed_values: z.array(z.string()).optional().describe('Fixed choices the customer picks from (shown as buttons); omit for free text'),
265
717
  }, async (args) => {
266
718
  try {
267
- const { agent_id, function_id, ...param } = args;
268
- const result = await functions.addParameter(agent_id, function_id, param);
719
+ const { agent_id, function_id, allowed_values, ...param } = args;
720
+ const result = await functions.addParameter(agent_id, (await skillOf(agent_id, function_id))._id, { ...param, allowedValues: allowed_values });
269
721
  return success(result);
270
722
  }
271
723
  catch (err) {
272
724
  return error(err);
273
725
  }
274
726
  });
275
- server.tool('update_function_parameter', 'Update a function parameter', {
727
+ reg.tool('update_function_parameter', 'Change a skill parameter.', {
276
728
  agent_id: z.string().describe('The agent ID'),
277
729
  function_id: z.string().describe('The function ID'),
278
730
  parameter_id: z.string().describe('The parameter ID'),
@@ -280,39 +732,41 @@ export function registerTools(server, client) {
280
732
  type: z.string().optional().describe('Updated type'),
281
733
  description: z.string().optional().describe('Updated description'),
282
734
  required: z.boolean().optional().describe('Updated required flag'),
735
+ allowed_values: z.array(z.string()).optional().describe('New fixed choices; [] removes them. Anything not given stays as it is'),
283
736
  }, async (args) => {
284
737
  try {
285
- const { agent_id, function_id, parameter_id, ...param } = args;
286
- const result = await functions.updateParameter(agent_id, function_id, parameter_id, param);
738
+ const { agent_id, function_id, parameter_id, allowed_values, ...param } = args;
739
+ const result = await functions.updateParameter(agent_id, (await skillOf(agent_id, function_id))._id, parameter_id, { ...param, allowedValues: allowed_values });
287
740
  return success(result);
288
741
  }
289
742
  catch (err) {
290
743
  return error(err);
291
744
  }
292
745
  });
293
- server.tool('delete_function_parameter', 'Delete a function parameter', {
746
+ reg.tool('delete_function_parameter', 'Remove a skill parameter.', {
294
747
  agent_id: z.string().describe('The agent ID'),
295
748
  function_id: z.string().describe('The function ID'),
296
749
  parameter_id: z.string().describe('The parameter ID'),
297
750
  }, async (args) => {
298
751
  try {
299
- await functions.deleteParameter(args.agent_id, args.function_id, args.parameter_id);
752
+ await functions.deleteParameter(args.agent_id, (await skillOf(args.agent_id, args.function_id))._id, args.parameter_id);
300
753
  return success({ deleted: true });
301
754
  }
302
755
  catch (err) {
303
756
  return error(err);
304
757
  }
305
758
  });
306
- server.tool('list_master_functions', 'List available master function templates', {}, async () => {
759
+ reg.tool('list_master_functions', 'Built-in skill types an agent can use as starting points (pass agent_id to get the ones for its channels, e.g. voice).', { agent_id: z.string().optional().describe('Agent whose channels (WEB, VOICE) decide which templates apply') }, async (args) => {
307
760
  try {
308
- const result = await functions.listMasterFunctions();
761
+ const channels = args.agent_id ? (await agents.getAgent(args.agent_id)).channels : undefined;
762
+ const result = await functions.listMasterFunctions(channels?.map((c) => (typeof c === 'string' ? c : String(c.type ?? ''))).filter(Boolean));
309
763
  return success(result);
310
764
  }
311
765
  catch (err) {
312
766
  return error(err);
313
767
  }
314
768
  });
315
- server.tool('list_llm_models', 'List available LLM models', {}, async () => {
769
+ reg.tool('list_llm_models', 'LLM providers and models an agent can run on (use provider/model with set_config).', {}, async () => {
316
770
  try {
317
771
  const result = await functions.listLLMModels();
318
772
  return success(result);
@@ -324,27 +778,27 @@ export function registerTools(server, client) {
324
778
  // ---------------------------------------------------------------------------
325
779
  // Knowledge base tools
326
780
  // ---------------------------------------------------------------------------
327
- server.tool('list_knowledge_bases', 'List all knowledge bases in the organization', { limit: z.number().optional().describe('Maximum number of KBs to return') }, async (args) => {
781
+ reg.tool('list_knowledge_bases', 'List the org\'s knowledge bases (what agents can look answers up in), with the agents each is linked to.', {}, async () => {
328
782
  try {
329
- let result = await kb.listKBs();
330
- if (args.limit)
331
- result = result.slice(0, args.limit);
332
- return success(result);
783
+ // A website base lists every crawled page (100k characters for a large site); get_knowledge_base has them.
784
+ const kbs = (await kb.listKBs());
785
+ const pages = await orgPages(client);
786
+ return success(kbs.map(({ fileWebSources, kbSampleFaqs: _faqs, ...rest }) => ({ ...rest, sources: fileWebSources?.length ?? 0, url: `${pages}/ai-studio/knowledge-base/${rest._id}` })));
333
787
  }
334
788
  catch (err) {
335
789
  return error(err);
336
790
  }
337
791
  });
338
- server.tool('get_knowledge_base', 'Get details of a specific knowledge base', { kb_id: z.string().describe('The knowledge base ID') }, async (args) => {
792
+ reg.tool('get_knowledge_base', 'One knowledge base with its website and file sources and their crawl status.', { kb_id: z.string().describe('The knowledge base ID') }, async (args) => {
339
793
  try {
340
794
  const result = await kb.getKB(args.kb_id);
341
- return success(result);
795
+ return success({ ...result, url: `${await orgPages(client)}/ai-studio/knowledge-base/${args.kb_id}` });
342
796
  }
343
797
  catch (err) {
344
798
  return error(err);
345
799
  }
346
800
  });
347
- server.tool('create_knowledge_base', 'Create a new knowledge base', {
801
+ reg.tool('create_knowledge_base', 'Create a knowledge base. type QNA (question/answer pairs), URL (websites) or FILE (documents).', {
348
802
  name: z.string().describe('Knowledge base name'),
349
803
  type: z.string().describe('Knowledge base type'),
350
804
  }, async (args) => {
@@ -356,7 +810,7 @@ export function registerTools(server, client) {
356
810
  return error(err);
357
811
  }
358
812
  });
359
- server.tool('update_knowledge_base', 'Update an existing knowledge base', {
813
+ reg.tool('update_knowledge_base', 'Rename a knowledge base.', {
360
814
  kb_id: z.string().describe('The knowledge base ID'),
361
815
  name: z.string().optional().describe('Updated name'),
362
816
  description: z.string().optional().describe('Updated description'),
@@ -374,7 +828,7 @@ export function registerTools(server, client) {
374
828
  return error(err);
375
829
  }
376
830
  });
377
- server.tool('delete_knowledge_base', 'Delete a knowledge base', { kb_id: z.string().describe('The knowledge base ID to delete') }, async (args) => {
831
+ reg.tool('delete_knowledge_base', 'Delete a knowledge base with all its sources. It must be unlinked from every agent first.', { kb_id: z.string().describe('The knowledge base ID to delete') }, async (args) => {
378
832
  try {
379
833
  await kb.deleteKB(args.kb_id);
380
834
  return success({ deleted: true, kb_id: args.kb_id });
@@ -383,7 +837,7 @@ export function registerTools(server, client) {
383
837
  return error(err);
384
838
  }
385
839
  });
386
- server.tool('list_kb_data', 'List data sources in a knowledge base', {
840
+ reg.tool('list_kb_data', 'The question/answer pairs typed into a knowledge base (website and file sources are listed by get_knowledge_base).', {
387
841
  kb_id: z.string().describe('The knowledge base ID'),
388
842
  page: z.number().optional().describe('Page number'),
389
843
  limit: z.number().optional().describe('Items per page'),
@@ -401,7 +855,7 @@ export function registerTools(server, client) {
401
855
  return error(err);
402
856
  }
403
857
  });
404
- server.tool('add_kb_data', 'Add a data source entry to a knowledge base. For URL scraping use scrape_url instead. For file uploads use upload_kb_file.', {
858
+ reg.tool('add_kb_data', 'Add a data source entry to a knowledge base. For URL scraping use scrape_url instead. For file uploads use upload_kb_file.', {
405
859
  kb_id: z.string().describe('The knowledge base ID'),
406
860
  question: z.string().optional().describe('FAQ question'),
407
861
  answer: z.string().optional().describe('FAQ answer'),
@@ -421,7 +875,7 @@ export function registerTools(server, client) {
421
875
  return error(err);
422
876
  }
423
877
  });
424
- server.tool('scrape_url', 'Scrape a website URL and add it as a data source to a knowledge base. Initiates async crawling.', {
878
+ reg.tool('scrape_url', 'Scrape a website URL and add it as a data source to a knowledge base. Initiates async crawling.', {
425
879
  kb_id: z.string().describe('The knowledge base ID'),
426
880
  url: z.string().describe('URL to scrape'),
427
881
  crawl_mode: z.string().optional().describe('Crawl mode: "full" (entire site) or "selected"'),
@@ -434,7 +888,7 @@ export function registerTools(server, client) {
434
888
  return error(err);
435
889
  }
436
890
  });
437
- server.tool('upload_kb_file', 'Upload a file to a knowledge base', {
891
+ reg.tool('upload_kb_file', 'Upload a document from this machine into a knowledge base; it is read in the background (see kb_scrape_status).', {
438
892
  kb_id: z.string().describe('The knowledge base ID'),
439
893
  file_path: z.string().describe('Path to the file to upload'),
440
894
  }, async (args) => {
@@ -446,7 +900,7 @@ export function registerTools(server, client) {
446
900
  return error(err);
447
901
  }
448
902
  });
449
- server.tool('update_kb_data', 'Update a data source entry in a knowledge base', {
903
+ reg.tool('update_kb_data', 'Change a question/answer pair of a knowledge base.', {
450
904
  kb_id: z.string().describe('The knowledge base ID'),
451
905
  source_id: z.string().describe('The data source ID'),
452
906
  question: z.string().optional().describe('Updated question'),
@@ -463,7 +917,7 @@ export function registerTools(server, client) {
463
917
  return error(err);
464
918
  }
465
919
  });
466
- server.tool('delete_kb_data', 'Delete a data source entry from a knowledge base', {
920
+ reg.tool('delete_kb_data', 'Delete a source of a knowledge base.', {
467
921
  kb_id: z.string().describe('The knowledge base ID'),
468
922
  source_id: z.string().describe('The data source ID to delete'),
469
923
  }, async (args) => {
@@ -475,7 +929,57 @@ export function registerTools(server, client) {
475
929
  return error(err);
476
930
  }
477
931
  });
478
- server.tool('link_kb', 'Link knowledge bases to an agent', {
932
+ reg.tool('kb_chunks', 'The text chunks the agent actually retrieves from one knowledge base source. Use it to check what was learned from a page or file.', { kb_id: z.string(), source_id: z.string() }, async (args) => {
933
+ try {
934
+ return success(await kb.chunks(args.kb_id, args.source_id));
935
+ }
936
+ catch (err) {
937
+ return error(err);
938
+ }
939
+ });
940
+ reg.tool('reimport_kb_source', 'Crawl a website source again after the site changed. Runs in the background; follow it with kb_scrape_status.', { kb_id: z.string(), source_id: z.string() }, async (args) => {
941
+ try {
942
+ return success(await kb.reimport(args.kb_id, args.source_id));
943
+ }
944
+ catch (err) {
945
+ return error(err);
946
+ }
947
+ });
948
+ reg.tool('kb_scrape_status', 'Progress of website crawls and file imports of a knowledge base (per source: status, job status, error).', { kb_id: z.string() }, async (args) => {
949
+ try {
950
+ return success(await kb.scrapeStatus(args.kb_id));
951
+ }
952
+ catch (err) {
953
+ return error(err);
954
+ }
955
+ });
956
+ reg.tool('kb_gaps', 'Questions the agents could not answer, to decide what to add to the knowledge base. themes: true groups them by topic.', {
957
+ agent_id: z.string().optional(),
958
+ kb_id: z.string().optional(),
959
+ themes: z.boolean().optional(),
960
+ days: z.number().int().min(1).max(365).optional().describe('With themes: look back this many days'),
961
+ include_test: z.boolean().optional(),
962
+ page: z.number().int().min(0).optional().describe('Starting at 0'),
963
+ limit: z.number().int().min(1).max(100).optional(),
964
+ }, async (args) => {
965
+ try {
966
+ if (args.themes)
967
+ return success(await kb.gapClusters({ days: args.days, page: args.page, limit: args.limit }));
968
+ return success(await kb.gaps({ agentId: args.agent_id, kbId: args.kb_id, includeTest: args.include_test, page: args.page, limit: args.limit }));
969
+ }
970
+ catch (err) {
971
+ return error(err);
972
+ }
973
+ });
974
+ reg.tool('kb_check', 'Could the org\'s knowledge bases answer this question? Returns whether it is answerable, the top score and the matching chunks.', { question: z.string() }, async (args) => {
975
+ try {
976
+ return success(await kb.check(args.question));
977
+ }
978
+ catch (err) {
979
+ return error(err);
980
+ }
981
+ });
982
+ reg.tool('link_kb', 'Let an agent answer from these knowledge bases.', {
479
983
  agent_id: z.string().describe('The agent ID'),
480
984
  kb_ids: z.array(z.string()).describe('Array of knowledge base IDs to link'),
481
985
  }, async (args) => {
@@ -487,7 +991,7 @@ export function registerTools(server, client) {
487
991
  return error(err);
488
992
  }
489
993
  });
490
- server.tool('unlink_kb', 'Unlink a knowledge base from an agent', {
994
+ reg.tool('unlink_kb', 'Stop an agent from using a knowledge base.', {
491
995
  agent_id: z.string().describe('The agent ID'),
492
996
  kb_id: z.string().describe('The knowledge base ID to unlink'),
493
997
  }, async (args) => {
@@ -499,7 +1003,7 @@ export function registerTools(server, client) {
499
1003
  return error(err);
500
1004
  }
501
1005
  });
502
- server.tool('export_kb', 'Export knowledge base data', {
1006
+ reg.tool('export_kb', 'Export the question/answer pairs of a knowledge base as a CSV or XLSX download link.', {
503
1007
  kb_id: z.string().describe('The knowledge base ID'),
504
1008
  format: z.string().optional().describe('Export format (e.g. "json", "csv")'),
505
1009
  }, async (args) => {
@@ -511,7 +1015,7 @@ export function registerTools(server, client) {
511
1015
  return error(err);
512
1016
  }
513
1017
  });
514
- server.tool('import_kb', 'Import data into a knowledge base', {
1018
+ reg.tool('import_kb', 'Import websites or files into a knowledge base; processed in the background (see kb_scrape_status).', {
515
1019
  kb_id: z.string().describe('The knowledge base ID'),
516
1020
  data: z.record(z.unknown()).describe('Data to import'),
517
1021
  }, async (args) => {
@@ -526,125 +1030,855 @@ export function registerTools(server, client) {
526
1030
  // ---------------------------------------------------------------------------
527
1031
  // Chat tool
528
1032
  // ---------------------------------------------------------------------------
529
- server.tool('chat_with_agent', 'Send a message to an agent and get a response. Not interactive — sends one message and returns the reply.', {
530
- agent_id: z.string().describe('The agent ID (bot type) to chat with'),
531
- message: z.string().describe('The message to send'),
532
- session_id: z.string().optional().describe('Optional session ID to continue a conversation'),
533
- }, async (args) => {
534
- try {
535
- const middlewareUrl = client.getHosts().middleware;
536
- const chat = new ChatSession(args.agent_id, middlewareUrl, args.session_id);
537
- // Track streaming state to distinguish greeting from response
538
- let receiving = false;
539
- let messageCount = 0;
540
- chat.on('stream', (data) => {
541
- if (data.isFirstChunk)
542
- receiving = true;
543
- if (data.streamFinished)
544
- receiving = false;
545
- });
546
- const greetingMessages = [];
547
- chat.on('message', (text) => {
548
- greetingMessages.push(text);
549
- messageCount++;
550
- });
551
- const sessionId = await chat.connect();
552
- // Wait for greeting to finish streaming (up to 8s)
553
- await new Promise((resolve) => {
554
- const check = setInterval(() => {
555
- if (!receiving) {
556
- clearInterval(check);
557
- resolve();
558
- }
559
- }, 100);
560
- setTimeout(() => { clearInterval(check); resolve(); }, 8000);
1033
+ const conversations = new ConversationsSDK(client);
1034
+ reg.tool('list_conversations', 'List conversations of one agent in the org, newest first, including test ones, with whether each is a test and whether the CLI opened it.', {
1035
+ agent_id: z.string().describe('Agent whose conversations to list'),
1036
+ limit: z.number().int().min(1).max(100).optional().describe('Rows per page (default 50, at most 100)'),
1037
+ page: z.number().int().min(0).optional().describe('Page, starting at 0'),
1038
+ live_only: z.boolean().optional().describe('Hide test conversations'),
1039
+ cli_only: z.boolean().optional().describe('Only conversations opened by the CLI'),
1040
+ }, async (args) => {
1041
+ try {
1042
+ const res = await conversations.list(args.agent_id, {
1043
+ limit: args.limit, page: args.page, includeTest: !args.live_only, cliOnly: args.cli_only,
561
1044
  });
562
- // Now swap the listener — next message is the actual response
563
- const greetingCount = messageCount;
564
- chat.removeAllListeners('message');
565
- const response = await new Promise((resolve, reject) => {
566
- const timeout = setTimeout(() => {
567
- chat.disconnect();
568
- reject(new Error('Agent did not respond within 60 seconds'));
569
- }, 60000);
570
- chat.on('message', (text) => {
571
- clearTimeout(timeout);
572
- resolve(text);
573
- });
574
- chat.send(args.message);
1045
+ return success(res.conversations.map((c) => ({
1046
+ conversation_id: c.request_id, created_at: c.created_at ?? c.createdAt, test: isTestConversation(c),
1047
+ cli: isCliConversation(c), first_message: c.first_text,
1048
+ })));
1049
+ }
1050
+ catch (err) {
1051
+ return error(err);
1052
+ }
1053
+ });
1054
+ reg.tool('get_conversation', 'Get one conversation and its messages (who said what, oldest first). format "testset" returns it as an evaluation test set with one scenario item to replay.', {
1055
+ agent_id: z.string().describe('Agent the conversation belongs to'),
1056
+ conversation_id: z.string().describe('Conversation (request) id'),
1057
+ format: z.enum(['messages', 'testset']).optional().describe('messages (default) or testset'),
1058
+ }, async (args) => {
1059
+ try {
1060
+ const projectId = await projectOfAgent(client, args.agent_id);
1061
+ const [conversation, messages] = await Promise.all([
1062
+ conversations.get(projectId, args.conversation_id),
1063
+ conversations.messages(projectId, args.conversation_id, args.agent_id),
1064
+ ]);
1065
+ if (args.format === 'testset')
1066
+ return success(conversationToTestSet(conversation, messages, args.agent_id));
1067
+ return success({
1068
+ conversation_id: conversation.request_id, test: conversation.isTestMode === true,
1069
+ url: conversationUrl(client, projectId, conversation.request_id),
1070
+ messages: messages.map((m) => ({ role: m.role, text: m.text, at: m.createdAt })),
575
1071
  });
576
- chat.disconnect();
577
- return success({ session_id: sessionId, greeting: greetingMessages.slice(0, greetingCount).join('\n'), response });
578
1072
  }
579
1073
  catch (err) {
580
1074
  return error(err);
581
1075
  }
582
1076
  });
583
- // ---------------------------------------------------------------------------
584
- // Eval tools
585
- // ---------------------------------------------------------------------------
586
- server.tool('run_evaluation', 'Trigger an evaluation run for a bot against a test set', {
587
- bot_id: z.string().describe('The bot ID to evaluate'),
588
- test_set_id: z.string().describe('The test set ID'),
589
- rubric_id: z.string().optional().describe('Optional rubric ID'),
590
- concurrency: z.number().optional().describe('Number of concurrent evaluations'),
591
- eval_model: z.string().optional().describe('Model to use for evaluation judging'),
592
- keep: z.boolean().optional().describe('Keep evaluation data after completion'),
593
- limit: z.number().optional().describe('Limit items to evaluate'),
594
- component_scope: z.string().optional().describe('Component scope filter'),
595
- component_ids: z.array(z.string()).optional().describe('Component ID filter'),
596
- mode: z.string().optional().describe('Evaluation mode'),
1077
+ reg.tool('mark_conversation_test', 'Mark conversations as test, as the dashboard does: removed from analytics and usage billing reversed.', {
1078
+ agent_id: z.string().describe('Agent the conversations belong to'),
1079
+ conversation_ids: z.array(z.string()).min(1).max(100).describe('Conversation (request) ids'),
597
1080
  }, async (args) => {
598
1081
  try {
599
- const input = {
600
- bot_id: args.bot_id,
601
- test_set_id: args.test_set_id,
602
- };
603
- if (args.rubric_id !== undefined)
604
- input.rubric_id = args.rubric_id;
605
- if (args.concurrency !== undefined)
606
- input.concurrency = args.concurrency;
607
- if (args.eval_model !== undefined)
608
- input.eval_model = args.eval_model;
609
- if (args.keep !== undefined)
610
- input.keep = args.keep;
611
- if (args.limit !== undefined)
612
- input.limit = args.limit;
613
- if (args.component_scope !== undefined)
614
- input.component_scope = args.component_scope;
615
- if (args.component_ids !== undefined)
616
- input.component_ids = args.component_ids;
617
- if (args.mode !== undefined)
618
- input.mode = args.mode;
619
- const result = await evals.triggerEvaluation(input);
620
- return success(result);
1082
+ const projectId = await projectOfAgent(client, args.agent_id);
1083
+ const done = [];
1084
+ for (const id of args.conversation_ids) {
1085
+ await conversations.markTest(projectId, id);
1086
+ done.push(id);
1087
+ }
1088
+ return success({ marked_test: done });
621
1089
  }
622
1090
  catch (err) {
623
1091
  return error(err);
624
1092
  }
625
1093
  });
626
- server.tool('get_eval_status', 'Get the status of an evaluation run', { run_id: z.string().describe('The evaluation run ID') }, async (args) => {
1094
+ const voice = new VoiceSDK(client);
1095
+ reg.tool('list_voices', 'Text-to-speech voices of a provider (cartesia default, or elevenlabs), filterable by language, gender and name.', {
1096
+ provider: z.enum(['cartesia', 'elevenlabs']).optional(),
1097
+ language: z.string().optional(),
1098
+ gender: z.enum(['masculine', 'feminine', 'gender_neutral']).optional(),
1099
+ search: z.string().optional(),
1100
+ limit: z.number().int().min(1).max(100).optional(),
1101
+ }, async (args) => {
627
1102
  try {
628
- const result = await evals.getRunStatus(args.run_id);
629
- return success(result);
1103
+ return success(await voice.listVoices({ provider: args.provider, language: args.language, gender: args.gender, q: args.search, limit: args.limit }));
630
1104
  }
631
1105
  catch (err) {
632
1106
  return error(err);
633
1107
  }
634
1108
  });
635
- server.tool('get_eval_results', 'Get the detailed results of a completed evaluation run', { run_id: z.string().describe('The evaluation run ID') }, async (args) => {
1109
+ reg.tool('list_voice_models', 'Speech provider and model catalog: stt (speech to text) or tts (text to speech).', { kind: z.enum(['stt', 'tts']) }, async (args) => {
636
1110
  try {
637
- const result = await evals.getRunResults(args.run_id);
638
- return success(result);
1111
+ return success(await voice.voiceModels(args.kind));
639
1112
  }
640
1113
  catch (err) {
641
1114
  return error(err);
642
1115
  }
643
1116
  });
644
- server.tool('get_bot_context', 'Get the full context of a bot (prompt, tools, KBs, LLM config)', { bot_id: z.string().describe('The bot ID') }, async (args) => {
1117
+ reg.tool('get_agent_voice', 'Voice settings of an agent (text-to-speech voice, speech-to-text, noise cancellation, interruption). They live on its phone number; an agent without one has none.', { agent_id: z.string() }, async (args) => {
645
1118
  try {
646
- const result = await evals.getBotContext(args.bot_id);
647
- return success(result);
1119
+ return success(await voice.getAgentVoice(args.agent_id));
1120
+ }
1121
+ catch (err) {
1122
+ return error(err);
1123
+ }
1124
+ });
1125
+ reg.tool('set_agent_voice', 'Change the text-to-speech voice of an agent. Take voice_id, name, gender, language and country_code from list_voices.', {
1126
+ agent_id: z.string(),
1127
+ provider: z.enum(['cartesia', 'elevenlabs']).optional(),
1128
+ voice_id: z.string(),
1129
+ name: z.string(),
1130
+ gender: z.string(),
1131
+ language: z.string(),
1132
+ country_code: z.string(),
1133
+ model: z.string().optional(),
1134
+ speed: z.number().optional(),
1135
+ }, async (args) => {
1136
+ try {
1137
+ return success(await voice.setAgentVoice(args.agent_id, {
1138
+ provider: args.provider ?? 'cartesia', voice_id: args.voice_id, name: args.name, gender: args.gender, language: args.language,
1139
+ countryCode: args.country_code, ...(args.model ? { model: args.model } : {}), ...(args.speed ? { settings: { speed: args.speed } } : {}),
1140
+ }));
1141
+ }
1142
+ catch (err) {
1143
+ return error(err);
1144
+ }
1145
+ });
1146
+ reg.tool('set_agent_transcriber', 'Change how a voice agent hears callers (speech-to-text): provider and model from list_voice_models (kind stt), language, and key terms to listen for (names, products).', {
1147
+ agent_id: z.string(),
1148
+ provider: z.string(),
1149
+ model: z.string(),
1150
+ language: z.string(),
1151
+ keyterms: z.array(z.string()).optional().describe('Up to 50 words or phrases, 100 characters each'),
1152
+ }, async (args) => {
1153
+ try {
1154
+ return success(await voice.setAgentTranscriber(args.agent_id, { provider: args.provider, model: args.model, language: args.language, keyterms: args.keyterms }));
1155
+ }
1156
+ catch (err) {
1157
+ return error(err);
1158
+ }
1159
+ });
1160
+ reg.tool('list_agent_numbers', 'Phone numbers attached to an agent, or (without agent_id) every number the org owns.', { agent_id: z.string().optional() }, async (args) => {
1161
+ try {
1162
+ return success(args.agent_id ? await voice.agentNumbers(args.agent_id) : await voice.orgNumbers());
1163
+ }
1164
+ catch (err) {
1165
+ return error(err);
1166
+ }
1167
+ });
1168
+ reg.tool('attach_phone_number', 'Attach a phone number the org already owns to an agent. Never buys a number.', { agent_id: z.string(), phone_number: z.string() }, async (args) => {
1169
+ try {
1170
+ return success(await voice.attachNumber(args.agent_id, args.phone_number));
1171
+ }
1172
+ catch (err) {
1173
+ return error(err);
1174
+ }
1175
+ });
1176
+ reg.tool('search_phone_numbers', 'Numbers available to buy for a voice agent (US, CA, GB, AU; local or toll-free). Free: nothing is bought. First ask the person what kind of number they want (local with which area code, or toll-free), then search and show 4-5 numbers with their location, and buy only the one they choose.', {
1177
+ area_code: z.string().optional().describe('Three-digit area code, e.g. 331'),
1178
+ contains: z.string().optional().describe('Digits the number should contain'),
1179
+ country: z.enum(['US', 'CA', 'GB', 'AU']).optional().describe('Default US'),
1180
+ toll_free: z.boolean().optional(),
1181
+ limit: z.number().int().min(1).max(20).optional().describe('How many (default 5)'),
1182
+ }, async (args) => {
1183
+ try {
1184
+ return success(await voice.searchNumbers({ areaCode: args.area_code, contains: args.contains, country: args.country, tollFree: args.toll_free, limit: args.limit }));
1185
+ }
1186
+ catch (err) {
1187
+ return error(err);
1188
+ }
1189
+ });
1190
+ reg.tool('get_phone_number', 'The phone number of a voice agent: number, location, type, and a summary of its voice and transcriber.', { agent_id: z.string() }, async (args) => {
1191
+ try {
1192
+ return success(await voice.numberDetails(args.agent_id));
1193
+ }
1194
+ catch (err) {
1195
+ return error(err);
1196
+ }
1197
+ });
1198
+ reg.tool('buy_phone_number', 'Buy a number from search_phone_numbers and attach it to a voice agent (costs the org money every month; no undo here). Only after the person chose that exact number. The host asks the person to approve; without an approval form this is refused.', {
1199
+ agent_id: z.string(),
1200
+ phone_number: z.string().describe('The number the person chose, as search_phone_numbers returned it'),
1201
+ country: z.enum(['US', 'CA', 'GB', 'AU']).optional(),
1202
+ toll_free: z.boolean().optional(),
1203
+ }, async (args, meta) => {
1204
+ try {
1205
+ // A purchase needs the person, not the model: confirm: true from a host without approval forms is not enough.
1206
+ if (!meta.confirmedByPerson)
1207
+ throw new Error('Buying a number needs a person to approve it in an approval form, and this host cannot show one. Nothing was bought. Buy it in the dashboard, or with `indemn agents numbers buy` in a terminal.');
1208
+ return success(await voice.buyNumber(args.agent_id, args.phone_number, { country: args.country, tollFree: args.toll_free }));
1209
+ }
1210
+ catch (err) {
1211
+ return error(err);
1212
+ }
1213
+ });
1214
+ reg.tool('detach_phone_number', 'Detach a phone number from an agent; the org keeps owning it.', { agent_id: z.string(), phone_number: z.string() }, async (args) => {
1215
+ try {
1216
+ return success(await voice.detachNumber(args.agent_id, args.phone_number));
1217
+ }
1218
+ catch (err) {
1219
+ return error(err);
1220
+ }
1221
+ });
1222
+ reg.tool('voice_preview', 'A browser link for a test voice call with an agent (marked as test, valid 1 hour). Give the join_url to the user; do not share it further.', { agent_id: z.string() }, async (args) => {
1223
+ try {
1224
+ return success(await voice.voicePreview(args.agent_id));
1225
+ }
1226
+ catch (err) {
1227
+ return error(err);
1228
+ }
1229
+ });
1230
+ const distribution = new DistributionSDK(client);
1231
+ reg.tool('list_distributions', 'Website paths that open agents (web widget) and the org\'s embed code.', {}, async () => {
1232
+ try {
1233
+ return success(await distribution.list());
1234
+ }
1235
+ catch (err) {
1236
+ return error(err);
1237
+ }
1238
+ });
1239
+ reg.tool('get_embed_code', 'The embed code and the website paths that open one agent.', { agent_id: z.string() }, async (args) => {
1240
+ try {
1241
+ return success(await distribution.embed(args.agent_id));
1242
+ }
1243
+ catch (err) {
1244
+ return error(err);
1245
+ }
1246
+ });
1247
+ reg.tool('create_distribution', 'Open an agent on a website path. Visitors of that page see it once the embed code is on the site.', { agent_id: z.string(), host: z.string(), path: z.string(), path_expression: z.string().optional(), greeting: z.string().optional(), allow_upload: z.boolean().optional() }, async (args) => {
1248
+ try {
1249
+ return success(await distribution.create({ agentId: args.agent_id, host: args.host, path: args.path, pathExpression: args.path_expression, greeting: args.greeting, allowUpload: args.allow_upload }));
1250
+ }
1251
+ catch (err) {
1252
+ return error(err);
1253
+ }
1254
+ });
1255
+ reg.tool('update_distribution', 'Change a website distribution of this org: which agent it opens, its greeting or page pattern.', { distribution_id: z.string(), agent_id: z.string().optional(), greeting: z.string().optional(), path_expression: z.string().optional() }, async (args) => {
1256
+ try {
1257
+ const changes = {};
1258
+ if (args.agent_id)
1259
+ changes.id_bot = args.agent_id;
1260
+ if (args.greeting)
1261
+ changes.greetingMessage = args.greeting;
1262
+ if (args.path_expression)
1263
+ changes.pathExpression = args.path_expression;
1264
+ return success(await distribution.update(args.distribution_id, changes));
1265
+ }
1266
+ catch (err) {
1267
+ return error(err);
1268
+ }
1269
+ });
1270
+ reg.tool('list_templates', 'Agent templates, shared by every org on the platform.', {}, async () => {
1271
+ try {
1272
+ return success(await distribution.listTemplates());
1273
+ }
1274
+ catch (err) {
1275
+ return error(err);
1276
+ }
1277
+ });
1278
+ reg.tool('create_template', 'Publish an agent of this org as a template that every org can copy.', { agent_id: z.string(), name: z.string(), description: z.string().optional(), category: z.array(z.string()).optional(), tags: z.array(z.string()).optional() }, async (args) => {
1279
+ try {
1280
+ return success(await distribution.createTemplate({ agentId: args.agent_id, name: args.name, description: args.description, category: args.category, tags: args.tags }));
1281
+ }
1282
+ catch (err) {
1283
+ return error(err);
1284
+ }
1285
+ });
1286
+ reg.tool('delete_template', 'Remove a template made from an agent of this org.', { template_id: z.string() }, async (args) => {
1287
+ try {
1288
+ return success(await distribution.deleteTemplate(args.template_id));
1289
+ }
1290
+ catch (err) {
1291
+ return error(err);
1292
+ }
1293
+ });
1294
+ reg.tool('create_agent_from_template', 'Create a new agent in this org copied from a template (prompt, model, skills, knowledge bases; not phone numbers, voice settings or distributions).', { template_id: z.string(), name: z.string().describe('Name of the new agent'), channels: z.array(z.string()).optional() }, async (args) => {
1295
+ try {
1296
+ if ((await agents.findByName(args.name)).length)
1297
+ throw new Error(`An agent named "${args.name}" already exists in this org.`);
1298
+ return success(await distribution.createFromTemplate(args.template_id, { name: args.name, channels: args.channels }));
1299
+ }
1300
+ catch (err) {
1301
+ return error(err);
1302
+ }
1303
+ });
1304
+ reg.tool('list_solutions', 'Solutions of the org with their connected systems.', {}, async () => {
1305
+ try {
1306
+ return success(await distribution.listSolutions());
1307
+ }
1308
+ catch (err) {
1309
+ return error(err);
1310
+ }
1311
+ });
1312
+ const analytics = new AnalyticsSDK(client);
1313
+ const orgSettings = new OrgSettingsSDK(client);
1314
+ const range = {
1315
+ date_from: z.string().optional().describe('YYYY-MM-DD, default 30 days ago'),
1316
+ date_to: z.string().optional().describe('YYYY-MM-DD, default today'),
1317
+ channel: z.enum(['web', 'voice', 'all']).optional(),
1318
+ agent_id: z.string().optional().describe('Only this agent'),
1319
+ };
1320
+ reg.tool('get_analytics', 'Dashboard analytics (test conversations excluded): metrics, model-performance, model-performance/bots, agent-volume, handoff-precursors, critical-feedback.', { kind: z.enum(['metrics', 'model-performance', 'model-performance/bots', 'agent-volume', 'handoff-precursors', 'critical-feedback']), ...range }, async (args) => {
1321
+ try {
1322
+ return success(await analytics.get(args.kind, { from: args.date_from, to: args.date_to, channel: args.channel, agentId: args.agent_id }));
1323
+ }
1324
+ catch (err) {
1325
+ return error(err);
1326
+ }
1327
+ });
1328
+ reg.tool('list_analytics_conversations', 'The conversations behind an analytics number, filtered by a dimension (outcome, feedback, sentiment, precursor, reason, critical_feedback_theme, attribute:<key>).', { ...range, dimension: z.string().optional(), value: z.string().optional(), search: z.string().optional(), page: z.number().int().min(1).optional(), page_size: z.number().int().min(1).max(200).optional() }, async (args) => {
1329
+ try {
1330
+ return success(await analytics.conversations({ from: args.date_from, to: args.date_to, channel: args.channel, agentId: args.agent_id, dimension: args.dimension, value: args.value, search: args.search, page: args.page, pageSize: args.page_size }));
1331
+ }
1332
+ catch (err) {
1333
+ return error(err);
1334
+ }
1335
+ });
1336
+ reg.tool('list_custom_charts', 'Custom charts configured for the org.', {}, async () => {
1337
+ try {
1338
+ return success(await analytics.charts());
1339
+ }
1340
+ catch (err) {
1341
+ return error(err);
1342
+ }
1343
+ });
1344
+ reg.tool('get_custom_chart', 'Data of one custom chart.', { chart_id: z.string(), ...range }, async (args) => {
1345
+ try {
1346
+ return success(await analytics.chart(args.chart_id, { from: args.date_from, to: args.date_to, channel: args.channel, agentId: args.agent_id }));
1347
+ }
1348
+ catch (err) {
1349
+ return error(err);
1350
+ }
1351
+ });
1352
+ reg.tool('get_org_settings', 'Org settings: timezone, business hours, holidays, handoff settings (read-only) and feature switches.', {}, async () => {
1353
+ try {
1354
+ return success(await orgSettings.get());
1355
+ }
1356
+ catch (err) {
1357
+ return error(err);
1358
+ }
1359
+ });
1360
+ reg.tool('set_org_timezone', 'Set the org timezone (business hours are read in it).', { timezone: z.string().describe('For example America/New_York') }, async (args) => {
1361
+ try {
1362
+ return success(await orgSettings.setTimezone(args.timezone));
1363
+ }
1364
+ catch (err) {
1365
+ return error(err);
1366
+ }
1367
+ });
1368
+ reg.tool('set_business_hours', 'Replace the org business hours. days: 0 Sunday to 6; an empty slots list closes that day.', { enabled: z.boolean(), hours: z.array(z.object({ day: z.number().int().min(0).max(6), slots: z.array(z.object({ start: z.string().describe('HH:MM'), end: z.string().describe('HH:MM') })) })).optional(), out_of_hours_message: z.string().optional() }, async (args) => {
1369
+ try {
1370
+ return success(await orgSettings.setBusinessHours({ businessHoursEnabled: args.enabled, businessHours: args.hours, ...(args.out_of_hours_message ? { businessHoursUtterance: args.out_of_hours_message } : {}) }));
1371
+ }
1372
+ catch (err) {
1373
+ return error(err);
1374
+ }
1375
+ });
1376
+ reg.tool('add_holiday', 'Close the org on a date.', { date: z.string().describe('YYYY-MM-DD'), name: z.string(), message: z.string().optional().describe('What agents say that day') }, async (args) => {
1377
+ try {
1378
+ return success(await orgSettings.addHolidays([{ name: args.name, date: args.date, ...(args.message ? { holidayFallbackUtterance: args.message } : {}) }]));
1379
+ }
1380
+ catch (err) {
1381
+ return error(err);
1382
+ }
1383
+ });
1384
+ reg.tool('set_org_admin_settings', 'Indemn staff only: the org Settings → Admin tab (feature switches, Intake Manager name, web AI disclosure, conversation timers). Give only what should change; get_org_settings shows the current values.', {
1385
+ conversation_summaries: z.boolean().optional().describe('AI Conversation Summaries: an AI summary when a conversation is closed'),
1386
+ canned_responses: z.boolean().optional().describe('Canned Responses: saved replies agents can insert'),
1387
+ indemn_branding: z.boolean().optional().describe('Show Indemn Branding: the Indemn logo in the chat widget'),
1388
+ underwriting_portal: z.boolean().optional().describe('Underwriting Portal: the Intake Manager item in the sidebar'),
1389
+ underwriting_portal_name: z.string().optional().describe('Name of the Intake Manager item in the sidebar (the default is "Intake Manager")'),
1390
+ smart_inbox: z.boolean().optional().describe('Smart Inbox: email triage and intake'),
1391
+ solutions: z.boolean().optional().describe('Solutions: renewal desks, campaigns and book management'),
1392
+ tickets: z.boolean().optional().describe('Tickets: the tickets page and the ticket scan when a conversation is resolved'),
1393
+ agent_testing: z.boolean().optional().describe('Agent Testing Mode: internal agent testing tools and debug details'),
1394
+ composer_dictation: z.boolean().optional().describe('Composer Dictation: a mic button in the agents\' chat box'),
1395
+ web_ai_disclosure: z.boolean().optional().describe('Show Web AI Disclosure below the first AI reply in the web widget (needs its text)'),
1396
+ web_ai_disclosure_text: z.string().optional().describe('The web AI disclosure text'),
1397
+ ongoing_to_ended_minutes: z.number().int().min(0).optional().describe('Ongoing → Ended: minutes a quiet AI conversation waits before it ends (at least 1; 0 = the default, 30 minutes)'),
1398
+ ended_to_resolved_minutes: z.number().int().min(0).optional().describe('Ended → Resolved: minutes before an ended conversation resolves (at least 10; 0 = the default, 24 hours)'),
1399
+ resolved_to_archived_minutes: z.number().int().min(0).optional().describe('Resolved → Archived: minutes before a resolved conversation is archived (at least 1; 0 = the default, 7 days)'),
1400
+ }, async (args) => {
1401
+ try {
1402
+ const { body } = await orgSettings.planAdmin(args);
1403
+ const saved = (await orgSettings.setAdminSettings(body));
1404
+ return success({ saved: Object.fromEntries(Object.keys(body).map((k) => [k, saved?.orgConfigs?.[k] ?? body[k]])) });
1405
+ }
1406
+ catch (err) {
1407
+ return error(err);
1408
+ }
1409
+ }, { preview: async (a) => `This will ${(await orgSettings.planAdmin(a)).changes.join('; ')}.` });
1410
+ reg.tool('list_audit_logs', 'Who changed what in the org, newest first, filtered here (the server has no filters).', { since: z.string().optional(), action: z.string().optional(), resource_type: z.string().optional(), user_id: z.string().optional(), limit: z.number().int().min(1).max(500).optional() }, async (args) => {
1411
+ try {
1412
+ return success(await new AuditSDK(client).list({ since: args.since, action: args.action, resourceType: args.resource_type, user: args.user_id, limit: args.limit }));
1413
+ }
1414
+ catch (err) {
1415
+ return error(err);
1416
+ }
1417
+ });
1418
+ const tickets = new TicketsSDK(client);
1419
+ const ticketStatus = z.enum(['not_started', 'in_progress', 'completed']);
1420
+ const ticketPriority = z.enum(['urgent', 'high', 'normal', 'low']);
1421
+ reg.tool('list_tickets', 'Support tickets of the org, newest first (the org must have tickets turned on).', { status: z.array(ticketStatus).optional(), assignee_user_id: z.string().optional(), search: z.string().optional(), limit: z.number().int().min(1).max(500).optional() }, async (args) => {
1422
+ try {
1423
+ return success(await tickets.list({ status: args.status, assignee: args.assignee_user_id, search: args.search, limit: args.limit }));
1424
+ }
1425
+ catch (err) {
1426
+ return error(err);
1427
+ }
1428
+ });
1429
+ reg.tool('get_ticket', 'One ticket with its conversations and related tickets.', { ticket_id: z.string() }, async (args) => {
1430
+ try {
1431
+ return success(await tickets.get(args.ticket_id));
1432
+ }
1433
+ catch (err) {
1434
+ return error(err);
1435
+ }
1436
+ });
1437
+ reg.tool('create_ticket', 'Create a ticket. Without force, similar open tickets are looked for first and nothing is created when some exist (the result lists them).', {
1438
+ title: z.string(), description: z.string().optional(), priority: ticketPriority.optional(),
1439
+ assignee_user_id: z.string().optional(), conversation_id: z.string().optional().describe('Conversation (request) id to attach'),
1440
+ force: z.boolean().optional(),
1441
+ }, async (args) => {
1442
+ try {
1443
+ return success(await tickets.create({
1444
+ title: args.title, description: args.description, priority: args.priority,
1445
+ ...(args.assignee_user_id ? { assignee: { type: 'user', target: args.assignee_user_id } } : {}),
1446
+ ...(args.conversation_id ? { conversations: [{ request_id: args.conversation_id }] } : {}),
1447
+ }, { force: args.force }));
1448
+ }
1449
+ catch (err) {
1450
+ return error(err);
1451
+ }
1452
+ });
1453
+ reg.tool('update_ticket', 'Change a ticket: fields, assignee (user id, or unassign: true), or close it with resolve: true (and an optional note).', {
1454
+ ticket_id: z.string(), title: z.string().optional(), description: z.string().optional(), notes: z.string().optional(),
1455
+ status: z.enum(['not_started', 'in_progress']).optional(), priority: ticketPriority.optional(),
1456
+ assignee_user_id: z.string().optional(), unassign: z.boolean().optional(), resolve: z.boolean().optional(), note: z.string().optional(),
1457
+ link_ticket_id: z.string().optional().describe('Link to another ticket'),
1458
+ }, async (args) => {
1459
+ try {
1460
+ const out = {};
1461
+ const changes = {};
1462
+ for (const k of ['title', 'description', 'notes', 'status', 'priority'])
1463
+ if (args[k] !== undefined)
1464
+ changes[k] = args[k];
1465
+ if (Object.keys(changes).length)
1466
+ out.updated = await tickets.update(args.ticket_id, changes);
1467
+ if (args.assignee_user_id || args.unassign)
1468
+ out.assigned = await tickets.assign(args.ticket_id, args.unassign ? null : { type: 'user', target: args.assignee_user_id });
1469
+ if (args.link_ticket_id)
1470
+ out.linked = await tickets.link(args.ticket_id, args.link_ticket_id);
1471
+ if (args.resolve)
1472
+ out.resolved = await tickets.resolve(args.ticket_id, args.note);
1473
+ if (!Object.keys(out).length)
1474
+ throw new Error('Nothing to change.');
1475
+ return success(out);
1476
+ }
1477
+ catch (err) {
1478
+ return error(err);
1479
+ }
1480
+ });
1481
+ reg.tool('archive_ticket', 'Archive a ticket.', { ticket_id: z.string() }, async (args) => {
1482
+ try {
1483
+ return success(await tickets.archive(args.ticket_id));
1484
+ }
1485
+ catch (err) {
1486
+ return error(err);
1487
+ }
1488
+ });
1489
+ reg.tool('list_ticket_rules', 'Rules that let agents open tickets from conversations.', { agent_id: z.string().optional().describe('Only rules enabled for this agent') }, async (args) => {
1490
+ try {
1491
+ return success(await tickets.listRules({ agentId: args.agent_id }));
1492
+ }
1493
+ catch (err) {
1494
+ return error(err);
1495
+ }
1496
+ });
1497
+ reg.tool('save_ticket_rule', 'Create a ticket rule (prompt required; checked for overlaps unless force) or change one (rule_id).', {
1498
+ rule_id: z.string().optional(), name: z.string().max(75).optional(), prompt: z.string().optional(),
1499
+ agent_ids: z.array(z.string()).optional().describe('Only these agents; default all'),
1500
+ assignee_user_id: z.string().optional(), enabled: z.boolean().optional(), force: z.boolean().optional(),
1501
+ }, async (args) => {
1502
+ try {
1503
+ return success(await tickets.saveRule({
1504
+ ruleId: args.rule_id, name: args.name, prompt: args.prompt, enabled: args.enabled,
1505
+ ...(args.agent_ids ? { scope: { kind: 'specific', agents: args.agent_ids } } : args.rule_id ? {} : { scope: { kind: 'all_agents', agents: [] } }),
1506
+ ...(args.assignee_user_id ? { assignment: { type: 'user', target: args.assignee_user_id } } : {}),
1507
+ }, { force: args.force }));
1508
+ }
1509
+ catch (err) {
1510
+ return error(err);
1511
+ }
1512
+ });
1513
+ reg.tool('delete_ticket_rule', 'Delete a ticket rule.', { rule_id: z.string() }, async (args) => {
1514
+ try {
1515
+ return success(await tickets.deleteRule(args.rule_id));
1516
+ }
1517
+ catch (err) {
1518
+ return error(err);
1519
+ }
1520
+ });
1521
+ reg.tool('get_billing', 'Read-only billing: status (subscription), invoices, unpaid (open invoices), usage_forecast, or plans. Nothing here changes a plan or moves money.', { what: z.enum(['status', 'invoices', 'unpaid', 'usage_forecast', 'plans']) }, async (args) => {
1522
+ try {
1523
+ const billing = new BillingSDK(client);
1524
+ const result = args.what === 'status' ? await billing.status()
1525
+ : args.what === 'invoices' ? await billing.invoices()
1526
+ : args.what === 'unpaid' ? await billing.unpaid()
1527
+ : args.what === 'usage_forecast' ? await billing.usageForecast()
1528
+ : await billing.plans();
1529
+ return success(result);
1530
+ }
1531
+ catch (err) {
1532
+ return error(err);
1533
+ }
1534
+ });
1535
+ reg.tool('get_campaigns', 'Read-only campaigns: list, one campaign (campaign_id), its counts / forecast / next-touchpoints / versions, templates, one template (template_id, versions: true for subject and body), or template skills.', {
1536
+ what: z.enum(['list', 'campaign', 'counts', 'forecast', 'next-touchpoints', 'versions', 'templates', 'template', 'template-skills']),
1537
+ campaign_id: z.string().optional(), template_id: z.string().optional(), versions: z.boolean().optional(),
1538
+ status: z.string().optional(), solution_id: z.string().optional(), channel: z.string().optional(),
1539
+ days: z.number().int().min(1).max(365).optional(), limit: z.number().int().min(1).max(500).optional(),
1540
+ }, async (args) => {
1541
+ try {
1542
+ const c = new CampaignsSDK(client);
1543
+ const need = (v, name) => { if (!v)
1544
+ throw new Error(`${name} is required for ${args.what}.`); return v; };
1545
+ switch (args.what) {
1546
+ case 'list': return success(await c.list({ status: args.status, solutionId: args.solution_id, limit: args.limit }));
1547
+ case 'campaign': return success(await c.get(need(args.campaign_id, 'campaign_id')));
1548
+ case 'templates': return success(await c.templates({ channel: args.channel, solutionId: args.solution_id, limit: args.limit }));
1549
+ case 'template': return success(await c.template(need(args.template_id, 'template_id'), Boolean(args.versions)));
1550
+ case 'template-skills': return success(await c.templateSkills(args.solution_id));
1551
+ default: return success(await c.detail(need(args.campaign_id, 'campaign_id'), args.what, args.days));
1552
+ }
1553
+ }
1554
+ catch (err) {
1555
+ return error(err);
1556
+ }
1557
+ });
1558
+ const platform = new PlatformSDK(client);
1559
+ const platformResource = z.enum(['agents', 'templates', 'components', 'connectors', 'connector-bindings', 'scenarios']);
1560
+ reg.tool('platform_list', 'List V2 platform items: agents, templates, components, connectors (shared catalogs), connector-bindings, scenarios (agent_id filters scenarios).', { resource: platformResource, agent_id: z.string().optional() }, async (args) => {
1561
+ try {
1562
+ return success(await platform.list(args.resource, args.agent_id ? { agent_id: args.agent_id } : undefined));
1563
+ }
1564
+ catch (err) {
1565
+ return error(err);
1566
+ }
1567
+ });
1568
+ reg.tool('platform_get', 'One V2 platform item by id.', { resource: platformResource, id: z.string() }, async (args) => {
1569
+ try {
1570
+ return success(await platform.get(args.resource, args.id));
1571
+ }
1572
+ catch (err) {
1573
+ return error(err);
1574
+ }
1575
+ });
1576
+ reg.tool('platform_save', 'Create (no id) or change (id) a V2 agent, template, connector binding or scenario. Components and connectors are read-only shared catalogs.', { resource: z.enum(['agents', 'templates', 'connector-bindings', 'scenarios']), id: z.string().optional(), body: z.record(z.unknown()) }, async (args) => {
1577
+ try {
1578
+ return success(args.id ? await platform.update(args.resource, args.id, args.body) : await platform.create(args.resource, args.body));
1579
+ }
1580
+ catch (err) {
1581
+ return error(err);
1582
+ }
1583
+ });
1584
+ reg.tool('platform_delete', 'Delete a V2 agent, template, connector binding or scenario.', { resource: z.enum(['agents', 'templates', 'connector-bindings', 'scenarios']), id: z.string() }, async (args) => {
1585
+ try {
1586
+ return success(await platform.remove(args.resource, args.id));
1587
+ }
1588
+ catch (err) {
1589
+ return error(err);
1590
+ }
1591
+ });
1592
+ reg.tool('validate_platform_template', 'Check a V2 template graph: components exist, edges point at real nodes.', { template_id: z.string() }, async (args) => {
1593
+ try {
1594
+ return success(await platform.validateTemplate(args.template_id));
1595
+ }
1596
+ catch (err) {
1597
+ return error(err);
1598
+ }
1599
+ });
1600
+ reg.tool('run_simulation', 'Run scenarios against a V2 agent; poll simulation_status with the job id.', { agent_id: z.string(), scenario_ids: z.array(z.string()).optional() }, async (args) => {
1601
+ try {
1602
+ return success(await platform.runSimulation(args.agent_id, args.scenario_ids));
1603
+ }
1604
+ catch (err) {
1605
+ return error(err);
1606
+ }
1607
+ });
1608
+ reg.tool('simulation_status', 'Simulation jobs (job_id for one) or results (result_id for one, or the latest of an agent).', { job_id: z.string().optional(), result_id: z.string().optional(), agent_id: z.string().optional() }, async (args) => {
1609
+ try {
1610
+ if (args.job_id)
1611
+ return success(await platform.simulationJob(args.job_id));
1612
+ if (args.result_id)
1613
+ return success(await platform.simulationResults({ resultId: args.result_id }));
1614
+ return success({ jobs: await platform.simulationJobs({ agentId: args.agent_id }), results: await platform.simulationResults({ agentId: args.agent_id }) });
1615
+ }
1616
+ catch (err) {
1617
+ return error(err);
1618
+ }
1619
+ });
1620
+ reg.tool('org_evaluations', 'Which agents of the org have a baseline evaluation (rubric and test set), and which are stale.', {}, async () => {
1621
+ try {
1622
+ return success(await platform.orgEvaluations());
1623
+ }
1624
+ catch (err) {
1625
+ return error(err);
1626
+ }
1627
+ });
1628
+ reg.tool('jarvis_create_job', 'Ask Jarvis to generate baseline evaluations for 1 to 50 agents of the org. Poll jarvis_jobs with the job id; per-agent failures are in progress.results.', { bot_ids: z.array(z.string()).min(1).max(50) }, async (args) => {
1629
+ try {
1630
+ return success(await platform.createJarvisJob(args.bot_ids));
1631
+ }
1632
+ catch (err) {
1633
+ return error(err);
1634
+ }
1635
+ });
1636
+ reg.tool('jarvis_jobs', 'Jarvis jobs of the org, or one job (job_id).', { job_id: z.string().optional(), status: z.enum(['pending', 'running', 'completed', 'failed']).optional() }, async (args) => {
1637
+ try {
1638
+ return success(args.job_id ? await platform.jarvisJob(args.job_id) : await platform.jarvisJobs(args.status));
1639
+ }
1640
+ catch (err) {
1641
+ return error(err);
1642
+ }
1643
+ });
1644
+ reg.tool('jarvis_chat', 'One turn with Jarvis, the V2 platform assistant. Returns its reply, the tools it used, and a conversation_id to continue. agent_id tells Jarvis which V1 agent this is about.', { message: z.string(), conversation_id: z.string().optional(), agent_id: z.string().optional() }, async (args) => {
1645
+ try {
1646
+ return success(await jarvisChat(client, args.message, { conversationId: args.conversation_id, context: args.agent_id ? { federation_bot_id: args.agent_id } : undefined }));
1647
+ }
1648
+ catch (err) {
1649
+ return error(err);
1650
+ }
1651
+ });
1652
+ reg.tool('chat_with_agent', 'Send one message to an agent and return its reply. Always a test conversation: never billed, never in analytics, shown with your name as tester. Pass the returned session_id to continue the same conversation.', {
1653
+ agent_id: z.string().describe('The agent ID to chat with'),
1654
+ message: z.string().describe('The message to send'),
1655
+ session_id: z.string().optional().describe('session_id from an earlier call, to continue that conversation'),
1656
+ end_conversation: z.boolean().optional().describe('End the conversation after this reply (default: keep it open so session_id can continue it)'),
1657
+ }, async (args) => {
1658
+ try {
1659
+ const chat = await openTestChat(client, args.agent_id, { sessionId: args.session_id });
1660
+ try {
1661
+ const { greeting, response } = await exchange(chat, args.message);
1662
+ return success({
1663
+ session_id: chat.sessionId,
1664
+ test_conversation: true,
1665
+ tester: chat.tester,
1666
+ url: chat.url,
1667
+ greeting: chat.resumed ? undefined : greeting,
1668
+ response,
1669
+ });
1670
+ }
1671
+ finally {
1672
+ await chat.session.disconnect({ end: args.end_conversation === true });
1673
+ }
1674
+ }
1675
+ catch (err) {
1676
+ return error(err);
1677
+ }
1678
+ });
1679
+ // ---------------------------------------------------------------------------
1680
+ // Eval tools
1681
+ // ---------------------------------------------------------------------------
1682
+ const testSetsForRun = new TestSetsSDK(client);
1683
+ reg.tool('run_evaluation', 'Start an evaluation of an agent against its test set (paid model calls, cannot be cancelled; asks first). The agent must be in the org and the test set must belong to it. date_from/date_to grade real conversations (transcript mode). Poll get_eval_status; eval_gate_verdict judges a finished run.', {
1684
+ agent_id: z.string().optional().describe('The agent ID to evaluate'),
1685
+ bot_id: z.string().optional().describe('Same as agent_id (older name)'),
1686
+ test_set_id: z.string().describe('The test set ID (must belong to this agent)'),
1687
+ rubric_id: z.string().optional().describe('Optional rubric ID'),
1688
+ concurrency: z.number().int().min(1).max(10).optional().describe('Parallel cases (default 1)'),
1689
+ eval_model: z.string().optional().describe('Judge model'),
1690
+ limit: z.number().int().min(1).optional().describe('Only the first N items'),
1691
+ component_scope: z.string().optional(),
1692
+ component_ids: z.array(z.string()).optional(),
1693
+ conversation_ids: z.array(z.string()).optional().describe('Transcript mode: these conversations'),
1694
+ date_from: z.string().optional().describe('Transcript mode: conversations from this ISO date'),
1695
+ date_to: z.string().optional().describe('Transcript mode: up to this ISO date, exclusive'),
1696
+ mode: z.enum(['transcript']).optional(),
1697
+ allow_other_agent: z.boolean().optional().describe('Run a test set written for another agent'),
1698
+ }, async (args) => {
1699
+ try {
1700
+ const agentId = args.agent_id ?? args.bot_id;
1701
+ if (!agentId)
1702
+ throw new Error('Give agent_id: the agent to evaluate.');
1703
+ await evals.assertAgentInOrg(agentId);
1704
+ const ts = await testSetsForRun.getTestSet(args.test_set_id);
1705
+ if (ts.agent_id && ts.agent_id !== agentId && !args.allow_other_agent) {
1706
+ throw new Error(`Test set ${args.test_set_id} was written for agent ${ts.agent_id}, not ${agentId}. Pass allow_other_agent: true to run it anyway.`);
1707
+ }
1708
+ if (Boolean(args.date_from) !== Boolean(args.date_to))
1709
+ throw new Error('date_from and date_to go together.');
1710
+ const input = { bot_id: agentId, test_set_id: args.test_set_id };
1711
+ if (args.rubric_id)
1712
+ input.rubric_id = args.rubric_id;
1713
+ if (args.concurrency)
1714
+ input.concurrency = args.concurrency;
1715
+ // Voice simulations share call rooms; the dashboard runs them one at a time.
1716
+ if ((ts.items ?? []).some((i) => i.type === 'voice_simulation'))
1717
+ input.concurrency = 1;
1718
+ if (args.eval_model)
1719
+ input.eval_model = args.eval_model;
1720
+ if (args.limit)
1721
+ input.limit = args.limit;
1722
+ if (args.component_scope)
1723
+ input.component_scope = args.component_scope;
1724
+ if (args.component_ids)
1725
+ input.component_ids = args.component_ids;
1726
+ if (args.conversation_ids)
1727
+ input.conversation_ids = args.conversation_ids;
1728
+ if (args.mode)
1729
+ input.mode = args.mode;
1730
+ if (args.date_from) {
1731
+ input.date_from = args.date_from;
1732
+ input.date_to = args.date_to;
1733
+ input.mode = 'transcript';
1734
+ }
1735
+ return success(await evals.triggerEvaluation(input));
1736
+ }
1737
+ catch (err) {
1738
+ return error(err);
1739
+ }
1740
+ });
1741
+ reg.tool('get_eval_status', 'Status and counts of an evaluation run (passed, failed, infrastructure errors, latency). While the run is still going it waits up to wait_seconds for it to finish, so call it again only if it is still running.', {
1742
+ run_id: z.string().describe('The evaluation run ID'),
1743
+ wait_seconds: z.number().int().min(0).max(60).optional().describe('How long to wait for a running run to finish (default 20; 0 = answer at once)'),
1744
+ }, async (args) => {
1745
+ try {
1746
+ // Waiting here instead of being called every few seconds: each call is a model turn for an agent.
1747
+ const deadline = Date.now() + 1000 * (args.wait_seconds ?? 20);
1748
+ let run = await evals.getRunInOrg(args.run_id);
1749
+ while (run.status !== 'completed' && run.status !== 'failed' && Date.now() < deadline) {
1750
+ await new Promise((resolve) => setTimeout(resolve, Math.min(5000, deadline - Date.now())));
1751
+ run = await evals.getRunStatus(args.run_id);
1752
+ }
1753
+ return success(run);
1754
+ }
1755
+ catch (err) {
1756
+ return error(err);
1757
+ }
1758
+ });
1759
+ reg.tool('eval_gate_verdict', 'Judge a finished run like the CI gate: pass rate = passed/total, optional p95 limit, failed runs fail by default.', {
1760
+ run_id: z.string(),
1761
+ min_pass_rate: z.number().min(0).max(1).optional(),
1762
+ max_p95_ms: z.number().optional(),
1763
+ error_policy: z.enum(['block', 'warn']).optional(),
1764
+ fail_on_error: z.boolean().optional(),
1765
+ }, async (args) => {
1766
+ try {
1767
+ const run = await evals.getRunInOrg(args.run_id);
1768
+ if (run.status !== 'completed' && run.status !== 'failed')
1769
+ return success({ finished: false, status: run.status });
1770
+ return success({ finished: true, ...gateVerdict(run, { minPassRate: args.min_pass_rate, maxP95Ms: args.max_p95_ms, errorPolicy: args.error_policy, failOnError: args.fail_on_error }) });
1771
+ }
1772
+ catch (err) {
1773
+ return error(err);
1774
+ }
1775
+ });
1776
+ reg.tool('get_eval_results', 'Per-case results of an evaluation run. response_format "concise" (default) keeps pass/fail, scores and failed criteria; "detailed" adds transcripts, tool calls and evidence.', {
1777
+ run_id: z.string().describe('The evaluation run ID'),
1778
+ response_format: z.enum(['concise', 'detailed']).optional(),
1779
+ failed_only: z.boolean().optional().describe('Only cases that did not pass'),
1780
+ }, async (args) => {
1781
+ try {
1782
+ await evals.getRunInOrg(args.run_id);
1783
+ let results = await evals.getRunResults(args.run_id);
1784
+ if (args.failed_only)
1785
+ results = results.filter((r) => r.passed !== true);
1786
+ if (args.response_format !== 'detailed') {
1787
+ results = results.map((r) => ({
1788
+ result_id: r.result_id, item_name: r.item_name, item_type: r.item_type, status: r.status, passed: r.passed,
1789
+ scores: r.scores, failed_criteria: Array.isArray(r.criteria_scores) ? r.criteria_scores.filter((c) => !c.passed) : undefined,
1790
+ failed_rules: Array.isArray(r.rubric_scores) ? r.rubric_scores.filter((c) => !c.passed) : undefined,
1791
+ error: r.error,
1792
+ }));
1793
+ }
1794
+ return success(results);
1795
+ }
1796
+ catch (err) {
1797
+ return error(err);
1798
+ }
1799
+ });
1800
+ reg.tool('get_bot_context', 'Everything the evaluator knows about an agent: prompt, tools, knowledge bases, LLM config.', { bot_id: z.string().describe('The agent ID') }, async (args) => {
1801
+ try {
1802
+ await evals.assertAgentInOrg(args.bot_id);
1803
+ return success(await evals.getBotContext(args.bot_id));
1804
+ }
1805
+ catch (err) {
1806
+ return error(err);
1807
+ }
1808
+ });
1809
+ reg.tool('eval_trends', 'Pass rate and latency over an agent\'s past runs, with drift when the pass rate dropped by 0.1 or more.', { agent_id: z.string(), test_set_id: z.string().optional(), limit: z.number().int().min(1).max(200).optional() }, async (args) => {
1810
+ try {
1811
+ await evals.assertAgentInOrg(args.agent_id);
1812
+ return success(await evals.trends(args.agent_id, args.test_set_id, args.limit));
1813
+ }
1814
+ catch (err) {
1815
+ return error(err);
1816
+ }
1817
+ });
1818
+ reg.tool('compare_eval_runs', 'Pass rate and p95 latency difference between two runs.', { run_a: z.string(), run_b: z.string() }, async (args) => {
1819
+ try {
1820
+ await evals.getRunInOrg(args.run_a);
1821
+ await evals.getRunInOrg(args.run_b);
1822
+ return success(await evals.compareRuns(args.run_a, args.run_b));
1823
+ }
1824
+ catch (err) {
1825
+ return error(err);
1826
+ }
1827
+ });
1828
+ reg.tool('eval_report_link', 'Have the evaluations service render the PDF report of a completed run; returns a link valid for 15 minutes.', { run_id: z.string(), force: z.boolean().optional().describe('Render again instead of reusing the last report') }, async (args) => {
1829
+ try {
1830
+ await evals.getRunInOrg(args.run_id);
1831
+ return success(await evals.exportReport(args.run_id, args.force));
1832
+ }
1833
+ catch (err) {
1834
+ return error(err);
1835
+ }
1836
+ });
1837
+ reg.tool('eval_quality_metrics', 'Voice quality metrics a test item may require (names for expected.voice.required_metrics).', {}, async () => {
1838
+ try {
1839
+ return success(await evals.qualityMetrics());
1840
+ }
1841
+ catch (err) {
1842
+ return error(err);
1843
+ }
1844
+ });
1845
+ reg.tool('list_prod_calls', 'Ingested production voice calls of an agent, newest first; search matches the transcript.', { agent_id: z.string(), search: z.string().optional(), limit: z.number().int().min(1).max(200).optional() }, async (args) => {
1846
+ try {
1847
+ await evals.assertAgentInOrg(args.agent_id);
1848
+ return success(await evals.prodCalls(args.agent_id, { search: args.search, limit: args.limit }));
1849
+ }
1850
+ catch (err) {
1851
+ return error(err);
1852
+ }
1853
+ });
1854
+ reg.tool('promote_prod_call', 'Add an ingested production voice call as a test item to the agent\'s "Regressions (review)" test set.', { session_id: z.string() }, async (args) => {
1855
+ try {
1856
+ const call = await evals.prodCall(args.session_id);
1857
+ if (call.bot_id)
1858
+ await evals.assertAgentInOrg(String(call.bot_id));
1859
+ return success(await evals.promoteProdCall(args.session_id));
1860
+ }
1861
+ catch (err) {
1862
+ return error(err);
1863
+ }
1864
+ });
1865
+ reg.tool('evaluate_transcript', 'Grade a replayed conversation against a baseline with the agent\'s stored evaluator (paid model calls).', {
1866
+ agent_id: z.string(),
1867
+ baseline_transcript: z.array(z.object({ sender: z.enum(['user', 'bot']), text: z.string() })),
1868
+ replay_transcript: z.array(z.object({ sender: z.enum(['user', 'bot']), text: z.string() })),
1869
+ }, async (args) => {
1870
+ try {
1871
+ await evals.assertAgentInOrg(args.agent_id);
1872
+ return success(await evals.evaluateTranscript(args));
1873
+ }
1874
+ catch (err) {
1875
+ return error(err);
1876
+ }
1877
+ });
1878
+ reg.tool('list_evaluator_configs', 'Stored evaluator configs of an agent (which judges and rubric a run uses).', { agent_id: z.string() }, async (args) => {
1879
+ try {
1880
+ await evals.assertAgentInOrg(args.agent_id);
1881
+ return success(await evals.listEvaluatorConfigs(args.agent_id));
648
1882
  }
649
1883
  catch (err) {
650
1884
  return error(err);
@@ -653,8 +1887,9 @@ export function registerTools(server, client) {
653
1887
  // ---------------------------------------------------------------------------
654
1888
  // Rubric tools
655
1889
  // ---------------------------------------------------------------------------
656
- server.tool('list_rubrics', 'List evaluation rubrics, optionally filtered by agent', { agent_id: z.string().optional().describe('Filter rubrics by agent ID') }, async (args) => {
1890
+ reg.tool('list_rubrics', 'Evaluation rubrics (behaviour rules an agent is graded on) of one agent.', { agent_id: z.string().describe('Agent ID (rubrics of other orgs are never listed)') }, async (args) => {
657
1891
  try {
1892
+ await evals.assertAgentInOrg(args.agent_id);
658
1893
  const result = await rubrics.listRubrics(args.agent_id);
659
1894
  return success(result);
660
1895
  }
@@ -662,30 +1897,32 @@ export function registerTools(server, client) {
662
1897
  return error(err);
663
1898
  }
664
1899
  });
665
- server.tool('get_rubric', 'Get details of a specific rubric', { rubric_id: z.string().describe('The rubric ID') }, async (args) => {
1900
+ reg.tool('get_rubric', 'One rubric with its rules.', { rubric_id: z.string().describe('The rubric ID') }, async (args) => {
666
1901
  try {
667
- const result = await rubrics.getRubric(args.rubric_id);
1902
+ const result = await rubricInOrg(args.rubric_id);
668
1903
  return success(result);
669
1904
  }
670
1905
  catch (err) {
671
1906
  return error(err);
672
1907
  }
673
1908
  });
674
- server.tool('create_rubric', 'Create a new evaluation rubric', {
1909
+ reg.tool('create_rubric', 'Create a rubric: 3 to 6 behaviour rules an agent must follow, each with a severity.', {
675
1910
  name: z.string().describe('Rubric name'),
676
- rules: z.array(z.record(z.unknown())).describe('Array of rubric rules'),
1911
+ rules: z.array(rubricRuleSchema).describe('The rubric rules'),
677
1912
  agent_id: z.string().optional().describe('Agent ID to associate the rubric with'),
678
1913
  description: z.string().optional().describe('Rubric description'),
679
1914
  severity_definitions: z.record(z.string()).optional().describe('Severity level definitions'),
680
1915
  }, async (args) => {
681
1916
  try {
682
- const input = { name: args.name, rules: args.rules };
1917
+ const input = { name: args.name, rules: args.rules.map(toRubricRule) };
683
1918
  if (args.agent_id !== undefined)
684
1919
  input.agent_id = args.agent_id;
685
1920
  if (args.description !== undefined)
686
1921
  input.description = args.description;
687
1922
  if (args.severity_definitions !== undefined)
688
1923
  input.severity_definitions = args.severity_definitions;
1924
+ if (args.agent_id)
1925
+ await evals.assertAgentInOrg(args.agent_id);
689
1926
  const result = await rubrics.createRubric(input);
690
1927
  return success(result);
691
1928
  }
@@ -693,11 +1930,11 @@ export function registerTools(server, client) {
693
1930
  return error(err);
694
1931
  }
695
1932
  });
696
- server.tool('update_rubric', 'Update an existing rubric', {
1933
+ reg.tool('update_rubric', 'Change a rubric; this saves a new version.', {
697
1934
  rubric_id: z.string().describe('The rubric ID'),
698
1935
  name: z.string().optional().describe('Updated name'),
699
1936
  description: z.string().optional().describe('Updated description'),
700
- rules: z.array(z.record(z.unknown())).optional().describe('Updated rules'),
1937
+ rules: z.array(rubricRuleSchema).optional().describe('Updated rules'),
701
1938
  }, async (args) => {
702
1939
  try {
703
1940
  const input = {};
@@ -706,7 +1943,8 @@ export function registerTools(server, client) {
706
1943
  if (args.description !== undefined)
707
1944
  input.description = args.description;
708
1945
  if (args.rules !== undefined)
709
- input.rules = args.rules;
1946
+ input.rules = args.rules.map(toRubricRule);
1947
+ await rubricInOrg(args.rubric_id);
710
1948
  const result = await rubrics.updateRubric(args.rubric_id, input);
711
1949
  return success(result);
712
1950
  }
@@ -714,8 +1952,9 @@ export function registerTools(server, client) {
714
1952
  return error(err);
715
1953
  }
716
1954
  });
717
- server.tool('delete_rubric', 'Delete a rubric', { rubric_id: z.string().describe('The rubric ID to delete') }, async (args) => {
1955
+ reg.tool('delete_rubric', 'Delete a rubric.', { rubric_id: z.string().describe('The rubric ID to delete') }, async (args) => {
718
1956
  try {
1957
+ await rubricInOrg(args.rubric_id);
719
1958
  await rubrics.deleteRubric(args.rubric_id);
720
1959
  return success({ deleted: true, rubric_id: args.rubric_id });
721
1960
  }
@@ -723,8 +1962,9 @@ export function registerTools(server, client) {
723
1962
  return error(err);
724
1963
  }
725
1964
  });
726
- server.tool('list_rubric_versions', 'List all versions of a rubric', { rubric_id: z.string().describe('The rubric ID') }, async (args) => {
1965
+ reg.tool('list_rubric_versions', 'Saved versions of a rubric.', { rubric_id: z.string().describe('The rubric ID') }, async (args) => {
727
1966
  try {
1967
+ await rubricInOrg(args.rubric_id);
728
1968
  const result = await rubrics.listVersions(args.rubric_id);
729
1969
  return success(result);
730
1970
  }
@@ -735,8 +1975,9 @@ export function registerTools(server, client) {
735
1975
  // ---------------------------------------------------------------------------
736
1976
  // Test set tools
737
1977
  // ---------------------------------------------------------------------------
738
- server.tool('list_test_sets', 'List test sets, optionally filtered by agent', { agent_id: z.string().optional().describe('Filter test sets by agent ID') }, async (args) => {
1978
+ reg.tool('list_test_sets', 'Evaluation test sets (scenarios an agent is tested on) of one agent.', { agent_id: z.string().describe('Agent ID (test sets of other orgs are never listed)') }, async (args) => {
739
1979
  try {
1980
+ await evals.assertAgentInOrg(args.agent_id);
740
1981
  const result = await testSets.listTestSets(args.agent_id);
741
1982
  return success(result);
742
1983
  }
@@ -744,27 +1985,29 @@ export function registerTools(server, client) {
744
1985
  return error(err);
745
1986
  }
746
1987
  });
747
- server.tool('get_test_set', 'Get details of a specific test set', { test_set_id: z.string().describe('The test set ID') }, async (args) => {
1988
+ reg.tool('get_test_set', 'One test set with its items (single turns, scenarios, voice simulations).', { test_set_id: z.string().describe('The test set ID') }, async (args) => {
748
1989
  try {
749
- const result = await testSets.getTestSet(args.test_set_id);
1990
+ const result = await testSetInOrg(args.test_set_id);
750
1991
  return success(result);
751
1992
  }
752
1993
  catch (err) {
753
1994
  return error(err);
754
1995
  }
755
1996
  });
756
- server.tool('create_test_set', 'Create a new test set', {
1997
+ reg.tool('create_test_set', 'Create a test set for an agent: items with inputs and success criteria.', {
757
1998
  name: z.string().describe('Test set name'),
758
- items: z.array(z.record(z.unknown())).describe('Array of test items'),
1999
+ items: z.array(testItemSchema).describe('The test items'),
759
2000
  agent_id: z.string().optional().describe('Agent ID to associate the test set with'),
760
2001
  description: z.string().optional().describe('Test set description'),
761
2002
  }, async (args) => {
762
2003
  try {
763
- const input = { name: args.name, items: args.items };
2004
+ const input = { name: args.name, items: args.items.map(toTestItem) };
764
2005
  if (args.agent_id !== undefined)
765
2006
  input.agent_id = args.agent_id;
766
2007
  if (args.description !== undefined)
767
2008
  input.description = args.description;
2009
+ if (args.agent_id)
2010
+ await evals.assertAgentInOrg(args.agent_id);
768
2011
  const result = await testSets.createTestSet(input);
769
2012
  return success(result);
770
2013
  }
@@ -772,11 +2015,11 @@ export function registerTools(server, client) {
772
2015
  return error(err);
773
2016
  }
774
2017
  });
775
- server.tool('update_test_set', 'Update an existing test set', {
2018
+ reg.tool('update_test_set', 'Change a test set; this saves a new version.', {
776
2019
  test_set_id: z.string().describe('The test set ID'),
777
2020
  name: z.string().optional().describe('Updated name'),
778
2021
  description: z.string().optional().describe('Updated description'),
779
- items: z.array(z.record(z.unknown())).optional().describe('Updated test items'),
2022
+ items: z.array(testItemSchema).optional().describe('Updated test items'),
780
2023
  }, async (args) => {
781
2024
  try {
782
2025
  const input = {};
@@ -785,7 +2028,8 @@ export function registerTools(server, client) {
785
2028
  if (args.description !== undefined)
786
2029
  input.description = args.description;
787
2030
  if (args.items !== undefined)
788
- input.items = args.items;
2031
+ input.items = args.items.map(toTestItem);
2032
+ await testSetInOrg(args.test_set_id);
789
2033
  const result = await testSets.updateTestSet(args.test_set_id, input);
790
2034
  return success(result);
791
2035
  }
@@ -793,8 +2037,9 @@ export function registerTools(server, client) {
793
2037
  return error(err);
794
2038
  }
795
2039
  });
796
- server.tool('delete_test_set', 'Delete a test set', { test_set_id: z.string().describe('The test set ID to delete') }, async (args) => {
2040
+ reg.tool('delete_test_set', 'Delete a test set.', { test_set_id: z.string().describe('The test set ID to delete') }, async (args) => {
797
2041
  try {
2042
+ await testSetInOrg(args.test_set_id);
798
2043
  await testSets.deleteTestSet(args.test_set_id);
799
2044
  return success({ deleted: true, test_set_id: args.test_set_id });
800
2045
  }
@@ -802,8 +2047,9 @@ export function registerTools(server, client) {
802
2047
  return error(err);
803
2048
  }
804
2049
  });
805
- server.tool('list_test_set_versions', 'List all versions of a test set', { test_set_id: z.string().describe('The test set ID') }, async (args) => {
2050
+ reg.tool('list_test_set_versions', 'Saved versions of a test set.', { test_set_id: z.string().describe('The test set ID') }, async (args) => {
806
2051
  try {
2052
+ await testSetInOrg(args.test_set_id);
807
2053
  const result = await testSets.listVersions(args.test_set_id);
808
2054
  return success(result);
809
2055
  }
@@ -814,7 +2060,7 @@ export function registerTools(server, client) {
814
2060
  // ---------------------------------------------------------------------------
815
2061
  // Export tools
816
2062
  // ---------------------------------------------------------------------------
817
- server.tool('export_eval_markdown', 'Export evaluation results as a structured markdown file with conversations, scores, and criteria', {
2063
+ reg.tool('export_eval_markdown', 'Export evaluation results as a structured markdown file with conversations, scores, and criteria', {
818
2064
  run_id: z.string().describe('The evaluation run ID'),
819
2065
  output_path: z.string().optional().describe('Output file path (defaults to current directory)'),
820
2066
  }, async (args) => {
@@ -826,7 +2072,7 @@ export function registerTools(server, client) {
826
2072
  return error(err);
827
2073
  }
828
2074
  });
829
- server.tool('export_eval_report', 'Generate a branded PDF evaluation report with cover page, scores, and per-item details', {
2075
+ reg.tool('export_eval_report', 'Generate a branded PDF evaluation report with cover page, scores, and per-item details', {
830
2076
  run_id: z.string().describe('The evaluation run ID'),
831
2077
  output_path: z.string().optional().describe('Output file path (defaults to current directory)'),
832
2078
  }, async (args) => {
@@ -838,7 +2084,7 @@ export function registerTools(server, client) {
838
2084
  return error(err);
839
2085
  }
840
2086
  });
841
- server.tool('export_agent_card', 'Generate a branded PDF agent card with system prompt, tools, KBs, LLM config, and evaluation history', {
2087
+ reg.tool('export_agent_card', 'Generate a branded PDF agent card with system prompt, tools, KBs, LLM config, and evaluation history', {
842
2088
  agent_id: z.string().describe('The agent ID'),
843
2089
  output_path: z.string().optional().describe('Output file path (defaults to current directory)'),
844
2090
  }, async (args) => {
@@ -850,5 +2096,6 @@ export function registerTools(server, client) {
850
2096
  return error(err);
851
2097
  }
852
2098
  });
2099
+ return reg;
853
2100
  }
854
2101
  //# sourceMappingURL=tools.js.map