@objectstack/service-ai 4.0.4 → 4.1.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.
Files changed (52) hide show
  1. package/dist/index.cjs +1176 -134
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +1752 -431
  4. package/dist/index.d.ts +1752 -431
  5. package/dist/index.js +1160 -127
  6. package/dist/index.js.map +1 -1
  7. package/package.json +35 -8
  8. package/.turbo/turbo-build.log +0 -22
  9. package/CHANGELOG.md +0 -61
  10. package/src/__tests__/ai-service.test.ts +0 -981
  11. package/src/__tests__/auth-and-toolcalling.test.ts +0 -677
  12. package/src/__tests__/chatbot-features.test.ts +0 -1116
  13. package/src/__tests__/metadata-tools.test.ts +0 -970
  14. package/src/__tests__/objectql-conversation-service.test.ts +0 -382
  15. package/src/__tests__/tool-routes.test.ts +0 -191
  16. package/src/__tests__/vercel-stream-encoder.test.ts +0 -310
  17. package/src/adapters/index.ts +0 -6
  18. package/src/adapters/memory-adapter.ts +0 -72
  19. package/src/adapters/types.ts +0 -3
  20. package/src/adapters/vercel-adapter.ts +0 -148
  21. package/src/agent-runtime.ts +0 -154
  22. package/src/agents/data-chat-agent.ts +0 -79
  23. package/src/agents/index.ts +0 -4
  24. package/src/agents/metadata-assistant-agent.ts +0 -87
  25. package/src/ai-service.ts +0 -364
  26. package/src/conversation/in-memory-conversation-service.ts +0 -103
  27. package/src/conversation/index.ts +0 -4
  28. package/src/conversation/objectql-conversation-service.ts +0 -301
  29. package/src/index.ts +0 -60
  30. package/src/objects/ai-conversation.object.ts +0 -86
  31. package/src/objects/ai-message.object.ts +0 -86
  32. package/src/objects/index.ts +0 -10
  33. package/src/plugin.ts +0 -391
  34. package/src/routes/agent-routes.ts +0 -190
  35. package/src/routes/ai-routes.ts +0 -439
  36. package/src/routes/index.ts +0 -5
  37. package/src/routes/message-utils.ts +0 -90
  38. package/src/routes/tool-routes.ts +0 -142
  39. package/src/stream/index.ts +0 -3
  40. package/src/stream/vercel-stream-encoder.ts +0 -153
  41. package/src/tools/add-field.tool.ts +0 -70
  42. package/src/tools/create-object.tool.ts +0 -66
  43. package/src/tools/data-tools.ts +0 -293
  44. package/src/tools/delete-field.tool.ts +0 -38
  45. package/src/tools/describe-object.tool.ts +0 -31
  46. package/src/tools/index.ts +0 -18
  47. package/src/tools/list-objects.tool.ts +0 -34
  48. package/src/tools/metadata-tools.ts +0 -430
  49. package/src/tools/modify-field.tool.ts +0 -44
  50. package/src/tools/tool-registry.ts +0 -132
  51. package/tsconfig.json +0 -17
  52. package/vitest.config.ts +0 -23
package/dist/index.js CHANGED
@@ -244,6 +244,10 @@ var init_create_object_tool = __esm({
244
244
  type: "string",
245
245
  description: "Human-readable display name (e.g. Project Task)"
246
246
  },
247
+ packageId: {
248
+ type: "string",
249
+ description: "Package ID that will own this object (e.g., com.acme.crm). If not provided, uses the active package from conversation context."
250
+ },
247
251
  fields: {
248
252
  type: "array",
249
253
  description: "Initial fields to create with the object",
@@ -293,6 +297,10 @@ var init_add_field_tool = __esm({
293
297
  parameters: {
294
298
  type: "object",
295
299
  properties: {
300
+ packageId: {
301
+ type: "string",
302
+ description: "Package ID that owns the target object (e.g., com.acme.crm). If not provided, uses the active package from conversation context."
303
+ },
296
304
  objectName: {
297
305
  type: "string",
298
306
  description: "Target object machine name (snake_case)"
@@ -359,6 +367,10 @@ var init_modify_field_tool = __esm({
359
367
  parameters: {
360
368
  type: "object",
361
369
  properties: {
370
+ packageId: {
371
+ type: "string",
372
+ description: "Package ID that owns the target object (e.g., com.acme.crm). If not provided, uses the active package from conversation context."
373
+ },
362
374
  objectName: {
363
375
  type: "string",
364
376
  description: "Target object machine name (snake_case)"
@@ -404,6 +416,10 @@ var init_delete_field_tool = __esm({
404
416
  parameters: {
405
417
  type: "object",
406
418
  properties: {
419
+ packageId: {
420
+ type: "string",
421
+ description: "Package ID that owns the target object (e.g., com.acme.crm). If not provided, uses the active package from conversation context."
422
+ },
407
423
  objectName: {
408
424
  type: "string",
409
425
  description: "Target object machine name (snake_case)"
@@ -492,12 +508,55 @@ __export(metadata_tools_exports, {
492
508
  function isSnakeCase(value) {
493
509
  return SNAKE_CASE_RE.test(value);
494
510
  }
511
+ async function getActivePackageId(ctx) {
512
+ if (!ctx.conversationService?.getMetadata || !ctx.conversationId) {
513
+ return null;
514
+ }
515
+ const metadata = await ctx.conversationService.getMetadata(ctx.conversationId);
516
+ return metadata?.activePackageId ?? null;
517
+ }
518
+ async function resolvePackageId(ctx, explicitPackageId) {
519
+ let packageId = null;
520
+ if (explicitPackageId) {
521
+ packageId = explicitPackageId;
522
+ } else {
523
+ packageId = await getActivePackageId(ctx);
524
+ }
525
+ if (!packageId) {
526
+ return {
527
+ packageId: null,
528
+ warning: "No package specified. Metadata will be created without package association. Consider using set_active_package or providing packageId parameter."
529
+ };
530
+ }
531
+ if (ctx.packageRegistry) {
532
+ const exists = await ctx.packageRegistry.exists(packageId);
533
+ if (!exists) {
534
+ return {
535
+ packageId: null,
536
+ error: `Package "${packageId}" not found. Use list_packages to see available packages or create_package to create a new one.`
537
+ };
538
+ }
539
+ const pkg = await ctx.packageRegistry.get(packageId);
540
+ if (pkg?.manifest.source === "filesystem") {
541
+ return {
542
+ packageId: null,
543
+ error: `Package "${packageId}" is read-only (loaded from code). Only database packages can be modified. Use create_package to create a new database package.`
544
+ };
545
+ }
546
+ }
547
+ return { packageId };
548
+ }
495
549
  function createCreateObjectHandler(ctx) {
496
550
  return async (args) => {
497
- const { name, label, fields, enableFeatures } = args;
551
+ const { name, label, packageId: explicitPackageId, fields, enableFeatures } = args;
498
552
  if (!name || !label) {
499
553
  return JSON.stringify({ error: 'Both "name" and "label" are required' });
500
554
  }
555
+ const resolved = await resolvePackageId(ctx, explicitPackageId);
556
+ if (resolved.error) {
557
+ return JSON.stringify({ error: resolved.error });
558
+ }
559
+ const packageId = resolved.packageId;
501
560
  if (!isSnakeCase(name)) {
502
561
  return JSON.stringify({ error: `Invalid object name "${name}". Must be snake_case.` });
503
562
  }
@@ -529,6 +588,7 @@ function createCreateObjectHandler(ctx) {
529
588
  const objectDef = {
530
589
  name,
531
590
  label,
591
+ ...packageId ? { packageId } : {},
532
592
  ...Object.keys(fieldMap).length > 0 ? { fields: fieldMap } : {},
533
593
  ...enableFeatures ? { enable: enableFeatures } : {}
534
594
  };
@@ -536,16 +596,21 @@ function createCreateObjectHandler(ctx) {
536
596
  return JSON.stringify({
537
597
  name,
538
598
  label,
599
+ ...packageId ? { packageId } : {},
539
600
  fieldCount: Object.keys(fieldMap).length
540
601
  });
541
602
  };
542
603
  }
543
604
  function createAddFieldHandler(ctx) {
544
605
  return async (args) => {
545
- const { objectName, name, label, type, required, defaultValue, options, reference } = args;
606
+ const { objectName, name, label, type, required, defaultValue, options, reference, packageId: explicitPackageId } = args;
546
607
  if (!objectName || !name || !type) {
547
608
  return JSON.stringify({ error: '"objectName", "name", and "type" are required' });
548
609
  }
610
+ const resolved = await resolvePackageId(ctx, explicitPackageId);
611
+ if (resolved.error) {
612
+ return JSON.stringify({ error: resolved.error });
613
+ }
549
614
  if (!isSnakeCase(objectName)) {
550
615
  return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
551
616
  }
@@ -586,16 +651,21 @@ function createAddFieldHandler(ctx) {
586
651
  return JSON.stringify({
587
652
  objectName,
588
653
  fieldName: name,
589
- fieldType: type
654
+ fieldType: type,
655
+ packageId: resolved.packageId
590
656
  });
591
657
  };
592
658
  }
593
659
  function createModifyFieldHandler(ctx) {
594
660
  return async (args) => {
595
- const { objectName, fieldName, changes } = args;
661
+ const { objectName, fieldName, changes, packageId: explicitPackageId } = args;
596
662
  if (!objectName || !fieldName || !changes) {
597
663
  return JSON.stringify({ error: '"objectName", "fieldName", and "changes" are required' });
598
664
  }
665
+ const resolved = await resolvePackageId(ctx, explicitPackageId);
666
+ if (resolved.error) {
667
+ return JSON.stringify({ error: resolved.error });
668
+ }
599
669
  if (!isSnakeCase(objectName)) {
600
670
  return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
601
671
  }
@@ -620,16 +690,21 @@ function createModifyFieldHandler(ctx) {
620
690
  return JSON.stringify({
621
691
  objectName,
622
692
  fieldName,
623
- updatedProperties: Object.keys(changes)
693
+ updatedProperties: Object.keys(changes),
694
+ packageId: resolved.packageId
624
695
  });
625
696
  };
626
697
  }
627
698
  function createDeleteFieldHandler(ctx) {
628
699
  return async (args) => {
629
- const { objectName, fieldName } = args;
700
+ const { objectName, fieldName, packageId: explicitPackageId } = args;
630
701
  if (!objectName || !fieldName) {
631
702
  return JSON.stringify({ error: '"objectName" and "fieldName" are required' });
632
703
  }
704
+ const resolved = await resolvePackageId(ctx, explicitPackageId);
705
+ if (resolved.error) {
706
+ return JSON.stringify({ error: resolved.error });
707
+ }
633
708
  if (!isSnakeCase(objectName)) {
634
709
  return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
635
710
  }
@@ -652,7 +727,8 @@ function createDeleteFieldHandler(ctx) {
652
727
  return JSON.stringify({
653
728
  objectName,
654
729
  fieldName,
655
- success: true
730
+ success: true,
731
+ packageId: resolved.packageId
656
732
  });
657
733
  };
658
734
  }
@@ -1265,25 +1341,41 @@ async function* encodeVercelDataStream(events) {
1265
1341
  yield sse({ type: "text-start", id: "0" });
1266
1342
  let textOpen = true;
1267
1343
  let finishReason = "stop";
1268
- for await (const part of events) {
1269
- if (part.type === "finish") {
1270
- finishReason = part.finishReason ?? "stop";
1271
- }
1272
- if (part.type === "finish-step" || part.type === "finish") {
1273
- if (textOpen) {
1274
- yield sse({ type: "text-end", id: "0" });
1275
- textOpen = false;
1344
+ let errorMessage;
1345
+ try {
1346
+ for await (const part of events) {
1347
+ if (part.type === "error") {
1348
+ const errPart = part;
1349
+ const raw = errPart.error;
1350
+ errorMessage = raw && typeof raw === "object" && "message" in raw ? String(raw.message) : typeof raw === "string" ? raw : "Unknown provider error";
1351
+ finishReason = "error";
1352
+ break;
1353
+ }
1354
+ if (part.type === "finish") {
1355
+ finishReason = part.finishReason ?? "stop";
1356
+ }
1357
+ if (part.type === "finish-step" || part.type === "finish") {
1358
+ if (textOpen) {
1359
+ yield sse({ type: "text-end", id: "0" });
1360
+ textOpen = false;
1361
+ }
1362
+ continue;
1363
+ }
1364
+ const frame = encodeStreamPart(part);
1365
+ if (frame) {
1366
+ yield frame;
1276
1367
  }
1277
- continue;
1278
- }
1279
- const frame = encodeStreamPart(part);
1280
- if (frame) {
1281
- yield frame;
1282
1368
  }
1369
+ } catch (err) {
1370
+ errorMessage = err instanceof Error ? err.message : String(err);
1371
+ finishReason = "error";
1283
1372
  }
1284
1373
  if (textOpen) {
1285
1374
  yield sse({ type: "text-end", id: "0" });
1286
1375
  }
1376
+ if (errorMessage) {
1377
+ yield sse({ type: "error", errorText: errorMessage });
1378
+ }
1287
1379
  yield sse({ type: "finish-step" });
1288
1380
  yield sse({ type: "finish", finishReason });
1289
1381
  yield "data: [DONE]\n\n";
@@ -1687,10 +1779,12 @@ function buildAgentRoutes(aiService, agentRuntime, logger) {
1687
1779
  return { status: 403, body: { error: `Agent "${agentName}" is not active` } };
1688
1780
  }
1689
1781
  try {
1690
- const systemMessages = agentRuntime.buildSystemMessages(agent, chatContext);
1782
+ const activeSkills = await agentRuntime.resolveActiveSkills(agent, chatContext);
1783
+ const systemMessages = agentRuntime.buildSystemMessages(agent, chatContext, activeSkills);
1691
1784
  const agentOptions = agentRuntime.buildRequestOptions(
1692
1785
  agent,
1693
- aiService.toolRegistry.getAll()
1786
+ aiService.toolRegistry.getAll(),
1787
+ activeSkills
1694
1788
  );
1695
1789
  const safeOverrides = {};
1696
1790
  if (extraOptions) {
@@ -1744,6 +1838,217 @@ function buildAgentRoutes(aiService, agentRuntime, logger) {
1744
1838
  ];
1745
1839
  }
1746
1840
 
1841
+ // src/routes/assistant-routes.ts
1842
+ var ALLOWED_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
1843
+ function validateAssistantMessage(raw) {
1844
+ if (typeof raw !== "object" || raw === null) {
1845
+ return "each message must be an object";
1846
+ }
1847
+ const msg = raw;
1848
+ if (typeof msg.role !== "string" || !ALLOWED_ROLES.has(msg.role)) {
1849
+ return `message.role must be one of ${[...ALLOWED_ROLES].map((r) => `"${r}"`).join(", ")} for assistant chat`;
1850
+ }
1851
+ const allowEmpty = msg.role === "assistant";
1852
+ return validateMessageContent(msg, { allowEmptyContent: allowEmpty });
1853
+ }
1854
+ function parseContext(raw) {
1855
+ if (typeof raw !== "object" || raw === null) return {};
1856
+ const obj = raw;
1857
+ const ctx = {};
1858
+ for (const key of Object.keys(obj)) {
1859
+ ctx[key] = obj[key];
1860
+ }
1861
+ return ctx;
1862
+ }
1863
+ function buildAssistantRoutes(aiService, agentRuntime, skillRegistry, logger) {
1864
+ return [
1865
+ // ── Resolve current assistant + skill set ──────────────────
1866
+ {
1867
+ method: "GET",
1868
+ path: "/api/v1/ai/assistant",
1869
+ description: "Resolve the default AI assistant and active skills for a given context",
1870
+ auth: true,
1871
+ permissions: ["ai:chat"],
1872
+ handler: async (req) => {
1873
+ try {
1874
+ const context = parseContextFromQuery(req.query);
1875
+ const explicitAgentName = typeof req.query?.agent === "string" ? req.query.agent : void 0;
1876
+ const agent = explicitAgentName ? await agentRuntime.loadAgent(explicitAgentName) : await agentRuntime.resolveDefaultAgent(context);
1877
+ if (!agent) {
1878
+ return {
1879
+ status: 200,
1880
+ body: { agent: null, skills: [] }
1881
+ };
1882
+ }
1883
+ const skills = await agentRuntime.resolveActiveSkills(agent, context);
1884
+ return {
1885
+ status: 200,
1886
+ body: {
1887
+ agent: {
1888
+ name: agent.name,
1889
+ label: agent.label,
1890
+ role: agent.role,
1891
+ avatar: agent.avatar,
1892
+ instructions: agent.instructions
1893
+ },
1894
+ skills: skills.map((s) => skillRegistry.toSummary(s)),
1895
+ context
1896
+ }
1897
+ };
1898
+ } catch (err) {
1899
+ logger.error("[AI Route] /assistant error", err instanceof Error ? err : void 0);
1900
+ return { status: 500, body: { error: "Internal AI service error" } };
1901
+ }
1902
+ }
1903
+ },
1904
+ // ── List active skills (slash-command palette) ─────────────
1905
+ {
1906
+ method: "GET",
1907
+ path: "/api/v1/ai/assistant/skills",
1908
+ description: "List active AI skills for a given context (used by slash-command palettes)",
1909
+ auth: true,
1910
+ permissions: ["ai:chat"],
1911
+ handler: async (req) => {
1912
+ try {
1913
+ const context = parseContextFromQuery(req.query);
1914
+ const agentName = typeof req.query?.agent === "string" ? req.query.agent : void 0;
1915
+ let restrictTo;
1916
+ if (agentName) {
1917
+ const agent = await agentRuntime.loadAgent(agentName);
1918
+ if (agent?.skills) restrictTo = agent.skills;
1919
+ }
1920
+ const skills = await skillRegistry.listActiveSkills(context, restrictTo);
1921
+ return {
1922
+ status: 200,
1923
+ body: { skills: skills.map((s) => skillRegistry.toSummary(s)) }
1924
+ };
1925
+ } catch (err) {
1926
+ logger.error("[AI Route] /assistant/skills error", err instanceof Error ? err : void 0);
1927
+ return { status: 500, body: { error: "Internal AI service error" } };
1928
+ }
1929
+ }
1930
+ },
1931
+ // ── Ambient chat (the "single entry" Claude-Code pattern) ──
1932
+ {
1933
+ method: "POST",
1934
+ path: "/api/v1/ai/assistant/chat",
1935
+ description: "Ambient AI chat \u2014 auto-resolves agent and skills from context (supports Vercel Data Stream Protocol)",
1936
+ auth: true,
1937
+ permissions: ["ai:chat"],
1938
+ handler: async (req) => {
1939
+ const body = req.body ?? {};
1940
+ const {
1941
+ messages: rawMessages,
1942
+ context: rawContext,
1943
+ options: extraOptions,
1944
+ agent: explicitAgentName,
1945
+ skill: explicitSkillName
1946
+ } = body;
1947
+ if (!Array.isArray(rawMessages) || rawMessages.length === 0) {
1948
+ return { status: 400, body: { error: "messages array is required" } };
1949
+ }
1950
+ for (const msg of rawMessages) {
1951
+ const err = validateAssistantMessage(msg);
1952
+ if (err) return { status: 400, body: { error: err } };
1953
+ }
1954
+ const context = parseContext(rawContext);
1955
+ const agent = explicitAgentName ? await agentRuntime.loadAgent(explicitAgentName) : await agentRuntime.resolveDefaultAgent(context);
1956
+ if (!agent) {
1957
+ return {
1958
+ status: 404,
1959
+ body: { error: "No active assistant available \u2014 register at least one agent or set defaultAgent on the app metadata" }
1960
+ };
1961
+ }
1962
+ if (agent.active === false) {
1963
+ return { status: 403, body: { error: `Agent "${agent.name}" is not active` } };
1964
+ }
1965
+ try {
1966
+ let activeSkills = await agentRuntime.resolveActiveSkills(agent, context);
1967
+ if (explicitSkillName) {
1968
+ activeSkills = activeSkills.filter((s) => s.name === explicitSkillName);
1969
+ if (activeSkills.length === 0) {
1970
+ const direct = await skillRegistry.loadSkill(explicitSkillName);
1971
+ if (direct && direct.active !== false) activeSkills = [direct];
1972
+ }
1973
+ }
1974
+ const systemMessages = agentRuntime.buildSystemMessages(agent, context, activeSkills);
1975
+ const agentOptions = agentRuntime.buildRequestOptions(
1976
+ agent,
1977
+ aiService.toolRegistry.getAll(),
1978
+ activeSkills
1979
+ );
1980
+ const safeOverrides = {};
1981
+ if (extraOptions) {
1982
+ const ALLOWED_KEYS = /* @__PURE__ */ new Set(["temperature", "maxTokens", "stop"]);
1983
+ for (const key of Object.keys(extraOptions)) {
1984
+ if (ALLOWED_KEYS.has(key)) safeOverrides[key] = extraOptions[key];
1985
+ }
1986
+ }
1987
+ const mergedOptions = { ...agentOptions, ...safeOverrides };
1988
+ const fullMessages = [
1989
+ ...systemMessages,
1990
+ ...rawMessages.map((m) => normalizeMessage(m))
1991
+ ];
1992
+ const chatWithToolsOptions = {
1993
+ ...mergedOptions,
1994
+ maxIterations: agent.planning?.maxIterations
1995
+ };
1996
+ const wantStream = body.stream !== false;
1997
+ if (wantStream) {
1998
+ if (!aiService.streamChatWithTools) {
1999
+ return { status: 501, body: { error: "Streaming is not supported by the configured AI service" } };
2000
+ }
2001
+ const events = aiService.streamChatWithTools(fullMessages, chatWithToolsOptions);
2002
+ return {
2003
+ status: 200,
2004
+ stream: true,
2005
+ vercelDataStream: true,
2006
+ contentType: "text/event-stream",
2007
+ headers: {
2008
+ "Content-Type": "text/event-stream",
2009
+ "Cache-Control": "no-cache",
2010
+ "Connection": "keep-alive",
2011
+ "x-vercel-ai-ui-message-stream": "v1",
2012
+ "x-objectstack-agent": agent.name,
2013
+ "x-objectstack-skills": activeSkills.map((s) => s.name).join(",")
2014
+ },
2015
+ events: encodeVercelDataStream(events)
2016
+ };
2017
+ }
2018
+ const result = await aiService.chatWithTools(fullMessages, chatWithToolsOptions);
2019
+ return {
2020
+ status: 200,
2021
+ body: {
2022
+ ...result ?? {},
2023
+ _agent: agent.name,
2024
+ _skills: activeSkills.map((s) => s.name)
2025
+ }
2026
+ };
2027
+ } catch (err) {
2028
+ logger.error(
2029
+ "[AI Route] /assistant/chat error",
2030
+ err instanceof Error ? err : void 0
2031
+ );
2032
+ const upstreamMsg = err instanceof Error ? err.message : String(err);
2033
+ return {
2034
+ status: 500,
2035
+ body: { error: "Internal AI service error", detail: upstreamMsg }
2036
+ };
2037
+ }
2038
+ }
2039
+ }
2040
+ ];
2041
+ }
2042
+ function parseContextFromQuery(query) {
2043
+ if (!query) return {};
2044
+ const ctx = {};
2045
+ for (const [key, value] of Object.entries(query)) {
2046
+ if (value == null) continue;
2047
+ ctx[key] = value;
2048
+ }
2049
+ return ctx;
2050
+ }
2051
+
1747
2052
  // src/routes/tool-routes.ts
1748
2053
  function extractOutputValue(output) {
1749
2054
  if (!output) return "";
@@ -2059,8 +2364,7 @@ var ObjectQLConversationService = class {
2059
2364
  // src/objects/ai-conversation.object.ts
2060
2365
  import { ObjectSchema, Field } from "@objectstack/spec/data";
2061
2366
  var AiConversationObject = ObjectSchema.create({
2062
- namespace: "ai",
2063
- name: "conversations",
2367
+ name: "ai_conversations",
2064
2368
  label: "AI Conversation",
2065
2369
  pluralLabel: "AI Conversations",
2066
2370
  icon: "message-square",
@@ -2079,15 +2383,14 @@ var AiConversationObject = ObjectSchema.create({
2079
2383
  description: "Conversation title or summary"
2080
2384
  }),
2081
2385
  agent_id: Field.text({
2082
- label: "Agent ID",
2386
+ label: "Agent",
2083
2387
  required: false,
2084
- maxLength: 255,
2085
- description: "Associated AI agent identifier"
2388
+ maxLength: 128,
2389
+ description: "Associated AI agent (metadata name \u2014 agents live as JSON in sys_metadata, no lookup table)"
2086
2390
  }),
2087
- user_id: Field.text({
2088
- label: "User ID",
2391
+ user_id: Field.lookup("sys_user", {
2392
+ label: "User",
2089
2393
  required: false,
2090
- maxLength: 255,
2091
2394
  description: "User who owns the conversation"
2092
2395
  }),
2093
2396
  metadata: Field.textarea({
@@ -2126,8 +2429,7 @@ var AiConversationObject = ObjectSchema.create({
2126
2429
  // src/objects/ai-message.object.ts
2127
2430
  import { ObjectSchema as ObjectSchema2, Field as Field2 } from "@objectstack/spec/data";
2128
2431
  var AiMessageObject = ObjectSchema2.create({
2129
- namespace: "ai",
2130
- name: "messages",
2432
+ name: "ai_messages",
2131
2433
  label: "AI Message",
2132
2434
  pluralLabel: "AI Messages",
2133
2435
  icon: "message-circle",
@@ -2139,8 +2441,8 @@ var AiMessageObject = ObjectSchema2.create({
2139
2441
  required: true,
2140
2442
  readonly: true
2141
2443
  }),
2142
- conversation_id: Field2.text({
2143
- label: "Conversation ID",
2444
+ conversation_id: Field2.lookup("ai_conversations", {
2445
+ label: "Conversation",
2144
2446
  required: true,
2145
2447
  description: "Foreign key to ai_conversations"
2146
2448
  }),
@@ -2198,8 +2500,9 @@ init_metadata_tools();
2198
2500
  // src/agent-runtime.ts
2199
2501
  import { AgentSchema } from "@objectstack/spec/ai";
2200
2502
  var AgentRuntime = class {
2201
- constructor(metadataService) {
2503
+ constructor(metadataService, skillRegistry) {
2202
2504
  this.metadataService = metadataService;
2505
+ this.skillRegistry = skillRegistry;
2203
2506
  }
2204
2507
  // ── Public API ────────────────────────────────────────────────
2205
2508
  /**
@@ -2243,12 +2546,21 @@ var AgentRuntime = class {
2243
2546
  /**
2244
2547
  * Build the system message(s) that should be prepended to the
2245
2548
  * conversation when chatting with the given agent.
2549
+ *
2550
+ * The composed prompt has up to three sections:
2551
+ * 1. The agent's base `instructions` (its persona / prime directives).
2552
+ * 2. UI context hints from {@link AgentChatContext} (current object,
2553
+ * record, view) so the agent can tailor responses without extra
2554
+ * tool calls.
2555
+ * 3. An "Active Skills" block describing the capabilities currently
2556
+ * available — only populated when `activeSkills` is provided.
2246
2557
  */
2247
- buildSystemMessages(agent, context) {
2558
+ buildSystemMessages(agent, context, activeSkills) {
2248
2559
  const parts = [];
2249
2560
  parts.push(agent.instructions);
2250
2561
  if (context) {
2251
2562
  const ctx = [];
2563
+ if (context.appName) ctx.push(`Current app: ${context.appName}`);
2252
2564
  if (context.objectName) ctx.push(`Current object: ${context.objectName}`);
2253
2565
  if (context.recordId) ctx.push(`Selected record ID: ${context.recordId}`);
2254
2566
  if (context.viewName) ctx.push(`Current view: ${context.viewName}`);
@@ -2256,6 +2568,10 @@ var AgentRuntime = class {
2256
2568
  parts.push("\n--- Current Context ---\n" + ctx.join("\n"));
2257
2569
  }
2258
2570
  }
2571
+ if (activeSkills && activeSkills.length > 0 && this.skillRegistry) {
2572
+ const block = this.skillRegistry.composeInstructionsBlock(activeSkills);
2573
+ if (block) parts.push(block);
2574
+ }
2259
2575
  return [{ role: "system", content: parts.join("\n") }];
2260
2576
  }
2261
2577
  /**
@@ -2263,37 +2579,272 @@ var AgentRuntime = class {
2263
2579
  *
2264
2580
  * Tool references declared in `agent.tools` are resolved by name against
2265
2581
  * `availableTools` (i.e. the full set of ToolRegistry definitions).
2266
- * Any unresolved references (tools the agent declares but that are not
2267
- * registered) are silently skipped this is intentional so that agents
2268
- * can be defined before all tools are available.
2582
+ * Tools belonging to `activeSkills` are also resolved and merged into
2583
+ * the final tool list (deduplicated by name).
2584
+ *
2585
+ * Any unresolved references (tools the agent or skill declares but
2586
+ * that are not registered) are silently skipped — this is intentional
2587
+ * so that agents/skills can be defined before all tools are available.
2269
2588
  *
2270
2589
  * @param agent - The agent definition to derive options from
2271
2590
  * @param availableTools - All tool definitions currently registered in the ToolRegistry
2591
+ * @param activeSkills - Skills resolved from agent.skills[] + context filtering
2272
2592
  * @returns Request options with model config and resolved tool definitions
2273
2593
  */
2274
- buildRequestOptions(agent, availableTools) {
2594
+ buildRequestOptions(agent, availableTools, activeSkills) {
2275
2595
  const options = {};
2276
2596
  if (agent.model) {
2277
2597
  options.model = agent.model.model;
2278
2598
  options.temperature = agent.model.temperature;
2279
2599
  options.maxTokens = agent.model.maxTokens;
2280
2600
  }
2601
+ const toolMap = new Map(availableTools.map((t) => [t.name, t]));
2602
+ const seen = /* @__PURE__ */ new Set();
2603
+ const resolved = [];
2281
2604
  if (agent.tools && agent.tools.length > 0) {
2282
- const toolMap = new Map(availableTools.map((t) => [t.name, t]));
2283
- const resolved = [];
2284
2605
  for (const ref of agent.tools) {
2606
+ if (seen.has(ref.name)) continue;
2285
2607
  const def = toolMap.get(ref.name);
2286
2608
  if (def) {
2287
2609
  resolved.push(def);
2610
+ seen.add(ref.name);
2288
2611
  }
2289
2612
  }
2290
- if (resolved.length > 0) {
2291
- options.tools = resolved;
2292
- options.toolChoice = "auto";
2613
+ }
2614
+ if (activeSkills && activeSkills.length > 0 && this.skillRegistry) {
2615
+ const skillTools = this.skillRegistry.flattenToTools(activeSkills, availableTools);
2616
+ for (const def of skillTools) {
2617
+ if (seen.has(def.name)) continue;
2618
+ resolved.push(def);
2619
+ seen.add(def.name);
2293
2620
  }
2294
2621
  }
2622
+ if (resolved.length > 0) {
2623
+ options.tools = resolved;
2624
+ options.toolChoice = "auto";
2625
+ }
2295
2626
  return options;
2296
2627
  }
2628
+ // ── Skill resolution helpers ─────────────────────────────────
2629
+ /**
2630
+ * Resolve the set of skills active for a given agent in a given
2631
+ * context. Combines:
2632
+ *
2633
+ * 1. The agent's declared `skills[]` whitelist (if any).
2634
+ * 2. Filtering by `triggerConditions` against the runtime context.
2635
+ *
2636
+ * When the agent declares no skills, returns the empty list (i.e.
2637
+ * the agent only uses its inline `tools[]`).
2638
+ *
2639
+ * Returns an empty array if no SkillRegistry was provided to the
2640
+ * runtime (legacy mode).
2641
+ */
2642
+ async resolveActiveSkills(agent, context) {
2643
+ if (!this.skillRegistry) return [];
2644
+ if (!agent.skills || agent.skills.length === 0) return [];
2645
+ return this.skillRegistry.listActiveSkills(context ?? {}, agent.skills);
2646
+ }
2647
+ /**
2648
+ * Pick a default agent for the given context, used by the ambient
2649
+ * chat endpoint when the client doesn't specify an `agentName`.
2650
+ *
2651
+ * Resolution order:
2652
+ * 1. The `defaultAgent` of the app named by `context.appName`.
2653
+ * 2. The first active agent in the registry (deterministic fallback).
2654
+ * 3. `undefined` if no agents are registered.
2655
+ */
2656
+ async resolveDefaultAgent(context) {
2657
+ if (context?.appName) {
2658
+ const rawApp = await this.metadataService.get("app", context.appName).catch(() => void 0);
2659
+ const defaultAgentName = rawApp?.defaultAgent;
2660
+ if (defaultAgentName) {
2661
+ const agent = await this.loadAgent(defaultAgentName);
2662
+ if (agent && agent.active !== false) return agent;
2663
+ }
2664
+ }
2665
+ const summaries = await this.listAgents();
2666
+ if (summaries.length === 0) return void 0;
2667
+ return this.loadAgent(summaries[0].name);
2668
+ }
2669
+ };
2670
+
2671
+ // src/skill-registry.ts
2672
+ import { SkillSchema } from "@objectstack/spec/ai";
2673
+ var SkillRegistry = class {
2674
+ constructor(metadataService) {
2675
+ this.metadataService = metadataService;
2676
+ }
2677
+ // ── Loading ────────────────────────────────────────────────────
2678
+ /**
2679
+ * Load and validate a single skill definition by name.
2680
+ *
2681
+ * Returns `undefined` when the skill is missing or fails Zod
2682
+ * validation (so callers don't accidentally feed malformed metadata
2683
+ * to the LLM).
2684
+ */
2685
+ async loadSkill(skillName) {
2686
+ const raw = await this.metadataService.get("skill", skillName);
2687
+ if (!raw) return void 0;
2688
+ const result = SkillSchema.safeParse(raw);
2689
+ if (!result.success) return void 0;
2690
+ return result.data;
2691
+ }
2692
+ /**
2693
+ * Load all skill definitions, dropping any that fail validation
2694
+ * or are explicitly inactive.
2695
+ */
2696
+ async listSkills() {
2697
+ const raw = await this.metadataService.list("skill");
2698
+ const skills = [];
2699
+ for (const item of raw) {
2700
+ const result = SkillSchema.safeParse(item);
2701
+ if (result.success && result.data.active !== false) {
2702
+ skills.push(result.data);
2703
+ }
2704
+ }
2705
+ return skills;
2706
+ }
2707
+ /**
2708
+ * Load only the skills referenced by `skillNames`, preserving
2709
+ * declaration order. Missing or invalid skill names are silently
2710
+ * dropped (logged at the route layer if needed) so an Agent can be
2711
+ * defined before all its skills are persisted.
2712
+ */
2713
+ async loadSkills(skillNames) {
2714
+ const skills = [];
2715
+ for (const name of skillNames) {
2716
+ const skill = await this.loadSkill(name);
2717
+ if (skill && skill.active !== false) {
2718
+ skills.push(skill);
2719
+ }
2720
+ }
2721
+ return skills;
2722
+ }
2723
+ // ── Context filtering ──────────────────────────────────────────
2724
+ /**
2725
+ * Return skills whose `triggerConditions` are satisfied by the
2726
+ * given context. Skills without any conditions are always considered
2727
+ * active and returned in their declaration order.
2728
+ *
2729
+ * If `restrictTo` is provided, the result is intersected with that
2730
+ * allow-list (typically the agent's `skills[]` field) so an agent
2731
+ * never sees skills outside its declared scope.
2732
+ */
2733
+ async listActiveSkills(context = {}, restrictTo) {
2734
+ const allowList = restrictTo ? new Set(restrictTo) : void 0;
2735
+ const all = await this.listSkills();
2736
+ return all.filter((skill) => {
2737
+ if (allowList && !allowList.has(skill.name)) return false;
2738
+ return this.matchesContext(skill, context);
2739
+ });
2740
+ }
2741
+ /**
2742
+ * Evaluate a skill's `triggerConditions` against the given context.
2743
+ *
2744
+ * Semantics:
2745
+ * - No conditions defined → always matches.
2746
+ * - All conditions must pass (logical AND).
2747
+ * - Operators: `eq`, `neq`, `in`, `not_in`, `contains`.
2748
+ * - `contains` does substring matching for strings and `Array.includes`
2749
+ * for arrays.
2750
+ * - Missing context fields fail unless the operator is `neq` /
2751
+ * `not_in` (treating "absent" as "not equal to anything").
2752
+ */
2753
+ matchesContext(skill, context) {
2754
+ const conditions = skill.triggerConditions;
2755
+ if (!conditions || conditions.length === 0) return true;
2756
+ return conditions.every((cond) => this.evaluateCondition(cond, context));
2757
+ }
2758
+ evaluateCondition(cond, context) {
2759
+ const fieldValue = context[cond.field];
2760
+ const expected = cond.value;
2761
+ switch (cond.operator) {
2762
+ case "eq":
2763
+ return fieldValue === expected;
2764
+ case "neq":
2765
+ return fieldValue !== expected;
2766
+ case "in": {
2767
+ const list = Array.isArray(expected) ? expected : [expected];
2768
+ return list.includes(fieldValue);
2769
+ }
2770
+ case "not_in": {
2771
+ const list = Array.isArray(expected) ? expected : [expected];
2772
+ return !list.includes(fieldValue);
2773
+ }
2774
+ case "contains": {
2775
+ if (typeof fieldValue === "string" && typeof expected === "string") {
2776
+ return fieldValue.includes(expected);
2777
+ }
2778
+ if (Array.isArray(fieldValue)) {
2779
+ return Array.isArray(expected) ? expected.every((v) => fieldValue.includes(v)) : fieldValue.includes(expected);
2780
+ }
2781
+ return false;
2782
+ }
2783
+ default:
2784
+ return false;
2785
+ }
2786
+ }
2787
+ // ── Tool resolution ───────────────────────────────────────────
2788
+ /**
2789
+ * Flatten a list of skills to a deduplicated array of concrete tool
2790
+ * definitions, preserving the order skills declared their tools.
2791
+ *
2792
+ * Tools that are declared by a skill but missing from the available
2793
+ * tool registry are silently dropped — this is intentional so a skill
2794
+ * can be authored before all its underlying tools are registered.
2795
+ */
2796
+ flattenToTools(skills, availableTools) {
2797
+ const toolMap = new Map(availableTools.map((t) => [t.name, t]));
2798
+ const seen = /* @__PURE__ */ new Set();
2799
+ const resolved = [];
2800
+ for (const skill of skills) {
2801
+ for (const toolName of skill.tools) {
2802
+ if (seen.has(toolName)) continue;
2803
+ const def = toolMap.get(toolName);
2804
+ if (def) {
2805
+ resolved.push(def);
2806
+ seen.add(toolName);
2807
+ }
2808
+ }
2809
+ }
2810
+ return resolved;
2811
+ }
2812
+ // ── System-prompt composition ─────────────────────────────────
2813
+ /**
2814
+ * Build the "Active Skills" block to append to an agent's system
2815
+ * prompt. The block lists each skill's label + instructions so the
2816
+ * LLM knows which capabilities are available and how to invoke them.
2817
+ *
2818
+ * Returns an empty string when there are no skills, so the caller
2819
+ * can safely concatenate without producing dangling whitespace.
2820
+ */
2821
+ composeInstructionsBlock(skills) {
2822
+ if (skills.length === 0) return "";
2823
+ const lines = ["", "--- Active Skills ---"];
2824
+ for (const skill of skills) {
2825
+ lines.push(`
2826
+ ### ${skill.label} (${skill.name})`);
2827
+ if (skill.description) lines.push(skill.description);
2828
+ if (skill.instructions) lines.push(skill.instructions);
2829
+ if (skill.tools.length > 0) {
2830
+ lines.push(`Tools: ${skill.tools.join(", ")}`);
2831
+ }
2832
+ }
2833
+ return lines.join("\n");
2834
+ }
2835
+ /**
2836
+ * Project a skill to a wire-friendly summary suitable for the
2837
+ * `/api/v1/ai/skills` endpoint and slash-command palettes.
2838
+ */
2839
+ toSummary(skill) {
2840
+ return {
2841
+ name: skill.name,
2842
+ label: skill.label,
2843
+ description: skill.description,
2844
+ triggerPhrases: skill.triggerPhrases,
2845
+ toolCount: skill.tools.length
2846
+ };
2847
+ }
2297
2848
  };
2298
2849
 
2299
2850
  // src/agents/data-chat-agent.ts
@@ -2303,34 +2854,15 @@ var DATA_CHAT_AGENT = {
2303
2854
  role: "Business Data Analyst",
2304
2855
  instructions: `You are a helpful data assistant that helps users explore and understand their business data through natural language.
2305
2856
 
2306
- Capabilities:
2307
- - List available data objects (tables) and their schemas
2308
- - Query records with filters, sorting, and pagination
2309
- - Look up individual records by ID
2310
- - Perform aggregations and statistical analysis (count, sum, avg, min, max)
2311
-
2312
- Guidelines:
2313
- 1. Always use the describe_object tool first to understand a table's structure before querying it.
2314
- 2. Respect the user's current context \u2014 if they are viewing a specific object or record, use that as the default scope.
2315
- 3. When presenting data, format it in a clear and readable way using markdown tables or bullet lists.
2316
- 4. For large result sets, summarize the data and mention the total count.
2317
- 5. When performing aggregations, explain the results in plain language.
2318
- 6. If a query returns no results, suggest possible reasons and alternative queries.
2319
- 7. Never expose internal IDs unless the user explicitly asks for them.
2320
- 8. Always answer in the same language the user is using.`,
2857
+ Always answer in the same language the user is using. Detailed tool-usage guidance is supplied by the skills attached to this agent.`,
2321
2858
  model: {
2322
2859
  provider: "openai",
2323
2860
  model: "gpt-4",
2324
2861
  temperature: 0.3,
2325
2862
  maxTokens: 4096
2326
2863
  },
2327
- tools: [
2328
- { type: "query", name: "list_objects", description: "List all available data objects" },
2329
- { type: "query", name: "describe_object", description: "Get schema/fields of a data object" },
2330
- { type: "query", name: "query_records", description: "Query records with filters and pagination" },
2331
- { type: "query", name: "get_record", description: "Get a single record by ID" },
2332
- { type: "query", name: "aggregate_data", description: "Aggregate/statistics on data" }
2333
- ],
2864
+ // Capability bundle lives on the skill; the agent only references it.
2865
+ skills: ["data_explorer"],
2334
2866
  active: true,
2335
2867
  visibility: "global",
2336
2868
  guardrails: {
@@ -2358,39 +2890,15 @@ var METADATA_ASSISTANT_AGENT = {
2358
2890
  role: "Schema Architect",
2359
2891
  instructions: `You are an expert metadata architect that helps users design and manage their data models through natural language.
2360
2892
 
2361
- Capabilities:
2362
- - Create new data objects (tables) with fields
2363
- - Add fields (columns) to existing objects
2364
- - Modify field properties (label, type, required, default value)
2365
- - Delete fields from objects
2366
- - List all registered metadata objects and their schemas
2367
- - Describe the full schema of a specific object
2368
-
2369
- Guidelines:
2370
- 1. Before creating a new object, use list_objects to check if a similar one already exists.
2371
- 2. Before modifying or deleting fields, use describe_object to understand the current schema.
2372
- 3. Always use snake_case for object names and field names (e.g. project_task, due_date).
2373
- 4. Suggest meaningful field types based on the user's description (e.g. "deadline" \u2192 date, "active" \u2192 boolean).
2374
- 5. When creating objects, propose a reasonable set of initial fields based on the entity type.
2375
- 6. Explain what changes you are about to make before executing them.
2376
- 7. After making changes, confirm the result by describing the updated schema.
2377
- 8. For destructive operations (deleting fields), always warn the user about potential data loss.
2378
- 9. Always answer in the same language the user is using.
2379
- 10. If the user's request is ambiguous, ask clarifying questions before proceeding.`,
2893
+ Always answer in the same language the user is using. If the user's request is ambiguous, ask clarifying questions before proceeding. Detailed tool-usage guidance is supplied by the skills attached to this agent.`,
2380
2894
  model: {
2381
2895
  provider: "openai",
2382
2896
  model: "gpt-4",
2383
2897
  temperature: 0.2,
2384
2898
  maxTokens: 4096
2385
2899
  },
2386
- tools: [
2387
- { type: "action", name: "create_object", description: "Create a new data object (table)" },
2388
- { type: "action", name: "add_field", description: "Add a field to an existing object" },
2389
- { type: "action", name: "modify_field", description: "Modify an existing field definition" },
2390
- { type: "action", name: "delete_field", description: "Delete a field from an object" },
2391
- { type: "query", name: "list_objects", description: "List all data objects" },
2392
- { type: "query", name: "describe_object", description: "Describe an object schema" }
2393
- ],
2900
+ // Capability bundle lives on the skill; the agent only references it.
2901
+ skills: ["metadata_authoring"],
2394
2902
  active: true,
2395
2903
  visibility: "global",
2396
2904
  guardrails: {
@@ -2411,6 +2919,98 @@ Guidelines:
2411
2919
  }
2412
2920
  };
2413
2921
 
2922
+ // src/skills/data-explorer-skill.ts
2923
+ var DATA_EXPLORER_SKILL = {
2924
+ name: "data_explorer",
2925
+ label: "Data Explorer",
2926
+ description: "Read-only Q&A over the user's business data \u2014 schema discovery, filtered queries, lookups, and aggregations.",
2927
+ instructions: `You can explore the user's business data through these tools.
2928
+
2929
+ Capabilities:
2930
+ - List available data objects (tables) and their schemas
2931
+ - Query records with filters, sorting, and pagination
2932
+ - Look up individual records by ID
2933
+ - Perform aggregations and statistical analysis (count, sum, avg, min, max)
2934
+
2935
+ Guidelines:
2936
+ 1. Always use the describe_object tool first to understand a table's structure before querying it.
2937
+ 2. Respect the user's current context \u2014 if they are viewing a specific object or record, use that as the default scope.
2938
+ 3. When presenting data, format it in a clear and readable way using markdown tables or bullet lists.
2939
+ 4. For large result sets, summarize the data and mention the total count.
2940
+ 5. When performing aggregations, explain the results in plain language.
2941
+ 6. If a query returns no results, suggest possible reasons and alternative queries.
2942
+ 7. Never expose internal IDs unless the user explicitly asks for them.
2943
+ 8. Always answer in the same language the user is using.`,
2944
+ tools: [
2945
+ "list_objects",
2946
+ "describe_object",
2947
+ "query_records",
2948
+ "get_record",
2949
+ "aggregate_data"
2950
+ ],
2951
+ triggerPhrases: [
2952
+ "show me",
2953
+ "list",
2954
+ "how many",
2955
+ "count",
2956
+ "find records",
2957
+ "query",
2958
+ "aggregate",
2959
+ "sum",
2960
+ "average"
2961
+ ],
2962
+ active: true
2963
+ };
2964
+
2965
+ // src/skills/metadata-authoring-skill.ts
2966
+ var METADATA_AUTHORING_SKILL = {
2967
+ name: "metadata_authoring",
2968
+ label: "Metadata Authoring",
2969
+ description: "Create and modify ObjectStack metadata \u2014 objects, fields, schema changes through natural language.",
2970
+ instructions: `You are an expert metadata architect. When the user asks you to design or change a data model, use these tools.
2971
+
2972
+ Capabilities:
2973
+ - Create new data objects (tables) with fields
2974
+ - Add fields (columns) to existing objects
2975
+ - Modify field properties (label, type, required, default value)
2976
+ - Delete fields from objects
2977
+ - List all registered metadata objects and their schemas
2978
+ - Describe the full schema of a specific object
2979
+
2980
+ Guidelines:
2981
+ 1. Before creating a new object, use list_objects to check if a similar one already exists.
2982
+ 2. Before modifying or deleting fields, use describe_object to understand the current schema.
2983
+ 3. Always use snake_case for object names and field names (e.g. project_task, due_date).
2984
+ 4. Suggest meaningful field types based on the user's description (e.g. "deadline" \u2192 date, "active" \u2192 boolean).
2985
+ 5. When creating objects, propose a reasonable set of initial fields based on the entity type.
2986
+ 6. Explain what changes you are about to make before executing them.
2987
+ 7. After making changes, confirm the result by describing the updated schema.
2988
+ 8. For destructive operations (deleting fields), always warn the user about potential data loss.
2989
+ 9. Always answer in the same language the user is using.
2990
+ 10. If the user's request is ambiguous, ask clarifying questions before proceeding.`,
2991
+ tools: [
2992
+ "create_object",
2993
+ "add_field",
2994
+ "modify_field",
2995
+ "delete_field",
2996
+ "list_objects",
2997
+ "describe_object"
2998
+ ],
2999
+ triggerPhrases: [
3000
+ "create object",
3001
+ "create table",
3002
+ "add field",
3003
+ "add column",
3004
+ "modify field",
3005
+ "change field",
3006
+ "delete field",
3007
+ "drop field",
3008
+ "design schema",
3009
+ "new entity"
3010
+ ],
3011
+ active: true
3012
+ };
3013
+
2414
3014
  // src/adapters/vercel-adapter.ts
2415
3015
  import { generateText, streamText, tool as vercelTool, jsonSchema } from "ai";
2416
3016
  function buildVercelOptions(options) {
@@ -2478,8 +3078,15 @@ var VercelLLMAdapter = class {
2478
3078
  messages,
2479
3079
  ...buildVercelOptions(options)
2480
3080
  });
2481
- for await (const part of result.fullStream) {
2482
- yield part;
3081
+ try {
3082
+ for await (const part of result.fullStream) {
3083
+ yield part;
3084
+ }
3085
+ } catch (err) {
3086
+ yield {
3087
+ type: "error",
3088
+ error: err instanceof Error ? err : new Error(String(err))
3089
+ };
2483
3090
  }
2484
3091
  }
2485
3092
  async embed(_input) {
@@ -2561,11 +3168,14 @@ var AIServicePlugin = class {
2561
3168
  /* webpackIgnore: true */
2562
3169
  pkg
2563
3170
  );
2564
- const createModel = mod[factory] ?? mod.default;
2565
- if (typeof createModel === "function") {
3171
+ const provider = mod[factory] ?? mod.default;
3172
+ if (typeof provider === "function") {
2566
3173
  const modelId = process.env.AI_MODEL ?? defaultModel;
2567
- const adapter = new VercelLLMAdapter({ model: createModel(modelId) });
2568
- return { adapter, description: `${displayName} (model: ${modelId})` };
3174
+ const useChatApi = factory === "openai" && typeof provider.chat === "function";
3175
+ const model = useChatApi ? provider.chat(modelId) : provider(modelId);
3176
+ const adapter = new VercelLLMAdapter({ model });
3177
+ const apiSuffix = useChatApi ? " [chat-completions]" : "";
3178
+ return { adapter, description: `${displayName} (model: ${modelId})${apiSuffix}` };
2569
3179
  }
2570
3180
  } catch (err) {
2571
3181
  ctx.logger.warn(
@@ -2626,6 +3236,7 @@ var AIServicePlugin = class {
2626
3236
  name: "AI Service",
2627
3237
  version: "1.0.0",
2628
3238
  type: "plugin",
3239
+ scope: "project",
2629
3240
  namespace: "ai",
2630
3241
  objects: [AiConversationObject, AiMessageObject]
2631
3242
  });
@@ -2634,25 +3245,12 @@ var AIServicePlugin = class {
2634
3245
  ctx.logger.debug("[AI] Before chat", { messages });
2635
3246
  });
2636
3247
  }
2637
- try {
2638
- const setupNav = ctx.getService("setupNav");
2639
- if (setupNav) {
2640
- setupNav.contribute({
2641
- areaId: "area_ai",
2642
- items: [
2643
- { id: "nav_ai_conversations", type: "object", label: "Conversations", objectName: "conversations", icon: "message-square", order: 10 },
2644
- { id: "nav_ai_messages", type: "object", label: "Messages", objectName: "messages", icon: "messages-square", order: 20 }
2645
- ]
2646
- });
2647
- ctx.logger.info("[AI] Navigation items contributed to Setup App");
2648
- }
2649
- } catch {
2650
- }
2651
3248
  ctx.logger.info("[AI] Service initialized");
2652
3249
  }
2653
3250
  async start(ctx) {
2654
3251
  if (!this.service) return;
2655
3252
  let metadataService;
3253
+ const withTimeout = (promise, ms = 2e3) => Promise.race([promise, new Promise((resolve) => setTimeout(() => resolve(null), ms))]);
2656
3254
  try {
2657
3255
  metadataService = ctx.getService("metadata");
2658
3256
  console.log("[AI Plugin] Retrieved metadata service:", !!metadataService, "has getRegisteredTypes:", typeof metadataService?.getRegisteredTypes);
@@ -2660,6 +3258,13 @@ var AIServicePlugin = class {
2660
3258
  console.log("[AI] Metadata service not available:", e.message);
2661
3259
  ctx.logger.debug("[AI] Metadata service not available");
2662
3260
  }
3261
+ if (metadataService && typeof metadataService.exists === "function") {
3262
+ const probeResult = await withTimeout(metadataService.exists("tool", "__probe__"), 3e3);
3263
+ if (probeResult === null) {
3264
+ ctx.logger.warn("[AI] Metadata service unreachable (timed out) \u2014 AI tools/agents will work but Studio visibility unavailable");
3265
+ metadataService = void 0;
3266
+ }
3267
+ }
2663
3268
  try {
2664
3269
  const dataEngine = ctx.getService("data");
2665
3270
  if (dataEngine) {
@@ -2668,18 +3273,31 @@ var AIServicePlugin = class {
2668
3273
  if (metadataService) {
2669
3274
  const { DATA_TOOL_DEFINITIONS: DATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_data_tools(), data_tools_exports));
2670
3275
  for (const toolDef of DATA_TOOL_DEFINITIONS2) {
2671
- const toolExists = typeof metadataService.exists === "function" ? await metadataService.exists("tool", toolDef.name) : false;
3276
+ const toolExists = typeof metadataService.exists === "function" ? await withTimeout(metadataService.exists("tool", toolDef.name)) : false;
3277
+ if (toolExists === null) {
3278
+ ctx.logger.warn("[AI] Metadata service timed out checking tool existence (non-fatal), skipping persistence");
3279
+ break;
3280
+ }
2672
3281
  if (!toolExists) {
2673
- await metadataService.register("tool", toolDef.name, toolDef);
3282
+ try {
3283
+ await withTimeout(metadataService.register("tool", toolDef.name, toolDef));
3284
+ } catch (err) {
3285
+ ctx.logger.warn(
3286
+ "[AI] Failed to persist tool metadata (non-fatal)",
3287
+ err instanceof Error ? { tool: toolDef.name, error: err.message } : { tool: toolDef.name }
3288
+ );
3289
+ }
2674
3290
  }
2675
3291
  }
2676
3292
  ctx.logger.info(`[AI] ${DATA_TOOL_DEFINITIONS2.length} data tools registered as metadata`);
2677
3293
  }
2678
3294
  if (metadataService) {
2679
3295
  try {
2680
- const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", DATA_CHAT_AGENT.name) : false;
2681
- if (!agentExists) {
2682
- await metadataService.register("agent", DATA_CHAT_AGENT.name, DATA_CHAT_AGENT);
3296
+ const agentExists = typeof metadataService.exists === "function" ? await withTimeout(metadataService.exists("agent", DATA_CHAT_AGENT.name)) : false;
3297
+ if (agentExists === null) {
3298
+ ctx.logger.warn("[AI] Metadata service timed out checking data_chat agent, skipping");
3299
+ } else if (!agentExists) {
3300
+ await withTimeout(metadataService.register("agent", DATA_CHAT_AGENT.name, DATA_CHAT_AGENT));
2683
3301
  console.log("[AI] Registered data_chat agent to metadataService");
2684
3302
  ctx.logger.info("[AI] data_chat agent registered");
2685
3303
  } else {
@@ -2689,6 +3307,19 @@ var AIServicePlugin = class {
2689
3307
  } catch (err) {
2690
3308
  ctx.logger.warn("[AI] Failed to register data_chat agent", err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) });
2691
3309
  }
3310
+ try {
3311
+ const skillExists = typeof metadataService.exists === "function" ? await withTimeout(metadataService.exists("skill", DATA_EXPLORER_SKILL.name)) : false;
3312
+ if (skillExists === null) {
3313
+ ctx.logger.warn("[AI] Metadata service timed out checking data_explorer skill, skipping");
3314
+ } else if (!skillExists) {
3315
+ await withTimeout(metadataService.register("skill", DATA_EXPLORER_SKILL.name, DATA_EXPLORER_SKILL));
3316
+ ctx.logger.info("[AI] data_explorer skill registered");
3317
+ } else {
3318
+ ctx.logger.debug("[AI] data_explorer skill already exists, skipping auto-registration");
3319
+ }
3320
+ } catch (err) {
3321
+ ctx.logger.warn("[AI] Failed to register data_explorer skill", err instanceof Error ? { error: err.message } : { error: String(err) });
3322
+ }
2692
3323
  }
2693
3324
  }
2694
3325
  } catch {
@@ -2700,16 +3331,29 @@ var AIServicePlugin = class {
2700
3331
  ctx.logger.info("[AI] Built-in metadata tools registered");
2701
3332
  const { METADATA_TOOL_DEFINITIONS: METADATA_TOOL_DEFINITIONS2 } = await Promise.resolve().then(() => (init_metadata_tools(), metadata_tools_exports));
2702
3333
  for (const toolDef of METADATA_TOOL_DEFINITIONS2) {
2703
- const toolExists = typeof metadataService.exists === "function" ? await metadataService.exists("tool", toolDef.name) : false;
3334
+ const toolExists = typeof metadataService.exists === "function" ? await withTimeout(metadataService.exists("tool", toolDef.name)) : false;
3335
+ if (toolExists === null) {
3336
+ ctx.logger.warn("[AI] Metadata service timed out checking tool existence (non-fatal), skipping persistence");
3337
+ break;
3338
+ }
2704
3339
  if (!toolExists) {
2705
- await metadataService.register("tool", toolDef.name, toolDef);
3340
+ try {
3341
+ await withTimeout(metadataService.register("tool", toolDef.name, toolDef));
3342
+ } catch (err) {
3343
+ ctx.logger.warn(
3344
+ "[AI] Failed to persist tool metadata (non-fatal)",
3345
+ err instanceof Error ? { tool: toolDef.name, error: err.message } : { tool: toolDef.name }
3346
+ );
3347
+ }
2706
3348
  }
2707
3349
  }
2708
3350
  ctx.logger.info(`[AI] ${METADATA_TOOL_DEFINITIONS2.length} metadata tools registered as metadata`);
2709
3351
  try {
2710
- const agentExists = typeof metadataService.exists === "function" ? await metadataService.exists("agent", METADATA_ASSISTANT_AGENT.name) : false;
2711
- if (!agentExists) {
2712
- await metadataService.register("agent", METADATA_ASSISTANT_AGENT.name, METADATA_ASSISTANT_AGENT);
3352
+ const agentExists = typeof metadataService.exists === "function" ? await withTimeout(metadataService.exists("agent", METADATA_ASSISTANT_AGENT.name)) : false;
3353
+ if (agentExists === null) {
3354
+ ctx.logger.warn("[AI] Metadata service timed out checking metadata_assistant agent, skipping");
3355
+ } else if (!agentExists) {
3356
+ await withTimeout(metadataService.register("agent", METADATA_ASSISTANT_AGENT.name, METADATA_ASSISTANT_AGENT));
2713
3357
  console.log("[AI] Registered metadata_assistant agent to metadataService");
2714
3358
  ctx.logger.info("[AI] metadata_assistant agent registered");
2715
3359
  } else {
@@ -2719,22 +3363,71 @@ var AIServicePlugin = class {
2719
3363
  } catch (err) {
2720
3364
  ctx.logger.warn("[AI] Failed to register metadata_assistant agent", err instanceof Error ? { error: err.message, stack: err.stack } : { error: String(err) });
2721
3365
  }
3366
+ try {
3367
+ const skillExists = typeof metadataService.exists === "function" ? await withTimeout(metadataService.exists("skill", METADATA_AUTHORING_SKILL.name)) : false;
3368
+ if (skillExists === null) {
3369
+ ctx.logger.warn("[AI] Metadata service timed out checking metadata_authoring skill, skipping");
3370
+ } else if (!skillExists) {
3371
+ await withTimeout(metadataService.register("skill", METADATA_AUTHORING_SKILL.name, METADATA_AUTHORING_SKILL));
3372
+ ctx.logger.info("[AI] metadata_authoring skill registered");
3373
+ } else {
3374
+ ctx.logger.debug("[AI] metadata_authoring skill already exists, skipping auto-registration");
3375
+ }
3376
+ } catch (err) {
3377
+ ctx.logger.warn("[AI] Failed to register metadata_authoring skill", err instanceof Error ? { error: err.message } : { error: String(err) });
3378
+ }
2722
3379
  } catch (err) {
2723
3380
  ctx.logger.debug("[AI] Failed to register metadata tools", err instanceof Error ? err : void 0);
2724
3381
  }
2725
3382
  }
2726
3383
  await ctx.trigger("ai:ready", this.service);
3384
+ if (metadataService) {
3385
+ try {
3386
+ const objectql = ctx.getService("objectql");
3387
+ const registry = objectql?.registry;
3388
+ if (registry && typeof registry.listItems === "function") {
3389
+ const stackAgents = registry.listItems("agent");
3390
+ let bridged = 0;
3391
+ for (const entry of stackAgents) {
3392
+ const agent = entry?.content ?? entry;
3393
+ const agentName = agent?.name;
3394
+ if (!agentName || typeof agentName !== "string") continue;
3395
+ const exists = typeof metadataService.exists === "function" ? await withTimeout(metadataService.exists("agent", agentName)) : false;
3396
+ if (exists === true) continue;
3397
+ try {
3398
+ await withTimeout(metadataService.register("agent", agentName, agent));
3399
+ bridged++;
3400
+ } catch (err) {
3401
+ ctx.logger.warn(
3402
+ "[AI] Failed to bridge stack agent into metadata service (non-fatal)",
3403
+ err instanceof Error ? { agent: agentName, error: err.message } : { agent: agentName }
3404
+ );
3405
+ }
3406
+ }
3407
+ if (bridged > 0) {
3408
+ ctx.logger.info(`[AI] Bridged ${bridged} stack-defined agent(s) from ObjectQL registry`);
3409
+ console.log(`[AI] Bridged ${bridged} stack-defined agent(s) from ObjectQL registry`);
3410
+ }
3411
+ }
3412
+ } catch (err) {
3413
+ ctx.logger.debug("[AI] ObjectQL registry not available, skipping agent bridge", err instanceof Error ? err : void 0);
3414
+ }
3415
+ }
2727
3416
  const routes = buildAIRoutes(this.service, this.service.conversationService, ctx.logger);
2728
3417
  const toolRoutes = buildToolRoutes(this.service, ctx.logger);
2729
3418
  routes.push(...toolRoutes);
2730
3419
  ctx.logger.info(`[AI] Tool routes registered (${toolRoutes.length} routes)`);
2731
3420
  if (metadataService) {
2732
- const agentRuntime = new AgentRuntime(metadataService);
3421
+ const skillRegistry = new SkillRegistry(metadataService);
3422
+ const agentRuntime = new AgentRuntime(metadataService, skillRegistry);
2733
3423
  const agentRoutes = buildAgentRoutes(this.service, agentRuntime, ctx.logger);
2734
3424
  routes.push(...agentRoutes);
2735
3425
  ctx.logger.info(`[AI] Agent routes registered (${agentRoutes.length} routes)`);
3426
+ const assistantRoutes = buildAssistantRoutes(this.service, agentRuntime, skillRegistry, ctx.logger);
3427
+ routes.push(...assistantRoutes);
3428
+ ctx.logger.info(`[AI] Assistant (ambient) routes registered (${assistantRoutes.length} routes)`);
2736
3429
  } else {
2737
- ctx.logger.debug("[AI] Metadata service not available, skipping agent routes");
3430
+ ctx.logger.debug("[AI] Metadata service not available, skipping agent and assistant routes");
2738
3431
  }
2739
3432
  await ctx.trigger("ai:routes", routes);
2740
3433
  const kernel = ctx.getKernel();
@@ -2754,6 +3447,337 @@ var AIServicePlugin = class {
2754
3447
  init_data_tools();
2755
3448
  init_metadata_tools();
2756
3449
  init_metadata_tools();
3450
+
3451
+ // src/tools/list-packages.tool.ts
3452
+ import { defineTool as defineTool7 } from "@objectstack/spec/ai";
3453
+ var listPackagesTool = defineTool7({
3454
+ name: "list_packages",
3455
+ label: "List Packages",
3456
+ description: "Lists all installed packages in the system. Use this to see what packages are available before creating or modifying metadata. Packages are the containers that hold metadata.",
3457
+ category: "utility",
3458
+ builtIn: true,
3459
+ parameters: {
3460
+ type: "object",
3461
+ properties: {
3462
+ status: {
3463
+ type: "string",
3464
+ description: "Filter by package status",
3465
+ enum: ["installed", "disabled", "installing", "upgrading", "uninstalling", "error"]
3466
+ },
3467
+ enabled: {
3468
+ type: "boolean",
3469
+ description: "Filter by enabled state (true = only enabled, false = only disabled)"
3470
+ }
3471
+ },
3472
+ additionalProperties: false
3473
+ }
3474
+ });
3475
+
3476
+ // src/tools/get-package.tool.ts
3477
+ import { defineTool as defineTool8 } from "@objectstack/spec/ai";
3478
+ var getPackageTool = defineTool8({
3479
+ name: "get_package",
3480
+ label: "Get Package",
3481
+ description: "Gets detailed information about a specific installed package, including its manifest, metadata, and installation status.",
3482
+ category: "utility",
3483
+ builtIn: true,
3484
+ parameters: {
3485
+ type: "object",
3486
+ properties: {
3487
+ packageId: {
3488
+ type: "string",
3489
+ description: "Package identifier (reverse domain notation, e.g., com.acme.crm)"
3490
+ }
3491
+ },
3492
+ required: ["packageId"],
3493
+ additionalProperties: false
3494
+ }
3495
+ });
3496
+
3497
+ // src/tools/create-package.tool.ts
3498
+ import { defineTool as defineTool9 } from "@objectstack/spec/ai";
3499
+ var createPackageTool = defineTool9({
3500
+ name: "create_package",
3501
+ label: "Create Package",
3502
+ description: "Creates a new package (metadata container) with the specified manifest. All metadata in ObjectStack should belong to a package. Use this when starting new development or when the user wants to organize their metadata into a new module.",
3503
+ category: "utility",
3504
+ builtIn: true,
3505
+ parameters: {
3506
+ type: "object",
3507
+ properties: {
3508
+ id: {
3509
+ type: "string",
3510
+ description: "Package identifier in reverse domain notation (e.g., com.acme.crm, org.mycompany.sales)"
3511
+ },
3512
+ name: {
3513
+ type: "string",
3514
+ description: 'Human-readable package name (e.g., "CRM Application", "Sales Module")'
3515
+ },
3516
+ version: {
3517
+ type: "string",
3518
+ description: 'Semantic version (e.g., "1.0.0")',
3519
+ default: "1.0.0"
3520
+ },
3521
+ description: {
3522
+ type: "string",
3523
+ description: "Brief description of what this package provides"
3524
+ },
3525
+ namespace: {
3526
+ type: "string",
3527
+ description: "Namespace prefix for metadata (snake_case, e.g., crm, sales). If not provided, derived from package ID."
3528
+ },
3529
+ type: {
3530
+ type: "string",
3531
+ description: "Package type",
3532
+ enum: ["application", "plugin", "library", "template"],
3533
+ default: "application"
3534
+ }
3535
+ },
3536
+ required: ["id", "name"],
3537
+ additionalProperties: false
3538
+ }
3539
+ });
3540
+
3541
+ // src/tools/get-active-package.tool.ts
3542
+ import { defineTool as defineTool10 } from "@objectstack/spec/ai";
3543
+ var getActivePackageTool = defineTool10({
3544
+ name: "get_active_package",
3545
+ label: "Get Active Package",
3546
+ description: "Gets the currently active package in this conversation. The active package determines where new metadata will be created. Returns null if no package is set.",
3547
+ category: "utility",
3548
+ builtIn: true,
3549
+ parameters: {
3550
+ type: "object",
3551
+ properties: {},
3552
+ additionalProperties: false
3553
+ }
3554
+ });
3555
+
3556
+ // src/tools/set-active-package.tool.ts
3557
+ import { defineTool as defineTool11 } from "@objectstack/spec/ai";
3558
+ var setActivePackageTool = defineTool11({
3559
+ name: "set_active_package",
3560
+ label: "Set Active Package",
3561
+ description: "Sets the active package for this conversation. All subsequent metadata creation operations (objects, views, flows, etc.) will be associated with this package unless explicitly overridden.",
3562
+ category: "utility",
3563
+ builtIn: true,
3564
+ parameters: {
3565
+ type: "object",
3566
+ properties: {
3567
+ packageId: {
3568
+ type: "string",
3569
+ description: "Package identifier to set as active (e.g., com.acme.crm)"
3570
+ }
3571
+ },
3572
+ required: ["packageId"],
3573
+ additionalProperties: false
3574
+ }
3575
+ });
3576
+
3577
+ // src/tools/package-tools.ts
3578
+ var PACKAGE_TOOL_DEFINITIONS = [
3579
+ listPackagesTool,
3580
+ getPackageTool,
3581
+ createPackageTool,
3582
+ getActivePackageTool,
3583
+ setActivePackageTool
3584
+ ];
3585
+ var REVERSE_DOMAIN_RE = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/;
3586
+ var SNAKE_CASE_RE2 = /^[a-z_][a-z0-9_]*$/;
3587
+ var SEMVER_RE = /^\d+\.\d+\.\d+(-[a-z0-9]+(\.[a-z0-9]+)*)?$/;
3588
+ function isReverseDomain(value) {
3589
+ return REVERSE_DOMAIN_RE.test(value);
3590
+ }
3591
+ function isSnakeCase2(value) {
3592
+ return SNAKE_CASE_RE2.test(value);
3593
+ }
3594
+ function isSemVer(value) {
3595
+ return SEMVER_RE.test(value);
3596
+ }
3597
+ function deriveNamespace(packageId) {
3598
+ const parts = packageId.split(".");
3599
+ return parts[parts.length - 1];
3600
+ }
3601
+ function createListPackagesHandler(ctx) {
3602
+ return async (args) => {
3603
+ const { status, enabled } = args ?? {};
3604
+ const filter = {};
3605
+ if (status) filter.status = status;
3606
+ if (enabled !== void 0) filter.enabled = enabled;
3607
+ const packages = await ctx.packageRegistry.list(filter);
3608
+ const result = packages.map((pkg) => ({
3609
+ id: pkg.manifest.id,
3610
+ name: pkg.manifest.name,
3611
+ version: pkg.manifest.version,
3612
+ type: pkg.manifest.type,
3613
+ status: pkg.status,
3614
+ enabled: pkg.enabled,
3615
+ installedAt: pkg.installedAt,
3616
+ description: pkg.manifest.description
3617
+ }));
3618
+ return JSON.stringify({
3619
+ packages: result,
3620
+ total: result.length
3621
+ });
3622
+ };
3623
+ }
3624
+ function createGetPackageHandler(ctx) {
3625
+ return async (args) => {
3626
+ const { packageId } = args;
3627
+ if (!packageId) {
3628
+ return JSON.stringify({ error: "packageId is required" });
3629
+ }
3630
+ const pkg = await ctx.packageRegistry.get(packageId);
3631
+ if (!pkg) {
3632
+ return JSON.stringify({ error: `Package "${packageId}" not found` });
3633
+ }
3634
+ return JSON.stringify({
3635
+ id: pkg.manifest.id,
3636
+ name: pkg.manifest.name,
3637
+ version: pkg.manifest.version,
3638
+ type: pkg.manifest.type,
3639
+ status: pkg.status,
3640
+ enabled: pkg.enabled,
3641
+ installedAt: pkg.installedAt,
3642
+ updatedAt: pkg.updatedAt,
3643
+ description: pkg.manifest.description,
3644
+ namespace: pkg.manifest.namespace,
3645
+ dependencies: pkg.manifest.dependencies,
3646
+ registeredNamespaces: pkg.registeredNamespaces
3647
+ });
3648
+ };
3649
+ }
3650
+ function createCreatePackageHandler(ctx) {
3651
+ return async (args) => {
3652
+ const { id, name, version = "1.0.0", description, namespace, type = "application" } = args;
3653
+ if (!id || !name) {
3654
+ return JSON.stringify({ error: 'Both "id" and "name" are required' });
3655
+ }
3656
+ if (!isReverseDomain(id)) {
3657
+ return JSON.stringify({
3658
+ error: `Invalid package ID "${id}". Must be in reverse domain notation (e.g., com.acme.crm, org.mycompany.sales)`
3659
+ });
3660
+ }
3661
+ if (!isSemVer(version)) {
3662
+ return JSON.stringify({
3663
+ error: `Invalid version "${version}". Must be semantic version (e.g., 1.0.0, 2.1.3-beta)`
3664
+ });
3665
+ }
3666
+ const exists = await ctx.packageRegistry.exists(id);
3667
+ if (exists) {
3668
+ return JSON.stringify({ error: `Package "${id}" already exists` });
3669
+ }
3670
+ const derivedNamespace = namespace || deriveNamespace(id);
3671
+ if (!isSnakeCase2(derivedNamespace)) {
3672
+ return JSON.stringify({
3673
+ error: `Invalid namespace "${derivedNamespace}". Must be snake_case (e.g., crm, sales_module)`
3674
+ });
3675
+ }
3676
+ const manifest = {
3677
+ id,
3678
+ name,
3679
+ version,
3680
+ type,
3681
+ namespace: derivedNamespace,
3682
+ ...description ? { description } : {}
3683
+ };
3684
+ const installedPackage = await ctx.packageRegistry.install(manifest);
3685
+ if (ctx.conversationService && ctx.conversationId) {
3686
+ try {
3687
+ await ctx.conversationService.updateMetadata?.(ctx.conversationId, {
3688
+ activePackageId: id
3689
+ });
3690
+ } catch (err) {
3691
+ console.warn("Failed to set active package in conversation:", err);
3692
+ }
3693
+ }
3694
+ return JSON.stringify({
3695
+ packageId: installedPackage.manifest.id,
3696
+ name: installedPackage.manifest.name,
3697
+ version: installedPackage.manifest.version,
3698
+ namespace: installedPackage.manifest.namespace,
3699
+ status: installedPackage.status,
3700
+ message: `Package "${name}" created successfully and set as active package`
3701
+ });
3702
+ };
3703
+ }
3704
+ function createGetActivePackageHandler(ctx) {
3705
+ return async () => {
3706
+ if (!ctx.conversationService || !ctx.conversationId) {
3707
+ return JSON.stringify({
3708
+ activePackageId: null,
3709
+ message: "No conversation context available to track active package"
3710
+ });
3711
+ }
3712
+ try {
3713
+ const metadata = await ctx.conversationService.getMetadata?.(ctx.conversationId);
3714
+ const activePackageId = metadata?.activePackageId;
3715
+ if (!activePackageId) {
3716
+ return JSON.stringify({
3717
+ activePackageId: null,
3718
+ message: "No active package set. Use set_active_package or create a new package."
3719
+ });
3720
+ }
3721
+ const pkg = await ctx.packageRegistry.get(activePackageId);
3722
+ if (!pkg) {
3723
+ return JSON.stringify({
3724
+ activePackageId,
3725
+ error: `Active package "${activePackageId}" not found. It may have been uninstalled.`
3726
+ });
3727
+ }
3728
+ return JSON.stringify({
3729
+ activePackageId: pkg.manifest.id,
3730
+ name: pkg.manifest.name,
3731
+ version: pkg.manifest.version,
3732
+ namespace: pkg.manifest.namespace,
3733
+ type: pkg.manifest.type
3734
+ });
3735
+ } catch (err) {
3736
+ return JSON.stringify({
3737
+ error: `Failed to get active package: ${err.message}`
3738
+ });
3739
+ }
3740
+ };
3741
+ }
3742
+ function createSetActivePackageHandler(ctx) {
3743
+ return async (args) => {
3744
+ const { packageId } = args;
3745
+ if (!packageId) {
3746
+ return JSON.stringify({ error: "packageId is required" });
3747
+ }
3748
+ const pkg = await ctx.packageRegistry.get(packageId);
3749
+ if (!pkg) {
3750
+ return JSON.stringify({ error: `Package "${packageId}" not found` });
3751
+ }
3752
+ if (!ctx.conversationService || !ctx.conversationId) {
3753
+ return JSON.stringify({
3754
+ error: "No conversation context available. Cannot set active package."
3755
+ });
3756
+ }
3757
+ try {
3758
+ await ctx.conversationService.updateMetadata?.(ctx.conversationId, {
3759
+ activePackageId: packageId
3760
+ });
3761
+ return JSON.stringify({
3762
+ activePackageId: packageId,
3763
+ name: pkg.manifest.name,
3764
+ namespace: pkg.manifest.namespace,
3765
+ message: `Active package set to "${pkg.manifest.name}"`
3766
+ });
3767
+ } catch (err) {
3768
+ return JSON.stringify({
3769
+ error: `Failed to set active package: ${err.message}`
3770
+ });
3771
+ }
3772
+ };
3773
+ }
3774
+ function registerPackageTools(registry, context) {
3775
+ registry.register(listPackagesTool, createListPackagesHandler(context));
3776
+ registry.register(getPackageTool, createGetPackageHandler(context));
3777
+ registry.register(createPackageTool, createCreatePackageHandler(context));
3778
+ registry.register(getActivePackageTool, createGetActivePackageHandler(context));
3779
+ registry.register(setActivePackageTool, createSetActivePackageHandler(context));
3780
+ }
2757
3781
  export {
2758
3782
  AIService,
2759
3783
  AIServicePlugin,
@@ -2767,20 +3791,29 @@ export {
2767
3791
  METADATA_TOOL_DEFINITIONS,
2768
3792
  MemoryLLMAdapter,
2769
3793
  ObjectQLConversationService,
3794
+ PACKAGE_TOOL_DEFINITIONS,
3795
+ SkillRegistry,
2770
3796
  ToolRegistry,
2771
3797
  VercelLLMAdapter,
2772
3798
  addFieldTool,
2773
3799
  buildAIRoutes,
2774
3800
  buildAgentRoutes,
3801
+ buildAssistantRoutes,
2775
3802
  buildToolRoutes,
2776
3803
  createObjectTool,
3804
+ createPackageTool,
2777
3805
  deleteFieldTool,
2778
3806
  describeObjectTool,
2779
3807
  encodeStreamPart,
2780
3808
  encodeVercelDataStream,
3809
+ getActivePackageTool,
3810
+ getPackageTool,
2781
3811
  listObjectsTool,
3812
+ listPackagesTool,
2782
3813
  modifyFieldTool,
2783
3814
  registerDataTools,
2784
- registerMetadataTools
3815
+ registerMetadataTools,
3816
+ registerPackageTools,
3817
+ setActivePackageTool
2785
3818
  };
2786
3819
  //# sourceMappingURL=index.js.map