@openagents-org/agent-launcher 0.2.155 → 0.2.157
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/daemon.js +113 -4
- package/src/workspace-client.js +14 -0
package/package.json
CHANGED
package/src/daemon.js
CHANGED
|
@@ -32,6 +32,7 @@ class Daemon {
|
|
|
32
32
|
this._cmdInterval = null;
|
|
33
33
|
this._nodeHeartbeatInterval = null; // device-level heartbeat (connect-a-node)
|
|
34
34
|
this._nodeClient = null; // lazily-created WorkspaceClient for the node
|
|
35
|
+
this._runningCommands = new Set(); // node-command ids currently executing
|
|
35
36
|
this._reloadInFlight = null; // serialize concurrent _reload() calls
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -46,15 +47,121 @@ class Daemon {
|
|
|
46
47
|
const n = nodeCfg.loadNode();
|
|
47
48
|
if (!n || !n.node_id || !n.token) return; // no node connected
|
|
48
49
|
if (!this._nodeClient) {
|
|
49
|
-
const { WorkspaceClient } = require('./workspace-client');
|
|
50
50
|
this._nodeClient = new WorkspaceClient(n.endpoint);
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
const info = { ...nodeCfg.gatherDeviceInfo(), agents: this._buildRoster() };
|
|
53
|
+
const resp = await this._nodeClient.nodeHeartbeat(n.node_id, n.token, info);
|
|
54
|
+
// The heartbeat response is our push channel: run any queued remote
|
|
55
|
+
// agent-management commands the workspace enqueued for this node. Fire
|
|
56
|
+
// them off without blocking the heartbeat loop (an install can take
|
|
57
|
+
// minutes — awaiting here would stall liveness and stack up commands).
|
|
58
|
+
const commands = (resp && resp.commands) || [];
|
|
59
|
+
for (const cmd of commands) {
|
|
60
|
+
const id = cmd.commandId;
|
|
61
|
+
if (!id || this._runningCommands.has(id)) continue;
|
|
62
|
+
this._runningCommands.add(id);
|
|
63
|
+
this._runNodeCommand(n, cmd).finally(() => this._runningCommands.delete(id));
|
|
64
|
+
}
|
|
53
65
|
} catch {
|
|
54
66
|
// ignore — will retry next interval
|
|
55
67
|
}
|
|
56
68
|
}
|
|
57
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Roster of agents this node hosts, for the workspace's node view. Sourced
|
|
72
|
+
* from config (the source of truth for what's configured) and augmented with
|
|
73
|
+
* live process state, so a removed agent drops off immediately rather than
|
|
74
|
+
* lingering as a stale 'stopped' process entry.
|
|
75
|
+
*/
|
|
76
|
+
_buildRoster() {
|
|
77
|
+
const roster = [];
|
|
78
|
+
try {
|
|
79
|
+
for (const a of this.config.getAgents()) {
|
|
80
|
+
const proc = this._processes[a.name];
|
|
81
|
+
roster.push({ name: a.name, type: a.type || 'unknown', status: (proc && proc.state) || 'stopped' });
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// best-effort
|
|
85
|
+
}
|
|
86
|
+
return roster;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Execute one remote command by shelling out to this same launcher's CLI, so
|
|
91
|
+
* all of create/install/connect/start/stop/remove reuses the exact code path a
|
|
92
|
+
* local `agn` invocation would — then report the outcome back to the workspace.
|
|
93
|
+
*/
|
|
94
|
+
async _runNodeCommand(n, cmd) {
|
|
95
|
+
const action = cmd.action;
|
|
96
|
+
const args = cmd.args || {};
|
|
97
|
+
const name = (args.name || '').trim();
|
|
98
|
+
let ok = false;
|
|
99
|
+
let message = '';
|
|
100
|
+
try {
|
|
101
|
+
if (action === 'create_agent') {
|
|
102
|
+
const type = (args.type || '').trim();
|
|
103
|
+
const r1 = await this._runAgn(['create', name, '--type', type, '--install']);
|
|
104
|
+
if (r1.code !== 0) throw new Error(r1.stderr || r1.stdout || 'create failed');
|
|
105
|
+
// Optional credentials for API-key agents (generic → provider mapping
|
|
106
|
+
// happens in env resolution).
|
|
107
|
+
if (args.apiKey) await this._runAgn(['env', type, '--set', `LLM_API_KEY=${args.apiKey}`]);
|
|
108
|
+
if (args.model) await this._runAgn(['env', type, '--set', `LLM_MODEL=${args.model}`]);
|
|
109
|
+
if (args.baseUrl) await this._runAgn(['env', type, '--set', `LLM_BASE_URL=${args.baseUrl}`]);
|
|
110
|
+
// Attach to this node's workspace so the agent shows up as a member.
|
|
111
|
+
const r2 = await this._runAgn(['connect', name, n.token, '--endpoint', n.endpoint]);
|
|
112
|
+
if (r2.code !== 0) throw new Error(r2.stderr || r2.stdout || 'connect failed');
|
|
113
|
+
ok = true;
|
|
114
|
+
message = `Agent '${name}' created`;
|
|
115
|
+
} else if (action === 'start_agent') {
|
|
116
|
+
const r = await this._runAgn(['start', name]);
|
|
117
|
+
ok = r.code === 0;
|
|
118
|
+
message = ok ? `Agent '${name}' started` : (r.stderr || r.stdout || 'start failed');
|
|
119
|
+
} else if (action === 'stop_agent') {
|
|
120
|
+
const r = await this._runAgn(['stop', name]);
|
|
121
|
+
ok = r.code === 0;
|
|
122
|
+
message = ok ? `Agent '${name}' stopped` : (r.stderr || r.stdout || 'stop failed');
|
|
123
|
+
} else if (action === 'remove_agent') {
|
|
124
|
+
const r = await this._runAgn(['remove', name]);
|
|
125
|
+
ok = r.code === 0;
|
|
126
|
+
message = ok ? `Agent '${name}' removed` : (r.stderr || r.stdout || 'remove failed');
|
|
127
|
+
} else {
|
|
128
|
+
message = `Unknown action '${action}'`;
|
|
129
|
+
}
|
|
130
|
+
} catch (e) {
|
|
131
|
+
ok = false;
|
|
132
|
+
message = e.message || String(e);
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
await this._nodeClient.nodeCommandResult(cmd.commandId, n.token, { ok, message });
|
|
136
|
+
} catch {
|
|
137
|
+
// best-effort; the command stays 'running' if we can't report back
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Run this launcher's own CLI as a child process, capturing output. */
|
|
142
|
+
_runAgn(cliArgs) {
|
|
143
|
+
return new Promise((resolve) => {
|
|
144
|
+
let bin;
|
|
145
|
+
try {
|
|
146
|
+
bin = require.resolve('../bin/agent-connector.js');
|
|
147
|
+
} catch {
|
|
148
|
+
bin = process.argv[1];
|
|
149
|
+
}
|
|
150
|
+
const child = spawn(process.execPath, [bin, ...cliArgs], {
|
|
151
|
+
env: { ...getEnhancedEnv(), OPENAGENTS_SKIP_UPDATE_CHECK: '1' },
|
|
152
|
+
// Run from home so a created agent's default working dir is sensible
|
|
153
|
+
// (not the ~/.openagents config dir).
|
|
154
|
+
cwd: os.homedir(),
|
|
155
|
+
});
|
|
156
|
+
let stdout = '';
|
|
157
|
+
let stderr = '';
|
|
158
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
159
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
160
|
+
child.on('error', (e) => resolve({ code: 1, stdout, stderr: stderr || e.message }));
|
|
161
|
+
child.on('close', (code) => resolve({ code, stdout, stderr }));
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
58
165
|
// ---------------------------------------------------------------------------
|
|
59
166
|
// Public API
|
|
60
167
|
// ---------------------------------------------------------------------------
|
|
@@ -113,9 +220,11 @@ class Daemon {
|
|
|
113
220
|
this._processCommands();
|
|
114
221
|
}, 200);
|
|
115
222
|
|
|
116
|
-
// Device-level heartbeat (connect-a-node), independent of agents.
|
|
223
|
+
// Device-level heartbeat (connect-a-node), independent of agents. Also the
|
|
224
|
+
// delivery channel for remote agent-management commands, so keep it brisk
|
|
225
|
+
// (10s) — the request is tiny and it bounds remote-command pickup latency.
|
|
117
226
|
this._nodeHeartbeat();
|
|
118
|
-
this._nodeHeartbeatInterval = setInterval(() => this._nodeHeartbeat(),
|
|
227
|
+
this._nodeHeartbeatInterval = setInterval(() => this._nodeHeartbeat(), 10000);
|
|
119
228
|
|
|
120
229
|
// Watch config file for hot-reload
|
|
121
230
|
this._watchConfig();
|
package/src/workspace-client.js
CHANGED
|
@@ -133,10 +133,24 @@ class WorkspaceClient {
|
|
|
133
133
|
if (deviceInfo.deviceType) body.device_type = deviceInfo.deviceType;
|
|
134
134
|
if (deviceInfo.os) body.os = deviceInfo.os;
|
|
135
135
|
if (deviceInfo.launcherVersion) body.launcher_version = deviceInfo.launcherVersion;
|
|
136
|
+
if (Array.isArray(deviceInfo.agents)) body.agents = deviceInfo.agents;
|
|
136
137
|
const data = await this._post('/v1/nodes/heartbeat', body, this._wsHeaders(token));
|
|
137
138
|
return data.data || data;
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Report the outcome of a remote node command via
|
|
143
|
+
* POST /v1/nodes/commands/{id}/result (authenticated by the workspace token).
|
|
144
|
+
*/
|
|
145
|
+
async nodeCommandResult(commandId, token, { ok, message } = {}) {
|
|
146
|
+
const data = await this._post(
|
|
147
|
+
`/v1/nodes/commands/${commandId}/result`,
|
|
148
|
+
{ ok: !!ok, message: message || null },
|
|
149
|
+
this._wsHeaders(token),
|
|
150
|
+
);
|
|
151
|
+
return data.data || data;
|
|
152
|
+
}
|
|
153
|
+
|
|
140
154
|
/**
|
|
141
155
|
* Send heartbeat via POST /v1/heartbeat.
|
|
142
156
|
*
|