langchain-quickjs 0.3.0__tar.gz → 0.3.2__tar.gz

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 (48) hide show
  1. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/.gitignore +3 -0
  2. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/CHANGELOG.md +21 -4
  3. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/PKG-INFO +4 -3
  4. langchain_quickjs-0.3.2/langchain_quickjs/__init__.py +21 -0
  5. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/langchain_quickjs/_format.py +13 -13
  6. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/langchain_quickjs/_prompt.py +21 -14
  7. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/langchain_quickjs/_ptc.py +33 -14
  8. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/langchain_quickjs/_repl.py +153 -94
  9. langchain_quickjs-0.3.2/langchain_quickjs/_snapshot.py +125 -0
  10. langchain_quickjs-0.3.2/langchain_quickjs/_subagent.py +351 -0
  11. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/langchain_quickjs/middleware.py +47 -75
  12. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/pyproject.toml +4 -3
  13. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/_common.py +2 -2
  14. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/benchmarks/_common.py +7 -7
  15. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/benchmarks/test_quickjs_memory.py +11 -9
  16. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/benchmarks/test_quickjs_throughput.py +10 -8
  17. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/integration_tests/test_rlm.py +10 -10
  18. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/smoke_tests/snapshots/quickjs_system_prompt_mixed_foreign_functions.md +14 -7
  19. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/smoke_tests/snapshots/quickjs_system_prompt_mixed_foreign_functions_call.md +14 -7
  20. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/smoke_tests/snapshots/quickjs_system_prompt_mixed_foreign_functions_turn.md +14 -7
  21. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/smoke_tests/snapshots/quickjs_system_prompt_no_tools.md +9 -2
  22. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/smoke_tests/snapshots/quickjs_system_prompt_no_tools_call.md +9 -2
  23. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/smoke_tests/snapshots/quickjs_system_prompt_no_tools_turn.md +9 -2
  24. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/test_end_to_end.py +11 -11
  25. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/test_end_to_end_async.py +2 -2
  26. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/test_ptc.py +78 -24
  27. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/test_repl_middleware.py +88 -175
  28. langchain_quickjs-0.3.2/tests/unit_tests/test_snapshot.py +514 -0
  29. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/test_snapshot_persistence.py +77 -8
  30. langchain_quickjs-0.3.2/tests/unit_tests/test_subagent_events.py +229 -0
  31. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/test_thread_affinity.py +20 -21
  32. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/uv.lock +65 -13
  33. langchain_quickjs-0.3.0/langchain_quickjs/__init__.py +0 -19
  34. langchain_quickjs-0.3.0/langchain_quickjs/_subagent.py +0 -153
  35. langchain_quickjs-0.3.0/langchain_quickjs/_swarm_task.py +0 -352
  36. langchain_quickjs-0.3.0/tests/unit_tests/test_swarm_task.py +0 -804
  37. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/LICENSE +0 -0
  38. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/Makefile +0 -0
  39. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/README.md +0 -0
  40. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/langchain_quickjs/py.typed +0 -0
  41. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/__init__.py +0 -0
  42. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/integration_tests/__init__.py +0 -0
  43. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/integration_tests/test_postgres.py +0 -0
  44. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/__init__.py +0 -0
  45. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/smoke_tests/__init__.py +0 -0
  46. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/smoke_tests/conftest.py +0 -0
  47. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/smoke_tests/test_system_prompt.py +0 -0
  48. {langchain_quickjs-0.3.0 → langchain_quickjs-0.3.2}/tests/unit_tests/test_prompt_modes.py +0 -0
@@ -251,3 +251,6 @@ CLAUDE.md
251
251
  libs/cli/frontend/node_modules/
252
252
  libs/cli/frontend/dist/
253
253
  node_modules/
254
+
255
+ # Generated at build time by libs/code/hatch_build.py (stamps the release commit)
256
+ libs/code/deepagents_code/_build_info.py
@@ -2,17 +2,34 @@
2
2
 
3
3
  # Changelog
4
4
 
5
- ## [0.3.0](https://github.com/langchain-ai/deepagents/compare/langchain-quickjs==0.2.0...langchain-quickjs==0.3.0) (2026-06-18)
5
+ ## [0.3.2](https://github.com/langchain-ai/deepagents/compare/langchain-quickjs==0.3.1...langchain-quickjs==0.3.2) (2026-06-25)
6
+
7
+ ### Features
8
+
9
+ * Dynamic subagents UI ([#4221](https://github.com/langchain-ai/deepagents/issues/4221)) ([10bcba2](https://github.com/langchain-ai/deepagents/commit/10bcba25600e51aba135f170b34aa6315c0f53d6))
10
+
11
+ ### Bug Fixes
6
12
 
13
+ * Ensure top-level title on subagent response schemas ([#4155](https://github.com/langchain-ai/deepagents/issues/4155)) ([08f917e](https://github.com/langchain-ai/deepagents/commit/08f917eea79513e7e894731f884e738c2d30383f))
14
+ * Normalize nested `undefined` tool args ([#3935](https://github.com/langchain-ai/deepagents/issues/3935)) ([1b461a0](https://github.com/langchain-ai/deepagents/commit/1b461a0d6c6a7a6d323db18505596fcce4326b92))
15
+
16
+ ## [0.3.1](https://github.com/langchain-ai/deepagents/compare/langchain-quickjs==0.3.0...langchain-quickjs==0.3.1) (2026-06-22)
17
+
18
+ ### Bug Fixes
19
+
20
+ * Persist top-level JS declarations across evals ([#4147](https://github.com/langchain-ai/deepagents/issues/4147)) ([7574fea](https://github.com/langchain-ai/deepagents/commit/7574fea88b51c77f7afa8279c4f561d6ecb47e3f))
21
+ * PTC tools in tools namespace are rendered without prepended `tools.` in system prompt and task as ptc duplicated task global ([#4075](https://github.com/langchain-ai/deepagents/issues/4075)) ([014a903](https://github.com/langchain-ai/deepagents/commit/014a9033af70b5b8b08ad2eb36f98590f5f1cca8))
22
+
23
+ ## [0.3.0](https://github.com/langchain-ai/deepagents/compare/langchain-quickjs==0.2.0...langchain-quickjs==0.3.0) (2026-06-18)
7
24
 
8
25
  ### ⚠ BREAKING CHANGES
9
26
 
10
- * **quickjs:** upgrade to 0.2.0 quickjs-rs ([#4067](https://github.com/langchain-ai/deepagents/issues/4067))
27
+ * Upgrade to 0.2.0 quickjs-rs ([#4067](https://github.com/langchain-ai/deepagents/issues/4067))
11
28
 
12
29
  ### Features
13
30
 
14
- * **quickjs:** prompt tuning on task global ([#4066](https://github.com/langchain-ai/deepagents/issues/4066)) ([a47696f](https://github.com/langchain-ai/deepagents/commit/a47696f6d3e57eccb5ea19fb344305a7995ecc76))
15
- * **quickjs:** upgrade to 0.2.0 quickjs-rs ([#4067](https://github.com/langchain-ai/deepagents/issues/4067)) ([4ffea88](https://github.com/langchain-ai/deepagents/commit/4ffea88690418207b5e4fa800ee8c1abfa454bec))
31
+ * Prompt tuning on task global ([#4066](https://github.com/langchain-ai/deepagents/issues/4066)) ([a47696f](https://github.com/langchain-ai/deepagents/commit/a47696f6d3e57eccb5ea19fb344305a7995ecc76))
32
+ * Upgrade to 0.2.0 quickjs-rs ([#4067](https://github.com/langchain-ai/deepagents/issues/4067)) ([4ffea88](https://github.com/langchain-ai/deepagents/commit/4ffea88690418207b5e4fa800ee8c1abfa454bec))
16
33
 
17
34
  ## [0.2.0](https://github.com/langchain-ai/deepagents/compare/langchain-quickjs==0.1.4...langchain-quickjs==0.2.0) (2026-06-12)
18
35
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain-quickjs
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: JavaScript REPL middleware for deepagents
5
5
  Project-URL: Homepage, https://github.com/langchain-ai/deepagents/tree/main/libs/partners/quickjs
6
6
  Project-URL: Repository, https://github.com/langchain-ai/deepagents
@@ -16,11 +16,12 @@ Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
18
  Requires-Python: <4.0,>=3.11
19
- Requires-Dist: deepagents<0.7.0,>=0.6.8
19
+ Requires-Dist: bsdiff4<2.0.0,>=1.2.6
20
+ Requires-Dist: deepagents<0.8.0,>=0.6.8
20
21
  Requires-Dist: langchain-core<2.0.0,>=1.4.7
21
22
  Requires-Dist: langchain<2.0.0,>=1.3.9
22
23
  Requires-Dist: langgraph<2.0.0,>=1.2.5
23
- Requires-Dist: quickjs-rs<0.3.0,>=0.2.0
24
+ Requires-Dist: quickjs-rs<0.3.0,>=0.2.3
24
25
  Description-Content-Type: text/markdown
25
26
 
26
27
  # langchain-quickjs
@@ -0,0 +1,21 @@
1
+ """langchain-quickjs: persistent JS REPL middleware for agents."""
2
+
3
+ from langchain_quickjs._ptc import PTCOption
4
+ from langchain_quickjs._subagent import (
5
+ SUBAGENT_STREAM_EVENT_TYPE,
6
+ SubagentCompleteEvent,
7
+ SubagentErrorEvent,
8
+ SubagentStartEvent,
9
+ SubagentStreamEvent,
10
+ )
11
+ from langchain_quickjs.middleware import CodeInterpreterMiddleware
12
+
13
+ __all__ = [
14
+ "SUBAGENT_STREAM_EVENT_TYPE",
15
+ "CodeInterpreterMiddleware",
16
+ "PTCOption",
17
+ "SubagentCompleteEvent",
18
+ "SubagentErrorEvent",
19
+ "SubagentStartEvent",
20
+ "SubagentStreamEvent",
21
+ ]
@@ -17,7 +17,7 @@ _TRUNCATE_MARKER = "… [truncated {n} chars]"
17
17
 
18
18
 
19
19
  def format_handle(handle: Any) -> str:
20
- """Describe a ``Handle`` value in REPL-style shorthand."""
20
+ """Describe a `Handle` value in REPL-style shorthand."""
21
21
  kind = handle.type_of
22
22
  if kind == "function":
23
23
  try:
@@ -60,7 +60,7 @@ def _format_jsvalue(value: Any) -> str:
60
60
 
61
61
 
62
62
  def _format_nested(value: Any) -> str:
63
- """Like ``_format_jsvalue`` but quotes nested strings."""
63
+ """Like `_format_jsvalue` but quotes nested strings."""
64
64
  if isinstance(value, str):
65
65
  return f'"{value}"'
66
66
  return _format_jsvalue(value)
@@ -84,8 +84,8 @@ def coerce_tool_output(value: Any) -> str:
84
84
 
85
85
 
86
86
  # Scalar types the quickjs_rs binding marshals natively. Compound shapes
87
- # (``dict`` / ``list`` / ``tuple``) are walked recursively in
88
- # ``_coerce_for_marshal``; anything else becomes ``str(value)`` so the JS
87
+ # (`dict` / `list` / `tuple`) are walked recursively in
88
+ # `_coerce_for_marshal`; anything else becomes `str(value)` so the JS
89
89
  # side can still see a usable value.
90
90
  _NATIVE_JS_SCALARS = (str, bool, int, float, type(None))
91
91
 
@@ -93,18 +93,18 @@ _NATIVE_JS_SCALARS = (str, bool, int, float, type(None))
93
93
  def coerce_tool_output_for_ptc(value: Any) -> Any:
94
94
  """Coerce a tool result for the PTC bridge, preserving native types.
95
95
 
96
- The quickjs_rs ``register`` bridge marshals Python primitives, ``list``,
97
- and ``dict`` directly to native JS values, so the model can use them
98
- without an explicit ``JSON.parse``. This helper unwraps LangChain's
99
- ``ToolMessage`` / ``Command`` envelopes (matching ``coerce_tool_output``'s
96
+ The quickjs_rs `register` bridge marshals Python primitives, `list`,
97
+ and `dict` directly to native JS values, so the model can use them
98
+ without an explicit `JSON.parse`. This helper unwraps LangChain's
99
+ `ToolMessage` / `Command` envelopes (matching `coerce_tool_output`'s
100
100
  selection rules) and returns the underlying value typed.
101
101
 
102
102
  Compound returns are walked recursively: nested values that the binding
103
- cannot marshal natively (``datetime``, Pydantic models, custom classes)
104
- are stringified in place via ``str(value)`` so the surrounding object
103
+ cannot marshal natively (`datetime`, Pydantic models, custom classes)
104
+ are stringified in place via `str(value)` so the surrounding object
105
105
  structure remains navigable from JS. Cyclic structures hit Python's
106
106
  recursion limit and surface as a host error in the eval — same outcome
107
- as ``json.dumps`` on a self-referencing dict.
107
+ as `json.dumps` on a self-referencing dict.
108
108
  """
109
109
  if isinstance(value, Command):
110
110
  return coerce_tool_output_for_ptc(_extract_command_content(value))
@@ -129,14 +129,14 @@ def _coerce_for_marshal(value: Any) -> Any:
129
129
  return [_coerce_for_marshal(v) for v in value]
130
130
  # Pydantic models are dumped to a plain dict so the JS side sees the
131
131
  # field shape its return-type signature advertises (rather than
132
- # ``str(model)``). Nested models / datetimes are handled by recursion.
132
+ # `str(model)`). Nested models / datetimes are handled by recursion.
133
133
  if isinstance(value, BaseModel):
134
134
  return _coerce_for_marshal(value.model_dump())
135
135
  return str(value)
136
136
 
137
137
 
138
138
  def _extract_command_content(command: Command) -> Any:
139
- """Return the trailing message content from a ``Command`` update, if any."""
139
+ """Return the trailing message content from a `Command` update, if any."""
140
140
  update = command.update
141
141
  if isinstance(update, dict):
142
142
  messages = update.get("messages")
@@ -45,6 +45,7 @@ flow, and synthesis - in plain JavaScript.
45
45
  await task({
46
46
  description, // full autonomous task prompt
47
47
  subagentType, // configured subagent name
48
+ label, // optional short UI label for this dispatch
48
49
  responseSchema, // optional JSON Schema for structured output
49
50
  }); // -> Promise<unknown>
50
51
  ```
@@ -62,6 +63,10 @@ exploring, still pass its path and let the subagent read it; do not paste back
62
63
  what you read. Each dispatch is stateless from the caller's perspective; you
63
64
  cannot send follow-up messages to the same subagent run.
64
65
 
66
+ `label` is optional: when provided, it is shown in the live progress UI
67
+ instead of the default description-derived fallback. It is not sent to the
68
+ subagent and does not affect execution.
69
+
65
70
  `responseSchema` is optional, but set it on any dispatch whose result feeds
66
71
  later code. A deterministic, typed shape is what lets you compose the next
67
72
  stage reliably — index it, sort it, compare fields, branch on it, merge it —
@@ -236,8 +241,10 @@ const findings = auditResults.flatMap((r) =>
236
241
  r.findings.map((f) => ({ ...f, file: r.file }))
237
242
  );
238
243
  const verified = await Promise.all(findings.map((f) =>
239
- task({ description: "Verify this finding: " + f.evidence, subagentType: "verifier" })
240
- .then((v) => ({ ...f, ...v }))
244
+ task({
245
+ description: "Verify this finding: " + f.evidence,
246
+ subagentType: "verifier",
247
+ }).then((v) => ({ ...f, ...v }))
241
248
  ));
242
249
  ```
243
250
 
@@ -260,10 +267,10 @@ def render_repl_system_prompt(
260
267
  mode: Literal["thread", "turn", "call"],
261
268
  ptc_attached: bool = False,
262
269
  ) -> str:
263
- """Render the base REPL system prompt text for ``CodeInterpreterMiddleware``.
270
+ """Render the base REPL system prompt text for `CodeInterpreterMiddleware`.
264
271
 
265
- ``ptc_attached`` controls the "external side effects" bullet: when host
266
- tools are exposed as the ``tools.*`` namespace it points the model at the
272
+ `ptc_attached` controls the "external side effects" bullet: when host
273
+ tools are exposed as the `tools.*` namespace it points the model at the
267
274
  API reference; otherwise it states the REPL is pure computation.
268
275
  """
269
276
  if ptc_attached:
@@ -362,7 +369,7 @@ def render_eval_tool_description(*, mode: Literal["thread", "turn", "call"]) ->
362
369
 
363
370
 
364
371
  def to_camel_case(name: str) -> str:
365
- """Convert ``snake_case`` / ``kebab-case````camelCase``."""
372
+ """Convert `snake_case` / `kebab-case``camelCase`."""
366
373
  return _CAMEL_SEP.sub(lambda m: m.group(1).upper(), name)
367
374
 
368
375
 
@@ -455,7 +462,7 @@ def _render_signature(
455
462
  ) -> str:
456
463
  return_clause = f"Promise<{return_type}>"
457
464
  default_signature = (
458
- f"async function {fn_name}(input: Record<string, unknown>): {return_clause}"
465
+ f"tools.{fn_name}(input: Record<string, unknown>): {return_clause}"
459
466
  )
460
467
  if not schema or not isinstance(schema.get("properties"), dict):
461
468
  return default_signature
@@ -471,20 +478,20 @@ def _render_signature(
471
478
  body = "\n".join(fields) if fields else ""
472
479
  if not body:
473
480
  return default_signature
474
- return f"async function {fn_name}(input: {{\n{body}\n}}): {return_clause}"
481
+ return f"tools.{fn_name}(input: {{\n{body}\n}}): {return_clause}"
475
482
 
476
483
 
477
484
  # Return types come from the tool's underlying function annotation. We feed
478
- # the annotation through ``pydantic.TypeAdapter`` to get a JSON Schema and
479
- # render it through the same ``_json_schema_to_ts`` we use for input args.
480
- # Compound shapes (TypedDict, BaseModel, recursive types) end up as ``$ref``
481
- # in the schema and currently render as ``unknown`` — same behaviour as
482
- # nested-model input args. Until that path resolves ``$ref`` / ``$defs``,
485
+ # the annotation through `pydantic.TypeAdapter` to get a JSON Schema and
486
+ # render it through the same `_json_schema_to_ts` we use for input args.
487
+ # Compound shapes (TypedDict, BaseModel, recursive types) end up as `$ref`
488
+ # in the schema and currently render as `unknown` — same behaviour as
489
+ # nested-model input args. Until that path resolves `$ref` / `$defs`,
483
490
  # the simpler unified renderer is the right trade-off here.
484
491
 
485
492
 
486
493
  def _render_return_type(tool: BaseTool) -> str:
487
- """Render the return annotation as a TS type, defaulting to ``unknown``."""
494
+ """Render the return annotation as a TS type, defaulting to `unknown`."""
488
495
  target = getattr(tool, "func", None) or getattr(tool, "coroutine", None)
489
496
  if target is None:
490
497
  return "unknown"
@@ -1,8 +1,8 @@
1
- """Programmatic tool calling (PTC) support for ``CodeInterpreterMiddleware``.
1
+ """Programmatic tool calling (PTC) support for `CodeInterpreterMiddleware`.
2
2
 
3
3
  PTC exposes the agent's LangChain tools inside the JavaScript REPL as
4
- ``tools.<camelCaseName>(input)`` async functions. Instead of issuing
5
- N serial tool calls, the model writes one ``eval`` that loops / parallelises
4
+ `tools.<camelCaseName>(input)` async functions. Instead of issuing
5
+ N serial tool calls, the model writes one `eval` that loops / parallelises
6
6
  / chains tools in-code:
7
7
 
8
8
  const [a, b] = await Promise.all([
@@ -14,10 +14,10 @@ Two pieces live here:
14
14
 
15
15
  - filtering — turn the live agent toolset into the subset exposed to PTC
16
16
  - prompt rendering — render a short TS-ish API-reference block describing
17
- each exposed tool, so the model knows the call shape
17
+ each exposed tool, so the model knows the call shape
18
18
 
19
19
  The host-function bridge that actually invokes each tool lives in
20
- ``_repl.py`` next to the rest of the context wiring.
20
+ `_repl.py` next to the rest of the context wiring.
21
21
  """
22
22
 
23
23
  from __future__ import annotations
@@ -34,6 +34,16 @@ if TYPE_CHECKING:
34
34
 
35
35
  PTCOption = list[str | BaseTool]
36
36
 
37
+ _RESERVED_SUBAGENT_TASK_NAME = "task"
38
+
39
+ _TASK_IN_PTC_MSG = (
40
+ "The subagent `task` tool cannot be exposed via `ptc`. It is always "
41
+ "available as the top-level `task()` global inside the REPL (with "
42
+ "`subagentType`, `label`, and `responseSchema` support); exposing it through "
43
+ "the `tools.*` namespace would create a second, conflicting dispatch path "
44
+ 'that drops `responseSchema`. Remove "task" from `ptc`.'
45
+ )
46
+
37
47
 
38
48
  def filter_tools_for_ptc(
39
49
  tools: Sequence[BaseTool],
@@ -41,23 +51,28 @@ def filter_tools_for_ptc(
41
51
  *,
42
52
  self_tool_name: str,
43
53
  ) -> list[BaseTool]:
44
- """Return the subset of ``tools`` exposed inside the REPL.
54
+ """Return the subset of `tools` exposed inside the REPL.
45
55
 
46
- ``self_tool_name`` is the REPL's own tool name; it is *always* excluded
47
- to prevent the model from recursing ``tools.eval("tools.eval(...)")``.
56
+ `self_tool_name` is the REPL's own tool name; it is *always* excluded
57
+ to prevent the model from recursing `tools.eval("tools.eval(...)")`.
48
58
  If the model wants a nested eval, it can just write nested code in one
49
59
  call — that's the whole point of PTC.
50
60
 
51
- ``config`` is allowlist-only:
61
+ `config` is allowlist-only:
52
62
 
53
- - ``str`` entries: expose matching tool names from ``tools``.
54
- - ``BaseTool`` entries: expose those tools directly (minus
55
- ``self_tool_name``).
63
+ - `str` entries: expose matching tool names from `tools`.
64
+ - `BaseTool` entries: expose those tools directly (minus
65
+ `self_tool_name`).
56
66
 
57
- Mixed lists are supported and merged. Explicit ``BaseTool`` entries
67
+ Mixed lists are supported and merged. Explicit `BaseTool` entries
58
68
  are included first, then name-matched agent tools are appended.
59
69
  Duplicate tool names are deduplicated.
60
70
 
71
+ The subagent `task` tool is reserved and may not appear in `config`
72
+ (by name or instance) — it is always available as the `task()` global,
73
+ so a `tools.task` PTC variant would be a conflicting, degraded duplicate.
74
+ A `"task"` entry raises `ValueError`.
75
+
61
76
  Warning:
62
77
  PTC tool calls execute through the REPL bridge and currently do
63
78
  not respect `interrupt_on` / HITL approval hooks for each
@@ -68,10 +83,14 @@ def filter_tools_for_ptc(
68
83
  allow_names: set[str] = set()
69
84
  for entry in config:
70
85
  if isinstance(entry, BaseTool):
86
+ if entry.name == _RESERVED_SUBAGENT_TASK_NAME:
87
+ raise ValueError(_TASK_IN_PTC_MSG)
71
88
  if entry.name != self_tool_name:
72
89
  explicit_tools.append(entry)
73
90
  continue
74
91
  if isinstance(entry, str):
92
+ if entry == _RESERVED_SUBAGENT_TASK_NAME:
93
+ raise ValueError(_TASK_IN_PTC_MSG)
75
94
  allow_names.add(entry)
76
95
  continue
77
96
  msg = "ptc list entries must be str or BaseTool"
@@ -98,7 +117,7 @@ def filter_tools_for_ptc(
98
117
 
99
118
 
100
119
  def to_camel_case(name: str) -> str:
101
- """Convert ``snake_case`` / ``kebab-case````camelCase``."""
120
+ """Convert `snake_case` / `kebab-case``camelCase`."""
102
121
  return _prompt.to_camel_case(name)
103
122
 
104
123