@xalia/agent 0.6.1 → 0.6.3

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.
Files changed (127) hide show
  1. package/dist/agent/src/agent/agent.js +109 -57
  2. package/dist/agent/src/agent/agentUtils.js +24 -26
  3. package/dist/agent/src/agent/compressingContextManager.js +3 -2
  4. package/dist/agent/src/agent/dummyLLM.js +1 -3
  5. package/dist/agent/src/agent/imageGenLLM.js +67 -0
  6. package/dist/agent/src/agent/imageGenerator.js +43 -0
  7. package/dist/agent/src/agent/llm.js +27 -0
  8. package/dist/agent/src/agent/mcpServerManager.js +18 -6
  9. package/dist/agent/src/agent/nullAgentEventHandler.js +6 -0
  10. package/dist/agent/src/agent/openAILLM.js +3 -3
  11. package/dist/agent/src/agent/openAILLMStreaming.js +41 -6
  12. package/dist/agent/src/chat/client/chatClient.js +154 -235
  13. package/dist/agent/src/chat/client/constants.js +1 -2
  14. package/dist/agent/src/chat/client/sessionClient.js +47 -15
  15. package/dist/agent/src/chat/client/sessionFiles.js +102 -0
  16. package/dist/agent/src/chat/data/apiKeyManager.js +38 -7
  17. package/dist/agent/src/chat/data/database.js +83 -70
  18. package/dist/agent/src/chat/data/dbSessionFileModels.js +49 -0
  19. package/dist/agent/src/chat/data/dbSessionFiles.js +76 -0
  20. package/dist/agent/src/chat/data/dbSessionMessages.js +57 -0
  21. package/dist/agent/src/chat/data/mimeTypes.js +44 -0
  22. package/dist/agent/src/chat/protocol/messages.js +21 -1
  23. package/dist/agent/src/chat/server/chatContextManager.js +19 -16
  24. package/dist/agent/src/chat/server/connectionManager.js +14 -36
  25. package/dist/agent/src/chat/server/connectionManager.test.js +3 -16
  26. package/dist/agent/src/chat/server/conversation.js +73 -44
  27. package/dist/agent/src/chat/server/imageGeneratorTools.js +111 -0
  28. package/dist/agent/src/chat/server/openSession.js +398 -233
  29. package/dist/agent/src/chat/server/openSessionMessageSender.js +2 -0
  30. package/dist/agent/src/chat/server/server.js +5 -8
  31. package/dist/agent/src/chat/server/sessionFileManager.js +171 -38
  32. package/dist/agent/src/chat/server/sessionRegistry.js +214 -42
  33. package/dist/agent/src/chat/server/test-utils/mockFactories.js +12 -11
  34. package/dist/agent/src/chat/server/tools.js +27 -6
  35. package/dist/agent/src/chat/utils/approvalManager.js +82 -64
  36. package/dist/agent/src/chat/utils/multiAsyncQueue.js +9 -1
  37. package/dist/agent/src/chat/{client/responseHandler.js → utils/responseAwaiter.js} +41 -18
  38. package/dist/agent/src/test/agent.test.js +104 -63
  39. package/dist/agent/src/test/approvalManager.test.js +79 -35
  40. package/dist/agent/src/test/chatContextManager.test.js +16 -17
  41. package/dist/agent/src/test/clientServerConnection.test.js +2 -2
  42. package/dist/agent/src/test/db.test.js +33 -70
  43. package/dist/agent/src/test/dbSessionFiles.test.js +179 -0
  44. package/dist/agent/src/test/dbSessionMessages.test.js +67 -0
  45. package/dist/agent/src/test/dbTestTools.js +6 -5
  46. package/dist/agent/src/test/imageLoad.test.js +1 -1
  47. package/dist/agent/src/test/mcpServerManager.test.js +1 -1
  48. package/dist/agent/src/test/multiAsyncQueue.test.js +50 -0
  49. package/dist/agent/src/test/responseAwaiter.test.js +74 -0
  50. package/dist/agent/src/test/testTools.js +12 -0
  51. package/dist/agent/src/tool/agentChat.js +25 -6
  52. package/dist/agent/src/tool/agentMain.js +1 -1
  53. package/dist/agent/src/tool/chatMain.js +115 -6
  54. package/dist/agent/src/tool/commandPrompt.js +7 -3
  55. package/dist/agent/src/tool/files.js +23 -15
  56. package/dist/agent/src/tool/options.js +2 -2
  57. package/package.json +1 -1
  58. package/scripts/setup_chat +2 -2
  59. package/scripts/test_chat +95 -36
  60. package/src/agent/agent.ts +152 -41
  61. package/src/agent/agentUtils.ts +34 -41
  62. package/src/agent/compressingContextManager.ts +5 -4
  63. package/src/agent/context.ts +1 -1
  64. package/src/agent/dummyLLM.ts +1 -3
  65. package/src/agent/iAgentEventHandler.ts +15 -2
  66. package/src/agent/imageGenLLM.ts +99 -0
  67. package/src/agent/imageGenerator.ts +60 -0
  68. package/src/agent/llm.ts +128 -4
  69. package/src/agent/mcpServerManager.ts +26 -7
  70. package/src/agent/nullAgentEventHandler.ts +6 -0
  71. package/src/agent/openAILLM.ts +3 -8
  72. package/src/agent/openAILLMStreaming.ts +60 -14
  73. package/src/chat/client/chatClient.ts +262 -286
  74. package/src/chat/client/constants.ts +0 -2
  75. package/src/chat/client/sessionClient.ts +82 -20
  76. package/src/chat/client/sessionFiles.ts +142 -0
  77. package/src/chat/data/apiKeyManager.ts +55 -7
  78. package/src/chat/data/dataModels.ts +17 -7
  79. package/src/chat/data/database.ts +107 -92
  80. package/src/chat/data/dbSessionFileModels.ts +91 -0
  81. package/src/chat/data/dbSessionFiles.ts +99 -0
  82. package/src/chat/data/dbSessionMessages.ts +68 -0
  83. package/src/chat/data/mimeTypes.ts +58 -0
  84. package/src/chat/protocol/messages.ts +136 -25
  85. package/src/chat/server/chatContextManager.ts +42 -24
  86. package/src/chat/server/connectionManager.test.ts +2 -22
  87. package/src/chat/server/connectionManager.ts +18 -53
  88. package/src/chat/server/conversation.ts +106 -59
  89. package/src/chat/server/imageGeneratorTools.ts +138 -0
  90. package/src/chat/server/openSession.ts +606 -325
  91. package/src/chat/server/openSessionMessageSender.ts +4 -0
  92. package/src/chat/server/server.ts +5 -11
  93. package/src/chat/server/sessionFileManager.ts +223 -63
  94. package/src/chat/server/sessionRegistry.ts +317 -52
  95. package/src/chat/server/test-utils/mockFactories.ts +13 -13
  96. package/src/chat/server/tools.ts +43 -8
  97. package/src/chat/utils/agentSessionMap.ts +2 -2
  98. package/src/chat/utils/approvalManager.ts +153 -81
  99. package/src/chat/utils/multiAsyncQueue.ts +11 -1
  100. package/src/chat/{client/responseHandler.ts → utils/responseAwaiter.ts} +73 -23
  101. package/src/test/agent.test.ts +152 -75
  102. package/src/test/approvalManager.test.ts +108 -40
  103. package/src/test/chatContextManager.test.ts +26 -22
  104. package/src/test/clientServerConnection.test.ts +3 -3
  105. package/src/test/compressingContextManager.test.ts +1 -1
  106. package/src/test/context.test.ts +2 -1
  107. package/src/test/conversation.test.ts +1 -1
  108. package/src/test/db.test.ts +41 -83
  109. package/src/test/dbSessionFiles.test.ts +258 -0
  110. package/src/test/dbSessionMessages.test.ts +85 -0
  111. package/src/test/dbTestTools.ts +9 -5
  112. package/src/test/imageLoad.test.ts +2 -2
  113. package/src/test/mcpServerManager.test.ts +3 -1
  114. package/src/test/multiAsyncQueue.test.ts +58 -0
  115. package/src/test/responseAwaiter.test.ts +103 -0
  116. package/src/test/testTools.ts +15 -1
  117. package/src/tool/agentChat.ts +36 -8
  118. package/src/tool/agentMain.ts +7 -7
  119. package/src/tool/chatMain.ts +128 -7
  120. package/src/tool/commandPrompt.ts +10 -5
  121. package/src/tool/files.ts +30 -13
  122. package/src/tool/options.ts +1 -1
  123. package/test_data/dummyllm_script_image_gen.json +19 -0
  124. package/test_data/dummyllm_script_invoke_image_gen_tool.json +30 -0
  125. package/test_data/image_gen_test_profile.json +5 -0
  126. package/dist/agent/src/test/responseHandler.test.js +0 -61
  127. package/src/test/responseHandler.test.ts +0 -78
@@ -128,21 +128,18 @@ export function createMockUserConnectionManager(): {
128
128
  mock: IUserConnectionManager<ServerToClient>;
129
129
  spies: {
130
130
  sendToUsers: ReturnType<typeof vi.fn>;
131
- getLiveUserApiKey: ReturnType<typeof vi.fn>;
132
131
  };
133
132
  } {
134
133
  const spies = {
135
134
  sendToUsers: vi.fn(),
136
135
  sendToConnection: vi.fn(),
137
136
  sendServerError: vi.fn(),
138
- getLiveUserApiKey: vi.fn(),
139
137
  };
140
138
 
141
139
  const mock = {
142
140
  sendToUsers: spies.sendToUsers,
143
141
  sendToConnection: spies.sendToConnection,
144
142
  sendServerError: spies.sendServerError,
145
- getLiveUserApiKey: spies.getLiveUserApiKey,
146
143
  } as IUserConnectionManager<ServerToClient>;
147
144
 
148
145
  return { mock, spies };
@@ -154,16 +151,25 @@ export function createMockUserConnectionManager(): {
154
151
  export function createMockSessionRegistry(): {
155
152
  mock: IMessageProcessor<ClientToServer>;
156
153
  spies: {
154
+ authenticate: ReturnType<typeof vi.fn>;
157
155
  processMessage: ReturnType<typeof vi.fn>;
158
156
  handleUserDisconnect: ReturnType<typeof vi.fn>;
159
157
  };
160
158
  } {
161
159
  const spies = {
160
+ authenticate: vi.fn().mockImplementation((apiKey) => {
161
+ if (apiKey === "valid-api-key")
162
+ return Promise.resolve(MOCK_USERS.owner.uuid);
163
+ if (apiKey === "participant-api-key")
164
+ return Promise.resolve(MOCK_USERS.participant.uuid);
165
+ return Promise.resolve(null);
166
+ }),
162
167
  processMessage: vi.fn(),
163
168
  handleUserDisconnect: vi.fn(),
164
169
  };
165
170
 
166
171
  const mock = {
172
+ authenticate: spies.authenticate,
167
173
  processMessage: spies.processMessage,
168
174
  handleUserDisconnect: spies.handleUserDisconnect,
169
175
  } as IMessageProcessor<ClientToServer>;
@@ -267,6 +273,7 @@ export function createMockSessionList(): Array<SessionData> {
267
273
  workspace: undefined,
268
274
  updated_at: MOCK_SESSIONS.active.updated_at || "",
269
275
  user_uuid: MOCK_SESSIONS.active.user_uuid,
276
+ agent_paused: false,
270
277
  },
271
278
  {
272
279
  session_uuid: MOCK_SESSIONS.secondary.uuid,
@@ -276,6 +283,7 @@ export function createMockSessionList(): Array<SessionData> {
276
283
  workspace: undefined,
277
284
  updated_at: MOCK_SESSIONS.secondary.updated_at || "",
278
285
  user_uuid: MOCK_SESSIONS.secondary.user_uuid,
286
+ agent_paused: false,
279
287
  },
280
288
  ];
281
289
  }
@@ -409,14 +417,6 @@ export function setupStandardMockBehaviors(mocks: {
409
417
  }
410
418
 
411
419
  // Setup user connection manager mocks
412
- if (mocks.userConnectionManager) {
413
- mocks.userConnectionManager.spies.getLiveUserApiKey.mockImplementation(
414
- (userId: string) => {
415
- if (userId === MOCK_USERS.owner.uuid) return "valid-api-key";
416
- if (userId === MOCK_USERS.participant.uuid)
417
- return "participant-api-key";
418
- return undefined;
419
- }
420
- );
421
- }
420
+ // if (mocks.userConnectionManager) {
421
+ // }
422
422
  }
@@ -10,6 +10,8 @@ import { IPlatform } from "../../agent/iplatform";
10
10
  import { htmlToText } from "../utils/htmlToText";
11
11
  import { getLogger } from "@xalia/xmcp/sdk";
12
12
  import { webSearch } from "../utils/search";
13
+ import { ChatSessionFileManager, fileManagerTool } from "./sessionFileManager";
14
+ import { genImageFileTool } from "./imageGeneratorTools";
13
15
 
14
16
  const logger = getLogger();
15
17
 
@@ -17,10 +19,19 @@ const logger = getLogger();
17
19
  * Returns a function which parses an `args` struct and attempts to extract
18
20
  * multiple string parameters with the given names. e.g.
19
21
  *
20
- * const parseFn = makeParseArgsFn(["arg0", "arg1"] as const)
22
+ * const parseFn = makeParseArgsFn(
23
+ * ["arg0", "arg1"] as const,
24
+ * ["opt0"] as const)
21
25
  *
22
- * creates `parseFn: (args: unknown) => { arg0: string, arg1: string }` which
23
- * can be used to parse tool arguments.
26
+ * creates
27
+ *
28
+ * parseFn: (args: unknown) => {
29
+ * arg0: string,
30
+ * arg1: string,
31
+ * opt0: string|undefined
32
+ * }
33
+ *
34
+ * which can be used to parse tool arguments.
24
35
  *
25
36
  * NOTE, the complex type parameters ensures that the name list is a
26
37
  * compile-time value, which in turn ensures that the return value of this
@@ -28,19 +39,36 @@ const logger = getLogger();
28
39
  */
29
40
  export function makeParseArgsFn<
30
41
  T extends readonly string[] & (string extends T[number] ? never : unknown),
31
- >(names: T): (args: unknown) => { [K in T[number]]: string } {
42
+ U extends readonly string[] & (string extends U[number] ? never : unknown),
43
+ >(
44
+ names: T,
45
+ optNames?: U
46
+ ): (
47
+ args: unknown
48
+ ) => { [K in T[number]]: string } & { [K in U[number]]: string | undefined } {
32
49
  return (args: unknown) => {
33
- if (typeof args !== "object") {
50
+ if (!args || typeof args !== "object") {
34
51
  throw new Error(`invalid args: ${typeof args}`);
35
52
  }
36
- const argsObj = args as Record<string, string>;
53
+ const argsObj = args as Record<string, string | undefined>;
37
54
  for (const name of names) {
38
55
  const val = argsObj[name];
39
56
  if (typeof val !== "string") {
40
57
  throw new Error(`invalid expr args.${name}: ${typeof val}`);
41
58
  }
42
59
  }
43
- return argsObj as { [K in T[number]]: string };
60
+ if (optNames) {
61
+ for (const name of optNames) {
62
+ const val = argsObj[name];
63
+ if (typeof val !== "undefined" && typeof val !== "string") {
64
+ throw new Error(`invalid expr args.${name}: ${typeof val}`);
65
+ }
66
+ }
67
+ }
68
+
69
+ return argsObj as { [K in T[number]]: string } & {
70
+ [K in U[number]]: string | undefined;
71
+ };
44
72
  };
45
73
  }
46
74
 
@@ -255,11 +283,18 @@ export function openURLTool(): IAgentToolProvider {
255
283
  export async function addDefaultChatTools(
256
284
  agent: Agent,
257
285
  timezone: string,
258
- platform: IPlatform
286
+ platform: IPlatform,
287
+ fileManager: ChatSessionFileManager,
288
+ llmUrl: string,
289
+ llmApiKey: string
259
290
  ): Promise<void> {
260
291
  await agent.addAgentToolProvider(datetimeTool(timezone));
261
292
  await agent.addAgentToolProvider(calculatorTool);
262
293
  await agent.addAgentToolProvider(renderTool(platform));
263
294
  await agent.addAgentToolProvider(webSearchTool());
264
295
  await agent.addAgentToolProvider(openURLTool());
296
+ await agent.addAgentToolProvider(fileManagerTool(fileManager));
297
+ await agent.addAgentToolProvider(
298
+ await genImageFileTool(llmUrl, llmApiKey, fileManager)
299
+ );
265
300
  }
@@ -1,5 +1,5 @@
1
1
  import { SavedAgentProfile, getLogger } from "@xalia/xmcp/sdk";
2
- import { SessionData, AgentSessionData } from "../data/dataModels";
2
+ import { AgentSessionData, SessionDescriptor } from "../data/dataModels";
3
3
 
4
4
  const logger = getLogger();
5
5
 
@@ -23,7 +23,7 @@ export function emptyAgentProfile(agentUuid: string): SavedAgentProfile {
23
23
 
24
24
  // build agentSessionMap from sessions and agents
25
25
  export function buildAgentSessionMap(
26
- sessions: Map<string, SessionData>,
26
+ sessions: Map<string, SessionDescriptor>,
27
27
  agents: Map<string, SavedAgentProfile>
28
28
  ): Map<string, AgentSessionData> {
29
29
  const agentSessionMap: Map<string, AgentSessionData> = new Map();
@@ -1,108 +1,180 @@
1
- import { getLogger } from "@xalia/xmcp/sdk";
1
+ import {
2
+ AgentPreferences,
3
+ getLogger,
4
+ prefsGetAutoApprove,
5
+ prefsSetAutoApprove,
6
+ } from "@xalia/xmcp/sdk";
7
+ import { ResponseAwaiter } from "./responseAwaiter";
8
+ import {
9
+ ClientToolCallApprovalResult,
10
+ ServerToClient,
11
+ ServerToolAutoApprovalSet,
12
+ } from "../protocol/messages";
13
+ import { Database } from "../data/database";
14
+ import { ChatCompletionMessageToolCall } from "../../agent/llm";
15
+ import { ISessionMessageSender } from "../server/openSessionMessageSender";
2
16
 
3
17
  const logger = getLogger();
4
18
 
5
- export type ApprovalResult = {
6
- approved: boolean;
7
- auto_approve: boolean;
8
- };
9
-
10
- /**
11
- * Thrown in the resultP promise when an approval times out.
12
- */
13
- export class ApprovalTimeout extends Error {
14
- constructor(message: string) {
15
- super(message);
16
- this.name = "ApprovalTimeout";
17
- }
19
+ export interface IAgentPreferencesWriter {
20
+ updatePreferences(
21
+ agentProfileUUID: string,
22
+ settings: AgentPreferences
23
+ ): Promise<void>;
18
24
  }
19
25
 
20
- export class ApprovalCancelled extends Error {
21
- constructor(message: string) {
22
- super(message);
23
- this.name = "ApprovalCancelled";
26
+ export class DbAgentPreferencesWriter implements IAgentPreferencesWriter {
27
+ constructor(private db: Database) {}
28
+
29
+ updatePreferences(
30
+ agentProfileUUID: string,
31
+ preferences: AgentPreferences
32
+ ): Promise<void> {
33
+ return this.db.updateAgentProfilePreferences(agentProfileUUID, preferences);
24
34
  }
25
35
  }
26
36
 
27
- type ApprovalData = {
28
- resolve: (result: ApprovalResult) => void;
29
- error: (e: Error) => void;
30
- timeoutId?: NodeJS.Timeout;
31
- };
32
-
33
37
  /**
34
- * The caller initiates an approval for a specific server, and a unique ID is
35
- * generated for it, along with a promise for the resolution of the approval.
36
- * The caller returns the ID to some client(s) and waits on the promise.
37
- *
38
- * When an approval or rejection (or timeout) is received, the promise is
39
- * resolved.
38
+ * Handles an in-memory caching / updating of the auto-approve settings for
39
+ * tool calls. Also handles querying the client for approval and waiting for
40
+ * responses.
40
41
  */
41
- export class ApprovalManager {
42
- private approvals = new Map<string, ApprovalData>();
43
- private timeoutMs?: number;
42
+ export class ToolApprovalManager {
43
+ private sessionUUID: string;
44
+ private agentProfileUUID: string;
45
+ private agentProfilePreferences: AgentPreferences;
46
+ private sender: ISessionMessageSender<ServerToClient>;
47
+ private writer: IAgentPreferencesWriter;
48
+ private responseAwaiter: ResponseAwaiter<ClientToolCallApprovalResult>;
44
49
 
45
- constructor(timeoutMs?: number) {
46
- this.timeoutMs = timeoutMs;
50
+ constructor(
51
+ sessionUUID: string,
52
+ agentProfileUUID: string,
53
+ agentProfilePreferences: AgentPreferences,
54
+ sender: ISessionMessageSender<ServerToClient>,
55
+ writer: IAgentPreferencesWriter,
56
+ timeoutMs?: number
57
+ ) {
58
+ this.sessionUUID = sessionUUID;
59
+ this.agentProfileUUID = agentProfileUUID;
60
+ this.agentProfilePreferences = agentProfilePreferences;
61
+ this.sender = sender;
62
+ this.writer = writer;
63
+ this.responseAwaiter = ResponseAwaiter.init(
64
+ undefined,
65
+ (msg) => msg.id,
66
+ timeoutMs
67
+ );
47
68
  }
48
69
 
49
- public shutdown(): void {
50
- for (const [id, approval] of this.approvals) {
51
- if (approval.timeoutId) {
52
- clearTimeout(approval.timeoutId);
53
- }
54
- approval.error(new ApprovalCancelled("shutdown"));
55
- this.approvals.delete(id);
70
+ /**
71
+ * Check for auto-approval, or query the client. Handle approval response
72
+ * (or timeout) and update auto-approval settings.
73
+ *
74
+ * The returned `requested` value indicates whether approval was requested.
75
+ */
76
+ public async getApproval(
77
+ serverName: string,
78
+ tool: string,
79
+ toolCall: ChatCompletionMessageToolCall
80
+ ): Promise<{ approved: boolean; requested: boolean }> {
81
+ const autoApproved = prefsGetAutoApprove(
82
+ this.agentProfilePreferences,
83
+ serverName,
84
+ tool
85
+ );
86
+ if (autoApproved) {
87
+ return { approved: true, requested: false };
56
88
  }
57
- }
58
89
 
59
- public startApproval(name: string): {
60
- id: string;
61
- resultP: Promise<ApprovalResult>;
62
- } {
63
- const id = this.generateUniqueId(name);
64
- const resultP = new Promise<ApprovalResult>((resolve, error) => {
65
- let timeoutId;
66
- if (this.timeoutMs) {
67
- timeoutId = setTimeout(() => {
68
- const approval = this.approvals.get(id);
69
- if (approval) {
70
- this.approvals.delete(id);
71
- error(new ApprovalTimeout(`approval ${id} (${name}) timed out`));
72
- }
73
- }, this.timeoutMs);
90
+ // Query the owner for approval
91
+
92
+ const id = this.generateUniqueId(toolCall.function.name);
93
+ try {
94
+ const approvalP = this.responseAwaiter.waitForResponse(id);
95
+
96
+ this.sender.broadcast({
97
+ type: "approve_tool_call",
98
+ id,
99
+ tool_call: toolCall,
100
+ session_id: this.sessionUUID,
101
+ });
102
+
103
+ logger.debug(`[ApprovalManager.getApproval] awaiting approval ${id}`);
104
+ const approval = await approvalP;
105
+ logger.debug(
106
+ `[ApprovalManager.getApproval] approval ${JSON.stringify(approval)}`
107
+ );
108
+
109
+ // Handle any auto-approve update, informing other clients.
110
+
111
+ if (approval.auto_approve) {
112
+ logger.debug("[ApprovalManager.getApproval] updated preferences");
113
+ const autoApprovalMsg = await this.setAutoApprove(
114
+ serverName,
115
+ tool,
116
+ true
117
+ );
118
+ if (autoApprovalMsg) {
119
+ this.sender.broadcast(autoApprovalMsg);
120
+ }
74
121
  }
75
122
 
76
- this.approvals.set(id, { resolve, error, timeoutId });
77
- });
123
+ // Broadcast the result of the approval
124
+
125
+ this.sender.broadcast({
126
+ type: "tool_call_approval_result",
127
+ id: approval.id,
128
+ result: approval.result,
129
+ session_id: this.sessionUUID,
130
+ });
78
131
 
79
- logger.debug(`new approval ${id}`);
80
- return { id, resultP };
132
+ return { approved: approval.result, requested: true };
133
+ } catch (e) {
134
+ logger.debug(
135
+ `[OpenSession.onToolCall] error waiting for approval ${id}: ` +
136
+ String(e)
137
+ );
138
+ throw e;
139
+ }
81
140
  }
82
141
 
83
142
  /**
84
- * Returns true if this result was accepted. False if the approval was
85
- * already answered.
143
+ * Handle a request to set auto-approval for a given tool. If there was a
144
+ * change, return the message to be broadcast.
86
145
  */
87
- public approvalResult(
88
- id: string,
89
- approved: boolean,
90
- auto_approve: boolean
91
- ): boolean {
92
- const approval = this.approvals.get(id);
93
- if (approval) {
94
- logger.debug(`approval ${id} present. resolving ${String(approved)}`);
95
-
96
- if (approval.timeoutId) {
97
- clearTimeout(approval.timeoutId);
98
- }
99
- this.approvals.delete(id);
100
- approval.resolve({ approved, auto_approve });
101
- return true;
146
+ public async setAutoApprove(
147
+ serverName: string,
148
+ tool: string,
149
+ autoApprove: boolean
150
+ ): Promise<ServerToolAutoApprovalSet | undefined> {
151
+ if (
152
+ prefsSetAutoApprove(
153
+ this.agentProfilePreferences,
154
+ serverName,
155
+ tool,
156
+ autoApprove
157
+ )
158
+ ) {
159
+ await this.writer.updatePreferences(
160
+ this.agentProfileUUID,
161
+ this.agentProfilePreferences
162
+ );
163
+ return {
164
+ type: "tool_auto_approval_set",
165
+ server_name: serverName,
166
+ tool,
167
+ auto_approve: autoApprove,
168
+ session_id: this.sessionUUID,
169
+ };
102
170
  }
171
+ }
103
172
 
104
- logger.debug(`approval ${id} not present`);
105
- return false;
173
+ /**
174
+ * Forward all approval result messages here.
175
+ */
176
+ public onApprovalResult(msg: ClientToolCallApprovalResult): void {
177
+ this.responseAwaiter.onMessage(msg);
106
178
  }
107
179
 
108
180
  private generateUniqueId(tag: string): string {
@@ -3,6 +3,7 @@ export class MultiAsyncQueue<T> {
3
3
  private process: (queueEntry: T[]) => Promise<void>;
4
4
  private maxBacklog: number;
5
5
  private running: boolean = false;
6
+ private paused: boolean = false;
6
7
 
7
8
  constructor(
8
9
  process: (queueEntry: T[]) => Promise<void>,
@@ -30,8 +31,17 @@ export class MultiAsyncQueue<T> {
30
31
  return true;
31
32
  }
32
33
 
34
+ public pause() {
35
+ this.paused = true;
36
+ }
37
+
38
+ public unpause() {
39
+ this.paused = false;
40
+ setTimeout(() => void this.tryNext(), 0);
41
+ }
42
+
33
43
  private async tryNext() {
34
- if (this.running) {
44
+ if (this.running || this.paused) {
35
45
  return;
36
46
  }
37
47
 
@@ -2,12 +2,13 @@ import { getLogger } from "@xalia/xmcp/sdk";
2
2
 
3
3
  const DEFAULT_TIMEOUT_MS: number = 10000;
4
4
 
5
- type ClientMsg = { client_message_id: string };
6
- type ServerMsg = { type: string; client_message_id?: string };
5
+ type ServerMsg = { type: string };
7
6
  // Messages of type S which also have a message field. The message
8
7
  // representing an error should be of this type.
9
8
  type ServerErr<S> = Extract<S, { message: string }>;
10
9
 
10
+ type IdExtractor<S extends ServerMsg> = (msg: S) => string | undefined;
11
+
11
12
  type WaitingEntry<ServerMessageT> = {
12
13
  resolve: (s: ServerMessageT) => void;
13
14
  error: (e: Error) => void;
@@ -16,13 +17,19 @@ type WaitingEntry<ServerMessageT> = {
16
17
 
17
18
  const logger = getLogger();
18
19
 
20
+ function defaultImmediate<T>(x: T): T {
21
+ return x;
22
+ }
23
+
19
24
  /**
20
25
  *
21
26
  * Handles response messages and timeouts for client request messages.
22
27
  *
23
- * Create a ResponseHandler for a specific class of queries
28
+ * Create a ResponseAwaiter for a specific class of queries
24
29
  *
25
- * this.responseHandler = new ResponseHandler<SomeRequest, SomeResponse>()
30
+ * this.responseHandler = new ResponseAwaiter<SomeRequest, SomeResponse>(
31
+ * (msg: SomeResponse) => msg.client_message_id;
32
+ * )
26
33
  *
27
34
  * Use as follows:
28
35
  *
@@ -36,14 +43,15 @@ const logger = getLogger();
36
43
  *
37
44
  * // Get a Promise representing the response to this message, which
38
45
  * // we can await.
39
- * const response = await this.responseHandler.waitForResponse(msg);
46
+ * const response =
47
+ * await this.responseHandler.waitForResponse(client_message_id);
40
48
  *
41
49
  * // Perform any processing on the response
42
50
  * return response.response_data;
43
51
  * }
44
52
  * ```
45
53
  *
46
- * ResponseHandler must be informed of relevant messages in order to resolve
54
+ * ResponseAwaiter must be informed of relevant messages in order to resolve
47
55
  * responses:
48
56
  *
49
57
  * ```
@@ -57,46 +65,82 @@ const logger = getLogger();
57
65
  * }
58
66
  * }
59
67
  * ```
68
+ *
69
+ * If some actions need to happen immediately on receipt of the message,
70
+ * instead of when the `Promise.resolve` function is resolved, add an
71
+ * `immediateAction` callback, which can optionally transfrom `ServerMsgT`
72
+ * into `FinalResponseT` to be passed back by `waitForResponse`.
60
73
  */
61
- export class ResponseHandler<
62
- ClientMsgT extends ClientMsg,
74
+ export class ResponseAwaiter<
63
75
  ServerMsgT extends ServerMsg,
76
+ FinalResponseT = ServerMsgT,
64
77
  > {
65
- readonly waiting: Map<string, WaitingEntry<ServerMsgT>>;
78
+ readonly waiting: Map<string, WaitingEntry<FinalResponseT>>;
79
+ readonly idExtractor: IdExtractor<ServerMsgT>;
80
+ readonly immediateAction: (x: ServerMsgT) => FinalResponseT;
66
81
  readonly timeoutMS: number;
67
82
  readonly errorType: ServerErr<ServerMsgT>["type"] | undefined;
68
83
 
69
84
  /**
70
85
  * errorType: the type field of the message which represents an error.
71
86
  */
72
- constructor(
87
+ private constructor(
73
88
  // value of the `type` field of an error message type,
74
89
  // e.g. "session_error". Compiler should ensure that the `ServerMsgT` with
75
90
  // this `type` field at least has a `message` field.
76
91
  errorType: undefined | ServerErr<ServerMsgT>["type"],
92
+ idExtractor: IdExtractor<ServerMsgT>,
93
+ immediateAction: (x: ServerMsgT) => FinalResponseT,
77
94
  timeoutMS: number = DEFAULT_TIMEOUT_MS
78
95
  ) {
79
96
  this.waiting = new Map();
97
+ this.idExtractor = idExtractor;
98
+ this.immediateAction = immediateAction;
80
99
  this.timeoutMS = timeoutMS;
81
100
  this.errorType = errorType;
82
101
  }
83
102
 
103
+ static init<S extends ServerMsg>(
104
+ errorType: undefined | ServerErr<S>["type"],
105
+ idExtractor: IdExtractor<S>,
106
+ timeoutMS: number = DEFAULT_TIMEOUT_MS
107
+ ): ResponseAwaiter<S, S> {
108
+ return new ResponseAwaiter(
109
+ errorType,
110
+ idExtractor,
111
+ defaultImmediate,
112
+ timeoutMS
113
+ );
114
+ }
115
+
116
+ static initWithImmediate<S extends ServerMsg, F>(
117
+ errorType: undefined | ServerErr<S>["type"],
118
+ idExtractor: IdExtractor<S>,
119
+ immediateAction: (x: S) => F,
120
+ timeoutMS: number = DEFAULT_TIMEOUT_MS
121
+ ): ResponseAwaiter<S, F> {
122
+ return new ResponseAwaiter(
123
+ errorType,
124
+ idExtractor,
125
+ immediateAction,
126
+ timeoutMS
127
+ );
128
+ }
129
+
84
130
  /**
85
131
  * Given a request message, return a promise representing the corresponding
86
132
  * response message from the server.
87
133
  */
88
- waitForResponse(msg: ClientMsgT): Promise<ServerMsgT> {
89
- return new Promise<ServerMsgT>((resolve, error) => {
90
- const msgId = msg.client_message_id;
91
-
134
+ waitForResponse(msgId: string): Promise<FinalResponseT> {
135
+ return new Promise<FinalResponseT>((resolve, error) => {
92
136
  const timeoutId = setTimeout(() => {
93
137
  const waiting = this.waiting.get(msgId);
94
138
  if (!waiting) {
95
- logger.warn(`[ResponseHandler] timeout for ${msgId} with no entry`);
139
+ logger.warn(`[ResponseAwaiter] timeout for ${msgId} with no entry`);
96
140
  return;
97
141
  }
98
142
  this.waiting.delete(msgId);
99
- logger.warn(`[ResponseHandler] timeout for client_msg_id ${msgId}`);
143
+ logger.warn(`[ResponseAwaiter] timeout for client_msg_id ${msgId}`);
100
144
  error(new Error(`timeout for client_msg_id ${msgId}`));
101
145
  }, this.timeoutMS);
102
146
 
@@ -104,20 +148,25 @@ export class ResponseHandler<
104
148
  });
105
149
  }
106
150
 
151
+ waitingForId(msgId: string): boolean {
152
+ return this.waiting.has(msgId);
153
+ }
154
+
107
155
  /**
108
- * Pass response (and error) messages in here.
156
+ * Pass response (and error) messages in here. Returns `true` if the
157
+ * message was consumed. Otherwise `false`.
109
158
  */
110
- onMessage(msg: ServerMsgT): void {
111
- const msgId = msg.client_message_id;
159
+ onMessage(msg: ServerMsgT): boolean {
160
+ const msgId = this.idExtractor(msg);
112
161
  if (!msgId) {
113
- return;
162
+ return false;
114
163
  }
115
164
  const waiting = this.waiting.get(msgId);
116
165
  if (!waiting) {
117
166
  logger.warn(
118
- `[ResponseHandler] resolve for ${msgId} with no entry (timeout?)`
167
+ `[ResponseAwaiter] resolve for ${msgId} with no entry (timeout?)`
119
168
  );
120
- return;
169
+ return false;
121
170
  }
122
171
 
123
172
  clearTimeout(waiting.timeoutId);
@@ -125,7 +174,8 @@ export class ResponseHandler<
125
174
  if (this.errorType && msg.type === this.errorType) {
126
175
  waiting.error(new Error((msg as ServerErr<ServerMsgT>).message));
127
176
  } else {
128
- waiting.resolve(msg);
177
+ waiting.resolve(this.immediateAction(msg));
129
178
  }
179
+ return true;
130
180
  }
131
181
  }