@lobu/core 3.0.5 → 3.0.6

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.
@@ -0,0 +1,200 @@
1
+ import type Redis from "ioredis";
2
+ import { createLogger, type Logger } from "../logger";
3
+ import { safeJsonParse, safeJsonStringify } from "../utils/json";
4
+
5
+ function errMsg(error: unknown): string {
6
+ return error instanceof Error ? error.message : String(error);
7
+ }
8
+
9
+ export interface RedisStoreConfig {
10
+ redis: Redis;
11
+ keyPrefix: string;
12
+ loggerName: string;
13
+ }
14
+
15
+ /**
16
+ * Base class for all Redis-backed stores
17
+ * Provides common CRUD operations with JSON serialization
18
+ *
19
+ * Consolidates:
20
+ * - packages/gateway/src/auth/credential-store.ts (BaseCredentialStore)
21
+ * - packages/gateway/src/infrastructure/redis/store.ts (BaseRedisStore)
22
+ */
23
+ export abstract class BaseRedisStore<T> {
24
+ protected logger: Logger;
25
+ protected redis: Redis;
26
+ protected keyPrefix: string;
27
+
28
+ constructor(config: RedisStoreConfig) {
29
+ this.redis = config.redis;
30
+ this.keyPrefix = config.keyPrefix;
31
+ this.logger = createLogger(config.loggerName);
32
+ }
33
+
34
+ /**
35
+ * Build Redis key from parts
36
+ */
37
+ protected buildKey(...parts: string[]): string {
38
+ return [this.keyPrefix, ...parts].join(":");
39
+ }
40
+
41
+ /**
42
+ * Get value from Redis
43
+ * Returns null if not found or validation fails
44
+ */
45
+ protected async get(key: string): Promise<T | null> {
46
+ try {
47
+ const data = await this.redis.get(key);
48
+ if (!data) {
49
+ return null;
50
+ }
51
+
52
+ const value = this.deserialize(data);
53
+
54
+ // Validate after deserialization
55
+ if (!this.validate(value)) {
56
+ this.logger.warn("Invalid data after deserialization", { key });
57
+ return null;
58
+ }
59
+
60
+ return value;
61
+ } catch (error) {
62
+ this.logger.error("Failed to get from Redis", {
63
+ error: errMsg(error),
64
+ key,
65
+ });
66
+ return null;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Set value in Redis
72
+ */
73
+ protected async set(
74
+ key: string,
75
+ value: T,
76
+ ttlSeconds?: number
77
+ ): Promise<void> {
78
+ try {
79
+ const data = this.serialize(value);
80
+ if (ttlSeconds) {
81
+ await this.redis.setex(key, ttlSeconds, data);
82
+ } else {
83
+ await this.redis.set(key, data);
84
+ }
85
+ } catch (error) {
86
+ this.logger.error("Failed to set in Redis", {
87
+ error: errMsg(error),
88
+ key,
89
+ });
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Delete value from Redis
96
+ */
97
+ protected async delete(key: string): Promise<void> {
98
+ try {
99
+ await this.redis.del(key);
100
+ } catch (error) {
101
+ this.logger.error("Failed to delete from Redis", {
102
+ error: errMsg(error),
103
+ key,
104
+ });
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Scan for all keys matching a prefix (cursor-based, production-safe).
110
+ * Returns full key strings matching `{prefix}*`.
111
+ */
112
+ protected async scanByPrefix(prefix: string): Promise<string[]> {
113
+ const results: string[] = [];
114
+ let cursor = "0";
115
+ try {
116
+ do {
117
+ const [nextCursor, keys] = await this.redis.scan(
118
+ cursor,
119
+ "MATCH",
120
+ `${prefix}*`,
121
+ "COUNT",
122
+ 100
123
+ );
124
+ cursor = nextCursor;
125
+ results.push(...keys);
126
+ } while (cursor !== "0");
127
+ } catch (error) {
128
+ this.logger.error("Failed to scan by prefix", {
129
+ error: errMsg(error),
130
+ prefix,
131
+ });
132
+ }
133
+ return results;
134
+ }
135
+
136
+ /**
137
+ * Check if key exists in Redis
138
+ */
139
+ protected async exists(key: string): Promise<boolean> {
140
+ try {
141
+ const result = await this.redis.exists(key);
142
+ return result === 1;
143
+ } catch (error) {
144
+ this.logger.error("Failed to check existence in Redis", {
145
+ error: errMsg(error),
146
+ key,
147
+ });
148
+ return false;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Serialize value to string
154
+ * Override for custom serialization
155
+ */
156
+ protected serialize(value: T): string {
157
+ const result = safeJsonStringify(value);
158
+ if (result === null) {
159
+ throw new Error("Failed to serialize value to JSON");
160
+ }
161
+ return result;
162
+ }
163
+
164
+ /**
165
+ * Deserialize string to value
166
+ * Override for custom deserialization
167
+ */
168
+ protected deserialize(data: string): T {
169
+ const result = safeJsonParse<T>(data);
170
+ if (result === null) {
171
+ throw new Error("Failed to deserialize JSON data");
172
+ }
173
+ return result;
174
+ }
175
+
176
+ /**
177
+ * Validate value after deserialization
178
+ * Override to add custom validation logic
179
+ * Return false to reject invalid data
180
+ */
181
+ protected validate(_value: T): boolean {
182
+ return true;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Specialized base for credential stores
188
+ * Validates that accessToken field exists
189
+ */
190
+ export abstract class BaseCredentialStore<
191
+ T extends { accessToken: string },
192
+ > extends BaseRedisStore<T> {
193
+ protected override validate(value: T): boolean {
194
+ if (!value.accessToken) {
195
+ this.logger.warn("Invalid credentials: missing accessToken");
196
+ return false;
197
+ }
198
+ return true;
199
+ }
200
+ }
package/src/sentry.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { createLogger, type Logger } from "./logger";
2
+
3
+ // Lazy logger initialization to avoid circular dependency
4
+ let _logger: Logger | null = null;
5
+ function getLogger(): Logger {
6
+ if (!_logger) {
7
+ _logger = createLogger("sentry");
8
+ }
9
+ return _logger;
10
+ }
11
+
12
+ let sentryInstance: typeof import("@sentry/node") | null = null;
13
+
14
+ /**
15
+ * Initialize Sentry with configuration from environment variables
16
+ * Falls back to hardcoded DSN if SENTRY_DSN is not provided
17
+ * Uses dynamic import to avoid module resolution issues in dev mode
18
+ */
19
+ export async function initSentry() {
20
+ try {
21
+ const Sentry = await import("@sentry/node");
22
+ sentryInstance = Sentry;
23
+
24
+ const sentryDsn =
25
+ process.env.SENTRY_DSN ||
26
+ "https://c5910e58d1a134d64ff93a95a9c535bb@o4507291398897664.ingest.us.sentry.io/4511097466781696";
27
+
28
+ Sentry.init({
29
+ dsn: sentryDsn,
30
+ sendDefaultPii: true,
31
+ profileSessionSampleRate: 1.0,
32
+ tracesSampleRate: 1.0, // Capture 100% of traces for better visibility
33
+ integrations: [
34
+ Sentry.consoleLoggingIntegration({ levels: ["log", "warn", "error"] }),
35
+ Sentry.redisIntegration(),
36
+ ],
37
+ });
38
+
39
+ getLogger().debug("Sentry monitoring initialized");
40
+ } catch (error) {
41
+ getLogger().warn(
42
+ "⚠️ Sentry initialization failed (continuing without monitoring):",
43
+ error
44
+ );
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Get the initialized Sentry instance
50
+ * @returns Sentry instance or null if not initialized
51
+ */
52
+ export function getSentry(): typeof import("@sentry/node") | null {
53
+ return sentryInstance;
54
+ }
package/src/trace.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Trace ID utilities for end-to-end message lifecycle observability.
3
+ * Trace IDs propagate through the entire pipeline:
4
+ * [WhatsApp Message] -> [Queue] -> [Worker Creation] -> [PVC Setup] -> [Agent Runtime] -> [Response]
5
+ *
6
+ * When OpenTelemetry is initialized, spans are sent to Tempo for waterfall visualization.
7
+ * Use createSpan/createChildSpan from ./otel.ts for actual span creation.
8
+ */
9
+
10
+ /**
11
+ * Generate a trace ID from a message ID.
12
+ * Format: tr-{messageId prefix}-{timestamp base36}-{random}
13
+ * Example: tr-abc12345-lx4k-a3b2
14
+ */
15
+ export function generateTraceId(messageId: string): string {
16
+ const timestamp = Date.now().toString(36);
17
+ const random = Math.random().toString(36).substring(2, 6);
18
+ // Take first 8 chars of messageId, sanitize for safe logging
19
+ const shortMessageId = messageId.replace(/[^a-zA-Z0-9]/g, "").substring(0, 8);
20
+ return `tr-${shortMessageId}-${timestamp}-${random}`;
21
+ }
22
+
23
+ /**
24
+ * Extract trace ID from various payload formats.
25
+ * Checks both top-level and nested platformMetadata.
26
+ */
27
+ export function extractTraceId(payload: {
28
+ traceId?: string;
29
+ platformMetadata?: { traceId?: string };
30
+ }): string | undefined {
31
+ return payload?.traceId || payload?.platformMetadata?.traceId;
32
+ }
package/src/types.ts ADDED
@@ -0,0 +1,430 @@
1
+ // ============================================================================
2
+ // Provider Catalog Types
3
+ // ============================================================================
4
+
5
+ /**
6
+ * Represents a provider installed for a specific agent.
7
+ * Stored in AgentSettings.installedProviders as an ordered array (index 0 = primary).
8
+ */
9
+ export interface InstalledProvider {
10
+ providerId: string; // "claude", "chatgpt", "gemini", "z-ai"
11
+ installedAt: number;
12
+ config?: {
13
+ baseUrl?: string; // override upstream (e.g. z.ai proxy)
14
+ [key: string]: unknown;
15
+ };
16
+ }
17
+
18
+ /**
19
+ * CLI backend configuration for pi-agent integration.
20
+ * Providers can ship CLI tools that pi-agent invokes as backends.
21
+ */
22
+ export interface CliBackendConfig {
23
+ name: string; // "claude-code", "codex"
24
+ command: string; // "/usr/local/bin/claude"
25
+ args?: string[];
26
+ env?: Record<string, string>;
27
+ modelArg?: string; // "--model"
28
+ sessionArg?: string; // "--session"
29
+ }
30
+
31
+ // ============================================================================
32
+ // Auth Profile Types
33
+ // ============================================================================
34
+
35
+ /**
36
+ * Unified authentication profile for any model provider.
37
+ * Stored in AgentSettings.authProfiles as an ordered array (index 0 = primary).
38
+ */
39
+ export interface AuthProfile {
40
+ id: string; // UUID
41
+ provider: string; // "anthropic", "openai-codex", "gemini", "nvidia"
42
+ model: string; // Full model ref: "openai-codex/gpt-5.2-codex"
43
+ credential: string; // API key or OAuth access token
44
+ label: string; // "user@gmail.com", "sk-ant-...1234"
45
+ authType: "oauth" | "device-code" | "api-key";
46
+ metadata?: {
47
+ email?: string;
48
+ expiresAt?: number;
49
+ refreshToken?: string;
50
+ accountId?: string;
51
+ };
52
+ createdAt: number;
53
+ }
54
+
55
+ export interface SessionContext {
56
+ // Core identifiers
57
+ platform: string; // Platform identifier (e.g., "slack", "discord", "teams")
58
+ channelId: string;
59
+ userId: string;
60
+ messageId: string; // Required - always needed for tracking
61
+
62
+ // Optional context
63
+ conversationId?: string;
64
+ teamId?: string; // Platform workspace/team identifier
65
+ userDisplayName?: string; // For logging/display purposes
66
+ workingDirectory?: string;
67
+ customInstructions?: string;
68
+ conversationHistory?: ConversationMessage[];
69
+ }
70
+
71
+ export interface ConversationMessage {
72
+ role: "system" | "user" | "assistant";
73
+ content: string;
74
+ timestamp: number;
75
+ }
76
+
77
+ // ============================================================================
78
+ // Conversation History Types
79
+ // ============================================================================
80
+
81
+ // ============================================================================
82
+ // Skills Configuration Types
83
+ // ============================================================================
84
+
85
+ /**
86
+ * Per-skill thinking budget level.
87
+ * Controls how much reasoning the model applies when executing a skill.
88
+ */
89
+ export type ThinkingLevel = "off" | "low" | "medium" | "high";
90
+
91
+ /**
92
+ * MCP server declared by a skill manifest.
93
+ */
94
+ export interface SkillMcpServer {
95
+ id: string;
96
+ name?: string;
97
+ url?: string;
98
+ type?: "sse" | "stdio";
99
+ command?: string;
100
+ args?: string[];
101
+ oauth?: {
102
+ authUrl: string;
103
+ tokenUrl: string;
104
+ clientId: string;
105
+ clientSecret?: string;
106
+ scopes?: string[];
107
+ grantType?: string;
108
+ responseType?: string;
109
+ };
110
+ resource?: string;
111
+ inputs?: Array<{ id: string; label?: string; type?: string }>;
112
+ headers?: Record<string, string>;
113
+ }
114
+
115
+ /**
116
+ * Individual skill configuration.
117
+ * Skills are SKILL.md files from GitHub repos that provide instructions to Claude.
118
+ */
119
+ export interface SkillConfig {
120
+ /** Skill repository in owner/repo format (e.g., "anthropics/skills/pdf") */
121
+ repo: string;
122
+ /** Skill name derived from SKILL.md frontmatter or folder name */
123
+ name: string;
124
+ /** Optional description from SKILL.md frontmatter */
125
+ description?: string;
126
+ /** Short always-inlined instruction block for critical rules */
127
+ instructions?: string;
128
+ /** Whether this skill is currently enabled */
129
+ enabled: boolean;
130
+ /** True for system-defined skills (from system-skills.json). Cannot be removed by users. */
131
+ system?: boolean;
132
+ /** Cached SKILL.md content (fetched from GitHub) */
133
+ content?: string;
134
+ /** When the content was last fetched (timestamp ms) */
135
+ contentFetchedAt?: number;
136
+ /** MCP servers declared by the skill */
137
+ mcpServers?: SkillMcpServer[];
138
+ /** System packages declared by the skill (nix) */
139
+ nixPackages?: string[];
140
+ /** Network domains the skill needs access to (legacy flat list) */
141
+ permissions?: string[];
142
+ /** Network access policy declared by the skill */
143
+ networkConfig?: { allowedDomains?: string[]; deniedDomains?: string[] };
144
+ /** Tool permission policy declared by the skill */
145
+ toolPermissions?: { allow?: string[]; deny?: string[] };
146
+ /** AI providers the skill requires */
147
+ providers?: string[];
148
+ /** Preferred model for this skill (e.g., "anthropic/claude-opus-4") */
149
+ modelPreference?: string;
150
+ /** Thinking level budget for this skill */
151
+ thinkingLevel?: ThinkingLevel;
152
+ }
153
+
154
+ /**
155
+ * Skills configuration for agent settings.
156
+ * Contains list of configured skills that can be enabled/disabled.
157
+ */
158
+ export interface SkillsConfig {
159
+ /** List of configured skills */
160
+ skills: SkillConfig[];
161
+ }
162
+
163
+ /**
164
+ * Platform-agnostic history message format.
165
+ * Used to pass conversation history to workers.
166
+ */
167
+ export interface HistoryMessage {
168
+ role: "user" | "assistant";
169
+ content: string;
170
+ timestamp: number;
171
+ /** Display name of the message sender */
172
+ userName?: string;
173
+ /** Platform-specific message ID for deduplication */
174
+ messageId?: string;
175
+ }
176
+
177
+ /**
178
+ * Network configuration for worker sandbox isolation.
179
+ * Controls which domains the worker can access via HTTP proxy.
180
+ *
181
+ * Filtering rules:
182
+ * - deniedDomains are checked first (take precedence)
183
+ * - allowedDomains are checked second
184
+ * - If neither matches, request is denied
185
+ *
186
+ * Domain pattern format:
187
+ * - "example.com" - exact match
188
+ * - ".example.com" or "*.example.com" - matches subdomains
189
+ */
190
+ export interface NetworkConfig {
191
+ /** Domains the worker is allowed to access. Empty array = no network access. */
192
+ allowedDomains?: string[];
193
+ /** Domains explicitly blocked (takes precedence over allowedDomains). */
194
+ deniedDomains?: string[];
195
+ }
196
+
197
+ /**
198
+ * Nix environment configuration for agent workspace.
199
+ * Allows agents to run with specific Nix packages or flakes.
200
+ *
201
+ * Resolution priority:
202
+ * 1. API-provided flakeUrl (highest)
203
+ * 2. API-provided packages
204
+ * 3. flake.nix in git repo
205
+ * 4. shell.nix in git repo
206
+ * 5. .nix-packages file in git repo
207
+ */
208
+ export interface NixConfig {
209
+ /** Nix flake URL (e.g., "github:user/repo#devShell") */
210
+ flakeUrl?: string;
211
+ /** Nixpkgs packages to install (e.g., ["python311", "ffmpeg"]) */
212
+ packages?: string[];
213
+ }
214
+
215
+ // ============================================================================
216
+ // Tools Configuration Types
217
+ // ============================================================================
218
+
219
+ /**
220
+ * Tool permission configuration for agent settings.
221
+ * Follows Claude Code's permission patterns for consistency.
222
+ *
223
+ * Pattern formats (Claude Code compatible):
224
+ * - "Read" - exact tool match
225
+ * - "Bash(git:*)" - Bash with command filter (only git commands)
226
+ * - "Bash(npm:*)" - Bash with npm commands only
227
+ * - "mcp__servername__*" - all tools from an MCP server
228
+ * - "*" - wildcard (all tools)
229
+ *
230
+ * Filtering rules:
231
+ * - deniedTools are checked first (take precedence)
232
+ * - allowedTools are checked second
233
+ * - If strictMode=true, only allowedTools are permitted
234
+ * - If strictMode=false, defaults + allowedTools are permitted
235
+ */
236
+ export interface ToolsConfig {
237
+ /**
238
+ * Tools to auto-allow (in addition to defaults unless strictMode=true).
239
+ * Supports patterns like "Bash(git:*)" or "mcp__github__*".
240
+ */
241
+ allowedTools?: string[];
242
+
243
+ /**
244
+ * Tools to always deny (takes precedence over allowedTools).
245
+ * Use to block specific tools even if they're in defaults.
246
+ */
247
+ deniedTools?: string[];
248
+
249
+ /**
250
+ * If true, ONLY allowedTools are permitted (ignores defaults).
251
+ * If false (default), allowedTools are ADDED to default permissions.
252
+ */
253
+ strictMode?: boolean;
254
+ }
255
+
256
+ /**
257
+ * MCP server configuration for per-agent MCP servers.
258
+ * Supports both HTTP/SSE and stdio MCP servers.
259
+ */
260
+ export interface McpServerConfig {
261
+ /** For HTTP/SSE MCPs: upstream URL */
262
+ url?: string;
263
+ /** Server type: "sse" for HTTP MCPs, "stdio" for command-based */
264
+ type?: "sse" | "stdio";
265
+ /** For stdio MCPs: command to execute */
266
+ command?: string;
267
+ /** For stdio MCPs: command arguments */
268
+ args?: string[];
269
+ /** For stdio MCPs: environment variables */
270
+ env?: Record<string, string>;
271
+ /** Additional headers for HTTP MCPs */
272
+ headers?: Record<string, string>;
273
+ /** Optional description for the MCP */
274
+ description?: string;
275
+ }
276
+
277
+ /**
278
+ * Per-agent MCP configuration.
279
+ * These MCPs are ADDED to global MCPs (not replacing).
280
+ */
281
+ export interface AgentMcpConfig {
282
+ /** Additional MCP servers for this agent */
283
+ mcpServers: Record<string, McpServerConfig>;
284
+ }
285
+
286
+ export interface MemoryFlushOptions {
287
+ enabled?: boolean;
288
+ softThresholdTokens?: number;
289
+ systemPrompt?: string;
290
+ prompt?: string;
291
+ }
292
+
293
+ export interface AgentCompactionOptions {
294
+ memoryFlush?: MemoryFlushOptions;
295
+ }
296
+
297
+ /**
298
+ * Platform-agnostic execution hints passed through gateway → worker.
299
+ * Flexible types (string | string[]) and index signature allow forward
300
+ * compatibility for different agent implementations.
301
+ */
302
+ export interface AgentOptions {
303
+ runtime?: string;
304
+ model?: string;
305
+ maxTokens?: number;
306
+ temperature?: number;
307
+ allowedTools?: string | string[];
308
+ disallowedTools?: string | string[];
309
+ timeoutMinutes?: number | string;
310
+ compaction?: AgentCompactionOptions;
311
+ // Additional settings passed through from gateway (can be nested objects)
312
+ networkConfig?: Record<string, unknown>;
313
+ envVars?: Record<string, string>;
314
+ [key: string]: unknown;
315
+ }
316
+
317
+ /**
318
+ * Platform-agnostic log level type
319
+ * Maps to common logging levels used across different platforms
320
+ */
321
+ export type LogLevel = "debug" | "info" | "warn" | "error";
322
+
323
+ // ============================================================================
324
+ // Instruction Provider Types
325
+ // ============================================================================
326
+
327
+ /**
328
+ * Context information passed to instruction providers
329
+ */
330
+ export interface InstructionContext {
331
+ userId: string;
332
+ agentId: string;
333
+ sessionKey: string;
334
+ workingDirectory: string;
335
+ availableProjects?: string[];
336
+ userPrompt?: string;
337
+ }
338
+
339
+ /**
340
+ * Interface for components that contribute custom instructions
341
+ */
342
+ export interface InstructionProvider {
343
+ /** Unique identifier for this provider */
344
+ name: string;
345
+
346
+ /** Priority for ordering (lower = earlier in output) */
347
+ priority: number;
348
+
349
+ /**
350
+ * Generate instruction text for this provider
351
+ * @param context - Context information for instruction generation
352
+ * @returns Instruction text or empty string if none
353
+ */
354
+ getInstructions(context: InstructionContext): Promise<string> | string;
355
+ }
356
+
357
+ // ============================================================================
358
+ // Thread Response Types
359
+ // ============================================================================
360
+
361
+ /**
362
+ * Shared payload contract for worker → platform thread responses.
363
+ * Ensures gateway consumers and workers stay type-aligned.
364
+ */
365
+ export interface ThreadResponsePayload {
366
+ messageId: string;
367
+ channelId: string;
368
+ conversationId: string;
369
+ userId: string;
370
+ teamId: string;
371
+ platform?: string; // Platform identifier (slack, whatsapp, api, etc.) for routing
372
+ content?: string; // Used only for ephemeral messages (OAuth/auth flows)
373
+ delta?: string;
374
+ isFullReplacement?: boolean;
375
+ processedMessageIds?: string[];
376
+ error?: string;
377
+ errorCode?: string;
378
+ timestamp: number;
379
+ originalMessageId?: string;
380
+ moduleData?: Record<string, unknown>;
381
+ botResponseId?: string;
382
+ ephemeral?: boolean; // If true, message should be sent as ephemeral (only visible to user)
383
+ platformMetadata?: Record<string, unknown>;
384
+ statusUpdate?: {
385
+ elapsedSeconds: number;
386
+ state: string; // e.g., "is running" or "is scheduling"
387
+ };
388
+
389
+ // Exec-specific response fields (for jobType === "exec")
390
+ execId?: string; // Exec job ID for response routing
391
+ execStream?: "stdout" | "stderr"; // Which stream this delta is from
392
+ execExitCode?: number; // Process exit code (sent on completion)
393
+ }
394
+
395
+ // ============================================================================
396
+ // User Interaction Types
397
+ // ============================================================================
398
+
399
+ /**
400
+ * Suggested prompt for user
401
+ */
402
+ export interface SuggestedPrompt {
403
+ title: string; // Short label shown as chip
404
+ message: string; // Full message sent when clicked
405
+ }
406
+
407
+ /**
408
+ * Skill registry entry (global or per-agent).
409
+ */
410
+ export interface RegistryEntry {
411
+ id: string;
412
+ type: string;
413
+ apiUrl: string;
414
+ }
415
+
416
+ /**
417
+ * Non-blocking suggestions - agent continues immediately
418
+ * Used for optional next steps
419
+ */
420
+ export interface UserSuggestion {
421
+ id: string;
422
+ userId: string;
423
+ conversationId: string;
424
+ channelId: string;
425
+ teamId?: string;
426
+
427
+ blocking: false; // Always false - distinguishes from interactions
428
+
429
+ prompts: SuggestedPrompt[];
430
+ }