@openagents-org/agent-connector 0.1.11 → 0.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-connector",
3
- "version": "0.1.11",
3
+ "version": "0.2.1",
4
4
  "description": "Agent management CLI and library for OpenAgents — install, configure, and run AI coding agents",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Base adapter for OpenAgents workspace.
3
+ *
4
+ * Extracts the common connectivity logic shared by all adapters:
5
+ * - Event cursor management and skip-existing-events on startup
6
+ * - Heartbeat loop (30s)
7
+ * - Adaptive poll loop with deduplication
8
+ * - Control event polling (mode changes, stop)
9
+ * - Per-channel task dispatch with queuing
10
+ * - Auto-titling of new channels
11
+ * - Graceful shutdown with disconnect
12
+ *
13
+ * Subclasses must implement _handleMessage(msg).
14
+ *
15
+ * Direct port of Python: src/openagents/adapters/base.py
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const { WorkspaceClient } = require('../workspace-client');
21
+ const { generateSessionTitle, SESSION_DEFAULT_RE } = require('./utils');
22
+
23
+ const DEFAULT_ENDPOINT = 'https://workspace-endpoint.openagents.org';
24
+
25
+ class BaseAdapter {
26
+ /**
27
+ * @param {object} opts
28
+ * @param {string} opts.workspaceId
29
+ * @param {string} opts.channelName - default/initial channel
30
+ * @param {string} opts.token
31
+ * @param {string} opts.agentName
32
+ * @param {string} [opts.endpoint]
33
+ */
34
+ constructor({ workspaceId, channelName, token, agentName, endpoint }) {
35
+ this.workspaceId = workspaceId;
36
+ this.channelName = channelName;
37
+ this.token = token;
38
+ this.agentName = agentName;
39
+ this.endpoint = endpoint || DEFAULT_ENDPOINT;
40
+ this.client = new WorkspaceClient(this.endpoint);
41
+ this._lastEventId = null;
42
+ this._running = false;
43
+ this._processedIds = new Set();
44
+ this._titledSessions = new Set();
45
+ this._mode = 'execute';
46
+ this._lastControlId = null;
47
+ // Per-channel task tracking for parallel execution
48
+ this._channelBusy = new Set();
49
+ this._channelQueues = {};
50
+ this._log = (msg) => {
51
+ const ts = new Date().toISOString();
52
+ console.log(`${ts} INFO adapter: ${msg}`);
53
+ };
54
+ }
55
+
56
+ // ------------------------------------------------------------------
57
+ // Lifecycle
58
+ // ------------------------------------------------------------------
59
+
60
+ async run() {
61
+ this._running = true;
62
+ await this._skipExistingEvents();
63
+
64
+ const heartbeatInterval = setInterval(() => this._heartbeat(), 30000);
65
+ const controlInterval = setInterval(() => this._pollControl(), 2000);
66
+
67
+ try {
68
+ // Send initial heartbeat
69
+ await this._heartbeat();
70
+ await this._pollLoop();
71
+ } finally {
72
+ this._running = false;
73
+ clearInterval(heartbeatInterval);
74
+ clearInterval(controlInterval);
75
+ try {
76
+ await this.client.disconnect(this.workspaceId, this.agentName, this.token);
77
+ } catch {}
78
+ }
79
+ }
80
+
81
+ stop() {
82
+ this._running = false;
83
+ }
84
+
85
+ // ------------------------------------------------------------------
86
+ // Event cursor / skip existing
87
+ // ------------------------------------------------------------------
88
+
89
+ async _skipExistingEvents() {
90
+ try {
91
+ while (true) {
92
+ const { cursor } = await this.client.pollPending(
93
+ this.workspaceId, this.agentName, this.token,
94
+ { after: this._lastEventId, limit: 200 }
95
+ );
96
+ if (!cursor || cursor === this._lastEventId) break;
97
+ this._lastEventId = cursor;
98
+ }
99
+ if (this._lastEventId) {
100
+ this._log(`Skipped existing events, cursor at ${this._lastEventId}`);
101
+ }
102
+ } catch (e) {
103
+ this._log(`Failed to skip existing events: ${e.message}`);
104
+ }
105
+ }
106
+
107
+ // ------------------------------------------------------------------
108
+ // Heartbeat
109
+ // ------------------------------------------------------------------
110
+
111
+ async _heartbeat() {
112
+ try {
113
+ await this.client.heartbeat(this.workspaceId, this.agentName, this.token);
114
+ } catch (e) {
115
+ this._log(`Heartbeat failed: ${e.message}`);
116
+ }
117
+ }
118
+
119
+ // ------------------------------------------------------------------
120
+ // Control polling
121
+ // ------------------------------------------------------------------
122
+
123
+ async _pollControl() {
124
+ try {
125
+ const events = await this.client.pollControl(
126
+ this.workspaceId, this.agentName, this.token,
127
+ { after: this._lastControlId }
128
+ );
129
+ for (const ev of events) {
130
+ if (ev.id) this._lastControlId = ev.id;
131
+ const payload = ev.payload || {};
132
+ const action = payload.action;
133
+ if (action === 'set_mode') {
134
+ const newMode = payload.mode || 'execute';
135
+ if ((newMode === 'execute' || newMode === 'plan') && newMode !== this._mode) {
136
+ const oldMode = this._mode;
137
+ this._mode = newMode;
138
+ this._log(`Mode changed: ${oldMode} -> ${newMode}`);
139
+ }
140
+ } else {
141
+ await this._onControlAction(action, payload);
142
+ }
143
+ }
144
+ } catch {}
145
+ }
146
+
147
+ /**
148
+ * Handle adapter-specific control actions. Override in subclasses.
149
+ */
150
+ async _onControlAction(_action, _payload) {}
151
+
152
+ // ------------------------------------------------------------------
153
+ // Poll loop
154
+ // ------------------------------------------------------------------
155
+
156
+ async _pollLoop() {
157
+ let idleCount = 0;
158
+
159
+ while (this._running) {
160
+ let messages, rawCursor;
161
+ try {
162
+ const result = await this.client.pollPending(
163
+ this.workspaceId, this.agentName, this.token,
164
+ { after: this._lastEventId }
165
+ );
166
+ messages = result.messages;
167
+ rawCursor = result.cursor;
168
+ } catch (e) {
169
+ this._log(`Poll failed: ${e.message}`);
170
+ await this._sleep(5000);
171
+ continue;
172
+ }
173
+
174
+ if (rawCursor) this._lastEventId = rawCursor;
175
+
176
+ // Deduplicate
177
+ const incoming = [];
178
+ for (const msg of messages) {
179
+ const msgId = msg.id || msg.messageId;
180
+ if (msgId && this._processedIds.has(msgId)) continue;
181
+ if (msg.messageType === 'status') continue;
182
+ incoming.push(msg);
183
+ }
184
+
185
+ if (incoming.length > 0) {
186
+ idleCount = 0;
187
+ for (const msg of incoming) {
188
+ const msgId = msg.id || msg.messageId;
189
+ if (msgId) this._processedIds.add(msgId);
190
+ await this._dispatchMessage(msg);
191
+ }
192
+ // Cap dedup set
193
+ if (this._processedIds.size > 2000) {
194
+ const arr = [...this._processedIds];
195
+ this._processedIds.clear();
196
+ for (const id of arr.slice(-1000)) this._processedIds.add(id);
197
+ }
198
+ } else {
199
+ idleCount++;
200
+ }
201
+
202
+ // Adaptive polling: 2s active, up to 15s idle
203
+ const delay = incoming.length > 0 ? 2000 : Math.min(2000 + idleCount * 1000, 15000);
204
+ await this._sleep(delay);
205
+ }
206
+ }
207
+
208
+ // ------------------------------------------------------------------
209
+ // Channel dispatch
210
+ // ------------------------------------------------------------------
211
+
212
+ async _dispatchMessage(msg) {
213
+ const channel = msg.sessionId || this.channelName;
214
+
215
+ if (this._channelBusy.has(channel)) {
216
+ if (!this._channelQueues[channel]) this._channelQueues[channel] = [];
217
+ this._channelQueues[channel].push(msg);
218
+ try {
219
+ await this.sendStatus(channel, 'message queued — will process after current task');
220
+ } catch {}
221
+ return;
222
+ }
223
+
224
+ // Run channel worker (don't await — parallel execution)
225
+ this._channelWorker(channel, msg);
226
+ }
227
+
228
+ async _channelWorker(channel, msg) {
229
+ this._channelBusy.add(channel);
230
+ try {
231
+ await this._handleMessage(msg);
232
+ } catch (e) {
233
+ this._log(`Error in channel worker for ${channel}: ${e.message}`);
234
+ try { await this.sendError(channel, `Agent error: ${e.message}`); } catch {}
235
+ }
236
+
237
+ // Drain queue
238
+ while (true) {
239
+ const queue = this._channelQueues[channel];
240
+ if (!queue || queue.length === 0) break;
241
+ const nextMsg = queue.shift();
242
+ try {
243
+ await this._handleMessage(nextMsg);
244
+ } catch (e) {
245
+ this._log(`Error processing queued message in ${channel}: ${e.message}`);
246
+ try { await this.sendError(channel, `Agent error: ${e.message}`); } catch {}
247
+ }
248
+ }
249
+ this._channelBusy.delete(channel);
250
+ }
251
+
252
+ // ------------------------------------------------------------------
253
+ // Auto-title helper
254
+ // ------------------------------------------------------------------
255
+
256
+ async _autoTitleChannel(channel, content) {
257
+ if (this._titledSessions.has(channel)) return;
258
+ this._titledSessions.add(channel);
259
+ const title = generateSessionTitle(content);
260
+ if (!title) return;
261
+ try {
262
+ const info = await this.client.getSession(this.workspaceId, channel, this.token);
263
+ if (!info.titleManuallySet && SESSION_DEFAULT_RE.test(info.title || '')) {
264
+ await this.client.updateSession(
265
+ this.workspaceId, channel, this.token,
266
+ { title, autoTitle: true }
267
+ );
268
+ this._log(`Auto-titled channel: ${title}`);
269
+ }
270
+ } catch (e) {
271
+ this._log(`Failed to auto-title channel: ${e.message}`);
272
+ }
273
+ }
274
+
275
+ // ------------------------------------------------------------------
276
+ // Message helpers
277
+ // ------------------------------------------------------------------
278
+
279
+ async sendStatus(channel, content) {
280
+ try {
281
+ await this.client.sendMessage(this.workspaceId, channel, this.token, content, {
282
+ senderType: 'agent',
283
+ senderName: this.agentName,
284
+ messageType: 'status',
285
+ metadata: { agent_mode: this._mode },
286
+ });
287
+ } catch {}
288
+ }
289
+
290
+ async sendResponse(channel, content) {
291
+ await this.client.sendMessage(this.workspaceId, channel, this.token, content, {
292
+ senderType: 'agent',
293
+ senderName: this.agentName,
294
+ });
295
+ }
296
+
297
+ async sendError(channel, error) {
298
+ try {
299
+ await this.client.sendMessage(this.workspaceId, channel, this.token, error, {
300
+ senderType: 'agent',
301
+ senderName: this.agentName,
302
+ });
303
+ } catch {}
304
+ }
305
+
306
+ // ------------------------------------------------------------------
307
+ // Abstract
308
+ // ------------------------------------------------------------------
309
+
310
+ /**
311
+ * Process a single incoming message. Must be implemented by subclasses.
312
+ * @param {object} msg
313
+ */
314
+ async _handleMessage(_msg) {
315
+ throw new Error('_handleMessage must be implemented by subclass');
316
+ }
317
+
318
+ // ------------------------------------------------------------------
319
+ // Utility
320
+ // ------------------------------------------------------------------
321
+
322
+ _sleep(ms) {
323
+ return new Promise((resolve) => setTimeout(resolve, ms));
324
+ }
325
+ }
326
+
327
+ module.exports = BaseAdapter;