@melaya/runner 1.1.37 → 1.1.38

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.
@@ -184,7 +184,16 @@ def _render_summary_text(summary) -> str:
184
184
  # Streaming state for the CURRENT turn — the pre_print / post_acting hooks read
185
185
  # this to emit delta / tool events keyed to the turn in flight. `cancel` is the
186
186
  # STOP flag: set by the stdin-reader thread, polled by the running turn.
187
- _stream = {"turnId": "", "lens": {}, "cancel": False, "usedBrowser": False}
187
+ _stream = {"turnId": "", "lens": {}, "cancel": False, "usedBrowser": False,
188
+ "deltaBuf": "", "deltaMid": ""}
189
+
190
+ # Coalesce per-token pre_print fragments before emitting a `delta`. agentscope
191
+ # hands us one cumulative snapshot per model chunk; a streaming Anthropic/OpenAI
192
+ # response fragments the answer into 2-15 char pieces, and emitting a MELASSIST
193
+ # line (→ socket → server → SSE → client) PER fragment made streaming read as
194
+ # ultra-choppy / ultra-slow (hundreds of round-trips for one answer). Hold the
195
+ # suffix until it reaches ~a clause or hits a line break, then flush one delta.
196
+ _DELTA_FLUSH_CHARS = 48
188
197
 
189
198
  # Sentinel returned by a turn's coroutine when the user pressed STOP.
190
199
  _CANCELLED = object()
@@ -417,6 +426,18 @@ def _emit(turn_id: str, kind: str, **fields) -> None:
417
426
  pass
418
427
 
419
428
 
429
+ def _flush_delta(turn_id: str) -> None:
430
+ """Emit any buffered pre_print suffix as one `delta` and clear the buffer.
431
+
432
+ Called from the pre_print hook when the buffer fills / a message boundary is
433
+ crossed, and from _run_turn just before the authoritative `text` swap so the
434
+ last partial clause is not left un-streamed."""
435
+ buf = _stream.get("deltaBuf") or ""
436
+ if buf and turn_id:
437
+ _emit(turn_id, "delta", content=buf)
438
+ _stream["deltaBuf"] = ""
439
+
440
+
420
441
  def _log(msg: str) -> None:
421
442
  # Diagnostics go to stderr (the runner logs stderr; stdout is the event channel).
422
443
  try:
@@ -826,19 +847,27 @@ def _register_stream_hooks(agent) -> None:
826
847
  return
827
848
  prev = _stream["lens"].get(mid, 0)
828
849
  if len(text) > prev:
829
- # ReAct emits one message PER iteration: the running chain-of-thought
830
- # ("Let me...", "Actually...", the plan before each tool call). That
831
- # is REASONING, not the answer. Streaming it as `delta` dumped every
832
- # round's raw thought into the chat bubble with no separator between
833
- # rounds ("...page content.I notice..."), which read as broken run-on
834
- # sentences. Route it to `think` instead (the ephemeral thinking
835
- # strip) so it MIRRORS the cloud provider mapping (thinking_delta ->
836
- # think, answer -> text). The clean authoritative answer still lands
837
- # in the bubble via the end-of-turn `text` event (_run_turn), so the
838
- # message reads like one professional reply, reasoning stays in its
839
- # own channel, and tool chips show the live steps.
840
- _emit(tid, "think", content=text[prev:])
850
+ # ReAct emits one message PER iteration. We stream every message's
851
+ # growing suffix into the answer bubble as `delta` so the reply
852
+ # TYPES OUT live (this is the same channel cloud providers stream
853
+ # on) instead of only landing whole at end-of-turn. The end-of-turn
854
+ # `text` event (_run_turn) still swaps in the clean, authoritative
855
+ # markdown answer, so transient tool preambles that streamed here
856
+ # get replaced by the final professional reply. A blank line
857
+ # separates successive ReAct messages so rounds don't run together
858
+ # ("...page content.I notice..."). Fragments are coalesced (see
859
+ # _DELTA_FLUSH_CHARS) so we emit clause-sized deltas, not one
860
+ # socket round-trip per token.
861
+ suffix = text[prev:]
841
862
  _stream["lens"][mid] = len(text)
863
+ if mid != _stream.get("deltaMid"):
864
+ _flush_delta(tid) # flush the previous message's tail
865
+ if _stream.get("deltaMid"):
866
+ _stream["deltaBuf"] = "\n\n"
867
+ _stream["deltaMid"] = mid
868
+ _stream["deltaBuf"] = _stream.get("deltaBuf", "") + suffix
869
+ if len(_stream["deltaBuf"]) >= _DELTA_FLUSH_CHARS or "\n" in suffix:
870
+ _flush_delta(tid)
842
871
  except Exception:
843
872
  pass
844
873
 
@@ -921,6 +950,8 @@ def _run_turn(agent, turn_id: str, message: str, browser_turn: bool = False, ima
921
950
  _emit(turn_id, "round", n=1)
922
951
  _stream["turnId"] = turn_id
923
952
  _stream["lens"] = {}
953
+ _stream["deltaBuf"] = "" # fresh turn — drop any stale streamed suffix
954
+ _stream["deltaMid"] = ""
924
955
  _stream["cancel"] = False # fresh turn — clear any stale STOP
925
956
  _stream["usedBrowser"] = False
926
957
 
@@ -993,6 +1024,7 @@ def _run_turn(agent, turn_id: str, message: str, browser_turn: bool = False, ima
993
1024
  _emit(turn_id, "done")
994
1025
  return
995
1026
 
1027
+ _flush_delta(turn_id) # push the last buffered clause before the swap
996
1028
  _stream["turnId"] = ""
997
1029
  # Authoritative final answer — the client swaps the streamed plain text for
998
1030
  # this markdown-rendered version.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.1.37",
3
+ "version": "1.1.38",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,