@objectstack/service-ai 6.3.0 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -17,8 +17,21 @@ __export(data_tools_exports, {
17
17
  QUERY_RECORDS_TOOL: () => QUERY_RECORDS_TOOL,
18
18
  registerDataTools: () => registerDataTools
19
19
  });
20
+ function buildEngineContext(ctx) {
21
+ if (ctx?.actor) {
22
+ return {
23
+ userId: ctx.actor.id,
24
+ roles: ctx.actor.roles ?? [],
25
+ permissions: ctx.actor.permissions ?? [],
26
+ isSystem: false,
27
+ ...ctx.environmentId ? { tenantId: ctx.environmentId } : {},
28
+ ...ctx.traceId ? { traceId: ctx.traceId } : {}
29
+ };
30
+ }
31
+ return { roles: [], permissions: [], isSystem: true };
32
+ }
20
33
  function createQueryRecordsHandler(ctx) {
21
- return async (args) => {
34
+ return async (args, execCtx) => {
22
35
  const {
23
36
  objectName,
24
37
  where,
@@ -35,17 +48,19 @@ function createQueryRecordsHandler(ctx) {
35
48
  fields,
36
49
  orderBy,
37
50
  limit: safeLimit,
38
- offset: safeOffset
51
+ offset: safeOffset,
52
+ context: buildEngineContext(execCtx)
39
53
  });
40
54
  return JSON.stringify({ count: records.length, records });
41
55
  };
42
56
  }
43
57
  function createGetRecordHandler(ctx) {
44
- return async (args) => {
58
+ return async (args, execCtx) => {
45
59
  const { objectName, recordId, fields } = args;
46
60
  const record = await ctx.dataEngine.findOne(objectName, {
47
61
  where: { id: recordId },
48
- fields
62
+ fields,
63
+ context: buildEngineContext(execCtx)
49
64
  });
50
65
  if (!record) {
51
66
  return JSON.stringify({ error: `Record "${recordId}" not found in "${objectName}"` });
@@ -54,7 +69,7 @@ function createGetRecordHandler(ctx) {
54
69
  };
55
70
  }
56
71
  function createAggregateDataHandler(ctx) {
57
- return async (args) => {
72
+ return async (args, execCtx) => {
58
73
  const { objectName, aggregations, groupBy, where } = args;
59
74
  for (const a of aggregations) {
60
75
  if (!VALID_AGG_FUNCTIONS.has(a.function)) {
@@ -70,7 +85,8 @@ function createAggregateDataHandler(ctx) {
70
85
  function: a.function,
71
86
  field: a.field,
72
87
  alias: a.alias
73
- }))
88
+ })),
89
+ context: buildEngineContext(execCtx)
74
90
  });
75
91
  return JSON.stringify(result);
76
92
  };
@@ -1205,8 +1221,15 @@ var ToolRegistry = class {
1205
1221
  }
1206
1222
  /**
1207
1223
  * Execute a tool call and return the result.
1224
+ *
1225
+ * @param toolCall The decoded tool-call part from the model response.
1226
+ * @param ctx Optional per-call execution context (actor, conversation,
1227
+ * environment). Handlers may use this to enforce RLS,
1228
+ * attribute audit entries, or correlate traces. When
1229
+ * omitted, handlers should fall back to system-level
1230
+ * behaviour for backward compatibility.
1208
1231
  */
1209
- async execute(toolCall) {
1232
+ async execute(toolCall, ctx) {
1210
1233
  const handler = this.handlers.get(toolCall.toolName);
1211
1234
  if (!handler) {
1212
1235
  return {
@@ -1219,7 +1242,7 @@ var ToolRegistry = class {
1219
1242
  }
1220
1243
  try {
1221
1244
  const args = typeof toolCall.input === "string" ? JSON.parse(toolCall.input) : toolCall.input ?? {};
1222
- const content = await handler(args);
1245
+ const content = await handler(args, ctx);
1223
1246
  return {
1224
1247
  type: "tool-result",
1225
1248
  toolCallId: toolCall.toolCallId,
@@ -1238,10 +1261,11 @@ var ToolRegistry = class {
1238
1261
  }
1239
1262
  }
1240
1263
  /**
1241
- * Execute multiple tool calls in parallel.
1264
+ * Execute multiple tool calls in parallel, threading the same
1265
+ * execution context to each handler.
1242
1266
  */
1243
- async executeAll(toolCalls) {
1244
- return Promise.all(toolCalls.map((tc) => this.execute(tc)));
1267
+ async executeAll(toolCalls, ctx) {
1268
+ return Promise.all(toolCalls.map((tc) => this.execute(tc, ctx)));
1245
1269
  }
1246
1270
  /**
1247
1271
  * Clear all registered tools.
@@ -1419,6 +1443,23 @@ var _AIService = class _AIService {
1419
1443
  get adapterName() {
1420
1444
  return this.adapter.name;
1421
1445
  }
1446
+ /**
1447
+ * Best-effort persistence of a single chat message to the conversation
1448
+ * store. Failures are logged at warn level and swallowed — chat requests
1449
+ * must never fail because the history write failed. Mirrors the
1450
+ * precedent set by `ObjectQLTraceRecorder.record`.
1451
+ */
1452
+ async persistMessage(conversationId, message) {
1453
+ try {
1454
+ await this.conversationService.addMessage(conversationId, message);
1455
+ } catch (err) {
1456
+ this.logger.warn("[AI] persist message failed", {
1457
+ conversationId,
1458
+ role: message.role,
1459
+ error: err instanceof Error ? err.message : String(err)
1460
+ });
1461
+ }
1462
+ }
1422
1463
  /**
1423
1464
  * Run an adapter call and emit a trace event.
1424
1465
  *
@@ -1532,9 +1573,15 @@ var _AIService = class _AIService {
1532
1573
  );
1533
1574
  }
1534
1575
  async chatWithToolsImpl(messages, options) {
1535
- const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
1576
+ const {
1577
+ maxIterations: maxIter,
1578
+ onToolError,
1579
+ toolExecutionContext,
1580
+ ...restOptions
1581
+ } = options ?? {};
1536
1582
  const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
1537
1583
  const registeredTools = this.toolRegistry.getAll();
1584
+ const conversationId = toolExecutionContext?.conversationId;
1538
1585
  const mergedTools = [
1539
1586
  ...registeredTools,
1540
1587
  ...restOptions.tools ?? []
@@ -1545,6 +1592,12 @@ var _AIService = class _AIService {
1545
1592
  toolChoice: mergedTools.length > 0 ? restOptions.toolChoice ?? "auto" : void 0
1546
1593
  };
1547
1594
  const conversation = [...messages];
1595
+ if (conversationId && messages.length > 0) {
1596
+ const last = messages[messages.length - 1];
1597
+ if (last && last.role === "user") {
1598
+ await this.persistMessage(conversationId, last);
1599
+ }
1600
+ }
1548
1601
  const toolErrors = [];
1549
1602
  this.logger.debug("[AI] chatWithTools start", {
1550
1603
  messageCount: conversation.length,
@@ -1556,6 +1609,12 @@ var _AIService = class _AIService {
1556
1609
  const result = await this.adapter.chat(conversation, chatOptions);
1557
1610
  if (!result.toolCalls || result.toolCalls.length === 0) {
1558
1611
  this.logger.debug("[AI] chatWithTools finished", { iteration, content: result.content.slice(0, 80) });
1612
+ if (conversationId) {
1613
+ await this.persistMessage(conversationId, {
1614
+ role: "assistant",
1615
+ content: result.content
1616
+ });
1617
+ }
1559
1618
  return result;
1560
1619
  }
1561
1620
  this.logger.debug("[AI] chatWithTools tool calls", {
@@ -1565,11 +1624,18 @@ var _AIService = class _AIService {
1565
1624
  const assistantContent = [];
1566
1625
  if (result.content) assistantContent.push({ type: "text", text: result.content });
1567
1626
  assistantContent.push(...result.toolCalls);
1568
- conversation.push({
1627
+ const assistantTurn = {
1569
1628
  role: "assistant",
1570
1629
  content: assistantContent
1571
- });
1572
- const toolResults = await this.toolRegistry.executeAll(result.toolCalls);
1630
+ };
1631
+ conversation.push(assistantTurn);
1632
+ if (conversationId) {
1633
+ await this.persistMessage(conversationId, assistantTurn);
1634
+ }
1635
+ const toolResults = await this.toolRegistry.executeAll(
1636
+ result.toolCalls,
1637
+ toolExecutionContext
1638
+ );
1573
1639
  for (const tr of toolResults) {
1574
1640
  if (tr.isError) {
1575
1641
  const matchedCall = result.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
@@ -1585,10 +1651,14 @@ var _AIService = class _AIService {
1585
1651
  }
1586
1652
  }
1587
1653
  }
1588
- conversation.push({
1654
+ const toolTurn = {
1589
1655
  role: "tool",
1590
1656
  content: [tr]
1591
- });
1657
+ };
1658
+ conversation.push(toolTurn);
1659
+ if (conversationId) {
1660
+ await this.persistMessage(conversationId, toolTurn);
1661
+ }
1592
1662
  }
1593
1663
  if (abortedByCallback) {
1594
1664
  break;
@@ -1606,6 +1676,12 @@ var _AIService = class _AIService {
1606
1676
  tools: void 0,
1607
1677
  toolChoice: void 0
1608
1678
  });
1679
+ if (conversationId) {
1680
+ await this.persistMessage(conversationId, {
1681
+ role: "assistant",
1682
+ content: finalResult.content
1683
+ });
1684
+ }
1609
1685
  return finalResult;
1610
1686
  }
1611
1687
  /**
@@ -1616,9 +1692,15 @@ var _AIService = class _AIService {
1616
1692
  * fed back until a final text stream is produced.
1617
1693
  */
1618
1694
  async *streamChatWithTools(messages, options) {
1619
- const { maxIterations: maxIter, onToolError, ...restOptions } = options ?? {};
1695
+ const {
1696
+ maxIterations: maxIter,
1697
+ onToolError,
1698
+ toolExecutionContext,
1699
+ ...restOptions
1700
+ } = options ?? {};
1620
1701
  const maxIterations = maxIter ?? _AIService.DEFAULT_MAX_ITERATIONS;
1621
1702
  const registeredTools = this.toolRegistry.getAll();
1703
+ const conversationId = toolExecutionContext?.conversationId;
1622
1704
  const mergedTools = [
1623
1705
  ...registeredTools,
1624
1706
  ...restOptions.tools ?? []
@@ -1630,9 +1712,21 @@ var _AIService = class _AIService {
1630
1712
  };
1631
1713
  const conversation = [...messages];
1632
1714
  let abortedByCallback = false;
1715
+ if (conversationId && messages.length > 0) {
1716
+ const last = messages[messages.length - 1];
1717
+ if (last && last.role === "user") {
1718
+ await this.persistMessage(conversationId, last);
1719
+ }
1720
+ }
1633
1721
  for (let iteration = 0; iteration < maxIterations; iteration++) {
1634
1722
  const result2 = await this.adapter.chat(conversation, chatOptions);
1635
1723
  if (!result2.toolCalls || result2.toolCalls.length === 0) {
1724
+ if (conversationId) {
1725
+ await this.persistMessage(conversationId, {
1726
+ role: "assistant",
1727
+ content: result2.content
1728
+ });
1729
+ }
1636
1730
  yield textDeltaPart("stream", result2.content);
1637
1731
  yield finishPart(result2);
1638
1732
  return;
@@ -1643,11 +1737,18 @@ var _AIService = class _AIService {
1643
1737
  const assistantContent = [];
1644
1738
  if (result2.content) assistantContent.push({ type: "text", text: result2.content });
1645
1739
  assistantContent.push(...result2.toolCalls);
1646
- conversation.push({
1740
+ const assistantTurn = {
1647
1741
  role: "assistant",
1648
1742
  content: assistantContent
1649
- });
1650
- const toolResults = await this.toolRegistry.executeAll(result2.toolCalls);
1743
+ };
1744
+ conversation.push(assistantTurn);
1745
+ if (conversationId) {
1746
+ await this.persistMessage(conversationId, assistantTurn);
1747
+ }
1748
+ const toolResults = await this.toolRegistry.executeAll(
1749
+ result2.toolCalls,
1750
+ toolExecutionContext
1751
+ );
1651
1752
  for (const tr of toolResults) {
1652
1753
  if (tr.isError && onToolError) {
1653
1754
  const matchedCall = result2.toolCalls.find((tc) => tc.toolCallId === tr.toolCallId);
@@ -1665,10 +1766,14 @@ var _AIService = class _AIService {
1665
1766
  toolName: tr.toolName,
1666
1767
  output: tr.output
1667
1768
  };
1668
- conversation.push({
1769
+ const toolTurn = {
1669
1770
  role: "tool",
1670
1771
  content: [tr]
1671
- });
1772
+ };
1773
+ conversation.push(toolTurn);
1774
+ if (conversationId) {
1775
+ await this.persistMessage(conversationId, toolTurn);
1776
+ }
1672
1777
  }
1673
1778
  if (abortedByCallback) {
1674
1779
  break;
@@ -1681,6 +1786,12 @@ var _AIService = class _AIService {
1681
1786
  }
1682
1787
  const finalOptions = { ...chatOptions, tools: void 0, toolChoice: void 0 };
1683
1788
  const result = await this.adapter.chat(conversation, finalOptions);
1789
+ if (conversationId) {
1790
+ await this.persistMessage(conversationId, {
1791
+ role: "assistant",
1792
+ content: result.content
1793
+ });
1794
+ }
1684
1795
  yield textDeltaPart("stream", result.content);
1685
1796
  yield finishPart(result);
1686
1797
  }
@@ -2185,6 +2296,32 @@ function buildAIRoutes(aiService, conversationService, logger) {
2185
2296
  }
2186
2297
  }
2187
2298
  },
2299
+ {
2300
+ method: "GET",
2301
+ path: "/api/v1/ai/conversations/:id",
2302
+ description: "Get a conversation with its full message history",
2303
+ auth: true,
2304
+ permissions: ["ai:conversations"],
2305
+ handler: async (req) => {
2306
+ const id = req.params?.id;
2307
+ if (!id) {
2308
+ return { status: 400, body: { error: "conversation id is required" } };
2309
+ }
2310
+ try {
2311
+ const conversation = await conversationService.get(id);
2312
+ if (!conversation) {
2313
+ return { status: 404, body: { error: `Conversation "${id}" not found` } };
2314
+ }
2315
+ if (req.user?.userId && conversation.userId && conversation.userId !== req.user.userId) {
2316
+ return { status: 403, body: { error: "You do not have access to this conversation" } };
2317
+ }
2318
+ return { status: 200, body: conversation };
2319
+ } catch (err) {
2320
+ logger.error("[AI Route] GET /conversations/:id error", err instanceof Error ? err : void 0);
2321
+ return { status: 500, body: { error: "Internal AI service error" } };
2322
+ }
2323
+ }
2324
+ },
2188
2325
  {
2189
2326
  method: "POST",
2190
2327
  path: "/api/v1/ai/conversations/:id/messages",
@@ -2351,7 +2488,19 @@ function buildAgentRoutes(aiService, agentRuntime, logger) {
2351
2488
  ];
2352
2489
  const chatWithToolsOptions = {
2353
2490
  ...mergedOptions,
2354
- maxIterations: agent.planning?.maxIterations
2491
+ maxIterations: agent.planning?.maxIterations,
2492
+ // Forward authenticated actor → built-in data tools enforce
2493
+ // ObjectQL RLS, action tools attribute audit to the user.
2494
+ toolExecutionContext: req.user ? {
2495
+ actor: {
2496
+ id: req.user.userId,
2497
+ name: req.user.displayName,
2498
+ roles: req.user.roles,
2499
+ permissions: req.user.permissions
2500
+ },
2501
+ conversationId: typeof body.conversationId === "string" ? body.conversationId : void 0,
2502
+ environmentId: typeof chatContext?.environmentId === "string" ? chatContext.environmentId : void 0
2503
+ } : void 0
2355
2504
  };
2356
2505
  const wantStream = body.stream !== false;
2357
2506
  if (wantStream) {
@@ -2540,7 +2689,21 @@ function buildAssistantRoutes(aiService, agentRuntime, skillRegistry, logger) {
2540
2689
  ];
2541
2690
  const chatWithToolsOptions = {
2542
2691
  ...mergedOptions,
2543
- maxIterations: agent.planning?.maxIterations
2692
+ maxIterations: agent.planning?.maxIterations,
2693
+ // Forward the authenticated actor into every tool the loop
2694
+ // invokes — built-in data tools promote this into the
2695
+ // ObjectQL execution context so row-level security scopes
2696
+ // the LLM to whatever the human user can already see/do.
2697
+ toolExecutionContext: req.user ? {
2698
+ actor: {
2699
+ id: req.user.userId,
2700
+ name: req.user.displayName,
2701
+ roles: req.user.roles,
2702
+ permissions: req.user.permissions
2703
+ },
2704
+ conversationId: typeof body.conversationId === "string" ? body.conversationId : void 0,
2705
+ environmentId: typeof context.environmentId === "string" ? context.environmentId : void 0
2706
+ } : void 0
2544
2707
  };
2545
2708
  const wantStream = body.stream !== false;
2546
2709
  if (wantStream) {
@@ -3888,6 +4051,19 @@ function describeField(field) {
3888
4051
  }
3889
4052
 
3890
4053
  // src/tools/query-data.tool.ts
4054
+ function buildAiEngineContext(ctx) {
4055
+ if (ctx?.actor) {
4056
+ return {
4057
+ userId: ctx.actor.id,
4058
+ roles: ctx.actor.roles ?? [],
4059
+ permissions: ctx.actor.permissions ?? [],
4060
+ isSystem: false,
4061
+ ...ctx.environmentId ? { tenantId: ctx.environmentId } : {},
4062
+ ...ctx.traceId ? { traceId: ctx.traceId } : {}
4063
+ };
4064
+ }
4065
+ return { roles: [], permissions: [], isSystem: true };
4066
+ }
3891
4067
  var QueryPlanSchema = z.object({
3892
4068
  objectName: z.string().min(1).describe('The snake_case object name to query (e.g. "task", "account").'),
3893
4069
  whereJson: z.string().nullable().describe(
@@ -3920,7 +4096,7 @@ var QUERY_DATA_TOOL = {
3920
4096
  function createQueryDataHandler(ctx) {
3921
4097
  const retriever = new SchemaRetriever(ctx.metadata);
3922
4098
  const maxLimit = ctx.maxLimit ?? 100;
3923
- return async (args) => {
4099
+ return async (args, execCtx) => {
3924
4100
  const { request } = args;
3925
4101
  if (!request || typeof request !== "string") {
3926
4102
  return JSON.stringify({ error: "query_data: `request` is required" });
@@ -3987,7 +4163,8 @@ function createQueryDataHandler(ctx) {
3987
4163
  where,
3988
4164
  fields: plan.fields ?? void 0,
3989
4165
  orderBy: plan.orderBy ?? void 0,
3990
- limit
4166
+ limit,
4167
+ context: buildAiEngineContext(execCtx)
3991
4168
  });
3992
4169
  return JSON.stringify({
3993
4170
  plan: { ...plan, where },
@@ -4167,11 +4344,13 @@ function buildHandlerEngineAdapter(engine) {
4167
4344
  };
4168
4345
  }
4169
4346
  function createActionToolHandler(action, ctx) {
4170
- const principal = ctx.principal ?? { id: "ai_agent", name: "AI Assistant" };
4347
+ const fallbackPrincipal = ctx.principal ?? { id: "ai_agent", name: "AI Assistant" };
4171
4348
  const requiresRecord = Array.isArray(action.locations) && action.locations.some(
4172
4349
  (l) => l === "list_item" || l === "record_header" || l === "record_more" || l === "record_related"
4173
4350
  );
4174
- return async (args) => {
4351
+ return async (args, execCtx) => {
4352
+ const principal = execCtx?.actor ? { id: execCtx.actor.id, name: execCtx.actor.name } : fallbackPrincipal;
4353
+ const engineCtx = buildActionEngineContext(execCtx);
4175
4354
  const objectName = action.objectName;
4176
4355
  const target = action.target;
4177
4356
  const result = {
@@ -4197,7 +4376,8 @@ function createActionToolHandler(action, ctx) {
4197
4376
  try {
4198
4377
  const found = await ctx.dataEngine.find(objectName, {
4199
4378
  where: { id: recordId },
4200
- limit: 1
4379
+ limit: 1,
4380
+ context: engineCtx
4201
4381
  });
4202
4382
  record = found[0];
4203
4383
  if (!record) {
@@ -4219,6 +4399,8 @@ function createActionToolHandler(action, ctx) {
4219
4399
  actionName: action.name,
4220
4400
  toolName,
4221
4401
  toolInput: args,
4402
+ conversationId: execCtx?.conversationId,
4403
+ messageId: execCtx?.messageId,
4222
4404
  proposedBy: principal.id
4223
4405
  });
4224
4406
  const pending = {
@@ -4256,6 +4438,19 @@ function createActionToolHandler(action, ctx) {
4256
4438
  }
4257
4439
  };
4258
4440
  }
4441
+ function buildActionEngineContext(ctx) {
4442
+ if (ctx?.actor) {
4443
+ return {
4444
+ userId: ctx.actor.id,
4445
+ roles: ctx.actor.roles ?? [],
4446
+ permissions: ctx.actor.permissions ?? [],
4447
+ isSystem: false,
4448
+ ...ctx.environmentId ? { tenantId: ctx.environmentId } : {},
4449
+ ...ctx.traceId ? { traceId: ctx.traceId } : {}
4450
+ };
4451
+ }
4452
+ return { roles: [], permissions: [], isSystem: true };
4453
+ }
4259
4454
  async function dispatchScriptAction(action, ctx, params, record, principal) {
4260
4455
  const engineAdapter = buildHandlerEngineAdapter(ctx.dataEngine);
4261
4456
  const handlerCtx = { record, user: principal, engine: engineAdapter, params };
@@ -5582,6 +5777,94 @@ var AIServicePlugin = class {
5582
5777
  // src/index.ts
5583
5778
  init_data_tools();
5584
5779
  init_metadata_tools();
5780
+
5781
+ // src/tools/knowledge-tools.ts
5782
+ var DEFAULT_TOP_K = 5;
5783
+ var MAX_TOP_K = 20;
5784
+ function buildEngineContext2(ctx) {
5785
+ if (ctx?.actor) {
5786
+ return {
5787
+ userId: ctx.actor.id,
5788
+ roles: ctx.actor.roles ?? [],
5789
+ permissions: ctx.actor.permissions ?? [],
5790
+ isSystem: false,
5791
+ ...ctx.environmentId ? { tenantId: ctx.environmentId } : {},
5792
+ ...ctx.traceId ? { traceId: ctx.traceId } : {}
5793
+ };
5794
+ }
5795
+ return { roles: [], permissions: [], isSystem: true };
5796
+ }
5797
+ var SEARCH_KNOWLEDGE_TOOL = {
5798
+ name: "search_knowledge",
5799
+ description: "Search registered knowledge sources (object snapshots, uploaded files, external URLs) and return the most relevant excerpts. Use this when the user asks a question whose answer is in documents, policies, or reference material the LLM does not natively know. Results are already permission-filtered for the current user.",
5800
+ parameters: {
5801
+ type: "object",
5802
+ properties: {
5803
+ query: {
5804
+ type: "string",
5805
+ description: "Free-text question or keywords to search for."
5806
+ },
5807
+ sourceIds: {
5808
+ type: "array",
5809
+ items: { type: "string" },
5810
+ description: "Optional list of source ids to restrict the search to. When omitted, every source the caller can see is queried."
5811
+ },
5812
+ topK: {
5813
+ type: "number",
5814
+ description: `Maximum hits to return (default ${DEFAULT_TOP_K}, max ${MAX_TOP_K}).`
5815
+ },
5816
+ filter: {
5817
+ type: "object",
5818
+ description: 'Optional adapter-specific metadata filter (e.g. {"topic":"refunds"}).'
5819
+ }
5820
+ },
5821
+ required: ["query"]
5822
+ }
5823
+ };
5824
+ function createSearchKnowledgeHandler(context) {
5825
+ return async (args, ctx) => {
5826
+ const query = typeof args.query === "string" ? args.query.trim() : "";
5827
+ if (!query) {
5828
+ return JSON.stringify({ error: "search_knowledge: `query` is required." });
5829
+ }
5830
+ const sourceIds = Array.isArray(args.sourceIds) ? args.sourceIds.filter((x) => typeof x === "string") : void 0;
5831
+ const topKRaw = typeof args.topK === "number" ? args.topK : DEFAULT_TOP_K;
5832
+ const topK = Math.max(1, Math.min(MAX_TOP_K, Math.floor(topKRaw)));
5833
+ const filter = args.filter && typeof args.filter === "object" && !Array.isArray(args.filter) ? args.filter : void 0;
5834
+ const opts = {
5835
+ topK,
5836
+ executionContext: buildEngineContext2(ctx)
5837
+ };
5838
+ if (sourceIds && sourceIds.length > 0) opts.sourceIds = sourceIds;
5839
+ if (filter) opts.filter = filter;
5840
+ try {
5841
+ const hits = await context.knowledgeService.search(query, opts);
5842
+ return JSON.stringify({
5843
+ query,
5844
+ count: hits.length,
5845
+ hits: hits.map((h) => ({
5846
+ documentId: h.documentId,
5847
+ chunkId: h.chunkId,
5848
+ sourceId: h.sourceId,
5849
+ sourceRecordId: h.sourceRecordId,
5850
+ score: Number(h.score?.toFixed?.(4) ?? h.score ?? 0),
5851
+ title: h.title,
5852
+ snippet: h.snippet,
5853
+ metadata: h.metadata
5854
+ }))
5855
+ });
5856
+ } catch (err) {
5857
+ return JSON.stringify({
5858
+ error: `search_knowledge failed: ${err instanceof Error ? err.message : String(err)}`
5859
+ });
5860
+ }
5861
+ };
5862
+ }
5863
+ function registerKnowledgeTools(registry, context) {
5864
+ registry.register(SEARCH_KNOWLEDGE_TOOL, createSearchKnowledgeHandler(context));
5865
+ }
5866
+
5867
+ // src/index.ts
5585
5868
  init_metadata_tools();
5586
5869
 
5587
5870
  // src/tools/list-packages.tool.ts
@@ -5937,6 +6220,7 @@ export {
5937
6220
  ObjectQLTraceRecorder,
5938
6221
  PACKAGE_TOOL_DEFINITIONS,
5939
6222
  QUERY_DATA_TOOL,
6223
+ SEARCH_KNOWLEDGE_TOOL,
5940
6224
  SchemaRetriever,
5941
6225
  SkillRegistry,
5942
6226
  ToolRegistry,
@@ -5965,6 +6249,7 @@ export {
5965
6249
  modifyFieldTool,
5966
6250
  registerActionsAsTools,
5967
6251
  registerDataTools,
6252
+ registerKnowledgeTools,
5968
6253
  registerMetadataTools,
5969
6254
  registerPackageTools,
5970
6255
  registerQueryDataTool,