Mona commited on
Commit
4e94437
Β·
1 Parent(s): bf21785

feat: add agent daemon with cloud-brain reasoning loop

Browse files

Core 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.

Files changed (1) hide show
  1. src/agent.js +152 -24
src/agent.js CHANGED
@@ -1,34 +1,162 @@
1
- // The local agent loop. Reasoning is remote; execution + telemetry are local.
 
 
 
 
 
2
  import { think } from './cloud.js';
3
- import { TelemetryStream } from './telemetry.js';
 
 
 
 
 
 
 
 
 
4
 
5
- export class Agent {
6
  constructor(creds) {
7
- this.creds = creds;
8
- this.telemetry = new TelemetryStream(creds.apiKey, creds.agentId).connect();
9
- this.messages = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  }
11
 
12
- async run(task, onToken) {
13
- this.telemetry.step('task.start', { task });
14
- this.messages.push({ role: 'user', content: task });
 
 
 
 
 
 
 
 
 
 
15
 
16
  let answer = '';
17
- await think({
18
- apiKey: this.creds.apiKey,
19
- messages: this.messages,
20
- tools: [], // local tool schema can be advertised here later
21
- onChunk: (delta) => {
22
- answer += delta;
23
- onToken?.(delta);
24
- this.telemetry.send('agent.token', { delta });
25
- },
26
- });
27
-
28
- this.messages.push({ role: 'assistant', content: answer });
29
- this.telemetry.step('task.done', { chars: answer.length });
30
- return answer;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  }
32
 
33
- close() { this.telemetry.close(); }
 
 
 
 
 
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
  }