@useago/sdk 1.0.0 → 1.0.2

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.
@@ -16,12 +16,26 @@ declare class AgoClient {
16
16
  private contextRegistry;
17
17
  private eventEmitter;
18
18
  private config;
19
+ /** Conversations already warned about an empty reply (one warning each). */
20
+ private warnedEmptyConversations;
19
21
  constructor(config: AgoConfig);
20
22
  /**
21
23
  * Send a message and receive a streaming response
22
24
  */
23
25
  sendMessage(content: string, options?: SendMessageOptions): Promise<AgoMessage>;
24
26
  private processSSEResponse;
27
+ /**
28
+ * Detect a reply that completed as DONE with no content and no activity —
29
+ * today's backend answers an unknown `agent` slug with an empty 200, which
30
+ * otherwise fails silently. Emits `message:empty` (always) and warns on the
31
+ * console once per conversation (unless `warnOnEmptyReply: false`).
32
+ *
33
+ * Deferred with setTimeout(0) so both land AFTER `sendMessage` resolves:
34
+ * `await client.sendMessage(...)` followed by `client.waitFor("message:empty")`
35
+ * must not race. TEMPORARY heuristic: remove once the backend returns an
36
+ * explicit error for unknown agents (see TODOS.md, backend issue).
37
+ */
38
+ private maybeFlagEmptyReply;
25
39
  private handleClientFunctionInvocation;
26
40
  /**
27
41
  * Get list of conversations
@@ -53,6 +67,13 @@ declare class AgoClient {
53
67
  * Reject a tool call
54
68
  */
55
69
  rejectToolCall(toolCallId: string): Promise<void>;
70
+ /**
71
+ * Fetch a backend-stored form collector definition by name (the single source
72
+ * of truth for the form's description, schema, and submit target). Used by
73
+ * `loadFormCollector` / the name-only form of `useFormCollector` so the schema
74
+ * lives in the backend instead of being hardcoded in client code.
75
+ */
76
+ getFormCollector(name: string): Promise<FormCollectorDefinition>;
56
77
  /**
57
78
  * Relay a completed form collector's values to a server-configured destination.
58
79
  *
@@ -187,6 +208,11 @@ declare class AgoClient {
187
208
  }): Promise<AgoClientEvents[K]>;
188
209
  /**
189
210
  * Update client configuration
211
+ *
212
+ * Validates the MERGED config before committing: omitting a key (or passing
213
+ * it as `undefined`, common with spread-built partials) keeps the current
214
+ * value; an explicit empty/non-string `baseUrl` throws `config_missing_base_url`
215
+ * and leaves the client unchanged (no half-updated state).
190
216
  */
191
217
  updateConfig(config: Partial<AgoConfig>): void;
192
218
  /**
@@ -222,8 +248,23 @@ declare interface AgoClientEvents {
222
248
  */
223
249
  "message:answer-complete": AgoMessage;
224
250
  "message:complete": AgoMessage;
251
+ /**
252
+ * A reply finished as `DONE` with empty content and no tool calls, client
253
+ * functions, or follow-up replies — usually an unknown `agent` slug (the
254
+ * backend currently answers those with an empty 200). Fires after
255
+ * `message:complete` AND after `sendMessage` resolves, so subscribing right
256
+ * after `await sendMessage(...)` still catches it. `messageId` and
257
+ * `conversationId` are empty strings when the stream completed without any
258
+ * message data at all (e.g. a proxy stripped the SSE stream).
259
+ */
260
+ "message:empty": {
261
+ conversationId: string;
262
+ messageId: string;
263
+ };
225
264
  "message:error": {
226
265
  error: string;
266
+ /** Stable error code (see configuration.md#error-codes) when the failure was an AgoError. */
267
+ code?: string;
227
268
  conversationId?: string;
228
269
  messageId?: string;
229
270
  };
@@ -252,7 +293,7 @@ declare interface AgoClientEvents {
252
293
  declare interface AgoConfig {
253
294
  /** API base URL (e.g., https://YOUR-DOMAIN.useago.com) */
254
295
  baseUrl: string;
255
- /** Widget ID (X-Widget-Id header). Auto-generated if not provided. */
296
+ /** Per-visitor anonymous ID (X-User-Anon-Id header). Auto-generated if not provided. */
256
297
  widgetId?: string;
257
298
  /** Default agent (id or slug) for new conversations. Shorthand for `defaultAgentId`. */
258
299
  agent?: string;
@@ -266,6 +307,13 @@ declare interface AgoConfig {
266
307
  userJwt?: string;
267
308
  /** Enable debug logging */
268
309
  debug?: boolean;
310
+ /**
311
+ * Warn on the console when a reply completes with empty content and no tool
312
+ * calls (the usual cause is an unknown `agent` slug, which the backend
313
+ * currently answers with an empty 200). Default `true`. The `message:empty`
314
+ * event fires regardless of this setting.
315
+ */
316
+ warnOnEmptyReply?: boolean;
269
317
  }
270
318
 
271
319
  declare type AgoEventHandler<K extends AgoEventName> = (data: AgoClientEvents[K]) => void;
@@ -400,6 +448,30 @@ export declare function createMockClient(options?: MockAgoClientOptions): MockAg
400
448
  */
401
449
  declare type DynamicContextProvider = () => ContextEntry | null | undefined;
402
450
 
451
+ /**
452
+ * The backend-stored shape returned by `GET /api/sdk/v1/forms/{name}` (see
453
+ * {@link AgoClient.getFormCollector}). Mirrors the persistable subset of
454
+ * {@link CreateFormCollectorOptions}: the `submit` here is never the client-only
455
+ * `handler` variant (a function can't be stored server-side).
456
+ */
457
+ declare interface FormCollectorDefinition<V = Record<string, unknown>> {
458
+ name: string;
459
+ description: string;
460
+ schema: FormCollectorSchema;
461
+ /** Server-stored submit target, if any. Absent means collect-only. */
462
+ submit?: SubmitConfig<V>;
463
+ }
464
+
465
+ /**
466
+ * Shape of a form's fields. Mirrors a client function's `parameters` (so the same
467
+ * object feeds the store, the update function, and the dynamic context). Additionally allows the SDK-only per-field `requiredWhen`, which {@link toWireParameters} removes before the schema reaches the LLM.
468
+ */
469
+ declare interface FormCollectorSchema {
470
+ type: "object";
471
+ properties: Record<string, FormFieldSchema>;
472
+ required?: string[];
473
+ }
474
+
403
475
  declare interface FormField {
404
476
  type: "string" | "number" | "boolean" | "array";
405
477
  title?: string;
@@ -408,6 +480,43 @@ declare interface FormField {
408
480
  default?: unknown;
409
481
  }
410
482
 
483
+ /**
484
+ * A field's conditional-requiredness rule. The field becomes required exactly
485
+ * when the controlling `property` matches `value` against the answers collected
486
+ * so far:
487
+ * - a single `value`, no `op` — equality (compared as strings);
488
+ * - a single `value` with `op` — numeric comparison (both sides coerced to numbers);
489
+ * - an array `value`, no `op` — "one of": matches when the property equals any
490
+ * listed value.
491
+ *
492
+ * SDK-only — it is stripped from the schema before it reaches the LLM tool
493
+ */
494
+ declare interface FormFieldCondition {
495
+ /** The controlling field whose value the rule tests. */
496
+ property: string;
497
+ /** Comparison operator; omitted means equality. Ignored when `value` is an array. */
498
+ op?: ">=" | "<=" | ">" | "<";
499
+ /**
500
+ * The value(s) to test the controlling field against. A single value is an
501
+ * equality (or, with `op`, a numeric comparison); an array is an "is one of"
502
+ * membership test, e.g. `value: ["1", "2", "3"]`.
503
+ */
504
+ value: string | number | Array<string | number>;
505
+ }
506
+
507
+ /** A single field's schema: the wire-legal keys plus the SDK-only `requiredWhen`. */
508
+ declare interface FormFieldSchema {
509
+ type: string;
510
+ description?: string;
511
+ enum?: string[];
512
+ default?: unknown;
513
+ /**
514
+ * Makes the field required only while this condition holds. Drives the dynamic
515
+ * `missing` list; stripped before the LLM tool — see {@link toWireParameters}.
516
+ */
517
+ requiredWhen?: FormFieldCondition;
518
+ }
519
+
411
520
  /**
412
521
  * Form schema for tool calls requiring user input
413
522
  */
@@ -461,6 +570,27 @@ declare interface SendMessageOptions {
461
570
  files?: File[];
462
571
  }
463
572
 
573
+ /**
574
+ * Where a completed form is sent.
575
+ * - `"https://..."` (a bare string) — shorthand for `{ via: "client", url }`: the browser
576
+ * POSTs the values to that URL.
577
+ * - `{ via: "client", url }` — same, written out.
578
+ * - `{ via: "client", handler }` — the browser runs your own submit logic.
579
+ * - `{ via: "backend", destination }` — the browser relays the values to the backend,
580
+ * which forwards them to the server-configured destination (URL + secret stay server-side).
581
+ * - `false` — collect only, no submit function.
582
+ */
583
+ declare type SubmitConfig<V = Record<string, unknown>> = string | {
584
+ via: "client";
585
+ url: string;
586
+ } | {
587
+ via: "client";
588
+ handler: (values: V) => Promise<unknown> | unknown;
589
+ } | {
590
+ via: "backend";
591
+ destination: string;
592
+ } | false;
593
+
464
594
  /**
465
595
  * Tool call data from SSE stream
466
596
  */
package/dist/testing.d.ts CHANGED
@@ -16,12 +16,26 @@ declare class AgoClient {
16
16
  private contextRegistry;
17
17
  private eventEmitter;
18
18
  private config;
19
+ /** Conversations already warned about an empty reply (one warning each). */
20
+ private warnedEmptyConversations;
19
21
  constructor(config: AgoConfig);
20
22
  /**
21
23
  * Send a message and receive a streaming response
22
24
  */
23
25
  sendMessage(content: string, options?: SendMessageOptions): Promise<AgoMessage>;
24
26
  private processSSEResponse;
27
+ /**
28
+ * Detect a reply that completed as DONE with no content and no activity —
29
+ * today's backend answers an unknown `agent` slug with an empty 200, which
30
+ * otherwise fails silently. Emits `message:empty` (always) and warns on the
31
+ * console once per conversation (unless `warnOnEmptyReply: false`).
32
+ *
33
+ * Deferred with setTimeout(0) so both land AFTER `sendMessage` resolves:
34
+ * `await client.sendMessage(...)` followed by `client.waitFor("message:empty")`
35
+ * must not race. TEMPORARY heuristic: remove once the backend returns an
36
+ * explicit error for unknown agents (see TODOS.md, backend issue).
37
+ */
38
+ private maybeFlagEmptyReply;
25
39
  private handleClientFunctionInvocation;
26
40
  /**
27
41
  * Get list of conversations
@@ -53,6 +67,13 @@ declare class AgoClient {
53
67
  * Reject a tool call
54
68
  */
55
69
  rejectToolCall(toolCallId: string): Promise<void>;
70
+ /**
71
+ * Fetch a backend-stored form collector definition by name (the single source
72
+ * of truth for the form's description, schema, and submit target). Used by
73
+ * `loadFormCollector` / the name-only form of `useFormCollector` so the schema
74
+ * lives in the backend instead of being hardcoded in client code.
75
+ */
76
+ getFormCollector(name: string): Promise<FormCollectorDefinition>;
56
77
  /**
57
78
  * Relay a completed form collector's values to a server-configured destination.
58
79
  *
@@ -187,6 +208,11 @@ declare class AgoClient {
187
208
  }): Promise<AgoClientEvents[K]>;
188
209
  /**
189
210
  * Update client configuration
211
+ *
212
+ * Validates the MERGED config before committing: omitting a key (or passing
213
+ * it as `undefined`, common with spread-built partials) keeps the current
214
+ * value; an explicit empty/non-string `baseUrl` throws `config_missing_base_url`
215
+ * and leaves the client unchanged (no half-updated state).
190
216
  */
191
217
  updateConfig(config: Partial<AgoConfig>): void;
192
218
  /**
@@ -222,8 +248,23 @@ declare interface AgoClientEvents {
222
248
  */
223
249
  "message:answer-complete": AgoMessage;
224
250
  "message:complete": AgoMessage;
251
+ /**
252
+ * A reply finished as `DONE` with empty content and no tool calls, client
253
+ * functions, or follow-up replies — usually an unknown `agent` slug (the
254
+ * backend currently answers those with an empty 200). Fires after
255
+ * `message:complete` AND after `sendMessage` resolves, so subscribing right
256
+ * after `await sendMessage(...)` still catches it. `messageId` and
257
+ * `conversationId` are empty strings when the stream completed without any
258
+ * message data at all (e.g. a proxy stripped the SSE stream).
259
+ */
260
+ "message:empty": {
261
+ conversationId: string;
262
+ messageId: string;
263
+ };
225
264
  "message:error": {
226
265
  error: string;
266
+ /** Stable error code (see configuration.md#error-codes) when the failure was an AgoError. */
267
+ code?: string;
227
268
  conversationId?: string;
228
269
  messageId?: string;
229
270
  };
@@ -252,7 +293,7 @@ declare interface AgoClientEvents {
252
293
  declare interface AgoConfig {
253
294
  /** API base URL (e.g., https://YOUR-DOMAIN.useago.com) */
254
295
  baseUrl: string;
255
- /** Widget ID (X-Widget-Id header). Auto-generated if not provided. */
296
+ /** Per-visitor anonymous ID (X-User-Anon-Id header). Auto-generated if not provided. */
256
297
  widgetId?: string;
257
298
  /** Default agent (id or slug) for new conversations. Shorthand for `defaultAgentId`. */
258
299
  agent?: string;
@@ -266,6 +307,13 @@ declare interface AgoConfig {
266
307
  userJwt?: string;
267
308
  /** Enable debug logging */
268
309
  debug?: boolean;
310
+ /**
311
+ * Warn on the console when a reply completes with empty content and no tool
312
+ * calls (the usual cause is an unknown `agent` slug, which the backend
313
+ * currently answers with an empty 200). Default `true`. The `message:empty`
314
+ * event fires regardless of this setting.
315
+ */
316
+ warnOnEmptyReply?: boolean;
269
317
  }
270
318
 
271
319
  declare type AgoEventHandler<K extends AgoEventName> = (data: AgoClientEvents[K]) => void;
@@ -400,6 +448,30 @@ export declare function createMockClient(options?: MockAgoClientOptions): MockAg
400
448
  */
401
449
  declare type DynamicContextProvider = () => ContextEntry | null | undefined;
402
450
 
451
+ /**
452
+ * The backend-stored shape returned by `GET /api/sdk/v1/forms/{name}` (see
453
+ * {@link AgoClient.getFormCollector}). Mirrors the persistable subset of
454
+ * {@link CreateFormCollectorOptions}: the `submit` here is never the client-only
455
+ * `handler` variant (a function can't be stored server-side).
456
+ */
457
+ declare interface FormCollectorDefinition<V = Record<string, unknown>> {
458
+ name: string;
459
+ description: string;
460
+ schema: FormCollectorSchema;
461
+ /** Server-stored submit target, if any. Absent means collect-only. */
462
+ submit?: SubmitConfig<V>;
463
+ }
464
+
465
+ /**
466
+ * Shape of a form's fields. Mirrors a client function's `parameters` (so the same
467
+ * object feeds the store, the update function, and the dynamic context). Additionally allows the SDK-only per-field `requiredWhen`, which {@link toWireParameters} removes before the schema reaches the LLM.
468
+ */
469
+ declare interface FormCollectorSchema {
470
+ type: "object";
471
+ properties: Record<string, FormFieldSchema>;
472
+ required?: string[];
473
+ }
474
+
403
475
  declare interface FormField {
404
476
  type: "string" | "number" | "boolean" | "array";
405
477
  title?: string;
@@ -408,6 +480,43 @@ declare interface FormField {
408
480
  default?: unknown;
409
481
  }
410
482
 
483
+ /**
484
+ * A field's conditional-requiredness rule. The field becomes required exactly
485
+ * when the controlling `property` matches `value` against the answers collected
486
+ * so far:
487
+ * - a single `value`, no `op` — equality (compared as strings);
488
+ * - a single `value` with `op` — numeric comparison (both sides coerced to numbers);
489
+ * - an array `value`, no `op` — "one of": matches when the property equals any
490
+ * listed value.
491
+ *
492
+ * SDK-only — it is stripped from the schema before it reaches the LLM tool
493
+ */
494
+ declare interface FormFieldCondition {
495
+ /** The controlling field whose value the rule tests. */
496
+ property: string;
497
+ /** Comparison operator; omitted means equality. Ignored when `value` is an array. */
498
+ op?: ">=" | "<=" | ">" | "<";
499
+ /**
500
+ * The value(s) to test the controlling field against. A single value is an
501
+ * equality (or, with `op`, a numeric comparison); an array is an "is one of"
502
+ * membership test, e.g. `value: ["1", "2", "3"]`.
503
+ */
504
+ value: string | number | Array<string | number>;
505
+ }
506
+
507
+ /** A single field's schema: the wire-legal keys plus the SDK-only `requiredWhen`. */
508
+ declare interface FormFieldSchema {
509
+ type: string;
510
+ description?: string;
511
+ enum?: string[];
512
+ default?: unknown;
513
+ /**
514
+ * Makes the field required only while this condition holds. Drives the dynamic
515
+ * `missing` list; stripped before the LLM tool — see {@link toWireParameters}.
516
+ */
517
+ requiredWhen?: FormFieldCondition;
518
+ }
519
+
411
520
  /**
412
521
  * Form schema for tool calls requiring user input
413
522
  */
@@ -461,6 +570,27 @@ declare interface SendMessageOptions {
461
570
  files?: File[];
462
571
  }
463
572
 
573
+ /**
574
+ * Where a completed form is sent.
575
+ * - `"https://..."` (a bare string) — shorthand for `{ via: "client", url }`: the browser
576
+ * POSTs the values to that URL.
577
+ * - `{ via: "client", url }` — same, written out.
578
+ * - `{ via: "client", handler }` — the browser runs your own submit logic.
579
+ * - `{ via: "backend", destination }` — the browser relays the values to the backend,
580
+ * which forwards them to the server-configured destination (URL + secret stay server-side).
581
+ * - `false` — collect only, no submit function.
582
+ */
583
+ declare type SubmitConfig<V = Record<string, unknown>> = string | {
584
+ via: "client";
585
+ url: string;
586
+ } | {
587
+ via: "client";
588
+ handler: (values: V) => Promise<unknown> | unknown;
589
+ } | {
590
+ via: "backend";
591
+ destination: string;
592
+ } | false;
593
+
464
594
  /**
465
595
  * Tool call data from SSE stream
466
596
  */
package/dist/vue.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const P=require("./rolldown-runtime-CaekUdDJ.cjs"),p=require("./AgoClient-CPqPT5s6.cjs");let t=require("vue");var I=Symbol("ago-client"),y={install(o,n){const e=new p.AgoClient(n);o.provide(I,e)}};function g(o){if(o)return new p.AgoClient(o);const n=(0,t.inject)(I);if(!n)throw new Error("useAgo(): no AgoClient found. Either pass config or install AgoPlugin.");return n}function E(o={}){const n=o.client??g(),e=(0,t.ref)([]),s=(0,t.ref)(o.conversationId),i=(0,t.ref)(!1),r=(0,t.ref)(null),c=a=>{s.value||(s.value=a.conversationId)},f=a=>{const u=e.value.findIndex(v=>v.id===a.messageId);u>=0&&(e.value[u]={...e.value[u],content:e.value[u].content+a.content})},m=a=>{const u=e.value.findIndex(v=>v.id===a.id);u>=0?e.value[u]=a:e.value.push(a),i.value=!1},C=a=>{r.value=new Error(a.error),i.value=!1};(0,t.onMounted)(()=>{n.on("message:start",c),n.on("message:chunk",f),n.on("message:complete",m),n.on("message:error",C)}),(0,t.onUnmounted)(()=>{n.off("message:start",c),n.off("message:chunk",f),n.off("message:complete",m),n.off("message:error",C)});async function d(a,u){i.value=!0,r.value=null;const v={id:`temp-${Date.now()}`,conversationId:s.value||"",content:a,role:"user",status:"DONE",createdAt:new Date},L={id:`temp-assistant-${Date.now()}`,conversationId:s.value||"",content:"",role:"assistant",status:"IN_PROGRESS",createdAt:new Date};e.value.push(v,L);try{const l=await n.sendMessage(a,{conversationId:s.value,files:u});l.conversationId&&!s.value&&(s.value=l.conversationId),e.value=e.value.filter(w=>!w.id.startsWith("temp-"));const A={...v,id:v.id.replace("temp-","user-"),conversationId:l.conversationId};return e.value.some(w=>w.id===l.id)?e.value.push(A):e.value.push(A,l),l}catch(l){return r.value=l instanceof Error?l:new Error("Failed to send"),i.value=!1,e.value=e.value.filter(A=>!A.id.startsWith("temp-")),null}}function h(){e.value=[],s.value=void 0,r.value=null}return{messages:e,isLoading:i,error:r,conversationId:s,sendMessage:d,clearMessages:h}}function M(o={}){const n=o.client??g(),e=o.autoLoad??!0,s=(0,t.ref)([]),i=(0,t.ref)(null),r=(0,t.ref)(!1),c=(0,t.ref)(null);async function f(){r.value=!0,c.value=null;try{s.value=await n.getConversations()}catch(d){c.value=d instanceof Error?d:new Error("Failed to load conversations")}finally{r.value=!1}}async function m(d){r.value=!0,c.value=null;try{i.value=await n.getConversation(d)}catch(h){c.value=h instanceof Error?h:new Error("Failed to load conversation")}finally{r.value=!1}}function C(){i.value=null}return(0,t.onMounted)(()=>{e&&f()}),{conversations:s,currentConversation:i,isLoading:r,error:c,selectConversation:m,startNewConversation:C,refreshConversations:f}}function N(o={}){const n=E({client:o.client,conversationId:o.conversationId}),e=M({client:o.client,autoLoad:o.autoLoad}),s=(0,t.computed)(()=>n.error.value||e.error.value);return{messages:n.messages,isLoading:n.isLoading,error:s,sendMessage:n.sendMessage,clearMessages:n.clearMessages,conversationId:n.conversationId,conversations:e.conversations,currentConversation:e.currentConversation,isConversationsLoading:e.isLoading,selectConversation:e.selectConversation,startNewConversation:e.startNewConversation,refreshConversations:e.refreshConversations}}function F(o,n){const e=g();(0,t.onMounted)(()=>{e.registerFunction(o,n.handler,{description:n.description,parameters:n.parameters})}),(0,t.onUnmounted)(()=>{e.unregisterFunction(o)})}function S(o,n){const e=g();(0,t.onMounted)(()=>{e.registerNavigationFunction(o,n)}),(0,t.onUnmounted)(()=>{e.unregisterFunction("navigateToPage")})}function _(o,n){const e=g();(0,t.onMounted)(()=>{e.on(o,n)}),(0,t.onUnmounted)(()=>{e.off(o,n)})}function D(o){const n=(0,t.shallowRef)(o.get());return(0,t.onScopeDispose)(o.subscribe(e=>{n.value=e})),n}exports.AGO_CLIENT_KEY=I;exports.AgoPlugin=y;exports.useAgo=g;exports.useAgoEvents=_;exports.useAgoFunction=F;exports.useAgoNavigation=S;exports.useAgoStore=D;exports.useChat=N;exports.useConversation=M;exports.useMessages=E;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const P=require("./rolldown-runtime-CaekUdDJ.cjs"),p=require("./AgoClient-_fig8FoN.cjs");let t=require("vue");var I=Symbol("ago-client"),y={install(o,n){const e=new p.AgoClient(n);o.provide(I,e)}};function g(o){if(o)return new p.AgoClient(o);const n=(0,t.inject)(I);if(!n)throw new Error("useAgo(): no AgoClient found. Either pass config or install AgoPlugin.");return n}function E(o={}){const n=o.client??g(),e=(0,t.ref)([]),s=(0,t.ref)(o.conversationId),i=(0,t.ref)(!1),r=(0,t.ref)(null),c=a=>{s.value||(s.value=a.conversationId)},f=a=>{const u=e.value.findIndex(v=>v.id===a.messageId);u>=0&&(e.value[u]={...e.value[u],content:e.value[u].content+a.content})},m=a=>{const u=e.value.findIndex(v=>v.id===a.id);u>=0?e.value[u]=a:e.value.push(a),i.value=!1},C=a=>{r.value=new Error(a.error),i.value=!1};(0,t.onMounted)(()=>{n.on("message:start",c),n.on("message:chunk",f),n.on("message:complete",m),n.on("message:error",C)}),(0,t.onUnmounted)(()=>{n.off("message:start",c),n.off("message:chunk",f),n.off("message:complete",m),n.off("message:error",C)});async function d(a,u){i.value=!0,r.value=null;const v={id:`temp-${Date.now()}`,conversationId:s.value||"",content:a,role:"user",status:"DONE",createdAt:new Date},L={id:`temp-assistant-${Date.now()}`,conversationId:s.value||"",content:"",role:"assistant",status:"IN_PROGRESS",createdAt:new Date};e.value.push(v,L);try{const l=await n.sendMessage(a,{conversationId:s.value,files:u});l.conversationId&&!s.value&&(s.value=l.conversationId),e.value=e.value.filter(w=>!w.id.startsWith("temp-"));const A={...v,id:v.id.replace("temp-","user-"),conversationId:l.conversationId};return e.value.some(w=>w.id===l.id)?e.value.push(A):e.value.push(A,l),l}catch(l){return r.value=l instanceof Error?l:new Error("Failed to send"),i.value=!1,e.value=e.value.filter(A=>!A.id.startsWith("temp-")),null}}function h(){e.value=[],s.value=void 0,r.value=null}return{messages:e,isLoading:i,error:r,conversationId:s,sendMessage:d,clearMessages:h}}function M(o={}){const n=o.client??g(),e=o.autoLoad??!0,s=(0,t.ref)([]),i=(0,t.ref)(null),r=(0,t.ref)(!1),c=(0,t.ref)(null);async function f(){r.value=!0,c.value=null;try{s.value=await n.getConversations()}catch(d){c.value=d instanceof Error?d:new Error("Failed to load conversations")}finally{r.value=!1}}async function m(d){r.value=!0,c.value=null;try{i.value=await n.getConversation(d)}catch(h){c.value=h instanceof Error?h:new Error("Failed to load conversation")}finally{r.value=!1}}function C(){i.value=null}return(0,t.onMounted)(()=>{e&&f()}),{conversations:s,currentConversation:i,isLoading:r,error:c,selectConversation:m,startNewConversation:C,refreshConversations:f}}function N(o={}){const n=E({client:o.client,conversationId:o.conversationId}),e=M({client:o.client,autoLoad:o.autoLoad}),s=(0,t.computed)(()=>n.error.value||e.error.value);return{messages:n.messages,isLoading:n.isLoading,error:s,sendMessage:n.sendMessage,clearMessages:n.clearMessages,conversationId:n.conversationId,conversations:e.conversations,currentConversation:e.currentConversation,isConversationsLoading:e.isLoading,selectConversation:e.selectConversation,startNewConversation:e.startNewConversation,refreshConversations:e.refreshConversations}}function F(o,n){const e=g();(0,t.onMounted)(()=>{e.registerFunction(o,n.handler,{description:n.description,parameters:n.parameters})}),(0,t.onUnmounted)(()=>{e.unregisterFunction(o)})}function S(o,n){const e=g();(0,t.onMounted)(()=>{e.registerNavigationFunction(o,n)}),(0,t.onUnmounted)(()=>{e.unregisterFunction("navigateToPage")})}function _(o,n){const e=g();(0,t.onMounted)(()=>{e.on(o,n)}),(0,t.onUnmounted)(()=>{e.off(o,n)})}function D(o){const n=(0,t.shallowRef)(o.get());return(0,t.onScopeDispose)(o.subscribe(e=>{n.value=e})),n}exports.AGO_CLIENT_KEY=I;exports.AgoPlugin=y;exports.useAgo=g;exports.useAgoEvents=_;exports.useAgoFunction=F;exports.useAgoNavigation=S;exports.useAgoStore=D;exports.useChat=N;exports.useConversation=M;exports.useMessages=E;
package/dist/vue.d.cts CHANGED
@@ -24,12 +24,26 @@ declare class AgoClient {
24
24
  private contextRegistry;
25
25
  private eventEmitter;
26
26
  private config;
27
+ /** Conversations already warned about an empty reply (one warning each). */
28
+ private warnedEmptyConversations;
27
29
  constructor(config: AgoConfig);
28
30
  /**
29
31
  * Send a message and receive a streaming response
30
32
  */
31
33
  sendMessage(content: string, options?: SendMessageOptions): Promise<AgoMessage>;
32
34
  private processSSEResponse;
35
+ /**
36
+ * Detect a reply that completed as DONE with no content and no activity —
37
+ * today's backend answers an unknown `agent` slug with an empty 200, which
38
+ * otherwise fails silently. Emits `message:empty` (always) and warns on the
39
+ * console once per conversation (unless `warnOnEmptyReply: false`).
40
+ *
41
+ * Deferred with setTimeout(0) so both land AFTER `sendMessage` resolves:
42
+ * `await client.sendMessage(...)` followed by `client.waitFor("message:empty")`
43
+ * must not race. TEMPORARY heuristic: remove once the backend returns an
44
+ * explicit error for unknown agents (see TODOS.md, backend issue).
45
+ */
46
+ private maybeFlagEmptyReply;
33
47
  private handleClientFunctionInvocation;
34
48
  /**
35
49
  * Get list of conversations
@@ -61,6 +75,13 @@ declare class AgoClient {
61
75
  * Reject a tool call
62
76
  */
63
77
  rejectToolCall(toolCallId: string): Promise<void>;
78
+ /**
79
+ * Fetch a backend-stored form collector definition by name (the single source
80
+ * of truth for the form's description, schema, and submit target). Used by
81
+ * `loadFormCollector` / the name-only form of `useFormCollector` so the schema
82
+ * lives in the backend instead of being hardcoded in client code.
83
+ */
84
+ getFormCollector(name: string): Promise<FormCollectorDefinition>;
64
85
  /**
65
86
  * Relay a completed form collector's values to a server-configured destination.
66
87
  *
@@ -195,6 +216,11 @@ declare class AgoClient {
195
216
  }): Promise<AgoClientEvents[K]>;
196
217
  /**
197
218
  * Update client configuration
219
+ *
220
+ * Validates the MERGED config before committing: omitting a key (or passing
221
+ * it as `undefined`, common with spread-built partials) keeps the current
222
+ * value; an explicit empty/non-string `baseUrl` throws `config_missing_base_url`
223
+ * and leaves the client unchanged (no half-updated state).
198
224
  */
199
225
  updateConfig(config: Partial<AgoConfig>): void;
200
226
  /**
@@ -230,8 +256,23 @@ declare interface AgoClientEvents {
230
256
  */
231
257
  "message:answer-complete": AgoMessage;
232
258
  "message:complete": AgoMessage;
259
+ /**
260
+ * A reply finished as `DONE` with empty content and no tool calls, client
261
+ * functions, or follow-up replies — usually an unknown `agent` slug (the
262
+ * backend currently answers those with an empty 200). Fires after
263
+ * `message:complete` AND after `sendMessage` resolves, so subscribing right
264
+ * after `await sendMessage(...)` still catches it. `messageId` and
265
+ * `conversationId` are empty strings when the stream completed without any
266
+ * message data at all (e.g. a proxy stripped the SSE stream).
267
+ */
268
+ "message:empty": {
269
+ conversationId: string;
270
+ messageId: string;
271
+ };
233
272
  "message:error": {
234
273
  error: string;
274
+ /** Stable error code (see configuration.md#error-codes) when the failure was an AgoError. */
275
+ code?: string;
235
276
  conversationId?: string;
236
277
  messageId?: string;
237
278
  };
@@ -260,7 +301,7 @@ declare interface AgoClientEvents {
260
301
  export declare interface AgoConfig {
261
302
  /** API base URL (e.g., https://YOUR-DOMAIN.useago.com) */
262
303
  baseUrl: string;
263
- /** Widget ID (X-Widget-Id header). Auto-generated if not provided. */
304
+ /** Per-visitor anonymous ID (X-User-Anon-Id header). Auto-generated if not provided. */
264
305
  widgetId?: string;
265
306
  /** Default agent (id or slug) for new conversations. Shorthand for `defaultAgentId`. */
266
307
  agent?: string;
@@ -274,6 +315,13 @@ export declare interface AgoConfig {
274
315
  userJwt?: string;
275
316
  /** Enable debug logging */
276
317
  debug?: boolean;
318
+ /**
319
+ * Warn on the console when a reply completes with empty content and no tool
320
+ * calls (the usual cause is an unknown `agent` slug, which the backend
321
+ * currently answers with an empty 200). Default `true`. The `message:empty`
322
+ * event fires regardless of this setting.
323
+ */
324
+ warnOnEmptyReply?: boolean;
277
325
  }
278
326
 
279
327
  declare type AgoEventHandler<K extends AgoEventName> = (data: AgoClientEvents[K]) => void;
@@ -417,6 +465,30 @@ export declare interface Conversation {
417
465
  */
418
466
  declare type DynamicContextProvider = () => ContextEntry | null | undefined;
419
467
 
468
+ /**
469
+ * The backend-stored shape returned by `GET /api/sdk/v1/forms/{name}` (see
470
+ * {@link AgoClient.getFormCollector}). Mirrors the persistable subset of
471
+ * {@link CreateFormCollectorOptions}: the `submit` here is never the client-only
472
+ * `handler` variant (a function can't be stored server-side).
473
+ */
474
+ declare interface FormCollectorDefinition<V = Record<string, unknown>> {
475
+ name: string;
476
+ description: string;
477
+ schema: FormCollectorSchema;
478
+ /** Server-stored submit target, if any. Absent means collect-only. */
479
+ submit?: SubmitConfig<V>;
480
+ }
481
+
482
+ /**
483
+ * Shape of a form's fields. Mirrors a client function's `parameters` (so the same
484
+ * object feeds the store, the update function, and the dynamic context). Additionally allows the SDK-only per-field `requiredWhen`, which {@link toWireParameters} removes before the schema reaches the LLM.
485
+ */
486
+ declare interface FormCollectorSchema {
487
+ type: "object";
488
+ properties: Record<string, FormFieldSchema>;
489
+ required?: string[];
490
+ }
491
+
420
492
  declare interface FormField {
421
493
  type: "string" | "number" | "boolean" | "array";
422
494
  title?: string;
@@ -433,6 +505,43 @@ declare interface FormField_2 {
433
505
  default?: unknown;
434
506
  }
435
507
 
508
+ /**
509
+ * A field's conditional-requiredness rule. The field becomes required exactly
510
+ * when the controlling `property` matches `value` against the answers collected
511
+ * so far:
512
+ * - a single `value`, no `op` — equality (compared as strings);
513
+ * - a single `value` with `op` — numeric comparison (both sides coerced to numbers);
514
+ * - an array `value`, no `op` — "one of": matches when the property equals any
515
+ * listed value.
516
+ *
517
+ * SDK-only — it is stripped from the schema before it reaches the LLM tool
518
+ */
519
+ declare interface FormFieldCondition {
520
+ /** The controlling field whose value the rule tests. */
521
+ property: string;
522
+ /** Comparison operator; omitted means equality. Ignored when `value` is an array. */
523
+ op?: ">=" | "<=" | ">" | "<";
524
+ /**
525
+ * The value(s) to test the controlling field against. A single value is an
526
+ * equality (or, with `op`, a numeric comparison); an array is an "is one of"
527
+ * membership test, e.g. `value: ["1", "2", "3"]`.
528
+ */
529
+ value: string | number | Array<string | number>;
530
+ }
531
+
532
+ /** A single field's schema: the wire-legal keys plus the SDK-only `requiredWhen`. */
533
+ declare interface FormFieldSchema {
534
+ type: string;
535
+ description?: string;
536
+ enum?: string[];
537
+ default?: unknown;
538
+ /**
539
+ * Makes the field required only while this condition holds. Drives the dynamic
540
+ * `missing` list; stripped before the LLM tool — see {@link toWireParameters}.
541
+ */
542
+ requiredWhen?: FormFieldCondition;
543
+ }
544
+
436
545
  /**
437
546
  * Form schema for tool calls requiring user input
438
547
  */
@@ -479,6 +588,27 @@ declare interface Store<T> {
479
588
  subscribe(listener: (state: T) => void): () => void;
480
589
  }
481
590
 
591
+ /**
592
+ * Where a completed form is sent.
593
+ * - `"https://..."` (a bare string) — shorthand for `{ via: "client", url }`: the browser
594
+ * POSTs the values to that URL.
595
+ * - `{ via: "client", url }` — same, written out.
596
+ * - `{ via: "client", handler }` — the browser runs your own submit logic.
597
+ * - `{ via: "backend", destination }` — the browser relays the values to the backend,
598
+ * which forwards them to the server-configured destination (URL + secret stay server-side).
599
+ * - `false` — collect only, no submit function.
600
+ */
601
+ declare type SubmitConfig<V = Record<string, unknown>> = string | {
602
+ via: "client";
603
+ url: string;
604
+ } | {
605
+ via: "client";
606
+ handler: (values: V) => Promise<unknown> | unknown;
607
+ } | {
608
+ via: "backend";
609
+ destination: string;
610
+ } | false;
611
+
482
612
  /**
483
613
  * Tool call data from SSE stream
484
614
  */