diff --git a/ts/packages/actionSchema/src/index.ts b/ts/packages/actionSchema/src/index.ts index fe08949fd..deea22e65 100644 --- a/ts/packages/actionSchema/src/index.ts +++ b/ts/packages/actionSchema/src/index.ts @@ -33,5 +33,8 @@ export { fromJSONActionSchemaFile, } from "./serialize.js"; +// Generic (non-action) Schema +export { validateType } from "./validate.js"; + // Schema Config export { SchemaConfig, ParamSpec, ActionParamSpecs } from "./schemaConfig.js"; diff --git a/ts/packages/actionSchema/src/toString.ts b/ts/packages/actionSchema/src/toString.ts new file mode 100644 index 000000000..ee48b79db --- /dev/null +++ b/ts/packages/actionSchema/src/toString.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { SchemaType } from "./type.js"; + +export function toStringSchemaType( + type: SchemaType, + paran: boolean = false, +): string { + let result: string; + switch (type.type) { + case "string": + return "string"; + case "number": + return "number"; + case "boolean": + return "boolean"; + case "undefined": + return "undefined"; + case "object": + return `{ ${Object.entries(type.fields) + .map(([name, field]) => { + return `${name}${field.optional ? "?" : ""}: ${toStringSchemaType(field.type)}`; + }) + .join(", ")}}`; + case "array": + return `${toStringSchemaType(type.elementType, true)}[]`; + case "type-reference": + return type.definition ? type.definition.name : "unknown"; + case "string-union": + result = type.typeEnum.join(" | "); + paran = paran && type.typeEnum.length > 1; + break; + case "type-union": + result = type.types.map((t) => toStringSchemaType(t)).join(" | "); + paran = paran && type.types.length > 1; + break; + } + return paran ? `(${result})` : result; +} diff --git a/ts/packages/actionSchema/src/validate.ts b/ts/packages/actionSchema/src/validate.ts index 4095268f0..6eb67b254 100644 --- a/ts/packages/actionSchema/src/validate.ts +++ b/ts/packages/actionSchema/src/validate.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { toStringSchemaType } from "./toString.js"; import { SchemaTypeArray, SchemaTypeObject, @@ -8,6 +9,14 @@ import { ActionSchemaTypeDefinition, } from "./type.js"; +function errorName(name: string) { + return name === "" ? "Input" : `Field '${name}'`; +} + +function indentMessage(message: string) { + return `${message.replace(/\n/g, "\n ")}`; +} + export function validateSchema( name: string, expected: SchemaType, @@ -15,19 +24,29 @@ export function validateSchema( coerce: boolean = false, // coerce string to the right primitive type ) { if (actual === null) { - throw new Error(`'${name}' should not be null`); + throw new Error(`${errorName(name)} should not be null`); } switch (expected.type) { case "type-union": { + const errors: [SchemaType, Error][] = []; for (const type of expected.types) { try { validateSchema(name, type, actual, coerce); return; - } catch (e) { - // ignore + } catch (e: any) { + errors.push([type, e]); } } - throw new Error(`'${name}' does not match any union type`); + const messages = errors + .map( + ([type, e], i) => + `\n-- Type: ${toStringSchemaType(type)}\n-- Error: ${indentMessage(e.message)}`, + ) + .join("\n"); + + throw new Error( + `${errorName(name)} does not match any union type\n${messages}`, + ); } case "type-reference": if (expected.definition !== undefined) { @@ -37,7 +56,7 @@ export function validateSchema( case "object": if (typeof actual !== "object" || Array.isArray(actual)) { throw new Error( - `'${name}' is not an object, got ${Array.isArray(actual) ? "array" : typeof actual} instead`, + `${errorName(name)} is not an object, got ${Array.isArray(actual) ? "array" : typeof actual} instead`, ); } validateObject( @@ -50,7 +69,7 @@ export function validateSchema( case "array": if (!Array.isArray(actual)) { throw new Error( - `'${name}' is not an array, got ${typeof actual} instead`, + `${errorName(name)} is not an array, got ${typeof actual} instead`, ); } validateArray(name, expected, actual, coerce); @@ -58,7 +77,7 @@ export function validateSchema( case "string-union": if (typeof actual !== "string") { throw new Error( - `'${name}' is not a string, got ${typeof actual} instead`, + `${errorName(name)} is not a string, got ${typeof actual} instead`, ); } if (!expected.typeEnum.includes(actual)) { @@ -67,7 +86,7 @@ export function validateSchema( ? `${expected.typeEnum[0]}` : `one of ${expected.typeEnum.map((s) => `'${s}'`).join(",")}`; throw new Error( - `'${name}' is not ${expectedValues}, got ${actual} instead`, + `${errorName(name)} is not ${expectedValues}, got ${actual} instead`, ); } break; @@ -92,7 +111,7 @@ export function validateSchema( } } throw new Error( - `'${name}' is not a ${expected.type}, got ${typeof actual} instead`, + `${errorName(name)} is not a ${expected.type}, got ${typeof actual} instead`, ); } } @@ -131,7 +150,7 @@ function validateObject( const fullName = name ? `${name}.${fieldName}` : fieldName; if (actualValue === undefined) { if (!fieldInfo.optional) { - throw new Error(`Missing required property ${fullName}`); + throw new Error(`Missing required property '${fullName}'`); } continue; } @@ -159,3 +178,7 @@ export function validateAction( ) { validateObject("", actionSchema.type, action, coerce, ["translatorName"]); } + +export function validateType(type: SchemaType, value: any) { + validateSchema("", type, value); +} diff --git a/ts/packages/defaultAgentProvider/test/data/translate-history-e2e.json b/ts/packages/defaultAgentProvider/test/data/translate-history-e2e.json new file mode 100644 index 000000000..66e0e0a72 --- /dev/null +++ b/ts/packages/defaultAgentProvider/test/data/translate-history-e2e.json @@ -0,0 +1,14 @@ +[ + { + "request": "play that song again", + "history": { + "user": "play some random songs", + "assistant": { "text": "Now playing: Soruwienf from album Wxifiel with artist Bnefisoe", "source": "player", "entities": [ + {"name": "Soruwienf", "type": ["track", "song"], "uniqueId": "a"}, + {"name": "Wxifiel", "type": ["album"], "uniqueId": "b"}, + {"name": "Bnefisoe", "type": ["artist"], "uniqueId": "c"} + ]} + }, + "action": {"translatorName": "player","actionName": "playTrack", "parameters": {"trackName": "Soruwienf", "albumName": "Wxifiel", "artists": ["Bnefisoe"]}} + } +] \ No newline at end of file diff --git a/ts/packages/defaultAgentProvider/test/translate.test.ts b/ts/packages/defaultAgentProvider/test/translate.test.ts index 54ef07473..16abc13c2 100644 --- a/ts/packages/defaultAgentProvider/test/translate.test.ts +++ b/ts/packages/defaultAgentProvider/test/translate.test.ts @@ -1,76 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import dotenv from "dotenv"; -dotenv.config({ path: new URL("../../../../.env", import.meta.url) }); - -import { getPackageFilePath } from "../src/utils/getPackageFilePath.js"; -import { getDefaultAppAgentProviders } from "../src/defaultAgentProviders.js"; -import fs from "node:fs"; -import { createDispatcher, Dispatcher } from "agent-dispatcher"; - +import { defineTranslateTest } from "./translateTestCommon.js"; const dataFiles = ["test/data/translate-e2e.json"]; -type TranslateTestRequest = { - request: string; - action: string | string[]; -}; -type TranslateTestEntry = TranslateTestRequest | TranslateTestRequest[]; -type TranslateTestFile = TranslateTestEntry[]; - -const inputs: TranslateTestEntry[] = ( - await Promise.all( - dataFiles.map<Promise<TranslateTestFile>>(async (f) => { - return JSON.parse( - await fs.promises.readFile(getPackageFilePath(f), "utf-8"), - ); - }), - ) -).flat(); - -const repeat = 5; -const defaultAppAgentProviders = getDefaultAppAgentProviders(undefined); - -describe("translation action stability", () => { - let dispatchers: Dispatcher[]; - beforeAll(async () => { - const dispatcherP: Promise<Dispatcher>[] = []; - for (let i = 0; i < repeat; i++) { - dispatcherP.push( - createDispatcher("cli test translate", { - appAgentProviders: defaultAppAgentProviders, - actions: null, - commands: { dispatcher: true }, - translation: { history: { enabled: false } }, - explainer: { enabled: false }, - cache: { enabled: false }, - }), - ); - } - dispatchers = await Promise.all(dispatcherP); - }); - it.each(inputs)("translate '$request'", async (test) => { - const requests = Array.isArray(test) ? test : [test]; - await Promise.all( - dispatchers.map(async (dispatcher) => { - for (const { request, action } of requests) { - const result = await dispatcher.processCommand(request); - expect(result?.actions).toBeDefined(); - - const expected = - typeof action === "string" ? [action] : action; - expect(result?.actions).toHaveLength(expected.length); - for (let i = 0; i < expected.length; i++) { - expect( - `${result?.actions?.[i].translatorName}.${result?.actions?.[i].actionName}`, - ).toBe(expected[i]); - } - } - }), - ); - }); - afterAll(async () => { - await Promise.all(dispatchers.map((d) => d.close())); - dispatchers = []; - }); -}); +await defineTranslateTest("translate (no history)", dataFiles); diff --git a/ts/packages/defaultAgentProvider/test/translateTestCommon.ts b/ts/packages/defaultAgentProvider/test/translateTestCommon.ts new file mode 100644 index 000000000..462277684 --- /dev/null +++ b/ts/packages/defaultAgentProvider/test/translateTestCommon.ts @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import dotenv from "dotenv"; +dotenv.config({ path: new URL("../../../../.env", import.meta.url) }); + +import { getPackageFilePath } from "../src/utils/getPackageFilePath.js"; +import { getDefaultAppAgentProviders } from "../src/defaultAgentProviders.js"; +import fs from "node:fs"; +import { createDispatcher, Dispatcher } from "agent-dispatcher"; +import { ChatHistoryInput } from "agent-dispatcher/internal"; +import { FullAction } from "agent-cache"; + +type TranslateTestRequest = { + request: string; + action: string | string[] | FullAction | FullAction[]; + history?: ChatHistoryInput | ChatHistoryInput[]; + match?: "exact" | "partial"; // default to "exact" +}; +type TranslateTestEntry = TranslateTestRequest | TranslateTestRequest[]; +type TranslateTestFile = TranslateTestEntry[]; +const repeat = 5; +const defaultAppAgentProviders = getDefaultAppAgentProviders(undefined); + +export async function defineTranslateTest(name: string, dataFiles: string[]) { + const inputs: TranslateTestEntry[] = ( + await Promise.all( + dataFiles.map<Promise<TranslateTestFile>>(async (f) => { + return JSON.parse( + await fs.promises.readFile(getPackageFilePath(f), "utf-8"), + ); + }), + ) + ).flat(); + + describe(`${name} action stability`, () => { + let dispatchers: Dispatcher[]; + beforeAll(async () => { + const dispatcherP: Promise<Dispatcher>[] = []; + for (let i = 0; i < repeat; i++) { + dispatcherP.push( + createDispatcher("cli test translate", { + appAgentProviders: defaultAppAgentProviders, + actions: null, + commands: { dispatcher: true }, + translation: { history: { enabled: false } }, + explainer: { enabled: false }, + cache: { enabled: false }, + }), + ); + } + dispatchers = await Promise.all(dispatcherP); + }); + it.each(inputs)(`${name} '$request'`, async (test) => { + const requests = Array.isArray(test) ? test : [test]; + await Promise.all( + dispatchers.map(async (dispatcher) => { + for (const { + request, + history, + action, + match, + } of requests) { + if (history !== undefined) { + await dispatcher.processCommand( + `@history insert ${JSON.stringify(history)}`, + ); + } + const result = await dispatcher.processCommand(request); + const actions = result?.actions; + expect(actions).toBeDefined(); + + const expectedValues = Array.isArray(action) + ? action + : [action]; + expect(actions).toHaveLength(expectedValues.length); + for (let i = 0; i < expectedValues.length; i++) { + const action = actions![i]; + const expected = expectedValues[i]; + if (typeof expected === "string") { + const actualFullActionName = `${action.translatorName}.${action.actionName}`; + if (match === "partial") { + expect(actualFullActionName).toContain( + expected, + ); + } else { + expect(actualFullActionName).toBe(expected); + } + } else { + if (match === "partial") { + expect(action).toMatchObject(expected); + } else { + expect(action).toEqual(expected); + } + } + } + } + }), + ); + }); + afterAll(async () => { + await Promise.all(dispatchers.map((d) => d.close())); + dispatchers = []; + }); + }); +} diff --git a/ts/packages/defaultAgentProvider/test/translate_history.test.ts b/ts/packages/defaultAgentProvider/test/translate_history.test.ts new file mode 100644 index 000000000..32141a2f1 --- /dev/null +++ b/ts/packages/defaultAgentProvider/test/translate_history.test.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { defineTranslateTest } from "./translateTestCommon.js"; +const dataFiles = ["test/data/translate-history-e2e.json"]; + +await defineTranslateTest("translate (w/history)", dataFiles); diff --git a/ts/packages/dispatcher/src/context/chatHistory.ts b/ts/packages/dispatcher/src/context/chatHistory.ts index 464deaf63..e2af4f257 100644 --- a/ts/packages/dispatcher/src/context/chatHistory.ts +++ b/ts/packages/dispatcher/src/context/chatHistory.ts @@ -10,20 +10,20 @@ import { normalizeParamString, PromptEntity } from "agent-cache"; type UserEntry = { role: "user"; text: string; - id: RequestId; + id?: RequestId; attachments?: CachedImageWithDetails[] | undefined; }; type AssistantEntry = { role: "assistant"; text: string; - id: string | undefined; + id?: RequestId; sourceAppAgentName: string; entities?: Entity[] | undefined; additionalInstructions?: string[] | undefined; }; -type ChatHistoryEntry = UserEntry | AssistantEntry; +export type ChatHistoryEntry = UserEntry | AssistantEntry; export interface ChatHistory { entries: ChatHistoryEntry[]; diff --git a/ts/packages/dispatcher/src/context/commandHandlerContext.ts b/ts/packages/dispatcher/src/context/commandHandlerContext.ts index bd8f2486d..3219adc17 100644 --- a/ts/packages/dispatcher/src/context/commandHandlerContext.ts +++ b/ts/packages/dispatcher/src/context/commandHandlerContext.ts @@ -204,7 +204,7 @@ async function getAgentCache( return agentCache; } -export type InitializeCommandHandlerContextOptions = SessionOptions & { +export type DispatcherOptions = SessionOptions & { appAgentProviders?: AppAgentProvider[]; explanationAsynchronousMode?: boolean; // default to false persistSession?: boolean; // default to false, @@ -337,7 +337,7 @@ export async function installAppProvider( export async function initializeCommandHandlerContext( hostName: string, - options?: InitializeCommandHandlerContextOptions, + options?: DispatcherOptions, ): Promise<CommandHandlerContext> { const metrics = options?.metrics ?? false; const explanationAsynchronousMode = diff --git a/ts/packages/dispatcher/src/context/system/handlers/historyCommandHandler.ts b/ts/packages/dispatcher/src/context/system/handlers/historyCommandHandler.ts index 10e6f4621..d4167b6cd 100644 --- a/ts/packages/dispatcher/src/context/system/handlers/historyCommandHandler.ts +++ b/ts/packages/dispatcher/src/context/system/handlers/historyCommandHandler.ts @@ -1,7 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { ActionContext, ParsedCommandParams } from "@typeagent/agent-sdk"; +import { + ActionContext, + Entity, + ParsedCommandParams, +} from "@typeagent/agent-sdk"; import { CommandHandler, CommandHandlerNoParams, @@ -9,8 +13,10 @@ import { } from "@typeagent/agent-sdk/helpers/command"; import { displayResult } from "@typeagent/agent-sdk/helpers/display"; import { CommandHandlerContext } from "../../commandHandlerContext.js"; +import { ChatHistoryEntry } from "../../chatHistory.js"; +import { ActionSchemaCreator as sc, validateType } from "action-schema"; -export class HistoryListCommandHandler implements CommandHandlerNoParams { +class HistoryListCommandHandler implements CommandHandlerNoParams { public readonly description = "List history"; public async run(context: ActionContext<CommandHandlerContext>) { const systemContext = context.sessionContext.agentContext; @@ -27,7 +33,7 @@ export class HistoryListCommandHandler implements CommandHandlerNoParams { } } -export class HistoryClearCommandHandler implements CommandHandlerNoParams { +class HistoryClearCommandHandler implements CommandHandlerNoParams { public readonly description = "Clear the history"; public async run(context: ActionContext<CommandHandlerContext>) { const systemContext = context.sessionContext.agentContext; @@ -39,7 +45,7 @@ export class HistoryClearCommandHandler implements CommandHandlerNoParams { } } -export class HistoryDeleteCommandHandler implements CommandHandler { +class HistoryDeleteCommandHandler implements CommandHandler { public readonly description = "Delete a specific message from the chat history"; public readonly parameters = { @@ -75,6 +81,118 @@ export class HistoryDeleteCommandHandler implements CommandHandler { } } +type ChatHistoryInputAssistant = { + text: string; + source: string; + entities?: Entity[]; +}; +export type ChatHistoryInput = { + user: string; + assistant: ChatHistoryInputAssistant | ChatHistoryInputAssistant[]; +}; + +function convertAssistantMessage( + entries: ChatHistoryEntry[], + message: ChatHistoryInputAssistant, +) { + entries.push({ + role: "assistant", + text: message.text, + sourceAppAgentName: message.source, + entities: message.entities, + }); +} + +function convertChatHistoryInputEntry( + entries: ChatHistoryEntry[], + message: ChatHistoryInput, +) { + entries.push({ + role: "user", + text: message.user, + }); + const assistant = message.assistant; + if (Array.isArray(assistant)) { + assistant.forEach((m) => convertAssistantMessage(entries, m)); + } else { + convertAssistantMessage(entries, assistant); + } +} + +function getChatHistoryInput( + message: ChatHistoryInput | ChatHistoryInput[], +): ChatHistoryEntry[] { + const entries: ChatHistoryEntry[] = []; + if (Array.isArray(message)) { + message.forEach((m) => convertChatHistoryInputEntry(entries, m)); + } else { + convertChatHistoryInputEntry(entries, message); + } + return entries; +} + +const assistantInputSchema = sc.obj({ + text: sc.string(), + source: sc.string(), + entities: sc.optional( + sc.array( + sc.obj({ + name: sc.string(), + type: sc.array(sc.string()), + uniqueId: sc.optional(sc.string()), + }), + ), + ), +}); + +const messageInputSchema = sc.obj({ + user: sc.string(), + assistant: sc.union(assistantInputSchema, sc.array(assistantInputSchema)), +}); + +const chatHistoryInputSchema = sc.union( + messageInputSchema, + sc.array(messageInputSchema), +); + +class HistoryInsertCommandHandler implements CommandHandler { + public readonly description = "Insert messages to chat history"; + public readonly parameters = { + args: { + messages: { + description: "Chat history messages to insert", + type: "json", + implicitQuotes: true, + }, + }, + } as const; + + public async run( + context: ActionContext<CommandHandlerContext>, + param: ParsedCommandParams<typeof this.parameters>, + ) { + const systemContext = context.sessionContext.agentContext; + const { messages } = param.args; + + if (messages.length === 0) { + throw new Error("No messages to insert."); + } + + validateType(chatHistoryInputSchema, messages); + + systemContext.chatHistory.entries.push( + ...getChatHistoryInput( + messages as unknown as ChatHistoryInput | ChatHistoryInput[], + ), + ); + + displayResult( + `Inserted ${messages.length} messages to chat history. ${systemContext.chatHistory.entries.length} messages in total.`, + context, + ); + } +} + export function getHistoryCommandHandlers(): CommandHandlerTable { return { description: "History commands", @@ -83,6 +201,7 @@ export function getHistoryCommandHandlers(): CommandHandlerTable { list: new HistoryListCommandHandler(), clear: new HistoryClearCommandHandler(), delete: new HistoryDeleteCommandHandler(), + insert: new HistoryInsertCommandHandler(), }, }; } diff --git a/ts/packages/dispatcher/src/context/system/systemAgent.ts b/ts/packages/dispatcher/src/context/system/systemAgent.ts index 08955879f..79c1bfc54 100644 --- a/ts/packages/dispatcher/src/context/system/systemAgent.ts +++ b/ts/packages/dispatcher/src/context/system/systemAgent.ts @@ -119,41 +119,35 @@ class HelpCommandHandler implements CommandHandler { undefined, context, ); - } else { - const result = await resolveCommand( - params.args.command, - systemContext, - ); - - const command = getParsedCommand(result); - if (result.suffix.length !== 0) { - displayError( - `ERROR: '${result.suffix}' is not a subcommand for '@${command}'`, - context, - ); - } + return; + } + const result = await resolveCommand(params.args.command, systemContext); - if (result.descriptor !== undefined) { - const defaultSubCommand = - result.table !== undefined - ? getDefaultSubCommandDescriptor(result.table) - : undefined; + const command = getParsedCommand(result); + if (result.suffix.length !== 0) { + displayError( + `ERROR: '${result.suffix}' is not a subcommand for '@${command}'`, + context, + ); + } - if (defaultSubCommand !== result.descriptor) { - displayResult( - getUsage(command, result.descriptor), - context, - ); - return; - } - } + if (result.descriptor !== undefined) { + const defaultSubCommand = + result.table !== undefined + ? getDefaultSubCommandDescriptor(result.table) + : undefined; - if (result.table === undefined) { - throw new Error(`Unknown command '${params.args.command}'`); + if (defaultSubCommand !== result.descriptor) { + displayResult(getUsage(command, result.descriptor), context); + return; } + } - printStructuredHandlerTableUsage(result.table, command, context); + if (result.table === undefined) { + throw new Error(`Unknown command '${params.args.command}'`); } + + printStructuredHandlerTableUsage(result.table, command, context); } } diff --git a/ts/packages/dispatcher/src/dispatcher.ts b/ts/packages/dispatcher/src/dispatcher.ts index 2fc2ff47b..702e50ccf 100644 --- a/ts/packages/dispatcher/src/dispatcher.ts +++ b/ts/packages/dispatcher/src/dispatcher.ts @@ -19,8 +19,8 @@ import { import { closeCommandHandlerContext, CommandHandlerContext, + DispatcherOptions, initializeCommandHandlerContext, - InitializeCommandHandlerContextOptions, } from "./context/commandHandlerContext.js"; import { RequestId } from "./context/interactiveIO.js"; import { RequestMetrics } from "./utils/metrics.js"; @@ -119,7 +119,6 @@ async function getTemplateCompletion( ); } -export type DispatcherOptions = InitializeCommandHandlerContextOptions; export async function createDispatcher( hostName: string, options?: DispatcherOptions, diff --git a/ts/packages/dispatcher/src/index.ts b/ts/packages/dispatcher/src/index.ts index b85713632..cb18acb89 100644 --- a/ts/packages/dispatcher/src/index.ts +++ b/ts/packages/dispatcher/src/index.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. export { createDispatcher, Dispatcher, CommandResult } from "./dispatcher.js"; +export type { DispatcherOptions } from "./context/commandHandlerContext.js"; export type { CommandCompletionResult } from "./command/completion.js"; export type { AppAgentProvider, diff --git a/ts/packages/dispatcher/src/internal.ts b/ts/packages/dispatcher/src/internal.ts index 7771ad98a..43acd9a84 100644 --- a/ts/packages/dispatcher/src/internal.ts +++ b/ts/packages/dispatcher/src/internal.ts @@ -32,3 +32,5 @@ export { getSchemaNamesForActionConfigProvider, } from "./agentProvider/agentProviderUtils.js"; export { getInstanceDir } from "./utils/userData.js"; + +export type { ChatHistoryInput } from "./context/system/handlers/historyCommandHandler.js"; diff --git a/ts/packages/dispatcher/src/utils/userData.ts b/ts/packages/dispatcher/src/utils/userData.ts index ce4613b79..b2713fe96 100644 --- a/ts/packages/dispatcher/src/utils/userData.ts +++ b/ts/packages/dispatcher/src/utils/userData.ts @@ -211,8 +211,18 @@ export function ensureCacheDir(instanceDir: string) { return dir; } -export function getUserId() { +let userid: string | undefined; +export function getUserId(): string { + if (userid !== undefined) { + return userid; + } + const currentGlobalUserConfig = readGlobalUserConfig(); + if (currentGlobalUserConfig !== undefined) { + userid = currentGlobalUserConfig.userid; + return userid; + } return lockUserData(() => { - return ensureGlobalUserConfig().userid; + userid = ensureGlobalUserConfig().userid; + return userid; }); }