@illuma-ai/agents 1.0.98 → 1.1.1
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/agents/AgentContext.cjs +6 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/constants.cjs +53 -0
- package/dist/cjs/common/constants.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +195 -31
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/main.cjs +14 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/dedup.cjs +95 -0
- package/dist/cjs/messages/dedup.cjs.map +1 -0
- package/dist/cjs/tools/CodeExecutor.cjs +22 -3
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/types/graph.cjs.map +1 -1
- package/dist/cjs/utils/pruneCalibration.cjs +78 -0
- package/dist/cjs/utils/pruneCalibration.cjs.map +1 -0
- package/dist/cjs/utils/run.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/cjs/utils/toolDiscoveryCache.cjs +127 -0
- package/dist/cjs/utils/toolDiscoveryCache.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +6 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/constants.mjs +48 -1
- package/dist/esm/common/constants.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +196 -32
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/main.mjs +4 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/dedup.mjs +93 -0
- package/dist/esm/messages/dedup.mjs.map +1 -0
- package/dist/esm/tools/CodeExecutor.mjs +22 -3
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/types/graph.mjs.map +1 -1
- package/dist/esm/utils/pruneCalibration.mjs +74 -0
- package/dist/esm/utils/pruneCalibration.mjs.map +1 -0
- package/dist/esm/utils/run.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/esm/utils/toolDiscoveryCache.mjs +125 -0
- package/dist/esm/utils/toolDiscoveryCache.mjs.map +1 -0
- package/dist/types/agents/AgentContext.d.ts +4 -1
- package/dist/types/common/constants.d.ts +35 -0
- package/dist/types/graphs/Graph.d.ts +34 -0
- package/dist/types/messages/dedup.d.ts +25 -0
- package/dist/types/messages/index.d.ts +1 -0
- package/dist/types/types/graph.d.ts +63 -0
- package/dist/types/utils/index.d.ts +2 -0
- package/dist/types/utils/pruneCalibration.d.ts +43 -0
- package/dist/types/utils/toolDiscoveryCache.d.ts +77 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +7 -0
- package/src/common/constants.ts +56 -0
- package/src/graphs/Graph.ts +250 -50
- package/src/graphs/gapFeatures.test.ts +520 -0
- package/src/graphs/nonBlockingSummarization.test.ts +307 -0
- package/src/messages/__tests__/dedup.test.ts +166 -0
- package/src/messages/dedup.ts +104 -0
- package/src/messages/index.ts +1 -0
- package/src/tools/CodeExecutor.ts +22 -3
- package/src/types/graph.ts +73 -0
- package/src/utils/__tests__/pruneCalibration.test.ts +148 -0
- package/src/utils/__tests__/toolDiscoveryCache.test.ts +214 -0
- package/src/utils/contextPressure.test.ts +24 -9
- package/src/utils/index.ts +2 -0
- package/src/utils/pruneCalibration.ts +92 -0
- package/src/utils/run.ts +108 -108
- package/src/utils/tokens.ts +118 -118
- package/src/utils/toolDiscoveryCache.ts +150 -0
package/src/utils/run.ts
CHANGED
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
import { CallbackManagerForChainRun } from '@langchain/core/callbacks/manager';
|
|
2
|
-
import {
|
|
3
|
-
mergeConfigs,
|
|
4
|
-
patchConfig,
|
|
5
|
-
Runnable,
|
|
6
|
-
RunnableConfig,
|
|
7
|
-
} from '@langchain/core/runnables';
|
|
8
|
-
import { AsyncLocalStorageProviderSingleton } from '@langchain/core/singletons';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Delays the execution for a specified number of milliseconds.
|
|
12
|
-
*
|
|
13
|
-
* @param {number} ms - The number of milliseconds to delay.
|
|
14
|
-
* @return {Promise<void>} A promise that resolves after the specified delay.
|
|
15
|
-
*/
|
|
16
|
-
export function sleep(ms: number): Promise<void> {
|
|
17
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
-
export interface RunnableCallableArgs extends Partial<any> {
|
|
22
|
-
name?: string;
|
|
23
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
-
func: (...args: any[]) => any;
|
|
25
|
-
tags?: string[];
|
|
26
|
-
trace?: boolean;
|
|
27
|
-
recurse?: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export class RunnableCallable<I = unknown, O = unknown> extends Runnable<I, O> {
|
|
31
|
-
lc_namespace: string[] = ['langgraph'];
|
|
32
|
-
|
|
33
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
-
func: (...args: any[]) => any;
|
|
35
|
-
|
|
36
|
-
tags?: string[];
|
|
37
|
-
|
|
38
|
-
config?: RunnableConfig;
|
|
39
|
-
|
|
40
|
-
trace: boolean = true;
|
|
41
|
-
|
|
42
|
-
recurse: boolean = true;
|
|
43
|
-
|
|
44
|
-
constructor(fields: RunnableCallableArgs) {
|
|
45
|
-
super();
|
|
46
|
-
this.name = fields.name ?? fields.func.name;
|
|
47
|
-
this.func = fields.func;
|
|
48
|
-
this.config = fields.tags ? { tags: fields.tags } : undefined;
|
|
49
|
-
this.trace = fields.trace ?? this.trace;
|
|
50
|
-
this.recurse = fields.recurse ?? this.recurse;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
protected async _tracedInvoke(
|
|
54
|
-
input: I,
|
|
55
|
-
config?: Partial<RunnableConfig>,
|
|
56
|
-
runManager?: CallbackManagerForChainRun
|
|
57
|
-
): Promise<O> {
|
|
58
|
-
return new Promise<O>((resolve, reject) => {
|
|
59
|
-
// Defensive check: ensure runManager has getChild method before calling
|
|
60
|
-
const childCallbacks =
|
|
61
|
-
typeof runManager?.getChild === 'function'
|
|
62
|
-
? runManager.getChild()
|
|
63
|
-
: undefined;
|
|
64
|
-
let childConfig: Partial<RunnableConfig> | null = patchConfig(config, {
|
|
65
|
-
callbacks: childCallbacks,
|
|
66
|
-
});
|
|
67
|
-
void AsyncLocalStorageProviderSingleton.runWithConfig(
|
|
68
|
-
childConfig,
|
|
69
|
-
async () => {
|
|
70
|
-
try {
|
|
71
|
-
const output = await this.func(input, childConfig);
|
|
72
|
-
childConfig = null;
|
|
73
|
-
resolve(output);
|
|
74
|
-
} catch (e) {
|
|
75
|
-
childConfig = null;
|
|
76
|
-
reject(e);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async invoke(
|
|
84
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
|
-
input: any,
|
|
86
|
-
options?: Partial<RunnableConfig> | undefined
|
|
87
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
|
-
): Promise<any> {
|
|
89
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
90
|
-
let returnValue: any;
|
|
91
|
-
|
|
92
|
-
if (this.trace) {
|
|
93
|
-
returnValue = await this._callWithConfig(
|
|
94
|
-
this._tracedInvoke,
|
|
95
|
-
input,
|
|
96
|
-
mergeConfigs(this.config, options)
|
|
97
|
-
);
|
|
98
|
-
} else {
|
|
99
|
-
returnValue = await this.func(input, mergeConfigs(this.config, options));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (Runnable.isRunnable(returnValue) && this.recurse) {
|
|
103
|
-
return await returnValue.invoke(input, options);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return returnValue;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
1
|
+
import { CallbackManagerForChainRun } from '@langchain/core/callbacks/manager';
|
|
2
|
+
import {
|
|
3
|
+
mergeConfigs,
|
|
4
|
+
patchConfig,
|
|
5
|
+
Runnable,
|
|
6
|
+
RunnableConfig,
|
|
7
|
+
} from '@langchain/core/runnables';
|
|
8
|
+
import { AsyncLocalStorageProviderSingleton } from '@langchain/core/singletons';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Delays the execution for a specified number of milliseconds.
|
|
12
|
+
*
|
|
13
|
+
* @param {number} ms - The number of milliseconds to delay.
|
|
14
|
+
* @return {Promise<void>} A promise that resolves after the specified delay.
|
|
15
|
+
*/
|
|
16
|
+
export function sleep(ms: number): Promise<void> {
|
|
17
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
export interface RunnableCallableArgs extends Partial<any> {
|
|
22
|
+
name?: string;
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
func: (...args: any[]) => any;
|
|
25
|
+
tags?: string[];
|
|
26
|
+
trace?: boolean;
|
|
27
|
+
recurse?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class RunnableCallable<I = unknown, O = unknown> extends Runnable<I, O> {
|
|
31
|
+
lc_namespace: string[] = ['langgraph'];
|
|
32
|
+
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
func: (...args: any[]) => any;
|
|
35
|
+
|
|
36
|
+
tags?: string[];
|
|
37
|
+
|
|
38
|
+
config?: RunnableConfig;
|
|
39
|
+
|
|
40
|
+
trace: boolean = true;
|
|
41
|
+
|
|
42
|
+
recurse: boolean = true;
|
|
43
|
+
|
|
44
|
+
constructor(fields: RunnableCallableArgs) {
|
|
45
|
+
super();
|
|
46
|
+
this.name = fields.name ?? fields.func.name;
|
|
47
|
+
this.func = fields.func;
|
|
48
|
+
this.config = fields.tags ? { tags: fields.tags } : undefined;
|
|
49
|
+
this.trace = fields.trace ?? this.trace;
|
|
50
|
+
this.recurse = fields.recurse ?? this.recurse;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
protected async _tracedInvoke(
|
|
54
|
+
input: I,
|
|
55
|
+
config?: Partial<RunnableConfig>,
|
|
56
|
+
runManager?: CallbackManagerForChainRun
|
|
57
|
+
): Promise<O> {
|
|
58
|
+
return new Promise<O>((resolve, reject) => {
|
|
59
|
+
// Defensive check: ensure runManager has getChild method before calling
|
|
60
|
+
const childCallbacks =
|
|
61
|
+
typeof runManager?.getChild === 'function'
|
|
62
|
+
? runManager.getChild()
|
|
63
|
+
: undefined;
|
|
64
|
+
let childConfig: Partial<RunnableConfig> | null = patchConfig(config, {
|
|
65
|
+
callbacks: childCallbacks,
|
|
66
|
+
});
|
|
67
|
+
void AsyncLocalStorageProviderSingleton.runWithConfig(
|
|
68
|
+
childConfig,
|
|
69
|
+
async () => {
|
|
70
|
+
try {
|
|
71
|
+
const output = await this.func(input, childConfig);
|
|
72
|
+
childConfig = null;
|
|
73
|
+
resolve(output);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
childConfig = null;
|
|
76
|
+
reject(e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async invoke(
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
|
+
input: any,
|
|
86
|
+
options?: Partial<RunnableConfig> | undefined
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
|
+
): Promise<any> {
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
90
|
+
let returnValue: any;
|
|
91
|
+
|
|
92
|
+
if (this.trace) {
|
|
93
|
+
returnValue = await this._callWithConfig(
|
|
94
|
+
this._tracedInvoke,
|
|
95
|
+
input,
|
|
96
|
+
mergeConfigs(this.config, options)
|
|
97
|
+
);
|
|
98
|
+
} else {
|
|
99
|
+
returnValue = await this.func(input, mergeConfigs(this.config, options));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (Runnable.isRunnable(returnValue) && this.recurse) {
|
|
103
|
+
return await returnValue.invoke(input, options);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return returnValue;
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/utils/tokens.ts
CHANGED
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
import { Tokenizer } from 'ai-tokenizer';
|
|
2
|
-
import type { BaseMessage } from '@langchain/core/messages';
|
|
3
|
-
import { ContentTypes } from '@/common/enum';
|
|
4
|
-
|
|
5
|
-
export type EncodingName = 'o200k_base' | 'claude';
|
|
6
|
-
|
|
7
|
-
const tokenizers: Partial<Record<EncodingName, Tokenizer>> = {};
|
|
8
|
-
|
|
9
|
-
async function getTokenizer(
|
|
10
|
-
encoding: EncodingName = 'o200k_base'
|
|
11
|
-
): Promise<Tokenizer> {
|
|
12
|
-
const cached = tokenizers[encoding];
|
|
13
|
-
if (cached) {
|
|
14
|
-
return cached;
|
|
15
|
-
}
|
|
16
|
-
const data =
|
|
17
|
-
encoding === 'claude'
|
|
18
|
-
? await import('ai-tokenizer/encoding/claude')
|
|
19
|
-
: await import('ai-tokenizer/encoding/o200k_base');
|
|
20
|
-
const instance = new Tokenizer(data);
|
|
21
|
-
tokenizers[encoding] = instance;
|
|
22
|
-
return instance;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function encodingForModel(model: string): EncodingName {
|
|
26
|
-
if (model.toLowerCase().includes('claude')) {
|
|
27
|
-
return 'claude';
|
|
28
|
-
}
|
|
29
|
-
return 'o200k_base';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function getTokenCountForMessage(
|
|
33
|
-
message: BaseMessage,
|
|
34
|
-
getTokenCount: (text: string) => number
|
|
35
|
-
): number {
|
|
36
|
-
const tokensPerMessage = 3;
|
|
37
|
-
|
|
38
|
-
const processValue = (value: unknown): void => {
|
|
39
|
-
if (Array.isArray(value)) {
|
|
40
|
-
for (const item of value) {
|
|
41
|
-
if (
|
|
42
|
-
!item ||
|
|
43
|
-
!item.type ||
|
|
44
|
-
item.type === ContentTypes.ERROR ||
|
|
45
|
-
item.type === ContentTypes.IMAGE_URL
|
|
46
|
-
) {
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (item.type === ContentTypes.TOOL_CALL && item.tool_call != null) {
|
|
51
|
-
const toolName = item.tool_call?.name || '';
|
|
52
|
-
if (toolName != null && toolName && typeof toolName === 'string') {
|
|
53
|
-
numTokens += getTokenCount(toolName);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const args = item.tool_call?.args || '';
|
|
57
|
-
if (args != null && args && typeof args === 'string') {
|
|
58
|
-
numTokens += getTokenCount(args);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const output = item.tool_call?.output || '';
|
|
62
|
-
if (output != null && output && typeof output === 'string') {
|
|
63
|
-
numTokens += getTokenCount(output);
|
|
64
|
-
}
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const nestedValue = item[item.type];
|
|
69
|
-
|
|
70
|
-
if (!nestedValue) {
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
processValue(nestedValue);
|
|
75
|
-
}
|
|
76
|
-
} else if (typeof value === 'string') {
|
|
77
|
-
numTokens += getTokenCount(value);
|
|
78
|
-
} else if (typeof value === 'number') {
|
|
79
|
-
numTokens += getTokenCount(value.toString());
|
|
80
|
-
} else if (typeof value === 'boolean') {
|
|
81
|
-
numTokens += getTokenCount(value.toString());
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
let numTokens = tokensPerMessage;
|
|
86
|
-
processValue(message.content);
|
|
87
|
-
return numTokens;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Creates a token counter function using the specified encoding.
|
|
92
|
-
* Lazily loads the encoding data on first use via dynamic import.
|
|
93
|
-
*/
|
|
94
|
-
export const createTokenCounter = async (
|
|
95
|
-
encoding: EncodingName = 'o200k_base'
|
|
96
|
-
): Promise<(message: BaseMessage) => number> => {
|
|
97
|
-
const tok = await getTokenizer(encoding);
|
|
98
|
-
const countTokens = (text: string): number => tok.count(text);
|
|
99
|
-
return (message: BaseMessage): number =>
|
|
100
|
-
getTokenCountForMessage(message, countTokens);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
/** Utility to manage the token encoder lifecycle explicitly. */
|
|
104
|
-
export const TokenEncoderManager = {
|
|
105
|
-
async initialize(): Promise<void> {
|
|
106
|
-
// No-op: ai-tokenizer is synchronously initialized from bundled data.
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
reset(): void {
|
|
110
|
-
for (const key of Object.keys(tokenizers)) {
|
|
111
|
-
delete tokenizers[key as EncodingName];
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
isInitialized(): boolean {
|
|
116
|
-
return Object.keys(tokenizers).length > 0;
|
|
117
|
-
},
|
|
118
|
-
};
|
|
1
|
+
import { Tokenizer } from 'ai-tokenizer';
|
|
2
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
3
|
+
import { ContentTypes } from '@/common/enum';
|
|
4
|
+
|
|
5
|
+
export type EncodingName = 'o200k_base' | 'claude';
|
|
6
|
+
|
|
7
|
+
const tokenizers: Partial<Record<EncodingName, Tokenizer>> = {};
|
|
8
|
+
|
|
9
|
+
async function getTokenizer(
|
|
10
|
+
encoding: EncodingName = 'o200k_base'
|
|
11
|
+
): Promise<Tokenizer> {
|
|
12
|
+
const cached = tokenizers[encoding];
|
|
13
|
+
if (cached) {
|
|
14
|
+
return cached;
|
|
15
|
+
}
|
|
16
|
+
const data =
|
|
17
|
+
encoding === 'claude'
|
|
18
|
+
? await import('ai-tokenizer/encoding/claude')
|
|
19
|
+
: await import('ai-tokenizer/encoding/o200k_base');
|
|
20
|
+
const instance = new Tokenizer(data);
|
|
21
|
+
tokenizers[encoding] = instance;
|
|
22
|
+
return instance;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function encodingForModel(model: string): EncodingName {
|
|
26
|
+
if (model.toLowerCase().includes('claude')) {
|
|
27
|
+
return 'claude';
|
|
28
|
+
}
|
|
29
|
+
return 'o200k_base';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getTokenCountForMessage(
|
|
33
|
+
message: BaseMessage,
|
|
34
|
+
getTokenCount: (text: string) => number
|
|
35
|
+
): number {
|
|
36
|
+
const tokensPerMessage = 3;
|
|
37
|
+
|
|
38
|
+
const processValue = (value: unknown): void => {
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
for (const item of value) {
|
|
41
|
+
if (
|
|
42
|
+
!item ||
|
|
43
|
+
!item.type ||
|
|
44
|
+
item.type === ContentTypes.ERROR ||
|
|
45
|
+
item.type === ContentTypes.IMAGE_URL
|
|
46
|
+
) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (item.type === ContentTypes.TOOL_CALL && item.tool_call != null) {
|
|
51
|
+
const toolName = item.tool_call?.name || '';
|
|
52
|
+
if (toolName != null && toolName && typeof toolName === 'string') {
|
|
53
|
+
numTokens += getTokenCount(toolName);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const args = item.tool_call?.args || '';
|
|
57
|
+
if (args != null && args && typeof args === 'string') {
|
|
58
|
+
numTokens += getTokenCount(args);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const output = item.tool_call?.output || '';
|
|
62
|
+
if (output != null && output && typeof output === 'string') {
|
|
63
|
+
numTokens += getTokenCount(output);
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const nestedValue = item[item.type];
|
|
69
|
+
|
|
70
|
+
if (!nestedValue) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
processValue(nestedValue);
|
|
75
|
+
}
|
|
76
|
+
} else if (typeof value === 'string') {
|
|
77
|
+
numTokens += getTokenCount(value);
|
|
78
|
+
} else if (typeof value === 'number') {
|
|
79
|
+
numTokens += getTokenCount(value.toString());
|
|
80
|
+
} else if (typeof value === 'boolean') {
|
|
81
|
+
numTokens += getTokenCount(value.toString());
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
let numTokens = tokensPerMessage;
|
|
86
|
+
processValue(message.content);
|
|
87
|
+
return numTokens;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Creates a token counter function using the specified encoding.
|
|
92
|
+
* Lazily loads the encoding data on first use via dynamic import.
|
|
93
|
+
*/
|
|
94
|
+
export const createTokenCounter = async (
|
|
95
|
+
encoding: EncodingName = 'o200k_base'
|
|
96
|
+
): Promise<(message: BaseMessage) => number> => {
|
|
97
|
+
const tok = await getTokenizer(encoding);
|
|
98
|
+
const countTokens = (text: string): number => tok.count(text);
|
|
99
|
+
return (message: BaseMessage): number =>
|
|
100
|
+
getTokenCountForMessage(message, countTokens);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** Utility to manage the token encoder lifecycle explicitly. */
|
|
104
|
+
export const TokenEncoderManager = {
|
|
105
|
+
async initialize(): Promise<void> {
|
|
106
|
+
// No-op: ai-tokenizer is synchronously initialized from bundled data.
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
reset(): void {
|
|
110
|
+
for (const key of Object.keys(tokenizers)) {
|
|
111
|
+
delete tokenizers[key as EncodingName];
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
isInitialized(): boolean {
|
|
116
|
+
return Object.keys(tokenizers).length > 0;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// src/utils/toolDiscoveryCache.ts
|
|
2
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
3
|
+
import { Constants, MessageTypes } from '@/common';
|
|
4
|
+
import { TOOL_DISCOVERY_CACHE_MAX_SIZE } from '@/common/constants';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cached tool discovery entry.
|
|
8
|
+
* Stores the tool name and the message index where it was discovered,
|
|
9
|
+
* enabling efficient lookups without re-parsing conversation history.
|
|
10
|
+
*/
|
|
11
|
+
export interface ToolDiscoveryEntry {
|
|
12
|
+
/** The tool name that was discovered */
|
|
13
|
+
toolName: string;
|
|
14
|
+
/** Message index in conversation history where discovery occurred */
|
|
15
|
+
discoveredAtIndex: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* ToolDiscoveryCache provides a run-scoped cache of tool search results.
|
|
20
|
+
*
|
|
21
|
+
* Problem: Without caching, every LLM iteration re-parses the full message
|
|
22
|
+
* history via extractToolDiscoveries() to find tool_search results. In long
|
|
23
|
+
* conversations with many tool iterations, this is redundant work.
|
|
24
|
+
*
|
|
25
|
+
* Solution: Cache discovered tool names by message index. On each iteration,
|
|
26
|
+
* only scan messages AFTER the last scanned index. Already-seen discoveries
|
|
27
|
+
* are returned from cache instantly.
|
|
28
|
+
*
|
|
29
|
+
* This mirrors the pattern used by VS Code Copilot Chat where tool search
|
|
30
|
+
* results from prior turns are cached to avoid re-discovery.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const cache = new ToolDiscoveryCache();
|
|
35
|
+
*
|
|
36
|
+
* // First call: scans all messages
|
|
37
|
+
* const newTools = cache.getNewDiscoveries(messages);
|
|
38
|
+
* // Returns: ['web_search', 'file_read']
|
|
39
|
+
*
|
|
40
|
+
* // Second call (3 new messages added): only scans new messages
|
|
41
|
+
* const moreTools = cache.getNewDiscoveries(messages);
|
|
42
|
+
* // Returns: ['code_exec'] (only newly discovered)
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export class ToolDiscoveryCache {
|
|
46
|
+
/** Set of all discovered tool names (deduped) */
|
|
47
|
+
private _discoveredTools: Set<string> = new Set();
|
|
48
|
+
/** Last message index that was scanned */
|
|
49
|
+
private _lastScannedIndex: number = -1;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Scan messages for new tool_search results since the last scan.
|
|
53
|
+
* Only processes messages after `_lastScannedIndex` to avoid redundant work.
|
|
54
|
+
*
|
|
55
|
+
* @param messages - Full conversation message array
|
|
56
|
+
* @returns Array of newly discovered tool names (not previously cached)
|
|
57
|
+
*/
|
|
58
|
+
getNewDiscoveries(messages: BaseMessage[]): string[] {
|
|
59
|
+
if (messages.length === 0) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const startIndex = this._lastScannedIndex + 1;
|
|
64
|
+
if (startIndex >= messages.length) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const newDiscoveries: string[] = [];
|
|
69
|
+
|
|
70
|
+
for (let i = startIndex; i < messages.length; i++) {
|
|
71
|
+
const msg = messages[i];
|
|
72
|
+
if (msg.getType() !== MessageTypes.TOOL) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check if this is a tool_search result
|
|
77
|
+
if ((msg as { name?: string }).name !== Constants.TOOL_SEARCH) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Extract tool references from artifact
|
|
82
|
+
const artifact = (msg as { artifact?: unknown }).artifact;
|
|
83
|
+
if (typeof artifact === 'object' && artifact != null) {
|
|
84
|
+
const refs = (
|
|
85
|
+
artifact as { tool_references?: Array<{ tool_name: string }> }
|
|
86
|
+
).tool_references;
|
|
87
|
+
if (refs && refs.length > 0) {
|
|
88
|
+
for (const ref of refs) {
|
|
89
|
+
if (!this._discoveredTools.has(ref.tool_name)) {
|
|
90
|
+
// Enforce cache size limit
|
|
91
|
+
if (this._discoveredTools.size >= TOOL_DISCOVERY_CACHE_MAX_SIZE) {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
this._discoveredTools.add(ref.tool_name);
|
|
95
|
+
newDiscoveries.push(ref.tool_name);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this._lastScannedIndex = messages.length - 1;
|
|
103
|
+
return newDiscoveries;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Returns all tool names discovered so far (across all scans).
|
|
108
|
+
*/
|
|
109
|
+
getAllDiscoveredTools(): string[] {
|
|
110
|
+
return [...this._discoveredTools];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if a specific tool has been discovered.
|
|
115
|
+
*/
|
|
116
|
+
has(toolName: string): boolean {
|
|
117
|
+
return this._discoveredTools.has(toolName);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Number of unique tools discovered.
|
|
122
|
+
*/
|
|
123
|
+
get size(): number {
|
|
124
|
+
return this._discoveredTools.size;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Reset the cache (e.g., on graph reset).
|
|
129
|
+
*/
|
|
130
|
+
reset(): void {
|
|
131
|
+
this._discoveredTools.clear();
|
|
132
|
+
this._lastScannedIndex = -1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Seed the cache with previously known tool names (e.g., from prior conversation turns).
|
|
137
|
+
* Does not affect _lastScannedIndex — the next getNewDiscoveries call will still
|
|
138
|
+
* scan all messages from the beginning.
|
|
139
|
+
*
|
|
140
|
+
* @param toolNames - Tool names to pre-seed into the cache
|
|
141
|
+
*/
|
|
142
|
+
seed(toolNames: string[]): void {
|
|
143
|
+
for (const name of toolNames) {
|
|
144
|
+
if (this._discoveredTools.size >= TOOL_DISCOVERY_CACHE_MAX_SIZE) {
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
this._discoveredTools.add(name);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|