From c3698361d8e194489f2ad7f113355dc9b52d273d Mon Sep 17 00:00:00 2001 From: nishank-bhatnagar Date: Fri, 15 May 2026 10:13:09 -0700 Subject: [PATCH 1/3] throw lint error on complex_data_type field for primitive type --- .../src/lint/passes/complex-data-type.ts | 100 ++++++----- dialect/agentforce/src/tests/lint.test.ts | 166 ++++++++++++++++++ 2 files changed, 223 insertions(+), 43 deletions(-) diff --git a/dialect/agentforce/src/lint/passes/complex-data-type.ts b/dialect/agentforce/src/lint/passes/complex-data-type.ts index dcde7b6..7d8f68b 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 must not specify 'complex_data_type_name'. Only 'object' and 'list[object]' types support 'complex_data_type_name'.`, + DiagnosticSeverity.Error, + '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..8f31487 100644 --- a/dialect/agentforce/src/tests/lint.test.ts +++ b/dialect/agentforce/src/tests/lint.test.ts @@ -2344,3 +2344,169 @@ 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('errors 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 errors = diagnostics.filter( + d => d.code === 'complex-data-type-on-primitive' + ); + expect(errors).toHaveLength(1); + expect(errors[0].severity).toBe(DiagnosticSeverity.Error); + expect(errors[0].message).toContain("'amount'"); + expect(errors[0].message).toContain("'A'"); + expect(errors[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('errors 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 errors = diagnostics.filter( + d => d.code === 'complex-data-type-on-primitive' + ); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain("'message'"); + expect(errors[0].message).toContain("'string'"); + }); + + it.each([ + ['boolean'], + ['integer'], + ['id'], + ['date'], + ['datetime'], + ['time'], + ['timestamp'], + ['currency'], + ['long'], + ])('errors 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 errors = diagnostics.filter( + d => d.code === 'complex-data-type-on-primitive' + ); + expect(errors).toHaveLength(1); + expect(errors[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('errors 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 errors = diagnostics.filter( + d => d.code === 'complex-data-type-on-primitive' + ); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain("'list[string]'"); + }); + + it('reports both error and warning 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 errors = diagnostics.filter( + d => d.code === 'complex-data-type-on-primitive' + ); + const warnings = diagnostics.filter( + d => d.code === 'object-type-missing-schema' + ); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain("'amount'"); + expect(warnings).toHaveLength(1); + expect(warnings[0].message).toContain("'result'"); + }); +}); From ec1a043206fd0ed5b3ba89effaf385b65bfa323f Mon Sep 17 00:00:00 2001 From: nishank-bhatnagar Date: Mon, 18 May 2026 10:44:35 -0700 Subject: [PATCH 2/3] switched to warning instead of error for primitive types having complex data type field --- dialect/agentforce/src/lint/passes/complex-data-type.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dialect/agentforce/src/lint/passes/complex-data-type.ts b/dialect/agentforce/src/lint/passes/complex-data-type.ts index 7d8f68b..99a36e5 100644 --- a/dialect/agentforce/src/lint/passes/complex-data-type.ts +++ b/dialect/agentforce/src/lint/passes/complex-data-type.ts @@ -129,8 +129,8 @@ class ComplexDataTypePass implements LintPass { obj, lintDiagnostic( getDeclRange(obj), - `Action ${kind} '${paramName}' in '${actionName}' has primitive type '${typeText}' and must not specify 'complex_data_type_name'. Only 'object' and 'list[object]' types support 'complex_data_type_name'.`, - DiagnosticSeverity.Error, + `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' ) ); From b2e4f15a78924d2faf0b85adec0e0e956f3ea2ed Mon Sep 17 00:00:00 2001 From: nishank-bhatnagar Date: Mon, 18 May 2026 11:19:16 -0700 Subject: [PATCH 3/3] tests for validating warnings instead of errors --- dialect/agentforce/src/tests/lint.test.ts | 58 ++++++++++++----------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/dialect/agentforce/src/tests/lint.test.ts b/dialect/agentforce/src/tests/lint.test.ts index 8f31487..4c06b3d 100644 --- a/dialect/agentforce/src/tests/lint.test.ts +++ b/dialect/agentforce/src/tests/lint.test.ts @@ -2361,21 +2361,21 @@ ${outputs} |Do it `; - it('errors when a primitive input has complex_data_type_name', () => { + 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 errors = diagnostics.filter( + const warnings = diagnostics.filter( d => d.code === 'complex-data-type-on-primitive' ); - expect(errors).toHaveLength(1); - expect(errors[0].severity).toBe(DiagnosticSeverity.Error); - expect(errors[0].message).toContain("'amount'"); - expect(errors[0].message).toContain("'A'"); - expect(errors[0].message).toContain("'number'"); + 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', () => { @@ -2390,19 +2390,20 @@ ${outputs} ).toHaveLength(0); }); - it('errors when a primitive output has complex_data_type_name', () => { + 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 errors = diagnostics.filter( + const warnings = diagnostics.filter( d => d.code === 'complex-data-type-on-primitive' ); - expect(errors).toHaveLength(1); - expect(errors[0].message).toContain("'message'"); - expect(errors[0].message).toContain("'string'"); + 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([ @@ -2415,18 +2416,19 @@ ${outputs} ['timestamp'], ['currency'], ['long'], - ])('errors when primitive type %s has complex_data_type_name', primitive => { + ])('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 errors = diagnostics.filter( + const warnings = diagnostics.filter( d => d.code === 'complex-data-type-on-primitive' ); - expect(errors).toHaveLength(1); - expect(errors[0].message).toContain(`'${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', () => { @@ -2477,36 +2479,38 @@ ${outputs} ).toHaveLength(0); }); - it('errors on list[string] input with complex_data_type_name', () => { + 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 errors = diagnostics.filter( + const warnings = diagnostics.filter( d => d.code === 'complex-data-type-on-primitive' ); - expect(errors).toHaveLength(1); - expect(errors[0].message).toContain("'list[string]'"); + expect(warnings).toHaveLength(1); + expect(warnings[0].severity).toBe(DiagnosticSeverity.Warning); + expect(warnings[0].message).toContain("'list[string]'"); }); - it('reports both error and warning for mixed declarations', () => { + 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 errors = diagnostics.filter( + const primitiveWarnings = diagnostics.filter( d => d.code === 'complex-data-type-on-primitive' ); - const warnings = diagnostics.filter( + const missingSchemaWarnings = diagnostics.filter( d => d.code === 'object-type-missing-schema' ); - expect(errors).toHaveLength(1); - expect(errors[0].message).toContain("'amount'"); - expect(warnings).toHaveLength(1); - expect(warnings[0].message).toContain("'result'"); + 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'"); }); });