haru-cli 0.1.1__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. {haru_cli-0.1.1 → haru_cli-0.1.2}/CHANGELOG.md +15 -0
  2. {haru_cli-0.1.1 → haru_cli-0.1.2}/PKG-INFO +35 -4
  3. {haru_cli-0.1.1 → haru_cli-0.1.2}/README.md +34 -3
  4. {haru_cli-0.1.1 → haru_cli-0.1.2}/config/agents.yaml +7 -0
  5. haru_cli-0.1.2/config/models.yaml +31 -0
  6. {haru_cli-0.1.1 → haru_cli-0.1.2}/docs/configuration.md +12 -0
  7. {haru_cli-0.1.1 → haru_cli-0.1.2}/pyproject.toml +1 -1
  8. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/agents/factory.py +20 -8
  9. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/commands/chat.py +119 -11
  10. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/commands/config.py +15 -2
  11. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/commands/run.py +32 -5
  12. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/config/schema.py +20 -1
  13. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/config/templates/agents.yaml +7 -0
  14. haru_cli-0.1.2/src/haru/config/templates/models.yaml +31 -0
  15. haru_cli-0.1.2/src/haru/models/bedrock.py +146 -0
  16. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/unit/test_agent_factory.py +19 -1
  17. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/unit/test_bedrock.py +74 -3
  18. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/unit/test_cli.py +68 -1
  19. {haru_cli-0.1.1 → haru_cli-0.1.2}/uv.lock +1 -1
  20. haru_cli-0.1.1/config/models.yaml +0 -20
  21. haru_cli-0.1.1/src/haru/config/templates/models.yaml +0 -20
  22. haru_cli-0.1.1/src/haru/models/bedrock.py +0 -77
  23. {haru_cli-0.1.1 → haru_cli-0.1.2}/.github/workflows/ci.yml +0 -0
  24. {haru_cli-0.1.1 → haru_cli-0.1.2}/.github/workflows/release.yml +0 -0
  25. {haru_cli-0.1.1 → haru_cli-0.1.2}/.gitignore +0 -0
  26. {haru_cli-0.1.1 → haru_cli-0.1.2}/.kiro/steering/coding-standards.md +0 -0
  27. {haru_cli-0.1.1 → haru_cli-0.1.2}/.kiro/steering/git-workflow.md +0 -0
  28. {haru_cli-0.1.1 → haru_cli-0.1.2}/.kiro/steering/product.md +0 -0
  29. {haru_cli-0.1.1 → haru_cli-0.1.2}/.kiro/steering/security.md +0 -0
  30. {haru_cli-0.1.1 → haru_cli-0.1.2}/.kiro/steering/structure.md +0 -0
  31. {haru_cli-0.1.1 → haru_cli-0.1.2}/.kiro/steering/tech.md +0 -0
  32. {haru_cli-0.1.1 → haru_cli-0.1.2}/.kiro/steering/testing.md +0 -0
  33. {haru_cli-0.1.1 → haru_cli-0.1.2}/.pre-commit-config.yaml +0 -0
  34. {haru_cli-0.1.1 → haru_cli-0.1.2}/.python-version +0 -0
  35. {haru_cli-0.1.1 → haru_cli-0.1.2}/CLAUDE.md +0 -0
  36. {haru_cli-0.1.1 → haru_cli-0.1.2}/LICENSE +0 -0
  37. {haru_cli-0.1.1 → haru_cli-0.1.2}/config/guardrails.yaml +0 -0
  38. {haru_cli-0.1.1 → haru_cli-0.1.2}/config/haru.yaml +0 -0
  39. {haru_cli-0.1.1 → haru_cli-0.1.2}/config/logging.yaml +0 -0
  40. {haru_cli-0.1.1 → haru_cli-0.1.2}/config/mcp.yaml +0 -0
  41. {haru_cli-0.1.1 → haru_cli-0.1.2}/config/prompts/researcher.md +0 -0
  42. {haru_cli-0.1.1 → haru_cli-0.1.2}/config/prompts/supervisor.md +0 -0
  43. {haru_cli-0.1.1 → haru_cli-0.1.2}/config/prompts/writer.md +0 -0
  44. {haru_cli-0.1.1 → haru_cli-0.1.2}/renovate.json +0 -0
  45. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/__init__.py +0 -0
  46. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/__main__.py +0 -0
  47. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/agents/__init__.py +0 -0
  48. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/agents/orchestration.py +0 -0
  49. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/auth/__init__.py +0 -0
  50. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/auth/cache.py +0 -0
  51. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/auth/pkce.py +0 -0
  52. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/auth/session.py +0 -0
  53. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/auth/sso.py +0 -0
  54. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/cli.py +0 -0
  55. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/commands/__init__.py +0 -0
  56. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/commands/agents.py +0 -0
  57. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/commands/login.py +0 -0
  58. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/commands/session.py +0 -0
  59. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/commands/streaming.py +0 -0
  60. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/config/__init__.py +0 -0
  61. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/config/loader.py +0 -0
  62. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/config/templates/__init__.py +0 -0
  63. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/config/templates/haru.yaml +0 -0
  64. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/config/templates/logging.yaml +0 -0
  65. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/config/templates/mcp.yaml +0 -0
  66. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/config/templates/prompts/researcher.md +0 -0
  67. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/config/templates/prompts/supervisor.md +0 -0
  68. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/config/templates/prompts/writer.md +0 -0
  69. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/errors.py +0 -0
  70. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/models/__init__.py +0 -0
  71. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/observability/__init__.py +0 -0
  72. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/observability/guardrails.py +0 -0
  73. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/observability/telemetry.py +0 -0
  74. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/sessions/__init__.py +0 -0
  75. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/sessions/manager.py +0 -0
  76. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/steering/__init__.py +0 -0
  77. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/steering/prompts.py +0 -0
  78. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/tools/__init__.py +0 -0
  79. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/tools/mcp.py +0 -0
  80. {haru_cli-0.1.1 → haru_cli-0.1.2}/src/haru/tools/registry.py +0 -0
  81. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/conftest.py +0 -0
  82. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/integration/test_packaging.py +0 -0
  83. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/unit/test_cache.py +0 -0
  84. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/unit/test_config_command.py +0 -0
  85. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/unit/test_config_loader.py +0 -0
  86. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/unit/test_guardrails.py +0 -0
  87. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/unit/test_main.py +0 -0
  88. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/unit/test_mcp.py +0 -0
  89. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/unit/test_orchestration.py +0 -0
  90. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/unit/test_pkce.py +0 -0
  91. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/unit/test_prompts.py +0 -0
  92. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/unit/test_session.py +0 -0
  93. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/unit/test_sessions.py +0 -0
  94. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/unit/test_sso.py +0 -0
  95. {haru_cli-0.1.1 → haru_cli-0.1.2}/tests/unit/test_telemetry.py +0 -0
@@ -9,6 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
 
12
+ - Sampling controls (`temperature`, `top_p`, `top_k`, `seed`) across three
13
+ surfaces: model entries, per-agent `sampling:` blocks, and CLI/REPL
14
+ overrides (`--temperature/--top-p/--top-k/--seed`, `/sampling`).
15
+ Precedence CLI/REPL → agent → model, merged per-field; unset fields are
16
+ omitted from requests. `top_k`/`seed` travel via Converse
17
+ `additionalModelRequestFields`.
18
+
19
+ ### Fixed
20
+
21
+ - `temperature` is no longer required (or sent by default) on model entries —
22
+ Claude 5-series and Opus 4.7+ reject non-default sampling values with an
23
+ HTTP 400, which the previous always-sent temperature would have triggered.
24
+
25
+ ### Added
26
+
12
27
  - `haru config init`: interactive starter configuration under
13
28
  `~/.config/haru` (SSO settings prompted; guardrails explicitly disabled
14
29
  when no id is provided). `haru config show` prints a secret-free summary.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haru-cli
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Secure, scriptable CLI for Amazon Bedrock Claude models via the AWS Strands Agents SDK.
5
5
  Author: haru-cli maintainers
6
6
  License-Expression: Apache-2.0
@@ -96,12 +96,43 @@ caching tokens in the botocore-compatible schema under `~/.aws/sso/cache`
96
96
  tokens are refreshed automatically; when a fresh login is needed, commands say
97
97
  so plainly.
98
98
 
99
+ ## Sampling controls
100
+
101
+ haru-cli exposes `temperature`, `top_p`, `top_k`, and `seed` on **three
102
+ configuration surfaces** — a capability Kiro CLI does not offer on any of its
103
+ configuration surfaces (its v2/v3 agent formats and `cli.json` define no
104
+ sampling fields):
105
+
106
+ | Surface | Where | Example |
107
+ | ------------------------ | ---------------------------------------------- | ----------------------------------------- |
108
+ | Model catalogue | `models.yaml`, per model entry | `temperature: 0.2`, `top_k: 50` |
109
+ | Agent profile | `agents.yaml`, per agent `sampling:` block | `sampling: {temperature: 0.1}` |
110
+ | CLI / REPL | `--temperature/--top-p/--top-k/--seed` flags on `run` and `chat`; `/sampling` in the REPL | `haru run "..." --top-k 20` |
111
+
112
+ Precedence: **CLI/REPL override → agent `sampling:` block → model entry**,
113
+ merged per-field. Unset fields are omitted from requests entirely, keeping
114
+ the provider's defaults. In the chat REPL, `/sampling` shows the model
115
+ default, session override, and effective value for each field;
116
+ `/sampling temperature=0.2 top_k=50` applies overrides and `/sampling reset`
117
+ clears them.
118
+
119
+ Model compatibility (AWS-side behaviour, not a haru limitation):
120
+
121
+ - **Claude 5-series and Opus 4.7+** (Sonnet 5, Opus 5, Opus 4.8) reject
122
+ non-default `temperature`/`top_p`/`top_k` with an HTTP 400 — leave them
123
+ unset on those entries (the shipped config does).
124
+ - **Claude Haiku 4.5, Sonnet 4.5, and older Claude models**, plus
125
+ non-Anthropic Bedrock models, accept them.
126
+ - `seed` is forwarded via Converse `additionalModelRequestFields` for Bedrock
127
+ models that support it; **no Claude model accepts a seed today**, so do not
128
+ expect determinism from Claude — the field exists for models that honour it.
129
+
99
130
  ## Configuration
100
131
 
101
132
  Declarative YAML under `config/` — models, agents, orchestration
102
- (supervisor/swarm/graph), MCP servers, guardrails, sessions, logging, and
103
- OpenTelemetry. No secrets in YAML: values reference environment variables with
104
- `${env:VAR}`. See [docs/configuration.md](docs/configuration.md).
133
+ (supervisor/swarm/graph), MCP servers, guardrails, sessions, sampling,
134
+ logging, and OpenTelemetry. No secrets in YAML: values reference environment
135
+ variables with `${env:VAR}`. See [docs/configuration.md](docs/configuration.md).
105
136
 
106
137
  ## Development
107
138
 
@@ -68,12 +68,43 @@ caching tokens in the botocore-compatible schema under `~/.aws/sso/cache`
68
68
  tokens are refreshed automatically; when a fresh login is needed, commands say
69
69
  so plainly.
70
70
 
71
+ ## Sampling controls
72
+
73
+ haru-cli exposes `temperature`, `top_p`, `top_k`, and `seed` on **three
74
+ configuration surfaces** — a capability Kiro CLI does not offer on any of its
75
+ configuration surfaces (its v2/v3 agent formats and `cli.json` define no
76
+ sampling fields):
77
+
78
+ | Surface | Where | Example |
79
+ | ------------------------ | ---------------------------------------------- | ----------------------------------------- |
80
+ | Model catalogue | `models.yaml`, per model entry | `temperature: 0.2`, `top_k: 50` |
81
+ | Agent profile | `agents.yaml`, per agent `sampling:` block | `sampling: {temperature: 0.1}` |
82
+ | CLI / REPL | `--temperature/--top-p/--top-k/--seed` flags on `run` and `chat`; `/sampling` in the REPL | `haru run "..." --top-k 20` |
83
+
84
+ Precedence: **CLI/REPL override → agent `sampling:` block → model entry**,
85
+ merged per-field. Unset fields are omitted from requests entirely, keeping
86
+ the provider's defaults. In the chat REPL, `/sampling` shows the model
87
+ default, session override, and effective value for each field;
88
+ `/sampling temperature=0.2 top_k=50` applies overrides and `/sampling reset`
89
+ clears them.
90
+
91
+ Model compatibility (AWS-side behaviour, not a haru limitation):
92
+
93
+ - **Claude 5-series and Opus 4.7+** (Sonnet 5, Opus 5, Opus 4.8) reject
94
+ non-default `temperature`/`top_p`/`top_k` with an HTTP 400 — leave them
95
+ unset on those entries (the shipped config does).
96
+ - **Claude Haiku 4.5, Sonnet 4.5, and older Claude models**, plus
97
+ non-Anthropic Bedrock models, accept them.
98
+ - `seed` is forwarded via Converse `additionalModelRequestFields` for Bedrock
99
+ models that support it; **no Claude model accepts a seed today**, so do not
100
+ expect determinism from Claude — the field exists for models that honour it.
101
+
71
102
  ## Configuration
72
103
 
73
104
  Declarative YAML under `config/` — models, agents, orchestration
74
- (supervisor/swarm/graph), MCP servers, guardrails, sessions, logging, and
75
- OpenTelemetry. No secrets in YAML: values reference environment variables with
76
- `${env:VAR}`. See [docs/configuration.md](docs/configuration.md).
105
+ (supervisor/swarm/graph), MCP servers, guardrails, sessions, sampling,
106
+ logging, and OpenTelemetry. No secrets in YAML: values reference environment
107
+ variables with `${env:VAR}`. See [docs/configuration.md](docs/configuration.md).
77
108
 
78
109
  ## Development
79
110
 
@@ -28,3 +28,10 @@ orchestration:
28
28
  edges:
29
29
  - from: research
30
30
  to: write
31
+ # Per-agent sampling overrides beat the model entry's values and are in turn
32
+ # beaten by CLI/REPL overrides. Example:
33
+ # researcher:
34
+ # model: haiku-4-5
35
+ # sampling:
36
+ # temperature: 0.1
37
+ # top_k: 20
@@ -0,0 +1,31 @@
1
+ # Sampling fields (temperature, top_p, top_k, seed) are optional; unset
2
+ # fields are omitted from requests, keeping the provider's defaults.
3
+ # Claude 5-series and Opus 4.7+ models reject non-default temperature/top_p/
4
+ # top_k with an HTTP 400, so leave them unset on those entries. seed is
5
+ # passed through to models that support it (no Claude model does today).
6
+ default_model: sonnet-5
7
+ models:
8
+ opus-5:
9
+ model_id: us.anthropic.claude-opus-5
10
+ region: us-east-1
11
+ max_tokens: 8192
12
+ streaming: true
13
+ sonnet-5:
14
+ model_id: us.anthropic.claude-sonnet-5
15
+ region: us-east-1
16
+ max_tokens: 8192
17
+ streaming: true
18
+ opus-4-8:
19
+ model_id: us.anthropic.claude-opus-4-8
20
+ region: us-east-1
21
+ max_tokens: 8192
22
+ streaming: true
23
+ # Haiku 4.5 accepts sampling parameters:
24
+ haiku-4-5:
25
+ model_id: us.anthropic.claude-haiku-4-5
26
+ region: us-east-1
27
+ max_tokens: 4096
28
+ temperature: 0.2
29
+ top_p: 0.9
30
+ top_k: 50
31
+ streaming: true
@@ -38,6 +38,18 @@ Bare Bedrock model ids receive the `us.` geographic inference-profile prefix
38
38
  by default. Explicit prefixes (`eu.`, `au.`, `global.`, ...) and full ARNs
39
39
  pass through as configured — writing one into YAML is the approval surface.
40
40
 
41
+ ## Sampling
42
+
43
+ `temperature`, `top_p`, `top_k`, and `seed` are optional on every model entry
44
+ and may also be set per agent (a `sampling:` block) or per invocation
45
+ (`--temperature/--top-p/--top-k/--seed`, or `/sampling` inside `haru chat`).
46
+ Precedence is CLI/REPL → agent → model, merged per-field; unset fields are
47
+ omitted from requests. `temperature`/`top_p` map to the Converse inference
48
+ configuration; `top_k` and `seed` travel via
49
+ `additionalModelRequestFields`. Claude 5-series and Opus 4.7+ models reject
50
+ non-default sampling values (HTTP 400), and no Claude model honours `seed` —
51
+ use these fields with models that accept them (for example Haiku 4.5).
52
+
41
53
  ## Sessions
42
54
 
43
55
  Conversation persistence is configured in the base file:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "haru-cli"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "Secure, scriptable CLI for Amazon Bedrock Claude models via the AWS Strands Agents SDK."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -15,9 +15,9 @@ from strands.models import BedrockModel
15
15
  from strands.session.session_manager import SessionManager
16
16
  from strands.tools.mcp import MCPClient
17
17
 
18
- from haru.config.schema import HaruConfig
18
+ from haru.config.schema import HaruConfig, SamplingConfig
19
19
  from haru.errors import ConfigError
20
- from haru.models.bedrock import build_model, get_model_config
20
+ from haru.models.bedrock import build_model, get_model_config, merge_sampling
21
21
  from haru.steering.prompts import DEFAULT_PROMPTS_ROOT, load_prompts, resolve_prompt
22
22
  from haru.tools.mcp import collect_tools
23
23
 
@@ -31,16 +31,19 @@ def build_agent( # noqa: PLR0913 - keyword-only wiring points, all optional
31
31
  mcp_clients: Mapping[str, MCPClient] | None = None,
32
32
  session_manager: SessionManager | None = None,
33
33
  model_name: str | None = None,
34
+ sampling: SamplingConfig | None = None,
34
35
  ) -> Agent:
35
36
  """Build the named agent from configuration (default model when unnamed).
36
37
 
37
38
  ``model_name`` overrides the model for the default (unnamed) agent only;
38
- named agents pin their own model. Raises ConfigError for unknown agents,
39
- prompt references, tools, or an invalid override.
39
+ named agents pin their own model. ``sampling`` overrides sampling fields
40
+ (precedence: this override, then the agent's ``sampling`` block, then the
41
+ model entry). Raises ConfigError for unknown agents, prompt references,
42
+ tools, or an invalid override.
40
43
  """
41
44
  if agent_name is None:
42
45
  model_cfg = get_model_config(config, model_name)
43
- model = build_model(model_cfg, session, guardrails=config.guardrails)
46
+ model = build_model(model_cfg, session, guardrails=config.guardrails, sampling=sampling)
44
47
  return Agent(model=model, session_manager=session_manager)
45
48
  if model_name is not None:
46
49
  raise ConfigError(
@@ -48,7 +51,12 @@ def build_agent( # noqa: PLR0913 - keyword-only wiring points, all optional
48
51
  " to the default agent only"
49
52
  )
50
53
  model, system_prompt, tools = resolve_agent_parts(
51
- config, agent_name, session, prompts_root=prompts_root, mcp_clients=mcp_clients
54
+ config,
55
+ agent_name,
56
+ session,
57
+ prompts_root=prompts_root,
58
+ mcp_clients=mcp_clients,
59
+ sampling=sampling,
52
60
  )
53
61
  return Agent(
54
62
  model=model,
@@ -59,13 +67,14 @@ def build_agent( # noqa: PLR0913 - keyword-only wiring points, all optional
59
67
  )
60
68
 
61
69
 
62
- def resolve_agent_parts(
70
+ def resolve_agent_parts( # noqa: PLR0913 - keyword-only wiring points, all optional
63
71
  config: HaruConfig,
64
72
  agent_name: str,
65
73
  session: boto3.Session,
66
74
  *,
67
75
  prompts_root: Path | None = None,
68
76
  mcp_clients: Mapping[str, MCPClient] | None = None,
77
+ sampling: SamplingConfig | None = None,
69
78
  ) -> tuple[BedrockModel, str | None, list[Any]]:
70
79
  """Resolve an agent's model, steering prompt, and tool list from config."""
71
80
  if config.agents is None or agent_name not in config.agents.agents:
@@ -73,7 +82,10 @@ def resolve_agent_parts(
73
82
  raise ConfigError(f"Unknown agent {agent_name!r}; configured agents: {available}")
74
83
  agent_cfg = config.agents.agents[agent_name]
75
84
  model = build_model(
76
- get_model_config(config, agent_cfg.model), session, guardrails=config.guardrails
85
+ get_model_config(config, agent_cfg.model),
86
+ session,
87
+ guardrails=config.guardrails,
88
+ sampling=merge_sampling(agent_cfg.sampling, sampling),
77
89
  )
78
90
 
79
91
  system_prompt: str | None = None
@@ -7,31 +7,37 @@ models and agents mid-session.
7
7
 
8
8
  from collections.abc import Callable
9
9
  from pathlib import Path
10
- from typing import Any
10
+ from typing import Any, cast
11
11
 
12
12
  import click
13
+ from pydantic import ValidationError
13
14
  from rich.console import Console
14
15
 
15
16
  from haru.agents.factory import build_agent
16
17
  from haru.auth.session import build_boto3_session
17
18
  from haru.commands.streaming import collect_response, surface_guardrail
18
19
  from haru.config import load_config, resolve_config_path
19
- from haru.config.schema import HaruConfig
20
- from haru.errors import HaruError
20
+ from haru.config.schema import HaruConfig, SamplingConfig
21
+ from haru.errors import ConfigError, HaruError
22
+ from haru.models.bedrock import effective_sampling, get_model_config, sampling_overrides
21
23
  from haru.observability.telemetry import configure_telemetry
22
24
  from haru.sessions.manager import build_session_manager
23
25
  from haru.tools.mcp import started_mcp_clients
24
26
 
25
27
  _EXIT_WORDS = frozenset({"exit", "quit"})
28
+ _SAMPLING_FIELDS = ("temperature", "top_p", "top_k", "seed")
26
29
 
27
30
  _HELP_TEXT = """\
28
31
  Commands:
29
- /help Show this help
30
- /model List configured models
31
- /model <name> Switch the default agent to <name> (resets the conversation)
32
- /agent List configured agents
33
- /agent <name> Switch to agent <name> (resets the conversation)
34
- exit | quit Leave the chat (also /exit, /quit, Ctrl-D)"""
32
+ /help Show this help
33
+ /model List configured models
34
+ /model <name> Switch the default agent to <name> (resets the conversation)
35
+ /agent List configured agents
36
+ /agent <name> Switch to agent <name> (resets the conversation)
37
+ /sampling Show effective sampling for the active target
38
+ /sampling k=v [k=v ...] Override temperature/top_p/top_k/seed (resets the conversation)
39
+ /sampling reset Clear sampling overrides (resets the conversation)
40
+ exit | quit Leave the chat (also /exit, /quit, Ctrl-D)"""
35
41
 
36
42
 
37
43
  def run_chat( # noqa: PLR0913 - keyword-only wiring points, all optional
@@ -42,6 +48,7 @@ def run_chat( # noqa: PLR0913 - keyword-only wiring points, all optional
42
48
  read_input: Callable[[str], str] = input,
43
49
  prompts_root: Path | None = None,
44
50
  session_id: str | None = None,
51
+ sampling: SamplingConfig | None = None,
45
52
  ) -> None:
46
53
  """Run the interactive chat loop until the user exits."""
47
54
  session = build_boto3_session(config.auth)
@@ -51,7 +58,11 @@ def run_chat( # noqa: PLR0913 - keyword-only wiring points, all optional
51
58
  else None
52
59
  )
53
60
  with started_mcp_clients(config.mcp) as mcp_clients:
54
- state: dict[str, Any] = {"agent_name": agent_name, "model_name": None}
61
+ state: dict[str, Any] = {
62
+ "agent_name": agent_name,
63
+ "model_name": None,
64
+ "sampling": sampling,
65
+ }
55
66
 
56
67
  def make_agent(name: str | None, model: str | None) -> Any:
57
68
  return build_agent(
@@ -62,6 +73,7 @@ def run_chat( # noqa: PLR0913 - keyword-only wiring points, all optional
62
73
  mcp_clients=mcp_clients,
63
74
  session_manager=session_manager,
64
75
  model_name=model,
76
+ sampling=state["sampling"],
65
77
  )
66
78
 
67
79
  state["agent"] = make_agent(agent_name, None)
@@ -121,11 +133,92 @@ def _handle_slash(
121
133
  _print_agents(config, console, state)
122
134
  else:
123
135
  _switch(state, console, make_agent, agent_name=argument, model_name=None)
136
+ elif name == "sampling":
137
+ if argument is None:
138
+ _print_sampling(config, console, state)
139
+ else:
140
+ _apply_sampling(argument, config, console, state, make_agent)
124
141
  else:
125
142
  console.print(f"Unknown command /{command}; type /help for commands.")
126
143
  return True
127
144
 
128
145
 
146
+ def _parse_sampling(argument: str) -> SamplingConfig:
147
+ """Parse ``key=value`` sampling tokens into a SamplingConfig."""
148
+ values: dict[str, str] = {}
149
+ for token in argument.split():
150
+ key, separator, raw_value = token.partition("=")
151
+ if not separator or key not in _SAMPLING_FIELDS or not raw_value:
152
+ fields = ", ".join(_SAMPLING_FIELDS)
153
+ raise ConfigError(f"Bad sampling token {token!r}; use key=value with keys: {fields}")
154
+ values[key] = raw_value
155
+ try:
156
+ return SamplingConfig.model_validate(values)
157
+ except ValidationError as exc:
158
+ raise ConfigError(f"Invalid sampling values: {exc}") from exc
159
+
160
+
161
+ def _apply_sampling(
162
+ argument: str,
163
+ config: HaruConfig,
164
+ console: Console,
165
+ state: dict[str, Any],
166
+ make_agent: Callable[[str | None, str | None], Any],
167
+ ) -> None:
168
+ """Apply or clear sampling overrides, keeping the old state on failure."""
169
+ if argument.lower() == "reset":
170
+ override: SamplingConfig | None = None
171
+ else:
172
+ try:
173
+ override = _parse_sampling(argument)
174
+ except ConfigError as exc:
175
+ console.print(f"[red]{exc}[/]")
176
+ return
177
+ previous = state["sampling"]
178
+ state["sampling"] = override
179
+ try:
180
+ agent = make_agent(state["agent_name"], state["model_name"])
181
+ except HaruError as exc:
182
+ state["sampling"] = previous
183
+ console.print(f"[red]{exc}[/]")
184
+ return
185
+ state["agent"] = agent
186
+ if override is None:
187
+ console.print("Sampling overrides cleared (conversation reset).")
188
+ else:
189
+ console.print("Sampling overrides applied (conversation reset).")
190
+ _print_sampling(config, console, state)
191
+
192
+
193
+ def _print_sampling(config: HaruConfig, console: Console, state: dict[str, Any]) -> None:
194
+ """Show model defaults, session overrides, and effective sampling values."""
195
+ agent_name = state["agent_name"]
196
+ model_key = state["model_name"]
197
+ if agent_name is not None and config.agents is not None:
198
+ agent_cfg = config.agents.agents.get(agent_name)
199
+ model_key = agent_cfg.model if agent_cfg is not None else model_key
200
+ try:
201
+ model_cfg = get_model_config(config, model_key)
202
+ except HaruError as exc:
203
+ console.print(f"[red]{exc}[/]")
204
+ return
205
+ override = cast(SamplingConfig | None, state["sampling"])
206
+ effective = effective_sampling(model_cfg, override)
207
+ console.print(f"Sampling for model {model_cfg.model_id!r} (unset = provider default):")
208
+ for field in _SAMPLING_FIELDS:
209
+ model_value = getattr(model_cfg, field)
210
+ override_value = getattr(override, field) if override is not None else None
211
+ effective_value = getattr(effective, field)
212
+ console.print(
213
+ f" {field:<12} model={_fmt(model_value):<8}"
214
+ f" override={_fmt(override_value):<8} effective={_fmt(effective_value)}"
215
+ )
216
+
217
+
218
+ def _fmt(value: Any) -> str:
219
+ return "-" if value is None else str(value)
220
+
221
+
129
222
  def _switch(
130
223
  state: dict[str, Any],
131
224
  console: Console,
@@ -190,7 +283,19 @@ def _print_agents(config: HaruConfig, console: Console, state: dict[str, Any]) -
190
283
  default=None,
191
284
  help="Persist and restore this conversation under the given session id.",
192
285
  )
193
- def chat(config_path: Path | None, agent_name: str | None, session_id: str | None) -> None:
286
+ @click.option("--temperature", type=float, default=None, help="Sampling temperature (0-1).")
287
+ @click.option("--top-p", "top_p", type=float, default=None, help="Nucleus sampling (0-1).")
288
+ @click.option("--top-k", "top_k", type=int, default=None, help="Top-k sampling (>=1).")
289
+ @click.option("--seed", type=int, default=None, help="Seed (models that support it only).")
290
+ def chat( # noqa: PLR0913, PLR0917 - one option per flag; Click passes positionally
291
+ config_path: Path | None,
292
+ agent_name: str | None,
293
+ session_id: str | None,
294
+ temperature: float | None,
295
+ top_p: float | None,
296
+ top_k: int | None,
297
+ seed: int | None,
298
+ ) -> None:
194
299
  """Chat interactively with a Bedrock agent (streaming)."""
195
300
  console = Console()
196
301
  try:
@@ -203,6 +308,9 @@ def chat(config_path: Path | None, agent_name: str | None, session_id: str | Non
203
308
  console=console,
204
309
  prompts_root=resolved.parent / "prompts",
205
310
  session_id=session_id,
311
+ sampling=sampling_overrides(
312
+ temperature=temperature, top_p=top_p, top_k=top_k, seed=seed
313
+ ),
206
314
  )
207
315
  except HaruError as exc:
208
316
  raise click.ClickException(str(exc)) from exc
@@ -6,8 +6,21 @@ from pathlib import Path
6
6
  import click
7
7
 
8
8
  from haru.config import load_config, resolve_config_path, user_config_path
9
+ from haru.config.schema import ModelConfig
9
10
  from haru.errors import HaruError
10
11
 
12
+
13
+ def _sampling_summary(entry: ModelConfig) -> str:
14
+ """Compact ``[temp=.. top_k=..]`` summary of a model's set sampling fields."""
15
+ labels = {"temperature": "temp", "top_p": "top_p", "top_k": "top_k", "seed": "seed"}
16
+ parts = [
17
+ f"{label}={value}"
18
+ for field, label in labels.items()
19
+ if (value := getattr(entry, field)) is not None
20
+ ]
21
+ return f" [{' '.join(parts)}]" if parts else ""
22
+
23
+
11
24
  _TEMPLATE_FILES = ("haru.yaml", "models.yaml", "agents.yaml", "mcp.yaml", "logging.yaml")
12
25
  _PROMPT_FILES = ("supervisor.md", "researcher.md", "writer.md")
13
26
 
@@ -128,8 +141,8 @@ def show(config_path: Path | None) -> None:
128
141
  if loaded.models is not None:
129
142
  default = loaded.models.default_model
130
143
  names = ", ".join(
131
- f"{name}{' (default)' if name == default else ''}"
132
- for name in sorted(loaded.models.models)
144
+ f"{name}{' (default)' if name == default else ''}{_sampling_summary(entry)}"
145
+ for name, entry in sorted(loaded.models.models.items())
133
146
  )
134
147
  click.echo(f"Models: {names}")
135
148
  if loaded.agents is not None:
@@ -9,19 +9,21 @@ from haru.agents.factory import build_agent
9
9
  from haru.auth.session import build_boto3_session
10
10
  from haru.commands.streaming import collect_response, surface_guardrail
11
11
  from haru.config import load_config, resolve_config_path
12
- from haru.config.schema import HaruConfig
12
+ from haru.config.schema import HaruConfig, SamplingConfig
13
13
  from haru.errors import HaruError
14
+ from haru.models.bedrock import sampling_overrides
14
15
  from haru.observability.telemetry import configure_telemetry
15
16
  from haru.tools.mcp import started_mcp_clients
16
17
 
17
18
 
18
- def run_prompt(
19
+ def run_prompt( # noqa: PLR0913 - keyword-only wiring points, all optional
19
20
  config: HaruConfig,
20
21
  prompt: str,
21
22
  agent_name: str | None = None,
22
23
  *,
23
24
  console: Console | None = None,
24
25
  prompts_root: Path | None = None,
26
+ sampling: SamplingConfig | None = None,
25
27
  ) -> str:
26
28
  """Execute ``prompt`` once and return the full response text.
27
29
 
@@ -30,7 +32,12 @@ def run_prompt(
30
32
  session = build_boto3_session(config.auth)
31
33
  with started_mcp_clients(config.mcp) as mcp_clients:
32
34
  agent = build_agent(
33
- config, agent_name, session, prompts_root=prompts_root, mcp_clients=mcp_clients
35
+ config,
36
+ agent_name,
37
+ session,
38
+ prompts_root=prompts_root,
39
+ mcp_clients=mcp_clients,
40
+ sampling=sampling,
34
41
  )
35
42
  chunks: list[str] = []
36
43
  result = collect_response(agent, prompt, chunks.append)
@@ -48,13 +55,33 @@ def run_prompt(
48
55
  help="Path to haru.yaml (default: config/haru.yaml).",
49
56
  )
50
57
  @click.option("--agent", "agent_name", default=None, help="Agent name from configuration.")
51
- def run(prompt: str, config_path: Path | None, agent_name: str | None) -> None:
58
+ @click.option("--temperature", type=float, default=None, help="Sampling temperature (0-1).")
59
+ @click.option("--top-p", "top_p", type=float, default=None, help="Nucleus sampling (0-1).")
60
+ @click.option("--top-k", "top_k", type=int, default=None, help="Top-k sampling (>=1).")
61
+ @click.option("--seed", type=int, default=None, help="Seed (models that support it only).")
62
+ def run( # noqa: PLR0913, PLR0917 - one option per flag; Click passes positionally
63
+ prompt: str,
64
+ config_path: Path | None,
65
+ agent_name: str | None,
66
+ temperature: float | None,
67
+ top_p: float | None,
68
+ top_k: int | None,
69
+ seed: int | None,
70
+ ) -> None:
52
71
  """Run a single prompt and print the answer."""
53
72
  try:
54
73
  resolved = resolve_config_path(config_path)
55
74
  config = load_config(resolved)
56
75
  configure_telemetry(config.observability)
57
- answer = run_prompt(config, prompt, agent_name, prompts_root=resolved.parent / "prompts")
76
+ answer = run_prompt(
77
+ config,
78
+ prompt,
79
+ agent_name,
80
+ prompts_root=resolved.parent / "prompts",
81
+ sampling=sampling_overrides(
82
+ temperature=temperature, top_p=top_p, top_k=top_k, seed=seed
83
+ ),
84
+ )
58
85
  except HaruError as exc:
59
86
  raise click.ClickException(str(exc)) from exc
60
87
  click.echo(answer)
@@ -53,13 +53,31 @@ class IncludesConfig(_FrozenModel):
53
53
  logging: str | None = None
54
54
 
55
55
 
56
+ class SamplingConfig(_FrozenModel):
57
+ """Optional sampling parameters.
58
+
59
+ Unset fields are omitted from requests entirely, keeping the provider's
60
+ defaults — required by Claude 5-series models, which reject non-default
61
+ sampling values. ``seed`` is passed through for Bedrock models that
62
+ support it (no Claude model does today).
63
+ """
64
+
65
+ temperature: float | None = Field(default=None, ge=0.0, le=1.0)
66
+ top_p: float | None = Field(default=None, ge=0.0, le=1.0)
67
+ top_k: int | None = Field(default=None, ge=1)
68
+ seed: int | None = None
69
+
70
+
56
71
  class ModelConfig(_FrozenModel):
57
72
  """A single Bedrock model entry."""
58
73
 
59
74
  model_id: str
60
75
  region: str
61
76
  max_tokens: int = Field(ge=1)
62
- temperature: float = Field(ge=0.0, le=1.0)
77
+ temperature: float | None = Field(default=None, ge=0.0, le=1.0)
78
+ top_p: float | None = Field(default=None, ge=0.0, le=1.0)
79
+ top_k: int | None = Field(default=None, ge=1)
80
+ seed: int | None = None
63
81
  streaming: bool = True
64
82
 
65
83
 
@@ -83,6 +101,7 @@ class AgentConfig(_FrozenModel):
83
101
  system_prompt_ref: str | None = None
84
102
  tools: tuple[str, ...] = ()
85
103
  mcp_servers: tuple[str, ...] = ()
104
+ sampling: SamplingConfig | None = None
86
105
 
87
106
 
88
107
  class SwarmConfig(_FrozenModel):
@@ -28,3 +28,10 @@ orchestration:
28
28
  edges:
29
29
  - from: research
30
30
  to: write
31
+ # Per-agent sampling overrides beat the model entry's values and are in turn
32
+ # beaten by CLI/REPL overrides. Example:
33
+ # researcher:
34
+ # model: haiku-4-5
35
+ # sampling:
36
+ # temperature: 0.1
37
+ # top_k: 20
@@ -0,0 +1,31 @@
1
+ # Sampling fields (temperature, top_p, top_k, seed) are optional; unset
2
+ # fields are omitted from requests, keeping the provider's defaults.
3
+ # Claude 5-series and Opus 4.7+ models reject non-default temperature/top_p/
4
+ # top_k with an HTTP 400, so leave them unset on those entries. seed is
5
+ # passed through to models that support it (no Claude model does today).
6
+ default_model: sonnet-5
7
+ models:
8
+ opus-5:
9
+ model_id: us.anthropic.claude-opus-5
10
+ region: us-east-1
11
+ max_tokens: 8192
12
+ streaming: true
13
+ sonnet-5:
14
+ model_id: us.anthropic.claude-sonnet-5
15
+ region: us-east-1
16
+ max_tokens: 8192
17
+ streaming: true
18
+ opus-4-8:
19
+ model_id: us.anthropic.claude-opus-4-8
20
+ region: us-east-1
21
+ max_tokens: 8192
22
+ streaming: true
23
+ # Haiku 4.5 accepts sampling parameters:
24
+ haiku-4-5:
25
+ model_id: us.anthropic.claude-haiku-4-5
26
+ region: us-east-1
27
+ max_tokens: 4096
28
+ temperature: 0.2
29
+ top_p: 0.9
30
+ top_k: 50
31
+ streaming: true