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
73 changes: 73 additions & 0 deletions dialect/agentscript/src/tests/lint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2200,6 +2200,79 @@ start_agent main:
);
expect(errors).toHaveLength(0);
});

it('rejects bare identifier as invocation target', () => {
const diagnostics = runLint(`
start_agent main:
description: "test"
reasoning:
instructions: ->
|do it
actions:
hello: world
`);
const errors = diagnostics.filter(
d => d.code === 'constraint-resolved-type'
);
expect(errors).toHaveLength(1);
expect(errors[0].message).toContain('@actions');
expect(errors[0].message).toContain('@utils');
expect(errors[0].message).toContain('Identifier');
});

it('rejects string literal as invocation target', () => {
const diagnostics = runLint(`
start_agent main:
description: "test"
reasoning:
instructions: ->
|do it
actions:
hello: "world"
`);
const errors = diagnostics.filter(
d => d.code === 'constraint-resolved-type'
);
expect(errors).toHaveLength(1);
expect(errors[0].message).toContain('@actions');
expect(errors[0].message).toContain('StringLiteral');
});

it('rejects ellipsis as invocation target', () => {
const diagnostics = runLint(`
start_agent main:
description: "test"
reasoning:
instructions: ->
|do it
actions:
hello: ...
`);
const errors = diagnostics.filter(
d => d.code === 'constraint-resolved-type'
);
expect(errors).toHaveLength(1);
expect(errors[0].message).toContain('@actions');
expect(errors[0].message).toContain('Ellipsis');
});

it('rejects lone @utils as invocation target', () => {
const diagnostics = runLint(`
start_agent main:
description: "test"
reasoning:
instructions: ->
|do it
actions:
hello: @utils
`);
const errors = diagnostics.filter(
d => d.code === 'constraint-resolved-type'
);
expect(errors).toHaveLength(1);
expect(errors[0].message).toContain('@actions');
expect(errors[0].message).toContain('AtIdentifier');
});
});

// ============================================================================
Expand Down
48 changes: 48 additions & 0 deletions packages/language/src/lint/constraint-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,29 @@ function resolvedTypeLabel(resolvedType: BlockCapability): string {
return resolvedType;
}

/**
* Format the list of namespaces that satisfy a resolvedType constraint for a
* diagnostic message. Pulls capability-bearing namespaces from the dialect
* ctx, drops aliases (e.g. start_agent → subagent), and includes @utils when
* the dialect declares it (the only tool-bearing global scope today).
*/
function formatAllowedNamespaces(
resolvedType: BlockCapability,
ctx: SchemaContext
): string {
const capability = resolveCapabilityNamespaces(resolvedType, ctx);
const aliasKeys = new Set(Object.keys(ctx.info.aliases));
const names = new Set<string>();
if (capability)
for (const n of capability) if (!aliasKeys.has(n)) names.add(n);
if (ctx.globalScopes.has('utils')) names.add('utils');
if (names.size === 0) return '@namespace.member';
return [...names]
.sort()
.map(n => `@${n}`)
.join(', ');
}

/** Validate a field value against its constraint metadata, attaching diagnostics to the AST node. */
function validateConstraints(
value: unknown,
Expand Down Expand Up @@ -155,6 +178,31 @@ function validateConstraints(
}
}

// resolvedType also rejects non-MemberExpression colinear values (bare
// identifiers, string literals, ellipsis, lone @ns). These compile to a
// no-op tool at runtime because compile-tool.ts only rebinds target for
// @actions.X / @connected_subagent.X member expressions. Enumerate the
// valid namespaces from the dialect ctx so the message stays correct
// across dialects (agentscript / agentforce / agentfabric ship different
// sets).
if (constraints.resolvedType && !(node instanceof MemberExpression)) {
validatedRefs?.add(node);
const kind = (node as { __kind?: string }).__kind ?? 'unknown';
const allowed = ctx
? formatAllowedNamespaces(constraints.resolvedType, ctx)
: '@namespace.member';
attachDiagnostic(
node,
lintDiagnostic(
range,
`'${fieldName}' must be a member of ${allowed}. Got ${kind}.`,
DiagnosticSeverity.Error,
'constraint-resolved-type'
)
);
return;
}

// Validate allowedNamespaces for ReferenceValue (MemberExpression) fields.
// Early return is safe: MemberExpression nodes won't match extractStaticValue()
// (which only handles NumberValue/BooleanValue/StringLiteral), so no downstream
Expand Down
Loading