@librechat/agents 3.3.5 → 3.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +1 -1
- package/dist/cjs/llm/openai/index.cjs +51 -6
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +14 -0
- package/dist/cjs/session/JsonlSessionStore.cjs +1 -1
- package/dist/cjs/stream.cjs +6 -1
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +3 -1
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -0
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +19 -16
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +2 -0
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ReadFile.cjs +8 -4
- package/dist/cjs/tools/ReadFile.cjs.map +1 -1
- package/dist/cjs/tools/SkillTool.cjs +2 -0
- package/dist/cjs/tools/SkillTool.cjs.map +1 -1
- package/dist/cjs/tools/SubagentTool.cjs +3 -0
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +88 -27
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +5 -1
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
- package/dist/cjs/tools/intentArg.cjs +230 -0
- package/dist/cjs/tools/intentArg.cjs.map +1 -0
- package/dist/cjs/tools/local/CompileCheckTool.cjs +3 -2
- package/dist/cjs/tools/local/CompileCheckTool.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalCodingTools.cjs +13 -12
- package/dist/cjs/tools/local/LocalCodingTools.cjs.map +1 -1
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +2 -2
- package/dist/cjs/tools/ptcTimeout.cjs.map +1 -1
- package/dist/cjs/tools/search/schema.cjs +2 -0
- package/dist/cjs/tools/search/schema.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +31 -1
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/toolOutputReferences.cjs +41 -7
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs +1 -1
- package/dist/esm/llm/openai/index.mjs +51 -6
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +4 -3
- package/dist/esm/session/JsonlSessionStore.mjs +1 -1
- package/dist/esm/stream.mjs +6 -1
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +3 -1
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +2 -0
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +19 -16
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +2 -0
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ReadFile.mjs +8 -4
- package/dist/esm/tools/ReadFile.mjs.map +1 -1
- package/dist/esm/tools/SkillTool.mjs +2 -0
- package/dist/esm/tools/SkillTool.mjs.map +1 -1
- package/dist/esm/tools/SubagentTool.mjs +3 -0
- package/dist/esm/tools/SubagentTool.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +88 -28
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +5 -1
- package/dist/esm/tools/ToolSearch.mjs.map +1 -1
- package/dist/esm/tools/intentArg.mjs +220 -0
- package/dist/esm/tools/intentArg.mjs.map +1 -0
- package/dist/esm/tools/local/CompileCheckTool.mjs +3 -2
- package/dist/esm/tools/local/CompileCheckTool.mjs.map +1 -1
- package/dist/esm/tools/local/LocalCodingTools.mjs +13 -12
- package/dist/esm/tools/local/LocalCodingTools.mjs.map +1 -1
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +2 -2
- package/dist/esm/tools/ptcTimeout.mjs.map +1 -1
- package/dist/esm/tools/search/schema.mjs +2 -0
- package/dist/esm/tools/search/schema.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +31 -2
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/toolOutputReferences.mjs +41 -7
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/llm/openai/index.d.ts +0 -11
- package/dist/types/tools/BashExecutor.d.ts +18 -0
- package/dist/types/tools/CodeExecutor.d.ts +18 -0
- package/dist/types/tools/ReadFile.d.ts +18 -0
- package/dist/types/tools/SkillTool.d.ts +18 -0
- package/dist/types/tools/SubagentTool.d.ts +9 -0
- package/dist/types/tools/ToolNode.d.ts +37 -5
- package/dist/types/tools/ToolSearch.d.ts +18 -0
- package/dist/types/tools/intentArg.d.ts +119 -0
- package/dist/types/tools/ptcTimeout.d.ts +2 -0
- package/dist/types/tools/search/schema.d.ts +18 -0
- package/dist/types/tools/search/tool.d.ts +18 -0
- package/dist/types/tools/toolOutputReferences.d.ts +12 -2
- package/dist/types/types/stream.d.ts +12 -0
- package/dist/types/types/tools.d.ts +27 -2
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/llm/openai/index.ts +74 -10
- package/src/llm/openai/llm.spec.ts +105 -0
- package/src/stream.ts +12 -1
- package/src/tools/BashExecutor.ts +6 -0
- package/src/tools/BashProgrammaticToolCalling.ts +2 -0
- package/src/tools/CodeExecutor.ts +6 -0
- package/src/tools/ProgrammaticToolCalling.ts +2 -0
- package/src/tools/ReadFile.ts +2 -0
- package/src/tools/SkillTool.ts +2 -0
- package/src/tools/SubagentTool.ts +3 -0
- package/src/tools/ToolNode.ts +190 -43
- package/src/tools/ToolSearch.ts +3 -0
- package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +174 -0
- package/src/tools/__tests__/intentArg.test.ts +281 -0
- package/src/tools/__tests__/intentCoverage.test.ts +139 -0
- package/src/tools/__tests__/toolOutputReferences.test.ts +83 -0
- package/src/tools/intentArg.ts +323 -0
- package/src/tools/local/CompileCheckTool.ts +4 -3
- package/src/tools/local/LocalCodingTools.ts +14 -13
- package/src/tools/ptcTimeout.ts +2 -0
- package/src/tools/search/outcome.test.ts +90 -0
- package/src/tools/search/schema.ts +3 -0
- package/src/tools/search/tool.ts +47 -1
- package/src/tools/subagent/SubagentExecutor.ts +2 -1
- package/src/tools/toolOutputReferences.ts +77 -9
- package/src/types/stream.ts +12 -0
- package/src/types/tools.ts +24 -2
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import type { JsonSchemaType } from '@/types';
|
|
3
|
+
import {
|
|
4
|
+
INTENT_ARG,
|
|
5
|
+
INTENT_PROPERTY,
|
|
6
|
+
INTENT_DESCRIPTION,
|
|
7
|
+
withIntent,
|
|
8
|
+
readIntent,
|
|
9
|
+
stripIntent,
|
|
10
|
+
applyOutcome,
|
|
11
|
+
readOutcomeFields,
|
|
12
|
+
resolveToolOutcome,
|
|
13
|
+
} from '../intentArg';
|
|
14
|
+
|
|
15
|
+
describe('withIntent', () => {
|
|
16
|
+
const base: JsonSchemaType = {
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
query: { type: 'string', description: 'Search query' },
|
|
20
|
+
limit: { type: 'number' },
|
|
21
|
+
},
|
|
22
|
+
required: ['query'],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
it('prepends intent as the FIRST property key', () => {
|
|
26
|
+
const next = withIntent(base);
|
|
27
|
+
expect(Object.keys(next.properties ?? {})).toEqual(['intent', 'query', 'limit']);
|
|
28
|
+
expect(next.properties?.[INTENT_ARG]).toEqual(INTENT_PROPERTY);
|
|
29
|
+
/**
|
|
30
|
+
* Must be a copy, not the frozen canonical instance: LangChain's
|
|
31
|
+
* JSON-schema validator stamps `__absolute_uri__` onto subschemas,
|
|
32
|
+
* which throws on a frozen object.
|
|
33
|
+
*/
|
|
34
|
+
expect(next.properties?.[INTENT_ARG]).not.toBe(INTENT_PROPERTY);
|
|
35
|
+
expect(Object.isFrozen(next.properties?.[INTENT_ARG])).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('never mutates the input schema', () => {
|
|
39
|
+
const frozen = Object.freeze<JsonSchemaType>({
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: Object.freeze({ query: { type: 'string' } }),
|
|
42
|
+
}) as JsonSchemaType;
|
|
43
|
+
const next = withIntent(frozen);
|
|
44
|
+
expect(next).not.toBe(frozen);
|
|
45
|
+
expect(Object.keys(frozen.properties ?? {})).toEqual(['query']);
|
|
46
|
+
expect(Object.keys(next.properties ?? {})).toEqual(['intent', 'query']);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('does not add intent to required', () => {
|
|
50
|
+
const next = withIntent(base);
|
|
51
|
+
expect(next.required).toEqual(['query']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('is idempotent when intent already exists (position preserved)', () => {
|
|
55
|
+
const once = withIntent(base);
|
|
56
|
+
const twice = withIntent(once);
|
|
57
|
+
expect(twice).toBe(once);
|
|
58
|
+
expect(Object.keys(twice.properties ?? {})[0]).toBe('intent');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('handles a schema with no properties', () => {
|
|
62
|
+
const next = withIntent({ type: 'object', properties: {} });
|
|
63
|
+
expect(Object.keys(next.properties ?? {})).toEqual(['intent']);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('handles an undefined schema', () => {
|
|
67
|
+
const next = withIntent(undefined);
|
|
68
|
+
expect(next.type).toBe('object');
|
|
69
|
+
expect(Object.keys(next.properties ?? {})).toEqual(['intent']);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('carries the model-facing instruction', () => {
|
|
73
|
+
expect(INTENT_PROPERTY.description).toBe(INTENT_DESCRIPTION);
|
|
74
|
+
expect(INTENT_DESCRIPTION).toContain('FIRST');
|
|
75
|
+
expect(INTENT_DESCRIPTION).toContain('distinguish that call from its siblings');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('readIntent', () => {
|
|
80
|
+
it('reads from object args', () => {
|
|
81
|
+
expect(readIntent({ intent: 'Searching for OAuth handling' })).toBe(
|
|
82
|
+
'Searching for OAuth handling'
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('reads from stringified JSON args', () => {
|
|
87
|
+
expect(readIntent('{"intent":"Searching for OAuth handling","query":"oauth"}')).toBe(
|
|
88
|
+
'Searching for OAuth handling'
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('returns undefined for absent, empty, or non-string intents', () => {
|
|
93
|
+
expect(readIntent({ query: 'oauth' })).toBeUndefined();
|
|
94
|
+
expect(readIntent({ intent: '' })).toBeUndefined();
|
|
95
|
+
expect(readIntent({ intent: ' ' })).toBeUndefined();
|
|
96
|
+
expect(readIntent({ intent: 42 })).toBeUndefined();
|
|
97
|
+
expect(readIntent(undefined)).toBeUndefined();
|
|
98
|
+
expect(readIntent('not json')).toBeUndefined();
|
|
99
|
+
expect(readIntent('{"intent": broken')).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('stripIntent', () => {
|
|
104
|
+
it('removes the key from object args', () => {
|
|
105
|
+
expect(stripIntent({ intent: 'Searching', query: 'oauth' })).toEqual({ query: 'oauth' });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('parses and strips stringified args', () => {
|
|
109
|
+
expect(stripIntent('{"intent":"Searching","query":"oauth"}')).toEqual({ query: 'oauth' });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('returns args unchanged when the key is absent', () => {
|
|
113
|
+
const args = { query: 'oauth' };
|
|
114
|
+
expect(stripIntent(args)).toBe(args);
|
|
115
|
+
expect(stripIntent('plain string')).toBe('plain string');
|
|
116
|
+
expect(stripIntent(undefined)).toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('applyOutcome', () => {
|
|
121
|
+
const intent = 'Searching for OAuth handling';
|
|
122
|
+
|
|
123
|
+
it('prefers a tool-supplied outcome (full replacement)', () => {
|
|
124
|
+
expect(
|
|
125
|
+
applyOutcome(intent, {
|
|
126
|
+
outcome: 'Found 12 results for OAuth handling',
|
|
127
|
+
outcome_patch: { from: 'Searching', to: 'Searched' },
|
|
128
|
+
})
|
|
129
|
+
).toBe('Found 12 results for OAuth handling');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('ignores a blank outcome', () => {
|
|
133
|
+
expect(applyOutcome(intent, { outcome: ' ' })).toBe('Searched for OAuth handling');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('applies outcome_patch to the first occurrence only, case-sensitive', () => {
|
|
137
|
+
expect(
|
|
138
|
+
applyOutcome('Searching for Searching patterns', {
|
|
139
|
+
outcome_patch: { from: 'Searching', to: 'Searched' },
|
|
140
|
+
})
|
|
141
|
+
).toBe('Searched for Searching patterns');
|
|
142
|
+
expect(
|
|
143
|
+
applyOutcome(intent, { outcome_patch: { from: 'searching', to: 'searched' } })
|
|
144
|
+
).toBe('Searched for OAuth handling');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('ignores a patch whose from is empty or absent from the intent', () => {
|
|
148
|
+
expect(applyOutcome(intent, { outcome_patch: { from: '', to: 'x' } })).toBe(
|
|
149
|
+
'Searched for OAuth handling'
|
|
150
|
+
);
|
|
151
|
+
expect(applyOutcome(intent, { outcome_patch: { from: 'Grepping', to: 'Grepped' } })).toBe(
|
|
152
|
+
'Searched for OAuth handling'
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('mechanically transforms the leading verb', () => {
|
|
157
|
+
expect(applyOutcome('Reading the callback router')).toBe('Read the callback router');
|
|
158
|
+
expect(applyOutcome('Writing the zod schema')).toBe('Wrote the zod schema');
|
|
159
|
+
expect(applyOutcome('Delegating the OAuth audit')).toBe('Delegated the OAuth audit');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('preserves a lowercase leading verb', () => {
|
|
163
|
+
expect(applyOutcome('searching for OAuth handling')).toBe('searched for OAuth handling');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('leaves an unknown leading word unchanged', () => {
|
|
167
|
+
expect(applyOutcome('Refactoring the zod schema')).toBe('Refactoring the zod schema');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('handles a single-word intent', () => {
|
|
171
|
+
expect(applyOutcome('Searching')).toBe('Searched');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('returns undefined with neither intent nor outcome', () => {
|
|
175
|
+
expect(applyOutcome(undefined)).toBeUndefined();
|
|
176
|
+
expect(applyOutcome('')).toBeUndefined();
|
|
177
|
+
expect(applyOutcome(undefined, { outcome_patch: { from: 'a', to: 'b' } })).toBeUndefined();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('returns the outcome even without an intent', () => {
|
|
181
|
+
expect(applyOutcome(undefined, { outcome: 'Found 12 results' })).toBe('Found 12 results');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('resolveToolOutcome', () => {
|
|
186
|
+
const args = { intent: 'Searching for OAuth handling', query: 'oauth' };
|
|
187
|
+
|
|
188
|
+
it('returns undefined when the tool authored no outcome fields', () => {
|
|
189
|
+
expect(resolveToolOutcome(args)).toBeUndefined();
|
|
190
|
+
expect(resolveToolOutcome(args, {})).toBeUndefined();
|
|
191
|
+
expect(resolveToolOutcome(args, null)).toBeUndefined();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('resolves a tool-supplied outcome', () => {
|
|
195
|
+
expect(resolveToolOutcome(args, { outcome: 'Found 12 results' })).toBe('Found 12 results');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('resolves a patch against the intent read from args', () => {
|
|
199
|
+
expect(
|
|
200
|
+
resolveToolOutcome(args, { outcome_patch: { from: 'Searching', to: 'Searched' } })
|
|
201
|
+
).toBe('Searched for OAuth handling');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('returns undefined for a patch with no intent in the args', () => {
|
|
205
|
+
expect(
|
|
206
|
+
resolveToolOutcome({ query: 'oauth' }, { outcome_patch: { from: 'a', to: 'b' } })
|
|
207
|
+
).toBeUndefined();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('collapses the label to a bounded single line', () => {
|
|
211
|
+
expect(
|
|
212
|
+
resolveToolOutcome(args, { outcome: 'Found 12 results\n for "OAuth handling"' })
|
|
213
|
+
).toBe('Found 12 results for "OAuth handling"');
|
|
214
|
+
const oversized = resolveToolOutcome(args, { outcome: `Found ${'x'.repeat(500)}` });
|
|
215
|
+
expect(oversized?.length).toBe(256);
|
|
216
|
+
expect(oversized?.endsWith('…')).toBe(true);
|
|
217
|
+
expect(resolveToolOutcome(args, { outcome: ' \n \t ' })).toBe(
|
|
218
|
+
'Searched for OAuth handling'
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('keeps $-token replacement text literal', () => {
|
|
223
|
+
expect(
|
|
224
|
+
resolveToolOutcome(args, {
|
|
225
|
+
outcome_patch: { from: 'Searching', to: 'Found $& via $\' and $$' },
|
|
226
|
+
})
|
|
227
|
+
).toBe('Found $& via $\' and $$ for OAuth handling');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('labels failed calls only with tool-authored text', () => {
|
|
231
|
+
expect(resolveToolOutcome(args, { outcome: 'Search failed for OAuth' }, { isError: true })).toBe(
|
|
232
|
+
'Search failed for OAuth'
|
|
233
|
+
);
|
|
234
|
+
expect(
|
|
235
|
+
resolveToolOutcome(
|
|
236
|
+
args,
|
|
237
|
+
{ outcome_patch: { from: 'Searching', to: 'Search failed' } },
|
|
238
|
+
{ isError: true }
|
|
239
|
+
)
|
|
240
|
+
).toBe('Search failed for OAuth handling');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('never falls back to the mechanical transform on a failed call', () => {
|
|
244
|
+
expect(
|
|
245
|
+
resolveToolOutcome(
|
|
246
|
+
args,
|
|
247
|
+
{ outcome_patch: { from: 'searching', to: 'searched' } },
|
|
248
|
+
{ isError: true }
|
|
249
|
+
)
|
|
250
|
+
).toBeUndefined();
|
|
251
|
+
expect(
|
|
252
|
+
resolveToolOutcome(
|
|
253
|
+
{ query: 'oauth' },
|
|
254
|
+
{ outcome_patch: { from: 'Searching', to: 'Searched' } },
|
|
255
|
+
{ isError: true }
|
|
256
|
+
)
|
|
257
|
+
).toBeUndefined();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('readOutcomeFields', () => {
|
|
262
|
+
it('reads valid fields from an artifact-shaped object', () => {
|
|
263
|
+
expect(readOutcomeFields({ outcome: 'Found 12 results', other: 1 })).toEqual({
|
|
264
|
+
outcome: 'Found 12 results',
|
|
265
|
+
outcome_patch: undefined,
|
|
266
|
+
});
|
|
267
|
+
expect(readOutcomeFields({ outcome_patch: { from: 'a', to: 'b' } })).toEqual({
|
|
268
|
+
outcome: undefined,
|
|
269
|
+
outcome_patch: { from: 'a', to: 'b' },
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('rejects malformed fields', () => {
|
|
274
|
+
expect(readOutcomeFields(undefined)).toBeUndefined();
|
|
275
|
+
expect(readOutcomeFields('outcome')).toBeUndefined();
|
|
276
|
+
expect(readOutcomeFields({ outcome: 42 })).toBeUndefined();
|
|
277
|
+
expect(readOutcomeFields({ outcome: ' ' })).toBeUndefined();
|
|
278
|
+
expect(readOutcomeFields({ outcome_patch: { from: 'a' } })).toBeUndefined();
|
|
279
|
+
expect(readOutcomeFields({ outcome_patch: 'Searched' })).toBeUndefined();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import type { CloudflareSandboxRuntime } from '@/types';
|
|
3
|
+
import {
|
|
4
|
+
CLOUDFLARE_CODING_TOOL_NAMES,
|
|
5
|
+
createCloudflareCodingTools,
|
|
6
|
+
} from '../cloudflare/CloudflareSandboxTools';
|
|
7
|
+
import {
|
|
8
|
+
createLocalCodingTools,
|
|
9
|
+
createLocalCodingToolDefinitions,
|
|
10
|
+
} from '../local/LocalCodingTools';
|
|
11
|
+
import {
|
|
12
|
+
BashExecutionToolSchema,
|
|
13
|
+
buildBashExecutionToolSchema,
|
|
14
|
+
} from '../BashExecutor';
|
|
15
|
+
import {
|
|
16
|
+
CodeExecutionToolSchema,
|
|
17
|
+
buildCodeExecutionToolSchema,
|
|
18
|
+
} from '../CodeExecutor';
|
|
19
|
+
import {
|
|
20
|
+
SubagentToolSchema,
|
|
21
|
+
createSubagentToolDefinition,
|
|
22
|
+
} from '../SubagentTool';
|
|
23
|
+
import { BashProgrammaticToolCallingSchema } from '../BashProgrammaticToolCalling';
|
|
24
|
+
import { ProgrammaticToolCallingSchema } from '../ProgrammaticToolCalling';
|
|
25
|
+
import { WebSearchToolSchema } from '../search/schema';
|
|
26
|
+
import { LOCAL_CODING_BUNDLE_NAMES } from '@/common';
|
|
27
|
+
import { ReadFileToolDefinition } from '../ReadFile';
|
|
28
|
+
import { ToolSearchToolSchema } from '../ToolSearch';
|
|
29
|
+
import { SkillToolDefinition } from '../SkillTool';
|
|
30
|
+
import { createSearchTool } from '../search/tool';
|
|
31
|
+
import { INTENT_ARG } from '../intentArg';
|
|
32
|
+
|
|
33
|
+
type SchemaLike = {
|
|
34
|
+
properties?: Record<string, unknown>;
|
|
35
|
+
required?: readonly string[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function firstKey(schema: SchemaLike | undefined): string | undefined {
|
|
39
|
+
return Object.keys(schema?.properties ?? {})[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function expectIntentFirst(name: string, schema: SchemaLike | undefined): void {
|
|
43
|
+
expect(`${name}:${firstKey(schema)}`).toBe(`${name}:${INTENT_ARG}`);
|
|
44
|
+
expect(schema?.required ?? []).not.toContain(INTENT_ARG);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const stubSandbox: CloudflareSandboxRuntime = {
|
|
48
|
+
exec: () => Promise.reject(new Error('schema-only stub')),
|
|
49
|
+
readFile: () => Promise.reject(new Error('schema-only stub')),
|
|
50
|
+
writeFile: () => Promise.reject(new Error('schema-only stub')),
|
|
51
|
+
mkdir: () => Promise.reject(new Error('schema-only stub')),
|
|
52
|
+
listFiles: () => Promise.reject(new Error('schema-only stub')),
|
|
53
|
+
deleteFile: () => Promise.reject(new Error('schema-only stub')),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Coverage pin for the tool-intent capability: every native coding tool, on
|
|
58
|
+
* every engine, must emit `intent` as the FIRST schema property (and never
|
|
59
|
+
* require it). Mirrors the `LOCAL_CODING_BUNDLE_NAMES` pin so "did we get
|
|
60
|
+
* them all" is a failing test rather than a review question.
|
|
61
|
+
*/
|
|
62
|
+
describe('intent coverage', () => {
|
|
63
|
+
it('every local bundle tool emits intent as its first schema property', () => {
|
|
64
|
+
const tools = createLocalCodingTools();
|
|
65
|
+
expect(tools.map((tool) => tool.name).sort()).toEqual(
|
|
66
|
+
[...LOCAL_CODING_BUNDLE_NAMES].sort()
|
|
67
|
+
);
|
|
68
|
+
for (const tool of tools) {
|
|
69
|
+
expectIntentFirst(tool.name, tool.schema as SchemaLike);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('every local registry definition emits intent first', () => {
|
|
74
|
+
for (const def of createLocalCodingToolDefinitions()) {
|
|
75
|
+
expectIntentFirst(def.name, def.parameters);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('every cloudflare bundle tool emits intent first', () => {
|
|
80
|
+
const tools = createCloudflareCodingTools({ sandbox: stubSandbox });
|
|
81
|
+
expect(tools.map((tool) => tool.name).sort()).toEqual(
|
|
82
|
+
[...CLOUDFLARE_CODING_TOOL_NAMES].sort()
|
|
83
|
+
);
|
|
84
|
+
for (const tool of tools) {
|
|
85
|
+
expectIntentFirst(tool.name, tool.schema as SchemaLike);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('shared execution schemas emit intent first (all engines)', () => {
|
|
90
|
+
expectIntentFirst('bash_tool', BashExecutionToolSchema);
|
|
91
|
+
expectIntentFirst('execute_code', CodeExecutionToolSchema);
|
|
92
|
+
expectIntentFirst(
|
|
93
|
+
'bash_tool:stateful',
|
|
94
|
+
buildBashExecutionToolSchema({ statefulSessions: true })
|
|
95
|
+
);
|
|
96
|
+
expectIntentFirst(
|
|
97
|
+
'execute_code:stateful',
|
|
98
|
+
buildCodeExecutionToolSchema({ statefulSessions: true })
|
|
99
|
+
);
|
|
100
|
+
expectIntentFirst(
|
|
101
|
+
'run_tools_with_code',
|
|
102
|
+
ProgrammaticToolCallingSchema
|
|
103
|
+
);
|
|
104
|
+
expectIntentFirst(
|
|
105
|
+
'run_tools_with_bash',
|
|
106
|
+
BashProgrammaticToolCallingSchema
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('read_file, skill, subagent, tool_search, and web_search emit intent first', () => {
|
|
111
|
+
expectIntentFirst('read_file', ReadFileToolDefinition.parameters);
|
|
112
|
+
expectIntentFirst('skill', SkillToolDefinition.parameters);
|
|
113
|
+
expectIntentFirst('subagent', SubagentToolSchema);
|
|
114
|
+
expectIntentFirst(
|
|
115
|
+
'subagent:runtime',
|
|
116
|
+
createSubagentToolDefinition([
|
|
117
|
+
{ type: 'researcher', name: 'Researcher', description: 'Researches' },
|
|
118
|
+
]).parameters
|
|
119
|
+
);
|
|
120
|
+
expectIntentFirst('tool_search', ToolSearchToolSchema);
|
|
121
|
+
expectIntentFirst('web_search', WebSearchToolSchema);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('the createSearchTool FACTORY emits intent first (runtime schema, not the static def)', () => {
|
|
125
|
+
const serperTool = createSearchTool({
|
|
126
|
+
searchProvider: 'serper',
|
|
127
|
+
scraperProvider: 'serper',
|
|
128
|
+
serperApiKey: 'test-key',
|
|
129
|
+
});
|
|
130
|
+
expectIntentFirst('web_search:serper', serperTool.schema as SchemaLike);
|
|
131
|
+
const searxngTool = createSearchTool({
|
|
132
|
+
searchProvider: 'searxng',
|
|
133
|
+
scraperProvider: 'serper',
|
|
134
|
+
serperApiKey: 'test-key',
|
|
135
|
+
searxngInstanceUrl: 'http://localhost:8080',
|
|
136
|
+
});
|
|
137
|
+
expectIntentFirst('web_search:searxng', searxngTool.schema as SchemaLike);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -187,6 +187,89 @@ describe('ToolOutputReferenceRegistry', () => {
|
|
|
187
187
|
expect(resolved).toBe('nothing to see here');
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
+
it('leaves the top-level intent arg verbatim (display label, not a data channel)', () => {
|
|
191
|
+
const reg = new ToolOutputReferenceRegistry();
|
|
192
|
+
reg.set('r', 'tool0turn0', 'HUGE-STORED-OUTPUT');
|
|
193
|
+
const { resolved } = reg.resolve('r', {
|
|
194
|
+
intent: 'Reusing {{tool0turn0}} for the next step',
|
|
195
|
+
command: 'cat {{tool0turn0}}',
|
|
196
|
+
nested: { intent: 'not top-level {{tool0turn0}}' },
|
|
197
|
+
});
|
|
198
|
+
expect(resolved).toEqual({
|
|
199
|
+
intent: 'Reusing {{tool0turn0}} for the next step',
|
|
200
|
+
command: 'cat HUGE-STORED-OUTPUT',
|
|
201
|
+
nested: { intent: 'not top-level HUGE-STORED-OUTPUT' },
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('preserves the intent key inside STRINGIFIED object args too', () => {
|
|
206
|
+
const reg = new ToolOutputReferenceRegistry();
|
|
207
|
+
reg.set('r', 'tool0turn0', 'HUGE-STORED-OUTPUT');
|
|
208
|
+
const { resolved } = reg.resolve(
|
|
209
|
+
'r',
|
|
210
|
+
'{"intent":"Reusing {{tool0turn0}}","command":"cat {{tool0turn0}}"}'
|
|
211
|
+
);
|
|
212
|
+
expect(JSON.parse(resolved as string)).toEqual({
|
|
213
|
+
intent: 'Reusing {{tool0turn0}}',
|
|
214
|
+
command: 'cat HUGE-STORED-OUTPUT',
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('keeps plain (non-object) string args on the raw substitution path', () => {
|
|
219
|
+
const reg = new ToolOutputReferenceRegistry();
|
|
220
|
+
reg.set('r', 'tool0turn0', 'DATA');
|
|
221
|
+
const { resolved } = reg.resolve('r', 'echo {{tool0turn0}} intent');
|
|
222
|
+
expect(resolved).toBe('echo DATA intent');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('substitutes a BUSINESS intent arg when the caller opts in', () => {
|
|
226
|
+
const reg = new ToolOutputReferenceRegistry();
|
|
227
|
+
reg.set('r', 'tool0turn0', 'STORED');
|
|
228
|
+
const { resolved } = reg.resolve(
|
|
229
|
+
'r',
|
|
230
|
+
{ intent: 'category {{tool0turn0}}', title: 'x {{tool0turn0}}' },
|
|
231
|
+
{ substituteIntentKey: true }
|
|
232
|
+
);
|
|
233
|
+
expect(resolved).toEqual({
|
|
234
|
+
intent: 'category STORED',
|
|
235
|
+
title: 'x STORED',
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('substitutes a business intent inside stringified args when opted in', () => {
|
|
240
|
+
const reg = new ToolOutputReferenceRegistry();
|
|
241
|
+
reg.set('r', 'tool0turn0', 'STORED');
|
|
242
|
+
const { resolved } = reg.resolve(
|
|
243
|
+
'r',
|
|
244
|
+
'{"intent":"category {{tool0turn0}}"}',
|
|
245
|
+
{ substituteIntentKey: true }
|
|
246
|
+
);
|
|
247
|
+
expect(resolved).toBe('{"intent":"category STORED"}');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('reports unresolved references from a business intent arg', () => {
|
|
251
|
+
const reg = new ToolOutputReferenceRegistry();
|
|
252
|
+
const { unresolved } = reg.resolve(
|
|
253
|
+
'r',
|
|
254
|
+
{ intent: 'category {{tool9turn9}}' },
|
|
255
|
+
{ substituteIntentKey: true }
|
|
256
|
+
);
|
|
257
|
+
expect(unresolved).toEqual(['tool9turn9']);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('snapshot views honor the opt-in too', () => {
|
|
261
|
+
const reg = new ToolOutputReferenceRegistry();
|
|
262
|
+
reg.set('r', 'tool0turn0', 'STORED');
|
|
263
|
+
const view = reg.snapshot('r');
|
|
264
|
+
expect(view.resolve({ intent: 'x {{tool0turn0}}' }).resolved).toEqual({
|
|
265
|
+
intent: 'x {{tool0turn0}}',
|
|
266
|
+
});
|
|
267
|
+
expect(
|
|
268
|
+
view.resolve({ intent: 'x {{tool0turn0}}' }, { substituteIntentKey: true })
|
|
269
|
+
.resolved
|
|
270
|
+
).toEqual({ intent: 'x STORED' });
|
|
271
|
+
});
|
|
272
|
+
|
|
190
273
|
it('passes through primitive values untouched', () => {
|
|
191
274
|
const reg = new ToolOutputReferenceRegistry();
|
|
192
275
|
const { resolved } = reg.resolve('r', {
|