@melaya/runner 1.0.103 → 1.0.105
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/dist/assistantHost.py +32 -7
- package/dist/connection.js +30 -4
- package/package.json +1 -1
package/dist/assistantHost.py
CHANGED
|
@@ -271,13 +271,25 @@ def _build_agent():
|
|
|
271
271
|
# then ALWAYS require the in-chat approval card (fail-safe).
|
|
272
272
|
if connector_services and not os.environ.get("MEL_ASSISTANT_CONNECTOR_HITL"):
|
|
273
273
|
os.environ["MEL_ASSISTANT_CONNECTOR_HITL"] = "1"
|
|
274
|
-
# Core primitives
|
|
275
|
-
#
|
|
276
|
-
#
|
|
277
|
-
#
|
|
278
|
-
#
|
|
279
|
-
#
|
|
280
|
-
|
|
274
|
+
# Core primitives: web_search / web_fetch, files, HTTP, data + office utilities,
|
|
275
|
+
# scraping, encoding, SQL, etc. — ALWAYS discoverable via search_tools/
|
|
276
|
+
# activate_tool. The side-effecting ones run on the USER's OWN machine (this
|
|
277
|
+
# runner) and are HITL-gated by the assistant's safe/payments modes, so exposing
|
|
278
|
+
# them for discovery is safe. They stay in the LAZY pool (not pinned) so the
|
|
279
|
+
# ~73-tool family never explodes the active budget.
|
|
280
|
+
#
|
|
281
|
+
# IMPORTANT: `melaya_core` is a CLIENT-SIDE DISPLAY bucket (toolServiceMap.ts),
|
|
282
|
+
# NOT a runtime registry category — the primitives actually live under `tools`,
|
|
283
|
+
# `scraping`, `msoffice`, `database`, … So we include the AUTHORITATIVE runtime
|
|
284
|
+
# category list (CORE_TOOL_CATEGORIES). Passing "melaya_core" matched NOTHING,
|
|
285
|
+
# which is why web_search/web_fetch never appeared in search_tools.
|
|
286
|
+
try:
|
|
287
|
+
from shared.runtime.registry import CORE_TOOL_CATEGORIES as core_categories
|
|
288
|
+
except Exception:
|
|
289
|
+
# Fallback if an older shared bundle predates the constant — hardcode the set.
|
|
290
|
+
core_categories = ["tools", "scraping", "data_utils", "msoffice", "aiml",
|
|
291
|
+
"media", "knowledge", "netutil_tools", "qr_tools",
|
|
292
|
+
"ics_tools"] # excluded (need a connector): video_pipeline, database, devops
|
|
281
293
|
try:
|
|
282
294
|
from shared.orchestration.lazy_registry import build_lazy_toolkit
|
|
283
295
|
# Base tools (melaya_agent + phone) stay active+pinned; core + any selected
|
|
@@ -339,6 +351,18 @@ def _build_agent():
|
|
|
339
351
|
"data — read it from the connector.\n"
|
|
340
352
|
if connector_services else ""
|
|
341
353
|
)
|
|
354
|
+
# Core primitives are ALWAYS in the lazy pool now, so the model must be told
|
|
355
|
+
# they exist (they are not pinned/loaded upfront) — otherwise it concludes "no
|
|
356
|
+
# web-search tool is available" and refuses, exactly the reported failure.
|
|
357
|
+
core_rule = (
|
|
358
|
+
"- You have built-in CORE tools that are NOT loaded upfront: web_search "
|
|
359
|
+
"(live web search) and web_fetch (fetch a URL's content), plus file read/"
|
|
360
|
+
"write, HTTP requests, scraping, data/CSV/Excel utilities, SQL, QR and media. "
|
|
361
|
+
"To use any of them, call search_tools(query=...) (e.g. search_tools(\"web "
|
|
362
|
+
"search\")), then activate_tool(name=\"web_search\"), then call it. Whenever a "
|
|
363
|
+
"question needs live/current web data or a page's contents, use web_search / "
|
|
364
|
+
"web_fetch — do NOT claim you lack a web tool.\n"
|
|
365
|
+
)
|
|
342
366
|
sys_prompt = (
|
|
343
367
|
"You are the Melaya Assistant, the in-app copilot of the Melaya "
|
|
344
368
|
"agent-orchestration platform. You have READ-ONLY tools over the data "
|
|
@@ -351,6 +375,7 @@ def _build_agent():
|
|
|
351
375
|
"supports dimension='pipeline' to find which pipeline cost the most.\n"
|
|
352
376
|
+ phone_rule
|
|
353
377
|
+ connector_rule
|
|
378
|
+
+ core_rule
|
|
354
379
|
+ "- Be concise and concrete; format small tables or bullet lists when comparing items.\n"
|
|
355
380
|
"- If a question is outside the platform, answer normally without tools.\n"
|
|
356
381
|
+ (f"- Answer in the user's language: {language}.\n" if language and language != "en" else "")
|
package/dist/connection.js
CHANGED
|
@@ -14,7 +14,7 @@ import { io } from "socket.io-client";
|
|
|
14
14
|
import chalk from "chalk";
|
|
15
15
|
import ora from "ora";
|
|
16
16
|
import { spawn } from "child_process";
|
|
17
|
-
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
17
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
|
|
18
18
|
import { dirname, join } from "path";
|
|
19
19
|
import { fileURLToPath } from "url";
|
|
20
20
|
import { tmpdir, homedir } from "os";
|
|
@@ -28,6 +28,19 @@ import { ensureSharedModules, getSharedDir } from "./sharedVendor.js";
|
|
|
28
28
|
import { startLumaBrowserBridge } from "./lumaBrowserBridge.js";
|
|
29
29
|
const HEARTBEAT_INTERVAL = 30_000;
|
|
30
30
|
const activeProcesses = new Map();
|
|
31
|
+
// PR4/P1-5: the assistant-session wire protocol version. Bumped whenever the
|
|
32
|
+
// handshake contract changes (this rev adds hostInstanceId + configHash + a
|
|
33
|
+
// versioned ready/hello). The server enforces a MINIMUM: a runner advertising a
|
|
34
|
+
// lower protocol never gets an assistant session routed to it (it gets a clear
|
|
35
|
+
// "update your runner" instead of a silent stale-memory turn).
|
|
36
|
+
const ASSISTANT_PROTOCOL_VERSION = 2;
|
|
37
|
+
// Best-effort read of our own package version (mirrors cli.ts). Advertised in the
|
|
38
|
+
// hello so the server can log / gate on a package floor too.
|
|
39
|
+
let RUNNER_VERSION = "0.0.0";
|
|
40
|
+
try {
|
|
41
|
+
RUNNER_VERSION = String(JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version || "0.0.0");
|
|
42
|
+
}
|
|
43
|
+
catch { /* dev tree layout differs — non-fatal */ }
|
|
31
44
|
const activeAssistants = new Map();
|
|
32
45
|
// PR4: sid-keyed boot reservation. The guard-then-spawn in runner:assistant_start
|
|
33
46
|
// has an async gap (ensureSharedModules / ensurePythonEnv awaits) between the
|
|
@@ -133,7 +146,7 @@ export async function connect(opts) {
|
|
|
133
146
|
// Advertise capabilities so the server only routes assistant chat sessions
|
|
134
147
|
// to runners new enough to host them (older runners never get one → the
|
|
135
148
|
// server falls back with a clear "update your runner" instead of hanging).
|
|
136
|
-
socket.emit("runner:hello", { capabilities: ["assistant_session"], package: "@melaya/runner" });
|
|
149
|
+
socket.emit("runner:hello", { capabilities: ["assistant_session"], package: "@melaya/runner", version: RUNNER_VERSION, assistantProtocol: ASSISTANT_PROTOCOL_VERSION });
|
|
137
150
|
// Start local event relay
|
|
138
151
|
if (!relay) {
|
|
139
152
|
relay = await startLocalRelay(socket, opts.verbose, opts.serverUrl);
|
|
@@ -894,7 +907,7 @@ export async function connect(opts) {
|
|
|
894
907
|
// that booted under an older generation and reboot it instead of serving the
|
|
895
908
|
// next turn from stale memory/config. A bare ready hid this.
|
|
896
909
|
if (live) {
|
|
897
|
-
emitEv({ kind: "ready", memoryWatermark: live.memoryWatermark || 0, generation: live.generation });
|
|
910
|
+
emitEv({ kind: "ready", memoryWatermark: live.memoryWatermark || 0, generation: live.generation, hostInstanceId: live.hostInstanceId, configHash: live.configHash, protocol: ASSISTANT_PROTOCOL_VERSION });
|
|
898
911
|
return;
|
|
899
912
|
}
|
|
900
913
|
// A boot is already in flight for this sid (concurrent start / server restart
|
|
@@ -993,7 +1006,10 @@ export async function connect(opts) {
|
|
|
993
1006
|
...(payload.credentials || {}),
|
|
994
1007
|
};
|
|
995
1008
|
const proc = spawn(envResult.pythonPath, ["-u", stagedHost], { cwd: workDir, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
996
|
-
|
|
1009
|
+
// hostInstanceId + configHash are filled from the host's OWN `ready` frame
|
|
1010
|
+
// (assistantHost.py _HOST_ID / _config_hash) as it boots — authoritative and
|
|
1011
|
+
// server-aligned. Start empty; the stdout parser captures them.
|
|
1012
|
+
const session = { proc, lastActivity: Date.now(), stdoutBuf: "", generation, memoryWatermark: 0, hostInstanceId: "", configHash: "" };
|
|
997
1013
|
activeAssistants.set(sid, session);
|
|
998
1014
|
console.log(chalk.hex("#7c6ff0")(` ◆ Assistant session ${sid.slice(0, 10)}… (${payload.provider})`));
|
|
999
1015
|
proc.stdout?.on("data", (data) => {
|
|
@@ -1011,6 +1027,16 @@ export async function connect(opts) {
|
|
|
1011
1027
|
// truthful watermark and the server skips a needless rehydrate.
|
|
1012
1028
|
if (typeof parsed?.memoryWatermark === "number")
|
|
1013
1029
|
session.memoryWatermark = parsed.memoryWatermark;
|
|
1030
|
+
// CAPTURE the host's authoritative identity off its `ready` frame
|
|
1031
|
+
// (_HOST_ID / _config_hash) so the live-host ready path can re-report
|
|
1032
|
+
// them without the host re-emitting; ADD only the wire protocol.
|
|
1033
|
+
if (parsed?.kind === "ready") {
|
|
1034
|
+
if (typeof parsed.hostInstanceId === "string")
|
|
1035
|
+
session.hostInstanceId = parsed.hostInstanceId;
|
|
1036
|
+
if (typeof parsed.configHash === "string")
|
|
1037
|
+
session.configHash = parsed.configHash;
|
|
1038
|
+
parsed.protocol = ASSISTANT_PROTOCOL_VERSION;
|
|
1039
|
+
}
|
|
1014
1040
|
socket.emit("runner:assistant_event", { sessionId: sid, ...parsed });
|
|
1015
1041
|
}
|
|
1016
1042
|
catch { /* skip malformed */ }
|