@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 +1 -1
- package/src/adapters/base.js +327 -0
- package/src/adapters/claude.js +420 -0
- package/src/adapters/codex.js +260 -0
- package/src/adapters/index.js +39 -0
- package/src/adapters/openclaw.js +259 -0
- package/src/adapters/utils.js +83 -0
- package/src/adapters/workspace-prompt.js +293 -0
- package/src/daemon.js +38 -291
- package/src/index.js +3 -1
- package/src/workspace-client.js +48 -11
package/package.json
CHANGED
|
@@ -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;
|