agentnet 0.1.2 → 0.1.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.
package/README.md CHANGED
@@ -267,7 +267,7 @@ const message = new Message({
267
267
 
268
268
  // Query the agent using the client
269
269
  console.log("Sending query to the entrypoint agent...");
270
- const response = await client.queryIo(natsIO, 'smartchat.entrypointAgent', message); // {namespace}.{name}
270
+ const response = await client.queryIo(natsIO, 'smartchat', 'entrypointAgent', message);
271
271
 
272
272
  // Process the response
273
273
  console.log("Agent Response:", response.getContent());
@@ -0,0 +1,92 @@
1
+ # Conversation Class
2
+
3
+ The `Conversation` class manages conversation history with additional metadata for each message. It provides an improved way to handle conversations compared to a plain array, allowing for more sophisticated conversation management, including proper trimming based on message types.
4
+
5
+ ## Features
6
+
7
+ - Store messages with additional metadata (type, timestamp)
8
+ - Proper conversation trimming (e.g., keeping at least one user message)
9
+ - Support for different message types (user inputs, model responses, function calls)
10
+ - Easy serialization/deserialization
11
+ - Backward compatibility with existing code
12
+
13
+ ## Usage
14
+
15
+ ```javascript
16
+ import { Conversation } from 'smartagent';
17
+
18
+ // Create a new conversation
19
+ const conversation = new Conversation();
20
+
21
+ // Add messages
22
+ conversation.addUserMessage({
23
+ role: 'user',
24
+ content: 'Hello, how can you help me?'
25
+ });
26
+
27
+ conversation.addModelResponse({
28
+ role: 'assistant',
29
+ content: 'I can help with many tasks. What do you need?'
30
+ });
31
+
32
+ // Get raw conversation for LLM APIs
33
+ const rawConversation = conversation.getRawConversation();
34
+
35
+ // Get all messages with metadata
36
+ const allMessages = conversation.getMessages();
37
+
38
+ // Trim conversation while ensuring it starts with a user message
39
+ conversation.trim(10);
40
+
41
+ // Import from existing array
42
+ conversation.importFromArray(existingArray);
43
+
44
+ // Serialize/deserialize
45
+ const serialized = conversation.serialize();
46
+ conversation.deserialize(serialized);
47
+ ```
48
+
49
+ ## Integration with SessionStore
50
+
51
+ The `SessionStore` class has been updated to use the `Conversation` class internally:
52
+
53
+ ```javascript
54
+ import { SessionStore } from 'smartagent';
55
+
56
+ const sessionStore = new SessionStore('session-id');
57
+
58
+ // Load the conversation
59
+ await sessionStore.load(store);
60
+
61
+ // Get the conversation manager
62
+ const conversationManager = sessionStore.getConversationManager();
63
+
64
+ // Add a message
65
+ conversationManager.addUserMessage({ /* message */ });
66
+
67
+ // Trim with proper handling
68
+ sessionStore.trimConversation(10);
69
+
70
+ // Save conversation
71
+ await sessionStore.dump(store);
72
+ ```
73
+
74
+ ## Message Types
75
+
76
+ The Conversation class tracks different message types:
77
+
78
+ - `user_input`: Messages from the user
79
+ - `model_response`: Responses from the LLM model
80
+ - `function_call`: Function/tool calls from the model
81
+ - `function_result`: Results of function/tool calls
82
+
83
+ ## Compatibility
84
+
85
+ The implementation maintains backward compatibility with existing code that uses plain arrays for conversations. The LLM implementations (`GeminiLLM` and `OpenAILLM`) have been updated to properly handle both plain arrays and `Conversation` objects.
86
+
87
+ ## Benefits
88
+
89
+ - **Better trimming**: Trimming maintains conversation context by ensuring at least one user message remains
90
+ - **Type awareness**: Messages are tracked by type, making it easier to understand the conversation flow
91
+ - **Metadata**: Additional information can be stored with each message
92
+ - **Serialization**: Easy serialization and deserialization for storage
@@ -0,0 +1,68 @@
1
+ import { Conversation, SessionStore } from '../src/index.js';
2
+ import { MemoryStore } from '../src/utils/store.js';
3
+
4
+ // Example of using the Conversation class
5
+ async function conversationExample() {
6
+ // Create a new conversation
7
+ const conversation = new Conversation();
8
+
9
+ // Add user and model messages
10
+ conversation.addUserMessage({
11
+ role: 'user',
12
+ content: 'Hello, I need help with my project.'
13
+ });
14
+
15
+ conversation.addModelResponse({
16
+ role: 'assistant',
17
+ content: 'I\'d be happy to help! What kind of project are you working on?'
18
+ });
19
+
20
+ // Get the raw conversation (format needed by LLMs)
21
+ const rawConversation = conversation.getRawConversation();
22
+ console.log('Raw conversation:', JSON.stringify(rawConversation, null, 2));
23
+
24
+ // Get full messages with metadata
25
+ const messages = conversation.getMessages();
26
+ console.log('Messages with metadata:', JSON.stringify(messages, null, 2));
27
+
28
+ // Trim the conversation
29
+ conversation.trim(5);
30
+ console.log('After trimming:', JSON.stringify(conversation.getMessages(), null, 2));
31
+
32
+ // Import from existing array
33
+ const existingConversation = [
34
+ { role: 'user', content: 'What is the weather today?' },
35
+ { role: 'assistant', content: 'I don\'t have access to real-time weather data.' },
36
+ { role: 'user', content: 'Can you help me with coding instead?' }
37
+ ];
38
+
39
+ conversation.importFromArray(existingConversation);
40
+ console.log('After importing:', JSON.stringify(conversation.getRawConversation(), null, 2));
41
+
42
+ // SessionStore example
43
+ const sessionStore = new SessionStore('example-session');
44
+ const store = new MemoryStore();
45
+
46
+ // Set conversation and state
47
+ sessionStore.setConversation(conversation);
48
+ sessionStore.setState({ userId: '12345', lastActivity: new Date().toISOString() });
49
+
50
+ // Trim conversation to keep only the latest 2 messages
51
+ sessionStore.trimConversation(2);
52
+ console.log('After trimming in SessionStore:',
53
+ JSON.stringify(sessionStore.getConversation(), null, 2));
54
+
55
+ // Save to store
56
+ await sessionStore.dump(store);
57
+
58
+ // Load from store
59
+ const newSessionStore = new SessionStore('example-session');
60
+ await newSessionStore.load(store);
61
+
62
+ console.log('Loaded conversation:',
63
+ JSON.stringify(newSessionStore.getConversation(), null, 2));
64
+ console.log('Loaded state:',
65
+ JSON.stringify(newSessionStore.getState(), null, 2));
66
+ }
67
+
68
+ conversationExample().catch(console.error);
@@ -349,7 +349,7 @@ async function main() {
349
349
 
350
350
  // Start with the triage agent
351
351
  const client = AgentClient();
352
- const res = await client.queryIo(natsIO, 'customerSupport.triageAgent', new Message({
352
+ const res = await client.queryIo(natsIO, 'customerSupport', 'triageAgent', new Message({
353
353
  content: customerQuery,
354
354
  session: {
355
355
  id: caseId,
@@ -363,7 +363,7 @@ async function main() {
363
363
 
364
364
  // For demonstration purposes, let's simulate a complete flow through the technical agent
365
365
  console.log("\nRouting to Technical Agent...");
366
- const res2 = await client.queryIo(natsIO, 'customerSupport.technicalAgent', new Message({
366
+ const res2 = await client.queryIo(natsIO, 'customerSupport', 'technicalAgent', new Message({
367
367
  content: customerQuery,
368
368
  session: {
369
369
  id: caseId,
@@ -388,7 +388,7 @@ async function main() {
388
388
 
389
389
  // Now follow up with the customer
390
390
  console.log("\nFollowing up with customer after resolution...");
391
- const res3 = await client.queryIo(natsIO, 'customerSupport.followupAgent', new Message({
391
+ const res3 = await client.queryIo(natsIO, 'customerSupport', 'followupAgent', new Message({
392
392
  content: `Please follow up on case ${caseId}`,
393
393
  session: {
394
394
  id: caseId
@@ -479,7 +479,7 @@ async function main() {
479
479
  const createRequest = "I need to schedule a design review meeting next Monday at 2pm for 1 hour with the design team. Today is 11th May 2025.";
480
480
 
481
481
  console.log(`User request: "${createRequest}"`);
482
- const createResponse = await client.queryIo(natsIO, 'eventPlanner.plannerAgent', new Message({
482
+ const createResponse = await client.queryIo(natsIO, 'eventPlanner', 'plannerAgent', new Message({
483
483
  content: createRequest,
484
484
  session: {
485
485
  id: uuidv4(),
@@ -495,7 +495,7 @@ async function main() {
495
495
  const searchRequest = "Show me all meetings with Bob next month. Today is october 2023.";
496
496
 
497
497
  console.log(`User request: "${searchRequest}"`);
498
- const searchResponse = await client.queryIo(natsIO, 'eventPlanner.eventFinderAgent', new Message({
498
+ const searchResponse = await client.queryIo(natsIO, 'eventPlanner', 'eventFinderAgent', new Message({
499
499
  content: searchRequest,
500
500
  session: {
501
501
  id: uuidv4(),
@@ -511,7 +511,7 @@ async function main() {
511
511
  const updateRequest = "Change the Weekly Team Meeting to start at 9:30am instead of 10am.";
512
512
 
513
513
  console.log(`User request: "${updateRequest}"`);
514
- const updateResponse = await client.queryIo(natsIO, 'eventPlanner.plannerAgent', new Message({
514
+ const updateResponse = await client.queryIo(natsIO, 'eventPlanner', 'plannerAgent', new Message({
515
515
  content: updateRequest,
516
516
  session: {
517
517
  id: uuidv4(),
@@ -527,7 +527,7 @@ async function main() {
527
527
  const conflictRequest = "Do I have any scheduling conflicts next week?";
528
528
 
529
529
  console.log(`User request: "${conflictRequest}"`);
530
- const conflictResponse = await client.queryIo(natsIO, 'eventPlanner.eventFinderAgent', new Message({
530
+ const conflictResponse = await client.queryIo(natsIO, 'eventPlanner', 'eventFinderAgent', new Message({
531
531
  content: conflictRequest,
532
532
  session: {
533
533
  id: uuidv4(),
@@ -62,7 +62,7 @@ const message = new Message({
62
62
  id: sessionId
63
63
  }
64
64
  })
65
- const res = await agentClient.queryIo(io, 'smartexample.entrypoint', message)
65
+ const res = await agentClient.queryIo(io, 'smartexample', 'entrypoint', message)
66
66
  console.log("=======\n", res.getContent())
67
67
  console.log("=======\n", res.getSession())
68
68
 
@@ -72,6 +72,6 @@ const message2 = new Message({
72
72
  id: sessionId
73
73
  }
74
74
  })
75
- const res2 = await agentClient.queryIo(io, 'smartexample.entrypoint', message2)
75
+ const res2 = await agentClient.queryIo(io, 'smartexample', 'entrypoint', message2)
76
76
  console.log("=======\n", res2.getContent())
77
77
  console.log("=======\n", res2.getSession())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentnet",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "Agent library used by Smartness",
6
6
  "main": "src/index.js",
@@ -1,13 +1,17 @@
1
1
  import { Message, Response } from '../index.js'
2
- export function AgentClient() {
2
+
3
+ export function AgentClient(config = {}) {
4
+ const requestTimeout = config.requestTimeout || 120000
5
+
3
6
  return {
4
7
  queryAgent: async (agent, input) => {
5
8
  return await agent.query(input)
6
9
  },
7
10
 
8
- queryIo: async (io, target, message) => {
11
+ queryIo: async (io, namespace, name, message) => {
9
12
  const transport = await io.connect()
10
- const response = await transport.request(target, message.serialize(), { timeout: 60000 })
13
+ const target = namespace + '.' + name
14
+ const response = await transport.request(target, message.serialize(), { timeout: requestTimeout })
11
15
  return new Response(JSON.parse(response.string()))
12
16
  }
13
17
  }
@@ -7,8 +7,8 @@ import {
7
7
  withRetry
8
8
  } from '../errors/index.js';
9
9
 
10
- const DEFAULT_TOOL_TIMEOUT = 120000;
11
- const DEFAULT_LLM_TIMEOUT = 120000;
10
+ const DEFAULT_TOOL_TIMEOUT = process.env.AGENT_DEFAULT_TOOL_TIMEOUT || 120000;
11
+ const DEFAULT_LLM_TIMEOUT = process.env.AGENT_DEFAULT_LLM_TIMEOUT || 120000;
12
12
 
13
13
  /**
14
14
  * Emits an event to the hooks system if hooks are available
package/src/index.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  memoryStore,
10
10
  session
11
11
  } from "./store/store.js";
12
+ import { Conversation } from "./utils/conversation.js";
12
13
 
13
14
  export const AgentLoaderFile = _AgentLoader.AgentLoaderFile
14
15
  export const AgentLoaderJSON = _AgentLoader.AgentLoaderJSON
@@ -112,4 +113,6 @@ export class Response {
112
113
  this.#content = parsed.content
113
114
  this.#session = parsed.session
114
115
  }
115
- }
116
+ }
117
+
118
+ export { Conversation };
package/src/llm/base.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { logger } from '../utils/logger.js'
2
2
  import { LLMError } from '../errors/index.js'
3
+ import { Conversation } from '../utils/conversation.js'
3
4
 
4
5
  /**
5
6
  * Base class for LLM implementations
@@ -47,7 +48,7 @@ export class BaseLLM {
47
48
 
48
49
  /**
49
50
  * Add a user prompt to the conversation
50
- * @param {Array} conversation - The conversation history
51
+ * @param {Array|Conversation} conversation - The conversation history
51
52
  * @param {string} formattedPrompt - The formatted user prompt
52
53
  * @returns {Promise<void>}
53
54
  */
@@ -56,6 +57,16 @@ export class BaseLLM {
56
57
  promptPreview: formattedPrompt.substring(0, 100)
57
58
  });
58
59
 
60
+ // Handle both raw array and Conversation object
61
+ if (conversation instanceof Conversation) {
62
+ // Subclasses should override this to add their specific format
63
+ // But we'll add a generic format by default
64
+ conversation.addUserMessage({
65
+ role: 'user',
66
+ content: formattedPrompt
67
+ });
68
+ }
69
+ // For array, we rely on subclass implementations
59
70
  // Subclasses must implement appropriate conversation format
60
71
  }
61
72
 
@@ -83,10 +94,7 @@ export class BaseLLM {
83
94
  * @returns {Promise<any>} Result of the tool execution
84
95
  */
85
96
  async executeToolCall(toolCall, name, args, state, toolsAndHandoffsMap) {
86
- logger.info(`Executing tool from ${this.type}`, {
87
- toolName: name,
88
- argsPreview: JSON.stringify(args).substring(0, 100)
89
- });
97
+ logger.info(`Executing tool from ${this.type} - Tool: ${name} - Args: ${JSON.stringify(args).substring(0, 100)}`);
90
98
 
91
99
  try {
92
100
  if (!toolsAndHandoffsMap[name] || !toolsAndHandoffsMap[name].function) {
@@ -104,7 +112,6 @@ export class BaseLLM {
104
112
 
105
113
  logger.debug('Tool execution successful', { toolName: name });
106
114
  return result;
107
-
108
115
  } catch (error) {
109
116
  logger.error(`Error executing tool "${name}"`, error.message);
110
117
  throw error;
package/src/llm/gemini.js CHANGED
@@ -2,6 +2,7 @@ import { GoogleGenAI } from '@google/genai'
2
2
  import { logger } from '../utils/logger.js'
3
3
  import { LLMError } from '../errors/index.js'
4
4
  import { BaseLLM } from './base.js'
5
+ import { Conversation } from '../utils/conversation.js'
5
6
 
6
7
  /**
7
8
  * Gemini LLM implementation
@@ -41,7 +42,13 @@ class GeminiLLM extends BaseLLM {
41
42
  */
42
43
  async callModel(llmClientConfig, context) {
43
44
  const { client, toolsAndHandoffsMap, conversation } = context;
44
- const input = { ...llmClientConfig, contents: conversation };
45
+
46
+ // Get raw conversation if it's a Conversation object
47
+ const conversationArray = conversation instanceof Conversation
48
+ ? conversation.getRawConversation()
49
+ : conversation;
50
+
51
+ const input = { ...llmClientConfig, contents: conversationArray };
45
52
 
46
53
  // Configure tools if provided
47
54
  if (input.config !== undefined && input.tools !== undefined) {
@@ -53,7 +60,7 @@ class GeminiLLM extends BaseLLM {
53
60
 
54
61
  logger.debug('Calling Gemini model', {
55
62
  model: input.model,
56
- conversationLength: conversation.length,
63
+ conversationLength: conversationArray.length,
57
64
  toolsCount: toolsAndHandoffsMap.tools.length
58
65
  });
59
66
 
@@ -86,7 +93,7 @@ class GeminiLLM extends BaseLLM {
86
93
  * Handle a specific tool call from Gemini response
87
94
  * @param {Object} toolCall - The tool call to process
88
95
  * @param {Object} state - Current application state
89
- * @param {Array} conversation - The conversation history
96
+ * @param {Array|Conversation} conversation - The conversation history
90
97
  * @param {Object} toolsAndHandoffsMap - Map of available tools
91
98
  */
92
99
  async handleToolCall(toolCall, state, conversation, toolsAndHandoffsMap) {
@@ -102,8 +109,16 @@ class GeminiLLM extends BaseLLM {
102
109
  response: typeof result === 'string' ? { answer: result } : result
103
110
  };
104
111
 
105
- conversation.push({ role: 'model', parts: [{ functionCall: toolCall }] });
106
- conversation.push({ role: 'user', parts: [{ functionResponse: function_response_part }] });
112
+ const functionCallMessage = { role: 'model', parts: [{ functionCall: toolCall }] };
113
+ const functionResponseMessage = { role: 'user', parts: [{ functionResponse: function_response_part }] };
114
+
115
+ if (conversation instanceof Conversation) {
116
+ conversation.addFunctionCall(functionCallMessage);
117
+ conversation.addFunctionResult(functionResponseMessage);
118
+ } else {
119
+ conversation.push(functionCallMessage);
120
+ conversation.push(functionResponseMessage);
121
+ }
107
122
 
108
123
  } catch (error) {
109
124
  // Return error as function response in Gemini-specific format
@@ -112,15 +127,23 @@ class GeminiLLM extends BaseLLM {
112
127
  response: { error: error.message }
113
128
  };
114
129
 
115
- conversation.push({ role: 'model', parts: [{ functionCall: toolCall }] });
116
- conversation.push({ role: 'user', parts: [{ functionResponse: errorResponse }] });
130
+ const functionCallMessage = { role: 'model', parts: [{ functionCall: toolCall }] };
131
+ const functionResponseMessage = { role: 'user', parts: [{ functionResponse: errorResponse }] };
132
+
133
+ if (conversation instanceof Conversation) {
134
+ conversation.addFunctionCall(functionCallMessage);
135
+ conversation.addFunctionResult(functionResponseMessage);
136
+ } else {
137
+ conversation.push(functionCallMessage);
138
+ conversation.push(functionResponseMessage);
139
+ }
117
140
  }
118
141
  }
119
142
 
120
143
  /**
121
144
  * Processes the model response, handling text responses and function calls
122
145
  * @param {Object} state - Current application state
123
- * @param {Array} conversation - The conversation history
146
+ * @param {Array|Conversation} conversation - The conversation history
124
147
  * @param {Object} toolsAndHandoffsMap - Map of available tools
125
148
  * @param {Object} response - The model response to process
126
149
  * @returns {Promise<string|null>} Text response or null if processing tool calls
@@ -129,6 +152,15 @@ class GeminiLLM extends BaseLLM {
129
152
  // Handle simple text response
130
153
  if (response.text !== undefined) {
131
154
  logger.debug('Gemini response contains text, returning directly');
155
+
156
+ if (conversation instanceof Conversation) {
157
+ const modelResponse = {
158
+ role: 'model',
159
+ parts: [{ text: response.text }]
160
+ };
161
+ conversation.addModelResponse(modelResponse);
162
+ }
163
+
132
164
  return response.text;
133
165
  }
134
166
 
@@ -149,17 +181,23 @@ class GeminiLLM extends BaseLLM {
149
181
 
150
182
  /**
151
183
  * Adds a user prompt to the conversation
152
- * @param {Array} conversation - The conversation history
184
+ * @param {Array|Conversation} conversation - The conversation history
153
185
  * @param {string} formattedPrompt - The formatted user prompt
154
186
  * @returns {Promise<void>}
155
187
  */
156
188
  async prompt(conversation, formattedPrompt) {
157
189
  await super.prompt(conversation, formattedPrompt);
158
190
 
159
- conversation.push({
191
+ const userMessage = {
160
192
  role: 'user',
161
193
  parts: [{ text: formattedPrompt }]
162
- });
194
+ };
195
+
196
+ if (conversation instanceof Conversation) {
197
+ conversation.addUserMessage(userMessage);
198
+ } else {
199
+ conversation.push(userMessage);
200
+ }
163
201
  }
164
202
  }
165
203
 
package/src/llm/gpt.js CHANGED
@@ -2,6 +2,7 @@ import OpenAI from 'openai'
2
2
  import { logger } from '../utils/logger.js'
3
3
  import { LLMError } from '../errors/index.js'
4
4
  import { BaseLLM } from './base.js'
5
+ import { Conversation } from '../utils/conversation.js'
5
6
 
6
7
  /**
7
8
  * OpenAI LLM implementation
@@ -43,11 +44,15 @@ class OpenAILLM extends BaseLLM {
43
44
  const { client, toolsAndHandoffsMap, conversation } = context;
44
45
  const input = { ...llmClientConfig };
45
46
  input.tools = toolsAndHandoffsMap.tools;
46
- input.input = conversation;
47
+
48
+ // Get raw conversation if it's a Conversation object
49
+ input.input = conversation instanceof Conversation
50
+ ? conversation.getRawConversation()
51
+ : conversation;
47
52
 
48
53
  logger.debug('Calling OpenAI model', {
49
54
  model: input.model,
50
- conversationLength: conversation.length,
55
+ conversationLength: input.input.length,
51
56
  toolsCount: toolsAndHandoffsMap.tools.length
52
57
  });
53
58
  //console.log(JSON.stringify(input, null, 2))
@@ -76,7 +81,7 @@ class OpenAILLM extends BaseLLM {
76
81
  * Handle a specific tool call from OpenAI response
77
82
  * @param {Object} toolCall - The tool call to process
78
83
  * @param {Object} state - Current application state
79
- * @param {Array} conversation - The conversation history
84
+ * @param {Array|Conversation} conversation - The conversation history
80
85
  * @param {Object} toolsAndHandoffsMap - Map of available tools
81
86
  */
82
87
  async handleToolCall(toolCall, state, conversation, toolsAndHandoffsMap) {
@@ -91,7 +96,13 @@ class OpenAILLM extends BaseLLM {
91
96
  });
92
97
 
93
98
  const result = await super.executeToolCall(toolCall, name, args, state, toolsAndHandoffsMap);
94
- conversation.push(toolCall);
99
+
100
+ // Add function call to conversation
101
+ if (conversation instanceof Conversation) {
102
+ conversation.addFunctionCall(toolCall);
103
+ } else {
104
+ conversation.push(toolCall);
105
+ }
95
106
 
96
107
  const resultString = typeof result === 'string' ? result : JSON.stringify(result);
97
108
 
@@ -100,28 +111,47 @@ class OpenAILLM extends BaseLLM {
100
111
  resultPreview: resultString.substring(0, 100)
101
112
  });
102
113
 
103
- conversation.push({
114
+ const functionOutput = {
104
115
  type: "function_call_output",
105
116
  call_id: toolCall.call_id,
106
117
  output: resultString
107
- });
118
+ };
119
+
120
+ // Add function result to conversation
121
+ if (conversation instanceof Conversation) {
122
+ conversation.addFunctionResult(functionOutput);
123
+ } else {
124
+ conversation.push(functionOutput);
125
+ }
108
126
  } catch (error) {
109
127
  logger.error(`Error executing tool "${toolCall.name}"`, { error });
110
128
 
111
129
  // Add error as function output
112
- conversation.push(toolCall);
113
- conversation.push({
114
- type: "function_call_output",
115
- call_id: toolCall.call_id,
116
- output: JSON.stringify({ error: error.message })
117
- });
130
+ if (conversation instanceof Conversation) {
131
+ conversation.addFunctionCall(toolCall);
132
+
133
+ const errorOutput = {
134
+ type: "function_call_output",
135
+ call_id: toolCall.call_id,
136
+ output: JSON.stringify({ error: error.message })
137
+ };
138
+
139
+ conversation.addFunctionResult(errorOutput);
140
+ } else {
141
+ conversation.push(toolCall);
142
+ conversation.push({
143
+ type: "function_call_output",
144
+ call_id: toolCall.call_id,
145
+ output: JSON.stringify({ error: error.message })
146
+ });
147
+ }
118
148
  }
119
149
  }
120
150
 
121
151
  /**
122
152
  * Processes the model response, handling text responses and function calls
123
153
  * @param {Object} state - Current application state
124
- * @param {Array} conversation - The conversation history
154
+ * @param {Array|Conversation} conversation - The conversation history
125
155
  * @param {Object} toolsAndHandoffsMap - Map of available tools
126
156
  * @param {Object} response - The model response to process
127
157
  * @returns {Promise<string|null>} Text response or null if processing tool calls
@@ -129,6 +159,15 @@ class OpenAILLM extends BaseLLM {
129
159
  async onResponse(state, conversation, toolsAndHandoffsMap, response) {
130
160
  if (response.output_text !== undefined && response.output_text.length > 0) {
131
161
  logger.debug('OpenAI response contains text, returning directly');
162
+
163
+ // Add model response to conversation if using Conversation object
164
+ if (conversation instanceof Conversation) {
165
+ conversation.addModelResponse({
166
+ role: 'assistant',
167
+ content: response.output_text
168
+ });
169
+ }
170
+
132
171
  return response.output_text;
133
172
  }
134
173
 
@@ -142,7 +181,11 @@ class OpenAILLM extends BaseLLM {
142
181
 
143
182
  // Add reasoning to conversation
144
183
  for (const res of reasoning) {
145
- conversation.push(res);
184
+ if (conversation instanceof Conversation) {
185
+ conversation.addModelResponse(res);
186
+ } else {
187
+ conversation.push(res);
188
+ }
146
189
  }
147
190
 
148
191
  // Process all tool calls sequentially
@@ -155,17 +198,23 @@ class OpenAILLM extends BaseLLM {
155
198
 
156
199
  /**
157
200
  * Adds a user prompt to the conversation
158
- * @param {Array} conversation - The conversation history
201
+ * @param {Array|Conversation} conversation - The conversation history
159
202
  * @param {string} formattedPrompt - The formatted user prompt
160
203
  * @returns {Promise<void>}
161
204
  */
162
205
  async prompt(conversation, formattedPrompt) {
163
206
  await super.prompt(conversation, formattedPrompt);
164
207
 
165
- conversation.push({
208
+ const userMessage = {
166
209
  role: 'user',
167
210
  content: formattedPrompt
168
- });
211
+ };
212
+
213
+ if (conversation instanceof Conversation) {
214
+ conversation.addUserMessage(userMessage);
215
+ } else {
216
+ conversation.push(userMessage);
217
+ }
169
218
  }
170
219
  }
171
220
 
@@ -1,14 +1,15 @@
1
1
  import { v4 as uuid } from 'uuid'
2
2
  import { createClient } from 'redis'
3
3
  import pgp from 'pg-promise'
4
+ import { Conversation } from '../utils/conversation.js'
4
5
 
5
6
  export function session (id) {
6
7
  let state = {}
7
- let conversation = []
8
+ let conversationManager = new Conversation()
8
9
 
9
10
  return {
10
11
  query: async function (agentInstance, input) {
11
- return await agentInstance.query(state, conversation, input)
12
+ return await agentInstance.query(state, conversationManager.getRawConversation(), input)
12
13
  },
13
14
  setState: function (_state) {
14
15
  state = _state
@@ -22,33 +23,39 @@ export function session (id) {
22
23
  return state
23
24
  },
24
25
  trimConversation: function (elementsToKeep) {
25
- conversation = conversation.slice(-elementsToKeep)
26
- let additionalElementsToRemove = 0
27
- for (const chatIndex in conversation) {
28
- if (conversation[chatIndex].role !== 'user' || conversation[chatIndex].role == undefined || (conversation[chatIndex].role === 'user' && conversation[chatIndex]?.parts?.[0]?.functionResponse != null)) {
29
- additionalElementsToRemove += 1
30
- } else {
31
- break
32
- }
33
- }
34
- if (additionalElementsToRemove > 0) {
35
- conversation = conversation.slice(additionalElementsToRemove)
36
- }
26
+ conversationManager.trim(elementsToKeep)
37
27
  },
38
28
  setConversation: function (_conversation) {
39
- conversation = _conversation
29
+ // Support both array and Conversation objects
30
+ if (_conversation instanceof Conversation) {
31
+ conversationManager = _conversation;
32
+ } else if (Array.isArray(_conversation)) {
33
+ conversationManager.importFromArray(_conversation);
34
+ }
40
35
  },
41
36
  getConversation: function () {
42
- return conversation
37
+ // Return raw conversation for backward compatibility
38
+ return conversationManager.getRawConversation()
39
+ },
40
+ getConversationManager: function() {
41
+ return conversationManager
43
42
  },
44
43
  load: async function (stateStore) {
45
44
  const _state = await stateStore.get(id)
46
45
  if (_state !== null) {
47
46
  const parsedState = JSON.parse(_state)
48
- conversation = parsedState.conversation || []
47
+
48
+ // Handle both old-style conversation array and new-style serialized Conversation
49
+ if (parsedState.conversationData) {
50
+ conversationManager.deserialize(parsedState.conversationData)
51
+ } else if (Array.isArray(parsedState.conversation)) {
52
+ conversationManager.importFromArray(parsedState.conversation)
53
+ }
54
+
49
55
  state = parsedState.state || {}
50
56
  return {
51
- conversation: conversation,
57
+ // Return raw conversation for backward compatibility
58
+ conversation: conversationManager.getRawConversation(),
52
59
  state: state
53
60
  }
54
61
  }
@@ -59,8 +66,11 @@ export function session (id) {
59
66
  },
60
67
  dump: async function (stateStore) {
61
68
  return await stateStore.set(id, JSON.stringify({
62
- conversation: conversation,
63
- state: state
69
+ // Keep conversation array for backward compatibility
70
+ conversation: conversationManager.getRawConversation(),
71
+ // Add serialized conversation data with metadata
72
+ conversationData: conversationManager.serialize(),
73
+ state: state
64
74
  }))
65
75
  },
66
76
  }
@@ -13,8 +13,8 @@ import {
13
13
  } from '../errors/index.js';
14
14
 
15
15
  // Constants
16
- const HEARTBEAT_INTERVAL = 1000;
17
- const TIMEOUT_TASK_REQUEST = 120000;
16
+ const HEARTBEAT_INTERVAL = process.env.AGENTNET_NATS_HEARTBEAT_INTERVAL || 1000;
17
+ const TIMEOUT_TASK_REQUEST = process.env.AGENTNET_NATS_TIMEOUT_TASK_REQUEST || 120000;
18
18
 
19
19
  /**
20
20
  * NATS implementation of the Transport interface
@@ -0,0 +1,178 @@
1
+ import { logger } from './logger.js';
2
+
3
+ /**
4
+ * Class representing a conversation with additional metadata for each message
5
+ */
6
+ export class Conversation {
7
+ /**
8
+ * Create a new Conversation instance
9
+ */
10
+ constructor() {
11
+ // The internal conversation array with metadata
12
+ this.messages = [];
13
+ }
14
+
15
+ /**
16
+ * Add a message to the conversation
17
+ * @param {Object} message - The message to add
18
+ * @param {Object} metadata - Additional metadata
19
+ * @param {string} metadata.type - Message type ('user_input', 'model_response', 'function_call', 'function_result')
20
+ */
21
+ addMessage(message, metadata = {}) {
22
+ this.messages.push({
23
+ content: message,
24
+ metadata: {
25
+ ...metadata,
26
+ timestamp: new Date().toISOString()
27
+ }
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Add a user message to the conversation
33
+ * @param {Object} message - The user message
34
+ */
35
+ addUserMessage(message) {
36
+ this.addMessage(message, { type: 'user_input' });
37
+ }
38
+
39
+ /**
40
+ * Add a model response to the conversation
41
+ * @param {Object} message - The model response
42
+ */
43
+ addModelResponse(message) {
44
+ this.addMessage(message, { type: 'model_response' });
45
+ }
46
+
47
+ /**
48
+ * Add a function call to the conversation
49
+ * @param {Object} message - The function call
50
+ */
51
+ addFunctionCall(message) {
52
+ this.addMessage(message, { type: 'function_call' });
53
+ }
54
+
55
+ /**
56
+ * Add a function result to the conversation
57
+ * @param {Object} message - The function result
58
+ */
59
+ addFunctionResult(message) {
60
+ this.addMessage(message, { type: 'function_result' });
61
+ }
62
+
63
+ /**
64
+ * Get the conversation messages
65
+ * @returns {Array} The messages in the conversation
66
+ */
67
+ getMessages() {
68
+ return this.messages;
69
+ }
70
+
71
+ /**
72
+ * Get the raw conversation history compatible with LLM providers
73
+ * This strips out the metadata and returns just the message content
74
+ * @returns {Array} The raw conversation history
75
+ */
76
+ getRawConversation() {
77
+ return this.messages.map(message => message.content);
78
+ }
79
+
80
+ /**
81
+ * Set the conversation from an existing array
82
+ * @param {Array} conversation - Existing conversation array
83
+ * @param {Object} options - Import options
84
+ * @param {boolean} options.detectTypes - Try to automatically detect message types
85
+ */
86
+ importFromArray(conversation, options = { detectTypes: true }) {
87
+ this.messages = [];
88
+
89
+ if (!Array.isArray(conversation)) {
90
+ logger.warn('Attempted to import non-array conversation');
91
+ return;
92
+ }
93
+
94
+ for (const message of conversation) {
95
+ let type = 'unknown';
96
+
97
+ // Try to automatically detect message types if enabled
98
+ if (options.detectTypes) {
99
+ if (message.role === 'user') {
100
+ // Check if it's a function response (from function result)
101
+ if (message.parts && message.parts[0] && message.parts[0].functionResponse) {
102
+ type = 'function_result';
103
+ } else {
104
+ type = 'user_input';
105
+ }
106
+ } else if (message.role === 'model' || message.role === 'assistant') {
107
+ // Check if it's a function call
108
+ if ((message.parts && message.parts[0] && message.parts[0].functionCall) ||
109
+ message.type === 'function_call') {
110
+ type = 'function_call';
111
+ } else {
112
+ type = 'model_response';
113
+ }
114
+ } else if (message.type === 'function_call') {
115
+ type = 'function_call';
116
+ } else if (message.type === 'function_call_output') {
117
+ type = 'function_result';
118
+ }
119
+ }
120
+
121
+ this.addMessage(message, { type });
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Trim the conversation to a maximum number of elements
127
+ * This will keep at least one user message at the start
128
+ * @param {number} maxElements - Maximum number of elements to keep
129
+ */
130
+ trim(maxElements) {
131
+ if (this.messages.length <= maxElements) {
132
+ return;
133
+ }
134
+
135
+ // First, keep only the latest maxElements
136
+ this.messages = this.messages.slice(-maxElements);
137
+
138
+ // Then, ensure we have a user message at the start
139
+ // Find the first user message
140
+ let firstUserMessageIndex = -1;
141
+ for (let i = 0; i < this.messages.length; i++) {
142
+ if (this.messages[i].metadata.type === 'user_input') {
143
+ firstUserMessageIndex = i;
144
+ break;
145
+ }
146
+ }
147
+
148
+ // If we didn't find a user message, or it's already at the start, nothing to do
149
+ if (firstUserMessageIndex === -1 || firstUserMessageIndex === 0) {
150
+ return;
151
+ }
152
+
153
+ // Otherwise, trim all messages before the first user message
154
+ this.messages = this.messages.slice(firstUserMessageIndex);
155
+ }
156
+
157
+ /**
158
+ * Serialize the conversation to an object
159
+ * @returns {Object} The serialized conversation
160
+ */
161
+ serialize() {
162
+ return {
163
+ messages: this.messages
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Deserialize the conversation from an object
169
+ * @param {Object} data - The serialized conversation
170
+ * @returns {Conversation} The deserialized conversation
171
+ */
172
+ deserialize(data) {
173
+ if (data && data.messages) {
174
+ this.messages = data.messages;
175
+ }
176
+ return this;
177
+ }
178
+ }