nighthawk-python 0.7.0__py3-none-any.whl → 0.9.0__py3-none-any.whl

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.
nighthawk/__init__.py CHANGED
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  from .configuration import (
4
4
  StepContextLimits,
5
5
  StepExecutorConfiguration,
6
- StepExecutorConfigurationPatch,
7
6
  StepPromptTemplates,
8
7
  )
9
8
  from .errors import (
@@ -32,7 +31,6 @@ __all__ = [
32
31
  "StepContextLimits",
33
32
  "StepExecutor",
34
33
  "StepExecutorConfiguration",
35
- "StepExecutorConfigurationPatch",
36
34
  "StepPromptTemplates",
37
35
  "ToolEvaluationError",
38
36
  "ToolRegistrationError",
@@ -32,7 +32,7 @@ Trust boundaries:
32
32
 
33
33
  Notes:
34
34
  - In async Natural functions, expressions may use `await`.
35
- - Tool calls return {"value": ..., "error": ...}. Values may contain \u2026 where content was omitted. Check "error" for failures.
35
+ - Tool calls return {"value": ..., "error": ...}; "value" is a preview (max $tool_result_max_tokens tokens), so persist large/intermediate data via nh_assign and inspect with focused nh_eval reads.
36
36
  """
37
37
 
38
38
 
@@ -81,8 +81,11 @@ class StepContextLimits(BaseModel):
81
81
  locals_max_items: Maximum items rendered in the locals section.
82
82
  globals_max_tokens: Maximum tokens for the globals section.
83
83
  globals_max_items: Maximum items rendered in the globals section.
84
- value_max_tokens: Maximum tokens for a single value rendering.
85
- tool_result_max_tokens: Maximum tokens for a tool result rendering.
84
+ value_max_tokens: Maximum tokens for a single value preview.
85
+ object_max_methods: Maximum public methods rendered for one object capability view.
86
+ object_max_fields: Maximum public fields rendered for one object capability view.
87
+ object_field_value_max_tokens: Maximum tokens for one object field value preview.
88
+ tool_result_max_tokens: Maximum tokens for a tool result preview.
86
89
  """
87
90
 
88
91
  model_config = ConfigDict(extra="forbid", frozen=True)
@@ -94,6 +97,9 @@ class StepContextLimits(BaseModel):
94
97
  globals_max_items: int = Field(default=40, ge=1)
95
98
 
96
99
  value_max_tokens: int = Field(default=200, ge=1)
100
+ object_max_methods: int = Field(default=16, ge=0)
101
+ object_max_fields: int = Field(default=16, ge=0)
102
+ object_field_value_max_tokens: int = Field(default=120, ge=1)
97
103
  tool_result_max_tokens: int = Field(default=1_200, ge=1)
98
104
 
99
105
 
@@ -154,44 +160,3 @@ class StepExecutorConfiguration(BaseModel):
154
160
  return tiktoken.encoding_for_model(model_name)
155
161
  except Exception:
156
162
  return tiktoken.get_encoding("o200k_base")
157
-
158
-
159
- class StepExecutorConfigurationPatch(BaseModel):
160
- """Partial override for StepExecutorConfiguration.
161
-
162
- Non-None fields replace the corresponding fields in the target configuration.
163
-
164
- Attributes:
165
- model: Model identifier override.
166
- model_settings: Model settings override. Accepts a dict or a
167
- backend-specific BaseModel instance (auto-converted to dict).
168
- prompts: Prompt templates override.
169
- context_limits: Context limits override.
170
- json_renderer_style: JSON renderer style override.
171
- tokenizer_encoding: Tokenizer encoding override.
172
- system_prompt_suffix_fragments: System prompt suffix fragments override.
173
- user_prompt_suffix_fragments: User prompt suffix fragments override.
174
- """
175
-
176
- model_config = ConfigDict(extra="forbid", frozen=True)
177
-
178
- model: str | None = None
179
- model_settings: dict[str, Any] | BaseModel | None = None
180
-
181
- @field_validator("model_settings", mode="before")
182
- @classmethod
183
- def _normalize_model_settings(cls, value: Any) -> dict[str, Any] | None:
184
- if isinstance(value, BaseModel):
185
- return value.model_dump()
186
- return value
187
-
188
- prompts: StepPromptTemplates | None = None
189
- context_limits: StepContextLimits | None = None
190
- json_renderer_style: JsonRendererStyle | None = None
191
- tokenizer_encoding: str | None = None
192
- system_prompt_suffix_fragments: tuple[str, ...] | None = None
193
- user_prompt_suffix_fragments: tuple[str, ...] | None = None
194
-
195
- def apply_to(self, configuration: StepExecutorConfiguration) -> StepExecutorConfiguration:
196
- """Apply non-None fields to the given configuration and return a new copy."""
197
- return configuration.model_copy(update=self.model_dump(exclude_none=True))
@@ -1,14 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import dataclasses
3
4
  import functools
4
5
  import inspect
5
6
  import logging
6
7
  import re
7
- from collections.abc import Iterable
8
+ from collections.abc import Iterable, Mapping, Sequence
8
9
  from string import Template
9
10
  from typing import Any, TypeAliasType
10
11
 
11
12
  import tiktoken
13
+ from pydantic import BaseModel
12
14
 
13
15
  from ..configuration import StepContextLimits, StepExecutorConfiguration
14
16
  from ..json_renderer import JsonRendererStyle, count_tokens, render_json_text
@@ -21,6 +23,8 @@ from .scoping import (
21
23
  )
22
24
  from .step_context import _MISSING, StepContext, resolve_name_in_step_context
23
25
 
26
+ type ReferenceAndValue = tuple[str, object]
27
+
24
28
 
25
29
  def _resolve_partial_effective_signature(partial_callable: functools.partial[Any]) -> str | None:
26
30
  try:
@@ -98,7 +102,7 @@ def _is_async_callable_value(value: object) -> bool:
98
102
 
99
103
  def _find_ambiguous_callable_signatures(
100
104
  *,
101
- reference_and_value_list: list[tuple[str, object]],
105
+ reference_and_value_list: list[ReferenceAndValue],
102
106
  ) -> dict[str, list[str]]:
103
107
  callable_signature_text_to_reference_list: dict[str, list[str]] = {}
104
108
  for reference, value in reference_and_value_list:
@@ -146,14 +150,275 @@ def _render_callable_line(
146
150
  return rendered_text
147
151
 
148
152
 
153
+ def _is_object_capability_candidate(value: object) -> bool:
154
+ if callable(value):
155
+ return False
156
+ if isinstance(value, TypeAliasType):
157
+ return False
158
+ if value is None:
159
+ return False
160
+ if isinstance(value, (bool, int, float, str, bytes, bytearray)):
161
+ return False
162
+ return not isinstance(value, (Mapping, Sequence, set, frozenset))
163
+
164
+
165
+ def _resolve_public_method_name_to_value(*, value: object) -> dict[str, object]:
166
+ method_name_to_value: dict[str, object] = {}
167
+ for value_type in type(value).__mro__:
168
+ for method_name, class_attribute in value_type.__dict__.items():
169
+ if method_name.startswith("_"):
170
+ continue
171
+ if method_name in method_name_to_value:
172
+ continue
173
+ if isinstance(class_attribute, property):
174
+ continue
175
+
176
+ method_value: object | None = None
177
+ if isinstance(class_attribute, staticmethod):
178
+ method_value = class_attribute.__func__
179
+ elif isinstance(class_attribute, classmethod) or inspect.isfunction(class_attribute):
180
+ method_value = class_attribute.__get__(value, type(value))
181
+ elif callable(class_attribute):
182
+ method_value = class_attribute
183
+
184
+ if method_value is None:
185
+ continue
186
+
187
+ method_name_to_value[method_name] = method_value
188
+
189
+ return {method_name: method_name_to_value[method_name] for method_name in sorted(method_name_to_value)}
190
+
191
+
192
+ def _resolve_public_slot_name_set(*, value: object) -> set[str]:
193
+ public_slot_name_set: set[str] = set()
194
+ for value_type in type(value).__mro__:
195
+ slot_definition = value_type.__dict__.get("__slots__")
196
+ if slot_definition is None:
197
+ continue
198
+
199
+ if isinstance(slot_definition, str):
200
+ slot_name_list = [slot_definition]
201
+ else:
202
+ slot_name_list = [slot_name for slot_name in slot_definition if isinstance(slot_name, str)]
203
+
204
+ for slot_name in slot_name_list:
205
+ if slot_name.startswith("_"):
206
+ continue
207
+ if slot_name in {"__dict__", "__weakref__"}:
208
+ continue
209
+ public_slot_name_set.add(slot_name)
210
+
211
+ return public_slot_name_set
212
+
213
+
214
+ def _resolve_public_field_name_to_value(*, value: object) -> dict[str, object]:
215
+ field_name_to_value: dict[str, object] = {}
216
+
217
+ def add_field(field_name: str, field_value: object) -> None:
218
+ if field_name.startswith("_"):
219
+ return
220
+ if field_name in field_name_to_value:
221
+ return
222
+ field_name_to_value[field_name] = field_value
223
+
224
+ try:
225
+ instance_dict = object.__getattribute__(value, "__dict__")
226
+ except Exception:
227
+ instance_dict = None
228
+
229
+ if isinstance(instance_dict, dict):
230
+ for field_name, field_value in instance_dict.items():
231
+ add_field(field_name, field_value)
232
+
233
+ if dataclasses.is_dataclass(value) and not isinstance(value, type):
234
+ for data_field in dataclasses.fields(value):
235
+ field_name = data_field.name
236
+ if field_name in field_name_to_value:
237
+ continue
238
+ try:
239
+ field_value = object.__getattribute__(value, field_name)
240
+ except Exception:
241
+ continue
242
+ add_field(field_name, field_value)
243
+
244
+ if isinstance(value, BaseModel):
245
+ for field_name, field_value in value.model_dump(mode="python").items():
246
+ add_field(field_name, field_value)
247
+
248
+ for slot_name in sorted(_resolve_public_slot_name_set(value=value)):
249
+ if slot_name in field_name_to_value:
250
+ continue
251
+ try:
252
+ slot_value = object.__getattribute__(value, slot_name)
253
+ except Exception:
254
+ continue
255
+ add_field(slot_name, slot_value)
256
+
257
+ for value_type in type(value).__mro__:
258
+ for field_name, class_attribute in value_type.__dict__.items():
259
+ if field_name in field_name_to_value:
260
+ continue
261
+ if field_name.startswith("_"):
262
+ continue
263
+ if isinstance(class_attribute, (property, staticmethod, classmethod)):
264
+ continue
265
+ if callable(class_attribute):
266
+ continue
267
+ add_field(field_name, class_attribute)
268
+
269
+ return {field_name: field_name_to_value[field_name] for field_name in sorted(field_name_to_value)}
270
+
271
+
272
+ def _render_value_preview_line(
273
+ *,
274
+ reference: str,
275
+ value: object,
276
+ value_max_tokens: int,
277
+ token_encoding: tiktoken.Encoding,
278
+ json_renderer_style: JsonRendererStyle,
279
+ ) -> str:
280
+ reference_type_name = type(value).__name__
281
+ rendered_name_and_type = f"{reference}: {reference_type_name} = "
282
+
283
+ rendered_value, _ = render_json_text(
284
+ value,
285
+ max_tokens=value_max_tokens,
286
+ encoding=token_encoding,
287
+ style=json_renderer_style,
288
+ )
289
+
290
+ return rendered_name_and_type + rendered_value
291
+
292
+
293
+ def _collect_callable_reference_and_value_list(*, reference_and_value_list: list[ReferenceAndValue]) -> list[ReferenceAndValue]:
294
+ callable_reference_and_value_list: list[ReferenceAndValue] = []
295
+ for reference, value in reference_and_value_list:
296
+ if callable(value):
297
+ callable_reference_and_value_list.append((reference, value))
298
+ continue
299
+
300
+ if not _is_object_capability_candidate(value):
301
+ continue
302
+
303
+ method_name_to_value = _resolve_public_method_name_to_value(value=value)
304
+ for method_name, method_value in method_name_to_value.items():
305
+ callable_reference_and_value_list.append((f"{reference}.{method_name}", method_value))
306
+
307
+ return callable_reference_and_value_list
308
+
309
+
310
+ def _render_object_capability_lines(
311
+ *,
312
+ reference: str,
313
+ value: object,
314
+ context_limits: StepContextLimits,
315
+ token_encoding: tiktoken.Encoding,
316
+ json_renderer_style: JsonRendererStyle,
317
+ callable_signature_text_to_reference_list: dict[str, list[str]],
318
+ ) -> list[str]:
319
+ lines = [f"{reference}: object = {type(value).__name__}"]
320
+
321
+ method_name_to_value = _resolve_public_method_name_to_value(value=value)
322
+ field_name_to_value = _resolve_public_field_name_to_value(value=value)
323
+
324
+ shown_method_count = 0
325
+ for method_name, method_value in method_name_to_value.items():
326
+ if shown_method_count >= context_limits.object_max_methods:
327
+ break
328
+ method_reference = f"{reference}.{method_name}"
329
+ lines.append(
330
+ _render_callable_line(
331
+ reference=method_reference,
332
+ value=method_value,
333
+ callable_signature_text_to_reference_list=callable_signature_text_to_reference_list,
334
+ )
335
+ )
336
+ shown_method_count += 1
337
+
338
+ omitted_method_count = len(method_name_to_value) - shown_method_count
339
+ if omitted_method_count > 0:
340
+ lines.append(f"{reference}.<methods>: <snipped {omitted_method_count} public methods>")
341
+
342
+ shown_field_count = 0
343
+ for field_name, field_value in field_name_to_value.items():
344
+ if shown_field_count >= context_limits.object_max_fields:
345
+ break
346
+ field_reference = f"{reference}.{field_name}"
347
+ lines.append(
348
+ _render_value_preview_line(
349
+ reference=field_reference,
350
+ value=field_value,
351
+ value_max_tokens=context_limits.object_field_value_max_tokens,
352
+ token_encoding=token_encoding,
353
+ json_renderer_style=json_renderer_style,
354
+ )
355
+ )
356
+ shown_field_count += 1
357
+
358
+ omitted_field_count = len(field_name_to_value) - shown_field_count
359
+ if omitted_field_count > 0:
360
+ lines.append(f"{reference}.<fields>: <snipped {omitted_field_count} public fields>")
361
+
362
+ return lines
363
+
364
+
365
+ def _render_reference_and_value_lines(
366
+ *,
367
+ reference: str,
368
+ value: object,
369
+ context_limits: StepContextLimits,
370
+ token_encoding: tiktoken.Encoding,
371
+ json_renderer_style: JsonRendererStyle,
372
+ callable_signature_text_to_reference_list: dict[str, list[str]],
373
+ ) -> list[str]:
374
+ if isinstance(value, TypeAliasType):
375
+ return [f"{reference}: type = {value.__value__}"]
376
+
377
+ if callable(value):
378
+ return [
379
+ _render_callable_line(
380
+ reference=reference,
381
+ value=value,
382
+ callable_signature_text_to_reference_list=callable_signature_text_to_reference_list,
383
+ )
384
+ ]
385
+
386
+ if _is_object_capability_candidate(value):
387
+ return _render_object_capability_lines(
388
+ reference=reference,
389
+ value=value,
390
+ context_limits=context_limits,
391
+ token_encoding=token_encoding,
392
+ json_renderer_style=json_renderer_style,
393
+ callable_signature_text_to_reference_list=callable_signature_text_to_reference_list,
394
+ )
395
+
396
+ return [
397
+ _render_value_preview_line(
398
+ reference=reference,
399
+ value=value,
400
+ value_max_tokens=context_limits.value_max_tokens,
401
+ token_encoding=token_encoding,
402
+ json_renderer_style=json_renderer_style,
403
+ )
404
+ ]
405
+
406
+
407
+ def _count_rendered_line_list_tokens(*, lines: list[str], token_encoding: tiktoken.Encoding) -> int:
408
+ total_tokens = 0
409
+ for line in lines:
410
+ total_tokens += count_tokens(line, token_encoding) + 1
411
+ return total_tokens
412
+
413
+
149
414
  def _render_reference_and_value_list_section(
150
415
  *,
151
416
  section_name: str,
152
417
  step_context: StepContext,
153
- reference_and_value_list: list[tuple[str, object]],
418
+ reference_and_value_list: list[ReferenceAndValue],
154
419
  max_items: int,
155
420
  section_max_tokens: int,
156
- value_max_tokens: int,
421
+ context_limits: StepContextLimits,
157
422
  token_encoding: tiktoken.Encoding,
158
423
  json_renderer_style: JsonRendererStyle,
159
424
  ) -> str:
@@ -161,42 +426,34 @@ def _render_reference_and_value_list_section(
161
426
  total_tokens = 0
162
427
  shown_items = 0
163
428
  token_limit_reached = False
164
- callable_signature_text_to_reference_list = _find_ambiguous_callable_signatures(reference_and_value_list=reference_and_value_list)
429
+
430
+ callable_reference_and_value_list = _collect_callable_reference_and_value_list(reference_and_value_list=reference_and_value_list)
431
+ callable_signature_text_to_reference_list = _find_ambiguous_callable_signatures(reference_and_value_list=callable_reference_and_value_list)
165
432
 
166
433
  for reference, value in reference_and_value_list:
167
434
  if shown_items >= max_items:
168
435
  break
169
436
 
170
- if isinstance(value, TypeAliasType):
171
- rendered = f"{reference}: type = {value.__value__}"
172
- rendered_tokens = count_tokens(rendered, token_encoding)
173
- elif callable(value):
174
- rendered = _render_callable_line(
175
- reference=reference,
176
- value=value,
177
- callable_signature_text_to_reference_list=callable_signature_text_to_reference_list,
178
- )
179
- rendered_tokens = count_tokens(rendered, token_encoding)
180
- else:
181
- reference_type_name = type(value).__name__
182
- rendered_name_and_type = f"{reference}: {reference_type_name} = "
183
-
184
- rendered_value, rendered_value_tokens = render_json_text(
185
- value,
186
- max_tokens=value_max_tokens,
187
- encoding=token_encoding,
188
- style=json_renderer_style,
189
- )
190
-
191
- rendered = rendered_name_and_type + rendered_value
192
- rendered_tokens = count_tokens(rendered_name_and_type, token_encoding) + rendered_value_tokens
193
-
194
- if total_tokens + rendered_tokens + 1 > section_max_tokens:
437
+ rendered_line_list = _render_reference_and_value_lines(
438
+ reference=reference,
439
+ value=value,
440
+ context_limits=context_limits,
441
+ token_encoding=token_encoding,
442
+ json_renderer_style=json_renderer_style,
443
+ callable_signature_text_to_reference_list=callable_signature_text_to_reference_list,
444
+ )
445
+
446
+ rendered_tokens = _count_rendered_line_list_tokens(
447
+ lines=rendered_line_list,
448
+ token_encoding=token_encoding,
449
+ )
450
+
451
+ if total_tokens + rendered_tokens > section_max_tokens:
195
452
  token_limit_reached = True
196
453
  break
197
454
 
198
- lines.append(rendered)
199
- total_tokens += rendered_tokens + 1
455
+ lines.extend(rendered_line_list)
456
+ total_tokens += rendered_tokens
200
457
  shown_items += 1
201
458
 
202
459
  truncated = shown_items < len(reference_and_value_list)
@@ -230,8 +487,7 @@ def _render_locals_section(
230
487
  context_limits: StepContextLimits,
231
488
  json_renderer_style: JsonRendererStyle,
232
489
  ) -> str:
233
-
234
- eligible_reference_and_value_list: list[tuple[str, object]] = []
490
+ eligible_reference_and_value_list: list[ReferenceAndValue] = []
235
491
  for reference in sorted(references):
236
492
  if reference.startswith("__"):
237
493
  continue
@@ -243,7 +499,7 @@ def _render_locals_section(
243
499
  reference_and_value_list=eligible_reference_and_value_list,
244
500
  max_items=context_limits.locals_max_items,
245
501
  section_max_tokens=context_limits.locals_max_tokens,
246
- value_max_tokens=context_limits.value_max_tokens,
502
+ context_limits=context_limits,
247
503
  token_encoding=token_encoding,
248
504
  json_renderer_style=json_renderer_style,
249
505
  )
@@ -275,8 +531,7 @@ def _render_globals_section(
275
531
  context_limits: StepContextLimits,
276
532
  json_renderer_style: JsonRendererStyle,
277
533
  ) -> str:
278
-
279
- eligible_reference_and_value_list: list[tuple[str, object]] = []
534
+ eligible_reference_and_value_list: list[ReferenceAndValue] = []
280
535
  for reference in sorted(references):
281
536
  if reference.startswith("__"):
282
537
  continue
@@ -293,12 +548,53 @@ def _render_globals_section(
293
548
  reference_and_value_list=eligible_reference_and_value_list,
294
549
  max_items=context_limits.globals_max_items,
295
550
  section_max_tokens=context_limits.globals_max_tokens,
296
- value_max_tokens=context_limits.value_max_tokens,
551
+ context_limits=context_limits,
297
552
  token_encoding=token_encoding,
298
553
  json_renderer_style=json_renderer_style,
299
554
  )
300
555
 
301
556
 
557
+ def _resolve_step_prompt_template_text(
558
+ *,
559
+ template_text: str,
560
+ name_to_value: dict[str, str],
561
+ ) -> str:
562
+ template = Template(template_text)
563
+ return template.safe_substitute(name_to_value)
564
+
565
+
566
+ def _resolve_step_system_prompt_text(*, configuration: StepExecutorConfiguration) -> str:
567
+ return _resolve_step_prompt_template_text(
568
+ template_text=configuration.prompts.step_system_prompt_template,
569
+ name_to_value={
570
+ "tool_result_max_tokens": str(configuration.context_limits.tool_result_max_tokens),
571
+ },
572
+ )
573
+
574
+
575
+ def _resolve_step_user_prompt_text(
576
+ *,
577
+ template_text: str,
578
+ program_text: str,
579
+ locals_text: str,
580
+ globals_text: str,
581
+ tool_result_max_tokens: int,
582
+ ) -> str:
583
+ return _resolve_step_prompt_template_text(
584
+ template_text=template_text,
585
+ name_to_value={
586
+ "program": program_text,
587
+ "locals": locals_text,
588
+ "globals": globals_text,
589
+ "tool_result_max_tokens": str(tool_result_max_tokens),
590
+ },
591
+ )
592
+
593
+
594
+ def build_system_prompt(*, configuration: StepExecutorConfiguration) -> str:
595
+ return _resolve_step_system_prompt_text(configuration=configuration)
596
+
597
+
302
598
  def build_user_prompt(
303
599
  *,
304
600
  processed_natural_program: str,
@@ -327,12 +623,12 @@ def build_user_prompt(
327
623
  json_renderer_style=configuration.json_renderer_style,
328
624
  )
329
625
 
330
- template = Template(template_text)
331
-
332
- prompt_text = template.substitute(
333
- program=program_text,
334
- locals=locals_text,
335
- globals=globals_text,
626
+ prompt_text = _resolve_step_user_prompt_text(
627
+ template_text=template_text,
628
+ program_text=program_text,
629
+ locals_text=locals_text,
630
+ globals_text=globals_text,
631
+ tool_result_max_tokens=context_limits.tool_result_max_tokens,
336
632
  )
337
633
 
338
634
  suffix_fragments = (
@@ -2,17 +2,17 @@ from __future__ import annotations
2
2
 
3
3
  import importlib.metadata
4
4
  import threading
5
- from collections.abc import Iterator, Mapping
5
+ from collections.abc import Iterator, Mapping, Sequence
6
6
  from contextlib import contextmanager
7
7
  from contextvars import ContextVar
8
8
  from copy import copy
9
9
  from dataclasses import dataclass, replace
10
- from typing import TYPE_CHECKING, Any
10
+ from typing import TYPE_CHECKING, Any, Literal
11
11
 
12
12
  from opentelemetry.trace import Span, get_tracer_provider
13
13
  from pydantic_ai.usage import RunUsage
14
14
 
15
- from ..configuration import StepExecutorConfiguration, StepExecutorConfigurationPatch
15
+ from ..configuration import StepExecutorConfiguration
16
16
  from ..errors import NighthawkError
17
17
  from ..tools.registry import tool_scope
18
18
  from ..ulid import generate_ulid
@@ -295,24 +295,28 @@ def run(
295
295
  @contextmanager
296
296
  def scope(
297
297
  *,
298
+ mode: Literal["inherit", "replace"] = "inherit",
298
299
  step_executor_configuration: StepExecutorConfiguration | None = None,
299
- step_executor_configuration_patch: StepExecutorConfigurationPatch | None = None,
300
300
  step_executor: StepExecutor | None = None,
301
- system_prompt_suffix_fragment: str | None = None,
302
- user_prompt_suffix_fragment: str | None = None,
301
+ system_prompt_suffix_fragments: Sequence[str] | None = None,
302
+ user_prompt_suffix_fragments: Sequence[str] | None = None,
303
303
  implicit_references: ImplicitReferenceNameToValue | None = None,
304
304
  ) -> Iterator[StepExecutor]:
305
- """Open a nested scope that can override the step executor or its configuration.
305
+ """Open a nested scope that can override the active execution context.
306
306
 
307
- Must be called inside an active run context. Creates a new scope_id while inheriting the run_id from the parent context.
307
+ Must be called inside an active run context. Creates a new ``scope_id`` while inheriting the ``run_id`` from the parent context.
308
308
 
309
309
  Args:
310
+ mode: Scope composition mode. ``"inherit"`` appends/merges values into the current scope. ``"replace"`` fully replaces provided list/dict values.
311
+ In both modes, ``None`` means no change.
310
312
  step_executor_configuration: Full replacement configuration for the step executor.
311
- step_executor_configuration_patch: Partial override applied on top of the current configuration.
312
313
  step_executor: Replacement step executor for this scope.
313
- system_prompt_suffix_fragment: Additional text appended to the system prompt.
314
- user_prompt_suffix_fragment: Additional text appended to the user prompt.
315
- implicit_references: Additional implicit global references merged into this scope and inherited by nested scopes.
314
+ system_prompt_suffix_fragments: Additional system prompt suffix fragments.
315
+ In ``mode="inherit"``, fragments are appended. In ``mode="replace"``, provided fragments fully replace the current fragments.
316
+ user_prompt_suffix_fragments: Additional user prompt suffix fragments.
317
+ In ``mode="inherit"``, fragments are appended. In ``mode="replace"``, provided fragments fully replace the current fragments.
318
+ implicit_references: Implicit global references for this scope.
319
+ In ``mode="inherit"``, values are merged with conflict checks. In ``mode="replace"``, provided mappings fully replace the current mapping.
316
320
 
317
321
  Yields:
318
322
  The step executor active within this scope.
@@ -321,9 +325,9 @@ def scope(
321
325
  ```python
322
326
  with nighthawk.run(executor):
323
327
  with nighthawk.scope(
324
- step_executor_configuration_patch=StepExecutorConfigurationPatch(
325
- model="openai:gpt-5.4-mini",
326
- ),
328
+ mode="replace",
329
+ implicit_references={},
330
+ system_prompt_suffix_fragments=["Use concise answers."],
327
331
  ) as scoped_executor:
328
332
  result = my_natural_function()
329
333
  ```
@@ -332,31 +336,13 @@ def scope(
332
336
  current_execution_context = get_execution_context()
333
337
 
334
338
  next_step_executor = current_step_executor
335
-
336
339
  if step_executor is not None:
337
340
  next_step_executor = step_executor
338
341
 
339
- has_configuration_update = any(
340
- value is not None
341
- for value in (
342
- step_executor_configuration,
343
- step_executor_configuration_patch,
344
- )
345
- )
346
-
347
- if has_configuration_update:
348
- current_agent_step_executor = _resolve_agent_step_executor(next_step_executor)
349
- next_configuration = current_agent_step_executor.configuration
350
-
351
- if step_executor_configuration is not None:
352
- next_configuration = step_executor_configuration
353
-
354
- if step_executor_configuration_patch is not None:
355
- next_configuration = step_executor_configuration_patch.apply_to(next_configuration)
356
-
342
+ if step_executor_configuration is not None:
357
343
  next_step_executor = _replace_step_executor_with_configuration(
358
344
  next_step_executor,
359
- configuration=next_configuration,
345
+ configuration=step_executor_configuration,
360
346
  )
361
347
 
362
348
  next_execution_context = replace(
@@ -364,27 +350,36 @@ def scope(
364
350
  scope_id=generate_ulid(),
365
351
  )
366
352
 
367
- next_system_fragments = _system_prompt_suffix_fragments_var.get()
368
- next_user_fragments = _user_prompt_suffix_fragments_var.get()
353
+ next_system_prompt_suffix_fragments = _system_prompt_suffix_fragments_var.get()
354
+ next_user_prompt_suffix_fragments = _user_prompt_suffix_fragments_var.get()
369
355
  next_implicit_reference_name_to_value = _implicit_reference_name_to_value_var.get()
370
356
 
371
- if system_prompt_suffix_fragment is not None:
372
- next_system_fragments = (*next_system_fragments, system_prompt_suffix_fragment)
357
+ if system_prompt_suffix_fragments is not None:
358
+ if mode == "inherit":
359
+ next_system_prompt_suffix_fragments = (*next_system_prompt_suffix_fragments, *system_prompt_suffix_fragments)
360
+ else:
361
+ next_system_prompt_suffix_fragments = tuple(system_prompt_suffix_fragments)
373
362
 
374
- if user_prompt_suffix_fragment is not None:
375
- next_user_fragments = (*next_user_fragments, user_prompt_suffix_fragment)
363
+ if user_prompt_suffix_fragments is not None:
364
+ if mode == "inherit":
365
+ next_user_prompt_suffix_fragments = (*next_user_prompt_suffix_fragments, *user_prompt_suffix_fragments)
366
+ else:
367
+ next_user_prompt_suffix_fragments = tuple(user_prompt_suffix_fragments)
376
368
 
377
369
  if implicit_references is not None:
378
- next_implicit_reference_name_to_value = _merge_implicit_reference_name_to_value_with_conflict_check(
379
- next_implicit_reference_name_to_value,
380
- implicit_references,
381
- )
370
+ if mode == "inherit":
371
+ next_implicit_reference_name_to_value = _merge_implicit_reference_name_to_value_with_conflict_check(
372
+ next_implicit_reference_name_to_value,
373
+ implicit_references,
374
+ )
375
+ else:
376
+ next_implicit_reference_name_to_value = dict(implicit_references)
382
377
 
383
378
  with tool_scope():
384
379
  step_executor_token = _step_executor_var.set(next_step_executor)
385
380
  execution_context_token = _execution_context_var.set(next_execution_context)
386
- system_fragments_token = _system_prompt_suffix_fragments_var.set(next_system_fragments)
387
- user_fragments_token = _user_prompt_suffix_fragments_var.set(next_user_fragments)
381
+ system_fragments_token = _system_prompt_suffix_fragments_var.set(next_system_prompt_suffix_fragments)
382
+ user_fragments_token = _user_prompt_suffix_fragments_var.set(next_user_prompt_suffix_fragments)
388
383
  implicit_reference_name_to_value_token = _implicit_reference_name_to_value_var.set(next_implicit_reference_name_to_value)
389
384
  try:
390
385
  with span(
@@ -11,7 +11,7 @@ from ..errors import ExecutionError
11
11
  from ..tools.execution import ToolResultWrapperToolset
12
12
  from ..tools.registry import get_visible_tools
13
13
  from .async_bridge import run_coroutine_synchronously
14
- from .prompt import build_user_prompt, extract_references_and_program
14
+ from .prompt import build_system_prompt, build_user_prompt, extract_references_and_program
15
15
  from .scoping import (
16
16
  get_current_usage_meter,
17
17
  get_system_prompt_suffix_fragments,
@@ -108,11 +108,13 @@ def _new_agent_step_executor(
108
108
  if configuration.model_settings is not None:
109
109
  constructor_arguments["model_settings"] = configuration.model_settings
110
110
 
111
+ system_prompt_text = build_system_prompt(configuration=configuration)
112
+
111
113
  agent = Agent(
112
114
  model=model,
113
115
  output_type=StepFinalResult,
114
116
  deps_type=StepContext,
115
- system_prompt=configuration.prompts.step_system_prompt_template,
117
+ system_prompt=system_prompt_text,
116
118
  **constructor_arguments,
117
119
  )
118
120
 
@@ -38,7 +38,7 @@ def render_tool_result_json_text(
38
38
  ) -> str:
39
39
  """Render a tool result envelope as compact JSON text.
40
40
 
41
- Both ``value`` and ``error`` are individually rendered under their own
41
+ Both ``value`` and ``error`` are individually previewed under their own
42
42
  token budgets via ``render_json_text``, then assembled into a
43
43
  ``{"value": ..., "error": ...}`` envelope using f-string interpolation.
44
44
  The sub-values are already valid JSON fragments produced by
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nighthawk-python
3
- Version: 0.7.0
3
+ Version: 0.9.0
4
4
  Summary: A Python library where Python controls flow and LLMs or coding agents reason within constrained Natural blocks.
5
5
  Project-URL: Repository, https://github.com/kurusugawa-computer/nighthawk-python
6
6
  Project-URL: Documentation, https://kurusugawa-computer.github.io/nighthawk-python/
@@ -1,5 +1,5 @@
1
- nighthawk/__init__.py,sha256=AYep0vIYT0t5p6Qb-GyhEY5u0Kmp9nP6-jRZhkgWDn4,1382
2
- nighthawk/configuration.py,sha256=M8y8fcH7m6Clhw50ya5LCF-Vu5Xv0Wx_VH-LHdmCa34,7538
1
+ nighthawk/__init__.py,sha256=YqE9InkNzbW12v9Bh3qF4INLUHfJfmNT_hPqmE_kNb8,1308
2
+ nighthawk/configuration.py,sha256=1gtdLxcDMMnvz9t2fBZP-U-MHW54IHWmInSPiByTaGo,6286
3
3
  nighthawk/errors.py,sha256=CSw_VfIoppCE2SdN2zMtVbMGu_wFwD628v8ZAVxeHIY,602
4
4
  nighthawk/identifier_path.py,sha256=Io3VcT8BlxAASHQUSUclrRcCwnRKTtMDPSR1bpaDN6s,984
5
5
  nighthawk/json_renderer.py,sha256=ixpUYTsVVKOp-iMdHldw7-6I3JxelXOQD8vK2KVXwbw,7640
@@ -27,20 +27,20 @@ nighthawk/resilience/_timeout.py,sha256=L5zWSXmhEYIyWlEjut0nA7ur3oRDOn_2JrhpBw_v
27
27
  nighthawk/resilience/_vote.py,sha256=YV0MTYWwFsHBEbGJhfDByCYaFSuuE6rZYB2VnILx0Es,4616
28
28
  nighthawk/runtime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
29
  nighthawk/runtime/async_bridge.py,sha256=k1s7e5dJEMNZJd0xHqkqAY7VkE7tOBzJf6nAEJH_Xh0,1478
30
- nighthawk/runtime/prompt.py,sha256=cd9lu9UJU9_Fvjl4oxm3NqnPJZkpZV3v6RQ4eMSa-sM,12360
30
+ nighthawk/runtime/prompt.py,sha256=fod1f8wS2xKoc3RM44eB8wrKwPmap9K1gwLw4EzUSP4,22669
31
31
  nighthawk/runtime/runner.py,sha256=saVkEeMkVfz1cm_AVaz8I23Y5OiWuWthqwv-z_4kwwY,23121
32
- nighthawk/runtime/scoping.py,sha256=jWvlNYMJ25cv3n0uA60vDkR9O-BBjREw1JVx6I9ChvY,14614
32
+ nighthawk/runtime/scoping.py,sha256=oa3SqjxctFQtTTZNYFj-scvf5Q0_zaVtluh8V1h8pqE,14986
33
33
  nighthawk/runtime/step_context.py,sha256=-MafRMUV9fo0IBTGnQfAF72IyQy90ZoUS9SK77OVgPc,5529
34
34
  nighthawk/runtime/step_contract.py,sha256=2rbZpUBbmWGTogAksYerIAZfv2xgBgaNmidWxjJtfmY,6391
35
- nighthawk/runtime/step_executor.py,sha256=xicyn5tuiaOqaCsl33rGqrZBEgeMCyQAfn3YA_k6CtI,12782
35
+ nighthawk/runtime/step_executor.py,sha256=Y4P7PuGzJT64FIwjhU2gABxoqHf6wPTXMiSFTSfkK6U,12847
36
36
  nighthawk/runtime/tool_calls.py,sha256=rPLzp2iLdOIWDYvJFw7nMcbo2i786YHHE-gm2_clfB4,3076
37
37
  nighthawk/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
38
  nighthawk/tools/assignment.py,sha256=5_VrhuM2KUBz6RHE5kNOnVC-NZUO0x2N3QvNE17z0GY,9462
39
- nighthawk/tools/contracts.py,sha256=lLfBue1VF8NMd76GMIp6y71cCx1p4MlVQyNhd4Ju2qk,2096
39
+ nighthawk/tools/contracts.py,sha256=9bFj88GiRqwyUSfjhZxY3jhtQafdBP3RvDSz6-QCYPs,2097
40
40
  nighthawk/tools/execution.py,sha256=e1uv09MWOkCoZ5uQu9myUXyhQzM_UziR9yE0raFYtQI,2740
41
41
  nighthawk/tools/provided.py,sha256=gsZ7xcrNmEmwcvdkQi4YndTAIcfpwY1jTxpKJsYU4QQ,2079
42
42
  nighthawk/tools/registry.py,sha256=wsZznwI2xPTflN6tfVE8je5keNVqBHHtTPd9bq0Cu7g,6425
43
- nighthawk_python-0.7.0.dist-info/METADATA,sha256=d2B15wRPtQT5RYhPLJqCHRFf3Q09Or4vaDgjPTPEbdo,6192
44
- nighthawk_python-0.7.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
45
- nighthawk_python-0.7.0.dist-info/licenses/LICENSE,sha256=R8tjrX79o5Pbi_l_aFPPWDbTZkmz4is0CxmzMMs3f0A,1076
46
- nighthawk_python-0.7.0.dist-info/RECORD,,
43
+ nighthawk_python-0.9.0.dist-info/METADATA,sha256=DIyU5rb7e0BvfBfC7hbiw8vc8SIeDQRapJ1XTM0gDg0,6192
44
+ nighthawk_python-0.9.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
45
+ nighthawk_python-0.9.0.dist-info/licenses/LICENSE,sha256=R8tjrX79o5Pbi_l_aFPPWDbTZkmz4is0CxmzMMs3f0A,1076
46
+ nighthawk_python-0.9.0.dist-info/RECORD,,