@getmegabrain/cli 0.1.7 → 0.1.9
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.
|
@@ -28,17 +28,25 @@ export class AgentSession {
|
|
|
28
28
|
agentContext;
|
|
29
29
|
cellCounter = 0;
|
|
30
30
|
tools;
|
|
31
|
-
|
|
31
|
+
messages;
|
|
32
|
+
constructor(model, tools, emit, agentContext = '', history = []) {
|
|
32
33
|
this.model = model;
|
|
33
34
|
this.emit = emit;
|
|
34
35
|
this.agentContext = agentContext;
|
|
35
36
|
this.tools = new Map(tools.map(t => [t.definition.name, t]));
|
|
37
|
+
this.messages = [...history];
|
|
38
|
+
}
|
|
39
|
+
/** The full model-visible message history (prior turns + this run) — persist to give the
|
|
40
|
+
* session memory across sends. */
|
|
41
|
+
getMessages() {
|
|
42
|
+
return this.messages;
|
|
36
43
|
}
|
|
37
44
|
async run(prompt) {
|
|
38
45
|
const system = this.agentContext
|
|
39
46
|
? `${SYSTEM_PROMPT}\n\nProject context: ${this.agentContext}`
|
|
40
47
|
: SYSTEM_PROMPT;
|
|
41
|
-
const messages =
|
|
48
|
+
const messages = this.messages;
|
|
49
|
+
messages.push({ role: 'user', content: [{ type: 'text', text: prompt }] });
|
|
42
50
|
const toolDefs = [...this.tools.values()].map(t => t.definition);
|
|
43
51
|
try {
|
|
44
52
|
for (let turn = 0; turn < MAX_TURNS; turn += 1) {
|
|
@@ -13,9 +13,14 @@ cell are visible in the next.
|
|
|
13
13
|
import contextlib
|
|
14
14
|
import io
|
|
15
15
|
import json
|
|
16
|
+
import os
|
|
16
17
|
import sys
|
|
17
18
|
import traceback
|
|
18
19
|
|
|
20
|
+
# Render matplotlib to a non-interactive backend so plt.show() never blocks; we capture
|
|
21
|
+
# figures ourselves after each cell (see emit_figures). Set before any user import.
|
|
22
|
+
os.environ.setdefault("MPLBACKEND", "Agg")
|
|
23
|
+
|
|
19
24
|
# Persistent namespace shared across all cells in this session.
|
|
20
25
|
NAMESPACE: dict = {"__name__": "__mb_science__"}
|
|
21
26
|
|
|
@@ -25,6 +30,30 @@ def emit(frame: dict) -> None:
|
|
|
25
30
|
sys.stdout.flush()
|
|
26
31
|
|
|
27
32
|
|
|
33
|
+
def emit_figures(cell_id: str) -> int:
|
|
34
|
+
"""Emit any open matplotlib figures as base64 PNG image frames, then close them.
|
|
35
|
+
|
|
36
|
+
This is what makes plots appear inline in the transcript (a science-notebook staple).
|
|
37
|
+
No-op if matplotlib hasn't been used in this session."""
|
|
38
|
+
pyplot = sys.modules.get("matplotlib.pyplot")
|
|
39
|
+
if pyplot is None:
|
|
40
|
+
return 0
|
|
41
|
+
import base64
|
|
42
|
+
|
|
43
|
+
count = 0
|
|
44
|
+
try:
|
|
45
|
+
for num in pyplot.get_fignums():
|
|
46
|
+
buf = io.BytesIO()
|
|
47
|
+
pyplot.figure(num).savefig(buf, format="png", bbox_inches="tight", dpi=110)
|
|
48
|
+
data = base64.b64encode(buf.getvalue()).decode("ascii")
|
|
49
|
+
emit({"type": "image", "cellId": cell_id, "mime": "image/png", "data": data})
|
|
50
|
+
count += 1
|
|
51
|
+
pyplot.close("all")
|
|
52
|
+
except Exception: # noqa: BLE001 - figure capture is best-effort
|
|
53
|
+
pass
|
|
54
|
+
return count
|
|
55
|
+
|
|
56
|
+
|
|
28
57
|
def run_cell(cell_id: str, code: str) -> None:
|
|
29
58
|
emit({"type": "status", "cellId": cell_id, "state": "busy"})
|
|
30
59
|
stdout, stderr = io.StringIO(), io.StringIO()
|
|
@@ -41,6 +70,11 @@ def run_cell(cell_id: str, code: str) -> None:
|
|
|
41
70
|
emit({"type": "stream", "cellId": cell_id, "kind": "stdout", "data": out})
|
|
42
71
|
if err:
|
|
43
72
|
emit({"type": "stream", "cellId": cell_id, "kind": "stderr", "data": err})
|
|
73
|
+
figures = emit_figures(cell_id)
|
|
74
|
+
if figures:
|
|
75
|
+
# A note in stdout so the (text-only) model knows a figure was produced.
|
|
76
|
+
emit({"type": "stream", "cellId": cell_id, "kind": "stdout",
|
|
77
|
+
"data": f"[{figures} figure(s) displayed]\n"})
|
|
44
78
|
emit({"type": "status", "cellId": cell_id, "state": "idle"})
|
|
45
79
|
|
|
46
80
|
|
package/dist/science/pages.js
CHANGED
|
@@ -307,6 +307,8 @@ export function sessionPage(projectId, sessionId) {
|
|
|
307
307
|
white-space:pre-wrap; overflow-x:auto; border-top:1px solid var(--border); }
|
|
308
308
|
.step .out { color:var(--muted); background:var(--bg); }
|
|
309
309
|
.step .out.err { color:#c0392b; }
|
|
310
|
+
img.figure { max-width:calc(100% - 24px); border-radius:8px; margin:10px 12px; display:block;
|
|
311
|
+
background:#fff; border:1px solid var(--border); }
|
|
310
312
|
/* Composer */
|
|
311
313
|
.composer { flex-shrink:0; border-top:1px solid var(--border); padding:14px 20px; }
|
|
312
314
|
.composer-inner { max-width:760px; margin:0 auto; display:flex; gap:10px; align-items:flex-end;
|
package/dist/science/server.js
CHANGED
|
@@ -224,11 +224,14 @@ export async function startScienceServer(host = '127.0.0.1') {
|
|
|
224
224
|
if (seg[0] === 'api' &&
|
|
225
225
|
seg[1] === 'sessions' &&
|
|
226
226
|
seg[2] &&
|
|
227
|
-
(seg[3] === 'events' || seg[3] === 'agent' || seg[3] === 'kernel')) {
|
|
227
|
+
(seg[3] === 'events' || seg[3] === 'agent' || seg[3] === 'kernel' || seg[3] === 'transcript')) {
|
|
228
228
|
const creds = readCredentials();
|
|
229
229
|
if (!creds)
|
|
230
230
|
return json(res, { error: 'signed-out' }, 401);
|
|
231
231
|
const sid = seg[2];
|
|
232
|
+
if (seg[3] === 'transcript' && method === 'GET') {
|
|
233
|
+
return json(res, store.getMessages(sid));
|
|
234
|
+
}
|
|
232
235
|
if (seg[3] === 'events' && method === 'GET') {
|
|
233
236
|
res.writeHead(200, {
|
|
234
237
|
'content-type': 'text/event-stream',
|
|
@@ -261,8 +264,10 @@ export async function startScienceServer(host = '127.0.0.1') {
|
|
|
261
264
|
const project = session ? store.getProject(session.projectId) : null;
|
|
262
265
|
store.touchSession(sid, session && !session.summary ? { summary: prompt } : undefined);
|
|
263
266
|
fanout(sid, 'agent', { type: 'user', text: prompt });
|
|
264
|
-
const agent = new AgentSession(new GatewayModel({ apiKey: creds.token }), [pythonTool(kernels.get(sid))], evt => fanout(sid, 'agent', evt), project?.agentContext ?? '');
|
|
265
|
-
|
|
267
|
+
const agent = new AgentSession(new GatewayModel({ apiKey: creds.token }), [pythonTool(kernels.get(sid))], evt => fanout(sid, 'agent', evt), project?.agentContext ?? '', store.getMessages(sid));
|
|
268
|
+
// Persist the full history when the turn ends, so the session remembers it and the
|
|
269
|
+
// transcript survives a refresh.
|
|
270
|
+
void agent.run(prompt).finally(() => store.setMessages(sid, agent.getMessages()));
|
|
266
271
|
return json(res, { ok: true });
|
|
267
272
|
}
|
|
268
273
|
}
|
|
@@ -64,12 +64,39 @@
|
|
|
64
64
|
if (stderr) out.classList.add('err');
|
|
65
65
|
scroll();
|
|
66
66
|
}
|
|
67
|
+
// Inline a rendered figure (matplotlib PNG) under the current step, or on its own if none.
|
|
68
|
+
function addFigure(mime, data) {
|
|
69
|
+
var img = el('img', 'figure');
|
|
70
|
+
img.src = 'data:' + mime + ';base64,' + data;
|
|
71
|
+
if (lastCard) lastCard.appendChild(img);
|
|
72
|
+
else {
|
|
73
|
+
var row = el('div', 'msg agent');
|
|
74
|
+
row.appendChild(img);
|
|
75
|
+
transcript.appendChild(row);
|
|
76
|
+
}
|
|
77
|
+
scroll();
|
|
78
|
+
}
|
|
67
79
|
|
|
68
80
|
function setRunning(on) {
|
|
69
81
|
sendBtn.disabled = on;
|
|
70
82
|
sendBtn.textContent = on ? 'Running…' : 'Send';
|
|
71
83
|
}
|
|
72
84
|
|
|
85
|
+
// Replay a persisted conversation (model messages) into the transcript on load.
|
|
86
|
+
function renderHistory(messages) {
|
|
87
|
+
messages.forEach(function (m) {
|
|
88
|
+
(m.content || []).forEach(function (b) {
|
|
89
|
+
if (m.role === 'user') {
|
|
90
|
+
if (b.type === 'text') addUser(b.text);
|
|
91
|
+
else if (b.type === 'tool_result') addToolResult(b.content, '');
|
|
92
|
+
} else {
|
|
93
|
+
if (b.type === 'text') addAgentText(b.text);
|
|
94
|
+
else if (b.type === 'tool_use') addToolCall(b.name, (b.input && b.input.code) || '');
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
73
100
|
// ── Sidebar: this project's sessions ────────────────────────────────
|
|
74
101
|
function loadSidebar() {
|
|
75
102
|
if (!pid) return;
|
|
@@ -97,6 +124,7 @@
|
|
|
97
124
|
if (m.channel === 'kernel') {
|
|
98
125
|
var f = m.payload;
|
|
99
126
|
if (f.type === 'status') kstate.textContent = f.state === 'busy' ? 'running' : f.state;
|
|
127
|
+
else if (f.type === 'image') addFigure(f.mime, f.data);
|
|
100
128
|
return;
|
|
101
129
|
}
|
|
102
130
|
var p = m.payload;
|
|
@@ -150,16 +178,27 @@
|
|
|
150
178
|
});
|
|
151
179
|
});
|
|
152
180
|
|
|
153
|
-
fetch('/api/sessions/' + sid)
|
|
181
|
+
fetch('/api/sessions/' + sid + '/transcript')
|
|
154
182
|
.then(function (r) {
|
|
155
183
|
return r.json();
|
|
156
184
|
})
|
|
157
|
-
.then(function (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
185
|
+
.then(function (msgs) {
|
|
186
|
+
var has = Array.isArray(msgs) && msgs.length > 0;
|
|
187
|
+
if (has) renderHistory(msgs);
|
|
188
|
+
return fetch('/api/sessions/' + sid)
|
|
189
|
+
.then(function (r) {
|
|
190
|
+
return r.json();
|
|
191
|
+
})
|
|
192
|
+
.then(function (s) {
|
|
193
|
+
if (s && s.title) $('session-title').textContent = s.title;
|
|
194
|
+
// Prefill the composer with the seeded first task only for a fresh session.
|
|
195
|
+
if (!has && s && (s.summary || s.title) && !composer.value)
|
|
196
|
+
composer.value = s.summary || s.title;
|
|
197
|
+
});
|
|
198
|
+
})
|
|
199
|
+
.catch(function () {})
|
|
200
|
+
.then(function () {
|
|
201
|
+
loadSidebar();
|
|
202
|
+
connect();
|
|
161
203
|
});
|
|
162
|
-
|
|
163
|
-
loadSidebar();
|
|
164
|
-
connect();
|
|
165
204
|
})();
|
package/dist/science/store.js
CHANGED
|
@@ -28,6 +28,8 @@ function defaultSettings() {
|
|
|
28
28
|
profile: '',
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
const MAX_MESSAGES = 60;
|
|
32
|
+
const MAX_TOOL_RESULT_CHARS = 4000;
|
|
31
33
|
const STORE_FILE = path.join(MEGABRAIN_CONFIG_DIR, 'science-workspace.json');
|
|
32
34
|
function id(prefix) {
|
|
33
35
|
return `${prefix}_${randomBytes(8).toString('hex')}`;
|
|
@@ -48,6 +50,7 @@ export class WorkspaceStore {
|
|
|
48
50
|
projects: parsed.projects ?? [],
|
|
49
51
|
sessions: parsed.sessions ?? [],
|
|
50
52
|
settings: parsed.settings,
|
|
53
|
+
transcripts: parsed.transcripts ?? {},
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
56
|
catch {
|
|
@@ -157,4 +160,19 @@ export class WorkspaceStore {
|
|
|
157
160
|
this.save();
|
|
158
161
|
return next;
|
|
159
162
|
}
|
|
163
|
+
// ── Per-session conversation history (agent memory + transcript replay) ──
|
|
164
|
+
getMessages(sessionId) {
|
|
165
|
+
return this.data.transcripts?.[sessionId] ?? [];
|
|
166
|
+
}
|
|
167
|
+
setMessages(sessionId, messages) {
|
|
168
|
+
// Cap history length and truncate large tool outputs so the on-disk workspace stays small.
|
|
169
|
+
const capped = messages.slice(-MAX_MESSAGES).map(m => ({
|
|
170
|
+
role: m.role,
|
|
171
|
+
content: m.content.map(b => b.type === 'tool_result' && b.content.length > MAX_TOOL_RESULT_CHARS
|
|
172
|
+
? { ...b, content: `${b.content.slice(0, MAX_TOOL_RESULT_CHARS)}\n…(truncated)` }
|
|
173
|
+
: b),
|
|
174
|
+
}));
|
|
175
|
+
(this.data.transcripts ??= {})[sessionId] = capped;
|
|
176
|
+
this.save();
|
|
177
|
+
}
|
|
160
178
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmegabrain/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "MegaBrain CLI — sign in once, then code from your terminal via OpenCode wired to the MegaBrain Gateway, or run `megabrain science` for the local research workbench.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|