thought-fork 0.6.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.
@@ -0,0 +1,165 @@
1
+ # Copyright 2026 Ameen Saeed
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Thought Fork — Branch your AI's reasoning like Git branches.
17
+
18
+ Thought Fork spawns N parallel reasoning paths ("forks") from a single prompt,
19
+ each biased by a different stance, then merges them into a synthesis with
20
+ explicit attribution.
21
+
22
+ Concept and vocabulary by Ameen Saeed, June 2026.
23
+
24
+ Simple usage::
25
+
26
+ from thought_fork import synthesize
27
+
28
+ result = await synthesize("Should I migrate to microservices?", fork_count=3)
29
+ print(result.synthesis) # Attributed final answer
30
+ print(result.forks["cautious"]) # Individual fork output
31
+ print(result.token_usage) # {"forks": ..., "synthesis": ..., "total": ...}
32
+
33
+ Advanced usage with custom stances::
34
+
35
+ from thought_fork import Fork, synthesize
36
+
37
+ result = await synthesize(
38
+ "Review this architecture",
39
+ forks=[
40
+ Fork(id="A", stance="security", system_prompt="Find vulnerabilities"),
41
+ Fork(id="B", stance="performance", system_prompt="Find bottlenecks"),
42
+ Fork(id="C", stance="maintainability", system_prompt="Find complexity risks"),
43
+ ]
44
+ )
45
+ """
46
+
47
+ from __future__ import annotations
48
+
49
+ import time
50
+
51
+ __version__ = "0.6.0"
52
+ __author__ = "Ameen Saeed"
53
+
54
+ from thought_fork.config import BUILT_IN_STANCES, ForkConfig
55
+ from thought_fork.fork import Fork, get_stance_prompt
56
+ from thought_fork.manager import ForkManager
57
+ from thought_fork.message import Message
58
+ from thought_fork.result import ForkResult
59
+ from thought_fork.stance_selector import SelectedStance, StanceSelector
60
+ from thought_fork.synthesis import (
61
+ FORK_OUTPUT_TEMPLATE,
62
+ SYNTHESIS_SYSTEM_PROMPT,
63
+ SYNTHESIS_USER_TEMPLATE,
64
+ SynthesisEngine,
65
+ )
66
+
67
+
68
+ async def synthesize(
69
+ prompt: str,
70
+ fork_count: int = 3,
71
+ stances: list[str] | None = None,
72
+ forks: list[Fork] | None = None,
73
+ history: list[Message] | None = None,
74
+ config: ForkConfig | None = None,
75
+ ) -> ForkResult:
76
+ """Fork a prompt into parallel reasoning paths and synthesize.
77
+
78
+ This is the main entry point for the Thought Fork library.
79
+ It wraps ForkManager + SynthesisEngine into a single async call.
80
+
81
+ Args:
82
+ prompt: The question or problem to fork.
83
+ fork_count: Number of forks to spawn (ignored if ``forks`` is provided).
84
+ stances: List of stance names. Defaults to the first N built-in stances.
85
+ forks: Advanced — provide pre-built Fork objects with custom system prompts.
86
+ history: Optional list of previous Message objects for multi-turn context.
87
+ config: Optional ForkConfig to override model selection and limits.
88
+
89
+ Returns:
90
+ A ForkResult containing the synthesis, individual fork outputs,
91
+ and token usage statistics.
92
+ """
93
+ config = config or ForkConfig()
94
+ manager = ForkManager(config)
95
+ engine = SynthesisEngine(config)
96
+
97
+ overall_start = time.perf_counter()
98
+
99
+ # Create or use provided forks
100
+ if forks is not None:
101
+ # Advanced: caller provided pre-built Fork objects
102
+ fork_list = forks
103
+ elif stances is not None:
104
+ # Caller specified explicit stance names → use built-ins / custom prompts
105
+ fork_list = manager.create_forks(prompt, stances)
106
+ elif config.use_dynamic_stances:
107
+ # Default: let AI invent the best stances for this prompt
108
+ selector = StanceSelector(config)
109
+ selected = await selector.select(prompt, fork_count, history)
110
+ fork_list = selector.to_forks(selected)
111
+ else:
112
+ # Dynamic stances disabled → use configured default stances
113
+ resolved_stances = list(BUILT_IN_STANCES.keys())[:fork_count]
114
+ fork_list = manager.create_forks(prompt, resolved_stances)
115
+
116
+ # Run forks in parallel
117
+ fork_list = await manager.run_parallel(fork_list, prompt, history)
118
+
119
+ # Synthesize
120
+ synthesis_text, synthesis_tokens, synthesis_duration = (
121
+ await engine.synthesize(prompt, fork_list, history)
122
+ )
123
+
124
+ # Build result
125
+ total_fork_tokens = sum(f.token_count for f in fork_list)
126
+ total_tokens = total_fork_tokens + synthesis_tokens
127
+ overall_duration = int((time.perf_counter() - overall_start) * 1000)
128
+
129
+ return ForkResult(
130
+ synthesis=synthesis_text,
131
+ forks={f.stance: f.output for f in fork_list},
132
+ fork_details=[
133
+ {
134
+ "id": f.id,
135
+ "stance": f.stance,
136
+ "output": f.output,
137
+ "token_count": f.token_count,
138
+ "duration_ms": f.duration_ms,
139
+ }
140
+ for f in fork_list
141
+ ],
142
+ token_usage={
143
+ "forks": total_fork_tokens,
144
+ "synthesis": synthesis_tokens,
145
+ "total": total_tokens,
146
+ },
147
+ duration_ms=overall_duration,
148
+ )
149
+
150
+
151
+ __all__ = [
152
+ "synthesize",
153
+ "Fork",
154
+ "ForkConfig",
155
+ "ForkManager",
156
+ "ForkResult",
157
+ "SelectedStance",
158
+ "StanceSelector",
159
+ "SynthesisEngine",
160
+ "BUILT_IN_STANCES",
161
+ "FORK_OUTPUT_TEMPLATE",
162
+ "SYNTHESIS_SYSTEM_PROMPT",
163
+ "SYNTHESIS_USER_TEMPLATE",
164
+ "get_stance_prompt",
165
+ ]
thought_fork/cli.py ADDED
@@ -0,0 +1,175 @@
1
+ # Copyright 2026 Ameen Saeed
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Thought Fork — Command-line interface.
17
+
18
+ Usage:
19
+ thought-fork "Should I use microservices?" --forks 3
20
+ thought-fork "Review this architecture" --forks 4 --static
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import asyncio
27
+ import sys
28
+ import time
29
+
30
+ from dotenv import load_dotenv
31
+
32
+
33
+ # Fork colors for terminal output (ANSI escape codes)
34
+ FORK_COLORS = [
35
+ "\033[94m", # Blue
36
+ "\033[93m", # Yellow/Amber
37
+ "\033[91m", # Red
38
+ "\033[92m", # Green
39
+ "\033[95m", # Magenta
40
+ "\033[96m", # Cyan
41
+ "\033[90m", # Gray
42
+ ]
43
+ RESET = "\033[0m"
44
+ BOLD = "\033[1m"
45
+ DIM = "\033[2m"
46
+
47
+
48
+ def _print_banner() -> None:
49
+ """Print the Thought Fork banner."""
50
+ print()
51
+ print(f"{BOLD}🔀 Thought Fork — Branch your AI's reasoning{RESET}")
52
+ print()
53
+
54
+
55
+ def _print_fork_result(fork_detail: dict, color: str) -> None:
56
+ """Print a single fork's output with styled header."""
57
+ print()
58
+ print(f"{color}{BOLD}{'═' * 60}{RESET}")
59
+ print(f"{color}{BOLD} Fork {fork_detail['id']} — {fork_detail['stance']}{RESET}")
60
+ print(f"{color}{BOLD}{'═' * 60}{RESET}")
61
+ print()
62
+ print(fork_detail["output"])
63
+ print()
64
+
65
+
66
+ def _print_synthesis(synthesis_text: str) -> None:
67
+ """Print the synthesis output with styled header."""
68
+ print()
69
+ print(f"{BOLD}{'═' * 60}{RESET}")
70
+ print(f"{BOLD} ✦ Synthesis (Convergence){RESET}")
71
+ print(f"{BOLD}{'═' * 60}{RESET}")
72
+ print()
73
+ print(synthesis_text)
74
+ print()
75
+
76
+
77
+ def _print_summary(fork_details: list[dict], token_usage: dict, duration_ms: int) -> None:
78
+ """Print token usage summary."""
79
+ print(f"{BOLD}📊 Token Usage Summary{RESET}")
80
+ print(f"┌──────┬──────────────────────────────┬────────┬──────────┐")
81
+ print(f"│ Fork │ Stance │ Tokens │ Duration │")
82
+ print(f"├──────┼──────────────────────────────┼────────┼──────────┤")
83
+
84
+ for detail in fork_details:
85
+ duration_s = detail["duration_ms"] / 1000
86
+ stance = detail["stance"][:28]
87
+ print(
88
+ f"│ {detail['id']:<4} │ {stance:<28} │ {detail['token_count']:>6} │ {duration_s:>6.1f}s │"
89
+ )
90
+
91
+ print(f"├──────┼──────────────────────────────┼────────┼──────────┤")
92
+ total_s = duration_ms / 1000
93
+ print(
94
+ f"│ {'ALL':<4} │ {'TOTAL':<28} │ {token_usage['total']:>6} │ {total_s:>6.1f}s │"
95
+ )
96
+ print(f"└──────┴──────────────────────────────┴────────┴──────────┘")
97
+ print()
98
+
99
+
100
+ async def _run(prompt: str, fork_count: int, use_dynamic: bool) -> None:
101
+ """Execute a Thought Fork session."""
102
+ from thought_fork import ForkConfig, synthesize
103
+
104
+ config = ForkConfig(use_dynamic_stances=use_dynamic)
105
+
106
+ _print_banner()
107
+ print(f"{BOLD}Prompt:{RESET} {prompt}")
108
+ print(f"{DIM}Forks: {fork_count} | Dynamic stances: {'on' if use_dynamic else 'off'}{RESET}")
109
+ print()
110
+ print("⏳ Forking...")
111
+
112
+ result = await synthesize(prompt, fork_count=fork_count, config=config)
113
+
114
+ # Print each fork
115
+ for i, detail in enumerate(result.fork_details):
116
+ color = FORK_COLORS[i % len(FORK_COLORS)]
117
+ _print_fork_result(detail, color)
118
+
119
+ # Print synthesis
120
+ _print_synthesis(result.synthesis)
121
+
122
+ # Print summary
123
+ _print_summary(result.fork_details, result.token_usage, result.duration_ms)
124
+
125
+ print(
126
+ f"{DIM}Forks ran in parallel — wall-clock time is the slowest fork, "
127
+ f"not the sum.{RESET}"
128
+ )
129
+ print()
130
+
131
+
132
+ def main() -> None:
133
+ """CLI entry point for Thought Fork."""
134
+ # Ensure UTF-8 output on Windows terminals
135
+ if sys.platform == "win32":
136
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
137
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
138
+
139
+ load_dotenv()
140
+
141
+ parser = argparse.ArgumentParser(
142
+ prog="thought-fork",
143
+ description="🔀 Thought Fork — Branch your AI's reasoning like Git branches.",
144
+ epilog="Example: thought-fork \"Should I use microservices?\" --forks 3",
145
+ )
146
+ parser.add_argument(
147
+ "prompt",
148
+ help="The question or problem to fork into parallel reasoning paths.",
149
+ )
150
+ parser.add_argument(
151
+ "--forks", "-n",
152
+ type=int,
153
+ default=3,
154
+ help="Number of parallel forks to spawn (default: 3).",
155
+ )
156
+ parser.add_argument(
157
+ "--static",
158
+ action="store_true",
159
+ help="Use static built-in stances instead of AI-selected dynamic stances.",
160
+ )
161
+
162
+ args = parser.parse_args()
163
+
164
+ try:
165
+ asyncio.run(_run(args.prompt, args.forks, use_dynamic=not args.static))
166
+ except KeyboardInterrupt:
167
+ print(f"\n{DIM}Cancelled.{RESET}")
168
+ sys.exit(0)
169
+ except ValueError as e:
170
+ print(f"\n❌ {e}")
171
+ sys.exit(1)
172
+
173
+
174
+ if __name__ == "__main__":
175
+ main()
thought_fork/config.py ADDED
@@ -0,0 +1,171 @@
1
+ # Copyright 2026 Ameen Saeed
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Thought Fork — Configuration and built-in stance definitions.
17
+
18
+ This module defines the ForkConfig dataclass for controlling fork/synthesis
19
+ behavior, and the BUILT_IN_STANCES dictionary mapping stance names to their
20
+ system prompts.
21
+ """
22
+
23
+ import os
24
+ from dataclasses import dataclass, field
25
+ from typing import Any
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Built-in Stances
30
+ # ---------------------------------------------------------------------------
31
+ # Each stance is a system prompt that biases a fork's reasoning toward a
32
+ # specific perspective. These are the stances defined as part of the
33
+ # Thought Fork framework (see CONCEPT.md).
34
+
35
+ BUILT_IN_STANCES: dict[str, str] = {
36
+ "cautious": (
37
+ "You are a seasoned risk analyst and cautious strategist. "
38
+ "Your methodology: identify every risk, edge case, failure mode, and second-order consequence BEFORE considering benefits. "
39
+ "You think in terms of 'what kills us?' not 'what helps us?' "
40
+ "Reason with precision. Use specific examples of real-world failures to ground your analysis. "
41
+ "Provide concrete safeguards and mitigation strategies, not vague warnings. "
42
+ "Your standard of evidence is high — demand data, precedent, or logical proof for every claim."
43
+ ),
44
+ "creative": (
45
+ "You are a lateral thinker and innovation strategist with deep expertise in analogical reasoning. "
46
+ "Your methodology: actively reject the first 3 obvious answers and force yourself to explore non-obvious angles. "
47
+ "Draw analogies from completely different domains — biology, game theory, art, history, physics — to illuminate the problem from unexpected directions. "
48
+ "Challenge the framing of the question itself. Propose solutions that feel counterintuitive but are logically sound. "
49
+ "Your output should make the reader say 'I never would have thought of that.' "
50
+ "Be specific and substantive — creative does NOT mean vague."
51
+ ),
52
+ "critical": (
53
+ "You are an adversarial critic and intellectual stress-tester. "
54
+ "Your methodology: assume the prevailing answer is wrong and rigorously argue why. "
55
+ "Challenge every premise, every assumption, every piece of 'conventional wisdom.' "
56
+ "Use formal logical analysis — identify specific fallacies, hidden assumptions, and gaps in evidence. "
57
+ "You are not cynical; you are surgically precise. Your goal is to make the argument stronger by finding its weakest links. "
58
+ "Quote specific claims and show exactly where they fail under scrutiny."
59
+ ),
60
+ "pragmatic": (
61
+ "You are a battle-tested execution strategist and operational pragmatist. "
62
+ "Your methodology: ruthlessly filter for what actually works in practice given real-world constraints. "
63
+ "Ignore theoretical elegance — focus on implementation complexity, resource requirements, timeline, and ROI. "
64
+ "Provide concrete action plans with specific steps, not abstract frameworks. "
65
+ "Draw on real examples of successful execution in similar situations. "
66
+ "Your output should be immediately actionable by someone with a budget and a deadline."
67
+ ),
68
+ "first-principles": (
69
+ "You are a first-principles thinker in the tradition of Feynman and Musk. "
70
+ "Your methodology: strip away every assumption, convention, and inherited belief until you reach bedrock truths. "
71
+ "Then rebuild the answer from those fundamentals using rigorous logical chains. "
72
+ "Explicitly name each assumption you're discarding and explain why it might be wrong. "
73
+ "Your reasoning should be transparent — show every step from axiom to conclusion. "
74
+ "If the conventional answer happens to be right, prove it from first principles rather than accepting it on authority."
75
+ ),
76
+ "optimistic": (
77
+ "You are a strategic optimist and opportunity analyst. "
78
+ "Your methodology: map the full landscape of upside potential, best-case trajectories, and compounding advantages. "
79
+ "Identify catalysts and tailwinds that others miss because they're focused on risks. "
80
+ "Your optimism is NOT naive — it is grounded in specific evidence, precedent, and logical reasoning about why things could go right. "
81
+ "Provide a concrete roadmap for capturing the identified opportunities. "
82
+ "Show how small advantages compound into transformative outcomes."
83
+ ),
84
+ "contrarian": (
85
+ "You are a professional contrarian and independent thinker. "
86
+ "Your methodology: identify the consensus view, then rigorously build the strongest possible case for the opposite position. "
87
+ "Find historical examples where the contrarian view was correct and the consensus was catastrophically wrong. "
88
+ "You are not being contrarian for its own sake — you genuinely believe the best insights hide in unpopular positions. "
89
+ "Steel-man the opposing view before attacking the consensus. "
90
+ "Your output should make the reader seriously reconsider what they thought they knew."
91
+ ),
92
+ }
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # ForkConfig
97
+ # ---------------------------------------------------------------------------
98
+
99
+ @dataclass
100
+ class ForkConfig:
101
+ """Configuration for the Thought Fork engine.
102
+
103
+ Attributes:
104
+ fork_model: Model identifier for fork reasoning (fast/cheap).
105
+ synthesis_model: Model identifier for synthesis (quality).
106
+ stance_selector_model: Model used for dynamic stance selection (fast/cheap).
107
+ default_stances: List of stance names used when none are specified and
108
+ dynamic stances are disabled.
109
+ max_tokens: Maximum tokens per fork/synthesis response.
110
+ api_base_url: Base URL for the API (default: OpenRouter).
111
+ api_key: API key for the provider. If None, attempts to resolve from
112
+ OPENAI_API_KEY or OPENROUTER_API_KEY environment variables.
113
+ use_dynamic_stances: If True (default), uses AI to invent custom stances
114
+ for each prompt. If False, uses default_stances (static built-ins).
115
+ timeout_seconds: Maximum time in seconds to wait for a single API call.
116
+ max_concurrent_forks: Maximum number of forks to run simultaneously.
117
+ max_retries: Maximum number of times to retry a failed API call.
118
+ client: Optional pre-configured AsyncOpenAI client instance.
119
+ """
120
+
121
+ fork_model: str = "anthropic/claude-haiku-4.5"
122
+ synthesis_model: str = "anthropic/claude-sonnet-4-6"
123
+ stance_selector_model: str = "anthropic/claude-haiku-4.5"
124
+ default_stances: list[str] = field(
125
+ default_factory=lambda: ["cautious", "creative", "critical"]
126
+ )
127
+ available_models: list[str] = field(default_factory=list)
128
+ max_tokens: int = 2048
129
+ synthesis_max_tokens: int = 6144
130
+ api_base_url: str = "https://openrouter.ai/api/v1"
131
+ api_key: str | None = None
132
+ use_dynamic_stances: bool = True
133
+ timeout_seconds: float = 120.0
134
+ max_concurrent_forks: int = 5
135
+ max_retries: int = 2
136
+ client: Any | None = None
137
+
138
+ def __post_init__(self):
139
+ """Resolve API key and overrides from environment if not explicitly provided."""
140
+ if not self.api_key:
141
+ self.api_key = os.getenv("THOUGHT_FORK_API_KEY") or os.getenv("OPENROUTER_API_KEY") or os.getenv("OPENAI_API_KEY")
142
+
143
+ if os.getenv("THOUGHT_FORK_API_BASE"):
144
+ self.api_base_url = os.getenv("THOUGHT_FORK_API_BASE")
145
+
146
+ if os.getenv("THOUGHT_FORK_MODEL"):
147
+ self.fork_model = os.getenv("THOUGHT_FORK_MODEL")
148
+
149
+ if os.getenv("THOUGHT_FORK_SYNTHESIS_MODEL"):
150
+ self.synthesis_model = os.getenv("THOUGHT_FORK_SYNTHESIS_MODEL")
151
+ elif os.getenv("THOUGHT_FORK_MODEL"):
152
+ self.synthesis_model = os.getenv("THOUGHT_FORK_MODEL")
153
+
154
+ if os.getenv("THOUGHT_FORK_SELECTOR_MODEL"):
155
+ self.stance_selector_model = os.getenv("THOUGHT_FORK_SELECTOR_MODEL")
156
+ elif os.getenv("THOUGHT_FORK_MODEL"):
157
+ self.stance_selector_model = os.getenv("THOUGHT_FORK_MODEL")
158
+
159
+ if os.getenv("THOUGHT_FORK_AVAILABLE_MODELS"):
160
+ models = os.getenv("THOUGHT_FORK_AVAILABLE_MODELS").split(",")
161
+ self.available_models = [m.strip() for m in models if m.strip()]
162
+
163
+ # Check if URL is local (Ollama/vLLM)
164
+ is_local = any(host in self.api_base_url for host in ["localhost", "127.0.0.1", "0.0.0.0"])
165
+
166
+ if not self.api_key and not is_local:
167
+ raise ValueError(
168
+ "No API key found. Set OPENROUTER_API_KEY or OPENAI_API_KEY "
169
+ "in your environment, or pass api_key= to ForkConfig(). "
170
+ "Note: API key is not required for local URLs (localhost/127.0.0.1)."
171
+ )
thought_fork/fork.py ADDED
@@ -0,0 +1,61 @@
1
+ # Copyright 2026 Ameen Saeed
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Thought Fork — Fork dataclass and stance resolution.
17
+
18
+ A Fork represents a single parallel reasoning path. Each fork has a stance
19
+ (its reasoning perspective) and a system prompt that biases the AI toward
20
+ that stance.
21
+ """
22
+
23
+ from dataclasses import dataclass
24
+
25
+ from thought_fork.config import BUILT_IN_STANCES
26
+
27
+
28
+ @dataclass
29
+ class Fork:
30
+ """A single parallel reasoning path in the Thought Fork framework.
31
+
32
+ Attributes:
33
+ id: Letter identifier for this fork ("A", "B", "C", ...).
34
+ stance: The reasoning perspective (e.g., "cautious", "creative").
35
+ system_prompt: The full system prompt that biases reasoning.
36
+ output: The fork's generated reasoning output (populated after execution).
37
+ token_count: Number of tokens used in the response (populated after execution).
38
+ duration_ms: Execution time in milliseconds (populated after execution).
39
+ """
40
+
41
+ id: str
42
+ stance: str
43
+ system_prompt: str
44
+ output: str = ""
45
+ token_count: int = 0
46
+ duration_ms: int = 0
47
+
48
+
49
+ def get_stance_prompt(stance: str) -> str:
50
+ """Resolve a stance name to its system prompt.
51
+
52
+ If the stance name matches a built-in stance, returns that prompt.
53
+ Otherwise, treats the stance string itself as a custom system prompt.
54
+
55
+ Args:
56
+ stance: A built-in stance name (e.g., "cautious") or a custom prompt string.
57
+
58
+ Returns:
59
+ The resolved system prompt for the given stance.
60
+ """
61
+ return BUILT_IN_STANCES.get(stance, stance)