@uipath/delegate-sdk 1.203.0-preview.20260929135425 → 1.203.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 CHANGED
@@ -128,7 +128,7 @@ Notes:
128
128
  - Registering your own client id requires the loopback redirect URIs `http://localhost:8055/oidc/login`, `http://localhost:42042/oidc/login` and `http://localhost:8104/oidc/login` (the login binds the first free one, in that order) and the `openid profile offline_access` scopes (`offline_access` is what yields the refresh token).
129
129
  - A refresh grant is bound to the client that issued it. After changing the client id, run login again — refreshing an old grant under a new client fails with `invalid_grant`.
130
130
 
131
- **No-browser alternative**: pass `auth: { accessToken, tenantId, organizationId }` directly (e.g. from `AUTH_TOKEN` / `TENANT_ID` / `ORG_ID` env vars).
131
+ **No-browser alternative**: pass `auth: { accessToken, tenantId, organizationId }` directly (e.g. from `AUTH_TOKEN` / `TENANT_ID` / `ORG_ID` env vars). Such a token is never refreshed; when your host can renew it, also pass `accessTokenFile` and keep that file rewritten with the current token — it is re-read on every request, so a run can outlive the token it started with.
132
132
 
133
133
  ---
134
134
 
@@ -182,36 +182,52 @@ await agent.initialize({
182
182
  keepInteropAlive: false,
183
183
  });
184
184
 
185
- await agent.sendMessage('open the Settings app');
185
+ const { text, failed } = await agent.sendMessage('open the Settings app');
186
+ if (failed) throw new Error(text); // the turn ended in an error; text is its message
186
187
  await agent.destroy();
187
188
  ```
188
189
 
190
+ `sendMessage(prompt, options?)` runs one agent turn and resolves with a `SendMessageResult` `{ text, failed, sessionId, turnId?, messages, structured?, structuredError? }`. `failed` is `true` when the turn ended in an actionable error (a model or backend failure the agent loop reported as the turn's outcome); `text` then holds the error message, not an answer. `messages` holds the messages the turn appended to the session, without the history loaded for a resumed session. `SendMessageOptions` is `{ sessionId?, attachments?, outputSchema?, extractionModel?, extractionMethod?, extractionPrompt? }`: without `sessionId` the turn runs in `AgentConfig.sessionId`, then the agent's current session, else a new one. See [Structured output from an agent turn](#structured-output-from-an-agent-turn) and [Attachments](#attachments). `sendMessage` rejects when the turn cannot run (not initialized, a bad attachment, a refused session) or is cancelled ([Cancelling a turn](#cancelling-a-turn)), not for a failed turn.
191
+
189
192
  ### `AgentConfig` options
190
193
 
191
194
  | Option | Default | Notes |
192
195
  |---|---|---|
193
196
  | `backendUrl` | — (required unless `environment` is set) | Full agent service URL, e.g. `https://cloud.uipath.com/<org>/<tenant>/delegate_`. `initialize()` rejects when neither it nor `environment` selects a backend — the SDK has no default. An explicit value (or the `BACKEND_URL` env var) wins over `environment`. |
194
197
  | `environment` | unset | `alpha` \| `staging` \| `production` \| `localhost`. Derives `backendUrl` when it is omitted: `localhost` → `http://localhost:5002`; a cloud → `<cloud>/<org>/<tenant>/delegate_` from the `organizationName` (or `orgLogicalName`) and `tenantName` slugs on `auth` — a login saved by `runLoginFlow` works as-is. Fails loudly when the slugs are missing. Also exported on its own as `resolveBackendUrl(env, auth, explicitUrl)`. |
195
- | `model` | `gemini_3_6_flash` | LLM id. |
198
+ | `model` | `gpt_5_6_luna` | LLM id; the Delegate app's default. An explicit value is checked against the tenant's catalog at `initialize()`, see [Model catalog](#model-catalog). |
199
+ | `validateModel` | `true` | False → skip that catalog check (no request). |
196
200
  | `enableComputerUse` | `true` | False → drop the **screen**, not interop: no screenshots, no window list, no focused element, and `AppFind` / `AppAct` / `AppGetDescriptor` leave the tool catalog. File, Office, PDF and shell tools are unaffected — they reach interop through the same URL. Pair with `autoSpawnInterop: true` (the default) to keep them working. |
197
201
  | `autoSpawnInterop` | `true` | False → manage interop yourself, pass `interopUrl`. This, not `enableComputerUse`, is the gate on whether interop exists at all. False **and** no `interopUrl` → no interop, so every interop-backed tool fails; skills still load (they read through Node). |
198
202
  | `keepInteropAlive` | `false` | True → leave interop running after `destroy()`. |
199
203
  | `enableConnections` | `true` | Orchestrator ConnectionService prewarm. |
200
204
  | `enableSecurity` | `true` | Tool security wrapper. |
201
205
  | `enableSkills` | `true` | Skills system. Local + remote skills always load when enabled; the app-shipped bundle additionally requires a discoverable path (a warning fires if none is found — skills are not disabled). |
206
+ | `enableMemory` | `true` | Send the Delegate app's local long-term memories with each turn: the file tree, `user_preferences.md` and `overview.md` from `<userData>/Memories` — `~/Library/Application Support/UiPath/Delegate` (macOS), `%APPDATA%\UiPath\Delegate` (Windows), `~/.config/UiPath/Delegate` (Linux), the same folder the desktop app uses (created if missing). The backend uses them on a conversation's first turn and then also asks the model to save preferences and learnings there with `WriteFile` / `EditFile`, so memories are shared with the app both ways. Writing needs interop (the file tools run there). `false` sends no memories and turns those instructions off. `resolveDelegateMemoriesLocation()` returns the folders. |
202
207
  | `bundledSkillsPath` | auto | Absolute path to a bundled skills directory. **Merged** (deduped by path) with `DELEGATE_BUNDLED_SKILLS_PATH` env and any installed UiPath desktop app — every existing root loads, not just the first. Installed-app locations: `/Applications/UiPath Delegate.app/Contents/Resources/skills` (standalone Delegate) and `/Applications/UiPath Assistant.app/Contents/Resources/skills` (merged Delegate + Assistant bundle) on macOS, `%LOCALAPPDATA%\Programs\UiPath\UiPath.Delegate\resources\skills` / `%PROGRAMFILES%\…` (Windows). Linux has no installed-app source; pass this option or set the env var. Same skill name across two roots is deduped downstream (explicit > env > app). |
203
208
  | `preloadSkills` | `[]` | Skill names attached to SDK-created sessions before the first model turn. |
204
209
  | `maxSteps` | `0` | Hard cap on agentic-loop steps (`0` = unlimited). |
205
210
  | `workingDirectory` | unset | Default cwd for shell tools and the userData base for wiki/project state. Must be absolute; created at `initialize()` if missing. |
206
211
  | `shellPathPrepend` | `[]` | Directories to prepend to `PATH` for every shell-tool command (`ExecuteBashCommand` / `ExecutePowershellCommand`), first entry wins. For harnesses that shadow real CLIs with mock scripts (e.g. a sandbox's `mocks/uip` must beat the installed `uip`). Delivered through the per-command `variables` env the interop service applies, so it reaches commands even though they run inside interop; any existing occurrence of a listed dir is de-duplicated out of the base PATH. |
207
- | `projectId` | unset | Local project routing key. Wiki state uses `<userDataBase>/projects/<projectId>/wiki`; the id is not sent to the backend. It is used as that folder name verbatim, so `initialize()` rejects an id that is not a safe single segment (`isSafeProjectFolderSegment`, exported for callers that validate earlier). With `business-analysis` preloaded, the SDK also supplies project paths and seeds its checklist/progress files without overwriting existing files. |
208
- | `sessionId` | unset | Pin the session id used when a `sendMessage` call omits one, so the standalone wiki lands at `<userDataBase>/sessions/<sessionId>/wiki` deterministically instead of a random server-assigned id. `projectId` takes precedence. Unlike `projectId`, the id is a backend entity: a pinned id skips `createSession`, so it must be one the backend accepts. An explicit `sendMessage` sessionId argument overrides it. |
212
+ | `projectId` | unset | Local project routing key. Wiki state uses `<userDataBase>/projects/<projectId>/wiki`; the id is not sent to the backend. It is used as that folder name verbatim, so `initialize()` rejects an id that is not a safe single segment (`isSafeProjectFolderSegment`, exported for callers that validate earlier). With `business-analysis` preloaded, the SDK also supplies project paths and seeds its task-list/progress files without overwriting existing files. Not a backend project; see `backendProjectId`. |
213
+ | `backendProjectId` | unset | Backend project id (from `listProjects()` / `createProject()`) that every session this agent **creates** is filed in, as the Delegate app does for a chat started inside a project. Sent as `project_id` when the session is created. Sessions you supply are not moved (use `addSessionToProject`). Independent of `projectId`. An id the backend does not know makes the first `sendMessage` reject with a 404. See [Backend projects](#backend-projects). |
214
+ | `sessionId` | unset | Pin the session id used when a `sendMessage` call omits one, so the standalone wiki lands at `<userDataBase>/sessions/<sessionId>/wiki` deterministically instead of a random server-assigned id. `projectId` takes precedence. Unlike `projectId`, the id is a backend entity: a pinned id skips `createSession`, so it must name an existing session ([Resuming a session](#resuming-a-session)). `sendMessage`'s `sessionId` option overrides it. |
209
215
  | `effort` | model default | Reasoning-effort tier: `low` / `medium` / `high` / `xhigh` / `max`. Forwarded to the backend as `user_config.effort`. Invalid values are ignored. |
210
216
  | `requestHeaders` | `{}` | Extra HTTP headers stamped on every backend request (REST + SSE), applied at `initialize()`. For service-token (S2S) callers that must supply `X-UiPath-Internal-*` context headers, or per-request routing flags. |
211
217
  | `verbose` | `false` | Surface framework's internal logs. |
212
218
 
213
219
  `initialize()` also rejects when `auth` disagrees with `backendUrl`: a token whose organization claim differs from `auth.organizationId`, or saved `orgLogicalName` / `tenantName` slugs that differ from the `/<org>/<tenant>/delegate_` segments of the URL (case-insensitive). This keeps a refreshed or tenant-switched login from running against another tenant. A `backendUrl` without that route (a local or self-hosted service) is not judged, and neither is a custom `authStateProvider`. The same check is exported as `findAuthBackendMismatches(auth, backendUrl)`.
214
220
 
221
+ ### Resuming a session
222
+
223
+ Passing an existing session id (`sendMessage(prompt, { sessionId })` or `AgentConfig.sessionId`) in a fresh process resumes it. Before the first turn the SDK loads the session's backend history (`GET /v1/chat/sessions/{id}/messages/`) into its session store, the same way the Delegate app opens a conversation, so `getSessionMessages()` returns the earlier turns followed by the new one, in the app's message shape.
224
+
225
+ - History is not replayed as events: `onEvent` sees only the new turn's `message` / `tool_call` / `tool_result` events, and `sendMessage` resolves to the new turn's answer, never an earlier one.
226
+ - History is not re-sent to the model: the backend already holds the conversation and each turn sends only the new prompt.
227
+ - The SDK looks the id up with `getSession` (`GET /v1/chat/sessions/{id}/metadata`). A 404 rejects `sendMessage` before anything is sent: the backend assigns the id when it creates a session, so it cannot run a turn on an id it does not know. Omit the id to start a new session. For a found session the SDK also finds its row in the session list (the newest 500), because only the list says whether it is a sub-agent. A sub-agent session id is refused. So is a resume whose lookup fails for any other reason, including a 422 for an id that is not a UUID.
228
+ - If the history itself cannot be loaded, the SDK logs a `console.warn` and runs the turn anyway: the model still has the full context on the backend, only the local `getSessionMessages()` transcript lacks the earlier turns.
229
+ - The lookup also finds a session another user shared with you, but the backend runs turns only in your own sessions. It refuses that send with a 404 `session_not_found`, and `sendMessage` resolves with `failed: true` and that message as `text`.
230
+
215
231
  ### Health check (no prompt sent)
216
232
 
217
233
  `runHealthCheck` verifies a setup without creating a session or sending a message — for a pre-flight step on an unattended machine, or to tell an auth problem from an interop problem:
@@ -229,7 +245,634 @@ for (const check of result.checks) console.log(check.name, check.status, check.d
229
245
  if (!result.ok) process.exit(1);
230
246
  ```
231
247
 
232
- Three entries come back, in order: `auth` (token not expired, and consistent with `backendUrl` as above), `backend` (one authenticated, read-only `GET <backendUrl>/v1/config`), and `interop` (`spawn` starts the bundled interop, waits for `/health`, and stops it again unless one was already running; `{ url }` probes an interop you manage; `skip` reports `skipped`). Failures are reported in the result, never thrown, so every problem is listed at once.
248
+ Four entries come back, in order: `auth` (token not expired, and consistent with `backendUrl` as above), `readiness` (one unauthenticated `GET <backendUrl>/readiness`, which the service answers after checking its database; the detail carries its status message), `backend` (one authenticated, read-only `GET <backendUrl>/v1/config`), and `interop` (`spawn` starts the bundled interop, waits for `/health`, and stops it again unless one was already running; `{ url }` probes an interop you manage; `skip` reports `skipped`). Failures are reported in the result, never thrown, so every problem is listed at once.
249
+
250
+ #### Backend readiness
251
+
252
+ `readiness` is resolved against `backendUrl` itself: the service root for a local backend (`http://localhost:5002/readiness`), and the `delegate_` route for UiPath cloud, which forwards the path to the service. It proves the service is up and its database reachable independently of the login, so a down service is not misread as an auth problem:
253
+
254
+ - a 5xx (the service answers `500 {"status":"Service is not ready."}` when its database check fails) or a network error fails `readiness`, and `backend` is then `skipped` — a not-ready service says nothing about the token;
255
+ - a 401, 403 or 404 means the URL does not expose readiness to an anonymous caller (a gateway in front of the service); `readiness` is `skipped` and the authenticated `backend` check still decides.
256
+
257
+ ### Direct backend calls
258
+
259
+ `BackendClient` calls Agents Backend routes directly, with the same auth and identifying headers the agent sends (`Authorization: Bearer …`, tenant and organization ids, the Delegate consuming-product id, and your `requestHeaders`). Get one from an initialized agent, or build one from a login without an agent:
260
+
261
+ ```ts
262
+ import { createBackendClient, isBackendRequestError, loadAndRefreshAuth } from '@uipath/delegate-sdk';
263
+
264
+ const auth = await loadAndRefreshAuth();
265
+ const client = createBackendClient({ auth, backendUrl: 'https://cloud.uipath.com/<org>/<tenant>/delegate_' });
266
+ // or: const client = agent.getBackendClient();
267
+
268
+ const config = await client.get<{ MODELS: unknown }>('v1/config', { query: { verbose: true } });
269
+ await client.post('v1/items', { name: 'n' }); // JSON body
270
+ await client.upload('v1/files', form); // FormData (multipart)
271
+ const stream = await client.raw('GET', 'v1/stream', { signal }); // the Response, body unread
272
+
273
+ try {
274
+ await client.get('v1/sessions/unknown');
275
+ } catch (error) {
276
+ if (isBackendRequestError(error, 404)) { /* not found */ } else throw error;
277
+ }
278
+ ```
279
+
280
+ - Paths are relative to `backendUrl`; absolute URLs are refused so the token never leaves the backend.
281
+ - The auth headers are read on every request, so a refreshed token is picked up. A 401 triggers one token refresh and retry when the auth provider can refresh.
282
+ - Each call has a 30 s deadline (`timeoutMs` on the client or per call; `0` turns it off) and takes an `AbortSignal`. A deadline rejects with an error named `TimeoutError`. For `raw()` the deadline covers only the wait for the response headers.
283
+ - A non-2xx answer rejects with `BackendRequestError`: `status`, `method`, `path`, the raw `body`, and `detail` parsed from a FastAPI `{ "detail": … }` body. For a 422, `validationIssues` lists the field errors. `isBackendRequestError(error, ...statuses)` narrows by status, for example 401 (login rejected), 404, 413 (payload too large) or 422.
284
+ - `createBackendClient` runs the same auth-against-`backendUrl` check as `initialize()`.
285
+
286
+ ### Listing sessions
287
+
288
+ `listSessions` returns one page of your backend chat sessions, most recently active first. Call it on an agent, or pass a `BackendClient` to use it without one:
289
+
290
+ ```ts
291
+ import { createBackendClient, listSessions } from '@uipath/delegate-sdk';
292
+
293
+ const { sessions, isLastPage } = await agent.listSessions({ page: 1, pageSize: 50 });
294
+ // or: await listSessions(createBackendClient({ auth, backendUrl }), { page: 2 });
295
+
296
+ for (const s of sessions) console.log(s.id, s.updatedAt, s.title); // resume one with AgentConfig.sessionId
297
+ ```
298
+
299
+ | Option | Default | |
300
+ |---|---|---|
301
+ | `page` | `1` | 1-based page number. |
302
+ | `pageSize` | `30` | Rows per page, 1–100 (`MAX_SESSION_PAGE_SIZE`). |
303
+ | `sortBy` / `order` | `'updatedAt'` / `'desc'` | `'updatedAt'` or `'createdAt'`; `'asc'` or `'desc'`. |
304
+ | `storageMode` | `'remote'` | `'remote'` (history stored by the backend) or `'local'`. |
305
+ | `productId` | every product | Only sessions created by this product. |
306
+ | `includeSubAgents` | `false` | Keep sub-agent sessions. By default they are dropped, as in the desktop app's history. |
307
+ | `signal` | — | Cancels the request. |
308
+
309
+ Each `SessionSummary` has `id`, `title`, `createdAt`, `updatedAt` (ISO-8601 strings as the backend sends them), `parentSessionId` (set on a sub-agent session), `storageMode`, `productId`, `projectId` and `metadata`. Messages are not included. The page also returns `page`, `pageSize` and `isLastPage`. `isLastPage` is `true` when the backend returned fewer than `pageSize` rows. Hidden sub-agent rows still count toward that total, so a page can show fewer sessions than `pageSize` and still have more pages after it. An out-of-range `page` or `pageSize` throws a `RangeError` before any request is sent. Backend errors reject with `BackendRequestError`.
310
+
311
+ ### Looking up, renaming and deleting a session
312
+
313
+ Each function takes a `BackendClient` and a session id, plus an optional `{ signal }`. An initialized `DelegateAgent` has the same methods without the client:
314
+
315
+ ```ts
316
+ import { createBackendClient, deleteSession, getSession, renameSession } from '@uipath/delegate-sdk';
317
+
318
+ const session = await agent.getSession(id); // undefined when the backend does not know the id
319
+ if (session) console.log(session.title, session.userId);
320
+ await agent.renameSession(id, 'Q3 close'); // → { id, title }
321
+ await deleteSession(createBackendClient({ auth, backendUrl }), id);
322
+ ```
323
+
324
+ | Function | Route | Result |
325
+ |---|---|---|
326
+ | `getSession(client, id)` | `GET /v1/chat/sessions/{id}/metadata` | A `SessionMetadata` (`id`, `title`, owner `userId`, `organizationId`, `delegationId`), or `undefined` on a 404. Unlike `SessionSummary`, it has no storage mode, timestamps or parent link, because the route does not return them. It also answers for a session another user shared in your organization, and `userId` shows who owns it. |
327
+ | `renameSession(client, id, title)` | `PATCH /v1/chat/sessions/{id}/title` | `{ id, title }` as the backend stored it. The backend accepts 1–500 characters. |
328
+ | `deleteSession(client, id)` | `DELETE /v1/chat/sessions/{id}` | Nothing. The backend deletes the session and its messages permanently, and its sub-agent sessions with it. There is no confirmation and no undo. `agent.deleteSession` on the agent's current session also clears it, so the next `sendMessage()` without an id starts a new session. |
329
+
330
+ Rename and delete work only on your own sessions. A 404 (no such session, or one already deleted) or a 422 (a rejected title or non-UUID id) rejects with `BackendRequestError`. `getSession` resolves `undefined` on a 404 and rejects on any other error status.
331
+
332
+ ### Forking a session at a message
333
+
334
+ `forkSession(client, id, options)` (`POST /v1/chat/sessions/{id}/branch`) copies one of your sessions into a new session and leaves the source untouched. Fork several times from the same point to try different continuations, for example in evals. The new session is an ordinary one, so you continue it with `sendMessage(prompt, { sessionId: fork.id })`. That call resumes it like any other session: the SDK looks it up and loads its copied history, so `getSessionMessages(fork.id)` returns it.
335
+
336
+ ```ts
337
+ await agent.sendMessage('Draft the vendor email');
338
+ const sourceId = agent.getSessionId();
339
+ const draft = agent.getSessionMessages().find(m => m.type === 'ai'); // fork right after the first answer
340
+ const fork = await agent.forkSession(sourceId, { atMessageId: draft?.id, title: 'Formal tone' });
341
+ await agent.sendMessage('Make it more formal', { sessionId: fork.id }); // the source session is unchanged
342
+ ```
343
+
344
+ | Option | Default | Meaning |
345
+ |---|---|---|
346
+ | `atMessageId` | whole conversation | Backend id of the last message to keep. Messages after it are not copied. |
347
+ | `title` | the source's title | Title of the fork, 1–500 characters. The branch route takes no title, so the SDK renames the fork in a second request. |
348
+ | `signal` | — | Cancels the requests. |
349
+
350
+ It resolves with the fork's `SessionSummary` (see [Listing sessions](#listing-sessions)). `agent.forkSession(id, options?)` does not change the agent's current session.
351
+
352
+ To fork at a message, pass the `id` of a message from `getSessionMessages()`. Those ids are backend message ids. History loaded on resume carries the backend ids. Messages sent or streamed in this process are saved under the same ids the SDK stores for them. You cannot fork at a message the backend keeps out of the conversation, such as a debug or error row.
353
+
354
+ Errors:
355
+
356
+ - A 404 means you own no such session, it has no messages, or `atMessageId` is not one of its visible messages. It rejects with `BackendRequestError`, as does a 422 for a non-UUID id.
357
+ - An empty or too long `title` throws a `RangeError` before any request is sent.
358
+ - If the fork is created but the rename fails, the call rejects with an `Error` that names the fork's id, with the rename error as its `cause`.
359
+ - Only remote sessions can be forked. A Delegate desktop conversation kept in the app's local store has its messages on the client. For such a source the backend creates an empty session and expects the client to copy the messages, which the SDK cannot do. The SDK deletes that empty session and throws.
360
+
361
+ ### Searching a session
362
+
363
+ `searchSession` finds the lines of a session's human (user) messages that contain a query, with a few lines of context around each match. It calls `POST /v1/sessions/{id}/search`:
364
+
365
+ ```ts
366
+ import { createBackendClient, searchSession } from '@uipath/delegate-sdk';
367
+
368
+ const { totalMatches, matches } = await agent.searchSession(id, 'invoice', { contextLines: 1 });
369
+ for (const m of matches) console.log(m.lineNumber, m.content, m.contextBefore, m.contextAfter);
370
+ // or: await searchSession(createBackendClient({ auth, backendUrl }), id, 'invoice');
371
+ ```
372
+
373
+ - The match is case-insensitive and treats the query as literal text, not a pattern. Assistant and tool messages are not searched.
374
+ - `lineNumber` is 1-based and counts lines across all of the session's human messages, taken in order.
375
+ - `contextLines` (0–10, default 2) sets how many lines are returned on each side of a match. The backend does not page results, so every match comes back in one response.
376
+ - An empty query or an out-of-range `contextLines` throws a `RangeError` before any request is sent. A 404 (unknown session), a 422 (non-UUID id) and other error statuses reject with `BackendRequestError`.
377
+ - Local-storage sessions are not stored on the backend, so their search comes back empty.
378
+
379
+ ### Backend projects
380
+
381
+ Backend projects are the folders the Delegate app's sidebar groups sessions into. Each function takes a `BackendClient` plus an optional `{ signal }`, and an initialized `DelegateAgent` has the same methods without the client:
382
+
383
+ ```ts
384
+ import { createBackendClient, listProjects } from '@uipath/delegate-sdk';
385
+
386
+ const project = await agent.createProject({ name: 'Q3 close', icon: 'briefcase' });
387
+ await agent.addSessionToProject(project.id, sessionId); // file an existing session
388
+ for (const p of await listProjects(createBackendClient({ auth, backendUrl }))) console.log(p.id, p.name, p.sessionCount);
389
+
390
+ // File every session a new agent creates in the project:
391
+ await other.initialize({ ...config, backendProjectId: project.id });
392
+ ```
393
+
394
+ | Function | Route | Result |
395
+ |---|---|---|
396
+ | `listProjects(client)` | `GET /v1/projects/` | `BackendProject[]`: `id`, `name`, `description`, `icon`, `sortOrder`, `metadata`, `sessionCount`, `userId`, `organizationId`, `tenantId`, `createdAt`, `updatedAt`. Deleted projects are left out. |
397
+ | `getProject(client, id)` | `GET /v1/projects/` | The project, or `undefined` when you have none with that id. The backend has no by-id route, so this reads the list. |
398
+ | `createProject(client, { name?, description?, icon?, metadata? })` | `POST /v1/projects/` | The new project. Without a name it is "Untitled", without an icon `folder`. Names are at most 200 characters. |
399
+ | `updateProject(client, id, { name?, description?, icon?, sortOrder?, metadata? })` | `PATCH /v1/projects/{id}` | The updated project. Omitted fields are kept and a field cannot be cleared. `metadata` replaces the stored object as a whole. |
400
+ | `deleteProject(client, id)` | `DELETE /v1/projects/{id}` | Nothing. The project is soft-deleted and its sessions stay, outside any project. |
401
+ | `addSessionToProject(client, id, sessionId)` | `POST /v1/projects/{id}/sessions/{sessionId}` | Nothing. A session is in at most one project, so this moves it out of any other. |
402
+ | `removeSessionFromProject(client, id, sessionId)` | `DELETE /v1/projects/{id}/sessions/{sessionId}` | Nothing. The session is kept. |
403
+
404
+ A 404 (no such project, or a session that is not yours or not in that project) or a 422 (a rejected field or a non-UUID id) rejects with `BackendRequestError`.
405
+
406
+ A backend project is **not** `AgentConfig.projectId`. `projectId` names a local folder (`<userDataBase>/projects/<projectId>/`) for the wiki and checklist and is never sent to the backend. `backendProjectId` files the sessions the agent creates in a backend project. Set both when a run should appear under a project in the app and also keep its local project files; the SDK does not derive one from the other.
407
+
408
+ ### Model catalog
409
+
410
+ List the models the backend offers this tenant, and any bring-your-own-model (BYO) configuration gaps. Each function takes a `BackendClient`; an initialized agent has the same methods without it:
411
+
412
+ ```ts
413
+ import { createBackendClient, getByoWarnings, listModels } from '@uipath/delegate-sdk';
414
+
415
+ const client = createBackendClient({ auth, backendUrl });
416
+ const models = await listModels(client); // or agent.listModels()
417
+ const warnings = await getByoWarnings(client); // or agent.getByoWarnings()
418
+ for (const m of models) console.log(m.id, m.locked ? '(locked)' : '', warnings[m.id] ?? '');
419
+ ```
420
+
421
+ | Function (and `DelegateAgent` method) | Route | Returns |
422
+ |---|---|---|
423
+ | `listModels(client)` | `GET /v1/config` (`MODELS`) | `ModelInfo[]` in the backend's order. Only `id` is always present. `name`, `description`, `secondaryModel`, the tier fields, `isByo`, and `locked` / `lockReason` appear when the backend sends them. The legacy `{ id: name }` map is normalized to the same list. A missing catalog gives `[]`. |
424
+ | `getByoWarnings(client)` | `GET /v1/config/byo-warnings` | `Record<modelId, warning>`: models whose primary LLM goes through the tenant's BYO connection but whose supporting model or provider features do not. `{}` when there is nothing to report. |
425
+
426
+ An explicit `model` passed to `initialize()` is checked against `listModels` before interop starts. If the id is not listed, or is `locked` on the caller's plan, `initialize()` rejects with a `ModelNotAvailableError` (`model`, `availableModels`, `lockReason`). Its message lists the selectable ids. If the catalog cannot be fetched, the SDK logs a warning and continues, and an empty catalog passes. The default model is not checked. Set `validateModel: false` to skip the request.
427
+
428
+ ### Cancelling a turn
429
+
430
+ `agent.cancel(sessionId?)` stops the turn the agent is running (default: the current session) the way the desktop app's Stop button does: the backend is told to cancel it (`POST /v1/chat/sessions/{id}/cancel-turn`), the local stream is aborted, running tools are marked interrupted, and detached sub-agents stop. It resolves `true` once that is done, `false` when the agent had no turn in flight on that session.
431
+
432
+ The pending `sendMessage` then emits `done` and **rejects with `TurnCancelledError`** (`message: 'Turn cancelled'`, `sessionId`; test with `isTurnCancelledError`). `runTurnForResult` reports it as `isError: true, error: 'Turn cancelled'`, with the text streamed so far. A `maxSteps` stop is not a cancellation: that `sendMessage` still resolves with the partial answer.
433
+
434
+ ```ts
435
+ import { cancelTurn, createBackendClient, isTurnCancelledError } from '@uipath/delegate-sdk';
436
+
437
+ const turn = agent.sendMessage('reconcile every invoice').catch((error) => {
438
+ if (!isTurnCancelledError(error)) throw error;
439
+ });
440
+ setTimeout(() => void agent.cancel(), 60_000); // give up after a minute
441
+ await turn;
442
+
443
+ // A turn running in another process: its turn_start event (or agent.getActiveTurnId()) names it.
444
+ const cancelled = await cancelTurn(createBackendClient({ auth, backendUrl }), sessionId, turnId);
445
+ ```
446
+
447
+ `cancelTurn(client, sessionId, turnId)` needs the turn id because the backend cancels only that exact turn, so a late cancel can never stop a newer one. Every backend round of a turn (one per tool-result round trip) has its own id. The agent announces each one with a `turn_start` event (`{ type: 'turn_start', sessionId, turnId }`), `TurnResult.turnId` carries the last, and `agent.getActiveTurnId(sessionId?)` returns the current one once the stream has opened (it stays set after the turn ends). Cancel with the latest. `cancelTurn` resolves `true` when the turn was flagged (the backend stops it within a few seconds), `false` when the session has no such turn in progress (404), and rejects with `BackendRequestError` otherwise. The backend request `agent.cancel()` makes is best-effort: before the stream opens there is no turn id, so only the local abort happens, and a failed request is logged rather than thrown.
448
+
449
+ ### Quota and spend
450
+
451
+ Check the caller's limits before a headless run starts, or report usage after it. Each function takes a `BackendClient` (so no agent is needed); an initialized `DelegateAgent` has the same methods without it:
452
+
453
+ ```ts
454
+ import {
455
+ createBackendClient, getActivitySummary, getCartographerAllowance, getDailyBreakdown, getLicensingSummary, getMonthlySpending,
456
+ } from '@uipath/delegate-sdk';
457
+
458
+ const client = createBackendClient({ auth, backendUrl });
459
+ const [licensing, spending, allowance] = await Promise.all([
460
+ getLicensingSummary(client), // or agent.getLicensingSummary()
461
+ getMonthlySpending(client),
462
+ getCartographerAllowance(client),
463
+ ]);
464
+ if (spending.limit_enabled && spending.monthly_limit_usd !== null && spending.monthly_spend_usd > spending.monthly_limit_usd) {
465
+ return; // the backend would refuse the turn (429 monthly_spending_limit_exceeded)
466
+ }
467
+
468
+ const activity = await getActivitySummary(client, { days: 7 }); // totals over the last 7 days
469
+ const daily = await getDailyBreakdown(client, { days: 7 }); // one entry per day with usage
470
+ ```
471
+
472
+ | Function (and `DelegateAgent` method) | Route | Returns |
473
+ |---|---|---|
474
+ | `getLicensingSummary(client, { sessionId? })` | `GET /v1/usage/licensing-summary` | `LicensingSummary`: monthly licensing quota in Platform Units. `limit_enabled: false` means no quota applies. `pct_used` can exceed 100, because usage past the quota spills into an extra pool (`extra_pool_consumed_pu`) and is not refused. `sessionId` adds that session's lifetime usage (`session_consumed_pu`). |
475
+ | `getMonthlySpending(client)` | `GET /v1/usage/monthly-spending` | `MonthlySpending`: spend this month (USD) against the per-user cap. The cap is enforced only while `limit_enabled` is true, and a `null` limit means unlimited. |
476
+ | `getCartographerAllowance(client)` | `GET /v1/usage/cartographer-allowance` | `CartographerAllowance`: `mode` is `not_applicable`, `licensed`, `allowance` or `exhausted`. `exhausted` refuses Cartographer turns only. |
477
+ | `getActivitySummary(client, { days? })` | `GET /v1/usage/activity-summary` | `ActivitySummary`: session, cost and token totals, plus token units per model. |
478
+ | `getDailyBreakdown(client, { days? })` | `GET /v1/usage/daily-breakdown` | `DailyBreakdown`: per-day usage, session counts by weekday and hour, and recent session titles. |
479
+
480
+ - Results are the backend's JSON bodies unchanged, with snake_case fields.
481
+ - `days` is 1 to 365 (default 30). Anything else rejects with a 422 `BackendRequestError`.
482
+ - Every function also takes `signal` and `timeoutMs`. A non-2xx answer rejects with `BackendRequestError`. The activity and daily routes reject with an `Error` when the token resolves to no user.
483
+ - No single field means "every run will be refused". The spending cap has model exemptions, licensing overage is allowed, and the allowance only gates Cartographer. Decide which limits gate your run. `delegate-cli usage --output-format json` prints the same three answers for shell schedulers.
484
+
485
+ ### Evaluate, rerank, verify (typed judgments)
486
+
487
+ Stateless, non-streaming judgments from the backend's `POST /v1/completions/evaluate`, `/rerank` and `/verify`: no session, no tools. Call them on an initialized agent (`agent.evaluate(request)`, `agent.rerank(request)`, `agent.verify(request)`) or standalone over any `BackendClient` (`evaluate(client, request, options)` and so on):
488
+
489
+ ```ts
490
+ import { createBackendClient, evaluate, isEvaluationRequestError, rerank, verify } from '@uipath/delegate-sdk';
491
+
492
+ const client = createBackendClient({ auth, backendUrl });
493
+
494
+ const { answers } = await evaluate(client, {
495
+ state: { invoice: { total: 1200, approver: null } },
496
+ questions: {
497
+ approved: { type: 'probability', instructions: 'Has the invoice been approved?' },
498
+ tier: { type: 'choice', instructions: 'Which approval tier applies?', options: { low: 'under 1000', high: null } },
499
+ risk: { type: 'score', instructions: 'Rate the fraud risk.', levels: ['none', 'some', 'high'] },
500
+ },
501
+ }, undefined);
502
+ // answers.approved → { type: 'probability', probability }, answers.tier → { type: 'choice', choice, probabilities, confidence }
503
+
504
+ const ranked = await rerank(client, { query: 'late payment', candidates: [{ id: 'a', content: '…' }], top_k: 5 }, undefined);
505
+ const checked = await verify(client, { claims: [{ id: 'c1', claim: 'The invoice is paid.', evidence: { status: 'paid' } }] }, undefined);
506
+ // checked.results[0].verdict → 'supported' | 'contradicted' | 'insufficient' | 'mixed'
507
+ ```
508
+
509
+ - Requests and responses use the backend's **snake_case wire names** (`top_k`, `model_version`, `rubric_version`, `usage.total_tokens`) unchanged. Every question needs its `type`. The backend refuses unknown fields and caps a request at 128 KiB and 64 questions, candidates or claims.
510
+ - `model` picks a catalog model (`jev_1_13`, `gemini_3_6_flash`, `gpt_5_6_luna`, `claude_sonnet_5`); omit it for the server default. The backend never substitutes another model.
511
+ - The third argument takes `{ signal, timeoutMs }`; pass `undefined` for the defaults.
512
+ - A non-2xx answer rejects with `EvaluationRequestError`: `operation`, `status`, `detail`, `validationIssues`, the `BackendRequestError` as `cause`, and a `reason` — `invalid_request` (422), `model_unavailable` (403, 503), `rate_limited` (429), `provider_error` (502), `timeout` (504), `unauthorized` (401) or `other`. Narrow with `isEvaluationRequestError(error, 'invalid_request')`.
513
+ - A verdict or probability is a signal, not permission to act.
514
+
515
+ ### Org governance policy
516
+
517
+ The agent runs under your organization's Automation Ops governance policy for Delegate, the same policy the desktop app applies. The SDK has no approval UI, so it auto-approves every tool call. Anything the policy **denies** is still refused:
518
+
519
+ | Policy setting | Enforced by the SDK | How |
520
+ |---|---|---|
521
+ | Disabled tools (`<Tool>-enabled: false`) | Yes | The backend removes them from the tools the model sees. `ExecuteSkillApi` also refuses the skill endpoints of a disabled umbrella tool (for example `/excel/…` when `GenerateExcel` is off). |
522
+ | Tool and skill-operation permissions set to **Block** | Yes | The call is refused before it runs, and the model is told it is unavailable. |
523
+ | Blocked websites and applications | Yes | UI-automation calls on them are refused, and the lists are pushed to the interop. |
524
+ | Path blocklist | Yes, when an interop is engaged | Pushed to the interop, which refuses those paths for file and search calls. Shell commands are not path-checked (the desktop app relies on its sandbox for that). |
525
+ | Pre-approved paths | Yes, when an interop is engaged | Pushed to the interop, which lets file and search calls reach them past its built-in sensitive-path blocks. |
526
+ | Permissions set to **Ask**, a locked security mode | No prompt | There is no UI to confirm in, so these calls stay auto-approved. |
527
+ | File attachments turned off | Yes | `sendMessage` with `attachments` rejects with an `AttachmentError` (`disallowed`) before the turn; see [Attachments](#attachments). |
528
+ | Restricted access (sandbox) settings, task recorder, telemetry, MCP server and model-picker settings | No | These govern desktop-app features the SDK does not have. |
529
+
530
+ The policy is fetched at `initialize()` and refreshed when a new session starts (at most every five minutes). If the backend cannot reach Automation Ops (HTTP 503), the agent keeps the last policy it applied; at `initialize()` there is none yet, so the run starts unrestricted by the org policy. If the deny lists cannot be pushed to the interop, a warning is logged and the run continues.
531
+
532
+ To read the policy:
533
+
534
+ ```ts
535
+ import { createBackendClient, fetchGovernancePolicy } from '@uipath/delegate-sdk';
536
+
537
+ const policy = await agent.getGovernancePolicy(); // fresh GET v1/governance/policy
538
+ // or, without an agent:
539
+ const same = await fetchGovernancePolicy(createBackendClient({ auth, backendUrl }));
540
+
541
+ console.log(policy.disabled_tools, policy.tool_permission_overrides, policy.path_blocklist);
542
+ ```
543
+
544
+ `GovernancePolicy` has the backend's snake_case wire shape. A tenant with no policy gets empty lists and `null` flags, which means no restrictions. HTTP 503 rejects with `BackendRequestError` (`isBackendRequestError(error, 503)`); retry later rather than treating it as no restrictions.
545
+
546
+ ### Structured completion
547
+
548
+ One LLM call that answers a prompt with a JSON object matching your JSON schema (`POST /v1/completions/structured`): no agent turn, no tools. Call it on an initialized agent (`agent.complete(request)`) or standalone over any `BackendClient`:
549
+
550
+ ```ts
551
+ import { complete, createBackendClient, isCompletionRequestError } from '@uipath/delegate-sdk';
552
+
553
+ const client = createBackendClient({ auth, backendUrl });
554
+
555
+ const { structured, usage } = await complete<{ total: number; currency: string }>(client, {
556
+ prompt: 'Extract the total from: "Invoice 42 — total due 1,200.00 EUR"',
557
+ jsonSchema: {
558
+ type: 'object',
559
+ properties: { total: { type: 'number' }, currency: { type: 'string' } },
560
+ required: ['total', 'currency'],
561
+ },
562
+ }, undefined);
563
+ // structured → { total: 1200, currency: 'EUR' }; usage → { model, total_tokens, … } or null
564
+ ```
565
+
566
+ - Request fields: `prompt` and `jsonSchema` (required; the backend names the model's tool after the schema's top-level `title`, so a schema whose `title` is missing or not a tool name — letters, digits, `_` and `-`, at most 64 characters — is sent titled `structured_output`, `DEFAULT_SCHEMA_TITLE`); `sessionId` loads that backend session's history as context before the prompt; `model` (a model with structured-output support; omit for the server default — `agent.complete` uses the agent's `model` instead); `strict` (default `true`); `method` — `function_calling` (default), `json_schema`, or `json_mode` (OpenAI models only, with `strict: false`); `systemPrompt` replaces the default system prompt. `buildStructuredCompletionBody(request)` returns the snake_case wire body.
567
+ - `T` types `structured` for you; the SDK does not check it. The backend validates the output against the schema.
568
+ - The third argument takes `{ signal, timeoutMs }`; the default deadline is `DEFAULT_COMPLETION_TIMEOUT_MS` (120 s).
569
+ - A non-2xx answer rejects with `CompletionRequestError`: `status`, `detail`, `validationIssues`, the `BackendRequestError` as `cause`, and a `reason` — `schema_validation` (422: the model output failed the schema), `context_too_large` (413: the session history does not fit even after summarization — start a new session), `invalid_request` (400, or 422 with field issues), `session_not_found` (404), `model_unavailable` (403), `rate_limited` (429), `unauthorized` (401) or `other`. Narrow with `isCompletionRequestError(error, 'schema_validation')`.
570
+
571
+ ### Structured output from an agent turn
572
+
573
+ Pass an options object with `outputSchema` to `sendMessage` to get the turn's result as a JSON object as well as text. The agent turn runs as usual (tools and all); then the SDK makes a second LLM call to `POST /v1/completions/structured` with the turn's session as context and `strict: true`, and the backend validates the answer against your schema:
574
+
575
+ ```ts
576
+ const invoiceSchema = {
577
+ type: 'object',
578
+ properties: { total: { type: 'number' }, currency: { type: 'string' } },
579
+ required: ['total', 'currency'],
580
+ };
581
+
582
+ const r = await agent.sendMessage<{ total: number; currency: string }>(
583
+ 'Open invoice.pdf in my Downloads and read the total', { outputSchema: invoiceSchema });
584
+ r.text; // the agent's final message
585
+ r.structured; // { total: 1200, currency: 'EUR' } — or undefined when extraction failed
586
+ r.structuredError; // why structured is missing (CompletionRequestError for a backend rejection)
587
+ r.sessionId; // the session the turn ran in; r.turnId when one was announced
588
+ ```
589
+
590
+ - **Extraction options.** `extractionModel` picks the extraction model — it must support structured output; the default is the agent's `model`. `extractionMethod` is `function_calling` (backend default) or `json_schema`. `extractionPrompt` replaces `DEFAULT_EXTRACTION_PROMPT`. The schema's root should be an object: the route returns a JSON object.
591
+ - **A failed extraction does not reject.** The turn already ran, so `sendMessage` resolves with its text, `structured` absent and `structuredError` set: a `CompletionRequestError` with `reason` `schema_validation` (422: no output matched the schema), `context_too_large` (413: the session is too long even after summarization — start a new one), … (see [Structured completion](#structured-completion)), or a plain `Error` when the turn itself ended in an error (`failed: true`) and nothing was extracted. `sendMessage` rejects only where it would without a schema (e.g. `TurnCancelledError`).
592
+ - **Trade-offs.** It costs two LLM calls per turn (more tokens and latency), and long sessions are summarized before extraction. A single-call route, where the backend constrains the turn's own answer, is not available yet.
593
+ - **Without an agent.** `extractStructuredOutput(client, sessionId, { outputSchema }, undefined)` runs the extraction step alone over any finished session, on the server default model unless `extractionModel` names one; `buildStructuredOutputRequest(sessionId, options)` returns the `complete()` request it sends.
594
+ - **Machine-readable output.** `runTurnForResult(agent, prompt, sessionId, onTurnEvent, { outputSchema })` adds `structured` / `structuredError` to the `TurnResult` (see below); the CLI's `--json-schema` on a prompt uses it.
595
+
596
+ ### Attachments
597
+
598
+ Send files with a message through `sendMessage(prompt, { attachments })`. Each entry is a file or folder path (relative paths resolve against the process cwd) or in-memory bytes `{ data: Uint8Array, filename, mimeType? }`:
599
+
600
+ ```ts
601
+ import { DelegateAgent, isAttachmentError } from '@uipath/delegate-sdk';
602
+
603
+ const r = await agent.sendMessage('Do the invoice and the purchase order match?', {
604
+ attachments: ['./invoice.pdf', './po.xlsx', { data: screenshotPng, filename: 'screen.png' }],
605
+ });
606
+ ```
607
+
608
+ The SDK builds the same attachment records the Delegate app builds for a file attached by path and sends them through the app's own send path, so they behave as they do in the app:
609
+
610
+ - **What the model gets.** Images (png, jpeg, webp, gif, bmp, tiff, svg) go inline. Printable text goes inline as `text/plain`, truncated at 16,000 characters with a pointer to the file. PDFs, Office documents, archives and other binaries with a path go as a short reference to the file, which the agent opens with its tools; bytes with no path of their own are first saved under the Delegate app's `sessions/<id>/session-data/attachments` folder, so they get one. A file too large to read is attached unread and goes as a reference to its path. A folder goes as its path.
611
+ - **MIME type.** Detected as the app detects a path attachment: from the extension; no extension is `text/plain` and an unmapped one is `application/octet-stream`, which the send step still sends as `text/plain` when its content is printable. `mimeType` overrides it for bytes.
612
+ - **Limits.** One cap, `MAX_ATTACHMENT_UPLOAD_BYTES` (30 MiB), covers every file of a message together. A file larger than that goes as a reference to the file, unread. When a message's inline attachments add up to more than 24 MiB, the oldest ones with a path are turned into references until it fits. Bytes over 30 MiB are refused, since there is no file to refer to.
613
+ - **Checked before the turn.** A missing path, a failed read, bytes over the cap, a malformed entry, or an org governance policy that bans file attachments rejects `sendMessage` with an `AttachmentError` (`reason`: `not_found` | `unreadable` | `too_large` | `invalid` | `disallowed`; `attachment`: the path or filename) before any session or turn starts. Test for it with `isAttachmentError(error, ...reasons)`.
614
+ - **Across turns.** Attachments stay in context on later turns of the session, as in the app; there is no need to send them again.
615
+
616
+ ### Speech-to-text (transcription)
617
+
618
+ `transcribe` sends audio to the backend's `POST /v1/transcription` (Whisper) — the route the desktop app's voice input uses — and resolves with `{ text }`. Call it on an initialized agent (`agent.transcribe(input)`) or standalone over any `BackendClient`:
619
+
620
+ ```ts
621
+ import { createBackendClient, isTranscriptionError, transcribe } from '@uipath/delegate-sdk';
622
+
623
+ const client = createBackendClient({ auth, backendUrl });
624
+
625
+ const { text } = await transcribe(client, './standup.m4a', undefined); // a file path
626
+ await transcribe(client, { data: buffer, filename: 'note.wav' }, undefined); // Buffer / Uint8Array / ArrayBuffer
627
+ await transcribe(client, { data: blob, mimeType: 'audio/webm;codecs=opus' }, { timeoutMs: 60_000 }); // Blob or File
628
+ ```
629
+
630
+ - The input is a file path (relative to the working directory) or `{ data, filename?, mimeType? }`. The backend decides the format by the upload's **file extension**: mp3, mp4, mpeg, mpga, m4a, wav or webm. For in-memory audio the name comes from `filename`, then a `File`'s own name, then a recognised `mimeType`, then the bytes' signature (WAV, WebM, MP3, MP4/M4A).
631
+ - At most 25 MiB (`MAX_TRANSCRIPTION_AUDIO_BYTES`), non-empty. A path is checked by size before it is read, and a bad format, empty or oversized audio is refused **without uploading**. The route has no language or prompt option; Whisper detects the language.
632
+ - The third argument takes `{ signal, timeoutMs }`; the deadline defaults to `DEFAULT_TRANSCRIPTION_TIMEOUT_MS` (120 s), not the client's 30 s.
633
+ - A refusal rejects with `TranscriptionError`: `status` (`undefined` when refused locally), `detail`, the `BackendRequestError` as `cause`, and a `reason` — `unsupported_format` (400, 415), `empty` (400), `too_large` (413), `invalid_request` (422, other 400s), `unavailable` (Automation Suite, Whisper not configured, 501), `unauthorized` (401) or `other`. Narrow with `isTranscriptionError(error, 'too_large')`. An unreadable path rejects with the `fs` error.
634
+
635
+ ### Screen grounding
636
+
637
+ `groundElement` maps an element description to a point on a screenshot through the backend's `POST /v1/tools/ground-element` — the route Delegate's own computer-use tools use — for callers that run their own computer-use loop (capture, ground, click with their own input layer). Call it on an initialized agent (`agent.groundElement(options)`) or standalone over any `BackendClient`:
638
+
639
+ ```ts
640
+ import { createBackendClient, groundElement } from '@uipath/delegate-sdk';
641
+
642
+ const client = createBackendClient({ auth, backendUrl });
643
+
644
+ const result = await groundElement(client, {
645
+ image: './screen.png', // a path, a Buffer/Uint8Array/ArrayBuffer, or { base64 } (a data: URL is fine)
646
+ description: 'the blue Submit button',
647
+ screenSize: { width: 2560, height: 1440 }, // optional: the captured area's size in your input layer's unit
648
+ }, undefined); // or { signal, timeoutMs }
649
+
650
+ if (result.success) {
651
+ result.coordinates; // { x, y } in image pixels of the screenshot sent
652
+ result.screenPoint; // { x, y } scaled to screenSize (only when screenSize is given and the image is PNG/JPEG)
653
+ } else {
654
+ result.message; // "Element not found: ..." or "Grounding failed: ..."
655
+ }
656
+ ```
657
+
658
+ - **Coordinate spaces.** `coordinates` are pixels of the image you sent, origin top-left; the backend never rescales them. `screenPoint` is `round(x * screenSize.width / imageWidth)` (likewise y), the scaling Delegate applies before it clicks; the SDK reads the image's pixel size from its PNG or JPEG header. It is relative to the captured area's top-left — add the display's (or window's) origin for a virtual-desktop position. Pass `screenSize` in the unit your input API takes: per-monitor-aware physical pixels on Windows, points on macOS. A screenshot at native resolution gives `screenPoint` equal to `coordinates`.
659
+ - **Options.** `actionType` (`'click'` default, `'hover'`, `'scroll'`, `'drag'`), `finder` (`'fast'` backend default, `'standard'`, `'complex'` — a thinking model for hard targets), `autoZoom` (two-pass for small targets; only with `finder: 'standard'` and a `screenSize` above 5,760,000 px), `environment` (`'browser'` or `'desktop'`), `finderExample` (a reference image of the element, same encodings as `image`), and `model` (your chat model's id, a hint for which backup grounder family to prefer).
660
+ - **Not found is not an error.** An element that is not found, or a grounder failure, resolves with `success: false`, `coordinates: null` and a `message`. A malformed request rejects with a `BackendRequestError` (422); an empty `description` or image is refused before any call. The backend bounds grounding at 28 s, inside the client's 30 s default.
661
+
662
+ ### Process knowledge
663
+
664
+ `retrieveProcessKnowledge` asks the backend (`POST /v1/process-knowledge/retrieve`) for summaries of the processes in its server-held corpus that match a free-text description — the same retrieval the Business Analysis skill grounds itself with. The corpus itself is never returned. Call it on an initialized agent or standalone over any `BackendClient`:
665
+
666
+ ```ts
667
+ import { createBackendClient, retrieveProcessKnowledge } from '@uipath/delegate-sdk';
668
+
669
+ const result = await agent.retrieveProcessKnowledge({
670
+ description: 'Retail bank onboarding new customers: KYC document checks, sanctions screening, account opening in Temenos.',
671
+ });
672
+ if (result.matched) {
673
+ for (const match of result.matches ?? []) console.log(match.display_name, match.content);
674
+ }
675
+
676
+ const client = createBackendClient({ auth, backendUrl });
677
+ await retrieveProcessKnowledge(client, { description: 'Invoice approval', model: 'claude_haiku_4_5' }, { timeoutMs: 60_000 });
678
+ ```
679
+
680
+ - The answer is the backend body: `matched`, the corpus `version`, and `matches` (`path`, `display_name`, `content`), most relevant first. `matched: false` means nothing in the corpus fits.
681
+ - `model` picks the ranker model; the agent method defaults it to the agent's `model`, the standalone function leaves it to the backend (`claude_haiku_4_5`).
682
+ - A blank `description` rejects with a `TypeError` without calling the backend. Backend rejections are `BackendRequestError`s (403 model denied by governance, 429 quota or spending limit).
683
+ - The third argument takes `{ signal, timeoutMs }`; the deadline defaults to `DEFAULT_PROCESS_KNOWLEDGE_TIMEOUT_MS` (120 s).
684
+
685
+ ### Web search, image search, image generation and web reading
686
+
687
+ The backend routes behind the agent's `WebSearch`, `ImageSearch`, `ImageGenerate` and `WebReader` tools, called directly — no agent turn, no model deciding to call them. Each is a method on an initialized agent and a standalone function over any `BackendClient`:
688
+
689
+ ```ts
690
+ import { createBackendClient, generateImage, isWebToolError, webSearch } from '@uipath/delegate-sdk';
691
+ import { writeFile } from 'node:fs/promises';
692
+
693
+ const { answer, sources } = await agent.webSearch({ query: 'UiPath Q2 earnings', allowedDomains: ['uipath.com'] });
694
+ const hits = await agent.imageSearch({ query: 'robot arm', page: 1 }); // [{ title, imageUrl, width, height, thumbnailUrl, sourcePage, … }]
695
+ const { images } = await agent.generateImage({ prompt: 'a lighthouse at dawn, watercolor' });
696
+ await writeFile(`lighthouse${images[0].extension}`, images[0].data); // data: bytes; base64 also given
697
+ const markdown = await agent.readWebPage({ url: 'https://example.com/article' });
698
+ const summary = await agent.readWebPage({ url: 'https://example.com/article', prompt: 'List the key dates.' });
699
+ const answer2 = await agent.queryWebContent({ content: pageText, url: 'https://example.com', prompt: 'Who wrote it?' });
700
+
701
+ const client = createBackendClient({ auth, backendUrl }); // without an agent
702
+ await webSearch(client, { query: 'capital of France' }, { timeoutMs: 60_000 });
703
+ ```
704
+
705
+ | Call | Route | Result |
706
+ |---|---|---|
707
+ | `webSearch({ query, allowedDomains?, blockedDomains?, model? })` | `POST /v1/tools/web-search` | `{ answer, sources: [{ url, title, snippet? }], queries, model?, usage? }` |
708
+ | `imageSearch({ query, page? })` | `POST /v1/tools/image-search` | up to 10 public-domain / Creative Commons images per page (1–10) |
709
+ | `generateImage({ prompt })` | `POST /v1/tools/image-generation` | `{ images: [{ mimeType, extension, data, base64 }], text? }` — in memory, nothing written to disk |
710
+ | `readWebPage({ url, prompt?, summarize? })` | `POST /v1/web-reader-tool/{url}` | the page as markdown, or the answer to `prompt` / a summary |
711
+ | `queryWebContent({ content, url, prompt? })` | `POST /v1/web-reader-tool/query` | the answer to `prompt` over `content` (a summary without one) |
712
+
713
+ - **Same rules as the tools.** `webSearch` validates like the tool (query 2–2000 characters; `allowedDomains` or `blockedDomains`, not both, 7 at most, bare domains) and filters the answer the same way: sources outside `allowedDomains`, inside `blockedDomains` or on a site the governance policy blocks are dropped, with their citations in the text. `agent.webSearch` runs the search on the agent's `model`; the standalone function uses the backend's default search model unless `model` is given.
714
+ - **Web reading happens on the backend.** `readWebPage` makes the backend fetch the page, so only pages the backend can reach work (no intranet, no signed-in pages). To read a page you fetched yourself, pass its text to `queryWebContent` (at most `MAX_WEB_CONTENT_QUERY_BYTES`, 1000 KiB). Both refuse a `url` on a site the governance policy blocks, as the WebReader tool does.
715
+ - **Availability.** Image search is not available on Automation Suite; image generation and web search need a suitable model enabled for the tenant; web reading is off in offline mode. These answer `reason: 'unavailable'`.
716
+ - The last argument takes `{ signal, timeoutMs }`; the deadline defaults to `DEFAULT_WEB_TOOL_TIMEOUT_MS` (120 s).
717
+ - A refusal rejects with `WebToolError`: `tool` (`web_search`, `image_search`, `image_generation`, `web_reader`), `status` (`undefined` when refused locally — nothing is sent), `detail`, the `BackendRequestError` as `cause`, and a `reason` — `invalid_request` (local, 400, 422), `unavailable`, `temporarily_unavailable` (503: provider rate-limited or failing), `upstream_failed` (the provider or the fetched site failed: 408, 424, 500, 502), `quota_exceeded` (429), `unauthorized` (401), `forbidden` (403), `blocked_site` (local: a governance-blocked site) or `other`. Narrow with `isWebToolError(error, 'unavailable')`.
718
+
719
+ ### Notifications
720
+
721
+ `listNotifications` reads the caller's notification feed (`GET /v1/notifications`), the same one the desktop app polls for its banner and notification bell. `waitForNotification` polls it until an entry matches, e.g. to block a robot workflow until a delegation's result comes in. Both are methods on an initialized agent and standalone functions over any `BackendClient`:
722
+
723
+ ```ts
724
+ import { createBackendClient, listNotifications, waitForNotification, type DelegationResultNotification } from '@uipath/delegate-sdk';
725
+
726
+ const feed = await agent.listNotifications(); // [{ type: 'announcement' | 'delegation_assignment' | 'delegation_result', id, … }]
727
+
728
+ const result = await agent.waitForNotification(
729
+ (n): n is DelegationResultNotification => n.type === 'delegation_result' && n.id === delegationId,
730
+ { intervalMs: 30_000, timeoutMs: 30 * 60_000, signal },
731
+ );
732
+ console.log(result.result_content?.content);
733
+
734
+ const client = createBackendClient({ auth, backendUrl }); // without an agent
735
+ await listNotifications(client, { timeoutMs: 10_000 });
736
+ ```
737
+
738
+ - **The whole feed, every call.** The route has no filter, paging or "since" parameter. It answers the active announcement for your product (at most one), then delegations assigned to you (when assigned delegation is enabled for you), then delegation results awaiting acceptance, oldest first. Fields are the backend's snake_case, typed as the `DelegateNotification` union (`AnnouncementNotification`, `DelegationAssignmentNotification`, `DelegationResultNotification`).
739
+ - **Nothing to mark read.** The backend has no read or dismiss route. A delegation result stays in the feed until the delegation is accepted (`acceptDelegation`, see [Delegating a task to a person](#delegating-a-task-to-a-person)), so a predicate that matches by `id` keeps matching.
740
+ - **Polling.** `intervalMs` (> 0) and `timeoutMs` (>= 0) are required. The first poll is immediate and a last one runs at the deadline. Then the call rejects with `NotificationWaitTimeoutError` (`timeoutMs`, `polls`). A type-guard predicate narrows the result type.
741
+ - **Errors.** A failed poll rejects the wait with its `BackendRequestError`, with no retry. Aborting `signal` rejects with `signal.reason`, including during an in-flight poll. Out-of-range durations reject with a `RangeError` before any request.
742
+
743
+ ### User settings
744
+
745
+ Your Delegate settings row — the one the Delegate desktop app syncs across devices (`GET` / `PATCH /v2/user/settings`, sent as the Delegate product for the current tenant):
746
+
747
+ ```ts
748
+ import { createBackendClient, updateUserSettings } from '@uipath/delegate-sdk';
749
+
750
+ const { settings, updatedAt } = await agent.getUserSettings(); // updatedAt: null → no stored row, backend defaults
751
+ await agent.updateUserSettings({
752
+ customInstruction: 'Answer in French.',
753
+ behaviorPreferences: { ...settings.behaviorPreferences, tone: 'casual' },
754
+ });
755
+ const servers = [...settings.mcpServers, { type: 'remote', url: 'https://mcp.example.com/sse', enabled: true, headers: {} }];
756
+ await agent.updateUserSettings({ mcpServers: servers }); // arrays replace the stored list: send the whole list
757
+
758
+ await updateUserSettings(createBackendClient({ auth, backendUrl }), { effort: 'high' }); // without an agent
759
+ ```
760
+
761
+ - **Partial updates.** Omitted fields keep their value. Nested objects (`behaviorPreferences`, `toolExecutionPolicy`, `toolSettings`) merge key by key; arrays (`mcpServers`, `favouriteSkills`, the policy's lists) replace the stored array. Both calls resolve with `{ settings, productId, schemaVersion, updatedAt }`, the whole row after the change.
762
+ - **Writable fields** (`WRITABLE_USER_SETTINGS_FIELDS`) are the ones the Delegate app syncs: `selectedModel`, `cartographerModel`, `effort`, `cartographerEffort` (and their `…HasUserOverride` flags), `executionMode`, `useEagerPlanning`, `useDeferredTools`, `showAdvancedSettings`, `behaviorPreferences`, `customProfiles`, `favoriteRoutineIds`, `favouriteSkills`, `alwaysCollapseToolCards`, `scriptCardDefaultView`, `connectionFilterMode`, `toolSettings`, `toolExecutionPolicy`, `customInstruction`, `useFollowUps` and the remote `mcpServers`. A field the app keeps on the device (`soundEffectsEnabled`, `pinnedConversationIds`, `contextGroundingIndexes`, `singleClickPerTurn`, `useDeterministicRoutineTitles`), any other field, or a `null` value rejects with `UserSettingsPatchError` (`issues: [{ field, reason: 'device_local' | 'unknown' | 'null' }]`) before any request. `selectedModel` / `cartographerModel` are trimmed to `{ id, name, model }`.
763
+ - **What changes where.** These are Delegate app settings: the app reads the row when it starts, and a running app may write its own copy of a field back over your change the next time it saves settings. None of them changes an SDK run: pass `model` in `AgentConfig` for the model, and send instructions in the prompt.
764
+ - **Errors** are `BackendRequestError`s: 403 when the token is not a user's (service or robot tokens), 422 for a value the backend's schema rejects (`validationIssues` names the field) or a missing tenant.
765
+
766
+ ### Delegating a task to a person
767
+
768
+ Hand a task from a robot workflow to a colleague in the same organization, and take the answer back — the Delegate app's "delegate to a person" flow over `/v1/chat/delegations/*` and `GET /v1/org-users`. Every call is a method on an initialized agent and a standalone function over any `BackendClient`:
769
+
770
+ ```ts
771
+ import { createBackendClient, claimDelegation, completeDelegation, isDelegationError } from '@uipath/delegate-sdk';
772
+
773
+ // Delegator (the robot): assign a task from one of its remote sessions.
774
+ const created = await agent.createDelegation({
775
+ sessionId, // the conversation the person is briefed on
776
+ recipient: 'ana@acme.com', // an email, or a name that matches one person
777
+ task: 'Approve the Q3 write-off of 1,200 EUR?',
778
+ contextSummary: await agent.summarizeDelegation({ sessionId, task: 'Approve the write-off?' }), // optional
779
+ });
780
+
781
+ // …later: poll the source session for the answer, then take it into the session.
782
+ const { pendingResults } = await agent.getSessionDelegations(sessionId);
783
+ for (const d of pendingResults) {
784
+ console.log(d.assignedUserEmail, d.result?.content);
785
+ await agent.acceptDelegation(d.id); // or dismissDelegation(d.id)
786
+ }
787
+
788
+ // Delegatee (another user's client): claim it, work in the session it opens, answer.
789
+ const client = createBackendClient({ auth: anasAuth, backendUrl });
790
+ const claimed = await claimDelegation(client, created.delegationId); // or a delegate://delegation/… link
791
+ await completeDelegation(client, claimed.delegationId, { content: 'Approved.' });
792
+ ```
793
+
794
+ | Call | Route | Who |
795
+ |---|---|---|
796
+ | `listOrgUsers(query)` / `resolveOrgUser(recipient)` | `GET /v1/org-users?query=` | anyone — search by name or email prefix, 2–100 characters; the caller is left out |
797
+ | `summarizeDelegation({ sessionId, task?, model? })` | `POST …/delegations/summarize` | delegator — drafts a briefing (LLM; the agent method uses the agent's `model`) |
798
+ | `createDelegation({ sessionId \| artifactRef, task?, contextSummary?, recipient? \| recipientId?, sourceDisplayText? })` | `POST /v1/chat/delegations` | delegator — `pending`; assigned to `recipient`, or a shareable `deepLink` without one |
799
+ | `updateDelegation(id, { task?, contextSummary? })`, `cancelDelegation(id)` | `PATCH …/{id}`, `POST …/{id}/cancel` | delegator — until it is claimed (cancel: until it is answered) |
800
+ | `getSessionDelegations(sessionId)` | `GET /v1/chat/sessions/{id}/metadata` | either — `pendingResults` (answers awaiting the delegator) and `delegation` (what a delegatee's session was claimed from) |
801
+ | `listDelegations(projectId)` | `GET /v1/chat/delegations?project_id=` | delegator — every delegation targeting a backend project, any status |
802
+ | `previewDelegation(target)`, `claimDelegation(target)` | `GET …/{id}/assignment-preview`, `POST …/{id}/claim` (or `…/preview`, `…/claim` with a link's token) | delegatee — `target` is an id assigned to you or a `delegate://delegation/…` link; claiming opens (or reopens) the session to work in |
803
+ | `previewDelegationResult(id, model)`, `completeDelegation(id, { content, attachments? })` | `POST …/{id}/preview-result`, `…/{id}/complete` | delegatee — draft the answer from the session (LLM), then send it (idempotent) |
804
+ | `acceptDelegation(id)`, `dismissDelegation(id)`, `deleteDelegation(id)` | `POST …/{id}/accept`, `…/{id}/dismiss`, `DELETE …/{id}` | delegator — accept adds the answer to the source session (idempotent; `messageId`) |
805
+
806
+ - **States.** `pending` → `claimed` → `completed`, then accepted or dismissed by the delegator; `expired` when nobody answers in time, `cancelled` by the delegator. Results come back camelCase (`Delegation`: `status`, `task`, `contextSummary`, `assignedUserEmail`, `result: { content, attachments }`, `resultAccepted`, `deepLink`, timestamps).
807
+ - **Anchors.** A delegation needs a remote session the caller owns (`storageMode: 'local'` sessions are refused, `local_mode_blocked`) or an `artifactRef` naming a backend project (`{ kind: 'project', projectId, title }`).
808
+ - **Waiting for the answer.** There is no push: poll `getSessionDelegations(sessionId)` on the source session, or the notifications feed with `waitForNotification` (`GET /v1/notifications`, which lists both new assignments and answers awaiting acceptance; see [Notifications](#notifications)).
809
+ - **Direct assignment is behind a backend feature flag.** Where it is off for the organization, `listOrgUsers`, assigning with `recipient` and the by-id preview/claim answer `not_found`; a link delegation still works. Org-user search needs an interactive user token (`interactive_user_required` otherwise).
810
+ - **Errors.** Every refusal is a `DelegationError` with a `code`: the backend's (`self_delegation`, `already_claimed`, `already_completed`, `expired`, `cancelled`, `unavailable`, `different_organization`, `not_editable`, `not_pending`, `local_mode_blocked`, `query_too_short`, `identity_*`, …), `recipient_not_found` / `recipient_ambiguous` / `invalid_request` when refused locally (`status` undefined, nothing sent), else `not_found`, `validation` (422), `unauthorized`, `forbidden` or `other`. `detail` and the `BackendRequestError` (`cause`) are kept. Narrow with `isDelegationError(error, 'already_claimed')`.
811
+ - The last argument takes `{ signal, timeoutMs }`; the two LLM calls default to `DEFAULT_DELEGATION_SUMMARY_TIMEOUT_MS` (120 s), the rest to the client's 30 s. Uploading attachments is not wrapped yet; pass metadata of files uploaded otherwise.
812
+
813
+ ### Remote execution (drive a remote machine)
814
+
815
+ Run Delegate tools on another machine — one of your PCs registered with `delegate-cli host`, or a cloud machine the Autopilot Runner allocates — over the Runner's client WebSocket (`/v1/control-plane/client-ws`). Needs Node.js 22 or later (the global `WebSocket`).
816
+
817
+ ```ts
818
+ import { isRemoteExecutionError, listMachines, RemoteExecutionClient, TokenAuthProvider } from '@uipath/delegate-sdk';
819
+
820
+ const machines = await agent.listMachines(); // GET /v1/machines: [{ workerId, name, os, status, lastSeen }]
821
+ const remote = agent.createRemoteExecutionClient({ runnerUrl: 'wss://<runner-host>/autopilot-runner' });
822
+ const session = await remote.openSession({ machineId: machines[0].workerId }); // omit machineId for a cloud machine
823
+ session.onEvent((event) => {
824
+ if (event.type === 'tool_status') console.error(event.message); // also manifest, tool_result, worker_error, runner_error, channel_closed
825
+ });
826
+ try {
827
+ const { content } = await session.executeTool('ExecuteBashCommand', { command: 'hostname' });
828
+ console.log(content);
829
+ } catch (error) {
830
+ if (isRemoteExecutionError(error, 'worker_error', 'timeout')) console.error(error.message);
831
+ else throw error;
832
+ } finally {
833
+ await session.close(); // chat.end: releases the machine
834
+ }
835
+
836
+ // Without an agent:
837
+ const client = new RemoteExecutionClient({ runnerUrl, auth: new TokenAuthProvider(authConfig) });
838
+ ```
839
+
840
+ - **`runnerUrl`** is the Runner base URL, its client-WS URL, or the worker URL a `delegate-cli host` setup uses (`…/v1/control-plane/ws`, swapped for the client path); `ws:`/`wss:` only. Behind the cloud front door (`/delegate_/websocket_/`) the account/tenant routing parameters are filled from the login's slugs, as `delegate-cli host` does.
841
+ - **`openSession({ machineId?, conversationRef?, executionMode?, folderId?, userId?, signal? })`** allocates the session (`chat.create`) and resolves once the Runner acknowledges it. A targeted machine that is offline or not yours rejects with `worker_offline`, a busy one with `worker_busy`; a cloud allocation can reject with `capacity_exceeded`, `no_pool_for_request` or `remote_execution_prerequisite` (folder/machine setup the caller can fix). `executionMode: 'headless'` asks for a session without computer-use tools; `folderId`/`userId` target an Orchestrator folder and robot for a cloud machine.
842
+ - **`executeTool(toolName, args, { executionId?, turnCorrelationId?, timeoutMs?, readyTimeoutMs?, signal? })`** sends one tool call and resolves with `{ executionId, content, responseType, retryable }`; a tool-level failure is a result too. Tool names are the Delegate tool catalog's; `session.manifest` holds the worker's tool list when its manifest reached the session (it is sent at lease start, so it is often absent). Calls on a session run one at a time. While a cloud machine is still booting the call is resent with backoff for up to `readyTimeoutMs` (`DEFAULT_REMOTE_READY_TIMEOUT_MS`, 180 s); the result deadline is `timeoutMs` (`DEFAULT_REMOTE_TOOL_TIMEOUT_MS`, 540 s).
843
+ - **Errors** are `RemoteExecutionError` with a `code`: the Runner's own (`worker_offline`, `worker_busy`, `session_not_ready`, `session_not_found`, `forbidden`, `unauthenticated`, …) or `worker_error`, `transport_closed`, `timeout`, `aborted`, `session_closed`, `protocol_error`. Narrow with `isRemoteExecutionError(error, ...codes)`. A `remote_execution_prerequisite` error also carries `missing` (keys such as `custom_folder_machine`) and `configure` (how to fix each), so branch on those rather than parsing `message`; both are empty for other codes and from runners that predate them.
844
+ - **No background reconnect.** A dropped tool channel fails the call in flight with `transport_closed`; the next `executeTool` opens a new channel for the same session.
845
+
846
+ ### Machine-readable turn output
847
+
848
+ The JSON shapes behind `delegate-cli --output-format json | stream-json`, for programs that want the same output:
849
+
850
+ ```ts
851
+ import { agentEventToJsonLine, runTurnForResult } from '@uipath/delegate-sdk';
852
+
853
+ // Every event as one NDJSON line while the turn runs, then one result object.
854
+ const result = await runTurnForResult(agent, 'Summarise my inbox', undefined,
855
+ (event) => process.stdout.write(agentEventToJsonLine(event)));
856
+ process.stdout.write(`${JSON.stringify(result)}\n`);
857
+ if (result.isError) process.exitCode = 1;
858
+ ```
859
+
860
+ `runTurnForResult(agent, prompt, sessionId, onTurnEvent, structuredOutput?)` runs one `sendMessage` and resolves to a `TurnResult` — it does not throw for a failed turn. With `structuredOutput` (`{ outputSchema, extractionModel?, extractionMethod?, extractionPrompt? }`) a successful turn is followed by the extraction described in [Structured output from an agent turn](#structured-output-from-an-agent-turn), through `agent.complete`:
861
+
862
+ | Field | Meaning |
863
+ |---|---|
864
+ | `type` | Always `'result'`. |
865
+ | `sessionId` | Session the turn ran in (`null` if none was created). |
866
+ | `text` | Final answer, `sendMessage`'s `text` (the error text for a failed turn). |
867
+ | `turnId` | Backend id of the turn's last round (its last `turn_start` event); absent when none was announced. |
868
+ | `isError` / `error` | `true` when `sendMessage` threw or resolved with `failed: true`; `error` then says why. |
869
+ | `usage` | `TurnResultUsage` — `promptTokens`, `completionTokens`, `promptTokensCached`, `cacheCreationTokens`, `turnTokenUnits` summed over **every backend round** the turn made, plus `rounds`. `null` when none was reported. `getLastTurnUsage()` is only the final round, so it under-counts turns that ran tools. |
870
+ | `toolCalls` | Distinct tool calls (by `toolId`). |
871
+ | `durationMs` | Wall-clock duration of the turn. |
872
+ | `structured` | With `structuredOutput`: the extracted object. Absent when the extraction failed, or was skipped because the turn failed. |
873
+ | `structuredError` | With `structuredOutput`: `{ message, reason?, status? }` when the extraction failed (`reason` / `status` of the `CompletionRequestError`). `isError` stays about the turn. |
874
+
875
+ Lower-level pieces, for hosts that call `sendMessage` themselves: `TurnResultCollector` (subscribe its `onEvent` before sending, then `finish({ endedAtMs, text, failed, usage, error })`; without `failed`, a turn whose last outcome was an `error` event counts as failed), `sumTurnUsage(entries)`, `toJsonLine(value)` (NDJSON line that never throws on cycles or BigInt), and `OUTPUT_FORMATS` / `isOutputFormat` (`'text' | 'json' | 'stream-json'`).
233
876
 
234
877
  ---
235
878
 
@@ -241,13 +884,13 @@ Cartographer uses the existing skills API; there is no separate project-template
241
884
  await agent.initialize({
242
885
  environment: 'alpha',
243
886
  auth,
244
- preloadSkills: ['business-analysis', 'project-checklist', 'knowledge-base'],
887
+ preloadSkills: ['business-analysis', 'project-task-list', 'knowledge-base'],
245
888
  projectId: 'invoice-approval',
246
889
  workingDirectory: '/srv/engagements',
247
890
  });
248
891
  ```
249
892
 
250
- Before the first message, the SDK copies the bundled Business Analysis `checklist.md` and `progress.yaml` assets into `projects/invoice-approval/` if absent. Each turn receives `ProjectId`, `ChecklistPath`, and `ProgressPath`; `{WikiPath}` in preloaded skill content resolves to the same project's `wiki/` directory.
893
+ Before the first message, the SDK copies the bundled Business Analysis `task-list.md` and `progress.yaml` assets into `projects/invoice-approval/` if absent. Each turn receives `ProjectId`, `TaskListPath`, and `ProgressPath`; `{WikiPath}` in preloaded skill content resolves to the same project's `wiki/` directory.
251
894
 
252
895
  ---
253
896
 
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/index.mjs
3
- size: 60932354 bytes
4
- sha256: b3666f1717d7077aebf167f7d608d0fe570dc6c364fa9f9454bae43e528e466e
3
+ size: 61526299 bytes
4
+ sha256: fc2ac024494d7316a48a56b37fbf43f7c55ce44a3dbf581032774d6ec8d0ec64
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/delegate-sdk",
3
- "version": "1.203.0-preview.20260929135425",
3
+ "version": "1.203.0",
4
4
  "description": "UiPath Delegate SDK - Programmatic agent interface",
5
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",
@@ -50,9 +50,9 @@
50
50
  "zod-to-json-schema": "^3.25.1"
51
51
  },
52
52
  "optionalDependencies": {
53
- "@uipath/delegate-runtime-darwin-arm64": "1.203.0-preview.20260929135425",
54
- "@uipath/delegate-runtime-win32-x64": "1.203.0-preview.20260929135425",
55
- "@uipath/delegate-runtime-linux-x64": "1.203.0-preview.20260929135425"
53
+ "@uipath/delegate-runtime-darwin-arm64": "1.203.0",
54
+ "@uipath/delegate-runtime-win32-x64": "1.203.0",
55
+ "@uipath/delegate-runtime-linux-x64": "1.203.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/node": "^22.0.0",