@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.
Files changed (49) hide show
  1. package/README.md +13 -7
  2. package/dist/ConnectionManager.d.ts +45 -0
  3. package/dist/ConnectionManager.js +118 -0
  4. package/dist/http/AgentSearchClient.d.ts +36 -0
  5. package/dist/http/AgentSearchClient.js +72 -0
  6. package/dist/http/AgreementClient.d.ts +153 -0
  7. package/dist/http/AgreementClient.js +457 -0
  8. package/dist/http/ArtifactsClient.d.ts +48 -0
  9. package/dist/http/ArtifactsClient.js +90 -0
  10. package/dist/http/ChatClient.d.ts +14 -0
  11. package/dist/http/ChatClient.js +69 -0
  12. package/dist/http/MarketplaceClient.d.ts +19 -0
  13. package/dist/http/MarketplaceClient.js +72 -0
  14. package/dist/http/MessagesClient.d.ts +22 -0
  15. package/dist/http/MessagesClient.js +41 -0
  16. package/dist/http/ScopeClient.d.ts +33 -0
  17. package/dist/http/ScopeClient.js +39 -0
  18. package/dist/http/TaskClient.d.ts +75 -0
  19. package/dist/http/TaskClient.js +343 -0
  20. package/dist/http/TelemetryClient.d.ts +11 -0
  21. package/dist/http/TelemetryClient.js +53 -0
  22. package/dist/http/index.d.ts +12 -0
  23. package/dist/http/index.js +9 -0
  24. package/dist/index.d.ts +9 -0
  25. package/{src → dist}/index.js +2 -12
  26. package/dist/shared/runtimeLog.d.ts +14 -0
  27. package/dist/shared/runtimeLog.js +64 -0
  28. package/dist/types.d.ts +120 -0
  29. package/dist/types.js +50 -0
  30. package/dist/utils/urlUtils.d.ts +2 -0
  31. package/dist/utils/urlUtils.js +8 -0
  32. package/dist/websocket/ControlSocket.d.ts +13 -0
  33. package/dist/websocket/ControlSocket.js +36 -0
  34. package/dist/websocket/WebSocketClient.d.ts +71 -0
  35. package/dist/websocket/WebSocketClient.js +217 -0
  36. package/dist/websocket/index.js +1 -0
  37. package/package.json +13 -6
  38. package/src/ConnectionManager.js +0 -179
  39. package/src/http/AgentSearchClient.js +0 -113
  40. package/src/http/ContextReader.js +0 -99
  41. package/src/http/ContextWriter.js +0 -98
  42. package/src/http/TaskClient.js +0 -612
  43. package/src/http/TelemetryClient.js +0 -43
  44. package/src/http/index.js +0 -6
  45. package/src/types.js +0 -28
  46. package/src/utils/urlUtils.js +0 -17
  47. package/src/websocket/ControlSocket.js +0 -55
  48. package/src/websocket/WebSocketClient.js +0 -318
  49. /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",
3
+ "version": "0.1.4",
4
4
  "description": "HTTP and WebSocket client for the Ziggs backend API",
5
5
  "type": "module",
6
- "main": "src/index.js",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
7
8
  "exports": {
8
- ".": "./src/index.js",
9
- "./http": "./src/http/index.js",
10
- "./websocket": "./src/websocket/index.js"
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": ["src", "README.md"],
28
+ "files": ["dist", "README.md"],
22
29
  "publishConfig": {
23
30
  "access": "public",
24
31
  "registry": "https://registry.npmjs.org/"
@@ -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
- }