@getmegabrain/cli 0.1.12 → 0.1.14
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.
|
@@ -22,7 +22,18 @@ cells and everything runs locally on the user's machine. Use run_python to write
|
|
|
22
22
|
code and iterate on its output, and search_literature to find and cite real papers (arXiv,
|
|
23
23
|
OpenAlex, PubMed) when the task calls for surveying prior work. Prefer small, verifiable
|
|
24
24
|
steps. When you have the answer, stop calling tools and give a concise final summary of
|
|
25
|
-
what you did and found
|
|
25
|
+
what you did and found.
|
|
26
|
+
|
|
27
|
+
Be proactive — act, don't stall. When a request is broad (e.g. "map the recent literature
|
|
28
|
+
of your subfield"), do NOT reply by asking the user to narrow it down. Infer a reasonable
|
|
29
|
+
scope from the project context below and immediately call search_literature (run several
|
|
30
|
+
focused queries if useful), then present what you found. Only ask a clarifying question
|
|
31
|
+
when you genuinely cannot proceed at all. "Your subfield", "your project", and similar
|
|
32
|
+
phrases refer to the project context below — not to you as an AI; you have no personal
|
|
33
|
+
research field, so interpret them as the project's topic.
|
|
34
|
+
|
|
35
|
+
You retain this session's full message history, so you DO remember earlier turns in this
|
|
36
|
+
conversation — never claim you have no memory of what was said earlier in the session.`;
|
|
26
37
|
const MAX_TURNS = 16;
|
|
27
38
|
export class AgentSession {
|
|
28
39
|
model;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the project-grounding context string injected into the agent's system prompt.
|
|
3
|
+
* Grounding broad tasks ("map the recent literature of your subfield") in the project's
|
|
4
|
+
* name/description and the researcher profile is what lets the agent resolve "your subfield"
|
|
5
|
+
* to a concrete topic instead of stalling to ask the user.
|
|
6
|
+
*/
|
|
7
|
+
export function buildAgentContext(input) {
|
|
8
|
+
const clean = (v) => (v ?? '').trim();
|
|
9
|
+
const name = clean(input.projectName);
|
|
10
|
+
const description = clean(input.projectDescription);
|
|
11
|
+
const context = clean(input.agentContext);
|
|
12
|
+
const profile = clean(input.profile);
|
|
13
|
+
return [
|
|
14
|
+
name ? `Project: ${name}` : '',
|
|
15
|
+
description ? `Project description: ${description}` : '',
|
|
16
|
+
context,
|
|
17
|
+
profile ? `Researcher profile: ${profile}` : '',
|
|
18
|
+
]
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.join('\n');
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A per-key serial task queue. Tasks enqueued under the same key run strictly one after
|
|
3
|
+
* another (in enqueue order); tasks under different keys run independently.
|
|
4
|
+
*
|
|
5
|
+
* The Science daemon uses this to serialize agent turns per session: without it, sending
|
|
6
|
+
* several messages in quick succession spawns overlapping AgentSessions that share one
|
|
7
|
+
* kernel and race on the persisted transcript — so turns clobber each other and messages
|
|
8
|
+
* appear "unseen". Serializing means each turn starts only after the previous finished and
|
|
9
|
+
* therefore reads the prior turn's saved history.
|
|
10
|
+
*/
|
|
11
|
+
export function createKeyedQueue() {
|
|
12
|
+
const tails = new Map();
|
|
13
|
+
return function enqueue(key, task) {
|
|
14
|
+
const prev = tails.get(key) ?? Promise.resolve();
|
|
15
|
+
// Run `task` after the previous one settles — on both fulfill and reject, so a failed
|
|
16
|
+
// turn never blocks the queue.
|
|
17
|
+
const result = prev.then(task, task);
|
|
18
|
+
// The tail we chain the *next* task onto must never reject (that would skip the catch
|
|
19
|
+
// handler on subsequent tasks); swallow it here. Callers still see `result`'s outcome.
|
|
20
|
+
tails.set(key, result.catch(() => undefined));
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
23
|
+
}
|
package/dist/science/server.js
CHANGED
|
@@ -9,11 +9,13 @@ import { homePage, loginPage, nonceInvalidPage, nonceLandingPage, projectPage, s
|
|
|
9
9
|
import { FEATURED_CONNECTORS, NETWORK_CATEGORIES, WorkspaceStore } from './store.js';
|
|
10
10
|
import { KernelManager } from './kernel.js';
|
|
11
11
|
import { AgentSession, pythonTool } from './agent/agent.js';
|
|
12
|
+
import { buildAgentContext } from './agent/context.js';
|
|
12
13
|
import { ArxivClient, literatureTool, MultiSourceClient } from './agent/literature.js';
|
|
13
14
|
import { OpenAlexClient } from './agent/openalex.js';
|
|
14
15
|
import { PubMedClient } from './agent/pubmed.js';
|
|
15
16
|
import { GatewayModel } from './agent/model.js';
|
|
16
17
|
import { DEFAULT_MODEL, fetchPickerModels } from './agent/models.js';
|
|
18
|
+
import { createKeyedQueue } from './queue.js';
|
|
17
19
|
/**
|
|
18
20
|
* The `megabrain science` local daemon's HTTP server. Serves the sign-in hand-off on
|
|
19
21
|
* localhost (docs/megabrain-science/reference/claude-science-flow.md §1.1) and, once signed
|
|
@@ -34,6 +36,9 @@ export async function startScienceServer(host = '127.0.0.1') {
|
|
|
34
36
|
// flow to every browser watching a session, tagged with a `channel`.
|
|
35
37
|
const sseClients = new Map();
|
|
36
38
|
const kernelHooked = new Set();
|
|
39
|
+
// Serialize agent turns per session so rapid messages are handled in order (each seeing the
|
|
40
|
+
// previous turn's history) instead of racing on the shared kernel and transcript.
|
|
41
|
+
const agentQueue = createKeyedQueue();
|
|
37
42
|
const fanout = (sid, channel, payload) => {
|
|
38
43
|
const set = sseClients.get(sid);
|
|
39
44
|
if (!set)
|
|
@@ -273,17 +278,36 @@ export async function startScienceServer(host = '127.0.0.1') {
|
|
|
273
278
|
if (!prompt)
|
|
274
279
|
return json(res, { error: 'prompt-required' }, 400);
|
|
275
280
|
const model = str(b.model)?.trim() || DEFAULT_MODEL;
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
281
|
+
const token = creds.token;
|
|
282
|
+
// Echo the user's message immediately so it shows up in order, then run the turn on the
|
|
283
|
+
// per-session queue. The turn reads history *at run time* so it includes any turn that
|
|
284
|
+
// was still in flight when this message arrived.
|
|
279
285
|
fanout(sid, 'agent', { type: 'user', text: prompt });
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
286
|
+
void agentQueue(sid, async () => {
|
|
287
|
+
const session = store.getSession(sid);
|
|
288
|
+
const project = session ? store.getProject(session.projectId) : null;
|
|
289
|
+
store.touchSession(sid, session && !session.summary ? { summary: prompt } : undefined);
|
|
290
|
+
// Ground the agent in the project + researcher profile so broad tasks like
|
|
291
|
+
// "map the recent literature of your subfield" resolve to a concrete topic.
|
|
292
|
+
const agentContext = buildAgentContext({
|
|
293
|
+
projectName: project?.name,
|
|
294
|
+
projectDescription: project?.description,
|
|
295
|
+
agentContext: project?.agentContext,
|
|
296
|
+
profile: store.getSettings().profile,
|
|
297
|
+
});
|
|
298
|
+
const agent = new AgentSession(new GatewayModel({ apiKey: token, model }), [
|
|
299
|
+
pythonTool(kernels.get(sid)),
|
|
300
|
+
literatureTool(new MultiSourceClient([new ArxivClient(), new OpenAlexClient(), new PubMedClient()])),
|
|
301
|
+
], evt => fanout(sid, 'agent', evt), agentContext, store.getMessages(sid));
|
|
302
|
+
try {
|
|
303
|
+
await agent.run(prompt);
|
|
304
|
+
}
|
|
305
|
+
finally {
|
|
306
|
+
// Persist the full history so the session remembers it and the transcript survives
|
|
307
|
+
// a refresh — and so the next queued turn reads it as its starting point.
|
|
308
|
+
store.setMessages(sid, agent.getMessages());
|
|
309
|
+
}
|
|
310
|
+
});
|
|
287
311
|
return json(res, { ok: true });
|
|
288
312
|
}
|
|
289
313
|
}
|
|
@@ -79,9 +79,13 @@
|
|
|
79
79
|
scroll();
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// Track how many turns are in flight. The send button stays enabled so you can queue
|
|
83
|
+
// follow-up messages while the agent works — the server runs them in order per session.
|
|
84
|
+
var pending = 0;
|
|
82
85
|
function setRunning(on) {
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
pending += on ? 1 : -1;
|
|
87
|
+
if (pending < 0) pending = 0;
|
|
88
|
+
sendBtn.textContent = pending > 0 ? 'Working…' : 'Send';
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
// Replay a persisted conversation (model messages) into the transcript on load.
|
|
@@ -183,7 +187,7 @@
|
|
|
183
187
|
|
|
184
188
|
function send() {
|
|
185
189
|
var text = composer.value.trim();
|
|
186
|
-
if (!text
|
|
190
|
+
if (!text) return;
|
|
187
191
|
composer.value = '';
|
|
188
192
|
setRunning(true);
|
|
189
193
|
fetch('/api/sessions/' + sid + '/agent', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmegabrain/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
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",
|