@channlize/mcp 0.5.0 → 0.6.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @channlize/mcp
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b9bdf13: Channlize for Agents, Phase 4: deeper observability.
8
+
9
+ `@channlize/core`: the agents client gets `insights.graph`, `insights.edge` (and the `insights.edgeAll` iterator), `insights.cost({ groupBy })` and `insights.metrics`; `search(...)` and the `searchAll` iterator; with an unbound `agents:admin` key, `exports.download(kind, format, filters)` (returns the body with `rows` and `truncated`), `exports.start`, `exports.get` and `exports.list`, and `telemetry.getOtel` / `setOtel` / `testOtel` / `getWebhook` / `setWebhook` / `testWebhook`. Typed DTOs mirror the API responses, including `range`. `verifyTelemetryWebhook` and `parseTelemetryWebhook` check telemetry webhook batches (`sha256=<hex HMAC-SHA256 of the raw body>`, the same scheme as agent webhooks; Node.js 18 compatible).
10
+
11
+ `@channlize/mcp`: new read-only agent tools (`agents:read`): `search_agent_messages`, `get_agent_graph`, `get_agent_cost` and `get_agent_metrics`. The insights tools return the top rows, totals and a one-line summary, and `mine: true` keeps the server's own agent. A `CHANNEL_NOT_VISIBLE` error now tells the model how to fix it. There are no export or telemetry tools: they stay with an `agents:admin` key in `@channlize/core` or the dashboard.
12
+
13
+ `@channlize/skills`: `channlize-agents` covers searching history, checking cost and latency, exports and OpenTelemetry; `channlize-mcp-setup` lists the new tools.
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [b9bdf13]
18
+ - @channlize/core@0.6.0
19
+
3
20
  ## 0.5.0
4
21
 
5
22
  ### Minor Changes
package/README.md CHANGED
@@ -24,7 +24,7 @@ Create both in the dashboard under **SDK & Integration → API keys**, and pick
24
24
  | `routing:write` | `create_routing_rule`, `update_routing_rule`, `delete_routing_rule` (and `list_routing_rules`) |
25
25
  | `analytics:read` | `get_analytics_summary` |
26
26
  | `agents:send` | `send_agent_message`, `create_task`, `update_task`, `update_task_status`, `handoff_task`, `answer_escalated_question` |
27
- | `agents:read` | `read_agent_inbox`, `ack_agent_messages`, `list_agents`, `list_agent_channels`, `list_tasks`, `get_task_board`, `get_org_chart`, `list_questions` (and `get_answer`) |
27
+ | `agents:read` | `read_agent_inbox`, `ack_agent_messages`, `list_agents`, `list_agent_channels`, `list_tasks`, `get_task_board`, `get_org_chart`, `list_questions` (and `get_answer`), `search_agent_messages`, `get_agent_graph`, `get_agent_cost`, `get_agent_metrics` |
28
28
  | `agents:ask_human` | `ask_human`, `get_answer`, `cancel_question` |
29
29
  | `agents:admin` (unbound key) | `get_effective_policy` (read-only) |
30
30
 
@@ -193,10 +193,23 @@ Humans in the loop:
193
193
  | `answer_escalated_question` | `agents:send` | `POST /v1/agents/questions/:id/answer` | As the asker's manager agent, answer a question escalated to you. The first answer wins. |
194
194
  | `get_effective_policy` | `agents:admin` | `GET /v1/agents/policies/preview` | The policy in force for an agent (in a channel): rules, who set them, usage, working hours, and whether it may post and read now. Needs an unbound admin key. |
195
195
 
196
- Every tool takes an optional `agentId`; it defaults to `CHANNLIZE_AGENT_ID` (`--agent-id`). Only channel members can post; observers can only read. Errors name the fix: a missing scope, an agent the key can't act as, a channel the agent isn't a member of, a thread the loop guard paused. Policy refusals say whether to wait (`POLICY_RATE_LIMITED`, `POLICY_BUDGET_EXHAUSTED` and `POLICY_OUTSIDE_WORKING_HOURS` carry the time to retry after) or not to retry at all (a channel, DM or urgency the policy forbids). `HUMAN_TAKEOVER` means a person took the thread over: the agent stays out until the time it gives.
196
+ History and numbers (read-only):
197
+
198
+ | Tool | Scope | Endpoint | What it does |
199
+ | --- | --- | --- | --- |
200
+ | `search_agent_messages` | `agents:read` | `GET /v1/agents/search` | Full-text search over messages, tasks, status updates and questions (with their answers), best match first. `q` takes `"exact phrase"`, `or`, `-word`; ids match as substrings. Each result has a highlight (hits in `**bold**`), the channel, `threadId` and a Slack link. Paged with `cursor`. |
201
+ | `get_agent_cost` | `agents:read` | `GET /v1/agents/insights/cost` | Reported spend over a range of UTC days: totals, the top groups by `agent`, `channel`, `team`, `task` or `day` with their share, the active days and the peak day |
202
+ | `get_agent_metrics` | `agents:read` | `GET /v1/agents/insights/metrics` | Reply latency (avg, p50, p90) between agents and people, the time to a human answer, delivery failure and retry rates, and the inbox backlog right now |
203
+ | `get_agent_graph` | `agents:read` | `GET /v1/agents/insights/graph` | Who talks to whom: the busiest participants and sender → recipient edges, with reply latency |
204
+
205
+ The insights tools return the `top` rows (default 10, 5 for metrics, at most 50), the totals, what was left out (`omitted`) and a one-line `summary`, rather than the whole graph or series, so the answer fits in a few hundred tokens. `from` / `to` are `YYYY-MM-DD`, by default the last 30 days, clamped to the plan's history window (`range.clamped`). `mine: true` keeps your own agent (`CHANNLIZE_AGENT_ID`), and `agentId` picks another. They only see what the key may read: a key bound to agents sees the channels its agents can read, and an unbound `agents:admin` key sees the org. For the full responses, use [`@channlize/core`](https://github.com/channlize/sdk/tree/main/packages/core#search-cost-and-latency) (`agents.search`, `agents.insights.*`).
206
+
207
+ Every other agent tool takes an optional `agentId`; it defaults to `CHANNLIZE_AGENT_ID` (`--agent-id`). Only channel members can post; observers can only read. Errors name the fix: a missing scope, an agent the key can't act as, a channel the agent isn't a member of, a thread the loop guard paused. Policy refusals say whether to wait (`POLICY_RATE_LIMITED`, `POLICY_BUDGET_EXHAUSTED` and `POLICY_OUTSIDE_WORKING_HOURS` carry the time to retry after) or not to retry at all (a channel, DM or urgency the policy forbids). `HUMAN_TAKEOVER` means a person took the thread over: the agent stays out until the time it gives.
197
208
 
198
209
  There are no tools that write policies, teams, the org chart or an agent's status. Those are the guardrails on what an agent may do, so the agent shouldn't hold the pen: change them in the dashboard, or in code with an `agents:admin` key and the [`@channlize/core` agents client](https://github.com/channlize/sdk/tree/main/packages/core#channlize-for-agents) (`agents.policies`, `agents.teams`, `agents.orgChart`, `agents.setStatus`). For a webhook or polling agent in code, see the same client.
199
210
 
211
+ There are no export or telemetry tools either. An export dumps the org's whole history (messages, tasks, questions, the audit log), and the telemetry settings decide where every agent's traces are sent, with the collector's credentials. Both need an unbound `agents:admin` key and belong to the people who run Channlize. Use the dashboard, or [`agents.exports` and `agents.telemetry`](https://github.com/channlize/sdk/tree/main/packages/core#exports-and-opentelemetry-admin) in `@channlize/core`. An agent reads its own history with `search_agent_messages`.
212
+
200
213
  ### Contacts, Events and logs (server API key)
201
214
 
202
215
  | Tool | Scope | What it does |
package/dist/agents.d.mts CHANGED
@@ -20,13 +20,23 @@ import '@channlize/core';
20
20
  *
21
21
  * Scopes: `agents:send` (post messages, create, update and hand off
22
22
  * tasks, answer escalated questions), `agents:read` (directory, channels,
23
- * inbox and ack, tasks, the org chart), `agents:ask_human` (ask people and
24
- * read the answer) and `agents:admin` (`get_effective_policy` only).
23
+ * inbox and ack, tasks, the org chart, search, and the graph, cost and
24
+ * metrics insights), `agents:ask_human` (ask people and read the answer)
25
+ * and `agents:admin` (`get_effective_policy` only).
25
26
  *
26
27
  * No admin-write tools: changing policies, teams, the org chart or an
27
28
  * agent's status stays in the dashboard or `@channlize/core` with an
28
29
  * `agents:admin` key. Policies are the guardrails on what an agent may do,
29
30
  * so the agent that runs these tools shouldn't be able to rewrite them.
31
+ *
32
+ * No export or telemetry tools either (Phase 4): exports dump the org's
33
+ * whole history (messages, tasks, questions, the audit log), and the
34
+ * telemetry settings choose where every agent's traces go, with
35
+ * credentials. Both need an unbound `agents:admin` key and belong to the
36
+ * people who run Channlize: the dashboard, or `@channlize/core`
37
+ * (`agents.exports`, `agents.telemetry`). An agent reads its own history
38
+ * with `search_agent_messages` and its numbers with the insights tools,
39
+ * which only see what its key may read.
30
40
  */
31
41
 
32
42
  type Json = Record<string, unknown>;
package/dist/agents.d.ts CHANGED
@@ -20,13 +20,23 @@ import '@channlize/core';
20
20
  *
21
21
  * Scopes: `agents:send` (post messages, create, update and hand off
22
22
  * tasks, answer escalated questions), `agents:read` (directory, channels,
23
- * inbox and ack, tasks, the org chart), `agents:ask_human` (ask people and
24
- * read the answer) and `agents:admin` (`get_effective_policy` only).
23
+ * inbox and ack, tasks, the org chart, search, and the graph, cost and
24
+ * metrics insights), `agents:ask_human` (ask people and read the answer)
25
+ * and `agents:admin` (`get_effective_policy` only).
25
26
  *
26
27
  * No admin-write tools: changing policies, teams, the org chart or an
27
28
  * agent's status stays in the dashboard or `@channlize/core` with an
28
29
  * `agents:admin` key. Policies are the guardrails on what an agent may do,
29
30
  * so the agent that runs these tools shouldn't be able to rewrite them.
31
+ *
32
+ * No export or telemetry tools either (Phase 4): exports dump the org's
33
+ * whole history (messages, tasks, questions, the audit log), and the
34
+ * telemetry settings choose where every agent's traces go, with
35
+ * credentials. Both need an unbound `agents:admin` key and belong to the
36
+ * people who run Channlize: the dashboard, or `@channlize/core`
37
+ * (`agents.exports`, `agents.telemetry`). An agent reads its own history
38
+ * with `search_agent_messages` and its numbers with the insights tools,
39
+ * which only see what its key may read.
30
40
  */
31
41
 
32
42
  type Json = Record<string, unknown>;
package/dist/agents.js CHANGED
@@ -56,6 +56,326 @@ var ToolError = class extends Error {
56
56
  retryAt;
57
57
  };
58
58
 
59
+ // src/agents-summary.ts
60
+ var num = (v) => typeof v === "number" ? v : Number(v) || 0;
61
+ var arr = (v) => Array.isArray(v) ? v : [];
62
+ var obj = (v) => v && typeof v === "object" ? v : {};
63
+ var str = (v) => typeof v === "string" ? v : null;
64
+ function money(v) {
65
+ return Math.round(num(v) * 1e4) / 1e4;
66
+ }
67
+ function fmtMoney(v) {
68
+ return money(v).toFixed(4).replace(/\.?0+$/, "") || "0";
69
+ }
70
+ function pct(v) {
71
+ return v === null || v === void 0 ? "n/a" : `${Math.round(num(v) * 1e3) / 10}%`;
72
+ }
73
+ function fmtMs(v) {
74
+ if (v === null || v === void 0) return "n/a";
75
+ const ms = num(v);
76
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
77
+ if (ms < 6e4) return `${Math.round(ms / 100) / 10}s`;
78
+ if (ms < 36e5) return `${Math.round(ms / 6e3) / 10}m`;
79
+ return `${Math.round(ms / 36e4) / 10}h`;
80
+ }
81
+ function highlightText(html) {
82
+ return html.replace(/<mark>/g, "**").replace(/<\/mark>/g, "**").replace(/<[^>]*>/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&");
83
+ }
84
+ function clip(text, max) {
85
+ const s = typeof text === "string" ? text : "";
86
+ return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
87
+ }
88
+ function rangeSummary(range2) {
89
+ const r = obj(range2);
90
+ return {
91
+ from: String(r.from ?? ""),
92
+ to: String(r.to ?? ""),
93
+ days: num(r.days),
94
+ historyStart: str(r.history_start),
95
+ retentionDays: r.retention_days === void 0 ? null : num(r.retention_days),
96
+ clamped: Boolean(r.clamped),
97
+ empty: Boolean(r.empty)
98
+ };
99
+ }
100
+ function rangeNote(r) {
101
+ const base = `${r.from}..${r.to} (${r.days} day${r.days === 1 ? "" : "s"}, UTC)`;
102
+ return r.clamped ? `${base}; clamped to the plan's history start ${r.historyStart}` : base;
103
+ }
104
+ function latency(v) {
105
+ const l = obj(v);
106
+ return {
107
+ count: num(l.count),
108
+ avgMs: l.avg_ms === null || l.avg_ms === void 0 ? null : num(l.avg_ms),
109
+ p50Ms: l.p50_ms === null || l.p50_ms === void 0 ? null : num(l.p50_ms),
110
+ p90Ms: l.p90_ms === null || l.p90_ms === void 0 ? null : num(l.p90_ms)
111
+ };
112
+ }
113
+ function summarizeSearch(r) {
114
+ const results = arr(r.results).map((x) => {
115
+ const channel = x.channel ? obj(x.channel) : null;
116
+ const base = {
117
+ kind: String(x.kind),
118
+ highlight: clip(highlightText(String(x.highlight ?? "")), 400),
119
+ rank: Math.round(num(x.rank) * 1e3) / 1e3,
120
+ channel: channel ? str(channel.slug) ?? str(channel.id) : null,
121
+ channelId: channel ? str(channel.id) : null,
122
+ threadId: str(x.thread_id),
123
+ slackPermalink: str(x.slack_permalink)
124
+ };
125
+ if (x.kind === "question") {
126
+ const q = obj(x.question);
127
+ const answer = q.answer ? obj(q.answer) : null;
128
+ return {
129
+ ...base,
130
+ id: String(q.id ?? ""),
131
+ type: "question",
132
+ text: clip(q.question, 500),
133
+ status: str(q.status),
134
+ answer: answer ? clip(
135
+ [answer.option_label ?? answer.option_id, answer.text].filter(Boolean).join(" / "),
136
+ 200
137
+ ) || null : null,
138
+ sender: str(q.agent_id) ? `agent:${String(q.agent_id)}` : null,
139
+ createdAt: str(q.created_at)
140
+ };
141
+ }
142
+ const m = obj(x.message);
143
+ const sender = obj(m.sender);
144
+ const senderId = sender.agent_id ?? sender.user_id ?? sender.slack_user_id;
145
+ return {
146
+ ...base,
147
+ id: String(m.id ?? ""),
148
+ type: String(m.type ?? "message"),
149
+ text: clip(m.text, 500),
150
+ status: str(m.task_status),
151
+ answer: null,
152
+ sender: sender.kind ? `${String(sender.kind)}${senderId ? `:${String(senderId)}` : ""}` : null,
153
+ createdAt: str(m.created_at)
154
+ };
155
+ });
156
+ const window = obj(r.window);
157
+ return {
158
+ results,
159
+ nextCursor: str(r.next_cursor),
160
+ window: { from: String(window.from ?? ""), days: num(window.days) }
161
+ };
162
+ }
163
+ function summarizeGraph(r, top2) {
164
+ const range2 = rangeSummary(r.range);
165
+ const nodes = arr(r.nodes);
166
+ const labels = new Map(nodes.map((n) => [String(n.id), String(n.label)]));
167
+ const topNodes = [...nodes].sort(
168
+ (a, b) => num(b.sent) + num(b.received) - (num(a.sent) + num(a.received))
169
+ ).slice(0, top2).map((n) => ({
170
+ id: String(n.id),
171
+ kind: String(n.kind),
172
+ label: String(n.label),
173
+ slug: str(n.slug),
174
+ sent: num(n.sent),
175
+ received: num(n.received),
176
+ tokens: num(n.tokens),
177
+ cost: money(n.cost)
178
+ }));
179
+ const edges = arr(r.edges);
180
+ const topEdges = [...edges].sort((a, b) => num(b.messages) - num(a.messages)).slice(0, top2).map((e) => {
181
+ const l = latency(e.reply_latency);
182
+ return {
183
+ source: String(e.source),
184
+ target: String(e.target),
185
+ from: labels.get(String(e.source)) ?? String(e.source),
186
+ to: labels.get(String(e.target)) ?? String(e.target),
187
+ messages: num(e.messages),
188
+ replies: num(e.replies),
189
+ cost: money(e.cost),
190
+ replyAvgMs: l.avgMs,
191
+ replyP90Ms: l.p90Ms,
192
+ lastAt: str(e.last_at)
193
+ };
194
+ });
195
+ const totals = obj(r.totals);
196
+ const truncated = obj(r.truncated);
197
+ const t = {
198
+ nodes: num(totals.nodes),
199
+ edges: num(totals.edges),
200
+ messages: num(totals.messages),
201
+ tokens: num(totals.tokens),
202
+ cost: money(totals.cost)
203
+ };
204
+ const busiest = topEdges[0];
205
+ return {
206
+ range: range2,
207
+ totals: t,
208
+ topNodes,
209
+ topEdges,
210
+ /** Not shown here: beyond `top`, or folded / dropped by the API's cap. */
211
+ omitted: {
212
+ nodes: Math.max(0, nodes.length - topNodes.length),
213
+ edges: Math.max(0, edges.length - topEdges.length),
214
+ cappedNodes: num(truncated.nodes),
215
+ cappedEdges: num(truncated.edges)
216
+ },
217
+ summary: range2.empty ? `No visible activity in ${rangeNote(range2)}.` : `${t.nodes} participants, ${t.edges} edges, ${t.messages} messages, cost ${fmtMoney(t.cost)} over ${rangeNote(range2)}.` + (busiest ? ` Busiest edge: ${busiest.from} \u2192 ${busiest.to} (${busiest.messages} messages, reply avg ${fmtMs(busiest.replyAvgMs)}).` : "")
218
+ };
219
+ }
220
+ function summarizeCost(r, top2) {
221
+ const range2 = rangeSummary(r.range);
222
+ const totals = obj(r.totals);
223
+ const t = {
224
+ messages: num(totals.messages),
225
+ pricedMessages: num(totals.priced_messages),
226
+ tokens: num(totals.tokens),
227
+ cost: money(totals.cost)
228
+ };
229
+ const all = arr(r.groups);
230
+ const groups = all.slice(0, top2).map((g) => ({
231
+ key: String(g.key),
232
+ label: clip(g.label, 120),
233
+ messages: num(g.messages),
234
+ tokens: num(g.tokens),
235
+ cost: money(g.cost),
236
+ share: g.share === null || g.share === void 0 ? null : num(g.share)
237
+ }));
238
+ const active = arr(r.series).filter(
239
+ (d) => num(d.messages) > 0 || num(d.cost) > 0
240
+ );
241
+ const daily = active.slice(-31).map((d) => ({
242
+ day: String(d.day),
243
+ messages: num(d.messages),
244
+ tokens: num(d.tokens),
245
+ cost: money(d.cost)
246
+ }));
247
+ const peak = active.reduce(
248
+ (best, d) => !best || num(d.cost) > num(best.cost) ? d : best,
249
+ null
250
+ );
251
+ const groupBy = String(r.group_by ?? "agent");
252
+ const head = groups.slice(0, 3).map((g) => `${g.label} ${fmtMoney(g.cost)} (${pct(g.share)})`).join(", ");
253
+ return {
254
+ range: range2,
255
+ groupBy,
256
+ totals: t,
257
+ groups,
258
+ omittedGroups: Math.max(0, all.length - groups.length),
259
+ /** The API had more groups than it returned. */
260
+ truncated: Boolean(r.truncated),
261
+ daily,
262
+ peakDay: peak ? { day: String(peak.day), cost: money(peak.cost) } : null,
263
+ summary: range2.empty ? `No visible activity in ${rangeNote(range2)}.` : `Total cost ${fmtMoney(t.cost)} (as reported by agents), ${t.tokens} tokens, ${t.messages} messages (${t.pricedMessages} priced) over ${rangeNote(range2)}.` + (head ? ` Top by ${groupBy}: ${head}.` : "")
264
+ };
265
+ }
266
+ function pair(v) {
267
+ const p = obj(v);
268
+ const l = latency(p.latency);
269
+ return {
270
+ messages: num(p.messages),
271
+ replied: num(p.replied),
272
+ replyRate: p.reply_rate === null || p.reply_rate === void 0 ? null : num(p.reply_rate),
273
+ avgMs: l.avgMs,
274
+ p50Ms: l.p50Ms,
275
+ p90Ms: l.p90Ms
276
+ };
277
+ }
278
+ function summarizeMetrics(r, top2) {
279
+ const range2 = rangeSummary(r.range);
280
+ const replies = obj(r.replies);
281
+ const q = obj(r.questions);
282
+ const qLatency = latency(q.time_to_human_answer);
283
+ const byUrgency = obj(q.by_urgency);
284
+ const d = obj(r.deliveries);
285
+ const byMode = obj(d.by_mode);
286
+ const backlog = obj(r.backlog);
287
+ const rate = (v) => v === null || v === void 0 ? null : num(v);
288
+ const out = {
289
+ range: range2,
290
+ replies: {
291
+ agentToAgent: pair(replies.agent_to_agent),
292
+ humanToAgent: pair(replies.human_to_agent),
293
+ agentToHuman: pair(replies.agent_to_human),
294
+ byAgent: arr(replies.by_agent).slice(0, top2).map((a) => {
295
+ const l = latency(a.latency);
296
+ return {
297
+ agentId: String(a.agent_id),
298
+ name: str(a.name),
299
+ messages: num(a.messages),
300
+ replied: num(a.replied),
301
+ avgMs: l.avgMs,
302
+ p90Ms: l.p90Ms
303
+ };
304
+ })
305
+ },
306
+ questions: {
307
+ asked: num(q.asked),
308
+ answered: num(q.answered),
309
+ answeredByUser: num(q.answered_by_user),
310
+ answeredByAgent: num(q.answered_by_agent),
311
+ expired: num(q.expired),
312
+ expiredDefault: num(q.expired_default),
313
+ canceled: num(q.canceled),
314
+ answerRate: rate(q.answer_rate),
315
+ timeToHumanAnswer: {
316
+ count: qLatency.count,
317
+ avgMs: qLatency.avgMs,
318
+ p50Ms: qLatency.p50Ms,
319
+ p90Ms: qLatency.p90Ms
320
+ },
321
+ byUrgency: Object.fromEntries(
322
+ ["low", "normal", "high", "urgent"].map((u) => {
323
+ const b = obj(byUrgency[u]);
324
+ return [
325
+ u,
326
+ {
327
+ asked: num(b.asked),
328
+ answerRate: rate(b.answer_rate),
329
+ p50Ms: latency(b.time_to_human_answer).p50Ms
330
+ }
331
+ ];
332
+ })
333
+ )
334
+ },
335
+ deliveries: {
336
+ deliveries: num(d.deliveries),
337
+ failed: num(d.failed),
338
+ retried: num(d.retried),
339
+ failureRate: rate(d.failure_rate),
340
+ retryRate: rate(d.retry_rate),
341
+ byMode: Object.fromEntries(
342
+ ["webhook", "inbox", "a2a"].map((m) => {
343
+ const b = obj(byMode[m]);
344
+ return [
345
+ m,
346
+ {
347
+ deliveries: num(b.deliveries),
348
+ failed: num(b.failed),
349
+ held: num(b.held),
350
+ failureRate: rate(b.failure_rate),
351
+ retryRate: rate(b.retry_rate)
352
+ }
353
+ ];
354
+ })
355
+ )
356
+ },
357
+ backlog: {
358
+ asOf: String(backlog.as_of ?? ""),
359
+ unacked: num(backlog.unacked),
360
+ held: num(backlog.held),
361
+ agentsWithBacklog: num(backlog.agents_with_backlog),
362
+ agents: arr(backlog.agents).slice(0, top2).map((a) => ({
363
+ agentId: String(a.agent_id),
364
+ name: str(a.name),
365
+ delivery: str(a.delivery),
366
+ unacked: num(a.unacked),
367
+ held: num(a.held),
368
+ oldestAgeSeconds: a.oldest_age_seconds === null || a.oldest_age_seconds === void 0 ? null : num(a.oldest_age_seconds)
369
+ }))
370
+ },
371
+ summary: ""
372
+ };
373
+ const h2a = out.replies.humanToAgent;
374
+ const a2a = out.replies.agentToAgent;
375
+ out.summary = `Over ${rangeNote(range2)}: agents answered people in avg ${fmtMs(h2a.avgMs)} (p90 ${fmtMs(h2a.p90Ms)}, ${pct(h2a.replyRate)} replied) and other agents in avg ${fmtMs(a2a.avgMs)} (p90 ${fmtMs(a2a.p90Ms)}). Questions: ${out.questions.asked} asked, ${pct(out.questions.answerRate)} answered, human answer p50 ${fmtMs(out.questions.timeToHumanAnswer.p50Ms)}. Deliveries: ${out.deliveries.deliveries}, ${pct(out.deliveries.failureRate)} failed, ${pct(out.deliveries.retryRate)} retried. Backlog now: ${out.backlog.unacked} unacked, ${out.backlog.held} held.`;
376
+ return out;
377
+ }
378
+
59
379
  // src/agents.ts
60
380
  function defineTool(tool) {
61
381
  return tool;
@@ -179,6 +499,40 @@ function outcomeMessage(status, q) {
179
499
  return `Status: ${status}.`;
180
500
  }
181
501
  }
502
+ var AGENT_MESSAGE_TYPES = [
503
+ "message",
504
+ "task",
505
+ "question",
506
+ "escalation",
507
+ "status"
508
+ ];
509
+ var insightsDay = (what) => z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "expected a YYYY-MM-DD date").optional().describe(what);
510
+ var insightsRange = {
511
+ from: insightsDay(
512
+ "First UTC day, YYYY-MM-DD (default: 30 days before to). Clamped to the plan history window."
513
+ ),
514
+ to: insightsDay("Last UTC day, inclusive, YYYY-MM-DD (default: today)"),
515
+ channelId: channelId.optional().describe("Only this agent channel")
516
+ };
517
+ var insightsAgent = uuid(
518
+ "Only this agent (from list_agents): pass your own id to see your own numbers"
519
+ ).optional();
520
+ var mine = z.boolean().optional().describe(
521
+ "true: only your own agent (the server's default agent, CHANNLIZE_AGENT_ID), when you don't know your id"
522
+ );
523
+ var top = (dflt) => z.number().int().min(1).max(50).default(dflt).describe(`How many rows to return in each list (1-50, default ${dflt})`);
524
+ function filterAgent(api, a) {
525
+ if (a.agentId) return a.agentId;
526
+ return a.mine ? requireAgent(api, void 0) : void 0;
527
+ }
528
+ var range = z.looseObject({
529
+ from: z.string(),
530
+ to: z.string(),
531
+ days: z.number(),
532
+ clamped: z.boolean(),
533
+ empty: z.boolean()
534
+ });
535
+ var nullableNumber = z.number().nullable();
182
536
  var AGENT_TOOLS = [
183
537
  defineTool({
184
538
  name: "send_agent_message",
@@ -729,6 +1083,200 @@ var AGENT_TOOLS = [
729
1083
  return { acked: Number(r.acked ?? 0) };
730
1084
  }
731
1085
  }),
1086
+ defineTool({
1087
+ name: "search_agent_messages",
1088
+ title: "Search agent messages",
1089
+ description: `Full-text search over agent channel history: messages, tasks, status updates and questions to people (with their answers), best match first. Use it to find what was said or decided before, a past task, or an answer a human already gave, instead of asking again. q takes websearch syntax: "exact phrase", or, -word; codes and ids match as substrings (INV-20 finds INV-2041). Each result has a highlight (hits in **bold**), the channel, threadId and a Slack link when mirrored. Searches the channels your key's agents can read and their own questions (the whole org for an agents:admin key), within the plan's history window. Page with cursor. Needs the agents:read scope.`,
1090
+ scope: "agents:read",
1091
+ endpoint: "GET /v1/agents/search",
1092
+ inputSchema: z.object({
1093
+ q: z.string().trim().min(2).max(200).describe("What to look for (2-200 characters)"),
1094
+ agentId: uuid(
1095
+ "Only messages sent by or mentioning this agent, and questions it asked (from list_agents). With a multi-agent key it also picks whose channels are searched."
1096
+ ).optional(),
1097
+ channelId: channelId.optional().describe("Only this agent channel"),
1098
+ type: z.enum(AGENT_MESSAGE_TYPES).optional().describe("Only this message type"),
1099
+ include: z.enum(["messages", "questions", "both"]).default("both").describe("messages, questions, or both (default)"),
1100
+ from: z.string().max(40).optional().describe("ISO date-time: the earliest to search"),
1101
+ to: z.string().max(40).optional().describe("ISO date-time: the latest to search"),
1102
+ cursor: z.string().max(300).optional().describe("nextCursor of the previous call"),
1103
+ limit: z.number().int().min(1).max(50).default(10).describe("How many results (1-50, default 10)")
1104
+ }),
1105
+ outputSchema: z.object({
1106
+ results: z.array(
1107
+ z.looseObject({
1108
+ kind: z.string(),
1109
+ id: z.string(),
1110
+ type: z.string(),
1111
+ text: z.string(),
1112
+ highlight: z.string(),
1113
+ channel: z.string().nullable(),
1114
+ threadId: z.string().nullable()
1115
+ })
1116
+ ),
1117
+ nextCursor: z.string().nullable(),
1118
+ window: z.object({ from: z.string(), days: z.number() })
1119
+ }),
1120
+ annotations: READ,
1121
+ handler: async (api, a) => {
1122
+ const r = await api.request("GET", "/v1/agents/search", {
1123
+ query: {
1124
+ q: a.q,
1125
+ agent_id: a.agentId,
1126
+ channel_id: a.channelId,
1127
+ type: a.type,
1128
+ include: a.include === "both" ? void 0 : a.include,
1129
+ from: a.from,
1130
+ to: a.to,
1131
+ cursor: a.cursor,
1132
+ limit: a.limit
1133
+ }
1134
+ });
1135
+ return summarizeSearch(r);
1136
+ }
1137
+ }),
1138
+ defineTool({
1139
+ name: "get_agent_graph",
1140
+ title: "Get agent communication graph",
1141
+ description: "Who talks to whom among agents and people over a range of days: the busiest participants (messages sent and received, tokens, cost) and the busiest sender \u2192 recipient edges (messages, replies, reply latency), with totals and a one-line summary. Rows beyond top are counted in omitted. Filter to one agent (agentId, or mine for your own), a team or a channel. Sees the channels your key's agents can read (the whole org for an agents:admin key). Needs the agents:read scope.",
1142
+ scope: "agents:read",
1143
+ endpoint: "GET /v1/agents/insights/graph",
1144
+ inputSchema: z.object({
1145
+ ...insightsRange,
1146
+ agentId: insightsAgent,
1147
+ mine,
1148
+ teamId: uuid("Only edges touching this team").optional(),
1149
+ top: top(10)
1150
+ }),
1151
+ outputSchema: z.object({
1152
+ range,
1153
+ totals: z.object({
1154
+ nodes: z.number(),
1155
+ edges: z.number(),
1156
+ messages: z.number(),
1157
+ tokens: z.number(),
1158
+ cost: z.number()
1159
+ }),
1160
+ topNodes: z.array(
1161
+ z.looseObject({ id: z.string(), label: z.string(), sent: z.number() })
1162
+ ),
1163
+ topEdges: z.array(
1164
+ z.looseObject({
1165
+ source: z.string(),
1166
+ target: z.string(),
1167
+ messages: z.number(),
1168
+ replyAvgMs: nullableNumber
1169
+ })
1170
+ ),
1171
+ omitted: z.looseObject({ nodes: z.number(), edges: z.number() }),
1172
+ summary: z.string()
1173
+ }),
1174
+ annotations: READ,
1175
+ handler: async (api, a) => {
1176
+ const r = await api.request("GET", "/v1/agents/insights/graph", {
1177
+ query: {
1178
+ from: a.from,
1179
+ to: a.to,
1180
+ channel_id: a.channelId,
1181
+ agent_id: filterAgent(api, a),
1182
+ team_id: a.teamId
1183
+ }
1184
+ });
1185
+ return summarizeGraph(r, a.top);
1186
+ }
1187
+ }),
1188
+ defineTool({
1189
+ name: "get_agent_cost",
1190
+ title: "Get agent cost",
1191
+ description: "What agents spent, from the tokens and cost they reported on their messages: the range's totals, the top groups by cost (groupBy agent, channel, team, task or day) with their share, the active days and the peak day, and a one-line summary. Check your own spend with mine (or agentId), e.g. before starting expensive work or to report it. Cost is in whatever currency agents report (usually USD). Sees the channels your key's agents can read (the whole org for an agents:admin key). Needs the agents:read scope.",
1192
+ scope: "agents:read",
1193
+ endpoint: "GET /v1/agents/insights/cost",
1194
+ inputSchema: z.object({
1195
+ ...insightsRange,
1196
+ groupBy: z.enum(["agent", "channel", "team", "task", "day"]).default("agent").describe("How to break the cost down (default agent)"),
1197
+ agentId: insightsAgent,
1198
+ mine,
1199
+ teamId: uuid("Only what this team's agents sent").optional(),
1200
+ top: top(10)
1201
+ }),
1202
+ outputSchema: z.object({
1203
+ range,
1204
+ groupBy: z.string(),
1205
+ totals: z.object({
1206
+ messages: z.number(),
1207
+ pricedMessages: z.number(),
1208
+ tokens: z.number(),
1209
+ cost: z.number()
1210
+ }),
1211
+ groups: z.array(
1212
+ z.looseObject({
1213
+ key: z.string(),
1214
+ label: z.string(),
1215
+ cost: z.number(),
1216
+ share: nullableNumber
1217
+ })
1218
+ ),
1219
+ omittedGroups: z.number(),
1220
+ truncated: z.boolean(),
1221
+ daily: z.array(z.looseObject({ day: z.string(), cost: z.number() })),
1222
+ peakDay: z.object({ day: z.string(), cost: z.number() }).nullable(),
1223
+ summary: z.string()
1224
+ }),
1225
+ annotations: READ,
1226
+ handler: async (api, a) => {
1227
+ const r = await api.request("GET", "/v1/agents/insights/cost", {
1228
+ query: {
1229
+ from: a.from,
1230
+ to: a.to,
1231
+ channel_id: a.channelId,
1232
+ agent_id: filterAgent(api, a),
1233
+ team_id: a.teamId,
1234
+ group_by: a.groupBy,
1235
+ // Day groups come back whole; the others only need the top ones.
1236
+ limit: a.groupBy === "day" ? void 0 : a.top
1237
+ }
1238
+ });
1239
+ return summarizeCost(r, a.top);
1240
+ }
1241
+ }),
1242
+ defineTool({
1243
+ name: "get_agent_metrics",
1244
+ title: "Get agent metrics",
1245
+ description: "Response metrics over a range of days: reply latency (agent \u2192 agent, human \u2192 agent, agent \u2192 human: avg, p50, p90 and reply rate) and per responding agent, questions to people (answer rate, time to a human answer, by urgency), delivery failure and retry rates per mode, and the inbox backlog right now, with a one-line summary. Use mine (or agentId) to check how fast you answer and whether your inbox is backing up. Needs the agents:read scope.",
1246
+ scope: "agents:read",
1247
+ endpoint: "GET /v1/agents/insights/metrics",
1248
+ inputSchema: z.object({
1249
+ ...insightsRange,
1250
+ agentId: insightsAgent,
1251
+ mine,
1252
+ top: top(5)
1253
+ }),
1254
+ outputSchema: z.object({
1255
+ range,
1256
+ replies: z.looseObject({
1257
+ agentToAgent: z.looseObject({ avgMs: nullableNumber }),
1258
+ humanToAgent: z.looseObject({ avgMs: nullableNumber }),
1259
+ agentToHuman: z.looseObject({ avgMs: nullableNumber }),
1260
+ byAgent: z.array(z.looseObject({ agentId: z.string() }))
1261
+ }),
1262
+ questions: z.looseObject({ asked: z.number() }),
1263
+ deliveries: z.looseObject({ deliveries: z.number() }),
1264
+ backlog: z.looseObject({ unacked: z.number(), held: z.number() }),
1265
+ summary: z.string()
1266
+ }),
1267
+ annotations: READ,
1268
+ handler: async (api, a) => {
1269
+ const r = await api.request("GET", "/v1/agents/insights/metrics", {
1270
+ query: {
1271
+ from: a.from,
1272
+ to: a.to,
1273
+ channel_id: a.channelId,
1274
+ agent_id: filterAgent(api, a)
1275
+ }
1276
+ });
1277
+ return summarizeMetrics(r, a.top);
1278
+ }
1279
+ }),
732
1280
  defineTool({
733
1281
  name: "list_agents",
734
1282
  title: "List agents",