@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.
- package/README.md +6 -0
- package/SKILL.md +724 -0
- package/dist/{agent-models-D6WgsFMZ.d.mts → agent-models-B-wTMdwF.d.mts} +44 -9
- package/dist/{agent-models-D6WgsFMZ.d.ts → agent-models-B-wTMdwF.d.ts} +44 -9
- package/dist/api/index.d.mts +6 -6
- package/dist/api/index.d.ts +6 -6
- package/dist/api/index.js +3 -4
- package/dist/api/index.js.map +1 -1
- package/dist/api/index.mjs +1 -2
- package/dist/api/server.d.mts +3 -3
- package/dist/api/server.d.ts +3 -3
- package/dist/api/server.js +6 -8
- package/dist/api/server.js.map +1 -1
- package/dist/api/server.mjs +5 -7
- package/dist/api/server.mjs.map +1 -1
- package/dist/{chunk-XQ7LOAN3.js → chunk-2FZZW4O4.js} +7 -5
- package/dist/chunk-2FZZW4O4.js.map +1 -0
- package/dist/{chunk-3EHE4O57.mjs → chunk-6FKG2JBT.mjs} +1 -3
- package/dist/{chunk-3EHE4O57.mjs.map → chunk-6FKG2JBT.mjs.map} +1 -1
- package/dist/{chunk-TY57JG3P.mjs → chunk-CCQNGD3U.mjs} +5 -3
- package/dist/chunk-CCQNGD3U.mjs.map +1 -0
- package/dist/{chunk-ZS7DRNCT.js → chunk-DQATIIAV.js} +2 -4
- package/dist/chunk-DQATIIAV.js.map +1 -0
- package/dist/{chunk-L5FLJB3H.mjs → chunk-NHIVBTLU.mjs} +5 -7
- package/dist/chunk-NHIVBTLU.mjs.map +1 -0
- package/dist/{chunk-3PFZRJ4A.js → chunk-QH234LAO.js} +6 -8
- package/dist/chunk-QH234LAO.js.map +1 -0
- package/dist/{chunk-2JDVRL35.js → chunk-QXMZEZRM.js} +2 -4
- package/dist/chunk-QXMZEZRM.js.map +1 -0
- package/dist/{chunk-HYR7PXFU.mjs → chunk-YQFBZW6F.mjs} +1 -3
- package/dist/{chunk-HYR7PXFU.mjs.map → chunk-YQFBZW6F.mjs.map} +1 -1
- package/dist/hooks/index.d.mts +354 -142
- package/dist/hooks/index.d.ts +354 -142
- package/dist/hooks/index.js +1094 -6
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/index.mjs +1107 -19
- package/dist/hooks/index.mjs.map +1 -1
- package/dist/index.d.mts +2 -4
- package/dist/index.d.ts +2 -4
- package/dist/index.js +5 -35
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +8 -38
- package/dist/models/index.d.mts +2 -2
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.js +6 -3
- package/dist/models/index.js.map +1 -1
- package/dist/models/index.mjs +5 -2
- package/dist/{sandbox-DOxoM2Ge.d.ts → sandbox-Djb8gA7e.d.mts} +32 -2
- package/dist/{sandbox-DOxoM2Ge.d.mts → sandbox-Djb8gA7e.d.ts} +32 -2
- package/dist/transport/index.d.mts +3 -3
- package/dist/transport/index.d.ts +3 -3
- package/dist/transport/index.js +3 -4
- package/dist/transport/index.js.map +1 -1
- package/dist/transport/index.mjs +1 -2
- package/dist/{types-BBPz_6kK.d.ts → types-BzNzXaqk.d.ts} +2 -2
- package/dist/{types-BtfxlyHk.d.mts → types-CSyY6Qv7.d.mts} +2 -2
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +3 -4
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/index.mjs +1 -2
- package/package.json +9 -8
- package/dist/chunk-2JDVRL35.js.map +0 -1
- package/dist/chunk-3PFZRJ4A.js.map +0 -1
- package/dist/chunk-43FTKGM6.mjs +0 -1114
- package/dist/chunk-43FTKGM6.mjs.map +0 -1
- package/dist/chunk-L5FLJB3H.mjs.map +0 -1
- package/dist/chunk-RG42SHBX.js +0 -1114
- package/dist/chunk-RG42SHBX.js.map +0 -1
- package/dist/chunk-TY57JG3P.mjs.map +0 -1
- package/dist/chunk-XQ7LOAN3.js.map +0 -1
- 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).
|