Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 57 additions & 43 deletions dialect/agentforce/src/lint/passes/complex-data-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -37,9 +40,14 @@ function getTypeText(decl: Record<string, unknown>): 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<string>(['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. */
Expand Down Expand Up @@ -86,60 +94,66 @@ class ComplexDataTypePass implements LintPass {
if (!actBlock || typeof actBlock !== 'object') continue;
const act = actBlock as Record<string, unknown>;

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<unknown>) {
for (const [paramName, decl] of decls as NamedMap<unknown>) {
if (!decl || typeof decl !== 'object') continue;
const obj = decl as AstNodeLike;
const typeText = getTypeText(obj as Record<string, unknown>);
if (!typeText || !isObjectType(typeText)) continue;
if (!typeText) continue;

const props = (obj as Record<string, unknown>).properties as
| Record<string, unknown>
| 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<unknown>) {
if (!decl || typeof decl !== 'object') continue;
const obj = decl as AstNodeLike;
const typeText = getTypeText(obj as Record<string, unknown>);
if (!typeText || !isObjectType(typeText)) continue;

const props = (obj as Record<string, unknown>).properties as
| Record<string, unknown>
| 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'
)
Expand Down
170 changes: 170 additions & 0 deletions dialect/agentforce/src/tests/lint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'");
});
});
Loading