@objectstack/service-ai 6.1.1 → 6.3.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.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { AIToolDefinition, ToolCallPart, ToolResultPart, IDataEngine, Logger, IAIService, IAIConversationService, LLMAdapter, ModelMessage, AIRequestOptions, AIResult, GenerateObjectOptions, AIObjectResult, TextStreamPart, ToolSet, ChatWithToolsOptions, AIConversation, IMetadataService } from '@objectstack/spec/contracts';
1
+ import { AIToolDefinition, ToolCallPart, ToolResultPart, IDataEngine, Logger, IAIService, IAIConversationService, LLMAdapter, ModelMessage, AIRequestOptions, AIResult, GenerateObjectOptions, AIObjectResult, TextStreamPart, ToolSet, ChatWithToolsOptions, ProposePendingActionInput, PendingActionStatus, PendingActionRow, AIConversation, IMetadataService, IAutomationService } from '@objectstack/spec/contracts';
2
2
  export { LLMAdapter } from '@objectstack/spec/contracts';
3
3
  import { z } from 'zod';
4
4
  import * as AI from '@objectstack/spec/ai';
@@ -258,6 +258,13 @@ interface AIServiceConfig {
258
258
  modelRegistry?: ModelRegistry;
259
259
  /** Trace recorder for per-call observability. Defaults to no-op. */
260
260
  traceRecorder?: TraceRecorder;
261
+ /**
262
+ * Data engine used to persist `ai_pending_actions` rows for the
263
+ * actions-as-tools HITL queue. Optional — when omitted, the
264
+ * `proposePendingAction` / `approvePendingAction` methods throw if
265
+ * called. Wired by `AIServicePlugin` after the data driver is up.
266
+ */
267
+ dataEngine?: IDataEngine;
261
268
  }
262
269
  /**
263
270
  * AIService — Unified AI capability service.
@@ -281,6 +288,15 @@ declare class AIService implements IAIService {
281
288
  readonly conversationService: IAIConversationService;
282
289
  readonly modelRegistry?: ModelRegistry;
283
290
  readonly traceRecorder: TraceRecorder;
291
+ /**
292
+ * Map of tool-name → dispatcher used to re-run an approved pending
293
+ * action. Populated by `registerActionsAsTools()` when action
294
+ * approval is enabled. Kept private because callers should go
295
+ * through `approvePendingAction()`.
296
+ */
297
+ private readonly pendingDispatchers;
298
+ /** Data engine for `ai_pending_actions` persistence. */
299
+ private readonly dataEngine?;
284
300
  constructor(config?: AIServiceConfig);
285
301
  /** The name of the active LLM adapter. */
286
302
  get adapterName(): string;
@@ -335,6 +351,29 @@ declare class AIService implements IAIService {
335
351
  * fed back until a final text stream is produced.
336
352
  */
337
353
  streamChatWithTools(messages: ModelMessage[], options?: ChatWithToolsOptions): AsyncIterable<TextStreamPart<ToolSet>>;
354
+ /**
355
+ * Register a dispatcher callback for a tool. Called by
356
+ * `registerActionsAsTools()` when action approval is enabled so the
357
+ * approval handler can re-run the exact same code path the LLM
358
+ * would have triggered.
359
+ */
360
+ registerPendingActionDispatcher(toolName: string, dispatch: (input: Record<string, unknown>) => Promise<unknown>): void;
361
+ proposePendingAction(input: ProposePendingActionInput): Promise<{
362
+ id: string;
363
+ }>;
364
+ approvePendingAction(id: string, actorId: string): Promise<{
365
+ status: 'executed' | 'failed';
366
+ result?: unknown;
367
+ error?: string;
368
+ }>;
369
+ rejectPendingAction(id: string, actorId: string, reason?: string): Promise<void>;
370
+ listPendingActions(filter?: {
371
+ status?: PendingActionStatus | PendingActionStatus[];
372
+ conversationId?: string;
373
+ objectName?: string;
374
+ limit?: number;
375
+ }): Promise<PendingActionRow[]>;
376
+ private loadPendingRow;
338
377
  }
339
378
 
340
379
  /**
@@ -356,6 +395,12 @@ interface AIServicePluginOptions {
356
395
  models?: AI.ModelConfig[];
357
396
  /** Default model id (must appear in `models`). */
358
397
  defaultModelId?: string;
398
+ /**
399
+ * Explicit trace recorder override. When set, auto-detection
400
+ * of {@link ObjectQLTraceRecorder} is skipped.
401
+ *
402
+ * Set to `null` to disable tracing entirely.
403
+ */
359
404
  /**
360
405
  * Explicit trace recorder override. When set, auto-detection
361
406
  * of {@link ObjectQLTraceRecorder} is skipped.
@@ -363,6 +408,31 @@ interface AIServicePluginOptions {
363
408
  * Set to `null` to disable tracing entirely.
364
409
  */
365
410
  traceRecorder?: TraceRecorder | null;
411
+ /**
412
+ * Base URL prepended to relative `target` paths for `type:'api'`
413
+ * actions invoked by the AI tool runtime. When unset, falls back to
414
+ * `process.env.OS_AI_ACTION_API_BASE_URL`. If neither is set, api
415
+ * actions are skipped at registration with a clear reason.
416
+ */
417
+ apiActionBaseUrl?: string;
418
+ /**
419
+ * Extra HTTP headers (e.g. `{ Authorization: 'Bearer ...' }`) applied
420
+ * to every `type:'api'` action dispatch. Useful for forwarding the
421
+ * caller's session token so server-side authorization still applies.
422
+ */
423
+ apiActionHeaders?: Record<string, string>;
424
+ /**
425
+ * Opt into Human-In-The-Loop approval for dangerous actions exposed
426
+ * as AI tools. When `true`, actions with `confirmText`, `mode:'delete'`,
427
+ * or `variant:'danger'` are still registered as tools — but invoking
428
+ * them enqueues an `ai_pending_actions` row and returns
429
+ * `{ status: 'pending_approval' }` instead of running. A human
430
+ * operator approves via Studio's pending-actions inbox to execute.
431
+ *
432
+ * Defaults to `false` (safer: dangerous actions stay invisible to LLM
433
+ * until an operator explicitly enables this routing).
434
+ */
435
+ enableActionApproval?: boolean;
366
436
  }
367
437
  /**
368
438
  * AIServicePlugin — Kernel plugin for the unified AI capability service.
@@ -988,6 +1058,16 @@ interface ObjectDef {
988
1058
  * subject record when a `recordIdParam` is configured and (b) dispatch
989
1059
  * to the registered handler via `executeAction`.
990
1060
  *
1061
+ * `automation` enables `type:'flow'` actions to dispatch into the
1062
+ * automation service's flow runner. When omitted, flow actions are
1063
+ * skipped at registration time with a clear reason.
1064
+ *
1065
+ * `apiClient` (or `apiBaseUrl`) enables `type:'api'` actions to perform
1066
+ * an HTTP call to the action's `target` URL. The default client uses
1067
+ * the global `fetch` and prepends `apiBaseUrl` to relative `target`s.
1068
+ * Supply a custom client when you need bespoke auth, in-process
1069
+ * routing, or stubbing in tests.
1070
+ *
991
1071
  * `principal` lets callers attribute AI-initiated mutations to a known
992
1072
  * user id; it defaults to a synthetic `'ai_agent'` user so traces /
993
1073
  * audit always have *some* actor.
@@ -995,6 +1075,14 @@ interface ObjectDef {
995
1075
  interface ActionToolsContext {
996
1076
  metadata: IMetadataService;
997
1077
  dataEngine: IDataEngine;
1078
+ /** Automation service for `type:'flow'` action dispatch. Optional. */
1079
+ automation?: IAutomationService;
1080
+ /** Custom API client for `type:'api'` actions. Defaults to a fetch-based client. */
1081
+ apiClient?: ApiActionClient;
1082
+ /** Base URL prepended to relative `target` paths for `type:'api'` actions. */
1083
+ apiBaseUrl?: string;
1084
+ /** Extra HTTP headers (e.g. auth bearer) applied to every `type:'api'` call. */
1085
+ apiHeaders?: Record<string, string>;
998
1086
  /** Synthetic user attribution for AI-initiated calls. */
999
1087
  principal?: {
1000
1088
  id: string;
@@ -1002,14 +1090,57 @@ interface ActionToolsContext {
1002
1090
  };
1003
1091
  /** Tool-name prefix (default: `action_`). Keeps namespace separate from data tools. */
1004
1092
  toolPrefix?: string;
1093
+ /**
1094
+ * AI service used to enqueue HITL approvals for dangerous actions.
1095
+ * When supplied together with `enableActionApproval: true`, actions
1096
+ * that would otherwise be skipped on safety grounds (`confirmText`,
1097
+ * `mode:'delete'`, `variant:'danger'`) are registered as tools whose
1098
+ * handler proposes a pending action and returns
1099
+ * `{ status: 'pending_approval' }` instead of executing.
1100
+ */
1101
+ aiService?: {
1102
+ proposePendingAction?: (input: {
1103
+ objectName: string;
1104
+ actionName: string;
1105
+ toolName: string;
1106
+ toolInput: Record<string, unknown>;
1107
+ conversationId?: string;
1108
+ messageId?: string;
1109
+ proposedBy?: string;
1110
+ }) => Promise<{
1111
+ id: string;
1112
+ }>;
1113
+ registerPendingActionDispatcher?: (toolName: string, dispatch: (input: Record<string, unknown>) => Promise<unknown>) => void;
1114
+ };
1115
+ /**
1116
+ * Opt into the HITL approval queue for dangerous actions. Default
1117
+ * is `false` (safer: dangerous actions stay invisible to the LLM
1118
+ * until an operator explicitly enables approval routing).
1119
+ */
1120
+ enableActionApproval?: boolean;
1005
1121
  }
1006
1122
  /**
1007
- * Decide whether an action should be auto-exposed as a tool.
1123
+ * Minimal HTTP client shape used by `type:'api'` action dispatch.
1008
1124
  *
1009
- * Returns `null` when exposed, or a string reason when skipped.
1010
- * Exported for tests and Studio "AI exposure" diagnostics.
1125
+ * Implementations are expected to return a JSON-deserialised body (or
1126
+ * `null` for empty responses) on 2xx, and throw on non-2xx so the tool
1127
+ * surfaces the failure to the LLM as a tool error.
1011
1128
  */
1012
- declare function actionSkipReason(action: Action): string | null;
1129
+ interface ApiActionClient {
1130
+ request(input: {
1131
+ url: string;
1132
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
1133
+ body?: Record<string, unknown>;
1134
+ headers?: Record<string, string>;
1135
+ }): Promise<unknown>;
1136
+ }
1137
+ declare function actionSkipReason(action: Action, ctx?: {
1138
+ automation?: IAutomationService;
1139
+ apiClient?: ApiActionClient;
1140
+ apiBaseUrl?: string;
1141
+ enableActionApproval?: boolean;
1142
+ aiService?: ActionToolsContext['aiService'];
1143
+ }): string | null;
1013
1144
  /** Compute the AI tool name for a given action (prefixed for namespacing). */
1014
1145
  declare function actionToolName(action: Action, prefix?: string): string;
1015
1146
  /**
@@ -1433,7 +1564,7 @@ declare const AiConversationObject: Omit<{
1433
1564
  abstract: boolean;
1434
1565
  datasource: string;
1435
1566
  fields: Record<string, {
1436
- type: "number" | "boolean" | "file" | "text" | "json" | "tags" | "currency" | "code" | "avatar" | "vector" | "date" | "datetime" | "signature" | "progress" | "url" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "percent" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "color" | "rating" | "slider" | "qrcode";
1567
+ type: "number" | "boolean" | "file" | "text" | "json" | "tags" | "currency" | "code" | "date" | "avatar" | "vector" | "datetime" | "signature" | "progress" | "url" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "percent" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "color" | "rating" | "slider" | "qrcode";
1437
1568
  required: boolean;
1438
1569
  searchable: boolean;
1439
1570
  multiple: boolean;
@@ -1572,7 +1703,7 @@ declare const AiConversationObject: Omit<{
1572
1703
  autoRotate: boolean;
1573
1704
  } | undefined;
1574
1705
  };
1575
- scope: "field" | "database" | "record" | "table";
1706
+ scope: "record" | "field" | "database" | "table";
1576
1707
  deterministicEncryption: boolean;
1577
1708
  searchableEncryption: boolean;
1578
1709
  } | undefined;
@@ -2005,7 +2136,7 @@ declare const AiConversationObject: Omit<{
2005
2136
  trash: boolean;
2006
2137
  mru: boolean;
2007
2138
  clone: boolean;
2008
- apiMethods?: ("restore" | "export" | "import" | "search" | "upsert" | "create" | "delete" | "list" | "get" | "update" | "history" | "bulk" | "aggregate" | "purge")[] | undefined;
2139
+ apiMethods?: ("restore" | "export" | "import" | "delete" | "purge" | "upsert" | "search" | "create" | "list" | "get" | "update" | "history" | "bulk" | "aggregate")[] | undefined;
2009
2140
  } | undefined;
2010
2141
  recordTypes?: string[] | undefined;
2011
2142
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
@@ -3358,7 +3489,7 @@ declare const AiMessageObject: Omit<{
3358
3489
  abstract: boolean;
3359
3490
  datasource: string;
3360
3491
  fields: Record<string, {
3361
- type: "number" | "boolean" | "file" | "text" | "json" | "tags" | "currency" | "code" | "avatar" | "vector" | "date" | "datetime" | "signature" | "progress" | "url" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "percent" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "color" | "rating" | "slider" | "qrcode";
3492
+ type: "number" | "boolean" | "file" | "text" | "json" | "tags" | "currency" | "code" | "date" | "avatar" | "vector" | "datetime" | "signature" | "progress" | "url" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "percent" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "color" | "rating" | "slider" | "qrcode";
3362
3493
  required: boolean;
3363
3494
  searchable: boolean;
3364
3495
  multiple: boolean;
@@ -3497,7 +3628,7 @@ declare const AiMessageObject: Omit<{
3497
3628
  autoRotate: boolean;
3498
3629
  } | undefined;
3499
3630
  };
3500
- scope: "field" | "database" | "record" | "table";
3631
+ scope: "record" | "field" | "database" | "table";
3501
3632
  deterministicEncryption: boolean;
3502
3633
  searchableEncryption: boolean;
3503
3634
  } | undefined;
@@ -3930,7 +4061,7 @@ declare const AiMessageObject: Omit<{
3930
4061
  trash: boolean;
3931
4062
  mru: boolean;
3932
4063
  clone: boolean;
3933
- apiMethods?: ("restore" | "export" | "import" | "search" | "upsert" | "create" | "delete" | "list" | "get" | "update" | "history" | "bulk" | "aggregate" | "purge")[] | undefined;
4064
+ apiMethods?: ("restore" | "export" | "import" | "delete" | "purge" | "upsert" | "search" | "create" | "list" | "get" | "update" | "history" | "bulk" | "aggregate")[] | undefined;
3934
4065
  } | undefined;
3935
4066
  recordTypes?: string[] | undefined;
3936
4067
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
@@ -5285,7 +5416,7 @@ declare const AiTraceObject: Omit<{
5285
5416
  abstract: boolean;
5286
5417
  datasource: string;
5287
5418
  fields: Record<string, {
5288
- type: "number" | "boolean" | "file" | "text" | "json" | "tags" | "currency" | "code" | "avatar" | "vector" | "date" | "datetime" | "signature" | "progress" | "url" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "percent" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "color" | "rating" | "slider" | "qrcode";
5419
+ type: "number" | "boolean" | "file" | "text" | "json" | "tags" | "currency" | "code" | "date" | "avatar" | "vector" | "datetime" | "signature" | "progress" | "url" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "percent" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "color" | "rating" | "slider" | "qrcode";
5289
5420
  required: boolean;
5290
5421
  searchable: boolean;
5291
5422
  multiple: boolean;
@@ -5424,7 +5555,7 @@ declare const AiTraceObject: Omit<{
5424
5555
  autoRotate: boolean;
5425
5556
  } | undefined;
5426
5557
  };
5427
- scope: "field" | "database" | "record" | "table";
5558
+ scope: "record" | "field" | "database" | "table";
5428
5559
  deterministicEncryption: boolean;
5429
5560
  searchableEncryption: boolean;
5430
5561
  } | undefined;
@@ -5857,7 +5988,7 @@ declare const AiTraceObject: Omit<{
5857
5988
  trash: boolean;
5858
5989
  mru: boolean;
5859
5990
  clone: boolean;
5860
- apiMethods?: ("restore" | "export" | "import" | "search" | "upsert" | "create" | "delete" | "list" | "get" | "update" | "history" | "bulk" | "aggregate" | "purge")[] | undefined;
5991
+ apiMethods?: ("restore" | "export" | "import" | "delete" | "purge" | "upsert" | "search" | "create" | "list" | "get" | "update" | "history" | "bulk" | "aggregate")[] | undefined;
5861
5992
  } | undefined;
5862
5993
  recordTypes?: string[] | undefined;
5863
5994
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
@@ -9970,19 +10101,26 @@ interface QueryDataToolContext {
9970
10101
  * Kept small and strict — every property is documented so providers like
9971
10102
  * OpenAI Structured Outputs and Anthropic Tool Use can render high-quality
9972
10103
  * prompts from the schema metadata.
10104
+ *
10105
+ * NOTE: `where` is intentionally typed as a JSON string rather than a free-form
10106
+ * record. OpenAI's Structured Outputs surface rejects `propertyNames`
10107
+ * (which Zod's `z.record(z.string(), ...)` emits), and Anthropic's tool-use
10108
+ * surface dislikes open-ended object schemas without `additionalProperties`.
10109
+ * Having the model emit a JSON-encoded filter sidesteps both restrictions and
10110
+ * keeps the tool portable across providers.
9973
10111
  */
9974
10112
  declare const QueryPlanSchema: z.ZodObject<{
9975
10113
  objectName: z.ZodString;
9976
- where: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
9977
- fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
9978
- orderBy: z.ZodOptional<z.ZodArray<z.ZodObject<{
10114
+ whereJson: z.ZodNullable<z.ZodString>;
10115
+ fields: z.ZodNullable<z.ZodArray<z.ZodString>>;
10116
+ orderBy: z.ZodNullable<z.ZodArray<z.ZodObject<{
9979
10117
  field: z.ZodString;
9980
10118
  order: z.ZodEnum<{
9981
10119
  asc: "asc";
9982
10120
  desc: "desc";
9983
10121
  }>;
9984
10122
  }, z.core.$strip>>>;
9985
- limit: z.ZodOptional<z.ZodNumber>;
10123
+ limit: z.ZodNullable<z.ZodNumber>;
9986
10124
  }, z.core.$strip>;
9987
10125
  /** Strongly-typed query plan inferred from the LLM. */
9988
10126
  type QueryPlan = z.infer<typeof QueryPlanSchema>;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AIToolDefinition, ToolCallPart, ToolResultPart, IDataEngine, Logger, IAIService, IAIConversationService, LLMAdapter, ModelMessage, AIRequestOptions, AIResult, GenerateObjectOptions, AIObjectResult, TextStreamPart, ToolSet, ChatWithToolsOptions, AIConversation, IMetadataService } from '@objectstack/spec/contracts';
1
+ import { AIToolDefinition, ToolCallPart, ToolResultPart, IDataEngine, Logger, IAIService, IAIConversationService, LLMAdapter, ModelMessage, AIRequestOptions, AIResult, GenerateObjectOptions, AIObjectResult, TextStreamPart, ToolSet, ChatWithToolsOptions, ProposePendingActionInput, PendingActionStatus, PendingActionRow, AIConversation, IMetadataService, IAutomationService } from '@objectstack/spec/contracts';
2
2
  export { LLMAdapter } from '@objectstack/spec/contracts';
3
3
  import { z } from 'zod';
4
4
  import * as AI from '@objectstack/spec/ai';
@@ -258,6 +258,13 @@ interface AIServiceConfig {
258
258
  modelRegistry?: ModelRegistry;
259
259
  /** Trace recorder for per-call observability. Defaults to no-op. */
260
260
  traceRecorder?: TraceRecorder;
261
+ /**
262
+ * Data engine used to persist `ai_pending_actions` rows for the
263
+ * actions-as-tools HITL queue. Optional — when omitted, the
264
+ * `proposePendingAction` / `approvePendingAction` methods throw if
265
+ * called. Wired by `AIServicePlugin` after the data driver is up.
266
+ */
267
+ dataEngine?: IDataEngine;
261
268
  }
262
269
  /**
263
270
  * AIService — Unified AI capability service.
@@ -281,6 +288,15 @@ declare class AIService implements IAIService {
281
288
  readonly conversationService: IAIConversationService;
282
289
  readonly modelRegistry?: ModelRegistry;
283
290
  readonly traceRecorder: TraceRecorder;
291
+ /**
292
+ * Map of tool-name → dispatcher used to re-run an approved pending
293
+ * action. Populated by `registerActionsAsTools()` when action
294
+ * approval is enabled. Kept private because callers should go
295
+ * through `approvePendingAction()`.
296
+ */
297
+ private readonly pendingDispatchers;
298
+ /** Data engine for `ai_pending_actions` persistence. */
299
+ private readonly dataEngine?;
284
300
  constructor(config?: AIServiceConfig);
285
301
  /** The name of the active LLM adapter. */
286
302
  get adapterName(): string;
@@ -335,6 +351,29 @@ declare class AIService implements IAIService {
335
351
  * fed back until a final text stream is produced.
336
352
  */
337
353
  streamChatWithTools(messages: ModelMessage[], options?: ChatWithToolsOptions): AsyncIterable<TextStreamPart<ToolSet>>;
354
+ /**
355
+ * Register a dispatcher callback for a tool. Called by
356
+ * `registerActionsAsTools()` when action approval is enabled so the
357
+ * approval handler can re-run the exact same code path the LLM
358
+ * would have triggered.
359
+ */
360
+ registerPendingActionDispatcher(toolName: string, dispatch: (input: Record<string, unknown>) => Promise<unknown>): void;
361
+ proposePendingAction(input: ProposePendingActionInput): Promise<{
362
+ id: string;
363
+ }>;
364
+ approvePendingAction(id: string, actorId: string): Promise<{
365
+ status: 'executed' | 'failed';
366
+ result?: unknown;
367
+ error?: string;
368
+ }>;
369
+ rejectPendingAction(id: string, actorId: string, reason?: string): Promise<void>;
370
+ listPendingActions(filter?: {
371
+ status?: PendingActionStatus | PendingActionStatus[];
372
+ conversationId?: string;
373
+ objectName?: string;
374
+ limit?: number;
375
+ }): Promise<PendingActionRow[]>;
376
+ private loadPendingRow;
338
377
  }
339
378
 
340
379
  /**
@@ -356,6 +395,12 @@ interface AIServicePluginOptions {
356
395
  models?: AI.ModelConfig[];
357
396
  /** Default model id (must appear in `models`). */
358
397
  defaultModelId?: string;
398
+ /**
399
+ * Explicit trace recorder override. When set, auto-detection
400
+ * of {@link ObjectQLTraceRecorder} is skipped.
401
+ *
402
+ * Set to `null` to disable tracing entirely.
403
+ */
359
404
  /**
360
405
  * Explicit trace recorder override. When set, auto-detection
361
406
  * of {@link ObjectQLTraceRecorder} is skipped.
@@ -363,6 +408,31 @@ interface AIServicePluginOptions {
363
408
  * Set to `null` to disable tracing entirely.
364
409
  */
365
410
  traceRecorder?: TraceRecorder | null;
411
+ /**
412
+ * Base URL prepended to relative `target` paths for `type:'api'`
413
+ * actions invoked by the AI tool runtime. When unset, falls back to
414
+ * `process.env.OS_AI_ACTION_API_BASE_URL`. If neither is set, api
415
+ * actions are skipped at registration with a clear reason.
416
+ */
417
+ apiActionBaseUrl?: string;
418
+ /**
419
+ * Extra HTTP headers (e.g. `{ Authorization: 'Bearer ...' }`) applied
420
+ * to every `type:'api'` action dispatch. Useful for forwarding the
421
+ * caller's session token so server-side authorization still applies.
422
+ */
423
+ apiActionHeaders?: Record<string, string>;
424
+ /**
425
+ * Opt into Human-In-The-Loop approval for dangerous actions exposed
426
+ * as AI tools. When `true`, actions with `confirmText`, `mode:'delete'`,
427
+ * or `variant:'danger'` are still registered as tools — but invoking
428
+ * them enqueues an `ai_pending_actions` row and returns
429
+ * `{ status: 'pending_approval' }` instead of running. A human
430
+ * operator approves via Studio's pending-actions inbox to execute.
431
+ *
432
+ * Defaults to `false` (safer: dangerous actions stay invisible to LLM
433
+ * until an operator explicitly enables this routing).
434
+ */
435
+ enableActionApproval?: boolean;
366
436
  }
367
437
  /**
368
438
  * AIServicePlugin — Kernel plugin for the unified AI capability service.
@@ -988,6 +1058,16 @@ interface ObjectDef {
988
1058
  * subject record when a `recordIdParam` is configured and (b) dispatch
989
1059
  * to the registered handler via `executeAction`.
990
1060
  *
1061
+ * `automation` enables `type:'flow'` actions to dispatch into the
1062
+ * automation service's flow runner. When omitted, flow actions are
1063
+ * skipped at registration time with a clear reason.
1064
+ *
1065
+ * `apiClient` (or `apiBaseUrl`) enables `type:'api'` actions to perform
1066
+ * an HTTP call to the action's `target` URL. The default client uses
1067
+ * the global `fetch` and prepends `apiBaseUrl` to relative `target`s.
1068
+ * Supply a custom client when you need bespoke auth, in-process
1069
+ * routing, or stubbing in tests.
1070
+ *
991
1071
  * `principal` lets callers attribute AI-initiated mutations to a known
992
1072
  * user id; it defaults to a synthetic `'ai_agent'` user so traces /
993
1073
  * audit always have *some* actor.
@@ -995,6 +1075,14 @@ interface ObjectDef {
995
1075
  interface ActionToolsContext {
996
1076
  metadata: IMetadataService;
997
1077
  dataEngine: IDataEngine;
1078
+ /** Automation service for `type:'flow'` action dispatch. Optional. */
1079
+ automation?: IAutomationService;
1080
+ /** Custom API client for `type:'api'` actions. Defaults to a fetch-based client. */
1081
+ apiClient?: ApiActionClient;
1082
+ /** Base URL prepended to relative `target` paths for `type:'api'` actions. */
1083
+ apiBaseUrl?: string;
1084
+ /** Extra HTTP headers (e.g. auth bearer) applied to every `type:'api'` call. */
1085
+ apiHeaders?: Record<string, string>;
998
1086
  /** Synthetic user attribution for AI-initiated calls. */
999
1087
  principal?: {
1000
1088
  id: string;
@@ -1002,14 +1090,57 @@ interface ActionToolsContext {
1002
1090
  };
1003
1091
  /** Tool-name prefix (default: `action_`). Keeps namespace separate from data tools. */
1004
1092
  toolPrefix?: string;
1093
+ /**
1094
+ * AI service used to enqueue HITL approvals for dangerous actions.
1095
+ * When supplied together with `enableActionApproval: true`, actions
1096
+ * that would otherwise be skipped on safety grounds (`confirmText`,
1097
+ * `mode:'delete'`, `variant:'danger'`) are registered as tools whose
1098
+ * handler proposes a pending action and returns
1099
+ * `{ status: 'pending_approval' }` instead of executing.
1100
+ */
1101
+ aiService?: {
1102
+ proposePendingAction?: (input: {
1103
+ objectName: string;
1104
+ actionName: string;
1105
+ toolName: string;
1106
+ toolInput: Record<string, unknown>;
1107
+ conversationId?: string;
1108
+ messageId?: string;
1109
+ proposedBy?: string;
1110
+ }) => Promise<{
1111
+ id: string;
1112
+ }>;
1113
+ registerPendingActionDispatcher?: (toolName: string, dispatch: (input: Record<string, unknown>) => Promise<unknown>) => void;
1114
+ };
1115
+ /**
1116
+ * Opt into the HITL approval queue for dangerous actions. Default
1117
+ * is `false` (safer: dangerous actions stay invisible to the LLM
1118
+ * until an operator explicitly enables approval routing).
1119
+ */
1120
+ enableActionApproval?: boolean;
1005
1121
  }
1006
1122
  /**
1007
- * Decide whether an action should be auto-exposed as a tool.
1123
+ * Minimal HTTP client shape used by `type:'api'` action dispatch.
1008
1124
  *
1009
- * Returns `null` when exposed, or a string reason when skipped.
1010
- * Exported for tests and Studio "AI exposure" diagnostics.
1125
+ * Implementations are expected to return a JSON-deserialised body (or
1126
+ * `null` for empty responses) on 2xx, and throw on non-2xx so the tool
1127
+ * surfaces the failure to the LLM as a tool error.
1011
1128
  */
1012
- declare function actionSkipReason(action: Action): string | null;
1129
+ interface ApiActionClient {
1130
+ request(input: {
1131
+ url: string;
1132
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
1133
+ body?: Record<string, unknown>;
1134
+ headers?: Record<string, string>;
1135
+ }): Promise<unknown>;
1136
+ }
1137
+ declare function actionSkipReason(action: Action, ctx?: {
1138
+ automation?: IAutomationService;
1139
+ apiClient?: ApiActionClient;
1140
+ apiBaseUrl?: string;
1141
+ enableActionApproval?: boolean;
1142
+ aiService?: ActionToolsContext['aiService'];
1143
+ }): string | null;
1013
1144
  /** Compute the AI tool name for a given action (prefixed for namespacing). */
1014
1145
  declare function actionToolName(action: Action, prefix?: string): string;
1015
1146
  /**
@@ -1433,7 +1564,7 @@ declare const AiConversationObject: Omit<{
1433
1564
  abstract: boolean;
1434
1565
  datasource: string;
1435
1566
  fields: Record<string, {
1436
- type: "number" | "boolean" | "file" | "text" | "json" | "tags" | "currency" | "code" | "avatar" | "vector" | "date" | "datetime" | "signature" | "progress" | "url" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "percent" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "color" | "rating" | "slider" | "qrcode";
1567
+ type: "number" | "boolean" | "file" | "text" | "json" | "tags" | "currency" | "code" | "date" | "avatar" | "vector" | "datetime" | "signature" | "progress" | "url" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "percent" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "color" | "rating" | "slider" | "qrcode";
1437
1568
  required: boolean;
1438
1569
  searchable: boolean;
1439
1570
  multiple: boolean;
@@ -1572,7 +1703,7 @@ declare const AiConversationObject: Omit<{
1572
1703
  autoRotate: boolean;
1573
1704
  } | undefined;
1574
1705
  };
1575
- scope: "field" | "database" | "record" | "table";
1706
+ scope: "record" | "field" | "database" | "table";
1576
1707
  deterministicEncryption: boolean;
1577
1708
  searchableEncryption: boolean;
1578
1709
  } | undefined;
@@ -2005,7 +2136,7 @@ declare const AiConversationObject: Omit<{
2005
2136
  trash: boolean;
2006
2137
  mru: boolean;
2007
2138
  clone: boolean;
2008
- apiMethods?: ("restore" | "export" | "import" | "search" | "upsert" | "create" | "delete" | "list" | "get" | "update" | "history" | "bulk" | "aggregate" | "purge")[] | undefined;
2139
+ apiMethods?: ("restore" | "export" | "import" | "delete" | "purge" | "upsert" | "search" | "create" | "list" | "get" | "update" | "history" | "bulk" | "aggregate")[] | undefined;
2009
2140
  } | undefined;
2010
2141
  recordTypes?: string[] | undefined;
2011
2142
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
@@ -3358,7 +3489,7 @@ declare const AiMessageObject: Omit<{
3358
3489
  abstract: boolean;
3359
3490
  datasource: string;
3360
3491
  fields: Record<string, {
3361
- type: "number" | "boolean" | "file" | "text" | "json" | "tags" | "currency" | "code" | "avatar" | "vector" | "date" | "datetime" | "signature" | "progress" | "url" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "percent" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "color" | "rating" | "slider" | "qrcode";
3492
+ type: "number" | "boolean" | "file" | "text" | "json" | "tags" | "currency" | "code" | "date" | "avatar" | "vector" | "datetime" | "signature" | "progress" | "url" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "percent" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "color" | "rating" | "slider" | "qrcode";
3362
3493
  required: boolean;
3363
3494
  searchable: boolean;
3364
3495
  multiple: boolean;
@@ -3497,7 +3628,7 @@ declare const AiMessageObject: Omit<{
3497
3628
  autoRotate: boolean;
3498
3629
  } | undefined;
3499
3630
  };
3500
- scope: "field" | "database" | "record" | "table";
3631
+ scope: "record" | "field" | "database" | "table";
3501
3632
  deterministicEncryption: boolean;
3502
3633
  searchableEncryption: boolean;
3503
3634
  } | undefined;
@@ -3930,7 +4061,7 @@ declare const AiMessageObject: Omit<{
3930
4061
  trash: boolean;
3931
4062
  mru: boolean;
3932
4063
  clone: boolean;
3933
- apiMethods?: ("restore" | "export" | "import" | "search" | "upsert" | "create" | "delete" | "list" | "get" | "update" | "history" | "bulk" | "aggregate" | "purge")[] | undefined;
4064
+ apiMethods?: ("restore" | "export" | "import" | "delete" | "purge" | "upsert" | "search" | "create" | "list" | "get" | "update" | "history" | "bulk" | "aggregate")[] | undefined;
3934
4065
  } | undefined;
3935
4066
  recordTypes?: string[] | undefined;
3936
4067
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
@@ -5285,7 +5416,7 @@ declare const AiTraceObject: Omit<{
5285
5416
  abstract: boolean;
5286
5417
  datasource: string;
5287
5418
  fields: Record<string, {
5288
- type: "number" | "boolean" | "file" | "text" | "json" | "tags" | "currency" | "code" | "avatar" | "vector" | "date" | "datetime" | "signature" | "progress" | "url" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "percent" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "color" | "rating" | "slider" | "qrcode";
5419
+ type: "number" | "boolean" | "file" | "text" | "json" | "tags" | "currency" | "code" | "date" | "avatar" | "vector" | "datetime" | "signature" | "progress" | "url" | "textarea" | "email" | "phone" | "password" | "markdown" | "html" | "richtext" | "percent" | "time" | "toggle" | "select" | "multiselect" | "radio" | "checkboxes" | "lookup" | "master_detail" | "tree" | "image" | "video" | "audio" | "formula" | "summary" | "autonumber" | "location" | "address" | "color" | "rating" | "slider" | "qrcode";
5289
5420
  required: boolean;
5290
5421
  searchable: boolean;
5291
5422
  multiple: boolean;
@@ -5424,7 +5555,7 @@ declare const AiTraceObject: Omit<{
5424
5555
  autoRotate: boolean;
5425
5556
  } | undefined;
5426
5557
  };
5427
- scope: "field" | "database" | "record" | "table";
5558
+ scope: "record" | "field" | "database" | "table";
5428
5559
  deterministicEncryption: boolean;
5429
5560
  searchableEncryption: boolean;
5430
5561
  } | undefined;
@@ -5857,7 +5988,7 @@ declare const AiTraceObject: Omit<{
5857
5988
  trash: boolean;
5858
5989
  mru: boolean;
5859
5990
  clone: boolean;
5860
- apiMethods?: ("restore" | "export" | "import" | "search" | "upsert" | "create" | "delete" | "list" | "get" | "update" | "history" | "bulk" | "aggregate" | "purge")[] | undefined;
5991
+ apiMethods?: ("restore" | "export" | "import" | "delete" | "purge" | "upsert" | "search" | "create" | "list" | "get" | "update" | "history" | "bulk" | "aggregate")[] | undefined;
5861
5992
  } | undefined;
5862
5993
  recordTypes?: string[] | undefined;
5863
5994
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
@@ -9970,19 +10101,26 @@ interface QueryDataToolContext {
9970
10101
  * Kept small and strict — every property is documented so providers like
9971
10102
  * OpenAI Structured Outputs and Anthropic Tool Use can render high-quality
9972
10103
  * prompts from the schema metadata.
10104
+ *
10105
+ * NOTE: `where` is intentionally typed as a JSON string rather than a free-form
10106
+ * record. OpenAI's Structured Outputs surface rejects `propertyNames`
10107
+ * (which Zod's `z.record(z.string(), ...)` emits), and Anthropic's tool-use
10108
+ * surface dislikes open-ended object schemas without `additionalProperties`.
10109
+ * Having the model emit a JSON-encoded filter sidesteps both restrictions and
10110
+ * keeps the tool portable across providers.
9973
10111
  */
9974
10112
  declare const QueryPlanSchema: z.ZodObject<{
9975
10113
  objectName: z.ZodString;
9976
- where: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
9977
- fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
9978
- orderBy: z.ZodOptional<z.ZodArray<z.ZodObject<{
10114
+ whereJson: z.ZodNullable<z.ZodString>;
10115
+ fields: z.ZodNullable<z.ZodArray<z.ZodString>>;
10116
+ orderBy: z.ZodNullable<z.ZodArray<z.ZodObject<{
9979
10117
  field: z.ZodString;
9980
10118
  order: z.ZodEnum<{
9981
10119
  asc: "asc";
9982
10120
  desc: "desc";
9983
10121
  }>;
9984
10122
  }, z.core.$strip>>>;
9985
- limit: z.ZodOptional<z.ZodNumber>;
10123
+ limit: z.ZodNullable<z.ZodNumber>;
9986
10124
  }, z.core.$strip>;
9987
10125
  /** Strongly-typed query plan inferred from the LLM. */
9988
10126
  type QueryPlan = z.infer<typeof QueryPlanSchema>;