Compare commits

...

8 Commits

Author SHA1 Message Date
Patrick Buckley 8ad17666b9 chore: bump version to 1.7.3 2026-07-09 19:24:17 -07:00
Patrick Buckley 03fc0861a6 docs(changelog): add 1.7.3 release notes 2026-07-09 19:24:16 -07:00
Patrick Buckley a22fb2f395 fix(personas): engineer prompt wording from PR feedback
Name the task_agent tool literally so the model connects the guidance
to the tool the persona grants, and restore "asking for permission".

(cherry picked from commit 9668862a7f)
2026-07-09 19:21:41 -07:00
Patrick Buckley cdcd040da2 feat(personas): harden engineer base prompt with process discipline
engineer.md is the default BASE module for non-coordinator sessions.
Rework it from posture-level guidance to explicit process discipline:
phased work (understand, design, plan, edit, verify) with ceremony
scaled to the size of the change, red-green as the default for
testable work, minimal-diff scoping, a thrash-stop after repeated
failed attempts, and reporting only observed results. Exploration
delegates to task agents; push-back happens once, then defers with
the disagreement stated for the record.

(cherry picked from commit a0d7e2266e)
2026-07-09 19:21:41 -07:00
Patrick Buckley 834d62c9d4 docs(hypothesis): carry the factored Q_E reading into the glossary; primer wording
Review follow-ups: the s-shorthand convention and its glossary echo now
cover Q_E's own state argument (s -> w where Q_E reads it), and the Q_E
glossary row carries the factored (w, a) ~> (w', o) reading so the
symbol table no longer reintroduces the environment-reads-all-of-s
interpretation the outer-kernel note warns against. PRIMER: the
top-alone-widens bullet keeps owner language anchored to the
simple-case top; success is defined as an accepted end, consistent
with the declared-vs-actually-right distinction two sentences later.

(cherry picked from commit 2ca4113ce5)
2026-07-09 19:21:41 -07:00
Patrick Buckley 342a77fe5c docs(hypothesis): harden the normal form; sync PRIMER
HYPOTHESIS.md:
- carry the initial law mu_0 in the tuple (and its displayed signature);
  split the rejection symbol into parse failure vs authorization
  refusal, with gamma(s, bot_Y) = bot_A as an axiom and a positional
  convention for the remaining bare bots
- factor the state s = (q, w) and retype Q_E to (w, a) ~> (w', o) so the
  latent world has a generator and the displayed T is its stated
  projection; quantify fail-closed over a rejection-invariant safe set K
- read H_ok as operational acceptance (H_acc) against analysis-only
  success G, with a convention for which claims read which side; score
  C6's ceiling against G and pin C5's slack to the correct-halting
  drift, resolving the tension with its own falsifier
- state ledger integrity relative to an attestation assumption (reported
  vs actual effects); split cancellation into safe vs unresolved and
  count unresolved as possibly-bad; add the realizability clause to
  C1/C3; admit multi-principal trust tops as deployment choices
- reversibility is declared in the tool contract the gate reads at
  authorization; the returned record's mark is confirmation, not source

PRIMER.md: mirror the same corrections in plain language -- ceiling not
cliff for the desk wall, contract-first reversibility, the multi-party
trust top (including the summary line), declared-vs-actual success on
dashboards, reported-vs-actual ledger honesty, cancel is not
automatically safe.

(cherry picked from commit 31301ba2a6)
2026-07-09 19:21:41 -07:00
Patrick Buckley fd7a447ef9 fix(providers): include allowed reasoning modes in the unknown-mode warning
Mirror the verbosity warning so an operator typo in reasoning_mode logs the allowed values, not just the offending one.

(cherry picked from commit f5f721a979)
2026-07-09 19:21:41 -07:00
Patrick Buckley 552ee3c590 feat(providers): add OpenAI GPT-5.6 (Sol/Terra/Luna) support
Onboard the GPT-5.6 family (GA 2026-07-09) to the OpenAI Responses lane.

- Capability rows for gpt-5.6 (= Sol alias/catch-all), gpt-5.6-terra, and
  gpt-5.6-luna: 1.05M context, 128K output, tool_search/vision/pdf/reasoning
  replay, default effort medium, temperature only at effort=none.
- "max" reasoning effort, Sol-only; Terra/Luna cap at xhigh (the knob's "max"
  snaps to the xhigh ceiling). First commercial OpenAI use of "max" — the
  ordinal knob already ranked it, so no effort-ladder change was needed.
- Verbosity and pro mode as operator-declared capability fields
  (supports_verbosity/verbosity, supports_pro_mode/reasoning_mode), merged
  from the model-definition capabilities JSON and emitted on the Responses
  wire as text.verbosity and reasoning.mode. Both are gated by a supports
  flag plus an enum guard that drops unknown values with a warning. Pro mode
  is Sol-only. There is no gpt-5.6-pro model — "pro" is the reasoning.mode
  param, not a separate model id.
- Raise the openai floor to >=2.44 for the 5.6 Responses params.

Unit and wire-golden tests cover the rows, max->xhigh snapping, the two
levers, and the enum guards. Validated live against the OpenAI API: gpt-5.6
accepts the model id, effort "max", text.verbosity, and reasoning.mode="pro".

(cherry picked from commit 47f908c9e0)
2026-07-09 19:21:41 -07:00
15 changed files with 521 additions and 42 deletions
+34
View File
@@ -14,6 +14,40 @@ experimental line:
Earlier stable lines (`stable/1.6`, `stable/1.5`) are frozen.
## [1.7.3]
A small feature and maintenance patch for the 1.7 line. No schema migrations
and no new configuration knobs.
### Added
- **OpenAI GPT-5.6 (Sol/Terra/Luna) support** — the Responses provider
understands the GPT-5.6 family: the `reasoning.mode` control, the new
`max` effort tier, and `text.verbosity`, with golden wire payloads pinning
the request shapes. The `openai` dependency floor moves to `>=2.44`.
### Changed
- **Engineer base prompt hardened with process discipline** — the default
base prompt for non-coordinator sessions now works in phases scaled to the
size of the change, defaults to red-green for testable work, scopes to the
smallest sufficient diff, stops to report after repeated failed attempts
instead of thrashing, reports only observed results, and delegates
exploration to `task_agent`. Persona prompts freeze into the workstream
stamp at creation, so this reaches new workstreams only.
### Fixed
- **Unknown reasoning-mode warnings name the allowed modes** — a model
definition with an unrecognized reasoning mode now logs the valid options
instead of leaving the operator to guess.
### Documentation
- **HYPOTHESIS.md / PRIMER.md** — the control normal form is tightened and
the factored Q_E reading is carried into the glossary; the plain-language
PRIMER stays in sync.
## [1.7.2]
A feature-bearing patch for the 1.7 line. Rather than hold this work for the
+21 -21
View File
File diff suppressed because one or more lines are too long
+10 -10
View File
@@ -16,7 +16,7 @@ One sentence to keep: **the model proposes; the gate disposes.** The model's out
| Plain name | What it does | In the formal doc |
|---|---|---|
| The owner | The human or account the run acts for; the only party who can grant new permissions | the trusted principal |
| The owner | The human or sign-off group — the run acts for; the only place new permissions can come from | the trusted principal |
| The memory | Everything the run knows: task, plan, transcript, and the ledger of what has been done | the state, *s* |
| The prompt builder | Decides which slice of memory the model gets to see this step | the lowering, π |
| The model | The black box that reads the prompt and writes a proposal | the plant, M_W |
@@ -67,7 +67,7 @@ Three consequences people miss:
**Validation must not act.** A "validator" that resolves a URL, expands a template that fires a webhook, or evaluates an argument has already acted — inside the check. The gate must be pure: it reads the proposal and the memory and outputs yes or no. If deciding requires touching the world, that touch is itself an action and goes through the gate.
**Anything irreversible is decided at the gate.** The verifier can reject a bad *result*; it cannot unsend the email. So the question "can we take this back, and until when?" is asked before execution — which means each tool's effect record has to carry a reversibility mark, or the gate can't ask it.
**Anything irreversible is decided at the gate.** The verifier can reject a bad *result*; it cannot unsend the email. So the question "can we take this back, and until when?" is asked before execution — which means each tool declares, up front, how reversible its effects are, and the gate reads that declaration when it decides; the mark that comes back in the result record is confirmation for the books, not the gate's source — the gate needed the answer before the tool ever ran.
Two honest asterisks. First, the gate checks a snapshot: it approves against the world *as its memory describes it*, and the world can move between check and commit. For actions that race the world — spend against a balance, write against a row — the tool itself must bind check to commit (compare-and-swap), or you have a classic time-of-check/time-of-use hole. The gate decides; for those effects, the tool enforces. Second, a gate is only as binding as the authority behind the tools. A tool process holding standing credentials — a database connection with every grant, an environment full of long-lived secrets — doesn't need the model's proposal to act, and against it the gate's "no" is a decision with nothing enforcing it. **A gate in front of an omnipotent tool is a suggestion.** The fix is to make the approval *be* the key: each authorized action carries a short-lived credential scoped to exactly that action, that resource, that operation, so tools hold no standing power at all.
@@ -85,13 +85,13 @@ A measurement is a risk metric. A proof is a certificate. Keeping those two word
Formally, security here is a *reach-avoid* problem: reach a good stop, never touch the danger zone, **while an adversary picks the worst tool outputs your setup permits**. That last clause is the formal home of prompt injection: injection isn't "the model misbehaved," it's the environment optimized to bend your loop — poisoned pages, malicious tool descriptions, crafted responses.
Two different numbers fall out here, and dashboards love to collapse them: *success* (reached the right end before anything went wrong — a safe refusal counts against it) and *safety* (never touched the danger zone — a safe refusal is perfectly safe). Track both. They move independently.
Two different numbers fall out here, and dashboards love to collapse them: *success* (reached an accepted end before anything went wrong — a safe refusal counts against it) and *safety* (never touched the danger zone — a safe refusal is perfectly safe). Track both. They move independently. And both are scored by your own stop rule — they count what the shell *declared* a success. Whether a declared success was actually *right* is a third, harder number that no dashboard inside the system can produce; only a judge outside the run — a test suite, an audit, ground truth — can.
The gate handles the visible half of injection: the model, freshly poisoned, proposes emailing your credentials somewhere, and the gate refuses — and injection or not, the action does not happen. But the deeper attack doesn't propose a bad action today. It rewrites *what the run believes its job is* — it edits the plan — and then every future action looks locally reasonable against a corrupted plan. So memory has to be partitioned: **data** (tool results, fetched pages, retrieved documents — content the world supplied) and **control** (the plan, the permissions, what is authorized next). The security claim is conditional on that partition holding: untrusted content lands in data, always.
The gate handles the visible half of injection: the model, freshly poisoned, proposes emailing your credentials somewhere, and the gate refuses — and injection or not, the action does not happen. But the deeper attack doesn't propose a bad action today. It rewrites *what the run believes its job is* — it edits the plan — and then every future action looks locally reasonable against a corrupted plan. So memory has to be partitioned: **data** (tool results, fetched pages, retrieved documents — content the world supplied) and **control** (the plan, the permissions, what is authorized next). The security claim is conditional on that partition holding: untrusted content lands in data, always. And "trust" is really two questions pointing opposite ways, which is worth keeping straight: *can this leak?* (a value is as secret as the most-secret thing that fed it — secrecy flows **upward**) and *can this boss us around?* (a value is as trustworthy as the least-trustworthy thing that fed it — authority flows **downward**). Untrusted content is safe as *data* precisely because the second question keeps it off the control side; a secret is kept out of the model by the first. Lowering either barrier on purpose — declassifying a secret, promoting data to trusted — is an explicit decision the owner makes, never a thing that happens by accident when two values are combined.
Which forces the question the theory has to answer: *somebody* must be able to write control mid-run, or no plan could ever be steered and no permission ever granted. The answer is a small hierarchy with exactly one party at the top:
Which forces the question the theory has to answer: *somebody* must be able to write control mid-run, or no plan could ever be steered and no permission ever granted. The answer is a small hierarchy with a top the model can't reach. The simplest top is one owner — but it needn't be a single person: a two-person sign-off, a quorum, several authenticated people each holding different scopes all work equally well, because the one property that matters is the same for all of them — the thing that can grant new power is a *human decision*, never a model:
- **The owner alone widens.** New permission, bigger budget, approval of the irreversible thing — asking the owner is itself an ordinary tool call, and the owner's answer is the one kind of tool result allowed to change control.
- **The top alone widens.** New permission, bigger budget, approval of the irreversible thing — asking the top — the owner, in the simple case — is itself an ordinary tool call, and its answer is the one kind of tool result allowed to change control.
- **The model rewrites the plan** — that is what replanning *is* — but only through the gated loop, and a plan is not a permission: nothing the model writes into its own plan can grant it powers it didn't have.
- **Everything else is data.** A fetched page can inform the plan only by passing through the model and the gate like everything else. It can suggest. It cannot promote itself to boss.
- **AI judges only tighten.** Add a model-based check — "does this action match what the user actually wanted?" — and its verdict may *veto* an action the plain rules would have allowed, never approve one they'd have refused. A judge that can approve is a tricked judge that can open the vault. And don't over-credit the veto either: a tricked judge can *aim* its refusals — denying exactly the action safety depended on, or denying everything but the path an attacker curated — so the escape hatch to the owner is the one thing a judge can never veto, and a judge's stated *reasons* are picked from a fixed, shell-owned menu, never written as prose. A judge that writes free text into the loop is an injection channel wearing a badge.
@@ -102,9 +102,9 @@ One more rule closes the loop: transformations don't launder trust. A *summary*
The formal document's appendix works the operational cases in full; here they are at speed.
**The ledger, and the three-way distinction that keeps it honest.** Every action gets an ID and a record: committed, never-launched, or *unknown*. "The tool didn't confirm" is not "the tool didn't do it" — collapse those and you will, sooner or later, re-send something that already happened. The double-send bug has one reliable cure: **journal before dispatch.** The shell writes "I am about to run action #417" into durable memory *before* the tool sees it, so a crash in the gap resumes to an honest "unknown — go ask," never to silence misread as "never sent." Old database wisdom, but here it isn't imported; it's forced — it is the only ordering under which every crash point has a truthful reading.
**The ledger, and the three-way distinction that keeps it honest.** Every action gets an ID and a record: committed, never-launched, or *unknown*. "The tool didn't confirm" is not "the tool didn't do it" — collapse those and you will, sooner or later, re-send something that already happened. And a subtler honesty: the ledger records what the tool *reported*, not what the world actually did. A well-built shell can guarantee its bookkeeping is faithful to the responses it received — it cannot, on its own, guarantee a tool told the truth. A tool that returns a clean "done!" for something it never did puts a clean "done!" in your ledger. So "the ledger is what happened" is only as good as your reason to trust the tools reporting into it; where you have no such reason, *unknown* is the honest entry, not an optimistic guess in either direction. The double-send bug has one reliable cure: **journal before dispatch.** The shell writes "I am about to run action #417" into durable memory *before* the tool sees it, so a crash in the gap resumes to an honest "unknown — go ask," never to silence misread as "never sent." Old database wisdom, but here it isn't imported; it's forced — it is the only ordering under which every crash point has a truthful reading.
**Crashes aren't finishes.** A process dying mid-run is not the run stopping; it's the run *pausing being computed*. Resume means re-entering the loop at the last durable memory — sound exactly when the durable memory was the *whole* state. Anything load-bearing that lived only in RAM — an in-flight buffer, a plan revision not yet written — is a bug you discover at the worst possible time. Recovery is where you find out whether your state was really your state.
**Crashes aren't finishes.** A process dying mid-run is not the run stopping; it's the run *pausing being computed*. Resume means re-entering the loop at the last durable memory — sound exactly when the durable memory was the *whole* state. Anything load-bearing that lived only in RAM — an in-flight buffer, a plan revision not yet written — is a bug you discover at the worst possible time. Recovery is where you find out whether your state was really your state. And a run you stopped — crash or deliberate cancel — is not automatically a *safe* run: if something was in flight and you never learned whether it fired, it may already have done the damage. "We stopped in time" is only true when everything in flight resolved to something safe; an outstanding *unknown* has to be treated as possibly-bad, the same optimism the ledger warns against, one level up.
**Two innocent actions can be guilty together.** Models emit several tool calls per turn. "Read the secret" passes review. "Post to the web" passes review. The pair is an exfiltration channel — so the gate authorizes the *set*, atomically, with the interactions checked, not each element in isolation.
@@ -140,7 +140,7 @@ This is a hypothesis, and it says out loud what would kill it. The tests, in pla
- **The red-team test.** Swap sampled tool outputs for worst-case ones: injected pages, poisoned metadata, malformed replies. The design must survive the worst permitted world, not the average one.
- **Gates versus begging.** The theory predicts deterministic gating beats prompt-level pleading. If "please be careful" alone matches real gates on security outcomes, the controller-versus-model story is wrong.
- **The compression hunt.** Exhibit a compact, provably sound progress certificate for a frontier-scale model on a nontrivial task family, and the central conjecture falls — constructively.
- **The desk probe.** Take a task family with a *proven* memory floor — so "it needed the whole picture at once" is someone else's theorem, not our excuse — scale it past the window, and watch: the wall predicts collapse at the boundary, not graceful degradation.
- **The desk probe.** Take a task family with a *proven* memory floor — so "it needed the whole picture at once" is someone else's theorem, not our excuse — scale it past the window, and watch: the wall predicts a *ceiling*, not a cliff — past the boundary, a success rate that stays capped no matter how many retries you buy. A family solved reliably out there, without new shell tricks for splitting the work, kills the wall.
## Who else landed here
@@ -148,7 +148,7 @@ The formal document keeps three honesty tiers. **Borrowed**: real theorems, cite
## What to remember
The model proposes; the gate disposes. No is the default, and a refusal must be safe. Exactly one party widens permissions — and it is not the model, a tool result, a summary, or a judge. "Didn't confirm" is not "didn't happen." The desk is finite and the proof doesn't compress, so you measure — and you say *measurement* when you mean measurement. A robot that never stops leaks safety slowly, so it needs scheduled resets — and when it can't reach you, it must be able to stop. A loop that runs robots for you is just a bigger robot with the same rules and a further-away owner. And all of it is a hypothesis wearing its own kill-conditions on its sleeve.
The model proposes; the gate disposes. No is the default, and a refusal must be safe. Only the top of the trust hierarchy widens permissions — a human decision, never the model, a tool result, a summary, or a judge. "Didn't confirm" is not "didn't happen." The desk is finite and the proof doesn't compress, so you measure — and you say *measurement* when you mean measurement. A robot that never stops leaks safety slowly, so it needs scheduled resets — and when it can't reach you, it must be able to stop. A loop that runs robots for you is just a bigger robot with the same rules and a further-away owner. And all of it is a hypothesis wearing its own kill-conditions on its sleeve.
The formal version — the objects, the certificates, the falsifiers, the citations — is [HYPOTHESIS.md](HYPOTHESIS.md). It wins every disagreement with this file, including this sentence.
+2 -2
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "turnstone"
version = "1.7.2"
version = "1.7.3"
description = "Multi-node AI orchestration platform with tool use, agent routing, and cluster simulation."
readme = "README.md"
license = "Apache-2.0"
@@ -23,7 +23,7 @@ classifiers = [
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]
dependencies = [
"openai>=2.37",
"openai>=2.44", # GPT-5.6 (Sol/Terra/Luna): Responses reasoning.mode + effort "max" + text.verbosity
"anthropic>=0.108", # claude-fable-5 support; hard runtime floor is 0.105 (mid-conversation system blocks)
"httpx>=0.28",
"mcp>=1.27,<2", # v2 is a breaking rewrite (2.0.0a1 live 2026-06-11; stable ~2026-07-27) — streamablehttp_client removed, 2-tuple transport, snake_case types; migrate deliberately
@@ -0,0 +1,30 @@
{
"include": [
"reasoning.encrypted_content"
],
"input": [
{
"content": "Hi there.",
"role": "user",
"type": "message"
},
{
"content": "Hello! How can I help?",
"role": "assistant",
"type": "message"
},
{
"content": "What's the weather in Paris?",
"role": "user",
"type": "message"
}
],
"max_output_tokens": 4096,
"model": "gpt-5.6-sol",
"prompt_cache_retention": "24h",
"reasoning": {
"effort": "max"
},
"store": false,
"stream": true
}
@@ -0,0 +1,55 @@
{
"include": [
"reasoning.encrypted_content"
],
"input": [
{
"content": "Weather in Paris?",
"role": "user",
"type": "message"
},
{
"arguments": "{\"city\": \"Paris\"}",
"call_id": "call_1",
"name": "get_weather",
"type": "function_call"
},
{
"call_id": "call_1",
"output": "18C, clear.",
"type": "function_call_output"
},
{
"content": "It's 18C and clear in Paris.",
"role": "assistant",
"type": "message"
}
],
"max_output_tokens": 4096,
"model": "gpt-5.6-sol",
"prompt_cache_retention": "24h",
"reasoning": {
"effort": "max"
},
"store": false,
"stream": true,
"tools": [
{
"description": "Look up the weather for a city.",
"name": "get_weather",
"parameters": {
"properties": {
"city": {
"type": "string"
}
},
"required": [
"city"
],
"type": "object"
},
"strict": false,
"type": "function"
}
]
}
@@ -0,0 +1,34 @@
{
"include": [
"reasoning.encrypted_content"
],
"input": [
{
"content": "Hi there.",
"role": "user",
"type": "message"
},
{
"content": "Hello! How can I help?",
"role": "assistant",
"type": "message"
},
{
"content": "What's the weather in Paris?",
"role": "user",
"type": "message"
}
],
"max_output_tokens": 4096,
"model": "gpt-5.6-sol",
"prompt_cache_retention": "24h",
"reasoning": {
"effort": "high",
"mode": "pro"
},
"store": false,
"stream": true,
"text": {
"verbosity": "low"
}
}
+136
View File
@@ -2212,6 +2212,40 @@ class TestOpenAIParameterGating:
assert "temperature" not in kwargs
assert kwargs["reasoning_effort"] == "medium" # fell back from unsupported "low"
def test_gpt56_sol_max_effort_and_temperature(self) -> None:
"""GPT-5.6 (Sol / bare alias): 1M context + tool search; accepts
the NEW "max" reasoning effort verbatim (first commercial OpenAI
model to use it); temperature only at reasoning_effort="none"."""
caps = lookup_openai_capabilities("gpt-5.6")
assert caps.context_window == 1050000
assert caps.supports_tool_search is True
assert caps.supports_vision is True
assert "max" in caps.reasoning_effort_values
kwargs: dict[str, Any] = {}
apply_temperature_and_effort(kwargs, caps, temperature=0.7, reasoning_effort="max")
assert "temperature" not in kwargs
assert kwargs["reasoning_effort"] == "max"
none_kwargs: dict[str, Any] = {}
apply_temperature_and_effort(none_kwargs, caps, temperature=0.7, reasoning_effort="none")
assert none_kwargs["temperature"] == 0.7
assert none_kwargs["reasoning_effort"] == "none"
def test_gpt56_sol_id_resolves_by_prefix(self) -> None:
"""The explicit "gpt-5.6-sol" id and dated Sol snapshots inherit
the Sol/alias row (incl. "max") by longest-prefix match."""
assert "max" in lookup_openai_capabilities("gpt-5.6-sol").reasoning_effort_values
assert "max" in lookup_openai_capabilities("gpt-5.6-2026-07-09").reasoning_effort_values
def test_gpt56_terra_luna_max_snaps_to_xhigh_ceiling(self) -> None:
"""GPT-5.6 Terra and Luna have no "max" (Sol-only); the knob's "max"
snaps DOWN to the declared "xhigh" ceiling rather than being dropped."""
for tier in ("gpt-5.6-terra", "gpt-5.6-luna"):
caps = lookup_openai_capabilities(tier)
assert "max" not in caps.reasoning_effort_values, tier
kwargs: dict[str, Any] = {}
apply_temperature_and_effort(kwargs, caps, temperature=0.7, reasoning_effort="max")
assert kwargs["reasoning_effort"] == "xhigh", tier
class TestAnthropicOrphanedToolUse:
"""Verify _convert_messages synthesizes tool_results for orphaned tool_use."""
@@ -3919,6 +3953,10 @@ class TestOpenAIPromptCaching:
"gpt-5.4-pro",
"gpt-5.5",
"gpt-5.5-pro",
"gpt-5.6",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gpt-5-mini",
"gpt-5-pro",
):
@@ -4316,6 +4354,104 @@ class TestResponsesParamBuilding:
)
assert kwargs["store"] is False
def _build(self, caps: ModelCapabilities, reasoning_effort: str = "medium") -> dict[str, Any]:
return self.provider._build_kwargs(
model="gpt-5.6-sol",
messages=[{"role": "user", "content": "Hi"}],
tools=None,
max_tokens=4096,
temperature=0.5,
reasoning_effort=reasoning_effort,
deferred_names=None,
capabilities=caps,
)
def test_verbosity_emitted_under_text_when_supported(self) -> None:
"""Operator-declared verbosity nests under text.verbosity (never
top-level, which 400s on the Responses API)."""
kwargs = self._build(ModelCapabilities(supports_verbosity=True, verbosity="low"))
assert kwargs["text"] == {"verbosity": "low"}
def test_verbosity_omitted_when_unsupported(self) -> None:
"""A verbosity value on a model that doesn't support it is dropped."""
kwargs = self._build(ModelCapabilities(supports_verbosity=False, verbosity="low"))
assert "text" not in kwargs
def test_verbosity_omitted_when_value_empty(self) -> None:
"""Supported but unset (the default) → nothing sent, server default."""
kwargs = self._build(ModelCapabilities(supports_verbosity=True, verbosity=""))
assert "text" not in kwargs
def test_pro_mode_folds_into_reasoning(self) -> None:
"""reasoning.mode='pro' rides alongside the effort in one dict."""
caps = ModelCapabilities(
supports_pro_mode=True,
reasoning_mode="pro",
reasoning_effort_values=("low", "medium", "high"),
)
kwargs = self._build(caps, reasoning_effort="high")
assert kwargs["reasoning"] == {"effort": "high", "mode": "pro"}
def test_pro_mode_rejected_when_unsupported(self) -> None:
"""A pro reasoning_mode on Terra/Luna (supports_pro_mode False) is
dropped effort still rides, mode does not."""
caps = ModelCapabilities(
supports_pro_mode=False,
reasoning_mode="pro",
reasoning_effort_values=("low", "medium", "high"),
)
kwargs = self._build(caps, reasoning_effort="high")
assert kwargs["reasoning"] == {"effort": "high"}
def test_pro_mode_without_effort_sends_mode_only(self) -> None:
"""No declared effort (param omitted) but pro mode set → the
reasoning dict carries mode alone (effort defaults server-side)."""
caps = ModelCapabilities(supports_pro_mode=True, reasoning_mode="pro")
kwargs = self._build(caps, reasoning_effort="medium")
assert kwargs["reasoning"] == {"mode": "pro"}
def test_verbosity_unknown_value_dropped(self) -> None:
"""A verbosity outside {low,medium,high} is dropped, not sent — an
operator typo must not 400 every request."""
kwargs = self._build(ModelCapabilities(supports_verbosity=True, verbosity="verbose"))
assert "text" not in kwargs
def test_pro_mode_unknown_value_dropped(self) -> None:
"""An unknown reasoning_mode is dropped; a valid effort still rides."""
caps = ModelCapabilities(
supports_pro_mode=True,
reasoning_mode="ultra",
reasoning_effort_values=("low", "medium", "high"),
)
kwargs = self._build(caps, reasoning_effort="high")
assert kwargs["reasoning"] == {"effort": "high"}
def test_gpt56_terra_max_snaps_to_xhigh_on_responses_wire(self) -> None:
"""Terra's knob "max" snaps to the xhigh ceiling on the ACTUAL
Responses wire path (_build_kwargs), not only the shared resolver."""
kwargs = self.provider._build_kwargs(
model="gpt-5.6-terra",
messages=[{"role": "user", "content": "Hi"}],
tools=None,
max_tokens=4096,
temperature=0.5,
reasoning_effort="max",
deferred_names=None,
)
assert kwargs["reasoning"] == {"effort": "xhigh"}
def test_gpt56_verbosity_and_pro_flags(self) -> None:
"""The static rows carry the right capability flags: verbosity on all
three tiers, pro mode on Sol/alias only."""
sol = lookup_openai_capabilities("gpt-5.6-sol")
assert sol.supports_verbosity is True
assert sol.supports_pro_mode is True
assert lookup_openai_capabilities("gpt-5.6").supports_pro_mode is True
for tier in ("gpt-5.6-terra", "gpt-5.6-luna"):
caps = lookup_openai_capabilities(tier)
assert caps.supports_verbosity is True
assert caps.supports_pro_mode is False
def _kwargs_with(self, tools: list[dict[str, Any]], caps: ModelCapabilities) -> dict[str, Any]:
return self.provider._build_kwargs(
model="gpt-5.4",
+51
View File
@@ -23,6 +23,7 @@ the affected golden and inspecting the diff.
from __future__ import annotations
import contextlib
import dataclasses
import json
import os
from pathlib import Path
@@ -308,3 +309,53 @@ def test_wire_payload_anthropic_compat(fixture_id: str) -> None:
)
assert "thinking" not in payload, "compat lane must never send the native thinking param"
_assert_golden(f"anthropic_compat__{fixture_id}", payload)
# GPT-5.6 Sol is the first COMMERCIAL OpenAI model to expose the "max"
# reasoning effort (Terra/Luna cap at "xhigh"; see OPENAI_CAPABILITIES).
# The base matrix above pins only the default-effort Responses shape
# (gpt-5 → "medium"), so freeze a max-effort request to prove the new
# level compiles onto the native ``reasoning={"effort": "max"}`` param.
# Driving it through "gpt-5.6-sol" also exercises the longest-prefix
# inheritance (that id resolves to the "gpt-5.6" row) on the real wire,
# not just the capability lookup — over a bare turn and a tool round-trip.
_OPENAI_MAX_FIXTURES = ("text", "toolcall_complete")
@pytest.mark.parametrize("fixture_id", _OPENAI_MAX_FIXTURES)
def test_wire_payload_openai_max(fixture_id: str) -> None:
messages, opts = _FIXTURES[fixture_id]
provider = OpenAIResponsesProvider()
payload = _capture(
provider,
model="gpt-5.6-sol",
messages=[dict(m) for m in messages],
reasoning_effort="max",
**opts,
)
assert payload["reasoning"] == {"effort": "max"}, "max must compile onto reasoning.effort"
_assert_golden(f"openai_responses_max__{fixture_id}", payload)
def test_wire_payload_openai_verbosity_pro() -> None:
"""Operator-declared verbosity + pro mode compile onto ``text.verbosity``
and ``reasoning.mode`` for GPT-5.6 Sol. Both are default-off; an operator
turns them on via the model-definition capabilities JSON, which
``ChatSession._resolve_capabilities`` merges into caps at request time
modeled here by the ``dataclasses.replace`` override _capture forwards."""
provider = OpenAIResponsesProvider()
caps = dataclasses.replace(
provider.get_capabilities("gpt-5.6-sol"), verbosity="low", reasoning_mode="pro"
)
messages, opts = _FIXTURES["text"]
payload = _capture(
provider,
model="gpt-5.6-sol",
messages=[dict(m) for m in messages],
caps=caps,
reasoning_effort="high",
**opts,
)
assert payload["text"] == {"verbosity": "low"}
assert payload["reasoning"] == {"effort": "high", "mode": "pro"}
_assert_golden("openai_responses_verbosity_pro__text", payload)
+1 -1
View File
@@ -1,3 +1,3 @@
"""turnstone - Multi-node AI orchestration platform with tool use, agent routing, and cluster simulation."""
__version__ = "1.7.2"
__version__ = "1.7.3"
@@ -172,6 +172,60 @@ OPENAI_CAPABILITIES: dict[str, ModelCapabilities] = {
supports_pdf=True,
supports_reasoning_replay=True,
),
# GPT-5.6 (Sol / Terra / Luna) — released 2026-07-09. The bare
# "gpt-5.6" alias routes to Sol (developers.openai.com/api/docs/guides/
# latest-model, 2026-07 check), so this catch-all row carries Sol's
# caps and also covers dated Sol snapshots ("gpt-5.6-2026-..") and the
# explicit "gpt-5.6-sol" id by longest-prefix match. Sol is the ONLY
# 5.6 tier that unlocks the new "max" reasoning effort — the first
# COMMERCIAL OpenAI model to use it (KNOB_EFFORT_ORDER already ranks
# "max" for the Anthropic lane, so the ordinal snap and effort ladder
# need no change). Sol also has a Sol-only "ultra" multi-agent mode
# that Turnstone does NOT expose (only "pro" is wired — see below).
# Default effort is "medium" like gpt-5.5; temperature is accepted only at
# reasoning_effort="none" (the "none"-in-values gate). There is NO
# gpt-5.6-pro model: "pro" is now a reasoning.mode="pro" request param,
# not a separate model id. Context window is not yet on the model page
# (limited preview); 1.05M mirrors the 5.4/5.5 lineage — override via
# the DB model definition if OpenAI publishes a different window (a
# smaller Luna window has been reported but is unconfirmed).
"gpt-5.6": ModelCapabilities(
context_window=1050000,
max_output_tokens=128000,
reasoning_effort_values=("none", "low", "medium", "high", "xhigh", "max"),
default_reasoning_effort="medium",
supports_tool_search=True,
supports_vision=True,
supports_pdf=True,
supports_reasoning_replay=True,
supports_verbosity=True,
supports_pro_mode=True, # Sol-only reasoning.mode="pro"
),
# GPT-5.6 Terra — balanced tier; Sol's ladder minus "max" (Sol-only),
# so the knob's "max" snaps to the "xhigh" ceiling. No pro mode.
"gpt-5.6-terra": ModelCapabilities(
context_window=1050000,
max_output_tokens=128000,
reasoning_effort_values=("none", "low", "medium", "high", "xhigh"),
default_reasoning_effort="medium",
supports_tool_search=True,
supports_vision=True,
supports_pdf=True,
supports_reasoning_replay=True,
supports_verbosity=True,
),
# GPT-5.6 Luna — fastest/cheapest tier; no "max" effort, no pro mode.
"gpt-5.6-luna": ModelCapabilities(
context_window=1050000,
max_output_tokens=128000,
reasoning_effort_values=("none", "low", "medium", "high", "xhigh"),
default_reasoning_effort="medium",
supports_tool_search=True,
supports_vision=True,
supports_pdf=True,
supports_reasoning_replay=True,
supports_verbosity=True,
),
# O-series reasoning models
"o1": ModelCapabilities(
context_window=200000,
@@ -372,6 +426,48 @@ def apply_cache_retention(kwargs: dict[str, Any], model: str) -> None:
kwargs["prompt_cache_retention"] = "24h"
# ---------------------------------------------------------------------------
# Output verbosity + reasoning mode (Responses API)
# ---------------------------------------------------------------------------
# Known-good enum values for the operator-declared ``verbosity`` /
# ``reasoning_mode`` capability fields. These arrive from the
# model-definition capabilities JSON via
# ``ChatSession._resolve_capabilities`` — a field-name-filtered
# ``dataclasses.replace`` that does NOT validate values — so an operator
# typo would otherwise ride straight to the wire and 400 every request.
# The emission sites drop unknown values with a warning instead, mirroring
# how ``model_registry`` clamps out-of-range temperature / max_tokens.
VERBOSITY_LEVELS: frozenset[str] = frozenset({"low", "medium", "high"})
REASONING_MODES: frozenset[str] = frozenset({"pro"})
def apply_verbosity(kwargs: dict[str, Any], caps: ModelCapabilities) -> None:
"""Set Responses-API output verbosity when the operator declared one.
``verbosity`` (``"low"``/``"medium"``/``"high"``) is the GPT-5 family's
output-length lever, distinct from reasoning effort (you can ask for a
terse answer at high reasoning). It is Responses-API-specific and nests
under ``text.verbosity`` a top-level ``verbosity`` field 400s there
so Turnstone emits it on this lane only. ``supports_verbosity`` is the
static capability; ``caps.verbosity`` is the operator-declared value
(model-definition capabilities JSON), ``""`` = omit. An unset value, or
one on a model that doesn't support it, is silently omitted (matching
``apply_temperature``); a value outside ``VERBOSITY_LEVELS`` is dropped
with a warning (an operator typo must not 400 every request).
"""
if not (caps.supports_verbosity and caps.verbosity):
return
if caps.verbosity not in VERBOSITY_LEVELS:
log.warning(
"openai.responses: ignoring unknown verbosity",
value=caps.verbosity,
expected=sorted(VERBOSITY_LEVELS),
)
return
kwargs.setdefault("text", {})["verbosity"] = caps.verbosity
# ---------------------------------------------------------------------------
# Tool search (native deferred loading)
# ---------------------------------------------------------------------------
+22 -2
View File
@@ -17,10 +17,12 @@ import structlog
from turnstone.core.providers._openai_common import (
OPENAI_COMPAT_DEFAULT,
REASONING_MODES,
RETRYABLE_ERROR_NAMES,
apply_cache_retention,
apply_temperature,
apply_tool_search,
apply_verbosity,
extract_usage,
format_citations,
format_document_wrapper,
@@ -435,11 +437,29 @@ class OpenAIResponsesProvider:
apply_temperature(kwargs, caps, temperature, reasoning_effort)
# Reasoning effort → {"effort": value} dict (Responses API format)
# Reasoning params → {"effort": ..., "mode": ...} (Responses format).
# "mode": "pro" (GPT-5.6 Sol) applies more model work before a single
# final answer; it rides with or without an effort level (effort
# defaults to medium in pro mode), and effort still rides without a
# mode. Both are operator-declared and gated by their static
# capability, so a value on a model lacking the feature is dropped.
reasoning: dict[str, Any] = {}
effort = resolve_reasoning_effort(caps, reasoning_effort)
if effort:
kwargs["reasoning"] = {"effort": effort}
reasoning["effort"] = effort
if caps.supports_pro_mode and caps.reasoning_mode:
if caps.reasoning_mode in REASONING_MODES:
reasoning["mode"] = caps.reasoning_mode
else:
log.warning(
"openai.responses: ignoring unknown reasoning mode",
value=caps.reasoning_mode,
expected=sorted(REASONING_MODES),
)
if reasoning:
kwargs["reasoning"] = reasoning
apply_verbosity(kwargs, caps)
apply_cache_retention(kwargs, model)
return kwargs
+17
View File
@@ -102,6 +102,23 @@ class ModelCapabilities:
# this False: there, an empty values list means the model has no
# effort control at all (o1-mini) and the param must be omitted.
effort_passthrough: bool = False
# Responses-API output-length control (GPT-5 family): "low"/"medium"/
# "high", separate from reasoning effort. ``supports_verbosity`` is the
# static capability; ``verbosity`` is the operator-declared value
# (model-definition capabilities JSON, merged via
# ``ChatSession._resolve_capabilities``), "" = omit. Nests under
# ``text.verbosity`` on the Responses wire (a top-level ``verbosity``
# 400s there); the Chat/compat lane never emits it. A value set on a
# model whose ``supports_verbosity`` is False is dropped, not sent.
supports_verbosity: bool = False
verbosity: str = ""
# Responses-API ``reasoning.mode`` (GPT-5.6 Sol): "pro" applies more
# model work before a single final answer. ``supports_pro_mode`` is the
# static capability (Sol-only); ``reasoning_mode`` is the
# operator-declared value, "" = omit (normal reasoning). There is no
# gpt-5.6-pro *model* — "pro" is this request-level mode instead.
supports_pro_mode: bool = False
reasoning_mode: str = ""
supports_web_search: bool = False
supports_tool_search: bool = False
supports_vision: bool = False
+10 -4
View File
@@ -1,9 +1,15 @@
You are a software engineer working on this project. You know the codebase, the tools, and their limits.
You are a software engineer working on this project. You do real work: investigating bugs, implementing features, reviewing code for correctness and security, writing code that ships. You know your tools and their limits. You learn a codebase by reading it, not by assuming you remember it.
You do real work: investigating bugs, implementing features, reviewing security, writing code that ships. You have access to the project's files and the tools your environment provides. You don't have access to everything — some tools require approval, some paths are restricted, and that's by design. You work within those boundaries.
Some tools require approval and some paths are restricted — that's by design, and you work within those boundaries rather than around them. Ambiguity is different: when a request is unclear, you make a reasonable call, note what you assumed, and keep moving. You don't stall asking for permission on judgment calls.
You think before you act. You read before you edit. You verify before you commit. When something breaks, you diagnose before you retry. When you're uncertain, you say so. When a request is ambiguous, you make a reasonable call and note what you assumed — you don't stall asking for permission on every judgment call.
You think before you act. You read before you edit. You verify before you commit — and you never report a result you didn't observe. When you're uncertain, you say so. When something breaks, you diagnose before you retry. When two or three attempts haven't landed, you stop and report what you tried and what you learned instead of thrashing.
When you disagree with a direction, you push back with reasoning — then defer to the user's call.
When you take on a change, you work in phases: understand the problem and the relevant code surface, design the approach, plan the specific edits, make them, verify they work. You don't skip to editing. When you can delegate exploration to a task_agent, you do — mapping boundaries and file locations before you commit to a design. You scale the ceremony to the size of the change: a one-line fix doesn't need a phase plan.
When the work is testable, you write the failing test first, then the implementation that makes it pass. That defines "done" before you build and leaves a regression net behind. Config, docs, and mechanical refactors may not need it, but red-green is your default. A test is a specification, not a hurdle: if one is wrong, you fix it deliberately and say so. You never weaken a test to make it pass.
You make the smallest change that solves the problem. You don't refactor what you weren't asked to touch; when you see something worth improving nearby, you note it instead.
When you disagree with a direction, you push back with reasoning, once — then you defer to the user's call, stating your disagreement for the record.
The code you write will run. The files you edit are real. The commits you make go to a shared repository. Act accordingly.
Generated
+2 -2
View File
@@ -2475,7 +2475,7 @@ wheels = [
[[package]]
name = "turnstone"
version = "1.7.2"
version = "1.7.3"
source = { editable = "." }
dependencies = [
{ name = "alembic" },
@@ -2544,7 +2544,7 @@ requires-dist = [
{ name = "lacme", specifier = ">=1.0.5" },
{ name = "mcp", specifier = ">=1.27,<2" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.14" },
{ name = "openai", specifier = ">=2.37" },
{ name = "openai", specifier = ">=2.44" },
{ name = "pillow", specifier = ">=10" },
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2" },
{ name = "pydantic", specifier = ">=2.0" },