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
7 changes: 6 additions & 1 deletion src/prompts/system.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,15 @@ function compressText(text, maxLength) {

/**
* Check if property name is self-explanatory
* @param {string} name - Property name
* @param {string|null} name - Property name, or null for a schema root
* @returns {boolean} True if name is obvious
*/
function isObviousFromName(name) {
// The schema ROOT is compressed with key=null (see
// compressSchemaDescriptions(out.input_schema, null)): there is no property
// name to judge. Treat any non-string key as "not obvious" instead of
// crashing on name.toLowerCase() — that throw aborted the whole pass.
if (typeof name !== 'string') return false;
const obvious = [
'id', 'name', 'type', 'value', 'data', 'text', 'content',
'message', 'query', 'command', 'url', 'path', 'file', 'filename',
Expand Down
57 changes: 57 additions & 0 deletions test/tool-schema-compression.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,60 @@ describe('compressToolDescriptions preserves schema shape (issue #116)', () => {
assert.strictEqual(t.strict, false);
});
});

// Regression tests: compressSchemaDescriptions is invoked as
// compressSchemaDescriptions(schema, null) for the schema ROOT, where there is
// no property name to judge. Passing that null into isObviousFromName threw
// "TypeError: Cannot read properties of null (reading 'toLowerCase')", which
// the orchestrator catches and logs as
// "System prompt optimization failed, continuing with original"
// (src/orchestrator/index.js). Net effect: for every tool whose input schema
// carries a root-level `description`, minimal-mode compression silently did
// nothing — and the sibling optimizeSystemPrompt() call in the same try block
// was skipped too.
describe('compressToolDescriptions tolerates a root-level schema description', () => {
const rootDescriptionTool = () => ({
type: 'function',
function: {
name: 'tool_root',
parameters: {
type: 'object',
description: 'A deliberately long root-level schema description that exceeds thirty chars',
properties: {
command: { type: 'string', description: 'The command to run' },
custom: { type: 'string', description: 'A very long custom description that definitely needs trimming down' },
},
required: ['command'],
},
},
});

it('does not throw (was: TypeError on null.toLowerCase)', () => {
assert.doesNotThrow(() => compressToolDescriptions([rootDescriptionTool()], 'minimal'));
});

it('compresses the root description while preserving schema shape', () => {
const [t] = compressToolDescriptions([rootDescriptionTool()], 'minimal');
assert.strictEqual(t.input_schema.type, 'object');
assert.deepStrictEqual(t.input_schema.required, ['command']);
assert.strictEqual(typeof t.input_schema.description, 'string');
assert.ok(t.input_schema.description.length <= 30, t.input_schema.description);
// Nested behaviour is unchanged.
assert.ok(!('description' in t.input_schema.properties.command));
assert.ok(t.input_schema.properties.custom.description.length <= 30);
});

it('handles a root-level schema with no properties without throwing', () => {
assert.doesNotThrow(() => compressToolDescriptions([{
name: 'bare',
input_schema: { type: 'object', description: 'x'.repeat(80) },
}], 'minimal'));
});

it('handles a root-level schema array without throwing', () => {
assert.doesNotThrow(() => compressToolDescriptions([{
name: 'listy',
input_schema: [{ type: 'object', description: 'x'.repeat(80) }],
}], 'minimal'));
});
});
Loading