Skip to content
Open
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
3 changes: 2 additions & 1 deletion packages/compiler/src/ast-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,8 @@ export function extractDictExpression(
}
}

return Object.keys(result).length > 0 ? result : undefined;
// An empty dictionary is a value, including inside other dictionaries or lists.
return result;
}

/**
Expand Down
80 changes: 79 additions & 1 deletion packages/compiler/test/ast-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,17 @@ import { DiagnosticSeverity } from '@agentscript/types';
import {
type Expression,
AtIdentifier,
DictLiteral,
ListLiteral,
StringLiteral,
Identifier,
MemberExpression,
} from '@agentscript/language';
import { resolveAtReference } from '../src/ast-helpers.js';
import {
resolveAtReference,
extractDictExpression,
extractExpressionValue,
} from '../src/ast-helpers.js';
import { CompilerContext } from '../src/compiler-context.js';

let ctx: CompilerContext;
Expand Down Expand Up @@ -72,3 +79,74 @@ describe('resolveAtReference', () => {
).toBe(true);
});
});

describe('issue #101: dictionary extraction contract', () => {
it('extracts an empty dictionary as an object', () => {
expect(extractDictExpression(new DictLiteral([]))).toStrictEqual({});
});

it('keeps missing dictionary input undefined', () => {
expect(extractDictExpression(undefined)).toBeUndefined();
});

it('keeps non-dictionary input undefined', () => {
expect(extractDictExpression(new StringLiteral('value'))).toBeUndefined();
});

it('keeps unsupported expressions undefined', () => {
expect(
extractExpressionValue(new Identifier('unresolved'))
).toBeUndefined();
});

it('preserves empty dictionary values while omitting unsupported values', () => {
const expression = new DictLiteral([
{
key: new StringLiteral('empty'),
value: new DictLiteral([]),
__diagnostics: [],
},
{
key: new StringLiteral('unsupported'),
value: new Identifier('unresolved'),
__diagnostics: [],
},
{
key: new StringLiteral('kept'),
value: new StringLiteral('value'),
__diagnostics: [],
},
]);

expect(extractExpressionValue(expression)).toStrictEqual({
empty: {},
kept: 'value',
});
});

it('preserves empty list elements while omitting unsupported expressions', () => {
const expression = new ListLiteral([
new DictLiteral([]),
new Identifier('unresolved'),
new StringLiteral('value'),
new DictLiteral([]),
]);

expect(extractExpressionValue(expression)).toStrictEqual([{}, 'value', {}]);
});

it('preserves empty plain-array elements while omitting unsupported expressions', () => {
const expressions = [
new DictLiteral([]),
new Identifier('unresolved'),
new StringLiteral('value'),
new DictLiteral([]),
];

expect(extractExpressionValue(expressions)).toStrictEqual([
{},
'value',
{},
]);
});
});
27 changes: 27 additions & 0 deletions packages/compiler/test/live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,30 @@ live:
});
});
});

describe('issue #101: empty objects in live parameters', () => {
it.each([
{ name: 'an empty dictionary value', literal: '{}', expected: {} },
{
name: 'empty objects at their original list positions',
literal: '[{}, {"n": 1}, {}]',
expected: [{}, { n: 1 }, {}],
},
])('preserves $name', ({ literal, expected }) => {
const { output, diagnostics } = compile(
parseSource(
agent(`
live:
enabled: True
params:
profile: ${literal}
`)
)
);

expect(diagnostics.filter(d => d.severity === 1)).toEqual([]);
expect(output.agent_version.live?.params).toStrictEqual({
profile: expected,
});
});
});
58 changes: 58 additions & 0 deletions packages/compiler/test/model-config-inheritance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,3 +525,61 @@ subagent topic4:
});
});
});

describe('issue #101: empty model parameter values', () => {
it('preserves nested empty values inherited from global parameters', () => {
const { output, diagnostics } = compileSource(`
config:
developer_name: "TestAgent"
model_config:
model: "model://gpt-4"
params:
profile: {"empty": {}, "full": {"n": 1}}
start_agent main:
description: "Entry point"
subagent support:
description: "Support"
reasoning:
instructions: ->
| Help with requests
`);

expect(diagnostics.filter(d => d.severity === 1)).toEqual([]);
expect(
findNode(output, 'support', 'subagent').model_configuration
).toStrictEqual({
model_ref: 'gpt-4',
configuration: { profile: { empty: {}, full: { n: 1 } } },
});
});

it('allows an empty topic parameter to override a populated global value', () => {
const { output, diagnostics } = compileSource(`
config:
developer_name: "TestAgent"
model_config:
model: "model://gpt-4"
params:
profile: {"n": 1}
temperature: 0.7
start_agent main:
description: "Entry point"
subagent support:
description: "Support"
model_config:
params:
profile: {}
reasoning:
instructions: ->
| Help with requests
`);

expect(diagnostics.filter(d => d.severity === 1)).toEqual([]);
expect(
findNode(output, 'support', 'subagent').model_configuration
).toStrictEqual({
model_ref: 'gpt-4',
configuration: { profile: {}, temperature: 0.7 },
});
});
});
48 changes: 48 additions & 0 deletions packages/compiler/test/state-variables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1253,3 +1253,51 @@ variables:
expect(findStateVar(result, 'obj_no_default')!.default).toBeUndefined();
});
});

describe('issue #101: empty objects in variable defaults', () => {
it.each([
{
name: 'an empty child beside a populated child',
type: 'object',
literal: '{"empty": {}, "full": {"n": 1}}',
expected: { empty: {}, full: { n: 1 } },
},
{
name: 'a recursive chain ending in an empty object',
type: 'object',
literal: '{"a": {"b": {}}}',
expected: { a: { b: {} } },
},
{
name: 'empty objects at their original list positions',
type: 'list[object]',
literal: '[{}, {"n": 1}, {}]',
expected: [{}, { n: 1 }, {}],
},
{
name: 'a list containing only empty objects',
type: 'list[object]',
literal: '[{}, {}]',
expected: [{}, {}],
},
{
name: 'an empty object inside a nested list',
type: 'object',
literal: '{"a": [{}]}',
expected: { a: [{}] },
},
])('preserves $name', ({ type, literal, expected }) => {
const result = compileSource(
agentSource(`
variables:
payload: mutable ${type} = ${literal}`)
);

expect(
result.diagnostics.filter(d => d.severity === DiagnosticSeverity.Error)
).toEqual([]);
const variable = findStateVar(result, 'payload');
expect(variable).toBeDefined();
expect(variable!.default).toStrictEqual(expected);
});
});
Loading