@agentuity/core 0.1.16 → 0.1.18

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.
@@ -2,6 +2,21 @@ import { FetchAdapter } from './adapter';
2
2
  import { buildUrl, toServiceException, toPayload } from './_util';
3
3
  import { StructuredError } from '../error';
4
4
 
5
+ /**
6
+ * Minimum TTL value in seconds (1 minute)
7
+ */
8
+ export const KV_MIN_TTL_SECONDS = 60;
9
+
10
+ /**
11
+ * Maximum TTL value in seconds (90 days)
12
+ */
13
+ export const KV_MAX_TTL_SECONDS = 7776000;
14
+
15
+ /**
16
+ * Default TTL value in seconds (7 days) - used when namespace is auto-created or no TTL specified
17
+ */
18
+ export const KV_DEFAULT_TTL_SECONDS = 604800;
19
+
5
20
  /**
6
21
  * the result of a data operation when the data is found
7
22
  */
@@ -49,6 +64,23 @@ export interface KeyValueStorageSetParams {
49
64
  contentType?: string;
50
65
  }
51
66
 
67
+ /**
68
+ * Parameters for creating a namespace
69
+ */
70
+ export interface CreateNamespaceParams {
71
+ /**
72
+ * Default TTL for keys in this namespace (in seconds).
73
+ * - If undefined/omitted: uses server default (7 days / 604,800 seconds)
74
+ * - If 0: keys will not expire by default
75
+ * - If 60-7,776,000: custom TTL in seconds (1 minute to 90 days)
76
+ *
77
+ * Keys can override this default by specifying TTL in the set() call.
78
+ * Active keys are automatically extended (sliding expiration) when read
79
+ * if their remaining TTL is less than 50% of the original TTL.
80
+ */
81
+ defaultTTLSeconds?: number;
82
+ }
83
+
52
84
  /**
53
85
  * Statistics for a key-value store namespace
54
86
  */
@@ -70,6 +102,46 @@ export interface KeyValueItemWithMetadata<T = unknown> {
70
102
  updated_at: string;
71
103
  }
72
104
 
105
+ /**
106
+ * Parameters for getting all namespace statistics with optional pagination
107
+ */
108
+ export interface GetAllStatsParams {
109
+ /**
110
+ * Maximum number of namespaces to return (default: 100, max: 1000)
111
+ */
112
+ limit?: number;
113
+ /**
114
+ * Number of namespaces to skip for pagination (default: 0)
115
+ */
116
+ offset?: number;
117
+ }
118
+
119
+ /**
120
+ * Paginated response for namespace statistics
121
+ */
122
+ export interface KeyValueStatsPaginated {
123
+ /**
124
+ * Map of namespace names to their statistics
125
+ */
126
+ namespaces: Record<string, KeyValueStats>;
127
+ /**
128
+ * Total number of namespaces across all pages
129
+ */
130
+ total: number;
131
+ /**
132
+ * Number of namespaces requested per page
133
+ */
134
+ limit: number;
135
+ /**
136
+ * Number of namespaces skipped
137
+ */
138
+ offset: number;
139
+ /**
140
+ * Whether there are more namespaces available
141
+ */
142
+ hasMore: boolean;
143
+ }
144
+
73
145
  export interface KeyValueStorage {
74
146
  /**
75
147
  * get a value from the key value storage
@@ -114,14 +186,20 @@ export interface KeyValueStorage {
114
186
  /**
115
187
  * get statistics for all namespaces
116
188
  *
117
- * @returns map of namespace names to statistics
189
+ * @param params - optional pagination parameters
190
+ * @returns map of namespace names to statistics, or paginated response if params provided
118
191
  */
119
- getAllStats(): Promise<Record<string, KeyValueStats>>;
192
+ getAllStats(params?: GetAllStatsParams): Promise<Record<string, KeyValueStats> | KeyValueStatsPaginated>;
120
193
 
121
194
  /**
122
195
  * get all namespace names
123
196
  *
124
- * @returns array of namespace names
197
+ * @returns array of namespace names (up to 1000)
198
+ *
199
+ * @remarks
200
+ * This method returns a maximum of 1000 namespace names.
201
+ * If you have more than 1000 namespaces, only the first 1000
202
+ * (ordered by creation date, most recent first) will be returned.
125
203
  */
126
204
  getNamespaces(): Promise<string[]>;
127
205
 
@@ -156,8 +234,9 @@ export interface KeyValueStorage {
156
234
  * create a new namespace
157
235
  *
158
236
  * @param name - the name of the key value storage to create
237
+ * @param params - optional parameters including default TTL
159
238
  */
160
- createNamespace(name: string): Promise<void>;
239
+ createNamespace(name: string, params?: CreateNamespaceParams): Promise<void>;
161
240
  }
162
241
 
163
242
  const KeyValueInvalidTTLError = StructuredError('KeyValueInvalidTTLError');
@@ -201,6 +280,21 @@ export class KeyValueStorageService implements KeyValueStorage {
201
280
  throw await toServiceException('GET', url, res.response);
202
281
  }
203
282
 
283
+ /**
284
+ * set a value in the key value storage
285
+ *
286
+ * @param name - the name of the key value storage
287
+ * @param key - the key to set the value of
288
+ * @param value - the value to set in any of the supported data types
289
+ * @param params - the KeyValueStorageSetParams
290
+ *
291
+ * @remarks
292
+ * TTL behavior:
293
+ * - If TTL is not specified, the key inherits the namespace's default TTL
294
+ * - TTL values below 60 seconds are clamped to 60 seconds
295
+ * - TTL values above 7,776,000 seconds (90 days) are clamped to 90 days
296
+ * - If the namespace doesn't exist, it is auto-created with a 7-day default TTL
297
+ */
204
298
  async set<T = unknown>(
205
299
  name: string,
206
300
  key: string,
@@ -282,10 +376,18 @@ export class KeyValueStorageService implements KeyValueStorage {
282
376
  throw await toServiceException('GET', url, res.response);
283
377
  }
284
378
 
285
- async getAllStats(): Promise<Record<string, KeyValueStats>> {
286
- const url = buildUrl(this.#baseUrl, '/kv/2025-03-17/stats');
379
+ async getAllStats(params?: GetAllStatsParams): Promise<Record<string, KeyValueStats> | KeyValueStatsPaginated> {
380
+ const queryParams = new URLSearchParams();
381
+ if (params?.limit !== undefined) {
382
+ queryParams.set('limit', String(params.limit));
383
+ }
384
+ if (params?.offset !== undefined) {
385
+ queryParams.set('offset', String(params.offset));
386
+ }
387
+ const queryString = queryParams.toString();
388
+ const url = buildUrl(this.#baseUrl, `/kv/2025-03-17/stats${queryString ? `?${queryString}` : ''}`);
287
389
  const signal = AbortSignal.timeout(10_000);
288
- const res = await this.#adapter.invoke<Record<string, KeyValueStats>>(url, {
390
+ const res = await this.#adapter.invoke<Record<string, KeyValueStats> | KeyValueStatsPaginated>(url, {
289
391
  method: 'GET',
290
392
  signal,
291
393
  telemetry: {
@@ -300,8 +402,20 @@ export class KeyValueStorageService implements KeyValueStorage {
300
402
  }
301
403
 
302
404
  async getNamespaces(): Promise<string[]> {
303
- const stats = await this.getAllStats();
304
- return Object.keys(stats);
405
+ const url = buildUrl(this.#baseUrl, '/kv/2025-03-17/namespaces');
406
+ const signal = AbortSignal.timeout(10_000);
407
+ const res = await this.#adapter.invoke<string[]>(url, {
408
+ method: 'GET',
409
+ signal,
410
+ telemetry: {
411
+ name: 'agentuity.keyvalue.getNamespaces',
412
+ attributes: {},
413
+ },
414
+ });
415
+ if (res.ok) {
416
+ return res.data;
417
+ }
418
+ throw await toServiceException('GET', url, res.response);
305
419
  }
306
420
 
307
421
  async search<T = unknown>(
@@ -361,12 +475,18 @@ export class KeyValueStorageService implements KeyValueStorage {
361
475
  throw await toServiceException('DELETE', url, res.response);
362
476
  }
363
477
 
364
- async createNamespace(name: string): Promise<void> {
478
+ async createNamespace(name: string, params?: CreateNamespaceParams): Promise<void> {
365
479
  const url = buildUrl(this.#baseUrl, `/kv/2025-03-17/${encodeURIComponent(name)}`);
366
480
  const signal = AbortSignal.timeout(10_000);
481
+
482
+ const body = params?.defaultTTLSeconds !== undefined
483
+ ? JSON.stringify({ default_ttl_seconds: params.defaultTTLSeconds })
484
+ : undefined;
485
+
367
486
  const res = await this.#adapter.invoke(url, {
368
487
  method: 'POST',
369
488
  signal,
489
+ ...(body && { body, contentType: 'application/json' }),
370
490
  telemetry: {
371
491
  name: 'agentuity.keyvalue.createNamespace',
372
492
  attributes: { name },
@@ -0,0 +1,384 @@
1
+ /**
2
+ * @module queue
3
+ *
4
+ * Queue service for publishing messages to Agentuity queues.
5
+ *
6
+ * This module provides a simplified interface for agents to publish messages
7
+ * to queues. For full queue management (CRUD, consume, acknowledge), use
8
+ * the `@agentuity/server` package.
9
+ *
10
+ * @example Publishing from an agent
11
+ * ```typescript
12
+ * // Inside an agent handler
13
+ * const result = await ctx.queue.publish('order-queue', {
14
+ * orderId: 123,
15
+ * action: 'process',
16
+ * });
17
+ * console.log(`Published message ${result.id}`);
18
+ * ```
19
+ */
20
+
21
+ import { FetchAdapter } from './adapter';
22
+ import { buildUrl, toServiceException, toPayload } from './_util';
23
+ import { StructuredError } from '../error';
24
+
25
+ /**
26
+ * Parameters for publishing a message to a queue.
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const params: QueuePublishParams = {
31
+ * metadata: { priority: 'high' },
32
+ * partitionKey: 'customer-123',
33
+ * idempotencyKey: 'order-456-v1',
34
+ * ttl: 3600, // 1 hour
35
+ * };
36
+ * ```
37
+ */
38
+ export interface QueuePublishParams {
39
+ /**
40
+ * Optional metadata to attach to the message.
41
+ * Can contain any JSON-serializable data for message routing or filtering.
42
+ */
43
+ metadata?: Record<string, unknown>;
44
+
45
+ /**
46
+ * Optional partition key for message ordering.
47
+ * Messages with the same partition key are guaranteed to be processed in order.
48
+ */
49
+ partitionKey?: string;
50
+
51
+ /**
52
+ * Optional idempotency key for deduplication.
53
+ * If a message with the same key was recently published, it will be deduplicated.
54
+ */
55
+ idempotencyKey?: string;
56
+
57
+ /**
58
+ * Optional time-to-live in seconds.
59
+ * Messages will expire and be removed after this duration.
60
+ */
61
+ ttl?: number;
62
+
63
+ /**
64
+ * Optional project ID for cross-project publishing.
65
+ * If not specified, uses the current project context.
66
+ */
67
+ projectId?: string;
68
+
69
+ /**
70
+ * Optional agent ID for attribution.
71
+ * If not specified, uses the current agent context.
72
+ */
73
+ agentId?: string;
74
+ }
75
+
76
+ /**
77
+ * Result of publishing a message to a queue.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * const result = await queue.publish('my-queue', payload);
82
+ * console.log(`Message ${result.id} published at offset ${result.offset}`);
83
+ * ```
84
+ */
85
+ export interface QueuePublishResult {
86
+ /**
87
+ * The unique message ID (prefixed with msg_).
88
+ * Use this ID to track, acknowledge, or delete the message.
89
+ */
90
+ id: string;
91
+
92
+ /**
93
+ * The sequential offset of the message in the queue.
94
+ * Offsets are monotonically increasing and can be used for log-style consumption.
95
+ */
96
+ offset: number;
97
+
98
+ /**
99
+ * ISO 8601 timestamp when the message was published.
100
+ */
101
+ publishedAt: string;
102
+ }
103
+
104
+ /**
105
+ * Queue service interface for publishing messages.
106
+ *
107
+ * This is the interface available to agents via `ctx.queue`. It provides
108
+ * a simple publish-only interface suitable for agent workflows.
109
+ *
110
+ * For full queue management (create queues, consume messages, manage destinations),
111
+ * use the `@agentuity/server` package.
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * // In an agent handler
116
+ * export default createAgent('my-agent', {
117
+ * handler: async (ctx, input) => {
118
+ * // Publish a message to a queue
119
+ * await ctx.queue.publish('notifications', {
120
+ * type: 'email',
121
+ * to: input.email,
122
+ * subject: 'Welcome!',
123
+ * });
124
+ * return { success: true };
125
+ * },
126
+ * });
127
+ * ```
128
+ */
129
+ export interface QueueService {
130
+ /**
131
+ * Publish a message to a queue.
132
+ *
133
+ * The payload can be a string or an object. Objects are automatically
134
+ * JSON-stringified before publishing.
135
+ *
136
+ * @param queueName - The name of the queue to publish to
137
+ * @param payload - The message payload (string or JSON-serializable object)
138
+ * @param params - Optional publish parameters (metadata, TTL, etc.)
139
+ * @returns The publish result with message ID and offset
140
+ * @throws {QueueNotFoundError} If the queue does not exist
141
+ * @throws {QueueValidationError} If validation fails (invalid name, payload too large, etc.)
142
+ * @throws {QueuePublishError} If the publish operation fails
143
+ *
144
+ * @example Publishing a simple message
145
+ * ```typescript
146
+ * const result = await ctx.queue.publish('my-queue', 'Hello, World!');
147
+ * ```
148
+ *
149
+ * @example Publishing with options
150
+ * ```typescript
151
+ * const result = await ctx.queue.publish('my-queue', { task: 'process' }, {
152
+ * metadata: { priority: 'high' },
153
+ * idempotencyKey: 'task-123',
154
+ * ttl: 3600,
155
+ * });
156
+ * ```
157
+ */
158
+ publish(
159
+ queueName: string,
160
+ payload: string | object,
161
+ params?: QueuePublishParams
162
+ ): Promise<QueuePublishResult>;
163
+ }
164
+
165
+ // ============================================================================
166
+ // Errors
167
+ // ============================================================================
168
+
169
+ /**
170
+ * Error thrown when a publish operation fails.
171
+ *
172
+ * This is a general error for publish failures that aren't specifically
173
+ * validation or not-found errors.
174
+ */
175
+ export const QueuePublishError = StructuredError('QueuePublishError');
176
+
177
+ /**
178
+ * Error thrown when a queue is not found.
179
+ *
180
+ * @example
181
+ * ```typescript
182
+ * try {
183
+ * await ctx.queue.publish('non-existent', 'payload');
184
+ * } catch (error) {
185
+ * if (error instanceof QueueNotFoundError) {
186
+ * console.error('Queue does not exist');
187
+ * }
188
+ * }
189
+ * ```
190
+ */
191
+ export const QueueNotFoundError = StructuredError('QueueNotFoundError');
192
+
193
+ /**
194
+ * Error thrown when validation fails.
195
+ *
196
+ * Contains the field name and optionally the invalid value for debugging.
197
+ */
198
+ export const QueueValidationError = StructuredError('QueueValidationError')<{
199
+ /** The field that failed validation */
200
+ field: string;
201
+ /** The invalid value (for debugging) */
202
+ value?: unknown;
203
+ }>();
204
+
205
+ // ============================================================================
206
+ // Internal Validation
207
+ // ============================================================================
208
+
209
+ const MAX_QUEUE_NAME_LENGTH = 256;
210
+ const MAX_PAYLOAD_SIZE = 1048576;
211
+ const MAX_PARTITION_KEY_LENGTH = 256;
212
+ const MAX_IDEMPOTENCY_KEY_LENGTH = 256;
213
+ const VALID_QUEUE_NAME_REGEX = /^[a-z_][a-z0-9_-]*$/;
214
+
215
+ /** @internal */
216
+ function validateQueueNameInternal(name: string): void {
217
+ if (!name || name.length === 0) {
218
+ throw new QueueValidationError({
219
+ message: 'Queue name cannot be empty',
220
+ field: 'queueName',
221
+ value: name,
222
+ });
223
+ }
224
+ if (name.length > MAX_QUEUE_NAME_LENGTH) {
225
+ throw new QueueValidationError({
226
+ message: `Queue name must not exceed ${MAX_QUEUE_NAME_LENGTH} characters`,
227
+ field: 'queueName',
228
+ value: name,
229
+ });
230
+ }
231
+ if (!VALID_QUEUE_NAME_REGEX.test(name)) {
232
+ throw new QueueValidationError({
233
+ message:
234
+ 'Queue name must start with a letter or underscore and contain only lowercase letters, digits, underscores, and hyphens',
235
+ field: 'queueName',
236
+ value: name,
237
+ });
238
+ }
239
+ }
240
+
241
+ /** @internal */
242
+ function validatePayloadInternal(payload: string): void {
243
+ if (!payload || payload.length === 0) {
244
+ throw new QueueValidationError({
245
+ message: 'Payload cannot be empty',
246
+ field: 'payload',
247
+ });
248
+ }
249
+ if (payload.length > MAX_PAYLOAD_SIZE) {
250
+ throw new QueueValidationError({
251
+ message: `Payload size exceeds ${MAX_PAYLOAD_SIZE} byte limit (${payload.length} bytes)`,
252
+ field: 'payload',
253
+ value: payload.length,
254
+ });
255
+ }
256
+ }
257
+
258
+ // ============================================================================
259
+ // QueueStorageService Implementation
260
+ // ============================================================================
261
+
262
+ /**
263
+ * HTTP-based implementation of the QueueService interface.
264
+ *
265
+ * This service communicates with the Agentuity Queue API to publish messages.
266
+ * It is automatically configured and available via `ctx.queue` in agent handlers.
267
+ *
268
+ * @internal This class is instantiated by the runtime; use `ctx.queue` instead.
269
+ */
270
+ export class QueueStorageService implements QueueService {
271
+ #adapter: FetchAdapter;
272
+ #baseUrl: string;
273
+
274
+ /**
275
+ * Creates a new QueueStorageService.
276
+ *
277
+ * @param baseUrl - The base URL of the Queue API
278
+ * @param adapter - The fetch adapter for making HTTP requests
279
+ */
280
+ constructor(baseUrl: string, adapter: FetchAdapter) {
281
+ this.#adapter = adapter;
282
+ this.#baseUrl = baseUrl;
283
+ }
284
+
285
+ /**
286
+ * @inheritdoc
287
+ */
288
+ async publish(
289
+ queueName: string,
290
+ payload: string | object,
291
+ params?: QueuePublishParams
292
+ ): Promise<QueuePublishResult> {
293
+ // Validate inputs before sending to API
294
+ validateQueueNameInternal(queueName);
295
+
296
+ const [body] = await toPayload(payload);
297
+ const payloadStr = typeof payload === 'string' ? payload : (body as string);
298
+ validatePayloadInternal(payloadStr);
299
+
300
+ // Validate optional params
301
+ if (params?.partitionKey && params.partitionKey.length > MAX_PARTITION_KEY_LENGTH) {
302
+ throw new QueueValidationError({
303
+ message: `Partition key must not exceed ${MAX_PARTITION_KEY_LENGTH} characters`,
304
+ field: 'partitionKey',
305
+ value: params.partitionKey.length,
306
+ });
307
+ }
308
+ if (params?.idempotencyKey && params.idempotencyKey.length > MAX_IDEMPOTENCY_KEY_LENGTH) {
309
+ throw new QueueValidationError({
310
+ message: `Idempotency key must not exceed ${MAX_IDEMPOTENCY_KEY_LENGTH} characters`,
311
+ field: 'idempotencyKey',
312
+ value: params.idempotencyKey.length,
313
+ });
314
+ }
315
+ if (params?.ttl !== undefined && params.ttl < 0) {
316
+ throw new QueueValidationError({
317
+ message: 'TTL cannot be negative',
318
+ field: 'ttl',
319
+ value: params.ttl,
320
+ });
321
+ }
322
+
323
+ const url = buildUrl(
324
+ this.#baseUrl,
325
+ `/queue/messages/publish/2026-01-15/${encodeURIComponent(queueName)}`
326
+ );
327
+
328
+ const requestBody: Record<string, unknown> = {
329
+ payload: typeof payload === 'string' ? payload : body,
330
+ };
331
+
332
+ if (params?.metadata) {
333
+ requestBody.metadata = params.metadata;
334
+ }
335
+ if (params?.partitionKey) {
336
+ requestBody.partition_key = params.partitionKey;
337
+ }
338
+ if (params?.idempotencyKey) {
339
+ requestBody.idempotency_key = params.idempotencyKey;
340
+ }
341
+ if (params?.ttl !== undefined) {
342
+ requestBody.ttl_seconds = params.ttl;
343
+ }
344
+ if (params?.projectId) {
345
+ requestBody.project_id = params.projectId;
346
+ }
347
+ if (params?.agentId) {
348
+ requestBody.agent_id = params.agentId;
349
+ }
350
+
351
+ const signal = AbortSignal.timeout(30_000);
352
+ const res = await this.#adapter.invoke<QueuePublishResult>(url, {
353
+ method: 'POST',
354
+ signal,
355
+ body: JSON.stringify(requestBody),
356
+ contentType: 'application/json',
357
+ telemetry: {
358
+ name: 'agentuity.queue.publish',
359
+ attributes: {
360
+ queueName,
361
+ },
362
+ },
363
+ });
364
+
365
+ if (res.ok) {
366
+ const data = res.data as unknown as {
367
+ message: { id: string; offset: number; published_at: string };
368
+ };
369
+ return {
370
+ id: data.message.id,
371
+ offset: data.message.offset,
372
+ publishedAt: data.message.published_at,
373
+ };
374
+ }
375
+
376
+ if (res.response.status === 404) {
377
+ throw new QueueNotFoundError({
378
+ message: `Queue not found: ${queueName}`,
379
+ });
380
+ }
381
+
382
+ throw await toServiceException('POST', url, res.response);
383
+ }
384
+ }
@@ -507,6 +507,11 @@ export interface SandboxInfo {
507
507
  */
508
508
  runtimeIconUrl?: string;
509
509
 
510
+ /**
511
+ * Runtime brand color (hex color code)
512
+ */
513
+ runtimeBrandColor?: string;
514
+
510
515
  /**
511
516
  * Snapshot ID this sandbox was created from
512
517
  */
@@ -522,6 +527,11 @@ export interface SandboxInfo {
522
527
  */
523
528
  executions: number;
524
529
 
530
+ /**
531
+ * Exit code from the last execution (only available for terminated/failed sandboxes)
532
+ */
533
+ exitCode?: number;
534
+
525
535
  /**
526
536
  * URL to the stdout output stream
527
537
  */