@ziggs-ai/api-client 0.1.3 → 0.1.4
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 +13 -7
- package/dist/ConnectionManager.d.ts +45 -0
- package/dist/ConnectionManager.js +118 -0
- package/dist/http/AgentSearchClient.d.ts +36 -0
- package/dist/http/AgentSearchClient.js +72 -0
- package/dist/http/AgreementClient.d.ts +153 -0
- package/dist/http/AgreementClient.js +457 -0
- package/dist/http/ArtifactsClient.d.ts +48 -0
- package/dist/http/ArtifactsClient.js +90 -0
- package/dist/http/ChatClient.d.ts +14 -0
- package/dist/http/ChatClient.js +69 -0
- package/dist/http/MarketplaceClient.d.ts +19 -0
- package/dist/http/MarketplaceClient.js +72 -0
- package/dist/http/MessagesClient.d.ts +22 -0
- package/dist/http/MessagesClient.js +41 -0
- package/dist/http/ScopeClient.d.ts +33 -0
- package/dist/http/ScopeClient.js +39 -0
- package/dist/http/TaskClient.d.ts +75 -0
- package/dist/http/TaskClient.js +343 -0
- package/dist/http/TelemetryClient.d.ts +11 -0
- package/dist/http/TelemetryClient.js +53 -0
- package/dist/http/index.d.ts +12 -0
- package/dist/http/index.js +9 -0
- package/dist/index.d.ts +9 -0
- package/{src → dist}/index.js +2 -12
- package/dist/shared/runtimeLog.d.ts +14 -0
- package/dist/shared/runtimeLog.js +64 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.js +50 -0
- package/dist/utils/urlUtils.d.ts +2 -0
- package/dist/utils/urlUtils.js +8 -0
- package/dist/websocket/ControlSocket.d.ts +13 -0
- package/dist/websocket/ControlSocket.js +36 -0
- package/dist/websocket/WebSocketClient.d.ts +71 -0
- package/dist/websocket/WebSocketClient.js +217 -0
- package/dist/websocket/index.js +1 -0
- package/package.json +13 -6
- package/src/ConnectionManager.js +0 -179
- package/src/http/AgentSearchClient.js +0 -113
- package/src/http/ContextReader.js +0 -99
- package/src/http/ContextWriter.js +0 -98
- package/src/http/TaskClient.js +0 -612
- package/src/http/TelemetryClient.js +0 -43
- package/src/http/index.js +0 -6
- package/src/types.js +0 -28
- package/src/utils/urlUtils.js +0 -17
- package/src/websocket/ControlSocket.js +0 -55
- package/src/websocket/WebSocketClient.js +0 -318
- /package/{src/websocket/index.js → dist/websocket/index.d.ts} +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { io } from 'socket.io-client';
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import { getWebSocketUrl } from '../utils/urlUtils.js';
|
|
4
|
+
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
5
|
+
export class WebSocketClient {
|
|
6
|
+
wsUrl;
|
|
7
|
+
operatorKey;
|
|
8
|
+
agentId;
|
|
9
|
+
userToken;
|
|
10
|
+
label;
|
|
11
|
+
socket;
|
|
12
|
+
messageHandler;
|
|
13
|
+
resourceEventHandler;
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.wsUrl = options.wsUrl || getWebSocketUrl();
|
|
16
|
+
this.userToken = options.userToken || null;
|
|
17
|
+
this.operatorKey = this.userToken ? null : (options.operatorKey || process.env.ZIGGS_OPERATOR_KEY || null);
|
|
18
|
+
this.agentId = this.userToken ? null : (options.agentId || null);
|
|
19
|
+
this.label = options.label || 'agent';
|
|
20
|
+
this.socket = null;
|
|
21
|
+
this.messageHandler = null;
|
|
22
|
+
this.resourceEventHandler = null;
|
|
23
|
+
}
|
|
24
|
+
setMessageHandler(handler) {
|
|
25
|
+
this.messageHandler = handler;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Subscribe to `resource_changed` notifications — the unified push channel
|
|
29
|
+
* for non-message resource changes (artifact, task-state, agreement). The
|
|
30
|
+
* agent decides whether to pull the corresponding primitive based on
|
|
31
|
+
* `event.kind` + ids. Replaces cursor-based polling for delta detection.
|
|
32
|
+
*/
|
|
33
|
+
setResourceEventHandler(handler) {
|
|
34
|
+
this.resourceEventHandler = handler;
|
|
35
|
+
}
|
|
36
|
+
_connect() {
|
|
37
|
+
if (this.socket?.connected)
|
|
38
|
+
return;
|
|
39
|
+
runtimeLog.debug(this.label, `Connecting to ${this.wsUrl}`);
|
|
40
|
+
const socketOptions = this.buildSocketOptions();
|
|
41
|
+
this.socket = io(this.wsUrl, socketOptions);
|
|
42
|
+
this.socket.on('connect', () => {
|
|
43
|
+
runtimeLog.info(this.label, 'Connected');
|
|
44
|
+
});
|
|
45
|
+
this.socket.on('connect_error', (error) => {
|
|
46
|
+
runtimeLog.error(this.label, `Connection error: ${error.message}`);
|
|
47
|
+
});
|
|
48
|
+
this.socket.on('disconnect', (reason) => {
|
|
49
|
+
const reasonDescriptions = {
|
|
50
|
+
'io server disconnect': 'Server forcefully disconnected the client',
|
|
51
|
+
'io client disconnect': 'Client manually disconnected',
|
|
52
|
+
'ping timeout': 'Server did not respond to ping (connection timeout)',
|
|
53
|
+
'transport close': 'Connection closed by transport layer',
|
|
54
|
+
'transport error': 'Transport error occurred',
|
|
55
|
+
'parse error': 'Error parsing server message',
|
|
56
|
+
'forced close': 'Connection was forcibly closed',
|
|
57
|
+
'forced server close': 'Server forcibly closed the connection',
|
|
58
|
+
};
|
|
59
|
+
const description = reasonDescriptions[reason] || 'Unknown disconnect reason';
|
|
60
|
+
runtimeLog.debug(this.label, `Disconnected: ${reason} — ${description}`);
|
|
61
|
+
});
|
|
62
|
+
this.socket.on('messages', async (payload) => {
|
|
63
|
+
try {
|
|
64
|
+
await this.handleIncomingMessage(payload);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
68
|
+
runtimeLog.error(this.label, `Error handling incoming message: ${msg}`);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
this.socket.on('resource_changed', async (payload) => {
|
|
73
|
+
if (!this.resourceEventHandler)
|
|
74
|
+
return;
|
|
75
|
+
try {
|
|
76
|
+
await this.resourceEventHandler(payload);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
80
|
+
runtimeLog.error(this.label, `Error handling resource_changed: ${msg}`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
this.socket.on('error', (error) => {
|
|
84
|
+
runtimeLog.error(this.label, `Socket.IO error: ${String(error)}`);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
connectAsync(timeout = 10_000) {
|
|
88
|
+
if (this.socket?.connected)
|
|
89
|
+
return Promise.resolve(this);
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
this._connect();
|
|
92
|
+
const timer = setTimeout(() => reject(new Error(`[${this.label}] Connection timeout after ${timeout}ms`)), timeout);
|
|
93
|
+
this.socket.once('connect', () => { clearTimeout(timer); resolve(this); });
|
|
94
|
+
this.socket.once('connect_error', (err) => { clearTimeout(timer); reject(err); });
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
disconnect() {
|
|
98
|
+
if (this.socket) {
|
|
99
|
+
this.socket.disconnect();
|
|
100
|
+
this.socket = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
send(chatId, receiverId, content, options = {}) {
|
|
104
|
+
if (!chatId || typeof chatId !== 'string')
|
|
105
|
+
throw new Error('send: chatId is required');
|
|
106
|
+
if (!receiverId || typeof receiverId !== 'string')
|
|
107
|
+
throw new Error('send: receiverId is required');
|
|
108
|
+
if (typeof content !== 'string' || content.trim().length === 0)
|
|
109
|
+
throw new Error('send: content must be a non-empty string');
|
|
110
|
+
if (!this.socket)
|
|
111
|
+
throw new Error('send: socket is not initialized. Call connect() first.');
|
|
112
|
+
if (!this.socket.connected)
|
|
113
|
+
throw new Error('send: socket is not connected. Call connect() and wait for connection.');
|
|
114
|
+
const messageId = this.generateMessageId();
|
|
115
|
+
if (!messageId || typeof messageId !== 'string')
|
|
116
|
+
throw new Error('send: failed to generate valid messageId');
|
|
117
|
+
const message = {
|
|
118
|
+
receiverId,
|
|
119
|
+
chatId,
|
|
120
|
+
messageId,
|
|
121
|
+
text: content,
|
|
122
|
+
entryType: options.entryType || 'message',
|
|
123
|
+
content_type: options.content_type || options.contentType || 'text',
|
|
124
|
+
};
|
|
125
|
+
const messagePreview = content.length > 100 ? content.substring(0, 100) + '...' : content;
|
|
126
|
+
const event = options.partial ? 'chat:chunk' : 'chat';
|
|
127
|
+
runtimeLog.debug(this.label, `Sending message (${event}) - chatId: ${chatId}, receiverId: ${receiverId}\n Message: ${messagePreview}`);
|
|
128
|
+
this.socket.emit(event, message);
|
|
129
|
+
}
|
|
130
|
+
isConnected() {
|
|
131
|
+
return this.socket?.connected || false;
|
|
132
|
+
}
|
|
133
|
+
async handleIncomingMessage(payload) {
|
|
134
|
+
if (!payload || typeof payload !== 'object') {
|
|
135
|
+
throw new Error('handleIncomingMessage: payload must be an object');
|
|
136
|
+
}
|
|
137
|
+
const p = payload;
|
|
138
|
+
if (!p['text'] || typeof p['text'] !== 'string' || p['text'].trim().length === 0) {
|
|
139
|
+
throw new Error('handleIncomingMessage: payload.text is required and must be a non-empty string');
|
|
140
|
+
}
|
|
141
|
+
if (!p['chatId'] || typeof p['chatId'] !== 'string') {
|
|
142
|
+
throw new Error('handleIncomingMessage: payload.chatId is required and must be a string');
|
|
143
|
+
}
|
|
144
|
+
const sender = p['sender'];
|
|
145
|
+
const senderId = sender?.['id'];
|
|
146
|
+
const senderType = sender?.['type'];
|
|
147
|
+
if (!senderId || typeof senderId !== 'string') {
|
|
148
|
+
throw new Error('handleIncomingMessage: payload.sender.id is required and must be a string');
|
|
149
|
+
}
|
|
150
|
+
runtimeLog.debug(this.label, `Received message - chatId: ${p['chatId']}, sender: ${senderId} (${senderType})`);
|
|
151
|
+
if (this.agentId && senderId === this.agentId)
|
|
152
|
+
return;
|
|
153
|
+
if (!this.messageHandler) {
|
|
154
|
+
throw new Error('handleIncomingMessage: messageHandler is not set. Call setMessageHandler() first.');
|
|
155
|
+
}
|
|
156
|
+
const chatId = p['chatId'];
|
|
157
|
+
const receiver = p['receiver'];
|
|
158
|
+
const task = p['task'] ?? null;
|
|
159
|
+
const agreement = p['agreement'] ?? null;
|
|
160
|
+
runtimeLog.info(this.label, `[wire-debug] incoming chatId=${chatId} sender=${senderId} content_type=${p['content_type'] ?? p['contentType'] ?? '<none>'} entryType=${p['entryType'] ?? '<none>'} task?=${task ? `taskId=${task['taskId']} state=${task['state']}` : 'no'} agreement?=${agreement ? `agreementId=${agreement['agreementId']} status=${agreement['status']}` : 'no'}`);
|
|
161
|
+
const metadata = {
|
|
162
|
+
chatId,
|
|
163
|
+
userId: senderId,
|
|
164
|
+
sender: { id: senderId, type: senderType },
|
|
165
|
+
senderId,
|
|
166
|
+
senderType,
|
|
167
|
+
receiver: receiver || null,
|
|
168
|
+
receiverId: receiver?.['id'] || null,
|
|
169
|
+
entryType: p['entryType'],
|
|
170
|
+
content_type: (p['content_type'] ?? p['contentType']),
|
|
171
|
+
taskId: task?.['taskId'] ?? null,
|
|
172
|
+
task,
|
|
173
|
+
agreement,
|
|
174
|
+
};
|
|
175
|
+
await this.messageHandler(p['text'], metadata);
|
|
176
|
+
}
|
|
177
|
+
buildSocketOptions() {
|
|
178
|
+
// `forceNew: true` is required when multiple WebSocketClient instances
|
|
179
|
+
// hit the same URL with DIFFERENT auth. socket.io's default
|
|
180
|
+
// `multiplex: true` shares the engine.io Manager across `io()` calls
|
|
181
|
+
// to the same URL — and the Manager's auth is fixed by whoever opened
|
|
182
|
+
// it first. So a second client (e.g. an eval's user-mode connection
|
|
183
|
+
// alongside agent-host operator-key connections) would silently inherit
|
|
184
|
+
// the first client's auth and get rejected. Forcing a fresh Manager
|
|
185
|
+
// per client avoids that aliasing.
|
|
186
|
+
const options = {
|
|
187
|
+
transports: ['websocket'],
|
|
188
|
+
reconnection: true,
|
|
189
|
+
reconnectionAttempts: Infinity,
|
|
190
|
+
reconnectionDelay: 1000,
|
|
191
|
+
reconnectionDelayMax: 5000,
|
|
192
|
+
timeout: 20000,
|
|
193
|
+
autoConnect: true,
|
|
194
|
+
forceNew: true,
|
|
195
|
+
multiplex: false,
|
|
196
|
+
};
|
|
197
|
+
if (this.userToken) {
|
|
198
|
+
const bearer = `Bearer ${this.userToken}`;
|
|
199
|
+
options.auth = { token: bearer };
|
|
200
|
+
options.extraHeaders = { Authorization: bearer };
|
|
201
|
+
// No agentId — the gateway sees the `userId` claim and routes as a human user.
|
|
202
|
+
}
|
|
203
|
+
else if (this.operatorKey) {
|
|
204
|
+
const bearer = `Bearer ${this.operatorKey}`;
|
|
205
|
+
options.auth = { token: bearer };
|
|
206
|
+
options.extraHeaders = { Authorization: bearer };
|
|
207
|
+
// agentId goes in query so the gateway can route; token stays out of the URL
|
|
208
|
+
// to avoid leaking the long-lived operator key into proxy access logs.
|
|
209
|
+
if (this.agentId)
|
|
210
|
+
options.query = { agentId: this.agentId };
|
|
211
|
+
}
|
|
212
|
+
return options;
|
|
213
|
+
}
|
|
214
|
+
generateMessageId() {
|
|
215
|
+
return crypto.randomUUID();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { WebSocketClient } from './WebSocketClient.js';
|
package/package.json
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziggs-ai/api-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "HTTP and WebSocket client for the Ziggs backend API",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
7
8
|
"exports": {
|
|
8
|
-
".": "./
|
|
9
|
-
"./http": "./
|
|
10
|
-
"./websocket": "./
|
|
9
|
+
".": "./dist/index.js",
|
|
10
|
+
"./http": "./dist/http/index.js",
|
|
11
|
+
"./websocket": "./dist/websocket/index.js"
|
|
11
12
|
},
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": ">=18"
|
|
14
15
|
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"prepare": "npm run build",
|
|
19
|
+
"test": "node --import tsx/esm --test --test-reporter=spec test/*.test.ts",
|
|
20
|
+
"test:watch": "node --import tsx/esm --test --watch test/*.test.ts"
|
|
21
|
+
},
|
|
15
22
|
"dependencies": {
|
|
16
23
|
"dotenv": "^17.2.3",
|
|
17
24
|
"socket.io-client": "^4.7.0"
|
|
18
25
|
},
|
|
19
26
|
"keywords": ["ziggs", "api", "client", "websocket"],
|
|
20
27
|
"license": "MIT",
|
|
21
|
-
"files": ["
|
|
28
|
+
"files": ["dist", "README.md"],
|
|
22
29
|
"publishConfig": {
|
|
23
30
|
"access": "public",
|
|
24
31
|
"registry": "https://registry.npmjs.org/"
|
package/src/ConnectionManager.js
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ConnectionManager — generic, agent-agnostic pool.
|
|
3
|
-
*
|
|
4
|
-
* Holds an in-memory map of registered entities keyed by id. Each entity is
|
|
5
|
-
* described by a pair of functions: `openFn()` returns a live "connection"
|
|
6
|
-
* handle, `closeFn(handle)` tears it down. The manager enforces a `maxActive`
|
|
7
|
-
* cap with LRU eviction and automatically releases idle entries after
|
|
8
|
-
* `idleTimeoutMs`.
|
|
9
|
-
*
|
|
10
|
-
* Optionally opens a control socket (`createControlSocket`) so the backend
|
|
11
|
-
* can push `wake` events for entries that aren't currently active — the
|
|
12
|
-
* manager handles receipt and calls `wake(id)` on your behalf.
|
|
13
|
-
*
|
|
14
|
-
* This lives in api-client because it has no dependency on ZiggsAgent (or any
|
|
15
|
-
* other SDK concept). Anyone writing their own agent runtime — or another
|
|
16
|
-
* language binding — can use ConnectionManager directly, or just use
|
|
17
|
-
* `createControlSocket` alone if they already manage their own lifecycle.
|
|
18
|
-
*
|
|
19
|
-
* Usage:
|
|
20
|
-
* const mgr = new ConnectionManager({
|
|
21
|
-
* maxActive: 50,
|
|
22
|
-
* idleTimeoutMs: 60_000,
|
|
23
|
-
* control: { wsUrl, operatorKey }, // optional
|
|
24
|
-
* });
|
|
25
|
-
* mgr.register(
|
|
26
|
-
* 'agent-42',
|
|
27
|
-
* async () => openMySocketFor('agent-42'),
|
|
28
|
-
* async (handle) => handle.close(),
|
|
29
|
-
* { domain: 'science' } // optional metadata for query()
|
|
30
|
-
* );
|
|
31
|
-
* mgr.start(); // opens the control socket (if configured)
|
|
32
|
-
* const handle = await mgr.wake('agent-42');
|
|
33
|
-
* await mgr.stop();
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
import { createControlSocket } from './websocket/ControlSocket.js';
|
|
37
|
-
|
|
38
|
-
export class ConnectionManager {
|
|
39
|
-
/**
|
|
40
|
-
* @param {object} opts
|
|
41
|
-
* @param {number} [opts.maxActive=50] Max simultaneous open handles
|
|
42
|
-
* @param {number} [opts.idleTimeoutMs=60000] Ms before an idle handle is auto-closed
|
|
43
|
-
* @param {{ wsUrl: string, operatorKey: string }} [opts.control] Enables control socket wake-on-demand.
|
|
44
|
-
*/
|
|
45
|
-
constructor({ maxActive = 50, idleTimeoutMs = 60_000, control } = {}) {
|
|
46
|
-
this.maxActive = maxActive;
|
|
47
|
-
this.idleTimeoutMs = idleTimeoutMs;
|
|
48
|
-
this._controlOpts = control ?? null;
|
|
49
|
-
this._controlHandle = null;
|
|
50
|
-
|
|
51
|
-
// id → { openFn, closeFn }
|
|
52
|
-
this._entries = new Map();
|
|
53
|
-
// id → { handle, timer, lastActive }
|
|
54
|
-
this._active = new Map();
|
|
55
|
-
// id → user-supplied metadata (for query())
|
|
56
|
-
this._meta = new Map();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ---- registration ----
|
|
60
|
-
|
|
61
|
-
register(id, openFn, closeFn, meta) {
|
|
62
|
-
if (!id) throw new Error('[ConnectionManager] id is required');
|
|
63
|
-
if (typeof openFn !== 'function') throw new Error('[ConnectionManager] openFn must be a function');
|
|
64
|
-
if (typeof closeFn !== 'function') throw new Error('[ConnectionManager] closeFn must be a function');
|
|
65
|
-
this._entries.set(id, { openFn, closeFn });
|
|
66
|
-
if (meta) this._meta.set(id, meta);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ---- lifecycle ----
|
|
70
|
-
|
|
71
|
-
/** Open the control socket (if configured). Safe to call multiple times. */
|
|
72
|
-
start() {
|
|
73
|
-
if (this._controlHandle || !this._controlOpts) return;
|
|
74
|
-
this._controlHandle = createControlSocket({
|
|
75
|
-
...this._controlOpts,
|
|
76
|
-
agentIds: () => this.list(),
|
|
77
|
-
onWake: (id) => this.wake(id).catch(err => console.warn(`[ConnectionManager] wake("${id}") failed: ${err.message}`)),
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Close the control socket and all active handles. */
|
|
82
|
-
async stop() {
|
|
83
|
-
this._controlHandle?.close();
|
|
84
|
-
this._controlHandle = null;
|
|
85
|
-
await this.sleepAll();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ---- connection lifecycle ----
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Ensure an entry is active. Returns the live handle from `openFn()`.
|
|
92
|
-
* If `maxActive` is hit, evicts the LRU entry first.
|
|
93
|
-
*/
|
|
94
|
-
async wake(id) {
|
|
95
|
-
const existing = this._active.get(id);
|
|
96
|
-
if (existing) {
|
|
97
|
-
this._resetTimer(id);
|
|
98
|
-
return existing.handle;
|
|
99
|
-
}
|
|
100
|
-
const entry = this._entries.get(id);
|
|
101
|
-
if (!entry) throw new Error(`[ConnectionManager] unknown id: "${id}"`);
|
|
102
|
-
|
|
103
|
-
if (this._active.size >= this.maxActive) await this._evictLRU();
|
|
104
|
-
|
|
105
|
-
const handle = await entry.openFn();
|
|
106
|
-
this._active.set(id, { handle, timer: null, lastActive: Date.now() });
|
|
107
|
-
this._scheduleIdle(id);
|
|
108
|
-
return handle;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Close a single active entry. */
|
|
112
|
-
async sleep(id) {
|
|
113
|
-
const entry = this._active.get(id);
|
|
114
|
-
if (!entry) return;
|
|
115
|
-
clearTimeout(entry.timer);
|
|
116
|
-
this._active.delete(id);
|
|
117
|
-
const reg = this._entries.get(id);
|
|
118
|
-
if (reg) await reg.closeFn(entry.handle);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async sleepAll() {
|
|
122
|
-
await Promise.all([...this._active.keys()].map(id => this.sleep(id)));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** Call after `wake` if you performed work and want to extend the idle timer. */
|
|
126
|
-
touch(id) { this._resetTimer(id); }
|
|
127
|
-
|
|
128
|
-
// ---- introspection ----
|
|
129
|
-
|
|
130
|
-
/** All registered ids. */
|
|
131
|
-
list() { return [...this._entries.keys()]; }
|
|
132
|
-
/** Currently active ids. */
|
|
133
|
-
listActive() { return [...this._active.keys()]; }
|
|
134
|
-
get size() { return this._entries.size; }
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Filter ids by metadata. `filter.domain` is exact match; `filter.expertise`
|
|
138
|
-
* and `filter.tags` are "at least one must match". No filter → all ids.
|
|
139
|
-
*/
|
|
140
|
-
query({ domain, expertise, tags } = {}) {
|
|
141
|
-
const results = [];
|
|
142
|
-
for (const [id, meta] of this._meta) {
|
|
143
|
-
if (domain && meta.domain !== domain) continue;
|
|
144
|
-
if (expertise?.length && !expertise.some(e => meta.expertise?.includes(e))) continue;
|
|
145
|
-
if (tags?.length && !tags.some(t => meta.tags?.includes(t))) continue;
|
|
146
|
-
results.push(id);
|
|
147
|
-
}
|
|
148
|
-
return results;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Read metadata for a single id (undefined if not registered / no meta). */
|
|
152
|
-
getMeta(id) { return this._meta.get(id); }
|
|
153
|
-
|
|
154
|
-
// ---- internals ----
|
|
155
|
-
|
|
156
|
-
_scheduleIdle(id) {
|
|
157
|
-
const entry = this._active.get(id);
|
|
158
|
-
if (!entry) return;
|
|
159
|
-
clearTimeout(entry.timer);
|
|
160
|
-
entry.timer = setTimeout(() => {
|
|
161
|
-
this.sleep(id).catch(err => console.warn(`[ConnectionManager] idle sleep("${id}") failed: ${err.message}`));
|
|
162
|
-
}, this.idleTimeoutMs);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
_resetTimer(id) {
|
|
166
|
-
const entry = this._active.get(id);
|
|
167
|
-
if (!entry) return;
|
|
168
|
-
entry.lastActive = Date.now();
|
|
169
|
-
this._scheduleIdle(id);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async _evictLRU() {
|
|
173
|
-
let oldest = null;
|
|
174
|
-
for (const [id, entry] of this._active) {
|
|
175
|
-
if (!oldest || entry.lastActive < this._active.get(oldest).lastActive) oldest = id;
|
|
176
|
-
}
|
|
177
|
-
if (oldest) await this.sleep(oldest);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import 'dotenv/config';
|
|
2
|
-
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Client for searching and retrieving agent information from the Ziggs API
|
|
6
|
-
*/
|
|
7
|
-
export class AgentSearchClient {
|
|
8
|
-
constructor(operatorKey, agentId) {
|
|
9
|
-
if (!operatorKey) {
|
|
10
|
-
throw new Error('AgentSearchClient: operatorKey is required');
|
|
11
|
-
}
|
|
12
|
-
if (!agentId) {
|
|
13
|
-
throw new Error('AgentSearchClient: agentId is required (operator-token impersonation)');
|
|
14
|
-
}
|
|
15
|
-
this.operatorKey = operatorKey;
|
|
16
|
-
this.agentId = agentId;
|
|
17
|
-
this.baseUrl = getBackendUrl();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Search for agents based on a text query
|
|
22
|
-
* @param {string} query - Search query text
|
|
23
|
-
* @param {Object} options - Additional options
|
|
24
|
-
* @param {boolean} options.fullResponse - Whether to return full response data
|
|
25
|
-
* @returns {Promise<Object>} Search results
|
|
26
|
-
*/
|
|
27
|
-
async searchAgents(query, options = {}) {
|
|
28
|
-
const { fullResponse = true } = options;
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const response = await fetch(`${this.baseUrl}/agents/searchAgents`, {
|
|
32
|
-
method: 'POST',
|
|
33
|
-
headers: this._buildHeaders(),
|
|
34
|
-
body: JSON.stringify({
|
|
35
|
-
query,
|
|
36
|
-
fullResponse
|
|
37
|
-
})
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
if (!response.ok) {
|
|
41
|
-
const errorText = await response.text();
|
|
42
|
-
return {
|
|
43
|
-
success: false,
|
|
44
|
-
error: `Search failed: ${response.status}`,
|
|
45
|
-
message: errorText
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const data = await response.json();
|
|
50
|
-
return data;
|
|
51
|
-
|
|
52
|
-
} catch (error) {
|
|
53
|
-
return {
|
|
54
|
-
success: false,
|
|
55
|
-
error: error.message || 'Failed to search agents'
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Get detailed information about a specific agent
|
|
62
|
-
* @param {string} agentId - The unique identifier of the agent
|
|
63
|
-
* @returns {Promise<Object>} Agent details
|
|
64
|
-
*/
|
|
65
|
-
async getAgentById(agentId) {
|
|
66
|
-
if (!agentId) {
|
|
67
|
-
return {
|
|
68
|
-
success: false,
|
|
69
|
-
error: 'agentId is required'
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const response = await fetch(`${this.baseUrl}/agents/getAgentById`, {
|
|
75
|
-
method: 'POST',
|
|
76
|
-
headers: this._buildHeaders(),
|
|
77
|
-
body: JSON.stringify({ agentId })
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
if (!response.ok) {
|
|
81
|
-
return {
|
|
82
|
-
success: false,
|
|
83
|
-
error: `Failed to get agent details: ${response.status}`
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const agent = await response.json();
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
success: true,
|
|
91
|
-
agentId: agent.agentId,
|
|
92
|
-
name: agent.name,
|
|
93
|
-
description: agent.storePage?.description || 'No description available',
|
|
94
|
-
storePage: agent.storePage,
|
|
95
|
-
type: agent.type || 'agent'
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
} catch (error) {
|
|
99
|
-
return {
|
|
100
|
-
success: false,
|
|
101
|
-
error: error.message || 'Failed to get agent details'
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
_buildHeaders() {
|
|
107
|
-
return {
|
|
108
|
-
'Content-Type': 'application/json',
|
|
109
|
-
'Authorization': `Bearer ${this.operatorKey}`,
|
|
110
|
-
'X-Agent-Id': this.agentId,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import 'dotenv/config';
|
|
2
|
-
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
-
|
|
4
|
-
export class ContextReader {
|
|
5
|
-
constructor(operatorKey, agentId) {
|
|
6
|
-
if (!operatorKey) {
|
|
7
|
-
throw new Error('ContextReader: operatorKey is required');
|
|
8
|
-
}
|
|
9
|
-
if (!agentId) {
|
|
10
|
-
throw new Error('ContextReader: agentId is required (operator-token impersonation)');
|
|
11
|
-
}
|
|
12
|
-
this.operatorKey = operatorKey;
|
|
13
|
-
this.agentId = agentId;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
_getBaseUrl() {
|
|
17
|
-
const route = process.env.HISTORY_SERVICE_ROUTE || 'context';
|
|
18
|
-
return `${getBackendUrl()}/${route}`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @param {string} chatId - Chat ID
|
|
23
|
-
* @param {Object} [options] - Optional limits (passed as query params to Shared Context Service API)
|
|
24
|
-
* @param {number} [options.maxMessages] - Max number of history entries (default: server default)
|
|
25
|
-
* @param {number} [options.maxCharacters] - Max total character count of history text (default: server default)
|
|
26
|
-
* @param {number} [options.maxTaskHistory] - Max number of task_history entries (default: server default)
|
|
27
|
-
* @param {boolean} [options.summarize] - Default true. Set false to skip timelineSummary generation.
|
|
28
|
-
*/
|
|
29
|
-
async read(chatId, options = {}) {
|
|
30
|
-
if (!chatId) {
|
|
31
|
-
return this._emptyContext();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const url = new URL(`${this._getBaseUrl()}/read`);
|
|
36
|
-
url.searchParams.set('chatId', chatId);
|
|
37
|
-
if (options.maxMessages != null) url.searchParams.set('maxMessages', String(options.maxMessages));
|
|
38
|
-
if (options.maxCharacters != null) url.searchParams.set('maxCharacters', String(options.maxCharacters));
|
|
39
|
-
if (options.maxTaskHistory != null) url.searchParams.set('maxTaskHistory', String(options.maxTaskHistory));
|
|
40
|
-
// Default summarize on; callers can disable with summarize: false
|
|
41
|
-
if (options.summarize !== false) url.searchParams.set('summarize', 'true');
|
|
42
|
-
|
|
43
|
-
const res = await fetch(url.toString(), {
|
|
44
|
-
method: 'GET',
|
|
45
|
-
headers: this._buildHeaders()
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
if (!res.ok) {
|
|
49
|
-
console.warn(`⚠️ Context read failed: ${res.status}`);
|
|
50
|
-
return this._emptyContext();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const data = await res.json();
|
|
54
|
-
return this._extractContext(data);
|
|
55
|
-
|
|
56
|
-
} catch (error) {
|
|
57
|
-
console.warn(`⚠️ Context read error: ${error.message}`);
|
|
58
|
-
return this._emptyContext();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
_extractContext(data) {
|
|
63
|
-
const history = Array.isArray(data?.history) ? data.history : [];
|
|
64
|
-
const users = Array.isArray(data?.users) ? data.users : [];
|
|
65
|
-
const activeTasks = Array.isArray(data?.openTasks) ? data.openTasks : [];
|
|
66
|
-
const agents = Array.isArray(data?.agents) ? data.agents : [];
|
|
67
|
-
const perspectiveId = data?.perspectiveId || null;
|
|
68
|
-
const timelineSummary = data?.timelineSummary ?? null;
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
history,
|
|
72
|
-
users,
|
|
73
|
-
activeTasks,
|
|
74
|
-
agents,
|
|
75
|
-
perspectiveId,
|
|
76
|
-
...(timelineSummary != null && { timelineSummary }),
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
_emptyContext() {
|
|
81
|
-
return {
|
|
82
|
-
history: [],
|
|
83
|
-
users: [],
|
|
84
|
-
activeTasks: [],
|
|
85
|
-
agents: [],
|
|
86
|
-
perspectiveId: null,
|
|
87
|
-
latestSequence: null,
|
|
88
|
-
timelineSummary: null
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
_buildHeaders() {
|
|
93
|
-
return {
|
|
94
|
-
'content-type': 'application/json',
|
|
95
|
-
'Authorization': `Bearer ${this.operatorKey}`,
|
|
96
|
-
'X-Agent-Id': this.agentId,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
}
|