noah-code 0.5.0__tar.gz → 0.5.1__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 (143) hide show
  1. {noah_code-0.5.0 → noah_code-0.5.1}/PKG-INFO +5 -5
  2. noah_code-0.5.1/docs/releases/v0.5.1.md +36 -0
  3. {noah_code-0.5.0 → noah_code-0.5.1}/pyproject.toml +5 -5
  4. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/__init__.py +1 -1
  5. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/agent.py +37 -1
  6. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/nooa_compat.py +1 -1
  7. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/tools/workspace_tools.py +5 -3
  8. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_agent_security.py +7 -2
  9. noah_code-0.5.1/tests/test_lean_reasoning.py +201 -0
  10. noah_code-0.5.1/tests/test_nooa_migration.py +28 -0
  11. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_textual_tui.py +5 -1
  12. {noah_code-0.5.0 → noah_code-0.5.1}/uv.lock +11 -11
  13. {noah_code-0.5.0 → noah_code-0.5.1}/.github/workflows/ci.yml +0 -0
  14. {noah_code-0.5.0 → noah_code-0.5.1}/.github/workflows/release.yml +0 -0
  15. {noah_code-0.5.0 → noah_code-0.5.1}/.gitignore +0 -0
  16. {noah_code-0.5.0 → noah_code-0.5.1}/.pre-commit-config.yaml +0 -0
  17. {noah_code-0.5.0 → noah_code-0.5.1}/README.md +0 -0
  18. {noah_code-0.5.0 → noah_code-0.5.1}/docs/assets/noah-in-action.svg +0 -0
  19. {noah_code-0.5.0 → noah_code-0.5.1}/docs/assets/noah-logo.svg +0 -0
  20. {noah_code-0.5.0 → noah_code-0.5.1}/docs/configuration.md +0 -0
  21. {noah_code-0.5.0 → noah_code-0.5.1}/docs/development.md +0 -0
  22. {noah_code-0.5.0 → noah_code-0.5.1}/docs/extensions.md +0 -0
  23. {noah_code-0.5.0 → noah_code-0.5.1}/docs/interactive-reference.md +0 -0
  24. {noah_code-0.5.0 → noah_code-0.5.1}/docs/releases/v0.1.0.md +0 -0
  25. {noah_code-0.5.0 → noah_code-0.5.1}/docs/releases/v0.1.1.md +0 -0
  26. {noah_code-0.5.0 → noah_code-0.5.1}/docs/releases/v0.2.0.md +0 -0
  27. {noah_code-0.5.0 → noah_code-0.5.1}/docs/releases/v0.2.1.md +0 -0
  28. {noah_code-0.5.0 → noah_code-0.5.1}/docs/releases/v0.2.2.md +0 -0
  29. {noah_code-0.5.0 → noah_code-0.5.1}/docs/releases/v0.2.3.md +0 -0
  30. {noah_code-0.5.0 → noah_code-0.5.1}/docs/releases/v0.2.4.md +0 -0
  31. {noah_code-0.5.0 → noah_code-0.5.1}/docs/releases/v0.3.0.md +0 -0
  32. {noah_code-0.5.0 → noah_code-0.5.1}/docs/releases/v0.4.0.md +0 -0
  33. {noah_code-0.5.0 → noah_code-0.5.1}/docs/releases/v0.4.1.md +0 -0
  34. {noah_code-0.5.0 → noah_code-0.5.1}/docs/releases/v0.5.0.md +0 -0
  35. {noah_code-0.5.0 → noah_code-0.5.1}/docs/reliability.md +0 -0
  36. {noah_code-0.5.0 → noah_code-0.5.1}/docs/security.md +0 -0
  37. {noah_code-0.5.0 → noah_code-0.5.1}/install.sh +0 -0
  38. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/__main__.py +0 -0
  39. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/agents.py +0 -0
  40. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/approvals.py +0 -0
  41. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/budget.py +0 -0
  42. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/checkpoints.py +0 -0
  43. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/cli.py +0 -0
  44. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/commands.py +0 -0
  45. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/composer.py +0 -0
  46. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/config.py +0 -0
  47. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/credentials.py +0 -0
  48. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/custom_commands.py +0 -0
  49. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/event_bridge.py +0 -0
  50. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/events.py +0 -0
  51. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/github.py +0 -0
  52. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/hooks.py +0 -0
  53. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/host.py +0 -0
  54. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/llm.py +0 -0
  55. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/llm_replies.py +0 -0
  56. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/macos_sandbox.py +0 -0
  57. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/mcp_setup.py +0 -0
  58. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/permissions.py +0 -0
  59. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/predict.py +0 -0
  60. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/project_notes.py +0 -0
  61. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/providers.py +0 -0
  62. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/redaction.py +0 -0
  63. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/runtime_state.py +0 -0
  64. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/secure_files.py +0 -0
  65. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/sessions.py +0 -0
  66. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/skills_setup.py +0 -0
  67. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/snapshots.py +0 -0
  68. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/steer.py +0 -0
  69. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/summarization.py +0 -0
  70. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/telemetry.py +0 -0
  71. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/themes.py +0 -0
  72. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/tool_output.py +0 -0
  73. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/tools/__init__.py +0 -0
  74. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/tools/diff_tools.py +0 -0
  75. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/tools/git_tools.py +0 -0
  76. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/tools/github_tools.py +0 -0
  77. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/tools/lsp_tools.py +0 -0
  78. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/tools/media_tools.py +0 -0
  79. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/tools/memory_tools.py +0 -0
  80. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/tools/plan_tools.py +0 -0
  81. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/tools/process_tools.py +0 -0
  82. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/tools/question_tools.py +0 -0
  83. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/tools/task_tools.py +0 -0
  84. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/tools/web_tools.py +0 -0
  85. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/ui/__init__.py +0 -0
  86. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/ui/console.py +0 -0
  87. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/ui/protocol.py +0 -0
  88. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/ui/textual.css +0 -0
  89. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/ui/textual_app.py +0 -0
  90. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/updates.py +0 -0
  91. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/usage.py +0 -0
  92. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/workspace.py +0 -0
  93. {noah_code-0.5.0 → noah_code-0.5.1}/src/noah_code/worktree.py +0 -0
  94. {noah_code-0.5.0 → noah_code-0.5.1}/tests/conftest.py +0 -0
  95. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_agents.py +0 -0
  96. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_approvals.py +0 -0
  97. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_budget.py +0 -0
  98. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_cache_context.py +0 -0
  99. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_checkpoints.py +0 -0
  100. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_cli.py +0 -0
  101. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_commands.py +0 -0
  102. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_compaction_eviction.py +0 -0
  103. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_composer.py +0 -0
  104. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_config.py +0 -0
  105. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_console_ui.py +0 -0
  106. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_credentials.py +0 -0
  107. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_custom_commands.py +0 -0
  108. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_diff_tools.py +0 -0
  109. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_efficiency.py +0 -0
  110. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_event_bridge.py +0 -0
  111. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_event_bridge_and_shell.py +0 -0
  112. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_git_tools.py +0 -0
  113. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_github.py +0 -0
  114. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_hooks.py +0 -0
  115. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_host.py +0 -0
  116. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_installer.py +0 -0
  117. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_llm.py +0 -0
  118. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_llm_replies.py +0 -0
  119. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_lsp_tools.py +0 -0
  120. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_mcp_setup.py +0 -0
  121. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_permissions.py +0 -0
  122. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_plan_memory_tools.py +0 -0
  123. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_process_tools.py +0 -0
  124. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_project_notes.py +0 -0
  125. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_prompt_scorecard.py +0 -0
  126. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_providers.py +0 -0
  127. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_question_tools.py +0 -0
  128. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_redaction.py +0 -0
  129. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_run_exit.py +0 -0
  130. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_runtime_state.py +0 -0
  131. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_sessions.py +0 -0
  132. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_skills_setup.py +0 -0
  133. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_snapshots.py +0 -0
  134. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_steer.py +0 -0
  135. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_summarization.py +0 -0
  136. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_task_tools.py +0 -0
  137. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_telemetry.py +0 -0
  138. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_tool_output.py +0 -0
  139. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_updates.py +0 -0
  140. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_wave1_e2e.py +0 -0
  141. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_web_tools.py +0 -0
  142. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_workspace_tools.py +0 -0
  143. {noah_code-0.5.0 → noah_code-0.5.1}/tests/test_worktree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: noah-code
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: Noah Code terminal coding agent, built on NVIDIA OO Agents (NOOA)
5
5
  Project-URL: Homepage, https://github.com/skundu42/noah-code
6
6
  Project-URL: Documentation, https://github.com/skundu42/noah-code#readme
@@ -21,8 +21,8 @@ Classifier: Topic :: Software Development
21
21
  Requires-Python: <3.14,>=3.12
22
22
  Requires-Dist: click<9.0.0,>=8.1.0
23
23
  Requires-Dist: litellm<1.99.0,>=1.96.0
24
- Requires-Dist: nooa-cli==0.0.9
25
- Requires-Dist: nooa==0.0.9
24
+ Requires-Dist: nooa-cli==0.0.10
25
+ Requires-Dist: nooa==0.0.10
26
26
  Requires-Dist: packaging<27.0,>=24.0
27
27
  Requires-Dist: pydantic<3.0.0,>=2.5.0
28
28
  Requires-Dist: pyyaml<7.0.0,>=6.0.0
@@ -37,9 +37,9 @@ Requires-Dist: pytest>=8.0.0; extra == 'dev'
37
37
  Requires-Dist: ruff>=0.8.0; extra == 'dev'
38
38
  Requires-Dist: types-pyyaml>=6.0.12.20260815; extra == 'dev'
39
39
  Provides-Extra: mcp
40
- Requires-Dist: nooa[mcp]==0.0.9; extra == 'mcp'
40
+ Requires-Dist: nooa[mcp]==0.0.10; extra == 'mcp'
41
41
  Provides-Extra: tracing
42
- Requires-Dist: nooa[tracing]==0.0.9; extra == 'tracing'
42
+ Requires-Dist: nooa[tracing]==0.0.10; extra == 'tracing'
43
43
  Requires-Dist: opentelemetry-exporter-otlp-proto-http<2.0.0,>=1.44.0; extra == 'tracing'
44
44
  Description-Content-Type: text/markdown
45
45
 
@@ -0,0 +1,36 @@
1
+ # Noah Code v0.5.1
2
+
3
+ This release migrates Noah to NOOA 0.0.10 and preserves reasoning state in the default
4
+ lean execution mode.
5
+
6
+ ## Reasoning continuity
7
+
8
+ - Structured reasoning returned alongside tool calls is retained in lean-mode history and
9
+ included in the next provider request. Noah keeps its compact tool transcript while gaining
10
+ upstream's reasoning-state support.
11
+ - Regression checks cover the follow-up provider request, multiple tool calls, and responses
12
+ without reasoning state.
13
+
14
+ ## Runtime migration
15
+
16
+ - Pin NOOA and NOOA CLI, including MCP and tracing extras, to 0.0.10.
17
+ - Adopt upstream's shell framing fixes: malformed quotes and heredocs report syntax errors,
18
+ and commands that read stdin cannot consume the shell's internal control protocol.
19
+ - Carry upstream's configured error-output limits into Noah's macOS sandbox worker.
20
+ - Preserve Noah's background-task and recovery notifications after upstream's queue changes.
21
+ - Supply absolute file locations for upstream's updated edit anchors, keeping Noah's stale-edit
22
+ protections and workspace-relative display paths.
23
+
24
+ The migration also includes upstream's bounded, source-aware sandbox diagnostics and preservation
25
+ of MCP call timeouts during OAuth refresh.
26
+
27
+ ## Upgrade
28
+
29
+ ```bash
30
+ noah update
31
+ ```
32
+
33
+ Validation uses deterministic provider responses and runtime regressions. This release does not
34
+ claim measured live-provider quality improvements or general token savings.
35
+
36
+ **Full changelog:** https://github.com/skundu42/noah-code/compare/v0.5.0...v0.5.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "noah-code"
3
- version = "0.5.0"
3
+ version = "0.5.1"
4
4
  description = "Noah Code terminal coding agent, built on NVIDIA OO Agents (NOOA)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<3.14"
@@ -19,8 +19,8 @@ classifiers = [
19
19
  ]
20
20
 
21
21
  dependencies = [
22
- "nooa==0.0.9",
23
- "nooa-cli==0.0.9",
22
+ "nooa==0.0.10",
23
+ "nooa-cli==0.0.10",
24
24
  # LiteLLM 1.92.0-1.95.x intermittently shipped without macOS wheels,
25
25
  # forcing a Rust source build. Wheels are back for every release since
26
26
  # 1.96.0 (abi3 since 1.96.2), so the installer stays binary-only.
@@ -53,10 +53,10 @@ dev = [
53
53
  "types-pyyaml>=6.0.12.20260815",
54
54
  ]
55
55
  mcp = [
56
- "nooa[mcp]==0.0.9",
56
+ "nooa[mcp]==0.0.10",
57
57
  ]
58
58
  tracing = [
59
- "nooa[tracing]==0.0.9",
59
+ "nooa[tracing]==0.0.10",
60
60
  "opentelemetry-exporter-otlp-proto-http>=1.44.0,<2.0.0",
61
61
  ]
62
62
 
@@ -1,3 +1,3 @@
1
1
  """Noah Code: terminal coding agent on NVIDIA OO Agents."""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.5.1"
@@ -17,10 +17,14 @@ from typing import Annotated, Any, Literal
17
17
 
18
18
  from nooa import Agent, Context, hidden, strategy
19
19
  from nooa.config import CodeActConfig, PredictConfig
20
+ from nooa.context_blocks import RenderedMessage, ResolvedBlock, Role, ToolCallEvent
20
21
  from nooa.interactive import InteractiveAgent, RespondReason, RespondResult
22
+ from nooa.runtime.channels import Channel
21
23
  from nooa.runtime.restrictions import RESTRICTED_MODULES, RestrictionsConfig
22
24
  from nooa.runtime.sandbox.config import FileRule, SandboxConfig, resolve_spec
23
25
  from nooa.runtime.sandbox.executor import SandboxedExecutor
26
+ from nooa.runtime.sandbox.serialization import effective_error_limit
27
+ from nooa.storage.markers import nosnapshot
24
28
  from nooa.strategies import CodeActStrategy
25
29
  from nooa.strategies.codeact_lite import PlainCodeActBlockFormatter
26
30
  from nooa.tools import TodoManager
@@ -284,6 +288,8 @@ class _MacOSPermissionSandboxedExecutor(_PermissionSandboxedExecutor):
284
288
  cell_timeout: float | None,
285
289
  framework_builtins: dict[str, Any] | None = None,
286
290
  restrictions: Any = None,
291
+ max_error: int | None = None,
292
+ error_tail: int | None = None,
287
293
  ) -> None:
288
294
  # NOOA probes specifically for Linux Landlock/seccomp. Preserve
289
295
  # its worker and broker implementation, but install equivalent native
@@ -301,6 +307,8 @@ class _MacOSPermissionSandboxedExecutor(_PermissionSandboxedExecutor):
301
307
  self._cell_timeout = cell_timeout
302
308
  self._framework_builtins = framework_builtins or {}
303
309
  self._restrictions = restrictions
310
+ self._max_error = effective_error_limit(max_error)
311
+ self._error_tail = error_tail
304
312
  self._spec = resolve_spec(worker_config)
305
313
  self._degraded: list[str] = []
306
314
  # Forking a multithreaded Textual process can deadlock in inherited
@@ -347,6 +355,8 @@ class _MacOSPermissionSandboxedExecutor(_PermissionSandboxedExecutor):
347
355
  ),
348
356
  "restrictions": self._restrictions,
349
357
  "spec": self._spec,
358
+ "max_error": self._max_error,
359
+ "error_tail": self._error_tail,
350
360
  }
351
361
  )
352
362
  except Exception:
@@ -580,9 +590,31 @@ class _PermissionCodeActStrategy(CodeActStrategy):
580
590
  cell_timeout=self.config.cell_timeout,
581
591
  framework_builtins=framework_builtins,
582
592
  restrictions=self.config.restrictions,
593
+ max_error=runtime.truncation_config.capture.max_error,
594
+ error_tail=runtime.truncation_config.capture.tail,
583
595
  )
584
596
 
585
597
 
598
+ class _ReasoningPlainCodeActBlockFormatter(PlainCodeActBlockFormatter):
599
+ """Retain opaque reasoning state omitted by NOOA 0.0.10's plain formatter."""
600
+
601
+ def format(self, blocks: list[ResolvedBlock]) -> list[RenderedMessage]:
602
+ messages = super().format(blocks)
603
+ # Plain formatting preserves event order; IDs may repeat across turns.
604
+ reasoning_items = iter(
605
+ block.event.reasoning_items
606
+ for block in blocks
607
+ if block.role not in (Role.SYSTEM, Role.RUNTIME_EVENT)
608
+ and isinstance(block.event, ToolCallEvent)
609
+ )
610
+ for index, message in enumerate(messages):
611
+ if message.tool_call is not None:
612
+ reasoning = next(reasoning_items)
613
+ if reasoning:
614
+ messages[index] = message.model_copy(update={"reasoning_items": reasoning})
615
+ return messages
616
+
617
+
586
618
  class _LeanPermissionCodeActStrategy(_PermissionCodeActStrategy):
587
619
  """Compact NOOA event rendering while retaining cross-turn history."""
588
620
 
@@ -594,7 +626,9 @@ class _LeanPermissionCodeActStrategy(_PermissionCodeActStrategy):
594
626
  original = runtime.agent.render_config
595
627
  event_format = nooa_compat.truncation_event_format(runtime.agent)
596
628
  runtime.agent.render_config = original.model_copy(
597
- update={"block_formatter": PlainCodeActBlockFormatter(event_format=event_format)}
629
+ update={
630
+ "block_formatter": _ReasoningPlainCodeActBlockFormatter(event_format=event_format)
631
+ }
598
632
  )
599
633
  try:
600
634
  return await super().execute(runtime, call)
@@ -630,6 +664,7 @@ class CodingAgent(InteractiveAgent):
630
664
 
631
665
  workspace_root: str = "."
632
666
  mode: Literal["build", "plan"] = "build"
667
+ _system_messages_in: Annotated[Channel, hidden, nosnapshot]
633
668
 
634
669
  def __init__(
635
670
  self,
@@ -656,6 +691,7 @@ class CodingAgent(InteractiveAgent):
656
691
 
657
692
  llm = wrap_conversational_replies(llm)
658
693
  super().__init__(llm=llm, storage=storage, **kwargs)
694
+ self._system_messages_in = self.queue_manager.queue("system_messages")
659
695
  if cache_namespace:
660
696
  # NOOA derives prompt_cache_key from this id. A durable Noah
661
697
  # session therefore keeps provider cache affinity across restart.
@@ -1,7 +1,7 @@
1
1
  """Single seam for NOOA internals that have no public API yet.
2
2
 
3
3
  Every private-attribute reach-through into the framework lives here so an
4
- upgrade only requires auditing this one module. Pinned upstream: nooa==0.0.9.
4
+ upgrade only requires auditing this one module. Pinned upstream: nooa==0.0.10.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
@@ -343,7 +343,7 @@ class WorkspaceTools(Skill):
343
343
  with resolved.open(encoding="utf-8") as stream:
344
344
  content = stream.read()
345
345
  total = len(content.splitlines(keepends=True))
346
- return WorkspaceMatch(str(resolved), 1, total, content)
346
+ return WorkspaceMatch(str(resolved), 1, total, content, resolved_path=resolved)
347
347
 
348
348
  @staticmethod
349
349
  def _read_line_range(resolved: Path, lines: tuple[int, int]) -> WorkspaceMatch:
@@ -363,7 +363,7 @@ class WorkspaceTools(Skill):
363
363
  else:
364
364
  # The file ended before the range did; clamp like a full read.
365
365
  end = min(end, last)
366
- return WorkspaceMatch(str(resolved), start, end, "".join(collected))
366
+ return WorkspaceMatch(str(resolved), start, end, "".join(collected), resolved_path=resolved)
367
367
 
368
368
  async def search(
369
369
  self,
@@ -1322,7 +1322,9 @@ class WorkspaceTools(Skill):
1322
1322
  if not candidate.is_absolute():
1323
1323
  candidate = (base / candidate).resolve()
1324
1324
  rewritten.append(
1325
- WorkspaceMatch(str(candidate), match.start, match.end, match.text)
1325
+ WorkspaceMatch(
1326
+ str(candidate), match.start, match.end, match.text, resolved_path=candidate
1327
+ )
1326
1328
  )
1327
1329
  result.matches = rewritten
1328
1330
 
@@ -12,6 +12,8 @@ from types import SimpleNamespace
12
12
  import pytest
13
13
  from nooa.runtime.restrictions import RestrictionsConfig
14
14
  from nooa.runtime.sandbox.config import SandboxConfig
15
+ from nooa.runtime.sandbox.errors import SandboxExecutionError
16
+ from nooa.runtime.sandbox.serialization import effective_error_limit
15
17
 
16
18
  from noah_code.agent import (
17
19
  _codeact_config,
@@ -75,6 +77,7 @@ def test_sandbox_broker_exposes_only_permission_gated_capabilities() -> None:
75
77
  @pytest.mark.asyncio
76
78
  async def test_sandbox_cannot_call_the_host_only_readonly_runner() -> None:
77
79
  executor = object.__new__(_PermissionSandboxedExecutor)
80
+ executor._max_error = effective_error_limit(None)
78
81
  executor._agent = SimpleNamespace(
79
82
  ws=SimpleNamespace(run_trusted_readonly=lambda _command: "private host data")
80
83
  )
@@ -125,8 +128,10 @@ async def test_macos_sandbox_blocks_unbrokered_file_and_network_access() -> None
125
128
 
126
129
  assert safe.stdout.strip() == "2"
127
130
  assert safe.error is None
128
- assert isinstance(blocked_file.error, PermissionError)
129
- assert isinstance(blocked_network.error, PermissionError)
131
+ for blocked in (blocked_file, blocked_network):
132
+ assert isinstance(blocked.error, SandboxExecutionError)
133
+ assert blocked.error.original_type == "PermissionError"
134
+ assert isinstance(blocked.error.original_error, PermissionError)
130
135
 
131
136
 
132
137
  def test_spawn_safe_local_agent_drops_instance_file_descriptors() -> None:
@@ -0,0 +1,201 @@
1
+ """Compact history must replay opaque provider reasoning with its tool call."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+ from nooa.context_blocks import BlockMetadata, ResolvedBlock, Role, ToolCallEvent, ToolResult
11
+ from nooa.context_blocks.formatter import OpenAIProviderFormatter, ResponsesProviderFormatter
12
+ from nooa.interactive import RespondReason
13
+ from nooa.strategies.codeact_lite import PlainCodeActBlockFormatter
14
+ from nooa.unifiedllm import FakeLLMClient, LLMResponse, ToolCall
15
+
16
+ from noah_code import nooa_compat
17
+ from noah_code.agent import CodingAgent, _ReasoningPlainCodeActBlockFormatter
18
+ from noah_code.config import NoahCodeConfig
19
+ from noah_code.workspace import Workspace
20
+
21
+
22
+ @pytest.mark.parametrize(
23
+ "reasoning_items",
24
+ [None, [], [{"type": "reasoning", "id": "rs_fixture", "encrypted_content": "opaque-fixture"}]],
25
+ )
26
+ def test_lean_formatter_preserves_reasoning_without_changing_compact_history(reasoning_items):
27
+ blocks = [
28
+ ResolvedBlock(
29
+ key="instructions",
30
+ content="Stable instructions.",
31
+ metadata=BlockMetadata(static=True),
32
+ ),
33
+ ResolvedBlock(key="task", content="Inspect the repository.", role=Role.USER),
34
+ ]
35
+ for index in range(2):
36
+ call_id = f"call_{index}"
37
+ blocks.append(
38
+ ResolvedBlock(
39
+ key=call_id,
40
+ content="",
41
+ role=Role.ASSISTANT,
42
+ event=ToolCallEvent(
43
+ tool_call_id=call_id,
44
+ name="execute_python",
45
+ arguments={"code": f"print({index})"},
46
+ reasoning_items=reasoning_items if index == 0 else None,
47
+ result=ToolResult(tool_call_id=call_id, content=f"result {index}"),
48
+ ),
49
+ )
50
+ )
51
+
52
+ baseline = PlainCodeActBlockFormatter().format(blocks)
53
+ rendered = _ReasoningPlainCodeActBlockFormatter().format(blocks)
54
+
55
+ assert [message.model_dump(exclude={"reasoning_items"}) for message in rendered] == [
56
+ message.model_dump(exclude={"reasoning_items"}) for message in baseline
57
+ ]
58
+ assert [message.reasoning_items for message in rendered if message.tool_call] == [
59
+ reasoning_items or None,
60
+ None,
61
+ ]
62
+ assert all(message.reasoning_items is None for message in rendered if not message.tool_call)
63
+
64
+ chat_messages = OpenAIProviderFormatter().format(rendered)
65
+ calls = [message for message in chat_messages if message.get("tool_calls")]
66
+ assert calls[0].get("reasoning_items") == (reasoning_items or None)
67
+ assert "reasoning_items" not in calls[1]
68
+ responses_messages = ResponsesProviderFormatter().format(rendered)
69
+ assert [message for message in responses_messages if message.get("type") == "reasoning"] == (
70
+ reasoning_items or []
71
+ )
72
+ if reasoning_items:
73
+ first_call = next(
74
+ i
75
+ for i, message in enumerate(responses_messages)
76
+ if message.get("type") == "function_call"
77
+ )
78
+ assert responses_messages[first_call - 1] == reasoning_items[0]
79
+
80
+
81
+ @pytest.mark.parametrize("later_state", [None, [], [{"type": "reasoning", "id": "rs_later"}]])
82
+ def test_reused_tool_call_ids_keep_reasoning_with_the_original_event(later_state):
83
+ first_state = [{"type": "reasoning", "id": "rs_first"}]
84
+ blocks = [
85
+ ResolvedBlock(
86
+ key=f"event_{index}",
87
+ content="",
88
+ role=role,
89
+ event=ToolCallEvent(
90
+ tool_call_id="reused-call",
91
+ name="execute_python",
92
+ arguments={"code": "print('ok')"},
93
+ reasoning_items=state,
94
+ ),
95
+ )
96
+ for index, (role, state) in enumerate(
97
+ [
98
+ (Role.SYSTEM, [{"type": "reasoning", "id": "not-rendered-system"}]),
99
+ (Role.RUNTIME_EVENT, [{"type": "reasoning", "id": "not-rendered-runtime"}]),
100
+ (Role.ASSISTANT, first_state),
101
+ (Role.ASSISTANT, later_state),
102
+ ]
103
+ )
104
+ ]
105
+
106
+ rendered = _ReasoningPlainCodeActBlockFormatter().format(blocks)
107
+
108
+ assert [message.reasoning_items for message in rendered if message.tool_call] == [
109
+ first_state,
110
+ later_state or None,
111
+ ]
112
+ provider_messages = OpenAIProviderFormatter().format(rendered)
113
+ assert [
114
+ message.get("reasoning_items") for message in provider_messages if message.get("tool_calls")
115
+ ] == [first_state, later_state or None]
116
+
117
+
118
+ @pytest.mark.asyncio
119
+ @pytest.mark.parametrize("with_reasoning", [False, True])
120
+ async def test_default_lean_agent_replays_reasoning_on_next_model_call(
121
+ tmp_path: Path,
122
+ with_reasoning: bool,
123
+ ) -> None:
124
+ reasoning_items = [
125
+ {"type": "reasoning", "id": "rs_fixture", "encrypted_content": "opaque-fixture"}
126
+ ]
127
+ first_assistant = {"role": "assistant", "content": "", "tool_calls": []}
128
+ if with_reasoning:
129
+ first_assistant["reasoning_items"] = reasoning_items
130
+ llm = FakeLLMClient(
131
+ scripted_responses=[
132
+ LLMResponse(
133
+ raw_response=None,
134
+ content="",
135
+ finish_reason="tool_calls",
136
+ assistant_message=first_assistant,
137
+ tool_calls=[
138
+ ToolCall(
139
+ id=f"call_{index}",
140
+ name="execute_python",
141
+ arguments=json.dumps({"code": f"print('result {index}')"}),
142
+ )
143
+ for index in range(2)
144
+ ],
145
+ ),
146
+ LLMResponse(
147
+ raw_response=None,
148
+ content="",
149
+ finish_reason="tool_calls",
150
+ assistant_message={"role": "assistant", "content": "", "tool_calls": []},
151
+ tool_calls=[
152
+ ToolCall(
153
+ id="call_done",
154
+ name="execute_python",
155
+ arguments=json.dumps(
156
+ {
157
+ "code": 'return_result(RespondReason.DONE, explanation="verified")',
158
+ }
159
+ ),
160
+ )
161
+ ],
162
+ ),
163
+ ]
164
+ )
165
+ agent = CodingAgent(
166
+ Workspace(root=tmp_path.resolve()),
167
+ NoahCodeConfig(auto_approve=True, unsafe_inprocess_code_execution=True),
168
+ llm=llm,
169
+ nested=True,
170
+ )
171
+ try:
172
+ nooa_compat.queue_user_message(agent, "Run both checks.")
173
+ wins = await agent.queue_manager.race()
174
+ notification: dict[str, list] = {}
175
+ for name, item in wins:
176
+ notification.setdefault(name, []).append(item)
177
+ result = await asyncio.wait_for(agent.handle(notification), timeout=10)
178
+ finally:
179
+ await agent.close_tools()
180
+
181
+ assert result.kind == RespondReason.DONE
182
+ assert llm.call_count == 2
183
+ # NOOA also includes a synthetic prefill call before the model's history.
184
+ calls = [
185
+ message
186
+ for message in llm.last_messages
187
+ if message.get("tool_calls") and message["tool_calls"][0]["id"].startswith("call_")
188
+ ]
189
+ assert [message["tool_calls"][0]["id"] for message in calls] == ["call_0", "call_1"]
190
+ assert calls[0].get("reasoning_items") == (reasoning_items if with_reasoning else None)
191
+ assert "reasoning_items" not in calls[1]
192
+ results = [
193
+ message
194
+ for message in llm.last_messages
195
+ if message.get("role") == "tool" and message["tool_call_id"].startswith("call_")
196
+ ]
197
+ assert [(message["tool_call_id"], message["content"]) for message in results] == [
198
+ ("call_0", "result 0\n"),
199
+ ("call_1", "result 1\n"),
200
+ ]
201
+ assert all("opaque-fixture" not in str(message.get("content")) for message in llm.last_messages)
@@ -0,0 +1,28 @@
1
+ """Runtime behaviors required from the pinned NOOA release."""
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+ from nooa.tools.shell_tools import ShellTools
7
+
8
+
9
+ @pytest.mark.asyncio
10
+ @pytest.mark.parametrize("command,expected_codes", [("printf 'unterminated", (1, 2)), ("cat", (0,))])
11
+ async def test_shell_commands_cannot_consume_the_control_protocol(
12
+ tmp_path: Path, command: str, expected_codes: tuple[int, ...]
13
+ ) -> None:
14
+ shell = ShellTools(cwd=str(tmp_path))
15
+ try:
16
+ await shell.run("printf ready", timeout=2)
17
+ result = await shell.run(command, timeout=0.5)
18
+ # Bash 3 (macOS) and Bash 5 (Linux) use different syntax-error codes.
19
+ assert result.returncode in expected_codes
20
+ assert not result.timed_out
21
+ assert result.stdout == ""
22
+ if result.returncode:
23
+ assert "unexpected EOF" in result.stderr
24
+ recovered = await shell.run("printf recovered", timeout=2)
25
+ assert recovered.returncode == 0
26
+ assert recovered.stdout == "recovered"
27
+ finally:
28
+ await shell.close()
@@ -2291,7 +2291,11 @@ async def test_live_tool_output_stays_compact_until_expanded(tmp_path: Path) ->
2291
2291
  meta={"activity_id": "tool-1", "stream": "stdout"},
2292
2292
  )
2293
2293
  )
2294
- await pilot.pause(0.08)
2294
+ # Stream output flushes on a separate timer after host events drain.
2295
+ output = app.query_one("#activity-output")
2296
+ async with asyncio.timeout(5):
2297
+ while "five" not in _log_text(output):
2298
+ await pilot.pause()
2295
2299
 
2296
2300
  live = app.query_one("#live-activity")
2297
2301
  assert live.styles.height.value == 6
@@ -1021,7 +1021,7 @@ wheels = [
1021
1021
 
1022
1022
  [[package]]
1023
1023
  name = "noah-code"
1024
- version = "0.5.0"
1024
+ version = "0.5.1"
1025
1025
  source = { editable = "." }
1026
1026
  dependencies = [
1027
1027
  { name = "click" },
@@ -1058,10 +1058,10 @@ requires-dist = [
1058
1058
  { name = "click", specifier = ">=8.1.0,<9.0.0" },
1059
1059
  { name = "litellm", specifier = ">=1.96.0,<1.99.0" },
1060
1060
  { name = "mypy", marker = "extra == 'dev'", specifier = ">=2.3.1" },
1061
- { name = "nooa", specifier = "==0.0.9" },
1062
- { name = "nooa", extras = ["mcp"], marker = "extra == 'mcp'", specifier = "==0.0.9" },
1063
- { name = "nooa", extras = ["tracing"], marker = "extra == 'tracing'", specifier = "==0.0.9" },
1064
- { name = "nooa-cli", specifier = "==0.0.9" },
1061
+ { name = "nooa", specifier = "==0.0.10" },
1062
+ { name = "nooa", extras = ["mcp"], marker = "extra == 'mcp'", specifier = "==0.0.10" },
1063
+ { name = "nooa", extras = ["tracing"], marker = "extra == 'tracing'", specifier = "==0.0.10" },
1064
+ { name = "nooa-cli", specifier = "==0.0.10" },
1065
1065
  { name = "opentelemetry-exporter-otlp-proto-http", marker = "extra == 'tracing'", specifier = ">=1.44.0,<2.0.0" },
1066
1066
  { name = "packaging", specifier = ">=24.0,<27.0" },
1067
1067
  { name = "pydantic", specifier = ">=2.5.0,<3.0.0" },
@@ -1079,7 +1079,7 @@ provides-extras = ["dev", "mcp", "tracing"]
1079
1079
 
1080
1080
  [[package]]
1081
1081
  name = "nooa"
1082
- version = "0.0.9"
1082
+ version = "0.0.10"
1083
1083
  source = { registry = "https://pypi.org/simple" }
1084
1084
  dependencies = [
1085
1085
  { name = "httpx" },
@@ -1087,9 +1087,9 @@ dependencies = [
1087
1087
  { name = "openinference-instrumentation-litellm" },
1088
1088
  { name = "pydantic" },
1089
1089
  ]
1090
- sdist = { url = "https://files.pythonhosted.org/packages/34/6f/c4e3ea70b373f72f98c12d23fd61f8525462bcecb4a969b58fbb67092699/nooa-0.0.9.tar.gz", hash = "sha256:c7a90ffb5b54094180efcb845e74a00c4182766745245f4258e613f9c12cb522", size = 3315964, upload-time = "2026-08-18T15:00:15.26Z" }
1090
+ sdist = { url = "https://files.pythonhosted.org/packages/27/90/f07e44b57d10e3687caba9b18a1b2064e7c9602f85fdeb9c9b0a4843aeba/nooa-0.0.10.tar.gz", hash = "sha256:91d44a48610b116561d2ddc42ed80a7c4011c7324a67236300d2fe78182d3c78", size = 3504802, upload-time = "2026-09-04T06:04:36.719Z" }
1091
1091
  wheels = [
1092
- { url = "https://files.pythonhosted.org/packages/ae/27/11b2754bc31994244418d0a55447f20bc0efbd9d2573802db13c7ca367b5/nooa-0.0.9-py3-none-any.whl", hash = "sha256:60b64663f34a47aec206137035d62e816c1147484bb74a8a4700a47d1a10b60c", size = 1104654, upload-time = "2026-08-18T15:00:13.588Z" },
1092
+ { url = "https://files.pythonhosted.org/packages/02/84/ec56e26a39873862f046146bf677b1d259e7e923180f508747baa1076ed9/nooa-0.0.10-py3-none-any.whl", hash = "sha256:8a74d3e62bb4a216d6fcb1ba912b5d9e49ecc0bf8c2d685f808594a816cbe4ca", size = 1123235, upload-time = "2026-09-04T06:04:38.491Z" },
1093
1093
  ]
1094
1094
 
1095
1095
  [package.optional-dependencies]
@@ -1106,16 +1106,16 @@ tracing = [
1106
1106
 
1107
1107
  [[package]]
1108
1108
  name = "nooa-cli"
1109
- version = "0.0.9"
1109
+ version = "0.0.10"
1110
1110
  source = { registry = "https://pypi.org/simple" }
1111
1111
  dependencies = [
1112
1112
  { name = "click" },
1113
1113
  { name = "nooa" },
1114
1114
  { name = "pyyaml" },
1115
1115
  ]
1116
- sdist = { url = "https://files.pythonhosted.org/packages/41/0b/ab13465d13cfa6b29e1521e672ea44e02a70ebd4977df7c2b4ed34f0d038/nooa_cli-0.0.9.tar.gz", hash = "sha256:5bc45f6328fd4cb02531278db1779f51a83f3b8e631cba818caf133f3631e4c8", size = 68335, upload-time = "2026-08-18T14:59:30.3Z" }
1116
+ sdist = { url = "https://files.pythonhosted.org/packages/36/c6/d1e555b461b508cf43b9c8ba55f3359dc94fbcbda231013c101ef5543297/nooa_cli-0.0.10.tar.gz", hash = "sha256:845d029afaed26166ca16ed15bfa69308703aac7aefc6afece5cddfa47ecbf6e", size = 89911, upload-time = "2026-09-04T06:04:33.448Z" }
1117
1117
  wheels = [
1118
- { url = "https://files.pythonhosted.org/packages/8a/34/9acdbac505979b0f765288db9272b10205674240689a9a3901d535412d38/nooa_cli-0.0.9-py3-none-any.whl", hash = "sha256:e8fad7b7a1601c9d7206c8d92c3cabeca2539af96c7e75923261b096a3616b09", size = 72613, upload-time = "2026-08-18T14:59:29.324Z" },
1118
+ { url = "https://files.pythonhosted.org/packages/4a/cd/c9a53e62e08394b30f0f2de466f6d61b2ab66b32de0be1e23be2ff9ffbfa/nooa_cli-0.0.10-py3-none-any.whl", hash = "sha256:0a9414898f12c51797b0ba2cf8ece97899dcf30e684690d72b54cceac6afdb4e", size = 86303, upload-time = "2026-09-04T06:04:34.245Z" },
1119
1119
  ]
1120
1120
 
1121
1121
  [[package]]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes