Skip to content

Commit 9bea8d6

Browse files
Claudehotlong
andauthored
feat(service-ai): complete package-aware metadata tools implementation
- Add packageId parameter to all metadata tool definitions - Implement package resolution helpers (getActivePackageId, resolvePackageId) - Update all metadata handlers to validate and use package context - Enforce read-only protection for filesystem-based packages - Return packageId in all tool responses for tracking Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/69c4b16a-f22c-45a6-88a1-e6b2d10527d8 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 1d463dc commit 9bea8d6

4 files changed

Lines changed: 120 additions & 4 deletions

File tree

packages/services/service-ai/src/tools/add-field.tool.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export const addFieldTool = defineTool({
2020
parameters: {
2121
type: 'object',
2222
properties: {
23+
packageId: {
24+
type: 'string',
25+
description: 'Package ID that owns the target object (e.g., com.acme.crm). If not provided, uses the active package from conversation context.',
26+
},
2327
objectName: {
2428
type: 'string',
2529
description: 'Target object machine name (snake_case)',

packages/services/service-ai/src/tools/delete-field.tool.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export const deleteFieldTool = defineTool({
2323
parameters: {
2424
type: 'object',
2525
properties: {
26+
packageId: {
27+
type: 'string',
28+
description: 'Package ID that owns the target object (e.g., com.acme.crm). If not provided, uses the active package from conversation context.',
29+
},
2630
objectName: {
2731
type: 'string',
2832
description: 'Target object machine name (snake_case)',

packages/services/service-ai/src/tools/metadata-tools.ts

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,76 @@ function isSnakeCase(value: string): boolean {
6969
return SNAKE_CASE_RE.test(value);
7070
}
7171

72+
// ---------------------------------------------------------------------------
73+
// Package Resolution Helpers
74+
// ---------------------------------------------------------------------------
75+
76+
/**
77+
* Retrieves the active package ID from the conversation context.
78+
* Returns null if no conversation service is available or no active package is set.
79+
*/
80+
async function getActivePackageId(ctx: MetadataToolContext): Promise<string | null> {
81+
if (!ctx.conversationService?.getMetadata || !ctx.conversationId) {
82+
return null;
83+
}
84+
85+
const metadata = await ctx.conversationService.getMetadata(ctx.conversationId);
86+
return (metadata?.activePackageId as string) ?? null;
87+
}
88+
89+
/**
90+
* Resolves the package ID to use for a metadata operation.
91+
* Priority: explicit packageId > active package from conversation > error
92+
*
93+
* Also validates that the package exists and checks if it's read-only.
94+
*
95+
* @returns Object with packageId or error message
96+
*/
97+
async function resolvePackageId(
98+
ctx: MetadataToolContext,
99+
explicitPackageId?: string,
100+
): Promise<{ packageId: string | null; error?: string; warning?: string }> {
101+
let packageId: string | null = null;
102+
103+
// 1. Try explicit packageId parameter
104+
if (explicitPackageId) {
105+
packageId = explicitPackageId;
106+
} else {
107+
// 2. Try active package from conversation
108+
packageId = await getActivePackageId(ctx);
109+
}
110+
111+
// If no package ID could be resolved, return error
112+
if (!packageId) {
113+
return {
114+
packageId: null,
115+
error: 'No package specified. Either provide packageId parameter or set an active package using set_active_package.',
116+
};
117+
}
118+
119+
// Validate package exists (if registry is available)
120+
if (ctx.packageRegistry) {
121+
const exists = await ctx.packageRegistry.exists(packageId);
122+
if (!exists) {
123+
return {
124+
packageId: null,
125+
error: `Package "${packageId}" not found. Use list_packages to see available packages or create_package to create a new one.`,
126+
};
127+
}
128+
129+
// Check if package is read-only (code-based)
130+
const pkg = await ctx.packageRegistry.get(packageId);
131+
if (pkg?.manifest.source === 'filesystem') {
132+
return {
133+
packageId: null,
134+
error: `Package "${packageId}" is read-only (loaded from code). Only database packages can be modified. Use create_package to create a new database package.`,
135+
};
136+
}
137+
}
138+
139+
return { packageId };
140+
}
141+
72142
// ---------------------------------------------------------------------------
73143
// Context — injected once at registration time
74144
// ---------------------------------------------------------------------------
@@ -104,9 +174,10 @@ export interface MetadataToolContext {
104174

105175
function createCreateObjectHandler(ctx: MetadataToolContext): ToolHandler {
106176
return async (args) => {
107-
const { name, label, fields, enableFeatures } = args as {
177+
const { name, label, packageId: explicitPackageId, fields, enableFeatures } = args as {
108178
name: string;
109179
label: string;
180+
packageId?: string;
110181
fields?: Array<{ name: string; label?: string; type: string; required?: boolean }>;
111182
enableFeatures?: Record<string, boolean>;
112183
};
@@ -115,6 +186,13 @@ function createCreateObjectHandler(ctx: MetadataToolContext): ToolHandler {
115186
return JSON.stringify({ error: 'Both "name" and "label" are required' });
116187
}
117188

189+
// Resolve package ID
190+
const resolved = await resolvePackageId(ctx, explicitPackageId);
191+
if (resolved.error) {
192+
return JSON.stringify({ error: resolved.error });
193+
}
194+
const packageId = resolved.packageId!;
195+
118196
// Validate snake_case name
119197
if (!isSnakeCase(name)) {
120198
return JSON.stringify({ error: `Invalid object name "${name}". Must be snake_case.` });
@@ -152,6 +230,7 @@ function createCreateObjectHandler(ctx: MetadataToolContext): ToolHandler {
152230
const objectDef: Record<string, unknown> = {
153231
name,
154232
label,
233+
packageId,
155234
...(Object.keys(fieldMap).length > 0 ? { fields: fieldMap } : {}),
156235
...(enableFeatures ? { enable: enableFeatures } : {}),
157236
};
@@ -161,14 +240,15 @@ function createCreateObjectHandler(ctx: MetadataToolContext): ToolHandler {
161240
return JSON.stringify({
162241
name,
163242
label,
243+
packageId,
164244
fieldCount: Object.keys(fieldMap).length,
165245
});
166246
};
167247
}
168248

169249
function createAddFieldHandler(ctx: MetadataToolContext): ToolHandler {
170250
return async (args) => {
171-
const { objectName, name, label, type, required, defaultValue, options, reference } = args as {
251+
const { objectName, name, label, type, required, defaultValue, options, reference, packageId: explicitPackageId } = args as {
172252
objectName: string;
173253
name: string;
174254
label?: string;
@@ -177,12 +257,19 @@ function createAddFieldHandler(ctx: MetadataToolContext): ToolHandler {
177257
defaultValue?: unknown;
178258
options?: Array<{ label: string; value: string }>;
179259
reference?: string;
260+
packageId?: string;
180261
};
181262

182263
if (!objectName || !name || !type) {
183264
return JSON.stringify({ error: '"objectName", "name", and "type" are required' });
184265
}
185266

267+
// Resolve package ID (for validation and tracking)
268+
const resolved = await resolvePackageId(ctx, explicitPackageId);
269+
if (resolved.error) {
270+
return JSON.stringify({ error: resolved.error });
271+
}
272+
186273
// Validate snake_case names
187274
if (!isSnakeCase(objectName)) {
188275
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
@@ -238,22 +325,30 @@ function createAddFieldHandler(ctx: MetadataToolContext): ToolHandler {
238325
objectName,
239326
fieldName: name,
240327
fieldType: type,
328+
packageId: resolved.packageId,
241329
});
242330
};
243331
}
244332

245333
function createModifyFieldHandler(ctx: MetadataToolContext): ToolHandler {
246334
return async (args) => {
247-
const { objectName, fieldName, changes } = args as {
335+
const { objectName, fieldName, changes, packageId: explicitPackageId } = args as {
248336
objectName: string;
249337
fieldName: string;
250338
changes: Record<string, unknown>;
339+
packageId?: string;
251340
};
252341

253342
if (!objectName || !fieldName || !changes) {
254343
return JSON.stringify({ error: '"objectName", "fieldName", and "changes" are required' });
255344
}
256345

346+
// Resolve package ID (for validation and tracking)
347+
const resolved = await resolvePackageId(ctx, explicitPackageId);
348+
if (resolved.error) {
349+
return JSON.stringify({ error: resolved.error });
350+
}
351+
257352
// Validate snake_case names
258353
if (!isSnakeCase(objectName)) {
259354
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
@@ -287,21 +382,29 @@ function createModifyFieldHandler(ctx: MetadataToolContext): ToolHandler {
287382
objectName,
288383
fieldName,
289384
updatedProperties: Object.keys(changes),
385+
packageId: resolved.packageId,
290386
});
291387
};
292388
}
293389

294390
function createDeleteFieldHandler(ctx: MetadataToolContext): ToolHandler {
295391
return async (args) => {
296-
const { objectName, fieldName } = args as {
392+
const { objectName, fieldName, packageId: explicitPackageId } = args as {
297393
objectName: string;
298394
fieldName: string;
395+
packageId?: string;
299396
};
300397

301398
if (!objectName || !fieldName) {
302399
return JSON.stringify({ error: '"objectName" and "fieldName" are required' });
303400
}
304401

402+
// Resolve package ID (for validation and tracking)
403+
const resolved = await resolvePackageId(ctx, explicitPackageId);
404+
if (resolved.error) {
405+
return JSON.stringify({ error: resolved.error });
406+
}
407+
305408
// Validate snake_case names
306409
if (!isSnakeCase(objectName)) {
307410
return JSON.stringify({ error: `Invalid object name "${objectName}". Must be snake_case.` });
@@ -332,6 +435,7 @@ function createDeleteFieldHandler(ctx: MetadataToolContext): ToolHandler {
332435
objectName,
333436
fieldName,
334437
success: true,
438+
packageId: resolved.packageId,
335439
});
336440
};
337441
}

packages/services/service-ai/src/tools/modify-field.tool.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ export const modifyFieldTool = defineTool({
1919
parameters: {
2020
type: 'object',
2121
properties: {
22+
packageId: {
23+
type: 'string',
24+
description: 'Package ID that owns the target object (e.g., com.acme.crm). If not provided, uses the active package from conversation context.',
25+
},
2226
objectName: {
2327
type: 'string',
2428
description: 'Target object machine name (snake_case)',

0 commit comments

Comments
 (0)