robotrock 0.8.5 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,200 @@
1
1
  // src/trigger/index.ts
2
2
  import { task, wait } from "@trigger.dev/sdk";
3
3
 
4
+ // src/record-handled-to-otel.ts
5
+ import {
6
+ context,
7
+ trace,
8
+ SpanStatusCode
9
+ } from "@opentelemetry/api";
10
+ var TRACER_NAME = "robotrock";
11
+ var WAIT_SPAN_NAME = "robotrock.wait_for_human";
12
+ var HANDLED_EVENT_NAME = "robotrock.task_handled";
13
+ var INVALID_TRACE_ID = "00000000000000000000000000000000";
14
+ function isValidTraceId(traceId) {
15
+ return typeof traceId === "string" && traceId.length > 0 && traceId !== INVALID_TRACE_ID;
16
+ }
17
+ function resolveSpanContext(span) {
18
+ const active = span ?? trace.getActiveSpan();
19
+ const ctx = active?.spanContext();
20
+ if (!ctx || !isValidTraceId(ctx.traceId)) {
21
+ return void 0;
22
+ }
23
+ return { traceId: ctx.traceId, spanId: ctx.spanId };
24
+ }
25
+ function parseTaskCreatedAt(submittedAt) {
26
+ if (submittedAt) {
27
+ const parsed = Date.parse(submittedAt);
28
+ if (!Number.isNaN(parsed)) {
29
+ return parsed;
30
+ }
31
+ }
32
+ return Date.now();
33
+ }
34
+ function parseHandledAtMs(handledAt) {
35
+ if (handledAt instanceof Date) {
36
+ return handledAt.getTime();
37
+ }
38
+ if (typeof handledAt === "number") {
39
+ return handledAt;
40
+ }
41
+ const parsed = Date.parse(handledAt);
42
+ return Number.isNaN(parsed) ? Date.now() : parsed;
43
+ }
44
+ function toRobotRockHandledOtelInput(handled) {
45
+ if ("actionId" in handled) {
46
+ return {
47
+ taskId: handled.taskId,
48
+ action: {
49
+ id: handled.actionId,
50
+ data: handled.data
51
+ },
52
+ handledBy: handled.handledBy,
53
+ handledAt: handled.handledAt
54
+ };
55
+ }
56
+ return {
57
+ taskId: handled.taskId,
58
+ action: {
59
+ id: handled.action.id,
60
+ title: handled.action.title,
61
+ data: handled.action.data
62
+ },
63
+ handledBy: handled.handledBy,
64
+ handledAt: handled.handledAt
65
+ };
66
+ }
67
+ function captureRobotRockOtelHandle(task2, options) {
68
+ const spanCtx = resolveSpanContext(options?.span);
69
+ return {
70
+ traceId: spanCtx?.traceId ?? "",
71
+ spanId: spanCtx?.spanId ?? "",
72
+ taskId: task2.taskId,
73
+ threadId: task2.threadId,
74
+ taskCreatedAt: parseTaskCreatedAt(task2.submittedAt)
75
+ };
76
+ }
77
+ function startRobotRockHumanWaitSpan(handle, options) {
78
+ const parent = options?.span ?? trace.getActiveSpan();
79
+ if (!parent) {
80
+ return void 0;
81
+ }
82
+ const tracer = trace.getTracer(options?.tracerName ?? TRACER_NAME);
83
+ const span = tracer.startSpan(
84
+ WAIT_SPAN_NAME,
85
+ {},
86
+ trace.setSpan(context.active(), parent)
87
+ );
88
+ span.setAttribute("robotrock.task_id", handle.taskId);
89
+ if (handle.threadId) {
90
+ span.setAttribute("robotrock.thread_id", handle.threadId);
91
+ }
92
+ return span;
93
+ }
94
+ function recordRobotRockHandledToOtel(handled, options = {}) {
95
+ const span = options.span ?? trace.getActiveSpan();
96
+ if (!span) {
97
+ return;
98
+ }
99
+ const input = toRobotRockHandledOtelInput(handled);
100
+ const handledAtMs = parseHandledAtMs(input.handledAt);
101
+ const handle = options.handle;
102
+ const humanWaitMs = handle != null ? Math.max(0, handledAtMs - handle.taskCreatedAt) : void 0;
103
+ span.setAttribute("robotrock.task_id", input.taskId);
104
+ span.setAttribute("robotrock.action.id", input.action.id);
105
+ if (input.action.title) {
106
+ span.setAttribute("robotrock.action.title", input.action.title);
107
+ }
108
+ if (input.handledBy) {
109
+ span.setAttribute("robotrock.handled_by", input.handledBy);
110
+ }
111
+ span.setAttribute("robotrock.handled_at", new Date(handledAtMs).toISOString());
112
+ if (humanWaitMs != null) {
113
+ span.setAttribute("robotrock.human_wait_ms", humanWaitMs);
114
+ }
115
+ if (input.threadId) {
116
+ span.setAttribute("robotrock.thread_id", input.threadId);
117
+ }
118
+ const eventAttributes = {
119
+ "robotrock.task_id": input.taskId,
120
+ "robotrock.action.id": input.action.id
121
+ };
122
+ if (input.handledBy) {
123
+ eventAttributes["robotrock.handled_by"] = input.handledBy;
124
+ }
125
+ if (humanWaitMs != null) {
126
+ eventAttributes["robotrock.human_wait_ms"] = humanWaitMs;
127
+ }
128
+ if (options.includeActionData && input.action.data != null) {
129
+ const serialized = JSON.stringify(input.action.data);
130
+ const truncated = serialized.length > 512 ? `${serialized.slice(0, 512)}\u2026` : serialized;
131
+ span.setAttribute("robotrock.action.data", truncated);
132
+ eventAttributes["robotrock.action.data"] = truncated;
133
+ }
134
+ span.addEvent(HANDLED_EVENT_NAME, eventAttributes);
135
+ }
136
+ function endRobotRockHumanWaitSpan(span, handled, options = {}) {
137
+ if (!span) {
138
+ return;
139
+ }
140
+ const outcome = options.outcome ?? (handled ? "handled" : "timeout");
141
+ if (handled && outcome === "handled") {
142
+ recordRobotRockHandledToOtel(handled, {
143
+ span,
144
+ handle: options.handle,
145
+ includeActionData: options.includeActionData
146
+ });
147
+ span.setStatus({ code: SpanStatusCode.OK });
148
+ } else {
149
+ span.setAttribute("robotrock.outcome", outcome);
150
+ span.setStatus({
151
+ code: SpanStatusCode.ERROR,
152
+ message: outcome === "timeout" ? "Human response timed out before validUntil" : "Human wait failed"
153
+ });
154
+ }
155
+ span.end();
156
+ }
157
+
158
+ // src/otel-platform.ts
159
+ function shouldRecordRobotRockOtel(recordOtel) {
160
+ if (recordOtel === true) {
161
+ return true;
162
+ }
163
+ if (recordOtel === false) {
164
+ return false;
165
+ }
166
+ const env = process.env.ROBOTROCK_OTEL_RECORD_HANDLED;
167
+ return env === "true" || env === "1";
168
+ }
169
+ function stripPlatformOtelFields(payload) {
170
+ const { recordOtel: _recordOtel, otelIncludeActionData: _otelIncludeActionData, ...rest } = payload;
171
+ return rest;
172
+ }
173
+ function beginRobotRockHumanWaitOtel(task2, options = {}) {
174
+ const recordOtel = shouldRecordRobotRockOtel(options.recordOtel);
175
+ if (!recordOtel) {
176
+ return { recordOtel: false };
177
+ }
178
+ const handle = captureRobotRockOtelHandle(task2);
179
+ const waitSpan = startRobotRockHumanWaitSpan(handle);
180
+ return {
181
+ recordOtel: true,
182
+ handle,
183
+ waitSpan,
184
+ includeActionData: options.otelIncludeActionData
185
+ };
186
+ }
187
+ function finishRobotRockHumanWaitOtel(session, handled, outcome) {
188
+ if (!session.recordOtel) {
189
+ return;
190
+ }
191
+ endRobotRockHumanWaitSpan(session.waitSpan, handled, {
192
+ handle: session.handle,
193
+ outcome,
194
+ includeActionData: session.includeActionData
195
+ });
196
+ }
197
+
4
198
  // src/schemas/index.ts
5
199
  import { z as z2 } from "zod";
6
200
 
@@ -167,12 +361,12 @@ function validateUrlValue(value, widget) {
167
361
  }
168
362
  return null;
169
363
  }
170
- function validateContextPublicUrls(context) {
171
- if (!context?.data) {
364
+ function validateContextPublicUrls(context2) {
365
+ if (!context2?.data) {
172
366
  return null;
173
367
  }
174
- for (const [field, value] of Object.entries(context.data)) {
175
- const widget = widgetType(context.ui, field);
368
+ for (const [field, value] of Object.entries(context2.data)) {
369
+ const widget = widgetType(context2.ui, field);
176
370
  if (!widget) {
177
371
  continue;
178
372
  }
@@ -205,13 +399,15 @@ var triggerHandlerSchema = webhookHandlerSchema.extend({
205
399
  tokenId: z.string().min(1)
206
400
  });
207
401
  var handlerSchema = z.discriminatedUnion("type", [webhookHandlerSchema, triggerHandlerSchema]);
208
- var taskActionSchema = z.object({
402
+ var taskActionInputSchema = z.object({
209
403
  id: z.string().min(1),
210
404
  title: z.string().min(1),
211
405
  description: z.string().optional(),
212
406
  schema: jsonSchema7Schema.optional(),
213
407
  ui: uiSchemaSchema.optional(),
214
- data: z.record(z.string(), z.unknown()).optional(),
408
+ data: z.record(z.string(), z.unknown()).optional()
409
+ });
410
+ var taskActionSchema = taskActionInputSchema.extend({
215
411
  // Optional handlers for this action - if present, must have at least 1
216
412
  handlers: z.array(handlerSchema).min(1).optional()
217
413
  });
@@ -227,7 +423,8 @@ var contextDataSchema = z.object({
227
423
  data: z.record(z.string(), z.unknown()),
228
424
  ui: contextUiSchema
229
425
  }).optional();
230
- var taskContextObjectSchema = z.object({
426
+ var TASK_CONTEXT_FORMAT_VERSION = 2;
427
+ var taskContextObjectBaseSchema = z.object({
231
428
  /** Source application id; omitted tasks are grouped in the default inbox. */
232
429
  app: z.string().min(1).optional(),
233
430
  type: z.string().min(1),
@@ -235,9 +432,22 @@ var taskContextObjectSchema = z.object({
235
432
  description: z.string().optional(),
236
433
  validUntil: z.string().optional(),
237
434
  context: contextDataSchema,
435
+ /** Task context wire format version. @default 2 */
436
+ contextVersion: z.literal(2).optional(),
437
+ /** @deprecated Use `contextVersion`. Accepted on ingest only. */
238
438
  version: z.literal(2).optional(),
239
439
  actions: z.array(taskActionSchema).min(1, "At least one action is required")
240
440
  });
441
+ function normalizeTaskContextVersion(data) {
442
+ const { version: legacyVersion, contextVersion, ...rest } = data;
443
+ return {
444
+ ...rest,
445
+ contextVersion: contextVersion ?? legacyVersion ?? TASK_CONTEXT_FORMAT_VERSION
446
+ };
447
+ }
448
+ var taskContextObjectSchema = taskContextObjectBaseSchema.transform(
449
+ normalizeTaskContextVersion
450
+ );
241
451
  function refineContextPublicUrls(data, ctx) {
242
452
  const error = validateContextPublicUrls(data.context);
243
453
  if (error) {
@@ -281,61 +491,11 @@ var threadUpdateInputSchema = z.object({
281
491
  });
282
492
  var taskPriorities = ["low", "normal", "high", "urgent"];
283
493
  var taskPrioritySchema = z.enum(taskPriorities);
284
- var nonNegativeInt = z.number().int().nonnegative();
285
- var agentCostTokensSchema = z.object({
286
- input: nonNegativeInt.optional(),
287
- output: nonNegativeInt.optional(),
288
- total: nonNegativeInt.optional()
289
- });
290
- var agentCostSchema = z.object({
291
- tokens: agentCostTokensSchema.optional(),
292
- eur: z.number().nonnegative().optional(),
293
- usd: z.number().nonnegative().optional()
294
- });
295
- var AGENT_INFO_MAX_KEYS = 32;
296
- var AGENT_TOOL_CALLS_MAX_KEYS = 64;
297
- var AGENT_OTEL_SPANS_MAX = 32;
298
- var agentToolCallsSchema = z.record(z.string().min(1), nonNegativeInt);
299
- var agentOtelSpanStatusSchema = z.enum(["ok", "error"]);
300
- var agentOtelSpanSummarySchema = z.object({
301
- name: z.string().min(1),
302
- durationMs: z.number().nonnegative(),
303
- status: agentOtelSpanStatusSchema
304
- });
305
- var agentOtelSchema = z.object({
306
- traceId: z.string().min(1),
307
- spanId: z.string().min(1).optional(),
308
- rootDurationMs: z.number().nonnegative().optional(),
309
- spans: z.array(agentOtelSpanSummarySchema).max(AGENT_OTEL_SPANS_MAX).optional()
310
- });
311
494
  var agentTelemetrySchema = z.object({
312
495
  /** Agent or app version (semver, git SHA, deploy tag — caller-defined). */
313
- version: z.string().min(1).optional(),
314
- /** Total tool invocations. When omitted but `toolCalls` is set, the sum of counts is used. */
315
- toolCallCount: nonNegativeInt.optional(),
316
- /** Per-tool invocation counts keyed by tool name. */
317
- toolCalls: agentToolCallsSchema.optional(),
318
- cost: agentCostSchema.optional(),
319
- /** Free-form metadata for experiments (model, prompt variant, etc.). */
320
- info: z.record(z.string(), z.unknown()).optional(),
321
- /** Structured OpenTelemetry span snapshot for feedback analysis. */
322
- otel: agentOtelSchema.optional()
323
- }).refine(
324
- (data) => {
325
- const keys = data.info ? Object.keys(data.info) : [];
326
- return keys.length <= AGENT_INFO_MAX_KEYS;
327
- },
328
- { message: `agent.info may contain at most ${AGENT_INFO_MAX_KEYS} keys` }
329
- ).refine(
330
- (data) => {
331
- const keys = data.toolCalls ? Object.keys(data.toolCalls) : [];
332
- return keys.length <= AGENT_TOOL_CALLS_MAX_KEYS;
333
- },
334
- {
335
- message: `agent.toolCalls may contain at most ${AGENT_TOOL_CALLS_MAX_KEYS} tool names`
336
- }
337
- );
338
- var createTaskBodySchema = taskContextObjectSchema.extend({
496
+ version: z.string().min(1)
497
+ });
498
+ var createTaskBodySchema = taskContextObjectBaseSchema.extend({
339
499
  assignTo: assignToSchema.optional(),
340
500
  /**
341
501
  * Groups related tasks together. When omitted, the server generates one and
@@ -352,9 +512,59 @@ var createTaskBodySchema = taskContextObjectSchema.extend({
352
512
  * the inbox status bar and the thread update log.
353
513
  */
354
514
  update: threadUpdateInputSchema.optional(),
355
- /** Agent telemetry (version, cost, tool calls) — not shown in inbox UI. */
515
+ /** Agent release version — not shown in inbox UI. */
356
516
  agent: agentTelemetrySchema.optional()
357
- }).superRefine(refineContextPublicUrls);
517
+ }).transform(normalizeTaskContextVersion).superRefine(refineContextPublicUrls);
518
+ var agentChatSeedActionSchema = z.object({
519
+ /** Stable action id echoed back on submit; auto-derived from title when omitted. */
520
+ id: z.string().min(1).optional(),
521
+ title: z.string().min(1),
522
+ description: z.string().optional(),
523
+ /** JSON Schema object for the form fields. */
524
+ schema: z.record(z.string(), z.unknown()).optional(),
525
+ /** RJSF ui schema overrides (ui:widget, ui:enumNames, etc.). */
526
+ ui: z.record(z.string(), z.unknown()).optional(),
527
+ /** Optional default field values. */
528
+ data: z.record(z.string(), z.unknown()).optional()
529
+ });
530
+ var agentChatSeedMessageSchema = z.object({
531
+ role: z.enum(["user", "assistant"]),
532
+ text: z.string().min(1),
533
+ /** Optional display-name override for the message sender. */
534
+ senderName: z.string().min(1).optional(),
535
+ /**
536
+ * Optional structured input request rendered as a styled RobotRock action
537
+ * widget below the message text. Use this instead of asking the user to reply
538
+ * in plain text so the request is always a proper action form.
539
+ */
540
+ requestActionInput: z.object({
541
+ prompt: z.string().optional(),
542
+ action: agentChatSeedActionSchema
543
+ }).optional()
544
+ });
545
+ var createAgentChatBodySchema = z.object({
546
+ /** Trigger chat agent id (task slug). Required unless `parentChatId` is set. */
547
+ agentIdentifier: z.string().min(1).optional(),
548
+ /** Inherit tenant/connection/agent from an existing chat (agent-spawned chats). */
549
+ parentChatId: z.string().min(1).optional(),
550
+ /** Convex tenantTriggerConnections id; resolved from the tenant when omitted. */
551
+ connectionId: z.string().min(1).optional(),
552
+ /** Source application id; groups the chat under an inbox section. */
553
+ app: z.string().min(1).optional(),
554
+ assignTo: assignToSchema.optional(),
555
+ title: z.string().min(1),
556
+ messages: z.array(agentChatSeedMessageSchema).default([]),
557
+ /** Provenance label stored on the session ("cron" | "agent" | ...). */
558
+ source: z.string().min(1).optional()
559
+ }).refine((data) => Boolean(data.agentIdentifier || data.parentChatId), {
560
+ message: "Provide either agentIdentifier or parentChatId"
561
+ });
562
+ var agentChatTransportBodySchema = z.object({
563
+ chatId: z.string().min(1),
564
+ publicAccessToken: z.string().min(1),
565
+ lastEventId: z.string().optional(),
566
+ isStreaming: z.boolean().optional()
567
+ });
358
568
 
359
569
  // src/schemas/index.ts
360
570
  var safeUrlSchema2 = z2.string().refine((url) => isPublicHttpUrl(url), {
@@ -377,13 +587,15 @@ var triggerHandlerSchema2 = webhookHandlerSchema2.extend({
377
587
  tokenId: z2.string().min(1)
378
588
  });
379
589
  var handlerSchema2 = z2.discriminatedUnion("type", [webhookHandlerSchema2, triggerHandlerSchema2]);
380
- var taskActionSchema2 = z2.object({
590
+ var taskActionInputSchema2 = z2.object({
381
591
  id: z2.string().min(1),
382
592
  title: z2.string().min(1),
383
593
  description: z2.string().optional(),
384
594
  schema: jsonSchema7Schema2.optional(),
385
595
  ui: uiSchemaSchema2.optional(),
386
- data: z2.record(z2.string(), z2.unknown()).optional(),
596
+ data: z2.record(z2.string(), z2.unknown()).optional()
597
+ });
598
+ var taskActionSchema2 = taskActionInputSchema2.extend({
387
599
  handlers: z2.array(handlerSchema2).min(1).optional()
388
600
  });
389
601
  var uiFieldSchemaSchema2 = z2.object({
@@ -398,16 +610,29 @@ var contextDataSchema2 = z2.object({
398
610
  data: z2.record(z2.string(), z2.unknown()),
399
611
  ui: contextUiSchema2
400
612
  }).optional();
401
- var taskContextObjectSchema2 = z2.object({
613
+ var TASK_CONTEXT_FORMAT_VERSION2 = 2;
614
+ var taskContextObjectBaseSchema2 = z2.object({
402
615
  app: z2.string().min(1).optional(),
403
616
  type: z2.string().min(1),
404
617
  name: z2.string().min(1),
405
618
  description: z2.string().optional(),
406
619
  validUntil: z2.string().optional(),
407
620
  context: contextDataSchema2,
621
+ contextVersion: z2.literal(2).optional(),
622
+ /** @deprecated Use `contextVersion`. Accepted on ingest only. */
408
623
  version: z2.literal(2).optional(),
409
624
  actions: z2.array(taskActionSchema2).min(1, "At least one action is required")
410
625
  });
626
+ function normalizeTaskContextVersion2(data) {
627
+ const { version: legacyVersion, contextVersion, ...rest } = data;
628
+ return {
629
+ ...rest,
630
+ contextVersion: contextVersion ?? legacyVersion ?? TASK_CONTEXT_FORMAT_VERSION2
631
+ };
632
+ }
633
+ var taskContextObjectSchema2 = taskContextObjectBaseSchema2.transform(
634
+ normalizeTaskContextVersion2
635
+ );
411
636
  function refineContextPublicUrls2(data, ctx) {
412
637
  const error = validateContextPublicUrls(data.context);
413
638
  if (error) {
@@ -451,56 +676,10 @@ var threadUpdateInputSchema2 = z2.object({
451
676
  });
452
677
  var taskPriorities2 = ["low", "normal", "high", "urgent"];
453
678
  var taskPrioritySchema2 = z2.enum(taskPriorities2);
454
- var nonNegativeInt2 = z2.number().int().nonnegative();
455
- var agentCostTokensSchema2 = z2.object({
456
- input: nonNegativeInt2.optional(),
457
- output: nonNegativeInt2.optional(),
458
- total: nonNegativeInt2.optional()
459
- });
460
- var agentCostSchema2 = z2.object({
461
- tokens: agentCostTokensSchema2.optional(),
462
- eur: z2.number().nonnegative().optional(),
463
- usd: z2.number().nonnegative().optional()
464
- });
465
- var AGENT_INFO_MAX_KEYS2 = 32;
466
- var AGENT_TOOL_CALLS_MAX_KEYS2 = 64;
467
- var AGENT_OTEL_SPANS_MAX2 = 32;
468
- var agentToolCallsSchema2 = z2.record(z2.string().min(1), nonNegativeInt2);
469
- var agentOtelSpanStatusSchema2 = z2.enum(["ok", "error"]);
470
- var agentOtelSpanSummarySchema2 = z2.object({
471
- name: z2.string().min(1),
472
- durationMs: z2.number().nonnegative(),
473
- status: agentOtelSpanStatusSchema2
474
- });
475
- var agentOtelSchema2 = z2.object({
476
- traceId: z2.string().min(1),
477
- spanId: z2.string().min(1).optional(),
478
- rootDurationMs: z2.number().nonnegative().optional(),
479
- spans: z2.array(agentOtelSpanSummarySchema2).max(AGENT_OTEL_SPANS_MAX2).optional()
480
- });
481
679
  var agentTelemetrySchema2 = z2.object({
482
- version: z2.string().min(1).optional(),
483
- toolCallCount: nonNegativeInt2.optional(),
484
- toolCalls: agentToolCallsSchema2.optional(),
485
- cost: agentCostSchema2.optional(),
486
- info: z2.record(z2.string(), z2.unknown()).optional(),
487
- otel: agentOtelSchema2.optional()
488
- }).refine(
489
- (data) => {
490
- const keys = data.info ? Object.keys(data.info) : [];
491
- return keys.length <= AGENT_INFO_MAX_KEYS2;
492
- },
493
- { message: `agent.info may contain at most ${AGENT_INFO_MAX_KEYS2} keys` }
494
- ).refine(
495
- (data) => {
496
- const keys = data.toolCalls ? Object.keys(data.toolCalls) : [];
497
- return keys.length <= AGENT_TOOL_CALLS_MAX_KEYS2;
498
- },
499
- {
500
- message: `agent.toolCalls may contain at most ${AGENT_TOOL_CALLS_MAX_KEYS2} tool names`
501
- }
502
- );
503
- var createTaskBodySchema2 = taskContextObjectSchema2.extend({
680
+ version: z2.string().min(1)
681
+ });
682
+ var createTaskBodySchema2 = taskContextObjectBaseSchema2.extend({
504
683
  assignTo: assignToSchema2.optional(),
505
684
  /**
506
685
  * Groups related tasks together. When omitted, the server generates one and
@@ -518,7 +697,7 @@ var createTaskBodySchema2 = taskContextObjectSchema2.extend({
518
697
  */
519
698
  update: threadUpdateInputSchema2.optional(),
520
699
  agent: agentTelemetrySchema2.optional()
521
- }).superRefine(refineContextPublicUrls2);
700
+ }).transform(normalizeTaskContextVersion2).superRefine(refineContextPublicUrls2);
522
701
  var threadUpdateBodySchema = threadUpdateInputSchema2;
523
702
 
524
703
  // src/approval-result.ts
@@ -548,12 +727,159 @@ function toDiscriminatedApprovalResult(actions, task2) {
548
727
  };
549
728
  }
550
729
 
730
+ // src/http.ts
731
+ var RobotRockError = class extends Error {
732
+ constructor(message, statusCode, response) {
733
+ super(message);
734
+ this.statusCode = statusCode;
735
+ this.response = response;
736
+ this.name = "RobotRockError";
737
+ }
738
+ };
739
+ async function parseResponseBody(response) {
740
+ const contentType = response.headers.get("content-type") ?? "";
741
+ const bodyText = await response.text();
742
+ if (!bodyText) {
743
+ return null;
744
+ }
745
+ if (contentType.toLowerCase().includes("application/json")) {
746
+ try {
747
+ return JSON.parse(bodyText);
748
+ } catch {
749
+ }
750
+ }
751
+ try {
752
+ return JSON.parse(bodyText);
753
+ } catch {
754
+ return bodyText;
755
+ }
756
+ }
757
+ function getErrorMessage(data, fallback) {
758
+ if (data && typeof data === "object" && !Array.isArray(data)) {
759
+ const maybeMessage = data.message;
760
+ if (typeof maybeMessage === "string" && maybeMessage.trim()) {
761
+ return maybeMessage;
762
+ }
763
+ }
764
+ if (typeof data === "string" && data.trim()) {
765
+ const compact = data.replace(/\s+/g, " ").trim();
766
+ const snippet = compact.length > 180 ? `${compact.slice(0, 180)}...` : compact;
767
+ return `${fallback}. Server returned non-JSON response: ${snippet}`;
768
+ }
769
+ return fallback;
770
+ }
771
+
772
+ // ../core/src/handler-retry.ts
773
+ var HANDLER_RETRY_DELAYS_MS = [
774
+ 6e4,
775
+ // 1m
776
+ 3e5,
777
+ // 5m
778
+ 18e5,
779
+ // 30m
780
+ 36e5,
781
+ // 1h
782
+ 216e5,
783
+ // 6h
784
+ 864e5,
785
+ // 24h
786
+ 1728e5
787
+ // 48h
788
+ ];
789
+ var HANDLER_MAX_ATTEMPTS = HANDLER_RETRY_DELAYS_MS.length + 1;
790
+
791
+ // ../core/src/attribution/index.ts
792
+ import { z as z3 } from "zod";
793
+
794
+ // ../core/src/app-url.ts
795
+ var PRODUCTION_APP_URL = "https://app.robotrock.io";
796
+ var APP_SIGNUP_URL = `${PRODUCTION_APP_URL}/sign-up`;
797
+
798
+ // ../core/src/attribution/index.ts
799
+ var ATTRIBUTION_COOKIE_MAX_AGE = 60 * 60 * 24 * 90;
800
+ var attributionSchema = z3.object({
801
+ utmSource: z3.string().optional(),
802
+ utmMedium: z3.string().optional(),
803
+ utmCampaign: z3.string().optional(),
804
+ utmContent: z3.string().optional(),
805
+ utmTerm: z3.string().optional(),
806
+ gclid: z3.string().optional(),
807
+ landingUrl: z3.string().optional(),
808
+ referrer: z3.string().optional(),
809
+ capturedAt: z3.number()
810
+ });
811
+
812
+ // src/chats.ts
813
+ function createChatsApi(config) {
814
+ const headers = () => ({
815
+ "Content-Type": "application/json",
816
+ "X-Api-Key": config.apiKey
817
+ });
818
+ return {
819
+ async create(input) {
820
+ const bodyPayload = {
821
+ ...input,
822
+ app: input.app ?? config.app,
823
+ messages: input.messages ?? []
824
+ };
825
+ const validation = createAgentChatBodySchema.safeParse(bodyPayload);
826
+ if (!validation.success) {
827
+ throw new RobotRockError(
828
+ `Invalid chat: ${validation.error.issues[0]?.message}`,
829
+ 400,
830
+ validation.error.issues
831
+ );
832
+ }
833
+ const response = await fetch(`${config.baseUrl}/agent-chats`, {
834
+ method: "POST",
835
+ headers: headers(),
836
+ body: JSON.stringify(validation.data)
837
+ });
838
+ const data = await parseResponseBody(response);
839
+ if (!response.ok) {
840
+ throw new RobotRockError(
841
+ getErrorMessage(data, "Failed to create chat"),
842
+ response.status,
843
+ data
844
+ );
845
+ }
846
+ const result = data;
847
+ return { tenantSlug: result.tenantSlug, chats: result.chats };
848
+ },
849
+ async close(chatId, options) {
850
+ if (!chatId) {
851
+ throw new RobotRockError("chatId is required to close a chat", 400);
852
+ }
853
+ const response = await fetch(
854
+ `${config.baseUrl}/agent-chats/${encodeURIComponent(chatId)}/close`,
855
+ {
856
+ method: "POST",
857
+ headers: headers(),
858
+ body: JSON.stringify({ reason: options?.reason })
859
+ }
860
+ );
861
+ if (!response.ok) {
862
+ const data = await parseResponseBody(response);
863
+ throw new RobotRockError(
864
+ getErrorMessage(data, "Failed to close chat"),
865
+ response.status,
866
+ data
867
+ );
868
+ }
869
+ }
870
+ };
871
+ }
872
+
551
873
  // src/client.ts
552
874
  var DEFAULT_POLL_INTERVAL_MS = 2e3;
553
875
  var DEFAULT_TIMEOUT_MS = 24 * 60 * 60 * 1e3;
554
876
  function sleep(ms) {
555
877
  return new Promise((resolve) => setTimeout(resolve, ms));
556
878
  }
879
+ function resolveAgentVersionFromEnv() {
880
+ const fromEnv = process.env.AGENT_VERSION?.trim() || process.env.ROBOTROCK_AGENT_VERSION?.trim();
881
+ return fromEnv || void 0;
882
+ }
557
883
  function parseValidUntilMs(value) {
558
884
  if (value === void 0) {
559
885
  return void 0;
@@ -581,21 +907,18 @@ function serializeValidUntil(value) {
581
907
  }
582
908
  throw new RobotRockError("Invalid validUntil: expected a Date or parseable date string", 400);
583
909
  }
584
- var RobotRockError = class extends Error {
585
- constructor(message, statusCode, response) {
586
- super(message);
587
- this.statusCode = statusCode;
588
- this.response = response;
589
- this.name = "RobotRockError";
590
- }
591
- };
592
910
  var RobotRock = class {
593
911
  apiKey;
594
912
  baseUrl;
595
913
  app;
596
- version;
914
+ agentVersion;
915
+ contextVersion;
597
916
  webhook;
598
917
  polling;
918
+ /** Task CRUD: `create`, `get`, `cancel`, `sendUpdate`. */
919
+ tasks;
920
+ /** Chat CRUD: `create`, `close`. */
921
+ chats;
599
922
  constructor(config) {
600
923
  if (config.webhook && config.polling) {
601
924
  throw new Error(
@@ -612,26 +935,37 @@ var RobotRock = class {
612
935
  const rawBase = config.baseUrl ?? "https://api.robotrock.io/v1";
613
936
  this.baseUrl = rawBase.replace(/\/+$/, "");
614
937
  this.app = config.app;
615
- this.version = config.version ?? 2;
938
+ this.agentVersion = config.version ?? resolveAgentVersionFromEnv();
939
+ this.contextVersion = config.advanced?.contextVersion ?? TASK_CONTEXT_FORMAT_VERSION2;
616
940
  this.webhook = config.webhook;
617
941
  this.polling = config.polling ?? {};
942
+ this.tasks = {
943
+ create: (task2) => this.createTaskRequest(task2),
944
+ get: (taskId) => this.getTaskById(taskId),
945
+ cancel: (taskId) => this.cancelTaskRequest(taskId),
946
+ sendUpdate: (input) => this.sendThreadUpdate(input)
947
+ };
948
+ this.chats = createChatsApi({
949
+ baseUrl: this.baseUrl,
950
+ apiKey: this.apiKey,
951
+ app: this.app
952
+ });
618
953
  }
619
- /**
620
- * Create a task via POST /v1 without waiting for a human response.
621
- */
622
- async createTask(task2) {
954
+ async createTaskRequest(task2) {
623
955
  const normalizedTask = normalizeSendToHumanInput(task2, {
624
956
  webhook: this.webhook,
625
957
  app: this.app,
626
- version: this.version
958
+ contextVersion: this.contextVersion,
959
+ agentVersion: this.agentVersion
627
960
  });
961
+ const agentVersion = task2.version ?? this.agentVersion;
628
962
  const bodyPayload = {
629
963
  ...normalizedTask,
630
964
  ...task2.assignTo !== void 0 ? { assignTo: task2.assignTo } : {},
631
965
  ...task2.threadId !== void 0 ? { threadId: task2.threadId } : {},
632
966
  ...task2.priority !== void 0 ? { priority: task2.priority } : {},
633
967
  ...task2.update !== void 0 ? { update: task2.update } : {},
634
- ...task2.agent !== void 0 ? { agent: task2.agent } : {}
968
+ ...agentVersion !== void 0 ? { agent: { version: agentVersion } } : {}
635
969
  };
636
970
  const validation = createTaskBodySchema2.safeParse(bodyPayload);
637
971
  if (!validation.success) {
@@ -667,9 +1001,10 @@ var RobotRock = class {
667
1001
  const normalizedTask = normalizeSendToHumanInput(task2, {
668
1002
  webhook: this.webhook,
669
1003
  app: this.app,
670
- version: this.version
1004
+ contextVersion: this.contextVersion,
1005
+ agentVersion: this.agentVersion
671
1006
  });
672
- const createdTaskTask = await this.createTask(task2);
1007
+ const createdTaskTask = await this.createTaskRequest(task2);
673
1008
  const hasHandlers = normalizedTask.actions.some(
674
1009
  (action) => Array.isArray(action.handlers) && action.handlers.length > 0
675
1010
  );
@@ -686,7 +1021,7 @@ var RobotRock = class {
686
1021
  const deadline = validUntilMs !== void 0 ? Math.min(pollingDeadline, validUntilMs) : pollingDeadline;
687
1022
  const taskId = createdTaskTask.taskId;
688
1023
  while (Date.now() < deadline) {
689
- const existing = await this.getTask(taskId);
1024
+ const existing = await this.getTaskById(taskId);
690
1025
  if (existing?.status === "handled" && existing.handled) {
691
1026
  return {
692
1027
  mode: "handled",
@@ -708,10 +1043,35 @@ var RobotRock = class {
708
1043
  }
709
1044
  throw new TaskTimeoutError(`No human response within ${timeoutMs}ms`);
710
1045
  }
1046
+ /**
1047
+ * Create a task via POST /v1 without waiting for a human response.
1048
+ * @deprecated Use `client.tasks.create()` instead.
1049
+ */
1050
+ async createTask(task2) {
1051
+ return this.tasks.create(task2);
1052
+ }
711
1053
  /**
712
1054
  * Get a task by public task id (returned as `task.taskId` from {@link sendToHuman}).
1055
+ * @deprecated Use `client.tasks.get()` instead.
713
1056
  */
714
1057
  async getTask(taskId) {
1058
+ return this.tasks.get(taskId);
1059
+ }
1060
+ /**
1061
+ * Log a status update against a thread.
1062
+ * @deprecated Use `client.tasks.sendUpdate()` instead.
1063
+ */
1064
+ async sendUpdate(input) {
1065
+ return this.tasks.sendUpdate(input);
1066
+ }
1067
+ /**
1068
+ * Cancel a task by public task id.
1069
+ * @deprecated Use `client.tasks.cancel()` instead.
1070
+ */
1071
+ async cancelTask(taskId) {
1072
+ return this.tasks.cancel(taskId);
1073
+ }
1074
+ async getTaskById(taskId) {
715
1075
  const response = await fetch(`${this.baseUrl}/tasks/${taskId}`, {
716
1076
  method: "GET",
717
1077
  headers: {
@@ -731,11 +1091,11 @@ var RobotRock = class {
731
1091
  }
732
1092
  return data;
733
1093
  }
734
- /**
735
- * Log a status update against a thread. The update shows in the inbox status
736
- * bar and thread update log for every task in the thread.
737
- */
738
- async sendUpdate({ threadId, message, status }) {
1094
+ async sendThreadUpdate({
1095
+ threadId,
1096
+ message,
1097
+ status
1098
+ }) {
739
1099
  if (!threadId) {
740
1100
  throw new RobotRockError("threadId is required to send an update", 400);
741
1101
  }
@@ -768,7 +1128,7 @@ var RobotRock = class {
768
1128
  }
769
1129
  return data.update;
770
1130
  }
771
- async cancelTask(taskId) {
1131
+ async cancelTaskRequest(taskId) {
772
1132
  const response = await fetch(`${this.baseUrl}/tasks/${taskId}/cancel`, {
773
1133
  method: "POST",
774
1134
  headers: {
@@ -811,6 +1171,7 @@ function normalizeSendToHumanInput(task2, clientDefaults) {
811
1171
  threadId: _threadId,
812
1172
  priority: _priority,
813
1173
  update: _update,
1174
+ version: _version,
814
1175
  validUntil,
815
1176
  app: taskApp,
816
1177
  ...rest
@@ -820,44 +1181,12 @@ function normalizeSendToHumanInput(task2, clientDefaults) {
820
1181
  const app = taskApp ?? clientDefaults.app;
821
1182
  return {
822
1183
  ...rest,
823
- version: clientDefaults.version,
1184
+ contextVersion: clientDefaults.contextVersion,
824
1185
  ...app ? { app } : {},
825
1186
  ...validUntil !== void 0 ? { validUntil: serializeValidUntil(validUntil) } : {},
826
1187
  actions: normalizedActions
827
1188
  };
828
1189
  }
829
- async function parseResponseBody(response) {
830
- const contentType = response.headers.get("content-type") ?? "";
831
- const bodyText = await response.text();
832
- if (!bodyText) {
833
- return null;
834
- }
835
- if (contentType.toLowerCase().includes("application/json")) {
836
- try {
837
- return JSON.parse(bodyText);
838
- } catch {
839
- }
840
- }
841
- try {
842
- return JSON.parse(bodyText);
843
- } catch {
844
- return bodyText;
845
- }
846
- }
847
- function getErrorMessage(data, fallback) {
848
- if (data && typeof data === "object" && !Array.isArray(data)) {
849
- const maybeMessage = data.message;
850
- if (typeof maybeMessage === "string" && maybeMessage.trim()) {
851
- return maybeMessage;
852
- }
853
- }
854
- if (typeof data === "string" && data.trim()) {
855
- const compact = data.replace(/\s+/g, " ").trim();
856
- const snippet = compact.length > 180 ? `${compact.slice(0, 180)}...` : compact;
857
- return `${fallback}. Server returned non-JSON response: ${snippet}`;
858
- }
859
- return fallback;
860
- }
861
1190
 
862
1191
  // src/env.ts
863
1192
  var DEFAULT_BASE_URL = "https://api.robotrock.io/v1";
@@ -940,7 +1269,7 @@ var APPROVE_BY_HUMAN_ACTIONS = [
940
1269
  { id: "decline", title: "Decline" }
941
1270
  ];
942
1271
  async function runSendToHuman(payload) {
943
- const { validUntil: validUntilInput, ...taskInput } = payload;
1272
+ const { validUntil: validUntilInput, app, recordOtel, otelIncludeActionData, ...taskFields } = payload;
944
1273
  const { validUntil, timeout } = resolveWaitTiming(validUntilInput);
945
1274
  const token = await wait.createToken({ timeout });
946
1275
  const baseConfig = resolveRobotRockConfig();
@@ -949,40 +1278,68 @@ async function runSendToHuman(payload) {
949
1278
  baseUrl: baseConfig.baseUrl,
950
1279
  ...baseConfig.app ? { app: baseConfig.app } : {},
951
1280
  ...baseConfig.version ? { version: baseConfig.version } : {},
1281
+ ...baseConfig.advanced ? { advanced: baseConfig.advanced } : {},
952
1282
  webhook: { url: token.url }
953
1283
  });
1284
+ const taskInput = stripPlatformOtelFields(taskFields);
954
1285
  const sendResult = await client.sendToHuman({
955
1286
  ...taskInput,
956
- validUntil
1287
+ validUntil,
1288
+ ...app !== void 0 ? { app } : {}
957
1289
  });
958
- const outcome = await wait.forToken(token.id);
959
- if (!outcome.ok) {
960
- throw new Error(`Human response timed out before validUntil (${timeout})`);
961
- }
962
- const output = outcome.output;
963
- if (!isRobotRockHandlerWebhookPayload(output)) {
964
- throw new Error(
965
- "Wait token completed with unexpected payload; expected RobotRock handler body (taskId, action.id, action.data)."
966
- );
967
- }
968
- return toDiscriminatedApprovalResult(
969
- payload.actions,
970
- {
971
- id: output.taskId,
972
- createdAt: /* @__PURE__ */ new Date(),
973
- status: "handled",
974
- context: sendResult.task.context,
975
- validUntil: Date.now(),
976
- handledAt: new Date(output.handledAt).getTime(),
977
- handled: {
978
- action: {
979
- id: output.action.id,
980
- data: output.action.data
981
- },
982
- handledBy: output.handledBy
1290
+ const otelSession = beginRobotRockHumanWaitOtel(sendResult.task, {
1291
+ recordOtel,
1292
+ otelIncludeActionData
1293
+ });
1294
+ let handledPayload = null;
1295
+ let waitOutcome = "pending";
1296
+ try {
1297
+ const outcome = await wait.forToken(token.id);
1298
+ if (!outcome.ok) {
1299
+ waitOutcome = "timeout";
1300
+ throw new Error(`Human response timed out before validUntil (${timeout})`);
1301
+ }
1302
+ const output = outcome.output;
1303
+ if (!isRobotRockHandlerWebhookPayload(output)) {
1304
+ waitOutcome = "error";
1305
+ throw new Error(
1306
+ "Wait token completed with unexpected payload; expected RobotRock handler body (taskId, action.id, action.data)."
1307
+ );
1308
+ }
1309
+ handledPayload = output;
1310
+ waitOutcome = "handled";
1311
+ return toDiscriminatedApprovalResult(
1312
+ payload.actions,
1313
+ {
1314
+ id: output.taskId,
1315
+ createdAt: /* @__PURE__ */ new Date(),
1316
+ status: "handled",
1317
+ context: sendResult.task.context,
1318
+ validUntil: Date.now(),
1319
+ handledAt: new Date(output.handledAt).getTime(),
1320
+ handled: {
1321
+ action: {
1322
+ id: output.action.id,
1323
+ data: output.action.data
1324
+ },
1325
+ handledBy: output.handledBy
1326
+ }
983
1327
  }
1328
+ );
1329
+ } catch (error) {
1330
+ if (waitOutcome === "pending") {
1331
+ waitOutcome = "error";
984
1332
  }
985
- );
1333
+ throw error;
1334
+ } finally {
1335
+ if (waitOutcome !== "pending") {
1336
+ finishRobotRockHumanWaitOtel(
1337
+ otelSession,
1338
+ handledPayload,
1339
+ waitOutcome === "handled" ? "handled" : waitOutcome
1340
+ );
1341
+ }
1342
+ }
986
1343
  }
987
1344
  var sendToHumanTask = task({
988
1345
  id: "robotrock/send-to-human",