@elqnt/agents 3.4.0 → 4.0.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 (72) hide show
  1. package/README.md +6 -0
  2. package/SKILL.md +724 -0
  3. package/dist/{agent-models-D6WgsFMZ.d.mts → agent-models-B-wTMdwF.d.mts} +44 -9
  4. package/dist/{agent-models-D6WgsFMZ.d.ts → agent-models-B-wTMdwF.d.ts} +44 -9
  5. package/dist/api/index.d.mts +6 -6
  6. package/dist/api/index.d.ts +6 -6
  7. package/dist/api/index.js +3 -4
  8. package/dist/api/index.js.map +1 -1
  9. package/dist/api/index.mjs +1 -2
  10. package/dist/api/server.d.mts +3 -3
  11. package/dist/api/server.d.ts +3 -3
  12. package/dist/api/server.js +6 -8
  13. package/dist/api/server.js.map +1 -1
  14. package/dist/api/server.mjs +5 -7
  15. package/dist/api/server.mjs.map +1 -1
  16. package/dist/{chunk-XQ7LOAN3.js → chunk-2FZZW4O4.js} +7 -5
  17. package/dist/chunk-2FZZW4O4.js.map +1 -0
  18. package/dist/{chunk-3EHE4O57.mjs → chunk-6FKG2JBT.mjs} +1 -3
  19. package/dist/{chunk-3EHE4O57.mjs.map → chunk-6FKG2JBT.mjs.map} +1 -1
  20. package/dist/{chunk-TY57JG3P.mjs → chunk-CCQNGD3U.mjs} +5 -3
  21. package/dist/chunk-CCQNGD3U.mjs.map +1 -0
  22. package/dist/{chunk-ZS7DRNCT.js → chunk-DQATIIAV.js} +2 -4
  23. package/dist/chunk-DQATIIAV.js.map +1 -0
  24. package/dist/{chunk-L5FLJB3H.mjs → chunk-NHIVBTLU.mjs} +5 -7
  25. package/dist/chunk-NHIVBTLU.mjs.map +1 -0
  26. package/dist/{chunk-3PFZRJ4A.js → chunk-QH234LAO.js} +6 -8
  27. package/dist/chunk-QH234LAO.js.map +1 -0
  28. package/dist/{chunk-2JDVRL35.js → chunk-QXMZEZRM.js} +2 -4
  29. package/dist/chunk-QXMZEZRM.js.map +1 -0
  30. package/dist/{chunk-HYR7PXFU.mjs → chunk-YQFBZW6F.mjs} +1 -3
  31. package/dist/{chunk-HYR7PXFU.mjs.map → chunk-YQFBZW6F.mjs.map} +1 -1
  32. package/dist/hooks/index.d.mts +354 -142
  33. package/dist/hooks/index.d.ts +354 -142
  34. package/dist/hooks/index.js +1094 -6
  35. package/dist/hooks/index.js.map +1 -1
  36. package/dist/hooks/index.mjs +1107 -19
  37. package/dist/hooks/index.mjs.map +1 -1
  38. package/dist/index.d.mts +2 -4
  39. package/dist/index.d.ts +2 -4
  40. package/dist/index.js +5 -35
  41. package/dist/index.js.map +1 -1
  42. package/dist/index.mjs +8 -38
  43. package/dist/models/index.d.mts +2 -2
  44. package/dist/models/index.d.ts +2 -2
  45. package/dist/models/index.js +6 -3
  46. package/dist/models/index.js.map +1 -1
  47. package/dist/models/index.mjs +5 -2
  48. package/dist/{sandbox-DOxoM2Ge.d.ts → sandbox-Djb8gA7e.d.mts} +32 -2
  49. package/dist/{sandbox-DOxoM2Ge.d.mts → sandbox-Djb8gA7e.d.ts} +32 -2
  50. package/dist/transport/index.d.mts +3 -3
  51. package/dist/transport/index.d.ts +3 -3
  52. package/dist/transport/index.js +3 -4
  53. package/dist/transport/index.js.map +1 -1
  54. package/dist/transport/index.mjs +1 -2
  55. package/dist/{types-BBPz_6kK.d.ts → types-BzNzXaqk.d.ts} +2 -2
  56. package/dist/{types-BtfxlyHk.d.mts → types-CSyY6Qv7.d.mts} +2 -2
  57. package/dist/utils/index.d.mts +1 -1
  58. package/dist/utils/index.d.ts +1 -1
  59. package/dist/utils/index.js +3 -4
  60. package/dist/utils/index.js.map +1 -1
  61. package/dist/utils/index.mjs +1 -2
  62. package/package.json +9 -8
  63. package/dist/chunk-2JDVRL35.js.map +0 -1
  64. package/dist/chunk-3PFZRJ4A.js.map +0 -1
  65. package/dist/chunk-43FTKGM6.mjs +0 -1114
  66. package/dist/chunk-43FTKGM6.mjs.map +0 -1
  67. package/dist/chunk-L5FLJB3H.mjs.map +0 -1
  68. package/dist/chunk-RG42SHBX.js +0 -1114
  69. package/dist/chunk-RG42SHBX.js.map +0 -1
  70. package/dist/chunk-TY57JG3P.mjs.map +0 -1
  71. package/dist/chunk-XQ7LOAN3.js.map +0 -1
  72. package/dist/chunk-ZS7DRNCT.js.map +0 -1
package/SKILL.md ADDED
@@ -0,0 +1,724 @@
1
+ ---
2
+ name: agents
3
+ description: Build a custom frontend that DESIGNS & SAVES an Eloquent agent using the @elqnt/agents hooks. Covers the one true path (custom app → @elqnt/agents hooks → API Gateway → agents Go service), the gateway-token/secret flow, the full Agent TS type (LLM config provider/model/temperature/maxTokens/systemPrompt — there is NO top_p, skills array, tools, csat/handoff), saving a type-valid Partial<Agent>, and the EXACT input/output spec of every method on useAgents, useSkills, useToolDefinitions, useSubAgents, useWidgets. For CHATTING with a saved agent, see the @elqnt/chat SKILL.md.
4
+ ---
5
+
6
+ # Agents — Designing & Saving an Agent
7
+
8
+ Eloquent's **agents** service is the agent registry: an agent is a saved
9
+ configuration object — an LLM config (provider/model/temperature/maxTokens/
10
+ systemPrompt) plus an attached **skills** array, **tools**, and CSAT/handoff
11
+ config. This skill is **only** about one thing: building a custom app that
12
+ **designs and saves** an agent through the `@elqnt/agents` package.
13
+
14
+ > **Two halves, two packages.** Designing/saving an agent is `@elqnt/agents`
15
+ > (this skill). **Chatting** with the saved agent is a different package with a
16
+ > different auth model — `@elqnt/chat` (its own `SKILL.md`). Bind the two by
17
+ > passing `{ agentId }` (or `{ agentName }`) when you start a chat thread; this
18
+ > skill does **not** cover chat. Cross-link:
19
+ > [`@elqnt/chat` → `SKILL.md`](../chat/SKILL.md).
20
+
21
+ | Concern | Package | Backend | Gateway prefix | Auth |
22
+ |---|---|---|---|---|
23
+ | **Design/save** an agent (this skill) | `@elqnt/agents` | `backend/services/agents` | `/api/v1/agents/…` | `@elqnt/api-client` auto-JWT (bearer) |
24
+ | **Chat** with it (separate skill) | `@elqnt/chat` | `backend/services/chat` | `/api/v1/chat/…` | realtime = origin allow-list (no bearer) |
25
+
26
+ ---
27
+
28
+ ## ⛔ The contract — read this before writing any code
29
+
30
+ This skill ships **inside the package**, so the contract is not a separate file
31
+ to keep in sync — it **is** the package's own published type declarations
32
+ (`@elqnt/agents` → `dist/**/*.d.ts`). Because your app *imports the real hooks*,
33
+ the TypeScript compiler enforces the contract for you: a wrong param or a misused
34
+ return value won't compile.
35
+
36
+ The single source of truth, in order:
37
+
38
+ 1. `import { useAgents, useSkills, useToolDefinitions, useSubAgents, useWidgets } from "@elqnt/agents/hooks"`
39
+ — the design-time hooks.
40
+ 2. `import type { UseAgentsReturn, UseSkillsReturn, UseToolDefinitionsReturn, UseSubAgentsReturn, UseWidgetsReturn } from "@elqnt/agents/hooks"`
41
+ — the **named, exact** method surface of each hook (every method, its params,
42
+ its return type). The tables below are a human-readable mirror of these.
43
+ 3. `import type { Agent, AgentSkill, AgentTool, Skill, ToolDefinition, SubAgent, AgentWidget, CSATConfig, HandoffConfig, AgentSummary } from "@elqnt/agents/models"`
44
+ — the DTOs you build and read.
45
+
46
+ **Rules (do not drift):**
47
+
48
+ 1. **Use only the exported hooks** and only the methods they expose. Do **not**
49
+ invent hooks, methods, params, or return shapes — if it's not on
50
+ `UseAgentsReturn` (etc.), it doesn't exist.
51
+ 2. **Let the compiler check you.** Build with `tsc`; never `as any` /
52
+ `@ts-ignore` your way around a hook's types to force a call that isn't there.
53
+ 3. **Never bypass the hooks.** No `fetch`/`axios` to `/api/v1/agents/...`, no
54
+ NATS, no direct service calls. (Server-side only: `@elqnt/agents/api` +
55
+ `createServerClient`, same signatures.)
56
+ 4. **Wrap, don't scatter.** Import the agents hook in exactly one app file (your
57
+ admin/design hook, e.g. `hooks/use-agent-designer.ts`) and have components
58
+ consume it through a context.
59
+ 5. If a requirement seems to need a call the package doesn't define, **stop and
60
+ flag it** — do not improvise an endpoint.
61
+
62
+ The prose/tables below explain each method; the package's shipped `.d.ts` is
63
+ what your code type-checks against.
64
+
65
+ ---
66
+
67
+ ## Architecture — the one and only path
68
+
69
+ ```
70
+ ┌──────────────────────────────────────────────────────────────────────────────┐
71
+ │ YOUR CUSTOM APP (Next.js / React) │
72
+ │ │
73
+ │ SSR layout: read API_GATEWAY_URL_PUBLIC (request-time) + ORG_ID │
74
+ │ └─► AppConfigProvider { apiGatewayUrl, orgId } │
75
+ │ │
76
+ │ components/agent-builder/* ── render the Agent designer UI │
77
+ │ │ useAgentsContext() │
78
+ │ ▼ │
79
+ │ contexts/agents-context.tsx ── one shared useAgents() instance │
80
+ │ │ │
81
+ │ ▼ │
82
+ │ hooks/use-agent-designer.ts ── app hook: the ONLY file importing │
83
+ │ │ @elqnt/agents; CRUD + state │
84
+ │ ▼ │
85
+ │ useAgents / useSkills / useToolDefinitions / … (@elqnt/agents/hooks) │
86
+ │ │ │
87
+ │ ▼ │
88
+ │ @elqnt/agents/api → browserApiRequest │
89
+ │ │ getGatewayToken() ⇒ GET /api/gateway-token (mint HS256 JWT, JWT_SECRET)│
90
+ │ │ attaches: Authorization: Bearer <jwt>, X-Org-ID, X-User-ID, X-Product│
91
+ └─────────┼──────────────────────────────────────────────────────────────────────┘
92
+
93
+ ┌─────────────────┐ verify JWT, stamp X-Org-ID/X-Product, route match
94
+ │ API GATEWAY │ /api/v1/agents/** → agents svc
95
+ └───────┬─────────┘
96
+
97
+ ┌─────────────────┐ agent CRUD, skills/tools/sub-agents/widgets
98
+ │ agents (Go) │ per-product storage, scoped by org_id
99
+ └─────────────────┘
100
+ ```
101
+
102
+ Rules: the frontend **never** calls the agents service directly and **never**
103
+ touches NATS. Every call is HTTP through the gateway carrying an org id + gateway
104
+ token.
105
+
106
+ > **Domain layer is OPTIONAL here.** Unlike the entities backend (where a raw
107
+ > `EntityRecord` is a generic `{ id, name, fields }` envelope that *must* be
108
+ > wrapped in translators), an `Agent` is **already a rich, camelCase domain
109
+ > type**. There's nothing to unwrap. So a translator layer is usually overkill —
110
+ > components can speak `Agent`/`AgentSummary` directly. Still keep the **single
111
+ > wrap point**: `@elqnt/agents` is imported in one app hook, and components
112
+ > consume it via a context (one shared instance, not N re-fetches). Add a
113
+ > translator only if you genuinely need a *different* domain shape than `Agent`.
114
+
115
+ ---
116
+
117
+ ## The gateway token (the secret) — how the custom app gets it
118
+
119
+ Every request to the agents API needs `Authorization: Bearer <token>` — a
120
+ short-lived **HS256 JWT** signed with the shared **gateway secret**
121
+ (`JWT_SECRET`). You never hardcode it; there are two flows depending on where the
122
+ code runs.
123
+
124
+ ### Browser flow (what `useAgents` uses)
125
+
126
+ The hooks → API fns → `browserApiRequest` → `getGatewayToken()` internally. You
127
+ do **not** pass a token to the hook. `getGatewayToken()` (from
128
+ `@elqnt/api-client/browser`) by default does:
129
+
130
+ ```
131
+ fetch("/api/gateway-token") ⇒ { token, expiresIn }
132
+ ```
133
+
134
+ So your custom app must expose a **`/api/gateway-token` route** that mints the
135
+ JWT server-side (the secret stays on the server, never reaches the browser). The
136
+ client caches the token and refreshes ~5 min before expiry.
137
+
138
+ ```ts
139
+ // app/api/gateway-token/route.ts (Next.js route handler — server only)
140
+ import { NextResponse } from "next/server";
141
+ import * as jose from "jose";
142
+
143
+ export async function GET() {
144
+ const secret = new TextEncoder().encode(process.env.JWT_SECRET!); // SAME secret the gateway validates with
145
+ const token = await new jose.SignJWT({
146
+ org_id: process.env.ORG_ID!,
147
+ user_id: "system",
148
+ email: "system@my-app.com",
149
+ role: "system",
150
+ scopes: ["read:agents", "write:agents"], // OR-matched against the route's required scopes
151
+ product: "my-product", // gateway resolves product from THIS claim first
152
+ })
153
+ .setProtectedHeader({ alg: "HS256" })
154
+ .setIssuedAt()
155
+ .setIssuer("eloquent-gateway") // must match gateway JWT_ISSUER
156
+ .setAudience("eloquent-api") // must match gateway JWT_AUDIENCE
157
+ .setExpirationTime("1h")
158
+ .sign(secret);
159
+
160
+ return NextResponse.json({ token, expiresIn: 3600 });
161
+ }
162
+ ```
163
+
164
+ > The gateway re-stamps `X-Org-ID` / `X-User-ID` / `X-Product` from the **signed
165
+ > JWT claims** before proxying, so a forged header can't override the token —
166
+ > the JWT is authoritative. Routes declare required `scopes` (e.g. `write:agents`);
167
+ > the check is an **OR** match, and `admin`/`super_admin` role or a `*` scope
168
+ > bypasses it.
169
+
170
+ Override the token source (mobile/native, or a non-default URL) once at startup:
171
+
172
+ ```ts
173
+ import { configureAuth } from "@elqnt/api-client/browser";
174
+ configureAuth(async () => myTokenProvider()); // URL string or async () => token|null
175
+ // clearGatewayTokenCache() — call after switching orgs
176
+ ```
177
+
178
+ ### Server flow (server actions / SSR)
179
+
180
+ `@elqnt/api-client/server` mints the JWT itself with `jose` — no
181
+ `/api/gateway-token` hop. This is where the secret is injected directly:
182
+
183
+ ```ts
184
+ import { createServerClient } from "@elqnt/api-client/server";
185
+
186
+ const client = createServerClient({
187
+ gatewayUrl: process.env.API_GATEWAY_URL_INTERNAL!, // in-cluster gateway URL (server-side)
188
+ jwtSecret: process.env.JWT_SECRET!, // the shared gateway secret
189
+ defaultProduct: "my-product", // REQUIRED: gateway reads product from the JWT claim first
190
+ defaultScopes: ["read", "write", "admin"],
191
+ });
192
+
193
+ await client.get("/api/v1/agents/by-name?name=support_concierge", { orgId });
194
+ ```
195
+
196
+ > The agents **hooks are browser-only** (`"use client"`). For SSR/server actions
197
+ > call the API fns or `createServerClient` directly — not the hooks.
198
+
199
+ ### Env vars
200
+
201
+ | Var | Used by | Purpose |
202
+ |---|---|---|
203
+ | `JWT_SECRET` | `/api/gateway-token` route + `createServerClient` | sign the gateway JWT (same value the gateway validates with) |
204
+ | `API_GATEWAY_URL_INTERNAL` | `createServerClient` `gatewayUrl` | in-cluster gateway URL (server) |
205
+ | `API_GATEWAY_URL_PUBLIC` | SSR layout → `AppConfigProvider` → browser `baseUrl` | public gateway URL, read **at request time** (not `NEXT_PUBLIC_*`) |
206
+ | `ORG_ID` | token route / app config | the org all requests are scoped to |
207
+
208
+ ### Headers the API layer sets per request
209
+
210
+ | From hook option | Header |
211
+ |---|---|
212
+ | auto token | `Authorization: Bearer <token>` |
213
+ | `orgId` | `X-Org-ID` |
214
+ | `userId` | `X-User-ID` |
215
+ | `userEmail` | `X-User-Email` |
216
+ | `product` (default `"eloquent"`) | `X-Product` |
217
+
218
+ ---
219
+
220
+ ## Hook options
221
+
222
+ There is **no provider/context** in the package. You pass options into each hook
223
+ call. All hooks extend `ApiClientOptions`:
224
+
225
+ ```ts
226
+ interface ApiClientOptions {
227
+ baseUrl: string; // API Gateway base URL — required (from API_GATEWAY_URL_PUBLIC, request-time)
228
+ orgId: string; // required → X-Org-ID
229
+ userId?: string; // → X-User-ID
230
+ userEmail?: string; // → X-User-Email
231
+ product?: string; // → X-Product, defaults to "eloquent"
232
+ headers?: Record<string, string>;
233
+ }
234
+
235
+ type UseAgentsOptions = ApiClientOptions;
236
+ type UseSkillsOptions = ApiClientOptions;
237
+ type UseToolDefinitionsOptions = ApiClientOptions;
238
+ type UseSubAgentsOptions = ApiClientOptions;
239
+
240
+ interface UseWidgetsOptions extends ApiClientOptions {
241
+ agentId: string; // the agent whose widgets you manage — required
242
+ }
243
+ ```
244
+
245
+ > **Imperative, not auto-fetching.** Every method is built on `useApiAsync` and
246
+ > returns an `execute` function. There is no `data`/auto-`loading` per call — you
247
+ > `await listAgents()` to get data; the hook exposes aggregate `loading` and
248
+ > `error` flags. On failure a method returns its default (`[]`, `null`, `false`)
249
+ > and sets `error` — it does **not** throw. `exportAgent` follows the same
250
+ > no-throw contract: it returns the exported `Agent`, or `null` on failure (with
251
+ > `error` set), and as a documented **side-effect** triggers a `.agent.json`
252
+ > browser download when an agent is returned.
253
+
254
+ ---
255
+
256
+ ## The `Agent` type (full)
257
+
258
+ From `@elqnt/agents/models` (`JSONSchema` from `@elqnt/types`):
259
+
260
+ ```ts
261
+ type AgentTypeTS = 'chat' | 'react';
262
+ type AgentSubTypeTS = 'chat' | 'react' | 'workflow' | 'document';
263
+ type AgentStatusTS = 'draft' | 'active' | 'inactive' | 'archived';
264
+ type AgentScopeTS = 'org' | 'team' | 'user' | 'custom';
265
+
266
+ interface Agent {
267
+ id?: string;
268
+ orgId: string;
269
+ product: string;
270
+ type: AgentTypeTS;
271
+ subType: AgentSubTypeTS;
272
+ name: string; // machine name, e.g. "rfp_builder"
273
+ title: string; // human title, e.g. "RFP Builder"
274
+ description?: string;
275
+ status: AgentStatusTS;
276
+ version: string;
277
+
278
+ // === LLM CONFIG (flat on the agent) ===
279
+ provider: string; // "anthropic" | "openai" | "azure-openai"
280
+ model: string; // logical model name, e.g. "claude-sonnet-4-5"
281
+ temperature: number;
282
+ maxTokens: number;
283
+ systemPrompt: string;
284
+ goal?: string; // optional; used to auto-generate a system prompt
285
+
286
+ tags?: string[];
287
+ isDefault: boolean;
288
+ isPublic: boolean;
289
+
290
+ tools?: AgentTool[];
291
+ subAgentIds?: string[];
292
+ skills?: AgentSkill[];
293
+
294
+ csatConfig: CSATConfig; // REQUIRED
295
+ handoffConfig: HandoffConfig; // REQUIRED
296
+ widgetConfig?: WidgetConfig;
297
+ reactConfig?: ReactAgentConfig;
298
+ userSuggestedActionsConfig?: UserSuggestedActionsConfig;
299
+ contextConfig?: AgentContextConfig;
300
+
301
+ configSchema: JSONSchema; // REQUIRED (schema-driven config)
302
+ config?: Record<string, any>;
303
+
304
+ iconName?: string; // Lucide icon name
305
+ capabilities?: string[];
306
+ samplePrompts?: string[];
307
+ responseStyle?: string;
308
+ personalityTraits?: string[];
309
+ fallbackMessage?: string;
310
+ responseDelay?: number; maxConcurrency?: number; sessionTimeout?: number;
311
+
312
+ sourceTemplateId?: string; sourceTemplateVersion?: string;
313
+ scope: AgentScopeTS; // REQUIRED
314
+ scopeId?: string;
315
+ isFavorite: boolean; // REQUIRED
316
+ lastUsedAt?: string;
317
+ usageCount: number; // REQUIRED
318
+ createdAt: string; updatedAt: string; createdBy: string; updatedBy: string; // server-filled
319
+ artifactVersion?: number;
320
+ metadata?: Record<string, any>;
321
+ }
322
+ ```
323
+
324
+ > **LLM knobs are `temperature` + `maxTokens` only** — there is **no `top_p`** on
325
+ > `Agent`, `ReactAgentConfig`, or anywhere in these packages. ReAct agents carry
326
+ > a *second* copy of the LLM config inside `reactConfig` (`provider`, `model`,
327
+ > `temperature`, `maxTokens` + `maxIterations`, `reasoningPrompt`, …).
328
+
329
+ ### The skills array — `AgentSkill` (what's stored ON the agent)
330
+
331
+ Skills are **two-layered**: a catalog `Skill` (CRUD'd separately via `useSkills`)
332
+ vs the per-agent `AgentSkill[]` embedded on `agent.skills` that *references* a
333
+ catalog skill by `skillId` and carries per-agent overrides.
334
+
335
+ ```ts
336
+ interface AgentSkill {
337
+ skillId: string; // → a catalog Skill.id
338
+ skillName?: string; // denormalized for runtime perf
339
+ title?: string; description?: string; category?: string;
340
+ slashCommand?: string; iconName?: string;
341
+ systemPromptExtension?: string; // appended to the system prompt when this skill is active
342
+ tools?: AgentSkillTool[];
343
+ enabled: boolean;
344
+ order: number; // 0 is valid
345
+ }
346
+ interface AgentSkillTool {
347
+ toolId: string; toolName: string;
348
+ title?: string; description?: string; type?: string;
349
+ inputSchema?: JSONSchema;
350
+ configSchema?: JSONSchema; // what CAN be configured
351
+ config?: Record<string, any>; // actual configured VALUES
352
+ enabled: boolean;
353
+ }
354
+ ```
355
+
356
+ The standalone catalog `Skill` (CRUD via `useSkills` → `/api/v1/skills`, **keyed
357
+ by name** in v2 for update/delete): `{ id, name, title, category, slashCommand?,
358
+ tools?: AgentTool[], systemPromptExtension?, configSchema?, persisted?, enabled,
359
+ artifactVersion?, … }`.
360
+
361
+ ### Tools, csat, handoff
362
+
363
+ ```ts
364
+ interface AgentTool {
365
+ toolId: string; toolName: string; title?: string; description?: string;
366
+ type?: string; // document | search | api | extraction
367
+ inputSchema?: JSONSchema; outputSchema?: JSONSchema; configSchema?: JSONSchema;
368
+ config?: Record<string, any>; mergeConfig?: MergeConfig; enabled: boolean;
369
+ isSystem?: boolean; order?: number;
370
+ }
371
+ interface CSATConfig { enabled: boolean; survey?: CSATSurvey; }
372
+ interface CSATSurvey {
373
+ questions: CSATQuestion[]; timeThreshold: number; closeOnResponse: boolean;
374
+ }
375
+ interface HandoffConfig {
376
+ enabled: boolean; triggerKeywords: string[]; queueId?: string; handoffMessage: string;
377
+ }
378
+ ```
379
+
380
+ `AgentTool`s on an agent are usually *customized copies* of a catalog
381
+ `ToolDefinition` (the abstract template — CRUD via `useToolDefinitions`).
382
+
383
+ ---
384
+
385
+ ## Saving a type-valid `Agent` object
386
+
387
+ `createAgent`/`updateAgent` take `Partial<Agent>`, so you don't supply
388
+ server-managed fields (`createdAt`/`updatedAt`/`createdBy`/`updatedBy`,
389
+ `usageCount` audit). The object below also satisfies the **non-optional** fields
390
+ (`csatConfig`, `handoffConfig`, `configSchema`, `scope`, `isFavorite`,
391
+ `usageCount`) so it's a fully-typed standalone `Agent` payload:
392
+
393
+ ```ts
394
+ import type { Agent } from "@elqnt/agents/models";
395
+
396
+ const agent: Partial<Agent> = {
397
+ orgId,
398
+ product: "my-product",
399
+ type: "chat",
400
+ subType: "chat",
401
+ name: "support_concierge",
402
+ title: "Support Concierge",
403
+ description: "Answers product questions and triages tickets.",
404
+ status: "active",
405
+ version: "1.0.0",
406
+
407
+ // LLM config (temperature + maxTokens only — NO top_p)
408
+ provider: "anthropic",
409
+ model: "claude-sonnet-4-5",
410
+ temperature: 0.4,
411
+ maxTokens: 2048,
412
+ systemPrompt: "You are a concise, friendly support concierge. Cite docs when relevant.",
413
+
414
+ isDefault: false,
415
+ isPublic: false,
416
+ scope: "org",
417
+ isFavorite: false,
418
+ usageCount: 0,
419
+
420
+ tools: [],
421
+ subAgentIds: [],
422
+
423
+ skills: [
424
+ {
425
+ skillId: "8f1c…", // a real catalog Skill.id from useSkills().listSkills()
426
+ skillName: "doc_search",
427
+ title: "Document Search",
428
+ enabled: true,
429
+ order: 0,
430
+ systemPromptExtension: "Use document search before answering factual questions.",
431
+ },
432
+ ],
433
+
434
+ csatConfig: { enabled: false },
435
+ handoffConfig: { enabled: false, triggerKeywords: [], handoffMessage: "" },
436
+
437
+ configSchema: { type: "object", properties: {} },
438
+ config: {},
439
+
440
+ iconName: "Bot",
441
+ capabilities: ["Q&A", "Triage"],
442
+ samplePrompts: ["What's your refund policy?", "Open a ticket for a broken item"],
443
+ };
444
+
445
+ const saved = await agents.createAgent(agent); // → Agent (with id + audit fields)
446
+ if (saved) await agents.updateAgent(saved.id!, { temperature: 0.2 });
447
+ ```
448
+
449
+ ---
450
+
451
+ ## Hook: `useAgents` — the agent itself
452
+
453
+ ```ts
454
+ import { useAgents } from "@elqnt/agents/hooks";
455
+
456
+ const agents = useAgents({ baseUrl, orgId, product: "my-product" });
457
+ ```
458
+
459
+ Returns `UseAgentsReturn` = `{ loading, error, ...methods }`:
460
+
461
+ | Method | Signature | Resolves to | Endpoint |
462
+ |---|---|---|---|
463
+ | `listAgents` | `() => Promise<Agent[]>` | `[]` on error | `GET /api/v1/agents` |
464
+ | `listAgentSummaries` | `() => Promise<AgentSummary[]>` | `[]` | `GET /api/v1/agents/summary` |
465
+ | `getAgent` | `(agentId: string) => Promise<Agent \| null>` | `null` | `GET /api/v1/agents/{agentId}` |
466
+ | `createAgent` | `(agent: Partial<Agent>) => Promise<Agent \| null>` | created agent (`null` on error) | `POST /api/v1/agents` |
467
+ | `updateAgent` | `(agentId: string, agent: Partial<Agent>) => Promise<Agent \| null>` | updated agent (`null` on error) | `PUT /api/v1/agents/{agentId}` |
468
+ | `deleteAgent` | `(agentId: string) => Promise<boolean>` | `true`/`false` | `DELETE /api/v1/agents/{agentId}` |
469
+ | `getDefaultAgent` | `() => Promise<Agent \| null>` | `null` | `GET /api/v1/agents/default` |
470
+ | `exportAgent` | `(agentId: string) => Promise<Agent \| null>` | the `Agent` (`null` on error, **non-throwing**); side-effect: triggers a `.agent.json` download when an agent is returned | `GET /api/v1/agents/{agentId}/export` |
471
+ | `importAgent` | `(agent: Agent) => Promise<Agent \| null>` | imported agent (`null` on error) | `POST /api/v1/agents/import` |
472
+
473
+ > `AgentSummary` is the lightweight list shape (`{ id, name, title, description?,
474
+ > iconName?, type, status, isDefault, isPublic, scope, provider?, model?,
475
+ > capabilities?, samplePrompts?, skills?, metadata? }`) — use
476
+ > `listAgentSummaries` for pickers/lists, `getAgent` for the full object.
477
+
478
+ The raw API fns in `@elqnt/agents/api` mirror these (`listAgentsApi`,
479
+ `listAgentsSummaryApi`, `getAgentApi`, `createAgentApi`, `updateAgentApi`,
480
+ `deleteAgentApi`, `getDefaultAgentApi`, `getAgentByNameApi(name, options)` →
481
+ `GET /api/v1/agents/by-name?name=`, `exportAgentApi`, `importAgentApi`), each
482
+ `(...args, options: ApiClientOptions) => Promise<ApiResponse<T>>`. Responses wrap
483
+ as `{ agent }` / `{ agents }`.
484
+
485
+ ---
486
+
487
+ ## Sibling hooks (catalog skills, tools, sub-agents, widgets)
488
+
489
+ All share the same `{ loading, error, ...methods }` shape and the same
490
+ imperative/non-throwing contract.
491
+
492
+ ### `useSkills` — catalog skills (`UseSkillsReturn`)
493
+
494
+ The standalone `Skill` catalog (keyed by **name** in v2). What an agent's
495
+ `AgentSkill[]` references.
496
+
497
+ ```ts
498
+ const skills = useSkills({ baseUrl, orgId, product: "my-product" });
499
+ ```
500
+
501
+ | Method | Signature | Resolves to | Endpoint |
502
+ |---|---|---|---|
503
+ | `listSkills` | `() => Promise<Skill[]>` | `[]` | `GET /api/v1/skills` |
504
+ | `getSkill` | `(skillId: string) => Promise<Skill \| null>` | `null` | `GET /api/v1/skills/{skillId}` |
505
+ | `createSkill` | `(skill: Partial<Skill>) => Promise<Skill \| null>` | created (`null` on error) | `POST /api/v1/skills` |
506
+ | `updateSkill` | `(skillId: string, skill: Partial<Skill>) => Promise<Skill \| null>` | updated (`null` on error) | `PUT /api/v1/skills/by-name?name=` (keyed by `skill.name`) |
507
+ | `deleteSkill` | `(skillId: string) => Promise<boolean>` | bool | `DELETE /api/v1/skills/by-name?name=` |
508
+ | `getCategories` | `() => Promise<string[]>` | `[]` | `GET /api/v1/skills/categories` |
509
+
510
+ ### `useToolDefinitions` — abstract tool templates (`UseToolDefinitionsReturn`)
511
+
512
+ The `ToolDefinition` "templates" agents customize into `AgentTool`s.
513
+
514
+ ```ts
515
+ const tools = useToolDefinitions({ baseUrl, orgId, product: "my-product" });
516
+ ```
517
+
518
+ | Method | Signature | Resolves to | Endpoint |
519
+ |---|---|---|---|
520
+ | `listToolDefinitions` | `() => Promise<ToolDefinition[]>` | `[]` | `GET /api/v1/tool-definitions` |
521
+ | `getToolDefinition` | `(toolDefId: string) => Promise<ToolDefinition \| null>` | `null` | `GET /api/v1/tool-definitions/{toolDefId}` |
522
+ | `getToolDefinitionsByIds` | `(ids: string[]) => Promise<ToolDefinition[]>` | `[]` | `POST /api/v1/tool-definitions/by-ids` body `{ ids }` |
523
+ | `createToolDefinition` | `(td: Partial<ToolDefinition>) => Promise<ToolDefinition \| null>` | created (`null`) | `POST /api/v1/tool-definitions` body `{ toolDefinition }` |
524
+ | `updateToolDefinition` | `(toolDefId: string, td: Partial<ToolDefinition>) => Promise<ToolDefinition \| null>` | updated (`null`) | `PUT /api/v1/tool-definitions/{toolDefId}` body `{ toolDefinition }` |
525
+ | `deleteToolDefinition` | `(toolDefId: string) => Promise<boolean>` | bool | `DELETE /api/v1/tool-definitions/{toolDefId}` |
526
+
527
+ ### `useSubAgents` — sub-agents (`UseSubAgentsReturn`)
528
+
529
+ The `SubAgent`s referenced by `agent.subAgentIds`.
530
+
531
+ ```ts
532
+ const subAgents = useSubAgents({ baseUrl, orgId, product: "my-product" });
533
+ ```
534
+
535
+ | Method | Signature | Resolves to | Endpoint |
536
+ |---|---|---|---|
537
+ | `listSubAgents` | `() => Promise<SubAgent[]>` | `[]` | `GET /api/v1/subagents` |
538
+ | `getSubAgent` | `(subAgentId: string) => Promise<SubAgent \| null>` | `null` | `GET /api/v1/subagents/{subAgentId}` |
539
+ | `createSubAgent` | `(sa: Partial<SubAgent>) => Promise<SubAgent \| null>` | created (`null`) | `POST /api/v1/subagents` body `{ subAgent }` |
540
+ | `updateSubAgent` | `(subAgentId: string, sa: Partial<SubAgent>) => Promise<SubAgent \| null>` | updated (`null`) | `PUT /api/v1/subagents/{subAgentId}` body `{ subAgent }` |
541
+ | `deleteSubAgent` | `(subAgentId: string) => Promise<boolean>` | bool | `DELETE /api/v1/subagents/{subAgentId}` |
542
+
543
+ ### `useWidgets` — embeddable chat widgets for one agent (`UseWidgetsReturn`)
544
+
545
+ The only sibling whose options carry an **`agentId`** — the agent is fixed at
546
+ hook creation (`agent.widgetConfig` is the inline copy; these are the stored
547
+ `AgentWidget`s).
548
+
549
+ ```ts
550
+ const widgets = useWidgets({ baseUrl, orgId, product: "my-product", agentId });
551
+ ```
552
+
553
+ | Method | Signature | Resolves to | Endpoint |
554
+ |---|---|---|---|
555
+ | `listWidgets` | `() => Promise<AgentWidget[]>` | `[]` | `GET /api/v1/agents/{agentId}/widgets` |
556
+ | `getWidget` | `(widgetId: string) => Promise<AgentWidget \| null>` | `null` | `GET /api/v1/widgets/{widgetId}` |
557
+ | `getDefaultWidget` | `() => Promise<AgentWidget \| null>` | `null` | `GET /api/v1/agents/{agentId}/widgets/default` |
558
+ | `createWidget` | `(w: Partial<AgentWidget>) => Promise<AgentWidget \| null>` | created (`null`) | `POST /api/v1/agents/{agentId}/widgets` body `{ widget }` |
559
+ | `updateWidget` | `(widgetId: string, w: Partial<AgentWidget>) => Promise<AgentWidget \| null>` | updated (`null`) | `PUT /api/v1/widgets/{widgetId}` body `{ widget }` |
560
+ | `deleteWidget` | `(widgetId: string) => Promise<boolean>` | bool | `DELETE /api/v1/widgets/{widgetId}` |
561
+ | `setDefaultWidget` | `(widgetId: string) => Promise<boolean>` | bool | `POST /api/v1/widgets/{widgetId}/default` body `{ agentId }` |
562
+
563
+ ### Other resource hooks (all export a named `Use<Name>Return`)
564
+
565
+ Beyond the five detailed above, every resource hook in this package now exports
566
+ its exact, named return interface from `@elqnt/agents/hooks` — the `.d.ts` is the
567
+ contract. All follow the same imperative, **non-throwing** shape
568
+ (`{ loading, error, ...methods }`), except `useBackgroundAgents`, which is a
569
+ **realtime** hook that additionally holds SSE stream state (`liveStates`,
570
+ `watchedChats`).
571
+
572
+ | Hook | Named return | Notes |
573
+ |---|---|---|
574
+ | `useAgentJobs` | `UseAgentJobsReturn` | PostgreSQL-backed job CRUD + pause/resume |
575
+ | `useAnalytics` | `UseAnalyticsReturn` | agent chats / CSAT / list / task-outcomes (defaults `[]`) |
576
+ | `useIntegrations` | `UseIntegrationsReturn` | OAuth connect/disconnect/refresh/triage |
577
+ | `useSandbox` | `UseSandboxReturn` | AI-generated HTML sandboxes |
578
+ | `useSchedulerTasks` | `UseSchedulerTasksReturn` | one-off scheduler tasks |
579
+ | `useSchedulerSchedules` | `UseSchedulerSchedulesReturn` | recurring schedules |
580
+ | `useSkillUserConfig` | `UseSkillUserConfigReturn` | per-user/per-agent skill config |
581
+ | `useStructuredOutput<T>` | `UseStructuredOutputReturn<T>` | one-shot structured LLM output (`run` → `T \| null`) |
582
+ | `useBackgroundAgents` | `UseBackgroundAgentsReturn` | **realtime** — SSE stream + watched-chat state |
583
+
584
+ ---
585
+
586
+ ## The raw hook (reference only — wrap it)
587
+
588
+ This is the bare `useAgents` hook, shown so the spec above is concrete. It's fine
589
+ for a one-off spike, but a real app **must** import `@elqnt/agents` in exactly one
590
+ app hook and let components consume it via a context. `baseUrl`/`orgId` come from
591
+ `useAppConfig()` (fed by SSR), never a `NEXT_PUBLIC_*` var.
592
+
593
+ ```tsx
594
+ "use client";
595
+ import { useAgents } from "@elqnt/agents/hooks";
596
+ import { useEffect, useState } from "react";
597
+ import type { Agent } from "@elqnt/agents/models";
598
+ import { useAppConfig } from "@/contexts/app-config-context";
599
+
600
+ export function AgentList() {
601
+ const { apiGatewayUrl, orgId } = useAppConfig();
602
+ const agents = useAgents({ baseUrl: apiGatewayUrl, orgId, product: "my-product" });
603
+
604
+ const [rows, setRows] = useState<Agent[]>([]);
605
+
606
+ useEffect(() => {
607
+ agents.listAgents().then(setRows);
608
+ }, []); // eslint-disable-line
609
+
610
+ async function createDraft() {
611
+ const created = await agents.createAgent({
612
+ orgId, product: "my-product",
613
+ type: "chat", subType: "chat",
614
+ name: "support_concierge", title: "Support Concierge",
615
+ status: "draft", version: "1.0.0",
616
+ provider: "anthropic", model: "claude-sonnet-4-5",
617
+ temperature: 0.4, maxTokens: 2048,
618
+ systemPrompt: "You are a friendly support concierge.",
619
+ isDefault: false, isPublic: false, scope: "org",
620
+ isFavorite: false, usageCount: 0,
621
+ csatConfig: { enabled: false },
622
+ handoffConfig: { enabled: false, triggerKeywords: [], handoffMessage: "" },
623
+ configSchema: { type: "object", properties: {} },
624
+ });
625
+ if (created) setRows((r) => [created, ...r]);
626
+ }
627
+
628
+ if (agents.error) return <p>Error: {agents.error}</p>;
629
+ return (
630
+ <div>
631
+ <button onClick={createDraft} disabled={agents.loading}>New</button>
632
+ <ul>{rows.map((a) => <li key={a.id}>{a.title}</li>)}</ul>
633
+ </div>
634
+ );
635
+ }
636
+ ```
637
+
638
+ ### The wrap point (context)
639
+
640
+ ```tsx
641
+ // contexts/agents-context.tsx
642
+ "use client";
643
+ import { createContext, useContext, type ReactNode } from "react";
644
+ import { useAgents, type UseAgentsReturn } from "@elqnt/agents/hooks";
645
+ import { useAppConfig } from "@/contexts/app-config-context";
646
+
647
+ const AgentsContext = createContext<UseAgentsReturn | undefined>(undefined);
648
+
649
+ export function AgentsProvider({ children }: { children: ReactNode }) {
650
+ const { apiGatewayUrl, orgId } = useAppConfig();
651
+ const value = useAgents({ baseUrl: apiGatewayUrl, orgId, product: "my-product" });
652
+ return <AgentsContext.Provider value={value}>{children}</AgentsContext.Provider>;
653
+ }
654
+
655
+ export function useAgentsContext(): UseAgentsReturn {
656
+ const ctx = useContext(AgentsContext);
657
+ if (!ctx) throw new Error("useAgentsContext must be used within AgentsProvider");
658
+ return ctx;
659
+ }
660
+ ```
661
+
662
+ `AppConfigProvider` (the `{ apiGatewayUrl, orgId }` SSR config context) is the
663
+ same one the entities skill defines — read `API_GATEWAY_URL_PUBLIC` server-side
664
+ at request time in the layout and forward it through the provider; do **not** use
665
+ a `NEXT_PUBLIC_*` var (those bake the build-env URL into the bundle forever).
666
+
667
+ ---
668
+
669
+ ## Provisioning agents (admin / bulk)
670
+
671
+ For seeding platform-default agents (with their tools/sub-agents/skills) rather
672
+ than one-off CRUD:
673
+
674
+ ```ts
675
+ import { provisionAgentsApi } from "@elqnt/agents/api";
676
+ import type { DefaultDefinitions } from "@elqnt/agents/models";
677
+
678
+ await provisionAgentsApi(defs /* DefaultDefinitions */, { baseUrl, orgId });
679
+ ```
680
+
681
+ ---
682
+
683
+ ## Gotchas
684
+
685
+ - **Two packages, two auth models.** Design/save = `@elqnt/agents` (this skill,
686
+ bearer JWT, `baseUrl` = gateway root). Chatting = `@elqnt/chat` (separate
687
+ `SKILL.md`; realtime uses an **origin allow-list, no bearer**, `baseUrl` =
688
+ `.../api/v1/chat`). Don't conflate them.
689
+ - **LLM knobs are `temperature` + `maxTokens` only** — **`top_p` does not exist**
690
+ in these types. Don't add it.
691
+ - **Methods don't throw** — *including* `exportAgent`. They return defaults
692
+ (`[]`, `null`, `false`) and set `error`. Check `error` / null results; don't
693
+ wrap in try/catch expecting throws. `exportAgent` returns `Agent | null`
694
+ (`null` on failure) and triggers a `.agent.json` browser download as a
695
+ documented side-effect when an agent is returned.
696
+ - **Create/update take `Partial<Agent>`** — don't send audit fields
697
+ (`createdAt`/`createdBy`/…); the backend fills them. But the required fields
698
+ (`csatConfig`, `handoffConfig`, `configSchema`, `scope`, `isFavorite`,
699
+ `usageCount`) must be present for a fully-typed standalone `Agent`.
700
+ - **Skills are two-layered** — catalog `Skill` (CRUD via `useSkills`, **keyed by
701
+ name** in v2 for update/delete) vs per-agent `AgentSkill[]` on `agent.skills`
702
+ (references `skillId`, carries `enabled`/`order`/overrides). To pick up later
703
+ changes to a catalog skill's `configSchema`, **re-attach** the skill on the
704
+ agent — it's snapshotted at attach time.
705
+ - **`useWidgets` needs `agentId` in options** — unlike the other sibling hooks.
706
+ - **The package has no provider.** Options go into every hook call — which is why
707
+ your app builds an `AppConfigProvider` (inject `baseUrl`/`orgId` once via SSR)
708
+ and wraps the hook in one context, so components and the app hook never
709
+ re-thread config by hand.
710
+ - **Components don't need a translator layer.** `Agent`/`AgentSummary` are
711
+ already rich domain types — speak them directly. (Only the single wrap point —
712
+ one hook import behind a context — is mandatory, not translators.)
713
+ - **`product` consistency** — set the same `product` in the JWT claim and the
714
+ hook options, or you'll read/write the wrong product's agents.
715
+ - **Bumping a platform-shipped agent?** Bump its `artifactVersion` in the
716
+ registry so the reconciler rolls the change out to existing orgs (see skill
717
+ `29-data-changes` §C). This is separate from per-org CRUD.
718
+
719
+ ## Related skills
720
+
721
+ - `@elqnt/chat` → `SKILL.md` — **chat** with the agent you saved here (POST +
722
+ SSE; bind the thread via `startChat({ agentId })`).
723
+ - `entities` (`@elqnt/entity` → `SKILL.md`) — drive the entities backend the same
724
+ way (hooks → gateway).