noah-code 0.2.4__tar.gz → 0.3.0__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.
- {noah_code-0.2.4 → noah_code-0.3.0}/PKG-INFO +2 -1
- {noah_code-0.2.4 → noah_code-0.3.0}/docs/configuration.md +18 -1
- {noah_code-0.2.4 → noah_code-0.3.0}/docs/extensions.md +44 -5
- noah_code-0.3.0/docs/releases/v0.3.0.md +75 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/pyproject.toml +2 -1
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/__init__.py +1 -1
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/agent.py +42 -1
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/approvals.py +22 -10
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/budget.py +44 -12
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/cli.py +55 -11
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/config.py +33 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/event_bridge.py +13 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/host.py +48 -17
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/permissions.py +114 -4
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/sessions.py +18 -3
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/snapshots.py +23 -6
- noah_code-0.3.0/src/noah_code/telemetry.py +801 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/tool_output.py +16 -9
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/tools/git_tools.py +25 -8
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/tools/github_tools.py +51 -5
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/tools/lsp_tools.py +77 -30
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/tools/process_tools.py +15 -5
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/tools/question_tools.py +19 -4
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/tools/web_tools.py +49 -11
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/tools/workspace_tools.py +10 -1
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/updates.py +74 -19
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/worktree.py +10 -3
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_agent_security.py +6 -4
- noah_code-0.3.0/tests/test_approvals.py +113 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_budget.py +43 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_cache_context.py +50 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_cli.py +56 -3
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_config.py +21 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_event_bridge.py +50 -4
- noah_code-0.3.0/tests/test_git_tools.py +270 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_github.py +52 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_host.py +60 -2
- noah_code-0.3.0/tests/test_lsp_tools.py +262 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_permissions.py +108 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_process_tools.py +168 -2
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_question_tools.py +57 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_sessions.py +63 -0
- noah_code-0.3.0/tests/test_snapshots.py +377 -0
- noah_code-0.3.0/tests/test_telemetry.py +217 -0
- noah_code-0.3.0/tests/test_tool_output.py +192 -0
- noah_code-0.3.0/tests/test_updates.py +399 -0
- noah_code-0.3.0/tests/test_web_tools.py +565 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_workspace_tools.py +25 -0
- noah_code-0.3.0/tests/test_worktree.py +356 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/uv.lock +72 -1
- noah_code-0.2.4/tests/test_approvals.py +0 -54
- noah_code-0.2.4/tests/test_git_tools.py +0 -107
- noah_code-0.2.4/tests/test_lsp_tools.py +0 -90
- noah_code-0.2.4/tests/test_snapshots.py +0 -99
- noah_code-0.2.4/tests/test_updates.py +0 -102
- noah_code-0.2.4/tests/test_web_tools.py +0 -166
- noah_code-0.2.4/tests/test_worktree.py +0 -92
- {noah_code-0.2.4 → noah_code-0.3.0}/.github/workflows/ci.yml +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/.github/workflows/release.yml +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/.gitignore +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/.pre-commit-config.yaml +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/README.md +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/docs/assets/noah-in-action.svg +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/docs/assets/noah-logo.svg +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/docs/development.md +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/docs/interactive-reference.md +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/docs/releases/v0.1.0.md +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/docs/releases/v0.1.1.md +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/docs/releases/v0.2.0.md +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/docs/releases/v0.2.1.md +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/docs/releases/v0.2.2.md +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/docs/releases/v0.2.3.md +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/docs/releases/v0.2.4.md +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/docs/reliability.md +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/docs/security.md +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/install.sh +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/__main__.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/agents.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/checkpoints.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/commands.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/composer.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/credentials.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/custom_commands.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/events.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/github.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/hooks.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/llm.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/llm_replies.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/macos_sandbox.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/mcp_setup.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/nooa_compat.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/project_notes.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/providers.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/redaction.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/runtime_state.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/secure_files.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/skills_setup.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/steer.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/summarization.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/themes.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/tools/__init__.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/tools/diff_tools.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/tools/media_tools.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/tools/memory_tools.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/tools/plan_tools.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/tools/task_tools.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/ui/__init__.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/ui/console.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/ui/protocol.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/ui/textual.css +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/ui/textual_app.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/usage.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/src/noah_code/workspace.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/conftest.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_agents.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_checkpoints.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_compaction_eviction.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_composer.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_console_ui.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_credentials.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_custom_commands.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_diff_tools.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_efficiency.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_event_bridge_and_shell.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_hooks.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_installer.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_llm.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_llm_replies.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_mcp_setup.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_plan_memory_tools.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_project_notes.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_providers.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_redaction.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_run_exit.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_runtime_state.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_skills_setup.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_steer.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_summarization.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_task_tools.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_textual_tui.py +0 -0
- {noah_code-0.2.4 → noah_code-0.3.0}/tests/test_wave1_e2e.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: noah-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -40,6 +40,7 @@ Provides-Extra: mcp
|
|
|
40
40
|
Requires-Dist: nooa[mcp]==0.0.9; extra == 'mcp'
|
|
41
41
|
Provides-Extra: tracing
|
|
42
42
|
Requires-Dist: nooa[tracing]==0.0.9; extra == 'tracing'
|
|
43
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2.0.0,>=1.44.0; extra == 'tracing'
|
|
43
44
|
Description-Content-Type: text/markdown
|
|
44
45
|
|
|
45
46
|
<div align="center">
|
|
@@ -179,8 +179,19 @@ target_chars = 2500
|
|
|
179
179
|
|
|
180
180
|
[tracing]
|
|
181
181
|
enabled = true
|
|
182
|
-
#
|
|
182
|
+
# Local traces remain available without a collector.
|
|
183
|
+
jsonl_enabled = true
|
|
183
184
|
# jsonl_dir = "~/.local/share/noah-code/traces"
|
|
185
|
+
# Send traces, metrics, and structured logs over OTLP/HTTP.
|
|
186
|
+
# otlp_endpoint = "http://localhost:4318"
|
|
187
|
+
logs_enabled = true
|
|
188
|
+
metrics_enabled = true
|
|
189
|
+
# Sensitive agent content is excluded unless explicitly opted in.
|
|
190
|
+
capture_content = false
|
|
191
|
+
max_span_attributes = 256
|
|
192
|
+
max_attribute_length = 4096
|
|
193
|
+
metric_export_interval_millis = 30000
|
|
194
|
+
export_timeout_millis = 5000
|
|
184
195
|
|
|
185
196
|
[updates]
|
|
186
197
|
auto_install = false
|
|
@@ -199,6 +210,12 @@ Supported environment overrides include:
|
|
|
199
210
|
- `NOAH_CODE_UNSAFE_INPROCESS`
|
|
200
211
|
- `NOAH_CODE_AUTO_UPDATE`
|
|
201
212
|
|
|
213
|
+
OpenTelemetry's standard environment variables are also honored. In particular,
|
|
214
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT` enables remote traces, metrics, and logs without storing an endpoint
|
|
215
|
+
in Noah configuration. Signal-specific endpoint variables and `OTEL_EXPORTER_OTLP_HEADERS` are
|
|
216
|
+
supported by the OTLP exporters; keep authentication headers in the environment or a collector
|
|
217
|
+
secret store rather than TOML.
|
|
218
|
+
|
|
202
219
|
Repository-controlled configuration cannot weaken the host trust boundary. Project config is
|
|
203
220
|
ignored for `auto_approve`, `budget`, `efficiency`, `enabled_skills`, `hooks`, `lsp`, `mcp`,
|
|
204
221
|
`permission_rules`, `processes`, `reliability`, `session_dir`, `tracing`, `updates`, and
|
|
@@ -149,8 +149,47 @@ local executables; define them only in `~/.config/noah-code/config.toml`.
|
|
|
149
149
|
|
|
150
150
|
## Tracing
|
|
151
151
|
|
|
152
|
-
Noah Code
|
|
153
|
-
|
|
154
|
-
`tracing.jsonl_dir` selects another trusted location.
|
|
155
|
-
`/
|
|
156
|
-
session-storage quota when they use the default
|
|
152
|
+
Noah Code emits OpenTelemetry traces for the complete agent invocation and NOOA/OpenInference child
|
|
153
|
+
spans for model generations and tool execution. Local JSONL traces are enabled by default and go
|
|
154
|
+
into the active session directory unless `tracing.jsonl_dir` selects another trusted location.
|
|
155
|
+
Use `/trace` to inspect active destinations and `/health` to inspect bounded runtime-event and
|
|
156
|
+
artifact state. Trace files count toward the session-storage quota when they use the default path.
|
|
157
|
+
|
|
158
|
+
Set an OTLP/HTTP collector endpoint to additionally export batched traces, metrics, and structured
|
|
159
|
+
logs:
|
|
160
|
+
|
|
161
|
+
```toml
|
|
162
|
+
[tracing]
|
|
163
|
+
otlp_endpoint = "http://localhost:4318"
|
|
164
|
+
logs_enabled = true
|
|
165
|
+
metrics_enabled = true
|
|
166
|
+
capture_content = false
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Install the `tracing` extra when Noah was installed without the standard installer. Configured
|
|
170
|
+
remote endpoints must use HTTPS; plain HTTP is accepted only for a loopback collector.
|
|
171
|
+
|
|
172
|
+
Or use standard OpenTelemetry environment configuration:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
export OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
|
|
176
|
+
export OTEL_EXPORTER_OTLP_HEADERS='authorization=Bearer%20...'
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Operational telemetry includes agent/LLM/tool duration, token usage, cached and reasoning token
|
|
180
|
+
counts, estimated cost, retries, outcomes, model/provider names, and trace-correlated lifecycle
|
|
181
|
+
events. Metric labels deliberately exclude session IDs, run IDs, workspace paths, prompts, and tool
|
|
182
|
+
targets to avoid high-cardinality series.
|
|
183
|
+
|
|
184
|
+
Content capture is off by default. Prompt and response messages, reasoning, generated code, tool
|
|
185
|
+
arguments/results, exception messages, stack traces, and file contents are removed at the exporter
|
|
186
|
+
boundary. `capture_content = true` is intended only for controlled development environments; values
|
|
187
|
+
and span attribute counts remain bounded, and NOOA's secret scrubber still runs before export. For
|
|
188
|
+
production, keep content disabled and apply an allowlist/redaction processor in the collector as a
|
|
189
|
+
second boundary.
|
|
190
|
+
|
|
191
|
+
The emitted standard instruments include `gen_ai.invoke_agent.duration`,
|
|
192
|
+
`gen_ai.client.operation.duration`, and `gen_ai.client.token.usage`. Noah-specific instruments cover
|
|
193
|
+
estimated cost, retries, and tool execution counts/duration. Export is best-effort and batched;
|
|
194
|
+
collector failure never blocks or fails an agent turn, and providers are flushed during orderly
|
|
195
|
+
shutdown.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Noah Code v0.3.0
|
|
2
|
+
|
|
3
|
+
This release adds production-grade OpenTelemetry observability, more control over long-running
|
|
4
|
+
agents, and a broad hardening pass across permissions, updates, sessions, worktrees, language
|
|
5
|
+
servers, and persisted tool state.
|
|
6
|
+
|
|
7
|
+
## Observability
|
|
8
|
+
|
|
9
|
+
- Added privacy-first OpenTelemetry support for the complete agent lifecycle, with OTLP/HTTP
|
|
10
|
+
export of traces, metrics, and structured logs alongside the existing local JSONL traces.
|
|
11
|
+
- Added standard GenAI instruments for agent and model duration and token usage, plus Noah-specific
|
|
12
|
+
cost, retry, and tool-execution metrics with deliberately low-cardinality labels.
|
|
13
|
+
- Content capture remains off by default. Exporters strip prompts, responses, reasoning, tool
|
|
14
|
+
arguments and results, exception details, host identity, and repository metadata unless content
|
|
15
|
+
capture is explicitly enabled; exported values and attribute counts remain bounded.
|
|
16
|
+
- Added secure collector configuration through `[tracing]` and standard OpenTelemetry environment
|
|
17
|
+
variables. Remote collectors require HTTPS, while plain HTTP remains available for loopback
|
|
18
|
+
development collectors.
|
|
19
|
+
|
|
20
|
+
## Agent controls and permissions
|
|
21
|
+
|
|
22
|
+
- Added `--max-iterations` and `NOAH_CODE_MAX_ITERATIONS` so larger or slower models can be given a
|
|
23
|
+
higher per-request tool-call budget than the default of 40.
|
|
24
|
+
- Added an explicit `--yolo` mode and matching configuration option for fully unrestricted local
|
|
25
|
+
operation. This bypasses every permission check, including secret, plan-mode, and destructive
|
|
26
|
+
command protections, and should only be used in an appropriately isolated environment.
|
|
27
|
+
- Added `run(..., read_only=True)` for trusted execution of whitelisted inspection commands inside
|
|
28
|
+
the sandbox.
|
|
29
|
+
- Read-only pipelines such as `git show | sed` and `rg | sort | wc` are now recognized when every
|
|
30
|
+
segment is independently read-only. Stream discards to `/dev/null` are also accepted without
|
|
31
|
+
weakening protections for file redirection or control-flow chains.
|
|
32
|
+
- Repaired disk-destruction deny rules so raw device writes are rejected correctly, and placed
|
|
33
|
+
ordinary `dd` use on the elevated-risk approval floor.
|
|
34
|
+
- Closed a concurrent approval race that could show duplicate prompts or miss a newly registered
|
|
35
|
+
session rule.
|
|
36
|
+
|
|
37
|
+
## Reliability and correctness
|
|
38
|
+
|
|
39
|
+
- Hardened language-server lifecycle management against timeout races, dead readers, failed
|
|
40
|
+
startup, and zombie processes; stale servers are now terminated and reaped before restart.
|
|
41
|
+
- Host shutdown now completes all teardown steps even under cancellation, and session switches
|
|
42
|
+
validate their targets before releasing the active session.
|
|
43
|
+
- Damaged session metadata and partially corrupted undo/redo journals now degrade to clear recovery
|
|
44
|
+
errors or skip only the affected records instead of crashing startup.
|
|
45
|
+
- Managed tool output is published atomically by content hash, with clean recovery from disk-full
|
|
46
|
+
and concurrent-writer races.
|
|
47
|
+
- Durable background-process log rotation now preserves the latest output line even when the final
|
|
48
|
+
pipe read contains only a small trailing fragment.
|
|
49
|
+
- Auto-update state paths, network reads, and version parsing are hardened, and a stale-aware lock
|
|
50
|
+
prevents concurrent processes from running duplicate upgrades.
|
|
51
|
+
- Orphaned worktrees whose directories were removed outside Noah can now be pruned and cleaned up.
|
|
52
|
+
- Git and GitHub helpers now turn missing binaries, timeouts, authentication failures, and network
|
|
53
|
+
failures into actionable user-facing errors.
|
|
54
|
+
- Fixed question timeout and background-process stdin races, bounded HTTP 429 retries using
|
|
55
|
+
`Retry-After`, and excluded skipped HTML blocks such as `noscript` from fetched page text.
|
|
56
|
+
- Non-finite cost and token reports can no longer disable budget enforcement or crash accounting.
|
|
57
|
+
|
|
58
|
+
## Agent guidance and tests
|
|
59
|
+
|
|
60
|
+
- Refined the model-facing tool guidance for synchronous messages, `return_result`, safe read-only
|
|
61
|
+
execution, forbidden imports, edit arguments, and iteration-limit recovery to avoid wasted turns.
|
|
62
|
+
- Expanded coverage across permissions, web transport, updates, snapshots, tool output, worktrees,
|
|
63
|
+
process jobs, Git, GitHub, sessions, LSP lifecycle, budgets, and host teardown.
|
|
64
|
+
|
|
65
|
+
## Upgrade
|
|
66
|
+
|
|
67
|
+
Existing managed installations can run:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
noah update
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
New installations can use the one-command installer from the README.
|
|
74
|
+
|
|
75
|
+
**Full changelog:** https://github.com/skundu42/noah-code/compare/v0.2.4...v0.3.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "noah-code"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
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"
|
|
@@ -57,6 +57,7 @@ mcp = [
|
|
|
57
57
|
]
|
|
58
58
|
tracing = [
|
|
59
59
|
"nooa[tracing]==0.0.9",
|
|
60
|
+
"opentelemetry-exporter-otlp-proto-http>=1.44.0,<2.0.0",
|
|
60
61
|
]
|
|
61
62
|
|
|
62
63
|
[project.scripts]
|
|
@@ -124,6 +124,7 @@ class _PermissionSandboxedExecutor(SandboxedExecutor):
|
|
|
124
124
|
("ws", "read_output"),
|
|
125
125
|
("ws", "replace"),
|
|
126
126
|
("ws", "run"),
|
|
127
|
+
("ws", "run_trusted_readonly"),
|
|
127
128
|
("ws", "search"),
|
|
128
129
|
("ws", "write"),
|
|
129
130
|
("ws", "write_file"),
|
|
@@ -469,6 +470,7 @@ class CodingAgent(InteractiveAgent):
|
|
|
469
470
|
config.permission_rules,
|
|
470
471
|
mode=config.mode,
|
|
471
472
|
auto_approve=config.auto_approve,
|
|
473
|
+
yolo=config.yolo,
|
|
472
474
|
)
|
|
473
475
|
self._engine.mode = config.mode
|
|
474
476
|
self._approvals = approvals or ApprovalBroker(
|
|
@@ -859,6 +861,11 @@ class CodingAgent(InteractiveAgent):
|
|
|
859
861
|
in the notification. Understand the requested end state before acting.
|
|
860
862
|
Conversational questions are first-class: answer them with
|
|
861
863
|
``self.message(...)`` then return DONE. Do not emit bare assistant prose.
|
|
864
|
+
IMPORTANT: ``self.message(...)`` is SYNCHRONOUS — call it WITHOUT
|
|
865
|
+
``await`` (``self.message("...")``). Prefixing ``await`` raises
|
|
866
|
+
``TypeError: object NoneType can't be used in 'await' expression``
|
|
867
|
+
because it returns ``None``. All ``self.ws.*``, ``self.web.*``,
|
|
868
|
+
``self.lsp.*``, ``self.processes.*`` calls ARE async and need ``await``.
|
|
862
869
|
|
|
863
870
|
Minimal tool cookbook:
|
|
864
871
|
- ``await self.ws.list("**/*.py")`` lists files.
|
|
@@ -866,7 +873,10 @@ class CodingAgent(InteractiveAgent):
|
|
|
866
873
|
- ``match = await self.ws.read("path.py", lines=(10, 30))`` returns an
|
|
867
874
|
editable Match; ``await self.ws.replace(match, "replacement")`` edits it.
|
|
868
875
|
- ``await self.ws.edit("path.py", "unique old text", "new text")`` is the
|
|
869
|
-
simple string-edit form
|
|
876
|
+
simple string-edit form and takes exactly three arguments (path, old,
|
|
877
|
+
new); two-argument calls are invalid. For a Match from read(), edit
|
|
878
|
+
with ``await self.ws.replace(match, "replacement")`` instead.
|
|
879
|
+
``await self.ws.write("new.py", content)`` creates files.
|
|
870
880
|
- Prefer ``await self.ws.apply_patch(changes)`` for coherent edits. Each change is
|
|
871
881
|
``{"path": ..., "old": exact_text_or_None, "new": replacement_or_None}``;
|
|
872
882
|
one call validates and atomically commits the full batch.
|
|
@@ -878,6 +888,19 @@ class CodingAgent(InteractiveAgent):
|
|
|
878
888
|
long-running commands. Consume logs by cursor; do not poll without new work.
|
|
879
889
|
- ``result = await self.ws.run("pytest -q")`` runs validation; inspect
|
|
880
890
|
``result.returncode``, ``result.stdout``, and ``result.stderr``.
|
|
891
|
+
``read_only=True`` skips approval ONLY for commands the engine
|
|
892
|
+
recognizes as read-only (``git status/log/diff/show``, ``rg``,
|
|
893
|
+
``grep``, ``ls``, ``find`` (no -delete/-exec), ``head``, ``tail``,
|
|
894
|
+
``wc``, ``sed``, ``awk``, ``sort``, ``uniq``, ``cut``, ``tr``,
|
|
895
|
+
``tac``, ``column``, ``pwd``, ``file``, ``stat``, ``test``). It is
|
|
896
|
+
REJECTED for anything else — including ``pytest``, ``uv``, ``python``,
|
|
897
|
+
and build commands — so do NOT pass ``read_only=True`` for those; run
|
|
898
|
+
them with plain ``await self.ws.run(cmd)`` (YOLO auto-approves;
|
|
899
|
+
``--auto`` prompts).
|
|
900
|
+
- If the host was launched with ``--yolo``, every approval is granted
|
|
901
|
+
automatically without prompting. That mode exists for throwaway or
|
|
902
|
+
sandboxed environments only; do not assume it is active — write code
|
|
903
|
+
that works under normal permission gating.
|
|
881
904
|
- ``await self.web.fetch(url)`` reads a page; ``await self.web.search(query)``
|
|
882
905
|
searches the public web. Both are read-only and allowed by default.
|
|
883
906
|
- ``await self.github.list()`` / ``view(number)`` inspect pull requests.
|
|
@@ -919,10 +942,28 @@ class CodingAgent(InteractiveAgent):
|
|
|
919
942
|
or mutating ``gh pr`` through the shell.
|
|
920
943
|
- Do not read secrets or expose sensitive environment values.
|
|
921
944
|
|
|
945
|
+
Forbidden inside sandboxed code cells (do NOT attempt - they always fail
|
|
946
|
+
and burn turns): ``import os``, ``import sys``, ``import subprocess``,
|
|
947
|
+
``import shutil``, ``import nooa``, ``import noah_code``, and any of
|
|
948
|
+
``eval()``, ``exec()``, ``compile()``, ``__import__()``. If you need to
|
|
949
|
+
run shell logic or a host feature, do it with ``self.ws.run(...)``
|
|
950
|
+
(read-only shell is auto-approved), ``self.web``, ``self.lsp``, or the
|
|
951
|
+
dedicated tools - never by importing a blocked module. Use
|
|
952
|
+
``self.ws.inspect(...)`` on Python data; do not try to reach host
|
|
953
|
+
objects from a cell.
|
|
954
|
+
|
|
922
955
|
Return exactly one valid RespondResult:
|
|
923
956
|
- DONE - request complete
|
|
924
957
|
- NEED_INPUT - user input genuinely required
|
|
925
958
|
- WAIT - a registered background job is still running
|
|
959
|
+
|
|
960
|
+
To end the turn, call ``return_result(RespondReason.DONE,
|
|
961
|
+
explanation="...")`` — ``return_result`` takes a ``RespondReason``
|
|
962
|
+
and an ``explanation`` string, NEVER a bare string as ``result=``
|
|
963
|
+
(that raises ``return_result validation error: 'result' has wrong
|
|
964
|
+
type``). To show the user text, call the synchronous
|
|
965
|
+
``self.message("...")`` first (no ``await``), then
|
|
966
|
+
``return_result(RespondReason.DONE, explanation="summary")``.
|
|
926
967
|
"""
|
|
927
968
|
...
|
|
928
969
|
|
|
@@ -85,15 +85,6 @@ class ApprovalBroker:
|
|
|
85
85
|
raise PermissionError(
|
|
86
86
|
f"rejected [{decision.category}] {decision.target}: {decision.reason}"
|
|
87
87
|
)
|
|
88
|
-
if choice == ApprovalChoice.SESSION:
|
|
89
|
-
self._engine.add_session_rule(
|
|
90
|
-
PermissionRule(
|
|
91
|
-
category=decision.category,
|
|
92
|
-
pattern=decision.remember_pattern,
|
|
93
|
-
action="allow",
|
|
94
|
-
reason="remembered for session",
|
|
95
|
-
)
|
|
96
|
-
)
|
|
97
88
|
|
|
98
89
|
async def _ask(self, decision: PermissionDecision) -> ApprovalChoice:
|
|
99
90
|
req_id = str(uuid.uuid4())
|
|
@@ -128,7 +119,28 @@ class ApprovalBroker:
|
|
|
128
119
|
async def _resolve() -> None:
|
|
129
120
|
try:
|
|
130
121
|
async with self._ui_lock:
|
|
131
|
-
|
|
122
|
+
# A concurrent twin of this request may have just been
|
|
123
|
+
# granted a session-wide allow. Re-check before putting
|
|
124
|
+
# a second identical prompt in front of the user, and
|
|
125
|
+
# register new session rules inside the same critical
|
|
126
|
+
# section so queued twins observe them immediately.
|
|
127
|
+
fresh = self._engine.decide(decision.category, decision.target)
|
|
128
|
+
if fresh.allowed:
|
|
129
|
+
choice = ApprovalChoice.ONCE
|
|
130
|
+
else:
|
|
131
|
+
choice = await handler(request)
|
|
132
|
+
if (
|
|
133
|
+
choice == ApprovalChoice.SESSION
|
|
134
|
+
and not fut.done()
|
|
135
|
+
):
|
|
136
|
+
self._engine.add_session_rule(
|
|
137
|
+
PermissionRule(
|
|
138
|
+
category=decision.category,
|
|
139
|
+
pattern=decision.remember_pattern,
|
|
140
|
+
action="allow",
|
|
141
|
+
reason="remembered for session",
|
|
142
|
+
)
|
|
143
|
+
)
|
|
132
144
|
except Exception as exc:
|
|
133
145
|
if not fut.done():
|
|
134
146
|
fut.set_exception(exc)
|
|
@@ -14,6 +14,7 @@ Enforcement points:
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import asyncio
|
|
17
|
+
import math
|
|
17
18
|
import threading
|
|
18
19
|
import time
|
|
19
20
|
from typing import Any
|
|
@@ -25,6 +26,30 @@ class BudgetExceeded(RuntimeError):
|
|
|
25
26
|
"""Raised when a configured session cap would be exceeded."""
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
def _sanitize_tokens(value: Any) -> int:
|
|
30
|
+
"""Garbage-proof a token count; providers occasionally report NaN/inf."""
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
number = float(value)
|
|
34
|
+
except (TypeError, ValueError):
|
|
35
|
+
return 0
|
|
36
|
+
if not math.isfinite(number):
|
|
37
|
+
return 0
|
|
38
|
+
return max(int(number), 0)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _sanitize_cost(value: Any) -> float:
|
|
42
|
+
"""Drop NaN/garbage cost, keep +inf so a broken pricing feed fails closed."""
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
number = float(value)
|
|
46
|
+
except (TypeError, ValueError):
|
|
47
|
+
return 0.0
|
|
48
|
+
if math.isnan(number):
|
|
49
|
+
return 0.0
|
|
50
|
+
return max(number, 0.0)
|
|
51
|
+
|
|
52
|
+
|
|
28
53
|
class BudgetGuard:
|
|
29
54
|
"""Thread-safe accumulator against optional token/cost/wall-clock caps."""
|
|
30
55
|
|
|
@@ -66,9 +91,9 @@ class BudgetGuard:
|
|
|
66
91
|
cost_usd: float = 0.0,
|
|
67
92
|
) -> None:
|
|
68
93
|
with self._lock:
|
|
69
|
-
self._prompt_tokens +=
|
|
70
|
-
self._completion_tokens +=
|
|
71
|
-
self._cost_usd +=
|
|
94
|
+
self._prompt_tokens += _sanitize_tokens(prompt_tokens)
|
|
95
|
+
self._completion_tokens += _sanitize_tokens(completion_tokens)
|
|
96
|
+
self._cost_usd += _sanitize_cost(cost_usd)
|
|
72
97
|
|
|
73
98
|
def enforce(self) -> None:
|
|
74
99
|
"""Raise BudgetExceeded when any configured cap is breached."""
|
|
@@ -92,7 +117,7 @@ class BudgetGuard:
|
|
|
92
117
|
"""
|
|
93
118
|
|
|
94
119
|
with self._lock:
|
|
95
|
-
self._cost_usd = max(self._cost_usd,
|
|
120
|
+
self._cost_usd = max(self._cost_usd, _sanitize_cost(total_cost_usd))
|
|
96
121
|
self.enforce()
|
|
97
122
|
|
|
98
123
|
def observe_cost_usd(self, total_cost_usd: float) -> None:
|
|
@@ -104,7 +129,7 @@ class BudgetGuard:
|
|
|
104
129
|
"""
|
|
105
130
|
|
|
106
131
|
with self._lock:
|
|
107
|
-
self._cost_usd = max(self._cost_usd,
|
|
132
|
+
self._cost_usd = max(self._cost_usd, _sanitize_cost(total_cost_usd))
|
|
108
133
|
if self.exceeded is None:
|
|
109
134
|
self.exceeded = self._breach()
|
|
110
135
|
|
|
@@ -143,9 +168,9 @@ class BudgetGuard:
|
|
|
143
168
|
if not data:
|
|
144
169
|
return
|
|
145
170
|
with self._lock:
|
|
146
|
-
self._prompt_tokens =
|
|
147
|
-
self._completion_tokens =
|
|
148
|
-
self._cost_usd =
|
|
171
|
+
self._prompt_tokens = _sanitize_tokens(data.get("prompt_tokens", 0))
|
|
172
|
+
self._completion_tokens = _sanitize_tokens(data.get("completion_tokens", 0))
|
|
173
|
+
self._cost_usd = _sanitize_cost(data.get("cost_usd", 0.0))
|
|
149
174
|
started_at = float(data.get("started_at", time.time()))
|
|
150
175
|
self._started_wall = min(started_at, time.time())
|
|
151
176
|
elapsed = max(time.time() - self._started_wall, 0.0)
|
|
@@ -176,7 +201,7 @@ def _cost_from_response(response: Any, usage: dict[str, Any]) -> float:
|
|
|
176
201
|
import litellm
|
|
177
202
|
|
|
178
203
|
cost = litellm.completion_cost(completion_response=raw)
|
|
179
|
-
return
|
|
204
|
+
return _sanitize_cost(cost)
|
|
180
205
|
except Exception: # noqa: BLE001 - pricing must never break a turn
|
|
181
206
|
return 0.0
|
|
182
207
|
|
|
@@ -192,12 +217,19 @@ def _usage_from_response(response: Any) -> tuple[int, int, float]:
|
|
|
192
217
|
|
|
193
218
|
usage = getattr(response, "usage", None)
|
|
194
219
|
usage_dict = usage if isinstance(usage, dict) else {}
|
|
195
|
-
prompt =
|
|
196
|
-
completion =
|
|
220
|
+
prompt = _sanitize_tokens(usage_dict.get("prompt_tokens") or usage_dict.get("input_tokens"))
|
|
221
|
+
completion = _sanitize_tokens(
|
|
222
|
+
usage_dict.get("completion_tokens") or usage_dict.get("output_tokens")
|
|
223
|
+
)
|
|
197
224
|
cost = _cost_from_response(response, usage_dict)
|
|
198
225
|
try:
|
|
199
226
|
if isinstance(usage, dict):
|
|
200
|
-
usage.
|
|
227
|
+
existing = usage.get("cost_usd")
|
|
228
|
+
if existing is None or not math.isfinite(float(existing)):
|
|
229
|
+
# Never leave provider-reported NaN/inf on the telemetry seam.
|
|
230
|
+
usage["cost_usd"] = cost
|
|
231
|
+
else:
|
|
232
|
+
usage.setdefault("cost_usd", cost)
|
|
201
233
|
elif cost > 0.0:
|
|
202
234
|
response.usage = {"cost_usd": cost}
|
|
203
235
|
except Exception: # noqa: BLE001 - telemetry must never break a turn
|
|
@@ -79,6 +79,11 @@ def _common_options(fn): # noqa: ANN001
|
|
|
79
79
|
is_flag=True,
|
|
80
80
|
help="Auto-approve routine asks (never overrides deny or elevated-risk approval)",
|
|
81
81
|
)(fn)
|
|
82
|
+
fn = click.option(
|
|
83
|
+
"--yolo",
|
|
84
|
+
is_flag=True,
|
|
85
|
+
help="Skip all approvals and permission checks (dangerous; for isolated/throwaway environments)",
|
|
86
|
+
)(fn)
|
|
82
87
|
fn = click.option("--model", "model", default=None, help="Override the model for this launch")(
|
|
83
88
|
fn
|
|
84
89
|
)
|
|
@@ -88,6 +93,13 @@ def _common_options(fn): # noqa: ANN001
|
|
|
88
93
|
default=None,
|
|
89
94
|
help="Reasoning effort for compatible models; default lets the provider decide",
|
|
90
95
|
)(fn)
|
|
96
|
+
fn = click.option(
|
|
97
|
+
"--max-iterations",
|
|
98
|
+
"max_iterations",
|
|
99
|
+
type=click.IntRange(min=1),
|
|
100
|
+
default=None,
|
|
101
|
+
help="Cap model tool-call turns per user request (default 40; budgets still brake)",
|
|
102
|
+
)(fn)
|
|
91
103
|
return fn
|
|
92
104
|
|
|
93
105
|
|
|
@@ -151,7 +163,9 @@ def interactive_cmd(
|
|
|
151
163
|
model: str | None,
|
|
152
164
|
reasoning_effort: str | None,
|
|
153
165
|
auto: bool,
|
|
166
|
+
yolo: bool,
|
|
154
167
|
mode: str | None,
|
|
168
|
+
max_iterations: int | None,
|
|
155
169
|
continue_session: bool,
|
|
156
170
|
session_id: str | None,
|
|
157
171
|
use_console: bool,
|
|
@@ -169,9 +183,11 @@ def interactive_cmd(
|
|
|
169
183
|
model=model,
|
|
170
184
|
reasoning_effort=reasoning_effort,
|
|
171
185
|
auto=auto,
|
|
186
|
+
yolo=yolo,
|
|
187
|
+
mode=mode,
|
|
188
|
+
max_iterations=max_iterations,
|
|
172
189
|
continue_session=continue_session,
|
|
173
190
|
session_id=session_id,
|
|
174
|
-
mode=mode,
|
|
175
191
|
use_console=use_console,
|
|
176
192
|
unsafe_inprocess_code_execution=unsafe_inprocess_code_execution,
|
|
177
193
|
)
|
|
@@ -196,7 +212,9 @@ def run_cmd(
|
|
|
196
212
|
model: str | None,
|
|
197
213
|
reasoning_effort: str | None,
|
|
198
214
|
auto: bool,
|
|
215
|
+
yolo: bool,
|
|
199
216
|
mode: str | None,
|
|
217
|
+
max_iterations: int | None,
|
|
200
218
|
session_id: str | None,
|
|
201
219
|
unsafe_inprocess_code_execution: bool,
|
|
202
220
|
) -> None:
|
|
@@ -208,7 +226,9 @@ def run_cmd(
|
|
|
208
226
|
model=model,
|
|
209
227
|
reasoning_effort=reasoning_effort,
|
|
210
228
|
auto=auto,
|
|
229
|
+
yolo=yolo,
|
|
211
230
|
mode=mode,
|
|
231
|
+
max_iterations=max_iterations,
|
|
212
232
|
session_id=session_id,
|
|
213
233
|
unsafe_inprocess_code_execution=unsafe_inprocess_code_execution,
|
|
214
234
|
)
|
|
@@ -793,7 +813,9 @@ async def _prepare(
|
|
|
793
813
|
model: str | None,
|
|
794
814
|
reasoning_effort: str | None,
|
|
795
815
|
auto: bool,
|
|
816
|
+
yolo: bool,
|
|
796
817
|
mode: str | None,
|
|
818
|
+
max_iterations: int | None = None,
|
|
797
819
|
continue_session: bool = False,
|
|
798
820
|
session_id: str | None = None,
|
|
799
821
|
frontend: Literal["tui", "console"] | None = None,
|
|
@@ -813,6 +835,10 @@ async def _prepare(
|
|
|
813
835
|
overrides["reasoning_effort"] = reasoning_effort
|
|
814
836
|
if auto:
|
|
815
837
|
overrides["auto_approve"] = True
|
|
838
|
+
if yolo:
|
|
839
|
+
overrides["yolo"] = True
|
|
840
|
+
if max_iterations is not None:
|
|
841
|
+
overrides["max_iterations"] = max_iterations
|
|
816
842
|
if mode:
|
|
817
843
|
overrides["mode"] = mode
|
|
818
844
|
if frontend is not None:
|
|
@@ -859,9 +885,11 @@ async def _interactive(
|
|
|
859
885
|
model: str | None,
|
|
860
886
|
reasoning_effort: str | None,
|
|
861
887
|
auto: bool,
|
|
888
|
+
yolo: bool,
|
|
889
|
+
mode: str | None,
|
|
890
|
+
max_iterations: int | None,
|
|
862
891
|
continue_session: bool,
|
|
863
892
|
session_id: str | None,
|
|
864
|
-
mode: str | None,
|
|
865
893
|
use_console: bool,
|
|
866
894
|
unsafe_inprocess_code_execution: bool,
|
|
867
895
|
) -> int:
|
|
@@ -882,7 +910,9 @@ async def _interactive(
|
|
|
882
910
|
model=model,
|
|
883
911
|
reasoning_effort=reasoning_effort,
|
|
884
912
|
auto=auto,
|
|
913
|
+
yolo=yolo,
|
|
885
914
|
mode=mode,
|
|
915
|
+
max_iterations=max_iterations,
|
|
886
916
|
continue_session=continue_session,
|
|
887
917
|
session_id=session_id,
|
|
888
918
|
frontend=frontend,
|
|
@@ -906,7 +936,9 @@ async def _interactive(
|
|
|
906
936
|
model=model,
|
|
907
937
|
reasoning_effort=reasoning_effort,
|
|
908
938
|
auto=auto,
|
|
939
|
+
yolo=yolo,
|
|
909
940
|
mode=mode,
|
|
941
|
+
max_iterations=max_iterations,
|
|
910
942
|
continue_session=continue_session,
|
|
911
943
|
session_id=session_id,
|
|
912
944
|
frontend=frontend,
|
|
@@ -925,12 +957,17 @@ async def _interactive(
|
|
|
925
957
|
and config.model == NoahCodeConfig().model
|
|
926
958
|
)
|
|
927
959
|
try:
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
960
|
+
try:
|
|
961
|
+
return await host.run_tui(onboarding_required=onboarding_required)
|
|
962
|
+
except RuntimeError as exc:
|
|
963
|
+
click.echo(f"error: {exc}", err=True)
|
|
964
|
+
return EXIT_CONFIG
|
|
965
|
+
except KeyboardInterrupt:
|
|
966
|
+
return EXIT_SIGINT
|
|
967
|
+
finally:
|
|
968
|
+
# run_tui closes itself, but an import failure or a close that was
|
|
969
|
+
# cut short by cancellation must not leak leases, storage, or tools.
|
|
970
|
+
await host.close()
|
|
934
971
|
host = AgentHost(
|
|
935
972
|
workspace,
|
|
936
973
|
config,
|
|
@@ -939,9 +976,12 @@ async def _interactive(
|
|
|
939
976
|
ui=ConsoleUI(markdown=config.ui.markdown),
|
|
940
977
|
)
|
|
941
978
|
try:
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
979
|
+
try:
|
|
980
|
+
return await host.run_interactive()
|
|
981
|
+
except KeyboardInterrupt:
|
|
982
|
+
return EXIT_SIGINT
|
|
983
|
+
finally:
|
|
984
|
+
await host.close()
|
|
945
985
|
|
|
946
986
|
|
|
947
987
|
async def _run_session(
|
|
@@ -951,7 +991,9 @@ async def _run_session(
|
|
|
951
991
|
model: str | None,
|
|
952
992
|
reasoning_effort: str | None,
|
|
953
993
|
auto: bool,
|
|
994
|
+
yolo: bool,
|
|
954
995
|
mode: str | None,
|
|
996
|
+
max_iterations: int | None,
|
|
955
997
|
session_id: str | None,
|
|
956
998
|
unsafe_inprocess_code_execution: bool,
|
|
957
999
|
) -> int:
|
|
@@ -962,7 +1004,9 @@ async def _run_session(
|
|
|
962
1004
|
model=model,
|
|
963
1005
|
reasoning_effort=reasoning_effort,
|
|
964
1006
|
auto=auto,
|
|
1007
|
+
yolo=yolo,
|
|
965
1008
|
mode=mode,
|
|
1009
|
+
max_iterations=max_iterations,
|
|
966
1010
|
session_id=session_id,
|
|
967
1011
|
frontend="console",
|
|
968
1012
|
unsafe_inprocess_code_execution=unsafe_inprocess_code_execution,
|