diff --git a/dialect/agentforce/src/lint/passes/complex-data-type.ts b/dialect/agentforce/src/lint/passes/complex-data-type.ts index dcde7b6..99a36e5 100644 --- a/dialect/agentforce/src/lint/passes/complex-data-type.ts +++ b/dialect/agentforce/src/lint/passes/complex-data-type.ts @@ -6,13 +6,16 @@ */ /** - * Complex data type warning rule for Agentforce. + * Complex data type rule for Agentforce. * - * Warns when object-type action inputs/outputs lack schema information: - * - Inputs: should have complex_data_type_name or schema - * - Outputs: should have complex_data_type_name + * Only `object` and `list[object]` declarations support a `complex_data_type_name`. * - * Diagnostic: object-type-missing-schema + * - When a whitelisted type (`object` / `list[object]`) lacks schema info: + * - Inputs: should have `complex_data_type_name` or `schema` (warning) + * - Outputs: should have `complex_data_type_name` (warning) + * - When a non-whitelisted (primitive) type has `complex_data_type_name`: error. + * + * Diagnostics: object-type-missing-schema, complex-data-type-on-primitive */ import type { AstNodeLike, AstRoot, NamedMap } from '@agentscript/language'; @@ -37,9 +40,14 @@ function getTypeText(decl: Record): string | null { return cst?.node?.text?.trim() ?? null; } -/** Check if a type string represents an object type. */ -function isObjectType(typeText: string): boolean { - return typeText === 'object' || typeText === 'list[object]'; +/** + * These required complex data types creates a warning without `complex_data_type_name` field. + * Anything outside this set is treated as a primitive and does not need a `complex_data_type_name`. + */ +const REQURIED_COMPLEX_DATA_TYPE = new Set(['object', 'list[object]']); + +function isComplexType(typeText: string): boolean { + return REQURIED_COMPLEX_DATA_TYPE.has(typeText); } /** Check if a field has a non-empty string value. */ @@ -86,60 +94,66 @@ class ComplexDataTypePass implements LintPass { if (!actBlock || typeof actBlock !== 'object') continue; const act = actBlock as Record; - this.checkInputs(act.inputs, actionName); - this.checkOutputs(act.outputs, actionName); + this.checkDecls(act.inputs, actionName, 'input'); + this.checkDecls(act.outputs, actionName, 'output'); } } } } - private checkInputs(inputs: unknown, actionName: string): void { - if (!inputs || !isNamedMap(inputs)) return; + private checkDecls( + decls: unknown, + actionName: string, + kind: 'input' | 'output' + ): void { + if (!decls || !isNamedMap(decls)) return; - for (const [paramName, decl] of inputs as NamedMap) { + for (const [paramName, decl] of decls as NamedMap) { if (!decl || typeof decl !== 'object') continue; const obj = decl as AstNodeLike; const typeText = getTypeText(obj as Record); - if (!typeText || !isObjectType(typeText)) continue; + if (!typeText) continue; const props = (obj as Record).properties as | Record | undefined; - if ( - !hasStringField(props, 'complex_data_type_name') && - !hasStringField(props, 'schema') - ) { - attachDiagnostic( - obj, - lintDiagnostic( - getDeclRange(obj), - `Action input '${paramName}' in '${actionName}' has type '${typeText}' but lacks 'complex_data_type_name' or 'schema'. Consider specifying the object schema for better type validation.`, - DiagnosticSeverity.Warning, - 'object-type-missing-schema' - ) - ); + const hasComplexDataTypeField = hasStringField( + props, + 'complex_data_type_name' + ); + + if (!isComplexType(typeText)) { + // Primitive types must NOT declare complex_data_type_name. + if (hasComplexDataTypeField) { + attachDiagnostic( + obj, + lintDiagnostic( + getDeclRange(obj), + `Action ${kind} '${paramName}' in '${actionName}' has primitive type '${typeText}' and does not require 'complex_data_type_name'. Only 'object' and 'list[object]' types require 'complex_data_type_name'.`, + DiagnosticSeverity.Warning, + 'complex-data-type-on-primitive' + ) + ); + } + continue; } - } - } - private checkOutputs(outputs: unknown, actionName: string): void { - if (!outputs || !isNamedMap(outputs)) return; - - for (const [outputName, decl] of outputs as NamedMap) { - if (!decl || typeof decl !== 'object') continue; - const obj = decl as AstNodeLike; - const typeText = getTypeText(obj as Record); - if (!typeText || !isObjectType(typeText)) continue; - - const props = (obj as Record).properties as - | Record - | undefined; - if (!hasStringField(props, 'complex_data_type_name')) { + // Complex types should declare schema info. + // Inputs may use `schema` as an alternative to `complex_data_type_name`. + const hasSchema = + hasComplexDataTypeField || + (kind === 'input' && hasStringField(props, 'schema')); + console.log('Schema: ', hasSchema); + if (!hasSchema) { + const required = + kind === 'input' + ? `'complex_data_type_name' or 'schema'` + : `'complex_data_type_name'`; attachDiagnostic( obj, lintDiagnostic( getDeclRange(obj), - `Action output '${outputName}' in '${actionName}' has type '${typeText}' but lacks 'complex_data_type_name'. Consider specifying the object schema for better type validation.`, + `Action ${kind} '${paramName}' in '${actionName}' has type '${typeText}' but lacks ${required}. Consider specifying the object schema for better type validation.`, DiagnosticSeverity.Warning, 'object-type-missing-schema' ) diff --git a/dialect/agentforce/src/tests/lint.test.ts b/dialect/agentforce/src/tests/lint.test.ts index 916c373..4c06b3d 100644 --- a/dialect/agentforce/src/tests/lint.test.ts +++ b/dialect/agentforce/src/tests/lint.test.ts @@ -2344,3 +2344,173 @@ subagent Order_Management: ); expect(warnings.length).toBeGreaterThan(0); }); + +describe('complex data type rule', () => { + const wrap = (inputs: string, outputs: string): string => ` +subagent S: + description: "S" + actions: + A: + description: "A" + inputs: +${inputs} + outputs: +${outputs} + reasoning: + instructions: -> + |Do it +`; + + it('warns when a primitive input has complex_data_type_name', () => { + const diagnostics = runSecurityLint( + wrap( + ` amount: number\n complex_data_type_name: "lightning__objectType"\n`, + ` ok: object\n complex_data_type_name: "lightning__objectType"\n` + ) + ); + const warnings = diagnostics.filter( + d => d.code === 'complex-data-type-on-primitive' + ); + expect(warnings).toHaveLength(1); + expect(warnings[0].severity).toBe(DiagnosticSeverity.Warning); + expect(warnings[0].message).toContain("'amount'"); + expect(warnings[0].message).toContain("'A'"); + expect(warnings[0].message).toContain("'number'"); + }); + + it('does not flag primitive inputs without complex_data_type_name', () => { + const diagnostics = runSecurityLint( + wrap( + ` amount: number\n description: "an amount"\n`, + ` ok: object\n complex_data_type_name: "lightning__objectType"\n` + ) + ); + expect( + diagnostics.filter(d => d.code === 'complex-data-type-on-primitive') + ).toHaveLength(0); + }); + + it('warns when a primitive output has complex_data_type_name', () => { + const diagnostics = runSecurityLint( + wrap( + ` in_ok: object\n complex_data_type_name: "lightning__objectType"\n`, + ` message: string\n complex_data_type_name: "lightning__objectType"\n` + ) + ); + const warnings = diagnostics.filter( + d => d.code === 'complex-data-type-on-primitive' + ); + expect(warnings).toHaveLength(1); + expect(warnings[0].severity).toBe(DiagnosticSeverity.Warning); + expect(warnings[0].message).toContain("'message'"); + expect(warnings[0].message).toContain("'string'"); + }); + + it.each([ + ['boolean'], + ['integer'], + ['id'], + ['date'], + ['datetime'], + ['time'], + ['timestamp'], + ['currency'], + ['long'], + ])('warns when primitive type %s has complex_data_type_name', primitive => { + const diagnostics = runSecurityLint( + wrap( + ` v: ${primitive}\n complex_data_type_name: "lightning__objectType"\n`, + ` ok: object\n complex_data_type_name: "lightning__objectType"\n` + ) + ); + const warnings = diagnostics.filter( + d => d.code === 'complex-data-type-on-primitive' + ); + expect(warnings).toHaveLength(1); + expect(warnings[0].severity).toBe(DiagnosticSeverity.Warning); + expect(warnings[0].message).toContain(`'${primitive}'`); + }); + + it('does not flag object input with complex_data_type_name', () => { + const diagnostics = runSecurityLint( + wrap( + ` order: object\n complex_data_type_name: "OrderRecord"\n`, + ` ok: object\n complex_data_type_name: "lightning__objectType"\n` + ) + ); + expect( + diagnostics.filter( + d => + d.code === 'complex-data-type-on-primitive' || + d.code === 'object-type-missing-schema' + ) + ).toHaveLength(0); + }); + + it('does not flag object input that uses schema:', () => { + const diagnostics = runSecurityLint( + wrap( + ` order: object\n schema: "schema://order_schema"\n`, + ` ok: object\n complex_data_type_name: "lightning__objectType"\n` + ) + ); + expect( + diagnostics.filter( + d => + d.code === 'complex-data-type-on-primitive' || + d.code === 'object-type-missing-schema' + ) + ).toHaveLength(0); + }); + + it('does not flag list[object] output with complex_data_type_name', () => { + const diagnostics = runSecurityLint( + wrap( + ` ok: object\n complex_data_type_name: "lightning__objectType"\n`, + ` items: list[object]\n complex_data_type_name: "OrderRecord"\n` + ) + ); + expect( + diagnostics.filter( + d => + d.code === 'complex-data-type-on-primitive' || + d.code === 'object-type-missing-schema' + ) + ).toHaveLength(0); + }); + + it('warns on list[string] input with complex_data_type_name', () => { + const diagnostics = runSecurityLint( + wrap( + ` tags: list[string]\n complex_data_type_name: "lightning__objectType"\n`, + ` ok: object\n complex_data_type_name: "lightning__objectType"\n` + ) + ); + const warnings = diagnostics.filter( + d => d.code === 'complex-data-type-on-primitive' + ); + expect(warnings).toHaveLength(1); + expect(warnings[0].severity).toBe(DiagnosticSeverity.Warning); + expect(warnings[0].message).toContain("'list[string]'"); + }); + + it('reports both warnings for mixed declarations', () => { + const diagnostics = runSecurityLint( + wrap( + ` amount: number\n complex_data_type_name: "lightning__objectType"\n`, + ` result: object\n description: "bare object output"\n` + ) + ); + const primitiveWarnings = diagnostics.filter( + d => d.code === 'complex-data-type-on-primitive' + ); + const missingSchemaWarnings = diagnostics.filter( + d => d.code === 'object-type-missing-schema' + ); + expect(primitiveWarnings).toHaveLength(1); + expect(primitiveWarnings[0].severity).toBe(DiagnosticSeverity.Warning); + expect(primitiveWarnings[0].message).toContain("'amount'"); + expect(missingSchemaWarnings).toHaveLength(1); + expect(missingSchemaWarnings[0].message).toContain("'result'"); + }); +});