@melaya/runner 1.0.83 → 1.0.84
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 +47 -4
- package/dist/connection.js +17 -0
- package/package.json +1 -1
package/dist/assistantHost.py
CHANGED
|
@@ -38,8 +38,12 @@ _EVENT_PREFIX = "MELASSIST "
|
|
|
38
38
|
_IDLE_EXIT_SECONDS = int(os.environ.get("MEL_ASSISTANT_IDLE_SECONDS", "900")) # 15 min
|
|
39
39
|
|
|
40
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
|
-
|
|
41
|
+
# this to emit delta / tool events keyed to the turn in flight. `cancel` is the
|
|
42
|
+
# STOP flag: set by the stdin-reader thread, polled by the running turn.
|
|
43
|
+
_stream = {"turnId": "", "lens": {}, "cancel": False}
|
|
44
|
+
|
|
45
|
+
# Sentinel returned by a turn's coroutine when the user pressed STOP.
|
|
46
|
+
_CANCELLED = object()
|
|
43
47
|
|
|
44
48
|
|
|
45
49
|
def _emit(turn_id: str, kind: str, **fields) -> None:
|
|
@@ -219,9 +223,24 @@ def _run_turn(agent, turn_id: str, message: str) -> None:
|
|
|
219
223
|
_emit(turn_id, "round", n=1)
|
|
220
224
|
_stream["turnId"] = turn_id
|
|
221
225
|
_stream["lens"] = {}
|
|
226
|
+
_stream["cancel"] = False # fresh turn — clear any stale STOP
|
|
222
227
|
|
|
228
|
+
# Run the agent as a cancellable task and poll the STOP flag (flipped by the
|
|
229
|
+
# stdin-reader thread when the user presses STOP in chat). asyncio.cancel()
|
|
230
|
+
# unwinds the agent loop wherever it is awaiting — including mid phone-command
|
|
231
|
+
# HTTP wait — so the run stops promptly instead of finishing the step first.
|
|
223
232
|
async def _go():
|
|
224
|
-
|
|
233
|
+
task = asyncio.ensure_future(agent(Msg("user", message, "user")))
|
|
234
|
+
while not task.done():
|
|
235
|
+
if _stream.get("cancel"):
|
|
236
|
+
task.cancel()
|
|
237
|
+
try:
|
|
238
|
+
await task
|
|
239
|
+
except BaseException: # CancelledError + any teardown error
|
|
240
|
+
pass
|
|
241
|
+
return _CANCELLED
|
|
242
|
+
await asyncio.sleep(0.12)
|
|
243
|
+
return task.result()
|
|
225
244
|
|
|
226
245
|
try:
|
|
227
246
|
result = asyncio.run(_go())
|
|
@@ -232,6 +251,13 @@ def _run_turn(agent, turn_id: str, message: str) -> None:
|
|
|
232
251
|
_emit(turn_id, "done")
|
|
233
252
|
return
|
|
234
253
|
|
|
254
|
+
if result is _CANCELLED:
|
|
255
|
+
_stream["turnId"] = ""
|
|
256
|
+
_log("turn cancelled by user STOP")
|
|
257
|
+
_emit(turn_id, "text", content="Stopped.")
|
|
258
|
+
_emit(turn_id, "done")
|
|
259
|
+
return
|
|
260
|
+
|
|
235
261
|
_stream["turnId"] = ""
|
|
236
262
|
# Authoritative final answer — the client swaps the streamed plain text for
|
|
237
263
|
# this markdown-rendered version.
|
|
@@ -242,9 +268,26 @@ def _run_turn(agent, turn_id: str, message: str) -> None:
|
|
|
242
268
|
|
|
243
269
|
def _stdin_reader(q: "queue.Queue[str]") -> None:
|
|
244
270
|
"""Feed stdin lines into a queue so the main loop can apply an idle timeout
|
|
245
|
-
(blocking readline can't be interrupted portably).
|
|
271
|
+
(blocking readline can't be interrupted portably).
|
|
272
|
+
|
|
273
|
+
STOP is special: a `{"cancel": true}` line must take effect WHILE a turn is
|
|
274
|
+
running, but the main thread is blocked in _run_turn and won't drain the
|
|
275
|
+
queue until it finishes. So handle cancel HERE, on the reader thread — flip
|
|
276
|
+
the shared flag the running turn polls — and never enqueue it as a new turn.
|
|
277
|
+
"""
|
|
246
278
|
try:
|
|
247
279
|
for line in sys.stdin:
|
|
280
|
+
s = line.strip()
|
|
281
|
+
if s:
|
|
282
|
+
try:
|
|
283
|
+
req = json.loads(s)
|
|
284
|
+
except Exception:
|
|
285
|
+
req = None
|
|
286
|
+
if isinstance(req, dict) and req.get("cancel"):
|
|
287
|
+
tid = str(req.get("turnId") or "")
|
|
288
|
+
if not tid or tid == _stream.get("turnId"):
|
|
289
|
+
_stream["cancel"] = True
|
|
290
|
+
continue # consumed — do not queue as a turn
|
|
248
291
|
q.put(line)
|
|
249
292
|
except Exception:
|
|
250
293
|
pass
|
package/dist/connection.js
CHANGED
|
@@ -966,6 +966,23 @@ export async function connect(opts) {
|
|
|
966
966
|
activeAssistants.delete(String(payload.sessionId));
|
|
967
967
|
}
|
|
968
968
|
});
|
|
969
|
+
// Cooperative cancel: the user pressed STOP in chat. Write a cancel line to the
|
|
970
|
+
// host's stdin — its stdin reader thread flips a flag the running turn's
|
|
971
|
+
// hooks poll, so the agent loop aborts BETWEEN steps (right where the next
|
|
972
|
+
// phone action would fire) WITHOUT killing the warm session. The server-side
|
|
973
|
+
// phone:kill gate independently halts any in-flight phone action, so even a
|
|
974
|
+
// host that predates this line stops driving the phone; this just also stops
|
|
975
|
+
// it burning model tokens and keeps the session warm for the next message.
|
|
976
|
+
socket.on("runner:assistant_cancel", (payload) => {
|
|
977
|
+
const s = activeAssistants.get(String(payload.sessionId || ""));
|
|
978
|
+
if (!s)
|
|
979
|
+
return;
|
|
980
|
+
s.lastActivity = Date.now();
|
|
981
|
+
try {
|
|
982
|
+
s.proc.stdin?.write(JSON.stringify({ turnId: payload.turnId, cancel: true }) + "\n");
|
|
983
|
+
}
|
|
984
|
+
catch { /* best-effort */ }
|
|
985
|
+
});
|
|
969
986
|
// Idle sweep — reap assistant hosts with no turn in ASSISTANT_IDLE_MS.
|
|
970
987
|
setInterval(() => {
|
|
971
988
|
const now = Date.now();
|