Mona commited on
Commit Β·
4e94437
1
Parent(s): bf21785
feat: add agent daemon with cloud-brain reasoning loop
Browse filesCore daemon orchestrating commands from the control plane:
- Command dispatcher for run, tool, ping, and reset actions
- Cloud reasoning via streaming SSE with token emission
- Tool execution with per-action lifecycle events
- Task statistics tracking (tasks, tokens, tool calls, errors)
- EventEmitter for TUI/headless integration
The daemon receives commands, delegates reasoning to the cloud,
executes local tools, and streams everything back upstream.
- src/agent.js +152 -24
src/agent.js
CHANGED
|
@@ -1,34 +1,162 @@
|
|
| 1 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import { think } from './cloud.js';
|
| 3 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
export class Agent {
|
| 6 |
constructor(creds) {
|
| 7 |
-
|
| 8 |
-
this.
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
}
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
let answer = '';
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
|
|
|
| 1 |
+
// Agent daemon runtime β the core loop.
|
| 2 |
+
// Receives commands from the website via the control channel,
|
| 3 |
+
// delegates reasoning to the cloud brain, executes local tools,
|
| 4 |
+
// and streams everything back.
|
| 5 |
+
|
| 6 |
+
import { EventEmitter } from 'node:events';
|
| 7 |
import { think } from './cloud.js';
|
| 8 |
+
import { ControlChannel } from './control.js';
|
| 9 |
+
import { tools } from './tools/index.js';
|
| 10 |
+
import { log } from './log.js';
|
| 11 |
+
|
| 12 |
+
export class AgentDaemon extends EventEmitter {
|
| 13 |
+
#creds;
|
| 14 |
+
#control;
|
| 15 |
+
#messages = [];
|
| 16 |
+
#stats = { tasks: 0, tokens: 0, toolCalls: 0, errors: 0 };
|
| 17 |
+
#currentTask = null;
|
| 18 |
|
|
|
|
| 19 |
constructor(creds) {
|
| 20 |
+
super();
|
| 21 |
+
this.#creds = creds;
|
| 22 |
+
|
| 23 |
+
this.#control = new ControlChannel(creds.apiKey, creds.agentId);
|
| 24 |
+
|
| 25 |
+
// Forward control events
|
| 26 |
+
this.#control.on('connected', () => this.emit('connected'));
|
| 27 |
+
this.#control.on('disconnected', (c) => this.emit('disconnected', c));
|
| 28 |
+
this.#control.on('metrics', (m) => this.emit('metrics', m));
|
| 29 |
+
this.#control.on('error', (err) => this.emit('error', err));
|
| 30 |
+
this.#control.on('command', (cmd) => this.#onCommand(cmd));
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/** Start the daemon β connect to cloud and begin accepting commands. */
|
| 34 |
+
start() {
|
| 35 |
+
log.info(`Agent starting`, { agentId: this.#creds.agentId });
|
| 36 |
+
log.info(`Tools: ${tools.names().join(', ')}`);
|
| 37 |
+
this.#control.connect();
|
| 38 |
+
return this;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
get stats() { return { ...this.#stats }; }
|
| 42 |
+
get currentTask() { return this.#currentTask; }
|
| 43 |
+
get connected() { return this.#control.connected; }
|
| 44 |
+
|
| 45 |
+
// ββ Command dispatcher ββββββββββββββββββββββββββββββββββββββββββ
|
| 46 |
+
async #onCommand(cmd) {
|
| 47 |
+
const { runId, action, payload } = cmd;
|
| 48 |
+
try {
|
| 49 |
+
switch (action) {
|
| 50 |
+
case 'run':
|
| 51 |
+
await this.#runTask(payload?.task, runId);
|
| 52 |
+
break;
|
| 53 |
+
|
| 54 |
+
case 'tool':
|
| 55 |
+
await this.#runTool(payload?.tool, payload?.args, runId);
|
| 56 |
+
break;
|
| 57 |
+
|
| 58 |
+
case 'ping':
|
| 59 |
+
this.#control.result(runId, { pong: true, ts: Date.now() });
|
| 60 |
+
break;
|
| 61 |
+
|
| 62 |
+
case 'reset':
|
| 63 |
+
this.#messages = [];
|
| 64 |
+
log.info('Conversation history cleared');
|
| 65 |
+
this.#control.result(runId, { ok: true, action: 'reset' });
|
| 66 |
+
break;
|
| 67 |
+
|
| 68 |
+
default:
|
| 69 |
+
log.warn(`Unknown action: ${action}`);
|
| 70 |
+
this.#control.result(runId, { error: `Unknown action: ${action}` });
|
| 71 |
+
}
|
| 72 |
+
} catch (err) {
|
| 73 |
+
this.#stats.errors++;
|
| 74 |
+
log.error(`${action} failed: ${err.message}`);
|
| 75 |
+
this.#control.result(runId, { error: err.message });
|
| 76 |
+
this.emit('error', err);
|
| 77 |
+
}
|
| 78 |
}
|
| 79 |
|
| 80 |
+
// ββ Task execution (cloud reasoning) ββββββββββββββββββββββββββββ
|
| 81 |
+
async #runTask(task, runId) {
|
| 82 |
+
if (!task) {
|
| 83 |
+
this.#control.result(runId, { error: 'No task provided' });
|
| 84 |
+
return;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
this.#currentTask = { task, runId, startedAt: Date.now(), tokens: 0 };
|
| 88 |
+
this.#control.step('task.start', { task, runId });
|
| 89 |
+
this.emit('task:start', this.#currentTask);
|
| 90 |
+
log.info(`Task: "${task.slice(0, 100)}"`);
|
| 91 |
+
|
| 92 |
+
this.#messages.push({ role: 'user', content: task });
|
| 93 |
|
| 94 |
let answer = '';
|
| 95 |
+
let tokenCount = 0;
|
| 96 |
+
|
| 97 |
+
try {
|
| 98 |
+
answer = await think({
|
| 99 |
+
apiKey: this.#creds.apiKey,
|
| 100 |
+
messages: this.#messages,
|
| 101 |
+
tools: tools.list(),
|
| 102 |
+
onChunk: (delta) => {
|
| 103 |
+
tokenCount++;
|
| 104 |
+
this.#control.token(delta, runId);
|
| 105 |
+
this.emit('task:token', delta, runId);
|
| 106 |
+
},
|
| 107 |
+
onUsage: (usage) => {
|
| 108 |
+
this.emit('task:usage', usage);
|
| 109 |
+
},
|
| 110 |
+
});
|
| 111 |
+
} catch (err) {
|
| 112 |
+
this.#stats.errors++;
|
| 113 |
+
this.#currentTask = null;
|
| 114 |
+
this.#control.step('task.error', { error: err.message });
|
| 115 |
+
this.#control.result(runId, { error: err.message });
|
| 116 |
+
this.emit('task:error', err);
|
| 117 |
+
log.error(`Think failed: ${err.message}`);
|
| 118 |
+
return;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
this.#messages.push({ role: 'assistant', content: answer });
|
| 122 |
+
this.#stats.tasks++;
|
| 123 |
+
this.#stats.tokens += tokenCount;
|
| 124 |
+
|
| 125 |
+
this.#currentTask = null;
|
| 126 |
+
this.#control.result(runId, { text: answer });
|
| 127 |
+
this.#control.step('task.done', { runId, tokens: tokenCount, chars: answer.length });
|
| 128 |
+
this.emit('task:done', { answer, tokens: tokenCount, runId });
|
| 129 |
+
log.info(`Task complete`, { tokens: tokenCount, chars: answer.length });
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
// ββ Tool execution ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 133 |
+
async #runTool(toolName, toolArgs, runId) {
|
| 134 |
+
if (!toolName) {
|
| 135 |
+
this.#control.result(runId, { error: 'No tool specified' });
|
| 136 |
+
return;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
this.#control.step('tool.start', { tool: toolName });
|
| 140 |
+
this.emit('tool:start', toolName, toolArgs);
|
| 141 |
+
|
| 142 |
+
const result = await tools.run(toolName, toolArgs || {});
|
| 143 |
+
this.#stats.toolCalls++;
|
| 144 |
+
|
| 145 |
+
this.#control.result(runId, result);
|
| 146 |
+
this.#control.step('tool.done', { tool: toolName });
|
| 147 |
+
this.emit('tool:done', toolName, result);
|
| 148 |
+
|
| 149 |
+
if (result.error) {
|
| 150 |
+
log.warn(`Tool ${toolName}: ${result.error}`);
|
| 151 |
+
} else {
|
| 152 |
+
log.info(`Tool ${toolName} done`);
|
| 153 |
+
}
|
| 154 |
}
|
| 155 |
|
| 156 |
+
// ββ Lifecycle βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 157 |
+
close() {
|
| 158 |
+
log.info('Agent shutting down');
|
| 159 |
+
this.#control.close();
|
| 160 |
+
this.emit('close');
|
| 161 |
+
}
|
| 162 |
}
|