@melaya/runner 1.0.78 → 1.0.81
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 +98 -12
- package/dist/connection.js +9 -0
- package/dist/modelLoader.d.ts +1 -0
- package/dist/modelLoader.js +6 -4
- package/package.json +1 -1
package/dist/assistantHost.py
CHANGED
|
@@ -37,6 +37,10 @@ import traceback
|
|
|
37
37
|
_EVENT_PREFIX = "MELASSIST "
|
|
38
38
|
_IDLE_EXIT_SECONDS = int(os.environ.get("MEL_ASSISTANT_IDLE_SECONDS", "900")) # 15 min
|
|
39
39
|
|
|
40
|
+
# Streaming state for the CURRENT turn — the pre_print / post_acting hooks read
|
|
41
|
+
# this to emit delta / tool events keyed to the turn in flight.
|
|
42
|
+
_stream = {"turnId": "", "lens": {}}
|
|
43
|
+
|
|
40
44
|
|
|
41
45
|
def _emit(turn_id: str, kind: str, **fields) -> None:
|
|
42
46
|
"""Write one structured event line to stdout (flushed) for the runner to relay."""
|
|
@@ -96,24 +100,51 @@ def _build_agent():
|
|
|
96
100
|
model = os.environ.get("MEL_ASSISTANT_MODEL", "") or None
|
|
97
101
|
language = os.environ.get("MEL_ASSISTANT_LANGUAGE", "en")
|
|
98
102
|
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
103
|
+
# Phone control is offered ONLY on the native mobile app surface — the same
|
|
104
|
+
# gate as the server-side melaya_run_phone_task tool. A desktop / web chat
|
|
105
|
+
# never gets the phone_* tools, even if a phone is paired.
|
|
106
|
+
phone_enabled = os.environ.get("MEL_ASSISTANT_SURFACE", "") == "mobile-native"
|
|
107
|
+
|
|
108
|
+
# Read-only platform tools (melaya_agent); phone control (Device Control) on
|
|
109
|
+
# mobile only. These POST to /api/v1/private/assistant-tool + /phone/command
|
|
110
|
+
# with MELAYA_API_KEY; tenant scope is enforced server-side.
|
|
111
|
+
categories = ["melaya_agent"] + (["phone"] if phone_enabled else [])
|
|
102
112
|
try:
|
|
103
|
-
toolkit = build_toolkit(categories=
|
|
113
|
+
toolkit = build_toolkit(categories=categories)
|
|
104
114
|
except Exception as exc:
|
|
105
|
-
_log(f"build_toolkit(
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
_log(f"build_toolkit({categories}) failed: {exc}; retrying melaya_agent only")
|
|
116
|
+
try:
|
|
117
|
+
toolkit = build_toolkit(categories=["melaya_agent"])
|
|
118
|
+
except Exception:
|
|
119
|
+
toolkit = build_toolkit(names=[])
|
|
120
|
+
|
|
121
|
+
phone_rule = (
|
|
122
|
+
"- If the user asks you to DO something on their phone (open an app, browse, "
|
|
123
|
+
"tap, type, comment, post), use the phone_* tools. ALWAYS call "
|
|
124
|
+
"phone_get_screen_tree before you tap or type. Publish comments/posts with "
|
|
125
|
+
"phone_post_comment / phone_create_post — every publish is approved by the "
|
|
126
|
+
"user ON THEIR PHONE, so never refuse for safety. Only touch apps the user "
|
|
127
|
+
"asked for; the phone enforces an allowlist and blocks the rest.\n"
|
|
128
|
+
"- You are FULLY AUTONOMOUS on the phone: NEVER ask the user a question, "
|
|
129
|
+
"never end your turn asking for confirmation or offering a choice — make the "
|
|
130
|
+
"reasonable decision and DO it. If a feed is algorithmic, use the app's own "
|
|
131
|
+
"Search / Explore to find the right content yourself. Keep going until the "
|
|
132
|
+
"task is COMPLETE (e.g. all N comments posted) or you are genuinely blocked; "
|
|
133
|
+
"only then report what you did and what (if anything) is blocked.\n"
|
|
134
|
+
if phone_enabled else ""
|
|
135
|
+
)
|
|
108
136
|
sys_prompt = (
|
|
109
137
|
"You are the Melaya Assistant, the in-app copilot of the Melaya "
|
|
110
138
|
"agent-orchestration platform. You have READ-ONLY tools over the data "
|
|
111
139
|
"this user is allowed to see: pipelines, runs, LLM costs, plan usage, "
|
|
112
|
-
"validated workflow templates and eval results
|
|
113
|
-
"
|
|
140
|
+
"validated workflow templates and eval results"
|
|
141
|
+
+ (" — plus phone-control tools to drive the user's paired phone." if phone_enabled else ".")
|
|
142
|
+
+ "\nRules:\n"
|
|
114
143
|
"- Use tools to answer questions about the user's pipelines, spend, usage, "
|
|
115
|
-
"templates or evals — never invent numbers
|
|
116
|
-
"
|
|
144
|
+
"templates or evals — never invent numbers. For cost, melaya_cost_summary "
|
|
145
|
+
"supports dimension='pipeline' to find which pipeline cost the most.\n"
|
|
146
|
+
+ phone_rule
|
|
147
|
+
+ "- Be concise and concrete; format small tables or bullet lists when comparing items.\n"
|
|
117
148
|
"- If a question is outside the platform, answer normally without tools.\n"
|
|
118
149
|
+ (f"- Answer in the user's language: {language}.\n" if language and language != "en" else "")
|
|
119
150
|
)
|
|
@@ -125,18 +156,69 @@ def _build_agent():
|
|
|
125
156
|
model_name=model,
|
|
126
157
|
provider=provider,
|
|
127
158
|
api_key="", # model.py resolves the provider's own creds (OAuth off disk / localhost)
|
|
128
|
-
|
|
159
|
+
# Phone tasks are many-step (each comment ≈ screen_tree→tap→type→post), so
|
|
160
|
+
# give them room like the device template (40); read-only Q&A needs few.
|
|
161
|
+
max_iters=40 if phone_enabled else 8,
|
|
129
162
|
reliability=True,
|
|
130
163
|
bounded_memory=True,
|
|
131
164
|
)
|
|
165
|
+
_register_stream_hooks(agent)
|
|
132
166
|
return agent
|
|
133
167
|
|
|
134
168
|
|
|
169
|
+
def _register_stream_hooks(agent) -> None:
|
|
170
|
+
"""Emit delta / tool events live as the agent produces output (pre_print gives
|
|
171
|
+
cumulative snapshots per message; we forward the new suffix as a delta)."""
|
|
172
|
+
try:
|
|
173
|
+
from shared.runtime.events import _flatten_agent_text
|
|
174
|
+
except Exception:
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
def _pre_print(*_args, **kwargs) -> None:
|
|
178
|
+
try:
|
|
179
|
+
tid = _stream.get("turnId")
|
|
180
|
+
if not tid:
|
|
181
|
+
return
|
|
182
|
+
msg = kwargs.get("msg") or kwargs.get("message")
|
|
183
|
+
if msg is None:
|
|
184
|
+
return
|
|
185
|
+
mid = str(getattr(msg, "id", "") or id(msg))
|
|
186
|
+
text = _flatten_agent_text(getattr(msg, "content", msg))
|
|
187
|
+
if not text:
|
|
188
|
+
return
|
|
189
|
+
prev = _stream["lens"].get(mid, 0)
|
|
190
|
+
if len(text) > prev:
|
|
191
|
+
_emit(tid, "delta", content=text[prev:])
|
|
192
|
+
_stream["lens"][mid] = len(text)
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
def _post_acting(*_args, **kwargs) -> None:
|
|
197
|
+
try:
|
|
198
|
+
tid = _stream.get("turnId")
|
|
199
|
+
if not tid:
|
|
200
|
+
return
|
|
201
|
+
for tc in (kwargs.get("tool_calls") or []):
|
|
202
|
+
name = tc.get("name", "") if isinstance(tc, dict) else getattr(tc, "name", "")
|
|
203
|
+
if name:
|
|
204
|
+
_emit(tid, "tool", name=str(name))
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
for hook_type, fn in (("pre_print", _pre_print), ("post_acting", _post_acting)):
|
|
209
|
+
try:
|
|
210
|
+
agent.register_instance_hook(hook_type, fn)
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
|
|
135
215
|
def _run_turn(agent, turn_id: str, message: str) -> None:
|
|
136
216
|
import asyncio
|
|
137
217
|
from agentscope.message import Msg
|
|
138
218
|
|
|
139
219
|
_emit(turn_id, "round", n=1)
|
|
220
|
+
_stream["turnId"] = turn_id
|
|
221
|
+
_stream["lens"] = {}
|
|
140
222
|
|
|
141
223
|
async def _go():
|
|
142
224
|
return await agent(Msg("user", message, "user"))
|
|
@@ -145,10 +227,14 @@ def _run_turn(agent, turn_id: str, message: str) -> None:
|
|
|
145
227
|
result = asyncio.run(_go())
|
|
146
228
|
except Exception as exc:
|
|
147
229
|
_log("turn error:\n" + traceback.format_exc())
|
|
230
|
+
_stream["turnId"] = ""
|
|
148
231
|
_emit(turn_id, "error", message=str(exc) or "assistant turn failed")
|
|
149
232
|
_emit(turn_id, "done")
|
|
150
233
|
return
|
|
151
234
|
|
|
235
|
+
_stream["turnId"] = ""
|
|
236
|
+
# Authoritative final answer — the client swaps the streamed plain text for
|
|
237
|
+
# this markdown-rendered version.
|
|
152
238
|
text = _extract_text(result)
|
|
153
239
|
_emit(turn_id, "text", content=text)
|
|
154
240
|
_emit(turn_id, "done")
|
package/dist/connection.js
CHANGED
|
@@ -370,6 +370,10 @@ export async function connect(opts) {
|
|
|
370
370
|
MEL_MODEL_PARAMS_B: String(preflight.profile.paramsBillion ?? ""),
|
|
371
371
|
MEL_MODEL_MAX_TOKENS: String(preflight.profile.recommendedMaxTokens),
|
|
372
372
|
MEL_MODEL_MAX_ITERS: String(preflight.profile.recommendedMaxIters),
|
|
373
|
+
// Real context window → sizes BoundedMemory so compression can't
|
|
374
|
+
// evict the task brief on a big local model. Empty when unknown
|
|
375
|
+
// (the agent falls back to a safe per-tier floor).
|
|
376
|
+
MEL_MODEL_CONTEXT_TOKENS: String(preflight.profile.contextTokens ?? ""),
|
|
373
377
|
MEL_MODEL_DISABLE_THINKING: preflight.profile.thinkingDefault === "off" ? "1" : "0",
|
|
374
378
|
}
|
|
375
379
|
: {}),
|
|
@@ -714,6 +718,10 @@ export async function connect(opts) {
|
|
|
714
718
|
MEL_MODEL_PARAMS_B: String(preflight.profile.paramsBillion ?? ""),
|
|
715
719
|
MEL_MODEL_MAX_TOKENS: String(preflight.profile.recommendedMaxTokens),
|
|
716
720
|
MEL_MODEL_MAX_ITERS: String(preflight.profile.recommendedMaxIters),
|
|
721
|
+
// Real context window → sizes BoundedMemory so compression can't
|
|
722
|
+
// evict the task brief on a big local model. Empty when unknown
|
|
723
|
+
// (the agent falls back to a safe per-tier floor).
|
|
724
|
+
MEL_MODEL_CONTEXT_TOKENS: String(preflight.profile.contextTokens ?? ""),
|
|
717
725
|
MEL_MODEL_DISABLE_THINKING: preflight.profile.thinkingDefault === "off" ? "1" : "0",
|
|
718
726
|
}
|
|
719
727
|
: {}),
|
|
@@ -850,6 +858,7 @@ export async function connect(opts) {
|
|
|
850
858
|
MEL_ASSISTANT_PROVIDER: String(payload.provider || "claude_code"),
|
|
851
859
|
MEL_ASSISTANT_MODEL: String(payload.model || ""),
|
|
852
860
|
MEL_ASSISTANT_LANGUAGE: String(payload.language || "en"),
|
|
861
|
+
MEL_ASSISTANT_SURFACE: String(payload.surface || ""),
|
|
853
862
|
// Per-user creds (incl. MELAYA_API_KEY for the tenant-gated tool bridge).
|
|
854
863
|
...(payload.credentials || {}),
|
|
855
864
|
};
|
package/dist/modelLoader.d.ts
CHANGED
package/dist/modelLoader.js
CHANGED
|
@@ -120,11 +120,12 @@ function _parseParamCount(id) {
|
|
|
120
120
|
}
|
|
121
121
|
function _classify(id, lmstudioMeta) {
|
|
122
122
|
const lower = id.toLowerCase();
|
|
123
|
+
const ctxTokens = lmstudioMeta?.max_context_length ?? null;
|
|
123
124
|
// 1. Family override wins if matched.
|
|
124
125
|
for (const ov of FAMILY_OVERRIDES) {
|
|
125
126
|
if (ov.pattern.test(lower)) {
|
|
126
127
|
const params = _parseParamCount(id);
|
|
127
|
-
return _buildProfile(id, ov.tier, params, ov.family, ov.thinking, ov.reason);
|
|
128
|
+
return _buildProfile(id, ov.tier, params, ov.family, ov.thinking, ov.reason, ctxTokens);
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
// 2. LM Studio metadata if present.
|
|
@@ -160,9 +161,9 @@ function _classify(id, lmstudioMeta) {
|
|
|
160
161
|
tier = "text-only";
|
|
161
162
|
reason = `${params}B params < ${TIER_PARAM_THRESHOLDS.agenticMarginal}B threshold — text-only, abort if used for tool-using pipeline`;
|
|
162
163
|
}
|
|
163
|
-
return _buildProfile(id, tier, params, null, "n/a", reason);
|
|
164
|
+
return _buildProfile(id, tier, params, null, "n/a", reason, ctxTokens);
|
|
164
165
|
}
|
|
165
|
-
function _buildProfile(id, tier, paramsBillion, family, thinkingDefault, reason) {
|
|
166
|
+
function _buildProfile(id, tier, paramsBillion, family, thinkingDefault, reason, contextTokens = null) {
|
|
166
167
|
// Per-tier resource caps. Smaller models get tighter ceilings to
|
|
167
168
|
// bound runaway thinking loops.
|
|
168
169
|
const recommendedMaxTokens = tier === "agentic-capable" ? 8192
|
|
@@ -190,6 +191,7 @@ function _buildProfile(id, tier, paramsBillion, family, thinkingDefault, reason)
|
|
|
190
191
|
thinkingDefault,
|
|
191
192
|
recommendedMaxTokens,
|
|
192
193
|
recommendedMaxIters,
|
|
194
|
+
contextTokens: contextTokens && contextTokens > 0 ? contextTokens : null,
|
|
193
195
|
detectedAt: new Date().toISOString(),
|
|
194
196
|
reason,
|
|
195
197
|
};
|
|
@@ -233,7 +235,7 @@ export function classifyModel(id, lmstudioMeta) {
|
|
|
233
235
|
// Bump whenever FAMILY_OVERRIDES, tier thresholds, or any classifier
|
|
234
236
|
// logic changes in a way that could re-tier an existing cached model.
|
|
235
237
|
// Cached profiles with a mismatched version are ignored and re-classified.
|
|
236
|
-
const PROFILE_SCHEMA_VERSION =
|
|
238
|
+
const PROFILE_SCHEMA_VERSION = 4;
|
|
237
239
|
const LMSTUDIO_BASE = process.env.LMSTUDIO_BASE_URL || "http://127.0.0.1:1234";
|
|
238
240
|
const OLLAMA_BASE = process.env.OLLAMA_BASE_URL || "http://127.0.0.1:11434";
|
|
239
241
|
// JIT-load timeout. 8B-class quantised models on M-series Macs typically
|