@oh-my-pi/pi-coding-agent 15.3.2 → 15.4.2

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.
Files changed (193) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/types/cli/file-processor.d.ts +1 -1
  3. package/dist/types/config/settings-schema.d.ts +45 -3
  4. package/dist/types/config/settings.d.ts +1 -1
  5. package/dist/types/debug/raw-sse.d.ts +2 -0
  6. package/dist/types/edit/file-read-cache.d.ts +15 -4
  7. package/dist/types/edit/index.d.ts +3 -8
  8. package/dist/types/edit/renderer.d.ts +1 -2
  9. package/dist/types/eval/__tests__/shared-executors.test.d.ts +1 -0
  10. package/dist/types/eval/js/shared/local-module-loader.d.ts +16 -0
  11. package/dist/types/eval/js/shared/rewrite-imports.d.ts +4 -0
  12. package/dist/types/eval/js/shared/runtime.d.ts +14 -8
  13. package/dist/types/eval/py/executor.d.ts +1 -2
  14. package/dist/types/eval/py/kernel.d.ts +6 -0
  15. package/dist/types/eval/py/tool-bridge.d.ts +1 -5
  16. package/dist/types/eval/session-id.d.ts +3 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +1 -3
  18. package/dist/types/hashline/anchors.d.ts +15 -9
  19. package/dist/types/hashline/constants.d.ts +0 -2
  20. package/dist/types/hashline/diff.d.ts +1 -2
  21. package/dist/types/hashline/executor.d.ts +52 -0
  22. package/dist/types/hashline/hash.d.ts +44 -93
  23. package/dist/types/hashline/index.d.ts +2 -1
  24. package/dist/types/hashline/input.d.ts +2 -9
  25. package/dist/types/hashline/recovery.d.ts +3 -9
  26. package/dist/types/hashline/tokenizer.d.ts +91 -0
  27. package/dist/types/hashline/types.d.ts +5 -7
  28. package/dist/types/modes/components/extensions/types.d.ts +0 -4
  29. package/dist/types/modes/types.d.ts +1 -0
  30. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  31. package/dist/types/sdk.d.ts +2 -0
  32. package/dist/types/session/agent-session.d.ts +11 -15
  33. package/dist/types/session/agent-storage.d.ts +11 -10
  34. package/dist/types/slash-commands/acp-builtins.d.ts +3 -3
  35. package/dist/types/slash-commands/types.d.ts +0 -5
  36. package/dist/types/task/executor.d.ts +2 -0
  37. package/dist/types/tool-discovery/tool-index.d.ts +0 -50
  38. package/dist/types/tools/index.d.ts +2 -8
  39. package/dist/types/tools/match-line-format.d.ts +4 -4
  40. package/dist/types/tools/output-schema-validator.d.ts +64 -0
  41. package/dist/types/tools/review.d.ts +13 -0
  42. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  43. package/dist/types/tools/search.d.ts +4 -3
  44. package/dist/types/utils/edit-mode.d.ts +1 -1
  45. package/dist/types/web/kagi.d.ts +4 -2
  46. package/dist/types/web/parallel.d.ts +4 -3
  47. package/dist/types/web/scrapers/types.d.ts +2 -1
  48. package/dist/types/web/search/index.d.ts +12 -4
  49. package/dist/types/web/search/provider.d.ts +2 -1
  50. package/dist/types/web/search/providers/anthropic.d.ts +9 -4
  51. package/dist/types/web/search/providers/base.d.ts +34 -2
  52. package/dist/types/web/search/providers/brave.d.ts +8 -1
  53. package/dist/types/web/search/providers/codex.d.ts +13 -9
  54. package/dist/types/web/search/providers/exa.d.ts +10 -1
  55. package/dist/types/web/search/providers/gemini.d.ts +20 -23
  56. package/dist/types/web/search/providers/jina.d.ts +2 -1
  57. package/dist/types/web/search/providers/kagi.d.ts +4 -1
  58. package/dist/types/web/search/providers/kimi.d.ts +10 -1
  59. package/dist/types/web/search/providers/parallel.d.ts +3 -2
  60. package/dist/types/web/search/providers/perplexity.d.ts +5 -2
  61. package/dist/types/web/search/providers/searxng.d.ts +2 -1
  62. package/dist/types/web/search/providers/synthetic.d.ts +5 -8
  63. package/dist/types/web/search/providers/tavily.d.ts +11 -4
  64. package/dist/types/web/search/providers/utils.d.ts +8 -6
  65. package/dist/types/web/search/providers/zai.d.ts +12 -3
  66. package/package.json +7 -7
  67. package/src/cli/file-processor.ts +12 -2
  68. package/src/cli.ts +0 -8
  69. package/src/commands/commit.ts +8 -8
  70. package/src/config/prompt-templates.ts +6 -6
  71. package/src/config/settings-schema.ts +47 -3
  72. package/src/config/settings.ts +5 -5
  73. package/src/debug/raw-sse.ts +68 -3
  74. package/src/edit/file-read-cache.ts +68 -25
  75. package/src/edit/index.ts +6 -37
  76. package/src/edit/renderer.ts +9 -47
  77. package/src/edit/streaming.ts +43 -56
  78. package/src/eval/__tests__/shared-executors.test.ts +520 -0
  79. package/src/eval/js/context-manager.ts +64 -53
  80. package/src/eval/js/shared/local-module-loader.ts +265 -0
  81. package/src/eval/js/shared/prelude.txt +4 -0
  82. package/src/eval/js/shared/rewrite-imports.ts +85 -0
  83. package/src/eval/js/shared/runtime.ts +129 -86
  84. package/src/eval/js/worker-core.ts +23 -38
  85. package/src/eval/py/executor.ts +155 -84
  86. package/src/eval/py/kernel.ts +10 -1
  87. package/src/eval/py/prelude.py +22 -24
  88. package/src/eval/py/runner.py +203 -85
  89. package/src/eval/py/tool-bridge.ts +17 -10
  90. package/src/eval/session-id.ts +8 -0
  91. package/src/exec/bash-executor.ts +27 -16
  92. package/src/extensibility/extensions/runner.ts +0 -1
  93. package/src/extensibility/extensions/types.ts +1 -3
  94. package/src/hashline/anchors.ts +56 -65
  95. package/src/hashline/apply.ts +29 -31
  96. package/src/hashline/constants.ts +0 -3
  97. package/src/hashline/diff-preview.ts +4 -5
  98. package/src/hashline/diff.ts +30 -4
  99. package/src/hashline/execute.ts +91 -26
  100. package/src/hashline/executor.ts +239 -0
  101. package/src/hashline/grammar.lark +12 -10
  102. package/src/hashline/hash.ts +69 -114
  103. package/src/hashline/index.ts +2 -1
  104. package/src/hashline/input.ts +48 -41
  105. package/src/hashline/prefixes.ts +21 -11
  106. package/src/hashline/recovery.ts +63 -71
  107. package/src/hashline/stream.ts +2 -2
  108. package/src/hashline/tokenizer.ts +467 -0
  109. package/src/hashline/types.ts +6 -8
  110. package/src/internal-urls/docs-index.generated.ts +7 -7
  111. package/src/modes/components/extensions/types.ts +0 -5
  112. package/src/modes/components/session-observer-overlay.ts +11 -2
  113. package/src/modes/components/settings-selector.ts +10 -1
  114. package/src/modes/components/tree-selector.ts +10 -2
  115. package/src/modes/controllers/command-controller.ts +1 -3
  116. package/src/modes/controllers/extension-ui-controller.ts +10 -11
  117. package/src/modes/controllers/selector-controller.ts +5 -5
  118. package/src/modes/theme/theme.ts +4 -2
  119. package/src/modes/types.ts +4 -1
  120. package/src/modes/utils/ui-helpers.ts +4 -0
  121. package/src/prompts/agents/explore.md +1 -1
  122. package/src/prompts/tools/ast-edit.md +1 -1
  123. package/src/prompts/tools/ast-grep.md +1 -1
  124. package/src/prompts/tools/eval.md +1 -1
  125. package/src/prompts/tools/hashline.md +73 -94
  126. package/src/prompts/tools/read.md +4 -4
  127. package/src/prompts/tools/search.md +3 -3
  128. package/src/sdk.ts +33 -26
  129. package/src/session/agent-session.ts +59 -66
  130. package/src/session/agent-storage.ts +13 -14
  131. package/src/slash-commands/acp-builtins.ts +3 -3
  132. package/src/slash-commands/types.ts +0 -6
  133. package/src/task/executor.ts +26 -57
  134. package/src/task/index.ts +8 -4
  135. package/src/tool-discovery/tool-index.ts +0 -134
  136. package/src/tools/ast-edit.ts +36 -13
  137. package/src/tools/ast-grep.ts +45 -4
  138. package/src/tools/browser/tab-worker.ts +3 -2
  139. package/src/tools/eval.ts +2 -1
  140. package/src/tools/fetch.ts +23 -14
  141. package/src/tools/index.ts +2 -8
  142. package/src/tools/irc.ts +59 -5
  143. package/src/tools/match-line-format.ts +5 -7
  144. package/src/tools/output-schema-validator.ts +132 -0
  145. package/src/tools/read.ts +142 -31
  146. package/src/tools/review.ts +23 -0
  147. package/src/tools/search-tool-bm25.ts +3 -30
  148. package/src/tools/search.ts +48 -16
  149. package/src/tools/write.ts +3 -3
  150. package/src/tools/yield.ts +32 -41
  151. package/src/utils/edit-mode.ts +1 -2
  152. package/src/utils/file-mentions.ts +2 -2
  153. package/src/web/kagi.ts +15 -6
  154. package/src/web/parallel.ts +9 -6
  155. package/src/web/scrapers/types.ts +7 -1
  156. package/src/web/scrapers/youtube.ts +13 -7
  157. package/src/web/search/index.ts +37 -11
  158. package/src/web/search/provider.ts +5 -3
  159. package/src/web/search/providers/anthropic.ts +30 -21
  160. package/src/web/search/providers/base.ts +35 -2
  161. package/src/web/search/providers/brave.ts +4 -4
  162. package/src/web/search/providers/codex.ts +118 -89
  163. package/src/web/search/providers/exa.ts +3 -2
  164. package/src/web/search/providers/gemini.ts +58 -155
  165. package/src/web/search/providers/jina.ts +4 -4
  166. package/src/web/search/providers/kagi.ts +17 -11
  167. package/src/web/search/providers/kimi.ts +29 -13
  168. package/src/web/search/providers/parallel.ts +171 -23
  169. package/src/web/search/providers/perplexity.ts +38 -37
  170. package/src/web/search/providers/searxng.ts +3 -1
  171. package/src/web/search/providers/synthetic.ts +16 -19
  172. package/src/web/search/providers/tavily.ts +23 -18
  173. package/src/web/search/providers/utils.ts +11 -17
  174. package/src/web/search/providers/zai.ts +16 -8
  175. package/dist/types/hashline/parser.d.ts +0 -7
  176. package/dist/types/mcp/discoverable-tool-metadata.d.ts +0 -7
  177. package/dist/types/tools/vim.d.ts +0 -58
  178. package/dist/types/vim/buffer.d.ts +0 -41
  179. package/dist/types/vim/commands.d.ts +0 -6
  180. package/dist/types/vim/engine.d.ts +0 -47
  181. package/dist/types/vim/parser.d.ts +0 -3
  182. package/dist/types/vim/render.d.ts +0 -25
  183. package/dist/types/vim/types.d.ts +0 -182
  184. package/src/hashline/parser.ts +0 -246
  185. package/src/mcp/discoverable-tool-metadata.ts +0 -24
  186. package/src/prompts/tools/vim.md +0 -98
  187. package/src/tools/vim.ts +0 -949
  188. package/src/vim/buffer.ts +0 -309
  189. package/src/vim/commands.ts +0 -382
  190. package/src/vim/engine.ts +0 -2409
  191. package/src/vim/parser.ts +0 -134
  192. package/src/vim/render.ts +0 -252
  193. package/src/vim/types.ts +0 -197
@@ -5,6 +5,7 @@ wrapper writes typed frames back.
5
5
 
6
6
  Host -> wrapper:
7
7
  {"id": str, "code": str, "silent": bool?, "storeHistory": bool?}
8
+ {"id": str, "code": str, "silent": bool?, "storeHistory": bool?, "cwd": str?, "env": dict?}
8
9
  {"type": "exit"} # graceful shutdown
9
10
 
10
11
  Wrapper -> host:
@@ -27,6 +28,7 @@ from __future__ import annotations
27
28
 
28
29
  import asyncio
29
30
  import ast
31
+ import contextvars
30
32
  import base64
31
33
  import builtins
32
34
  import inspect
@@ -43,7 +45,7 @@ import threading
43
45
  import time
44
46
  import traceback
45
47
  from pathlib import Path
46
- from typing import Any, Callable
48
+ from typing import Any
47
49
 
48
50
  # ---------------------------------------------------------------------------
49
51
  # Frame writer
@@ -93,7 +95,7 @@ class _StreamProxy(io.TextIOBase):
93
95
  data = str(data)
94
96
  if not data:
95
97
  return 0
96
- rid = _STATE.current_id
98
+ rid = _CURRENT_RID.get()
97
99
  if rid is None:
98
100
  _RAW_STDERR.write(data)
99
101
  _RAW_STDERR.flush()
@@ -112,7 +114,6 @@ class _StreamProxy(io.TextIOBase):
112
114
 
113
115
  class _RunnerState:
114
116
  def __init__(self) -> None:
115
- self.current_id: str | None = None
116
117
  self.execution_count: int = 0
117
118
  self.cancel_requested: bool = False
118
119
  # User globals — kept across requests when running in session mode.
@@ -123,8 +124,11 @@ class _RunnerState:
123
124
  }
124
125
  self.last_install_marker: int = 0
125
126
  self.loop: asyncio.AbstractEventLoop | None = None
127
+ self.active_executions: int = 0
126
128
 
127
129
 
130
+ _CURRENT_RID: contextvars.ContextVar[str | None] = contextvars.ContextVar("omp_current_rid", default=None)
131
+
128
132
  _STATE = _RunnerState()
129
133
 
130
134
 
@@ -284,7 +288,7 @@ def cell_magic(name: str) -> Callable[[Callable[[str, str], Any]], Callable[[str
284
288
 
285
289
  def _emit_status(op: str, **data: Any) -> None:
286
290
  bundle = {"application/x-omp-status": {"op": op, **data}}
287
- rid = _STATE.current_id
291
+ rid = _CURRENT_RID.get()
288
292
  if rid is None:
289
293
  return
290
294
  _emit({"type": "display", "id": rid, "bundle": bundle})
@@ -424,7 +428,7 @@ def _magic_reset(_args: str) -> None:
424
428
  def _magic_load(args: str) -> None:
425
429
  path = Path(os.path.expanduser(args.strip()))
426
430
  source = path.read_text(encoding="utf-8")
427
- _emit({"type": "display", "id": _STATE.current_id, "bundle": {"text/plain": source}})
431
+ _emit({"type": "display", "id": _CURRENT_RID.get(), "bundle": {"text/plain": source}})
428
432
  _exec_source(source, _STATE.user_ns)
429
433
 
430
434
 
@@ -622,7 +626,7 @@ def _mime_bundle(value: Any) -> dict:
622
626
 
623
627
 
624
628
  def _emit_display(bundle: dict, *, kind: str = "display") -> None:
625
- rid = _STATE.current_id
629
+ rid = _CURRENT_RID.get()
626
630
  if rid is None:
627
631
  return
628
632
  _emit({"type": kind, "id": rid, "bundle": bundle})
@@ -681,6 +685,7 @@ def _install_builtins(ns: dict) -> None:
681
685
  ns["__omp_magic"] = __omp_magic
682
686
  ns["__omp_magic_cell"] = __omp_magic_cell
683
687
  ns["__omp_shell"] = __omp_shell
688
+ ns["__omp_current_run_id__"] = lambda: _CURRENT_RID.get()
684
689
 
685
690
 
686
691
  _install_builtins(_STATE.user_ns)
@@ -694,25 +699,37 @@ _install_builtins(_STATE.user_ns)
694
699
  _TLA_FLAG = getattr(ast, "PyCF_ALLOW_TOP_LEVEL_AWAIT", 0x2000)
695
700
 
696
701
 
697
- def _get_event_loop() -> asyncio.AbstractEventLoop:
698
- loop = _STATE.loop
699
- if loop is None or loop.is_closed():
700
- loop = asyncio.new_event_loop()
701
- asyncio.set_event_loop(loop)
702
- _STATE.loop = loop
703
- return loop
702
+ def _await_sync(coro) -> Any:
703
+ try:
704
+ running_loop = asyncio.get_running_loop()
705
+ except RuntimeError:
706
+ running_loop = None
707
+ if running_loop is not None and running_loop.is_running():
708
+ raise RuntimeError("top-level await is not supported from synchronous magic execution")
709
+ return asyncio.run(coro)
710
+
711
+
712
+ def _run_compiled_sync(code, ns: dict, *, want_value: bool) -> Any:
713
+ """Synchronous execution path used by nested magic helpers."""
714
+ if code.co_flags & inspect.CO_COROUTINE:
715
+ result = _await_sync(eval(code, ns))
716
+ return result if want_value else None
717
+ if want_value:
718
+ return eval(code, ns)
719
+ exec(code, ns)
720
+ return None
721
+
704
722
 
705
723
 
706
- def _run_compiled(code, ns: dict, *, want_value: bool) -> Any:
707
- """Execute a code object, awaiting it if compiled as a coroutine.
724
+ async def _run_compiled_async(code, ns: dict, *, want_value: bool) -> Any:
725
+ """Execute a code object in the persistent event loop.
708
726
 
709
- ``want_value`` is True for the trailing expression we return ``eval``'s
710
- result (or the awaited coroutine's value). For statement blocks the
711
- return is always ``None``.
727
+ Coroutine code is awaited in this task so top-level ``await`` interleaves
728
+ with sibling requests. Plain statement/expression code runs on the main
729
+ runner thread so SIGINT can interrupt it reliably.
712
730
  """
713
731
  if code.co_flags & inspect.CO_COROUTINE:
714
- coro = eval(code, ns)
715
- result = _get_event_loop().run_until_complete(coro)
732
+ result = await eval(code, ns)
716
733
  return result if want_value else None
717
734
  if want_value:
718
735
  return eval(code, ns)
@@ -720,15 +737,10 @@ def _run_compiled(code, ns: dict, *, want_value: bool) -> Any:
720
737
  return None
721
738
 
722
739
 
723
- def _exec_source(source: str, ns: dict) -> None:
724
- """Compile + execute ``source``; if the last node is an expression, route
725
- its value through ``__omp_display`` so dataframes/figures render rich.
726
- Top-level ``await`` / ``async for`` / ``async with`` is permitted; the
727
- cell is driven through the runner's persistent event loop."""
740
+ def _compile_source(source: str) -> tuple[Any, Any | None, bool]:
728
741
  module = ast.parse(source, mode="exec")
729
-
730
742
  if not module.body:
731
- return
743
+ return None, None, False
732
744
 
733
745
  last = module.body[-1]
734
746
  if isinstance(last, ast.Expr):
@@ -737,14 +749,36 @@ def _exec_source(source: str, ns: dict) -> None:
737
749
  ast.copy_location(expr_module, last)
738
750
  body_code = compile(body_module, "<cell>", "exec", flags=_TLA_FLAG)
739
751
  expr_code = compile(expr_module, "<cell>", "eval", flags=_TLA_FLAG)
740
- _run_compiled(body_code, ns, want_value=False)
741
- value = _run_compiled(expr_code, ns, want_value=True)
752
+ return body_code, expr_code, True
753
+
754
+ return compile(module, "<cell>", "exec", flags=_TLA_FLAG), None, False
755
+
756
+
757
+ def _exec_source(source: str, ns: dict) -> None:
758
+ """Synchronous source execution for legacy magic helpers."""
759
+ body_code, expr_code, has_expr = _compile_source(source)
760
+ if body_code is None:
761
+ return
762
+ _run_compiled_sync(body_code, ns, want_value=False)
763
+ if has_expr and expr_code is not None:
764
+ value = _run_compiled_sync(expr_code, ns, want_value=True)
742
765
  if value is not None:
743
766
  __omp_display(value, kind="result")
744
- return
745
767
 
746
- code = compile(module, "<cell>", "exec", flags=_TLA_FLAG)
747
- _run_compiled(code, ns, want_value=False)
768
+
769
+ async def _exec_source_async(source: str, ns: dict) -> None:
770
+ """Compile + execute ``source``; if the last node is an expression, route
771
+ its value through ``__omp_display`` so dataframes/figures render rich.
772
+ Top-level ``await`` / ``async for`` / ``async with`` is permitted; awaited
773
+ regions yield to other requests in the runner's persistent event loop."""
774
+ body_code, expr_code, has_expr = _compile_source(source)
775
+ if body_code is None:
776
+ return
777
+ await _run_compiled_async(body_code, ns, want_value=False)
778
+ if has_expr and expr_code is not None:
779
+ value = await _run_compiled_async(expr_code, ns, want_value=True)
780
+ if value is not None:
781
+ __omp_display(value, kind="result")
748
782
 
749
783
 
750
784
  # ---------------------------------------------------------------------------
@@ -767,6 +801,46 @@ def _install_exec_sigint() -> None:
767
801
  pass
768
802
 
769
803
 
804
+ def _begin_exec_sigint() -> None:
805
+ _STATE.active_executions += 1
806
+ _install_exec_sigint()
807
+
808
+
809
+ def _end_exec_sigint() -> None:
810
+ if _STATE.active_executions > 0:
811
+ _STATE.active_executions -= 1
812
+ if _STATE.active_executions == 0:
813
+ _install_idle_sigint()
814
+
815
+
816
+ _MANAGED_ENV_KEYS = (
817
+ "PI_SESSION_FILE",
818
+ "PI_ARTIFACTS_DIR",
819
+ "PI_TOOL_BRIDGE_URL",
820
+ "PI_TOOL_BRIDGE_TOKEN",
821
+ "PI_TOOL_BRIDGE_SESSION",
822
+ )
823
+
824
+
825
+ def _apply_request_runtime(req: dict) -> None:
826
+ cwd = req.get("cwd")
827
+ if isinstance(cwd, str) and cwd:
828
+ os.chdir(cwd)
829
+ try:
830
+ sys.path.remove(cwd)
831
+ except ValueError:
832
+ pass
833
+ sys.path.insert(0, cwd)
834
+
835
+ env = req.get("env")
836
+ if isinstance(env, dict):
837
+ for key in _MANAGED_ENV_KEYS:
838
+ value = env.get(key)
839
+ if isinstance(value, str):
840
+ os.environ[key] = value
841
+ elif value is None:
842
+ os.environ.pop(key, None)
843
+
770
844
  def _start_parent_watchdog() -> None:
771
845
  """Self-terminate when the host process dies.
772
846
 
@@ -802,61 +876,72 @@ def _start_parent_watchdog() -> None:
802
876
  # ---------------------------------------------------------------------------
803
877
 
804
878
 
805
- def _handle_request(req: dict) -> None:
806
- if req.get("type") == "exit":
807
- sys.exit(0)
808
-
879
+ async def _handle_request_async(req: dict) -> None:
809
880
  rid = str(req.get("id"))
810
- code = req.get("code", "")
811
- _STATE.current_id = rid
881
+ token = _CURRENT_RID.set(rid)
882
+ _STATE.user_ns["__omp_run_id__"] = rid
812
883
  _STATE.cancel_requested = False
813
884
  _STATE.execution_count += 1
885
+ execution_count = _STATE.execution_count
814
886
  _emit({"type": "started", "id": rid})
815
887
 
816
888
  status: str = "ok"
817
889
  cancelled = False
818
890
 
819
891
  try:
820
- transformed = transform_cell(code)
821
- except SyntaxError as exc:
822
- _emit_error(rid, exc)
892
+ try:
893
+ _apply_request_runtime(req)
894
+ transformed = transform_cell(req.get("code", ""))
895
+ except SyntaxError as exc:
896
+ _emit_error(rid, exc)
897
+ _emit({
898
+ "type": "done",
899
+ "id": rid,
900
+ "status": "error",
901
+ "executionCount": execution_count,
902
+ "cancelled": False,
903
+ })
904
+ return
905
+ except BaseException as exc: # noqa: BLE001 - runtime setup errors must settle the request
906
+ _emit_error(rid, exc)
907
+ _emit({
908
+ "type": "done",
909
+ "id": rid,
910
+ "status": "error",
911
+ "executionCount": execution_count,
912
+ "cancelled": False,
913
+ })
914
+ return
915
+
916
+ _begin_exec_sigint()
917
+ try:
918
+ await _exec_source_async(transformed, _STATE.user_ns)
919
+ except KeyboardInterrupt:
920
+ cancelled = True
921
+ status = "error"
922
+ _emit_error(rid, KeyboardInterrupt("Execution interrupted"))
923
+ except SystemExit as exc:
924
+ status = "error"
925
+ _emit_error(rid, exc)
926
+ except BaseException as exc: # noqa: BLE001 - we want to surface every user error
927
+ status = "error"
928
+ _emit_error(rid, exc)
929
+ finally:
930
+ _end_exec_sigint()
931
+ try:
932
+ _flush_matplotlib_figures()
933
+ except Exception:
934
+ pass
935
+
823
936
  _emit({
824
937
  "type": "done",
825
938
  "id": rid,
826
- "status": "error",
827
- "executionCount": _STATE.execution_count,
828
- "cancelled": False,
939
+ "status": status,
940
+ "executionCount": execution_count,
941
+ "cancelled": cancelled,
829
942
  })
830
- _STATE.current_id = None
831
- return
832
-
833
- _install_exec_sigint()
834
- try:
835
- _exec_source(transformed, _STATE.user_ns)
836
- except KeyboardInterrupt:
837
- cancelled = True
838
- status = "error"
839
- _emit_error(rid, KeyboardInterrupt("Execution interrupted"))
840
- except SystemExit:
841
- raise
842
- except BaseException as exc: # noqa: BLE001 - we want to surface every user error
843
- status = "error"
844
- _emit_error(rid, exc)
845
943
  finally:
846
- _install_idle_sigint()
847
- try:
848
- _flush_matplotlib_figures()
849
- except Exception:
850
- pass
851
-
852
- _emit({
853
- "type": "done",
854
- "id": rid,
855
- "status": status,
856
- "executionCount": _STATE.execution_count,
857
- "cancelled": cancelled,
858
- })
859
- _STATE.current_id = None
944
+ _CURRENT_RID.reset(token)
860
945
 
861
946
 
862
947
  def _emit_error(rid: str, exc: BaseException) -> None:
@@ -875,16 +960,7 @@ def _emit_error(rid: str, exc: BaseException) -> None:
875
960
  # ---------------------------------------------------------------------------
876
961
 
877
962
 
878
- def main() -> None:
879
- sys.stdout = _StreamProxy("stdout")
880
- sys.stderr = _StreamProxy("stderr")
881
- _install_idle_sigint()
882
- _start_parent_watchdog()
883
-
884
- stdin = sys.__stdin__
885
- if stdin is None:
886
- return
887
-
963
+ def _read_stdin(loop: asyncio.AbstractEventLoop, queue: asyncio.Queue, stdin) -> None:
888
964
  for raw_line in stdin:
889
965
  line = raw_line.strip()
890
966
  if not line:
@@ -900,10 +976,52 @@ def main() -> None:
900
976
  "traceback": [],
901
977
  })
902
978
  continue
979
+ loop.call_soon_threadsafe(queue.put_nowait, req)
980
+ loop.call_soon_threadsafe(queue.put_nowait, {"type": "exit"})
981
+
982
+
983
+ async def _main_async() -> None:
984
+ sys.stdout = _StreamProxy("stdout")
985
+ sys.stderr = _StreamProxy("stderr")
986
+ _install_idle_sigint()
987
+ _start_parent_watchdog()
988
+
989
+ stdin = sys.__stdin__
990
+ if stdin is None:
991
+ return
992
+
993
+ loop = asyncio.get_running_loop()
994
+ _STATE.loop = loop
995
+ queue: asyncio.Queue = asyncio.Queue()
996
+ reader = threading.Thread(target=_read_stdin, args=(loop, queue, stdin), name="omp-stdin-reader", daemon=True)
997
+ reader.start()
998
+
999
+ tasks: set[asyncio.Task] = set()
1000
+ def _task_done(task: asyncio.Task) -> None:
1001
+ tasks.discard(task)
903
1002
  try:
904
- _handle_request(req)
905
- except SystemExit:
1003
+ exc = task.exception()
1004
+ except asyncio.CancelledError:
906
1005
  return
1006
+ if exc is not None:
1007
+ _emit_error("", exc)
1008
+ try:
1009
+ while True:
1010
+ req = await queue.get()
1011
+ if req.get("type") == "exit":
1012
+ break
1013
+ task = asyncio.create_task(_handle_request_async(req))
1014
+ tasks.add(task)
1015
+ task.add_done_callback(_task_done)
1016
+ finally:
1017
+ for task in tasks:
1018
+ task.cancel()
1019
+ if tasks:
1020
+ await asyncio.gather(*tasks, return_exceptions=True)
1021
+
1022
+
1023
+ def main() -> None:
1024
+ asyncio.run(_main_async())
907
1025
 
908
1026
 
909
1027
  if __name__ == "__main__":
@@ -44,21 +44,23 @@ async function startServer(): Promise<BridgeServer> {
44
44
  return new Response("Forbidden", { status: 403 });
45
45
  }
46
46
 
47
- let body: { session?: unknown; name?: unknown; args?: unknown };
47
+ let body: { session?: unknown; run?: unknown; name?: unknown; args?: unknown };
48
48
  try {
49
- body = (await req.json()) as { session?: unknown; name?: unknown; args?: unknown };
49
+ body = (await req.json()) as { session?: unknown; run?: unknown; name?: unknown; args?: unknown };
50
50
  } catch {
51
51
  return Response.json({ ok: false, error: "Invalid JSON body" }, { status: 400 });
52
52
  }
53
53
  const sessionId = typeof body.session === "string" ? body.session : "";
54
+ const runId = typeof body.run === "string" ? body.run : "";
54
55
  const name = typeof body.name === "string" ? body.name : "";
55
- if (!sessionId || !name) {
56
- return Response.json({ ok: false, error: "Missing session/name" }, { status: 400 });
56
+ if (!sessionId || !runId || !name) {
57
+ return Response.json({ ok: false, error: "Missing session/run/name" }, { status: 400 });
57
58
  }
58
- const entry = registrations.get(sessionId);
59
+ const registrationKey = bridgeRegistrationKey(sessionId, runId);
60
+ const entry = registrations.get(registrationKey) ?? registrations.get(sessionId);
59
61
  if (!entry) {
60
62
  return Response.json(
61
- { ok: false, error: `No active Python tool bridge session: ${sessionId}` },
63
+ { ok: false, error: `No active Python tool bridge session: ${registrationKey}` },
62
64
  { status: 200 },
63
65
  );
64
66
  }
@@ -111,11 +113,16 @@ export async function ensurePyToolBridge(): Promise<PyToolBridgeInfo> {
111
113
  * Register a tool session for the duration of one execution. The returned
112
114
  * function MUST be called to remove the entry once execution finishes.
113
115
  */
114
- export function registerPyToolBridge(sessionId: string, entry: PyToolBridgeEntry): () => void {
115
- registrations.set(sessionId, entry);
116
+ function bridgeRegistrationKey(sessionId: string, runId: string): string {
117
+ return `${sessionId}:${runId}`;
118
+ }
119
+
120
+ export function registerPyToolBridge(sessionId: string, runId: string, entry: PyToolBridgeEntry): () => void {
121
+ const key = bridgeRegistrationKey(sessionId, runId);
122
+ registrations.set(key, entry);
116
123
  return () => {
117
- if (registrations.get(sessionId) === entry) {
118
- registrations.delete(sessionId);
124
+ if (registrations.get(key) === entry) {
125
+ registrations.delete(key);
119
126
  }
120
127
  };
121
128
  }
@@ -0,0 +1,8 @@
1
+ import type { ToolSession } from "../tools";
2
+
3
+ export type EvalSessionSource = Pick<ToolSession, "cwd" | "getSessionFile">;
4
+
5
+ export function defaultEvalSessionId(session: EvalSessionSource): string {
6
+ const sessionFile = session.getSessionFile?.() ?? undefined;
7
+ return sessionFile ? `session:${sessionFile}:cwd:${session.cwd}` : `cwd:${session.cwd}`;
8
+ }
@@ -48,8 +48,6 @@ export interface BashResult {
48
48
  artifactId?: string;
49
49
  }
50
50
 
51
- const HARD_TIMEOUT_GRACE_MS = 5_000;
52
-
53
51
  const shellSessions = new Map<string, Shell>();
54
52
  const brokenShellSessions = new Set<string>();
55
53
 
@@ -106,8 +104,9 @@ export async function executeBash(command: string, options?: BashExecutorOptions
106
104
  // sink.push() is synchronous — buffer management, counters, and onChunk
107
105
  // all run inline. File writes (artifact path) are handled asynchronously
108
106
  // inside the sink. No promise chain needed.
107
+ let acceptingChunks = true;
109
108
  const enqueueChunk = (chunk: string) => {
110
- sink.push(chunk);
109
+ if (acceptingChunks) sink.push(chunk);
111
110
  };
112
111
 
113
112
  if (options?.signal?.aborted) {
@@ -144,21 +143,22 @@ export async function executeBash(command: string, options?: BashExecutorOptions
144
143
  void shellSession.abort();
145
144
  }
146
145
  };
146
+ const abortDeferred = Promise.withResolvers<"abort">();
147
147
  const abortHandler = () => {
148
148
  abortCurrentExecution();
149
+ abortDeferred.resolve("abort");
149
150
  };
150
151
  if (userSignal) {
151
152
  userSignal.addEventListener("abort", abortHandler, { once: true });
152
153
  }
153
154
 
154
- let hardTimeoutTimer: NodeJS.Timeout | undefined;
155
- const hardTimeoutDeferred = Promise.withResolvers<"hard-timeout">();
155
+ let timeoutTimer: NodeJS.Timeout | undefined;
156
+ const timeoutDeferred = Promise.withResolvers<"timeout">();
156
157
  const baseTimeoutMs = Math.max(1_000, options?.timeout ?? 300_000);
157
- const hardTimeoutMs = baseTimeoutMs + HARD_TIMEOUT_GRACE_MS;
158
- hardTimeoutTimer = setTimeout(() => {
158
+ timeoutTimer = setTimeout(() => {
159
159
  abortCurrentExecution();
160
- hardTimeoutDeferred.resolve("hard-timeout");
161
- }, hardTimeoutMs);
160
+ timeoutDeferred.resolve("timeout");
161
+ }, baseTimeoutMs);
162
162
 
163
163
  let resetSession = false;
164
164
 
@@ -198,22 +198,33 @@ export async function executeBash(command: string, options?: BashExecutorOptions
198
198
 
199
199
  const winner = await Promise.race([
200
200
  runPromise.then(result => ({ kind: "result" as const, result })),
201
- hardTimeoutDeferred.promise.then(() => ({ kind: "hard-timeout" as const })),
201
+ timeoutDeferred.promise.then(kind => ({ kind })),
202
+ abortDeferred.promise.then(kind => ({ kind })),
202
203
  ]);
203
204
 
204
- if (winner.kind === "hard-timeout") {
205
+ if (winner.kind === "timeout" || winner.kind === "abort") {
206
+ acceptingChunks = false;
205
207
  if (shellSession) {
206
208
  resetSession = true;
207
- // Fall back to one-shot execution for the rest of the process once
208
- // a persistent session has stopped responding to cancellation.
209
209
  brokenShellSessions.add(sessionKey);
210
+ void runPromise.finally(() => brokenShellSessions.delete(sessionKey)).catch(() => undefined);
211
+ } else {
212
+ void runPromise.catch(() => undefined);
210
213
  }
211
214
  return {
212
215
  exitCode: undefined,
213
216
  cancelled: true,
214
- ...(await sink.dump(`Command exceeded hard timeout after ${Math.round(hardTimeoutMs / 1000)} seconds`)),
217
+ ...(await sink.dump(
218
+ winner.kind === "timeout"
219
+ ? `Command timed out after ${Math.round(baseTimeoutMs / 1000)} seconds`
220
+ : "Command cancelled",
221
+ )),
215
222
  };
216
223
  }
224
+ if (timeoutTimer) {
225
+ clearTimeout(timeoutTimer);
226
+ timeoutTimer = undefined;
227
+ }
217
228
 
218
229
  // Handle timeout
219
230
  if (winner.result.timedOut) {
@@ -268,8 +279,8 @@ export async function executeBash(command: string, options?: BashExecutorOptions
268
279
  resetSession = true;
269
280
  throw err;
270
281
  } finally {
271
- if (hardTimeoutTimer) {
272
- clearTimeout(hardTimeoutTimer);
282
+ if (timeoutTimer) {
283
+ clearTimeout(timeoutTimer);
273
284
  }
274
285
  if (userSignal) {
275
286
  userSignal.removeEventListener("abort", abortHandler);
@@ -462,7 +462,6 @@ export class ExtensionRunner {
462
462
  hasPendingMessages: () => this.#hasPendingMessagesFn(),
463
463
  shutdown: () => this.#shutdownHandler(),
464
464
  getSystemPrompt: () => this.#getSystemPromptFn(),
465
- hasQueuedMessages: () => this.#hasPendingMessagesFn(), // deprecated alias
466
465
  };
467
466
  }
468
467
 
@@ -283,8 +283,6 @@ export interface ExtensionContext {
283
283
  shutdown(): void;
284
284
  /** Get the current effective system prompt. */
285
285
  getSystemPrompt(): string[];
286
- /** @deprecated Use hasPendingMessages() instead */
287
- hasQueuedMessages(): boolean;
288
286
  }
289
287
 
290
288
  /**
@@ -968,7 +966,7 @@ export interface ExtensionAPI {
968
966
  options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
969
967
  ): void;
970
968
 
971
- /** Send a user message to the agent. Always triggers a turn. */
969
+ /** Send a user message to the agent, or queue it when deliverAs is set. */
972
970
  sendUserMessage(
973
971
  content: string | (TextContent | ImageContent)[],
974
972
  options?: { deliverAs?: "steer" | "followUp" },