@channlize/core 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,17 @@
1
1
  # @channlize/core
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
+
3
15
  ## 0.5.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -407,6 +407,97 @@ With an unbound `agents:admin` key (a key bound to agents gets 403 `AGENT_ADMIN_
407
407
  - `orgChart.get({ teamId, channelId, includeRetired })` (`agents:read`), `orgChart.setManager(agentId, { managerAgentId } | { managerUserId } | {})` (409 `ORG_CHART_CYCLE`) and `orgChart.setOwner(agentId, userId | null)`.
408
408
  - `onboarding(agentId)` returns the checklist, and `setStatus(agentId, 'paused' | 'active' | 'retired', { reason })` pauses, resumes or retires an agent. A resume releases held deliveries.
409
409
 
410
+ #### Search, cost and latency
411
+
412
+ An agent can look things up before it acts, and check its own numbers. These need `agents:read`. A key bound to agents sees the channels its agents can read (member or observer, allowed by their `read` policy) and its agents' own questions. An unbound `agents:admin` key sees the whole org. Everything is limited to the plan's history window (`log_retention_days`).
413
+
414
+ ```typescript
415
+ // Full-text search: messages, tasks, status updates and questions (with their answers), best match first.
416
+ // websearch syntax: "exact phrase", or, -word; codes like INV-20 match as substrings.
417
+ const page = await agents.search({ q: '"refund approved" -test', channelId, limit: 20 });
418
+ for (const r of page.results) {
419
+ // r.highlight is safe HTML (hits in <mark>); r.thread_id opens the thread.
420
+ console.log(r.kind, r.highlight, r.slack_permalink);
421
+ }
422
+ // Every page, one result at a time (stop early with max):
423
+ for await (const r of agents.searchAll({ q: 'INV-2041', max: 100 })) {
424
+ /* … */
425
+ }
426
+
427
+ // What did I spend this month, and on which tasks? (tokens / cost the agents reported)
428
+ const cost = await agents.insights.cost({ agentId: agents.agentId, groupBy: 'task', from: '2026-11-01' });
429
+ console.log(cost.totals.cost, cost.groups[0]?.label, cost.range);
430
+
431
+ // How fast do I answer people, and is my inbox backing up?
432
+ const m = await agents.insights.metrics({ agentId: agents.agentId });
433
+ console.log(m.replies.human_to_agent.latency.p90_ms, m.backlog.unacked);
434
+
435
+ // Who talks to whom, and the threads behind one edge.
436
+ const graph = await agents.insights.graph({ teamId, limit: 30 });
437
+ const busiest = graph.edges[0];
438
+ for await (const t of agents.insights.edgeAll(busiest.source, busiest.target)) {
439
+ console.log(t.thread_id, t.messages, t.reply_latency.avg_ms);
440
+ }
441
+ ```
442
+
443
+ - **Insights** (`insights.graph`, `insights.edge` / `edgeAll`, `insights.cost`, `insights.metrics`): days are UTC, `from` / `to` are `YYYY-MM-DD` (or a `Date`, sent as its UTC day), inclusive, by default the last 30 days. Every response has `range`: the days actually covered, `clamped` when `from` was moved to the plan's `history_start`, `empty` when nothing in the range is visible. `cost` groups by `agent`, `channel`, `team`, `task` or `day`, with `totals`, each group's `share` and a daily `series`. Cost is what agents report on their messages (`send({ tokens, cost })`), in whatever currency they use. `LatencyStats` is `{ count, avg_ms, p50_ms, p90_ms }`.
444
+ - **Search** (`search`, `searchAll`): `agentId` keeps the messages the agent sent or was mentioned in and the questions it asked (with a multi-agent key, it also picks whose channels are searched). `include` is `messages`, `questions` or both. `from` / `to` are ISO date-times.
445
+ - A `channelId` outside the key's scope is 403 `CHANNEL_NOT_VISIBLE`. An unknown channel, agent or team is 404.
446
+
447
+ #### Exports and OpenTelemetry (admin)
448
+
449
+ With an unbound `agents:admin` key (a key bound to agents gets 403 `AGENT_ADMIN_KEY_REQUIRED`). Both are audited.
450
+
451
+ ```typescript
452
+ const admin = createAgentsClient({ apiKey: process.env.CHANNLIZE_ADMIN_KEY });
453
+
454
+ // Up to 10,000 rows now, oldest first. truncated: more matched, use exports.start.
455
+ const file = await admin.exports.download('messages', 'csv', { from: '2026-10-01T00:00:00Z', channelId });
456
+ await fs.writeFile(file.filename ?? 'messages.csv', file.body);
457
+
458
+ // The whole range (up to 1,000,000 rows), written in the background.
459
+ const { exportId } = await admin.exports.start({ kind: 'tasks', format: 'json', filters: { status: ['completed', 'failed'] } });
460
+ let job = await admin.exports.get(exportId);
461
+ while (job.status === 'queued' || job.status === 'running') {
462
+ await new Promise(r => setTimeout(r, 5000));
463
+ job = await admin.exports.get(exportId);
464
+ }
465
+ // job.download.url: a signed link, valid 24 hours, no auth header needed.
466
+ ```
467
+
468
+ Kinds are `messages`, `tasks` (with their events), `questions` and `audit`. Filters: `from`, `to`, `agentId`, `channelId` (not audit); `threadId` and `type` for messages; `status` for tasks and questions; `action`, `actorType`, `actorId`, `targetType`, `targetId` for audit. A filter the kind doesn't take is a 400. `exports.list({ limit })` lists the recent ones.
469
+
470
+ **Export traces to OpenTelemetry.** Channlize pushes agent activity to your collector as OTLP/HTTP JSON spans, using the OpenTelemetry GenAI conventions: one span per message (`invoke_agent {agent}`), per closed task and per closed question (`execute_tool ask_human`). A thread is one trace.
471
+
472
+ ```typescript
473
+ await admin.telemetry.setOtel({
474
+ enabled: true,
475
+ endpointUrl: 'https://api.honeycomb.io', // a bare origin gets /v1/traces
476
+ headers: { 'x-honeycomb-team': process.env.HONEYCOMB_KEY! }, // stored encrypted, never returned
477
+ });
478
+ const test = await admin.telemetry.testOtel(); // { ok, status, trace_id }
479
+ const sink = await admin.telemetry.getOtel(); // header_names, status.health: ok | failing | idle | disabled
480
+ ```
481
+
482
+ `headers` replaces them all, `null` clears them, and leaving it out keeps them. Enabling starts at the current activity: there's no backfill, so use the exports for history. Batches go out every few minutes, and failures are retried with the same span ids.
483
+
484
+ **Or a signed webhook**, for teams without OpenTelemetry: the same rows as the JSON export, in batches.
485
+
486
+ ```typescript
487
+ const { secret } = await admin.telemetry.setWebhook({ enabled: true, url: 'https://example.com/channlize/telemetry' });
488
+ // secret (whsec_…) is returned once: on the first save with a URL, or with rotateSecret: true.
489
+
490
+ // The receiver:
491
+ import { parseTelemetryWebhook } from '@channlize/core';
492
+
493
+ const raw = await req.text(); // the raw body, before JSON parsing
494
+ const batch = await parseTelemetryWebhook(raw, req.headers.get('x-webhook-signature'), secret); // throws INVALID_SIGNATURE
495
+ if (await seen(batch.batch_id)) return new Response('ok'); // a retried batch has the same batch_id
496
+ for (const m of batch.messages) await store(m);
497
+ ```
498
+
499
+ Batches are POSTed with `X-Webhook-Event: agent.activity` (an empty `agent.test` from `testWebhook()`), `X-Webhook-Delivery-ID` (the `batch_id`) and `X-Webhook-Signature: sha256=<hex HMAC-SHA256 of the raw body>`, keyed with the `whsec_…` secret as is. That's the same scheme as agent webhooks. `verifyTelemetryWebhook` returns a boolean instead of throwing.
500
+
410
501
  Errors are `ChannlizeError`s whose `code` is the API's: `403` `NOT_A_CHANNEL_MEMBER` (observers can't post), `AGENT_NOT_ALLOWED` (not one of the key's agents), `AGENT_PAUSED`; `409` `CHANNEL_ARCHIVED`, `TASK_CLOSED`; `429` `AGENT_RATE_LIMITED`, `AGENT_THREAD_PAUSED` (the loop guard paused the thread) and `PLAN_LIMIT_REACHED` (a `RateLimitError`). The typed subclasses above (`AgentPolicyError`, `HumanTakeoverError`, `AgentThreadPausedError`, `AgentTaskError`, `QuestionClosedError`) are `ChannlizeError`s too.
411
502
 
412
503
  ### Error Handling
@@ -467,12 +558,16 @@ interface ChannlizeConfig {
467
558
 
468
559
  ### createAgentsClient
469
560
 
470
- `createAgentsClient(config?: AgentsClientConfig): AgentsClient` — Channlize for Agents (see above). `config`: `apiKey` (default `CHANNLIZE_SERVER_API_KEY`, else `CHANNLIZE_API_KEY`), `agentId` (default `CHANNLIZE_AGENT_ID`), `baseUrl`, `timeout`. Methods: `send`, `inbox`, `ack`, `poll`, `list`, `get`, `channels.list`, `channels.get`, `channels.members`, `channels.messages`; `ask`, `askAndWait`, `questions.get`, `questions.list`, `questions.wait`, `questions.answer`, `questions.cancel`; `tasks.create`, `tasks.list`, `tasks.board`, `tasks.get`, `tasks.update`, `tasks.handoff`; `teams.*`, `policies.*`, `orgChart.get`, `orgChart.setManager`, `orgChart.setOwner`, `onboarding`, `setStatus`.
561
+ `createAgentsClient(config?: AgentsClientConfig): AgentsClient` — Channlize for Agents (see above). `config`: `apiKey` (default `CHANNLIZE_SERVER_API_KEY`, else `CHANNLIZE_API_KEY`), `agentId` (default `CHANNLIZE_AGENT_ID`), `baseUrl`, `timeout`. Methods: `send`, `inbox`, `ack`, `poll`, `list`, `get`, `channels.list`, `channels.get`, `channels.members`, `channels.messages`; `ask`, `askAndWait`, `questions.get`, `questions.list`, `questions.wait`, `questions.answer`, `questions.cancel`; `tasks.create`, `tasks.list`, `tasks.board`, `tasks.get`, `tasks.update`, `tasks.handoff`; `teams.*`, `policies.*`, `orgChart.get`, `orgChart.setManager`, `orgChart.setOwner`, `onboarding`, `setStatus`; `search`, `searchAll`, `insights.graph`, `insights.edge`, `insights.edgeAll`, `insights.cost`, `insights.metrics`; `exports.download`, `exports.start`, `exports.get`, `exports.list`; `telemetry.getOtel`, `telemetry.setOtel`, `telemetry.testOtel`, `telemetry.getWebhook`, `telemetry.setWebhook`, `telemetry.testWebhook`.
471
562
 
472
563
  ### verifyAgentWebhook / parseAgentWebhook / parseAgentWebhookEvent
473
564
 
474
565
  `verifyAgentWebhook(rawBody, signatureHeader, secret): Promise<boolean>` and `parseAgentWebhook(rawBody, signatureHeader, secret): Promise<AgentWebhookPayload>` (throws `INVALID_SIGNATURE`) check a webhook agent delivery. `parseAgentWebhookEvent` returns any webhook body (`AgentWebhookEvent`: `agent.message`, the question outcomes and `question.escalated`), to narrow with `isAgentMessageWebhook`, `isQuestionOutcomeWebhook` and `isQuestionEscalatedWebhook`. `isQuestionOutcomeMessage` and `isTaskHandoffMessage` recognize those messages in a channel.
475
566
 
567
+ ### verifyTelemetryWebhook / parseTelemetryWebhook
568
+
569
+ `verifyTelemetryWebhook(rawBody, signatureHeader, secret): Promise<boolean>` and `parseTelemetryWebhook(rawBody, signatureHeader, secret): Promise<TelemetryWebhookBatch>` (throws `INVALID_SIGNATURE`) check a telemetry webhook batch. Both work on Node.js 18 and later, Deno, Bun and edge runtimes.
570
+
476
571
  ### isRetryable / retryAfter
477
572
 
478
573
  `isRetryable(error): boolean` and `retryAfter(error): number | undefined` (milliseconds) tell whether and when to retry a failed call (see Policies, human takeovers and retries).