@openagents-org/agent-launcher 0.1.0

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.
@@ -0,0 +1,338 @@
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+ const http = require('http');
5
+
6
+ const DEFAULT_ENDPOINT = 'https://workspace-endpoint.openagents.org';
7
+
8
+ /**
9
+ * HTTP client for workspace API operations.
10
+ *
11
+ * Mirrors the Python SDK's WorkspaceClient — same endpoints, same
12
+ * auth headers (X-Workspace-Token), same request/response shapes.
13
+ */
14
+ class WorkspaceClient {
15
+ constructor(endpoint) {
16
+ this.endpoint = (endpoint || DEFAULT_ENDPOINT).replace(/\/$/, '');
17
+ }
18
+
19
+ /**
20
+ * Register an agent identity via POST /v1/agentid/register.
21
+ */
22
+ async registerAgent(agentName, { apiKey, origin = 'cli' } = {}) {
23
+ const headers = { 'Content-Type': 'application/json' };
24
+ if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
25
+
26
+ const data = await this._post('/v1/agentid/register', {
27
+ agent_name: agentName,
28
+ origin,
29
+ }, headers);
30
+
31
+ return data.data || data;
32
+ }
33
+
34
+ /**
35
+ * Create a workspace via POST /v1/workspaces.
36
+ * @returns {{ workspaceId, slug, name, token, url, channelName }}
37
+ */
38
+ async createWorkspace({ agentName, name, agentType } = {}) {
39
+ const payload = {
40
+ name: name || (agentName ? `${agentName}'s workspace` : 'My Workspace'),
41
+ };
42
+ if (agentName) payload.agent_name = agentName;
43
+ if (agentType) payload.agent_type = agentType;
44
+
45
+ const data = await this._post('/v1/workspaces', payload);
46
+ const result = data.data || data;
47
+
48
+ const frontendUrl = this.endpoint
49
+ .replace('workspace-endpoint', 'workspace')
50
+ .replace('/v1', '');
51
+
52
+ return {
53
+ workspaceId: result.workspaceId,
54
+ slug: result.slug || result.workspaceId,
55
+ name: result.name,
56
+ token: result.token,
57
+ url: `${frontendUrl}/${result.slug || result.workspaceId}?token=${result.token}`,
58
+ channelName: (result.channel || {}).name || '',
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Join a workspace via POST /v1/join.
64
+ */
65
+ async joinNetwork(agentName, token, { network, agentType, serverHost, workingDir } = {}) {
66
+ const body = { agent_name: agentName, token };
67
+ if (network) body.network = network;
68
+ if (agentType) body.agent_type = agentType;
69
+ if (serverHost) body.server_host = serverHost;
70
+ if (workingDir) body.working_dir = workingDir;
71
+
72
+ const data = await this._post('/v1/join', body);
73
+ return data.data || data;
74
+ }
75
+
76
+ /**
77
+ * Resolve a workspace token to workspace info via POST /v1/token/resolve.
78
+ * @returns {{ workspace_id, slug, name }}
79
+ */
80
+ async resolveToken(token) {
81
+ const data = await this._post('/v1/token/resolve', { token });
82
+ return data.data || data;
83
+ }
84
+
85
+ /**
86
+ * Send heartbeat via POST /v1/heartbeat.
87
+ */
88
+ async heartbeat(workspaceId, agentName, token) {
89
+ const data = await this._post('/v1/heartbeat', {
90
+ agent_name: agentName,
91
+ network: workspaceId,
92
+ }, this._wsHeaders(token));
93
+ return data.data || data;
94
+ }
95
+
96
+ /**
97
+ * Disconnect agent via POST /v1/leave. Best-effort (ignores errors).
98
+ */
99
+ async disconnect(workspaceId, agentName, token) {
100
+ try {
101
+ await this._post('/v1/leave', {
102
+ agent_name: agentName,
103
+ network: workspaceId,
104
+ }, this._wsHeaders(token));
105
+ } catch {}
106
+ }
107
+
108
+ /**
109
+ * Post a task result via POST /v1/events.
110
+ */
111
+ async sendEvent(workspaceId, event, token) {
112
+ event.network = workspaceId;
113
+ const data = await this._post('/v1/events', event, this._wsHeaders(token));
114
+ return data.data || data;
115
+ }
116
+
117
+ /**
118
+ * Send a chat message to a workspace channel.
119
+ */
120
+ async sendMessage(workspaceId, channelName, token, content, {
121
+ senderType = 'agent', senderName, messageType = 'chat', metadata,
122
+ } = {}) {
123
+ const sourcePrefix = senderType === 'agent' ? 'openagents' : 'human';
124
+ const source = senderName ? `${sourcePrefix}:${senderName}` : `${sourcePrefix}:unknown`;
125
+
126
+ return this.sendEvent(workspaceId, {
127
+ type: 'workspace.message.posted',
128
+ source,
129
+ target: `channel/${channelName}`,
130
+ payload: { content, message_type: messageType },
131
+ metadata: metadata || {},
132
+ }, token);
133
+ }
134
+
135
+ /**
136
+ * Poll for pending messages targeted at an agent via GET /v1/events.
137
+ * Returns { messages, cursor } where cursor is the last event ID.
138
+ */
139
+ async pollPending(workspaceId, agentName, token, { after, limit = 50 } = {}) {
140
+ const params = new URLSearchParams({
141
+ network: workspaceId,
142
+ type: 'workspace.message',
143
+ limit: String(limit),
144
+ });
145
+ if (after) params.set('after', after);
146
+
147
+ const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token));
148
+ const result = data.data || data;
149
+ const events = (result && result.events) || [];
150
+
151
+ let cursor = null;
152
+ if (events.length > 0) {
153
+ cursor = events[events.length - 1].id || null;
154
+ }
155
+
156
+ // Filter for messages targeted at this agent
157
+ const messages = [];
158
+ for (const e of events) {
159
+ const source = e.source || '';
160
+ const meta = e.metadata || {};
161
+ const targetAgents = meta.target_agents || [];
162
+
163
+ // Skip own messages
164
+ if (source === `openagents:${agentName}`) continue;
165
+
166
+ if (source.startsWith('human:')) {
167
+ // Human messages: pick up if targeted at this agent or broadcast
168
+ if (!targetAgents.length || targetAgents.includes(agentName)) {
169
+ messages.push(this._eventToMessage(e));
170
+ }
171
+ } else if (source.startsWith('openagents:')) {
172
+ // Agent messages: only pick up if explicitly mentioned
173
+ if (targetAgents.includes(agentName)) {
174
+ messages.push(this._eventToMessage(e));
175
+ }
176
+ }
177
+ }
178
+
179
+ return { messages, cursor };
180
+ }
181
+
182
+ /**
183
+ * Get session/channel info via GET /v1/sessions/{channelName}.
184
+ */
185
+ async getSession(workspaceId, channelName, token) {
186
+ try {
187
+ const params = new URLSearchParams({ network: workspaceId });
188
+ const data = await this._get(`/v1/sessions/${channelName}?${params}`, this._wsHeaders(token));
189
+ return (data.data || data) || {};
190
+ } catch {
191
+ return {};
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Update session/channel info via PUT /v1/sessions/{channelName}.
197
+ */
198
+ async updateSession(workspaceId, channelName, token, { title, autoTitle } = {}) {
199
+ const body = { network: workspaceId };
200
+ if (title !== undefined) body.title = title;
201
+ if (autoTitle !== undefined) body.auto_title = autoTitle;
202
+ try {
203
+ await this._post(`/v1/sessions/${channelName}`, body, this._wsHeaders(token));
204
+ } catch {}
205
+ }
206
+
207
+ /**
208
+ * Poll for control events targeted at an agent via GET /v1/events.
209
+ */
210
+ async pollControl(workspaceId, agentName, token, { after } = {}) {
211
+ try {
212
+ const params = new URLSearchParams({
213
+ network: workspaceId,
214
+ type: 'workspace.control',
215
+ limit: '10',
216
+ });
217
+ if (after) params.set('after', after);
218
+ const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token));
219
+ const result = data.data || data;
220
+ const events = (result && result.events) || [];
221
+ return events.filter((e) => {
222
+ const targets = (e.metadata || {}).target_agents || [];
223
+ return !targets.length || targets.includes(agentName);
224
+ });
225
+ } catch {
226
+ return [];
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Convert an ONM event to a message-compatible object.
232
+ */
233
+ _eventToMessage(event) {
234
+ const source = event.source || '';
235
+ const isHuman = source.startsWith('human:');
236
+ const senderName = source.replace('openagents:', '').replace('human:', '');
237
+ const payload = event.payload || {};
238
+ const target = event.target || '';
239
+ const ts = event.timestamp;
240
+
241
+ const msg = {
242
+ messageId: event.id || '',
243
+ sessionId: target.startsWith('channel/') ? target.replace('channel/', '') : target,
244
+ senderType: isHuman ? 'human' : 'agent',
245
+ senderName,
246
+ content: (payload.content || ''),
247
+ messageType: payload.message_type || 'chat',
248
+ metadata: event.metadata || {},
249
+ };
250
+ if (ts) {
251
+ msg.createdAt = new Date(ts).toISOString();
252
+ }
253
+ return msg;
254
+ }
255
+
256
+ // -- Internal --
257
+
258
+ _wsHeaders(token) {
259
+ return {
260
+ 'Content-Type': 'application/json',
261
+ 'X-Workspace-Token': token,
262
+ };
263
+ }
264
+
265
+ _get(urlPath, headers = {}) {
266
+ const fullUrl = this.endpoint + urlPath;
267
+
268
+ return new Promise((resolve, reject) => {
269
+ const parsedUrl = new URL(fullUrl);
270
+ const transport = parsedUrl.protocol === 'https:' ? https : http;
271
+
272
+ const req = transport.request(fullUrl, {
273
+ method: 'GET',
274
+ headers,
275
+ timeout: 15000,
276
+ }, (res) => {
277
+ let data = '';
278
+ res.on('data', (chunk) => { data += chunk; });
279
+ res.on('end', () => {
280
+ try {
281
+ const parsed = JSON.parse(data);
282
+ if (res.statusCode >= 400) {
283
+ reject(new Error(parsed.message || `HTTP ${res.statusCode}`));
284
+ } else {
285
+ resolve(parsed);
286
+ }
287
+ } catch {
288
+ reject(new Error(`Invalid response: ${data.slice(0, 200)}`));
289
+ }
290
+ });
291
+ });
292
+
293
+ req.on('error', reject);
294
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
295
+ req.end();
296
+ });
297
+ }
298
+
299
+ _post(urlPath, body, headers = {}) {
300
+ if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
301
+ const jsonBody = JSON.stringify(body);
302
+ const fullUrl = this.endpoint + urlPath;
303
+
304
+ return new Promise((resolve, reject) => {
305
+ const parsedUrl = new URL(fullUrl);
306
+ const transport = parsedUrl.protocol === 'https:' ? https : http;
307
+
308
+ const req = transport.request(fullUrl, {
309
+ method: 'POST',
310
+ headers: { ...headers, 'Content-Length': Buffer.byteLength(jsonBody) },
311
+ timeout: 30000,
312
+ }, (res) => {
313
+ let data = '';
314
+ res.on('data', (chunk) => { data += chunk; });
315
+ res.on('end', () => {
316
+ try {
317
+ const parsed = JSON.parse(data);
318
+ if (res.statusCode >= 400) {
319
+ const msg = parsed.message || `HTTP ${res.statusCode}`;
320
+ reject(new Error(msg));
321
+ } else {
322
+ resolve(parsed);
323
+ }
324
+ } catch {
325
+ reject(new Error(`Invalid response: ${data.slice(0, 200)}`));
326
+ }
327
+ });
328
+ });
329
+
330
+ req.on('error', reject);
331
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
332
+ req.write(jsonBody);
333
+ req.end();
334
+ });
335
+ }
336
+ }
337
+
338
+ module.exports = { WorkspaceClient };