@webless/agent 0.10.4 → 0.12.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/dist/index.js CHANGED
@@ -12,9 +12,204 @@ import {
12
12
  // src/runtime/client.ts
13
13
  import {
14
14
  Client,
15
- ClientError as ClientError2
15
+ ClientError as ClientError3
16
16
  } from "eve/client";
17
17
 
18
+ // src/runtime/handoff.ts
19
+ import { ClientError as ClientError2 } from "eve/client";
20
+
21
+ // src/runtime/generated/handoff-contract.ts
22
+ import { z } from "zod";
23
+ var id = z.string().min(1).max(200);
24
+ var sequence = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
25
+ var text = z.string().trim().min(1).max(16e3);
26
+ var agentRuntimeHandoffVersion = "webless.ai/agent-runtime-handoff/v1";
27
+ var agentRuntimeHandoffStatusSchema = z.enum([
28
+ "ai",
29
+ "requesting",
30
+ "queued",
31
+ "human",
32
+ "resolved",
33
+ "failed"
34
+ ]);
35
+ var agentRuntimeHandoffActorSchema = z.strictObject({
36
+ id,
37
+ name: z.string().trim().min(1).max(200)
38
+ });
39
+ var eventBase = {
40
+ id,
41
+ sequence: sequence.refine((value) => value > 0),
42
+ handoffId: id,
43
+ epoch: sequence.refine((value) => value > 0),
44
+ createdAt: z.iso.datetime()
45
+ };
46
+ var agentRuntimeHandoffEventSchema = z.discriminatedUnion("type", [
47
+ z.strictObject({
48
+ ...eventBase,
49
+ type: z.literal("status"),
50
+ status: agentRuntimeHandoffStatusSchema,
51
+ actor: agentRuntimeHandoffActorSchema.nullable()
52
+ }),
53
+ z.strictObject({
54
+ ...eventBase,
55
+ type: z.literal("visitor.message"),
56
+ message: text,
57
+ operationId: id
58
+ }),
59
+ z.strictObject({
60
+ ...eventBase,
61
+ type: z.literal("human.message"),
62
+ message: text,
63
+ actor: agentRuntimeHandoffActorSchema
64
+ })
65
+ ]);
66
+ var agentRuntimeHandoffReadRequestSchema = z.strictObject({
67
+ after: sequence.default(0)
68
+ });
69
+ var agentRuntimeHandoffPageSchema = z.strictObject({
70
+ apiVersion: z.literal(agentRuntimeHandoffVersion),
71
+ sessionId: id,
72
+ enabled: z.boolean(),
73
+ status: agentRuntimeHandoffStatusSchema,
74
+ handoffId: id.nullable(),
75
+ epoch: sequence,
76
+ after: sequence,
77
+ cursor: sequence,
78
+ hasMore: z.boolean(),
79
+ events: z.array(agentRuntimeHandoffEventSchema).max(100)
80
+ }).superRefine((page, ctx) => {
81
+ let cursor = page.after;
82
+ let previousEpoch = 0;
83
+ const statuses = /* @__PURE__ */ new Map();
84
+ const ids = /* @__PURE__ */ new Set();
85
+ const handoffs = /* @__PURE__ */ new Map();
86
+ const terminalEpochs = /* @__PURE__ */ new Set();
87
+ let currentStatus;
88
+ for (const event of page.events) {
89
+ if (event.epoch < previousEpoch)
90
+ ctx.addIssue({
91
+ code: "custom",
92
+ message: "Handoff epochs cannot decrease."
93
+ });
94
+ previousEpoch = event.epoch;
95
+ if (ids.has(event.id))
96
+ ctx.addIssue({
97
+ code: "custom",
98
+ message: "Duplicate handoff event ID."
99
+ });
100
+ ids.add(event.id);
101
+ const handoffId = handoffs.get(event.epoch);
102
+ if (handoffId !== void 0 && handoffId !== event.handoffId || event.epoch === page.epoch && event.handoffId !== page.handoffId)
103
+ ctx.addIssue({
104
+ code: "custom",
105
+ message: "Inconsistent handoff epoch identity."
106
+ });
107
+ handoffs.set(event.epoch, event.handoffId);
108
+ if (terminalEpochs.has(event.epoch) && (event.type !== "status" || event.status !== "ai"))
109
+ ctx.addIssue({
110
+ code: "custom",
111
+ message: "Handoff event follows resolution."
112
+ });
113
+ if (event.type === "status") {
114
+ if (event.status === "human" && event.actor === null)
115
+ ctx.addIssue({
116
+ code: "custom",
117
+ message: "Human ownership requires a representative."
118
+ });
119
+ const previous = statuses.get(event.epoch);
120
+ const transitions = {
121
+ ai: [],
122
+ requesting: ["queued", "resolved", "failed"],
123
+ queued: ["human", "resolved", "failed"],
124
+ human: ["resolved", "failed"],
125
+ resolved: ["ai"],
126
+ failed: []
127
+ };
128
+ if (previous !== void 0 && !transitions[previous].includes(event.status))
129
+ ctx.addIssue({
130
+ code: "custom",
131
+ message: "Invalid handoff ownership transition."
132
+ });
133
+ statuses.set(event.epoch, event.status);
134
+ if (["resolved", "failed", "ai"].includes(event.status))
135
+ terminalEpochs.add(event.epoch);
136
+ if (event.epoch === page.epoch) currentStatus = event.status;
137
+ }
138
+ if (event.sequence !== cursor + 1) {
139
+ ctx.addIssue({
140
+ code: "custom",
141
+ message: "Handoff event gap or replay."
142
+ });
143
+ }
144
+ cursor = event.sequence;
145
+ if (event.epoch > page.epoch) {
146
+ ctx.addIssue({
147
+ code: "custom",
148
+ message: "Event exceeds current epoch."
149
+ });
150
+ }
151
+ }
152
+ if (!page.hasMore && currentStatus !== void 0 && currentStatus !== page.status)
153
+ ctx.addIssue({
154
+ code: "custom",
155
+ message: "Handoff ownership contradicts its final status event."
156
+ });
157
+ if (page.cursor !== cursor || page.hasMore && page.events.length === 0) {
158
+ ctx.addIssue({ code: "custom", message: "Invalid handoff page cursor." });
159
+ }
160
+ if (page.epoch === 0 !== (page.handoffId === null)) {
161
+ ctx.addIssue({ code: "custom", message: "Invalid handoff identity." });
162
+ }
163
+ if (page.handoffId === null && page.status !== "ai") {
164
+ ctx.addIssue({
165
+ code: "custom",
166
+ message: "Handoff identity is required."
167
+ });
168
+ }
169
+ });
170
+ var agentRuntimeHandoffStartSchema = z.strictObject({
171
+ operationId: id
172
+ });
173
+ var agentRuntimeHandoffCommandSchema = z.strictObject({
174
+ operationId: id,
175
+ handoffId: id,
176
+ epoch: sequence.refine((value) => value > 0)
177
+ });
178
+ var agentRuntimeHandoffMessageSchema = agentRuntimeHandoffCommandSchema.extend({ message: text });
179
+ var agentRuntimeHandoffBindingSchema = z.strictObject({
180
+ tenantId: id,
181
+ indexId: id,
182
+ visitorSubject: id,
183
+ sessionId: id,
184
+ handoffId: id,
185
+ epoch: sequence.refine((value) => value > 0)
186
+ });
187
+ var providerEventBase = {
188
+ apiVersion: z.literal(agentRuntimeHandoffVersion),
189
+ binding: agentRuntimeHandoffBindingSchema,
190
+ eventId: id,
191
+ sequence: sequence.refine((value) => value > 0)
192
+ };
193
+ var agentRuntimeHandoffProviderEventSchema = z.discriminatedUnion(
194
+ "type",
195
+ [
196
+ z.strictObject({ ...providerEventBase, type: z.literal("queued") }),
197
+ z.strictObject({
198
+ ...providerEventBase,
199
+ type: z.literal("assigned"),
200
+ actor: agentRuntimeHandoffActorSchema
201
+ }),
202
+ z.strictObject({
203
+ ...providerEventBase,
204
+ type: z.literal("human.message"),
205
+ actor: agentRuntimeHandoffActorSchema,
206
+ message: text
207
+ }),
208
+ z.strictObject({ ...providerEventBase, type: z.literal("resolved") }),
209
+ z.strictObject({ ...providerEventBase, type: z.literal("failed") })
210
+ ]
211
+ );
212
+
18
213
  // src/runtime/capability.ts
19
214
  import { ClientError } from "eve/client";
20
215
  var MAX_REFRESH_SKEW_MS = 3e4;
@@ -84,18 +279,26 @@ function createAgentRuntimeCapability(options) {
84
279
  );
85
280
  }
86
281
  }
87
- const bootstrapBody = JSON.stringify({
282
+ const bootstrapBody = {
88
283
  clientSessionId: options.visitorSessionId,
89
284
  indexId: options.indexId,
90
285
  ...previewBuildId ? { previewBuildId } : {},
91
286
  ...previewGrant ? { previewGrant } : {},
92
287
  version: options.version
93
- });
94
- const postBootstrap = (origin) => fetchImplementation(`${origin}/webless/v1/bootstrap`, {
95
- body: bootstrapBody,
96
- headers: { "content-type": "application/json" },
97
- method: "POST"
98
- });
288
+ };
289
+ const postBootstrap = async (origin) => {
290
+ const send = (advertiseLocation) => fetchImplementation(`${origin}/webless/v1/bootstrap`, {
291
+ body: JSON.stringify({ ...bootstrapBody, ...advertiseLocation ? { locationConsent: "v1" } : {} }),
292
+ headers: { "content-type": "application/json" },
293
+ method: "POST"
294
+ });
295
+ const response2 = await send(Boolean(options.locationConsent));
296
+ if (options.locationConsent && response2.status === 400) {
297
+ const error = await response2.clone().json().catch(() => null);
298
+ if (isRecord(error) && error.code === "invalid_request") return send(false);
299
+ }
300
+ return response2;
301
+ };
99
302
  let response;
100
303
  let lastError;
101
304
  for (const origin of localBootstrapOrigins(options.runtimeOrigin)) {
@@ -161,6 +364,75 @@ async function withCapabilityRefresh(capability, request) {
161
364
  }
162
365
  }
163
366
 
367
+ // src/runtime/handoff.ts
368
+ function createHandoffClient(options) {
369
+ const target = () => {
370
+ const sessionId = options.getSessionId();
371
+ if (!sessionId)
372
+ throw new Error(
373
+ "Start a conversation before requesting a representative."
374
+ );
375
+ return {
376
+ sessionId,
377
+ path: `/webless/v1/session/${encodeURIComponent(sessionId)}/handoff`
378
+ };
379
+ };
380
+ const request = (path, init) => withCapabilityRefresh(options.capability, async () => {
381
+ const response = await options.getClient().fetch(path, {
382
+ ...init,
383
+ cache: "no-store",
384
+ redirect: "error"
385
+ });
386
+ if (!response.ok) {
387
+ throw new ClientError2(
388
+ response.status,
389
+ await response.text(),
390
+ response.headers
391
+ );
392
+ }
393
+ return response;
394
+ });
395
+ const command = async (suffix, body, signal) => {
396
+ const { path } = target();
397
+ await request(path + suffix, {
398
+ method: "POST",
399
+ headers: { "content-type": "application/json" },
400
+ body: JSON.stringify(body),
401
+ signal
402
+ });
403
+ };
404
+ return {
405
+ async read(input = {}) {
406
+ const { after } = agentRuntimeHandoffReadRequestSchema.parse({
407
+ after: input.after
408
+ });
409
+ const { sessionId, path } = target();
410
+ const response = await request(`${path}?after=${after}`, {
411
+ signal: input.signal
412
+ });
413
+ const value = await response.json();
414
+ const page = agentRuntimeHandoffPageSchema.parse(value);
415
+ if (page.sessionId !== sessionId || page.after !== after) {
416
+ throw new Error(
417
+ "The handoff response does not belong to this conversation or cursor."
418
+ );
419
+ }
420
+ return page;
421
+ },
422
+ start: (operationId, signal) => command(
423
+ "",
424
+ agentRuntimeHandoffStartSchema.parse({ operationId }),
425
+ signal
426
+ ),
427
+ send: (input, signal) => command(
428
+ "/messages",
429
+ agentRuntimeHandoffMessageSchema.parse(input),
430
+ signal
431
+ ),
432
+ returnToAI: (input, signal) => command("/return", agentRuntimeHandoffCommandSchema.parse(input), signal)
433
+ };
434
+ }
435
+
164
436
  // src/runtime/config.ts
165
437
  var DEFAULT_RUNTIME_ORIGIN = "https://runtime.staging.webless.ai";
166
438
  var trimTrailingSlash = (value) => value.replace(/\/+$/, "");
@@ -639,6 +911,7 @@ function specialistNameFromInput(input) {
639
911
  return match?.[1]?.trim() || void 0;
640
912
  }
641
913
  function requestedWorkItem(action) {
914
+ if (action.kind === "tool-call" && action.toolName === "request_location") return null;
642
915
  if (action.kind === "tool-call" && action.toolName === "search_discovery") {
643
916
  return {
644
917
  id: action.callId,
@@ -744,13 +1017,14 @@ function applyWorkEvent(event, handlers, workItems) {
744
1017
  );
745
1018
  }
746
1019
  var AgentSession = class {
747
- constructor(indexId, version, runtimeOrigin, visitorSessionId, storeOptions, previewBuildId, getUnpublishedPreviewGrant) {
1020
+ constructor(indexId, version, runtimeOrigin, visitorSessionId, storeOptions, previewBuildId, getUnpublishedPreviewGrant, locationConsent) {
748
1021
  this.indexId = indexId;
749
1022
  this.version = version;
750
1023
  this.runtimeOrigin = runtimeOrigin;
751
1024
  this.visitorSessionId = visitorSessionId;
752
1025
  this.storeOptions = storeOptions;
753
1026
  this.capability = createAgentRuntimeCapability({
1027
+ locationConsent,
754
1028
  getUnpublishedPreviewGrant,
755
1029
  indexId,
756
1030
  previewBuildId,
@@ -758,6 +1032,11 @@ var AgentSession = class {
758
1032
  version,
759
1033
  visitorSessionId
760
1034
  });
1035
+ this.handoff = createHandoffClient({
1036
+ getClient: () => this.ensureClient(),
1037
+ getSessionId: () => this.getActiveSessionId(),
1038
+ capability: this.capability
1039
+ });
761
1040
  }
762
1041
  indexId;
763
1042
  version;
@@ -770,6 +1049,7 @@ var AgentSession = class {
770
1049
  activeResponse;
771
1050
  childStreams;
772
1051
  capability;
1052
+ handoff;
773
1053
  getActiveSessionId() {
774
1054
  return this.session?.state.sessionId ?? loadPersistedAgentSession(this.visitorSessionId, this.storeOptions)?.sessionId;
775
1055
  }
@@ -880,7 +1160,7 @@ var AgentSession = class {
880
1160
  () => activeSession.send(message, { signal })
881
1161
  );
882
1162
  } catch (error) {
883
- if (error instanceof ClientError2 && error.status === 409 && error.code === "session_not_active") {
1163
+ if (error instanceof ClientError3 && error.status === 409 && error.code === "session_not_active") {
884
1164
  clearPersistedAgentSession(this.visitorSessionId, this.storeOptions);
885
1165
  this.session = void 0;
886
1166
  session = void 0;
@@ -1054,10 +1334,10 @@ var AgentSession = class {
1054
1334
  }
1055
1335
  this.session = session;
1056
1336
  const inputResponses = responses.map(
1057
- ({ requestId, optionId, text }) => ({
1337
+ ({ requestId, optionId, text: text2 }) => ({
1058
1338
  requestId,
1059
1339
  ...optionId ? { optionId } : {},
1060
- ...text ? { text } : {}
1340
+ ...text2 ? { text: text2 } : {}
1061
1341
  })
1062
1342
  );
1063
1343
  const response = await withCapabilityRefresh(
@@ -1142,10 +1422,12 @@ function createAgentClient(options) {
1142
1422
  visitorSessionId,
1143
1423
  storeOptions,
1144
1424
  options.previewBuildId,
1145
- options.getUnpublishedPreviewGrant
1425
+ options.getUnpublishedPreviewGrant,
1426
+ options.locationConsent
1146
1427
  );
1147
1428
  return {
1148
1429
  indexId,
1430
+ handoff: session.handoff,
1149
1431
  version,
1150
1432
  runtimeOrigin,
1151
1433
  visitorSessionId,
@@ -1177,7 +1459,7 @@ function createAgentClient(options) {
1177
1459
  }
1178
1460
 
1179
1461
  // src/runtime/errors.ts
1180
- import { ClientError as ClientError3 } from "eve/client";
1462
+ import { ClientError as ClientError4 } from "eve/client";
1181
1463
  var TRANSIENT_AGENT_ERROR_MESSAGE = "The agent run stopped before the action finished. Please try again.";
1182
1464
  var PREVIEW_AUTHORIZATION_ERROR_MESSAGE = "This preview could not be authorized. Open the latest preview from Webless.";
1183
1465
  function isTransientRuntimeMessage(message) {
@@ -1189,7 +1471,7 @@ function isPreviewAuthorizationMessage(message) {
1189
1471
  return normalized.includes("unpublished preview authorization") || normalized.includes("unpublished preview grant") || normalized.includes("agent studio preview grant") || normalized.includes("preview authorization");
1190
1472
  }
1191
1473
  function formatAgentError(error) {
1192
- if (error instanceof ClientError3) {
1474
+ if (error instanceof ClientError4) {
1193
1475
  if (error.status === 401 && error.code === "index_required") {
1194
1476
  return "Missing indexId \u2014 pass a published index id to createAgentClient().";
1195
1477
  }