Skip to content

Commit 068f2cf

Browse files
committed
fix: drop a malformed intent field instead of failing the tool call
The injected intent field is required and string-typed, so a model that emitted a non-string value left it in the arguments and the call died in schema validation. The field is now removed whenever present, and an intent is produced only from a usable string.
1 parent e5ff2c3 commit 068f2cf

2 files changed

Lines changed: 23 additions & 8 deletions

File tree

packages/agent-core/src/loop/tool-intent.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,14 @@ export function extractIntentFromArgs(args: unknown): {
4747
args: unknown;
4848
intent: string | undefined;
4949
} {
50-
if (!isPlainRecord(args) || typeof args[INTENT_FIELD] !== 'string') {
50+
if (!isPlainRecord(args) || !Object.hasOwn(args, INTENT_FIELD)) {
5151
return { args, intent: undefined };
5252
}
5353
const { [INTENT_FIELD]: rawIntent, ...rest } = args;
54-
return { args: rest, intent: sanitizeIntent(rawIntent as string) };
54+
return {
55+
args: rest,
56+
intent: typeof rawIntent === 'string' ? sanitizeIntent(rawIntent) : undefined,
57+
};
5558
}
5659

5760
export function sanitizeIntent(raw: string): string | undefined {

packages/agent-core/test/loop/tool-intent.test.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,17 @@ const SANITIZER_FIXTURES = [
1919

2020
function makeTool(
2121
name = 'test',
22-
parameters: Record<string, unknown> = {
22+
parameters?: Record<string, unknown>,
23+
): ExecutableTool {
24+
const toolParameters = parameters ?? {
2325
type: 'object',
2426
properties: { value: { type: 'string' } },
2527
required: ['value'],
26-
},
27-
): ExecutableTool {
28+
};
2829
return {
2930
name,
3031
description: 'Test tool.',
31-
parameters,
32+
parameters: toolParameters,
3233
resolveExecution: () => ({
3334
approvalRule: name,
3435
execute: () => Promise.resolve({ output: 'ok' }),
@@ -107,6 +108,13 @@ describe('tool intent extraction', () => {
107108
});
108109
});
109110

111+
it.each([1, null])('removes a non-string intent value %j', (intent) => {
112+
expect(extractIntentFromArgs({ i: intent, value: 1 })).toEqual({
113+
args: { value: 1 },
114+
intent: undefined,
115+
});
116+
});
117+
110118
it('passes non-object args through', () => {
111119
const args = ['value'];
112120
expect(extractIntentFromArgs(args)).toEqual({ args, intent: undefined });
@@ -124,8 +132,12 @@ describe('tool intent sanitization', () => {
124132
});
125133

126134
it('caps the result by code points', () => {
127-
const sanitized = sanitizeIntent('🙂'.repeat(INTENT_MAX_LENGTH + 1));
128-
expect(Array.from(sanitized ?? '')).toHaveLength(INTENT_MAX_LENGTH);
135+
// U+1F642 SLIGHTLY SMILING FACE. Must stay a surrogate pair: this test proves
136+
// the cap counts code points rather than UTF-16 code units.
137+
const surrogatePair = String.fromCodePoint(0x1f642);
138+
const sanitized = sanitizeIntent(surrogatePair.repeat(INTENT_MAX_LENGTH + 1));
139+
expect(typeof sanitized).toBe('string');
140+
expect(Array.from(sanitized as string)).toHaveLength(INTENT_MAX_LENGTH);
129141
});
130142

131143
it('returns undefined for an empty result', () => {

0 commit comments

Comments
 (0)