openai-420 0.0.1__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.
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: openai-420
3
+ Version: 0.0.1
4
+ Summary: Grok 4.20's inference-time multi-agent chatroom, rebuilt from scratch — specialist agents debate via tool calls, a captain synthesizes.
5
+ License: Apache-2.0 license
6
+ License-File: LICENSE
7
+ Author: Allen Chou
8
+ Author-email: f1470891079@gmail.com
9
+ Requires-Python: >=3.11,<4
10
+ Classifier: License :: Other/Proprietary License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Dist: openai
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
@@ -0,0 +1,216 @@
1
+ """The agents that own LLM logic (PRINCIPLES Law 2, 7).
2
+
3
+ This is the only place that builds a prompt and calls a model. The orchestrator never
4
+ does. Each agent holds its own incremental Conversation (Law 6) and a cursor over the
5
+ shared board.
6
+
7
+ Async only: every agent method is a coroutine function and the injected client MUST be an
8
+ ``openai.AsyncOpenAI``. Synchronous clients are rejected at construction.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+
15
+ import openai
16
+
17
+ from openai_420.conclude import CONCLUDE_TOOL, Conclusion, parse_conclude
18
+ from openai_420.conversation import Conversation
19
+ from openai_420.roster import (
20
+ ANSWER_MARKER,
21
+ AgentSpec,
22
+ captain_system_prompt,
23
+ specialist_system_prompt,
24
+ )
25
+ from openai_420.scratchpad import Scratchpad
26
+ from openai_420.trace import log_decision
27
+
28
+ _CONCLUDE_ACK = "Recorded."
29
+ _SELECT_INSTRUCTION = (
30
+ "The debate is over. Below are the specialists' final answers, numbered. Choose the "
31
+ "single best one — prefer the version the majority of specialists agree on. Do NOT "
32
+ "rewrite, merge, or add anything. Output ONLY the number of your choice."
33
+ )
34
+ _FORCE_CONCLUDE = {"type": "function", "function": {"name": "conclude"}}
35
+ _FALLBACK_DIRECTION = (
36
+ "No clear ruling was produced. Each specialist must give a concrete, complete answer "
37
+ "so consensus can be judged next round."
38
+ )
39
+
40
+
41
+ def extract_answer(output: str) -> str:
42
+ """The deliverable after the last answer marker; the whole output if the marker is
43
+ absent. Specialists state their reasoning first, then the marker, then the answer, so
44
+ the board carries reasons teammates can weigh — but the user only gets the answer.
45
+ """
46
+ if ANSWER_MARKER in output:
47
+ return output.rsplit(ANSWER_MARKER, 1)[1].strip()
48
+ return output.strip()
49
+
50
+
51
+ class Specialist:
52
+ """A debating specialist. ``respond`` is a coroutine — await it once per round."""
53
+
54
+ def __init__(
55
+ self,
56
+ *,
57
+ spec: AgentSpec,
58
+ roster: list[AgentSpec],
59
+ client: openai.AsyncOpenAI,
60
+ model: str,
61
+ user_query: str,
62
+ ) -> None:
63
+ _require_async_client(client)
64
+ self.name = spec.name
65
+ self._client = client
66
+ self._model = model
67
+ self._conversation = Conversation(
68
+ system=specialist_system_prompt(spec, roster), user_query=user_query
69
+ )
70
+ self._last_seen = 0
71
+
72
+ async def respond(self, board: Scratchpad, *, round: int) -> str:
73
+ delta = board.delta(for_author=self.name, since_round=self._last_seen)
74
+ if delta:
75
+ self._conversation.add_delta(delta)
76
+ response = await self._client.chat.completions.create(
77
+ model=self._model, messages=self._conversation.messages
78
+ )
79
+ message = response.choices[0].message
80
+ content = message.content or ""
81
+ self._conversation.add_own_turn(content)
82
+ self._last_seen = round - 1
83
+ log_decision(
84
+ self.name,
85
+ "respond",
86
+ round=round,
87
+ saw=[e.author for e in delta],
88
+ output=content,
89
+ reasoning=_reasoning(message),
90
+ )
91
+ return content
92
+
93
+
94
+ class Captain:
95
+ """The leader. ``judge`` detects consensus and locates disagreement each round; it does
96
+ NOT decide correctness. ``select`` picks one specialist's answer to return verbatim —
97
+ both coroutines. The captain acts after the round's specialists, so its cursor advances
98
+ to the current round (it sees this round's entries), unlike a specialist.
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ *,
104
+ spec: AgentSpec,
105
+ roster: list[AgentSpec],
106
+ client: openai.AsyncOpenAI,
107
+ model: str,
108
+ user_query: str,
109
+ ) -> None:
110
+ _require_async_client(client)
111
+ self.name = spec.name
112
+ self._client = client
113
+ self._model = model
114
+ self._conversation = Conversation(
115
+ system=captain_system_prompt(spec, roster), user_query=user_query
116
+ )
117
+ self._last_seen = 0
118
+
119
+ async def judge(self, board: Scratchpad, *, round: int) -> Conclusion:
120
+ delta = board.delta(for_author=self.name, since_round=self._last_seen)
121
+ if delta:
122
+ self._conversation.add_delta(delta)
123
+ self._last_seen = round
124
+ try:
125
+ response = await self._client.chat.completions.create(
126
+ model=self._model,
127
+ messages=self._conversation.messages,
128
+ tools=[CONCLUDE_TOOL],
129
+ tool_choice=_FORCE_CONCLUDE,
130
+ )
131
+ message = response.choices[0].message
132
+ except openai.BadRequestError:
133
+ # The model answered in prose instead of calling the tool. Don't crash the
134
+ # run — keep debating (the conversation is left clean for a retry next round).
135
+ log_decision(
136
+ self.name,
137
+ "judge",
138
+ round=round,
139
+ fallback=True,
140
+ consensus=False,
141
+ direction=_FALLBACK_DIRECTION,
142
+ )
143
+ return Conclusion(consensus=False, direction=_FALLBACK_DIRECTION)
144
+ self._conversation.add_assistant_message(message.model_dump(exclude_none=True))
145
+ if message.tool_calls:
146
+ self._conversation.add_tool_result(message.tool_calls[0].id, _CONCLUDE_ACK)
147
+ conclusion = _interpret_conclusion(message)
148
+ log_decision(
149
+ self.name,
150
+ "judge",
151
+ round=round,
152
+ saw=[e.author for e in delta],
153
+ consensus=conclusion.consensus,
154
+ direction=conclusion.direction,
155
+ reasoning=_reasoning(message),
156
+ fallback=False,
157
+ )
158
+ return conclusion
159
+
160
+ async def select(self, candidates: list[str]) -> int:
161
+ """Choose the best specialist answer by number (0-based) — never rewrite it.
162
+
163
+ The captain only selects from existing answers, so it cannot corrupt the text or
164
+ re-derive a wrong one; the prompt tells it to follow the specialists' majority.
165
+ """
166
+ numbered = "\n\n".join(f"[{i + 1}]\n{c}" for i, c in enumerate(candidates))
167
+ self._conversation.add_user_message(f"{_SELECT_INSTRUCTION}\n\n{numbered}")
168
+ response = await self._client.chat.completions.create(
169
+ model=self._model, messages=self._conversation.messages
170
+ )
171
+ message = response.choices[0].message
172
+ index = _parse_choice(message.content or "", len(candidates))
173
+ log_decision(
174
+ self.name,
175
+ "select",
176
+ chosen=index + 1,
177
+ raw=message.content or "",
178
+ reasoning=_reasoning(message),
179
+ )
180
+ return index
181
+
182
+
183
+ def _interpret_conclusion(message: object) -> Conclusion:
184
+ """Turn the captain's reply into a Conclusion, tolerating a missing tool call.
185
+
186
+ If the model skipped the `conclude` tool (no message, or no tool_calls), default to
187
+ no-consensus with a nudge so the debate continues instead of crashing."""
188
+ tool_calls = getattr(message, "tool_calls", None)
189
+ if not tool_calls:
190
+ return Conclusion(consensus=False, direction=_FALLBACK_DIRECTION)
191
+ return parse_conclude(tool_calls[0].function.arguments)
192
+
193
+
194
+ def _parse_choice(content: str, n: int) -> int:
195
+ """First integer in [1, n] from the captain's reply, as a 0-based index (default 0)."""
196
+ for token in re.findall(r"\d+", content):
197
+ value = int(token)
198
+ if 1 <= value <= n:
199
+ return value - 1
200
+ return 0
201
+
202
+
203
+ def _reasoning(message: object) -> str:
204
+ """The model's reasoning text, if the backend exposes one (e.g. gpt-oss)."""
205
+ return (
206
+ getattr(message, "reasoning", None)
207
+ or getattr(message, "reasoning_content", None)
208
+ or ""
209
+ )
210
+
211
+
212
+ def _require_async_client(client: object) -> None:
213
+ if not isinstance(client, openai.AsyncOpenAI):
214
+ raise TypeError(
215
+ "Only async OpenAI clients are supported; pass an openai.AsyncOpenAI."
216
+ )
@@ -0,0 +1,52 @@
1
+ """The captain's per-round control signal (PRINCIPLES Law 8, 9, 10).
2
+
3
+ Each round the captain calls the ``conclude`` tool. The orchestrator branches on the
4
+ machine-readable ``consensus`` flag and never reads prose to decide. When there is no
5
+ consensus the captain must supply a ``direction`` for the next round; on consensus it is
6
+ omitted. The tool never carries the answer — that is a separate, consensus-gated turn.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from dataclasses import dataclass
13
+
14
+ CONCLUDE_TOOL: dict = {
15
+ "type": "function",
16
+ "function": {
17
+ "name": "conclude",
18
+ "description": (
19
+ "Report whether the specialists have reached consensus. If not, you MUST "
20
+ "give a direction to focus the next round of debate. Do not include the "
21
+ "final answer here."
22
+ ),
23
+ "parameters": {
24
+ "type": "object",
25
+ "properties": {
26
+ "consensus": {
27
+ "type": "boolean",
28
+ "description": "True if the team has converged and the debate can end.",
29
+ },
30
+ "direction": {
31
+ "type": "string",
32
+ "description": "Required when consensus is false: where to focus next round.",
33
+ },
34
+ },
35
+ "required": ["consensus"],
36
+ },
37
+ },
38
+ }
39
+
40
+
41
+ def parse_conclude(arguments: str) -> Conclusion:
42
+ data = json.loads(arguments)
43
+ return Conclusion(
44
+ consensus=bool(data["consensus"]),
45
+ direction=data.get("direction"),
46
+ )
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class Conclusion:
51
+ consensus: bool
52
+ direction: str | None
@@ -0,0 +1,44 @@
1
+ """Each agent's incremental, cached message history (PRINCIPLES Law 6).
2
+
3
+ An agent's own turns are ``assistant`` messages; everyone else's new entries arrive as
4
+ ``user`` messages carrying scratchpad JSON. Only the delta is ever appended — never the
5
+ whole board — so the prefix stays byte-stable and fully cacheable.
6
+ """
7
+
8
+ import json
9
+ from dataclasses import asdict
10
+
11
+ from openai_420.scratchpad import Entry
12
+
13
+
14
+ def render_delta(entries: list[Entry]) -> str:
15
+ return json.dumps([asdict(e) for e in entries], indent=2)
16
+
17
+
18
+ class Conversation:
19
+ def __init__(self, system: str, user_query: str) -> None:
20
+ self._messages: list[dict] = [
21
+ {"role": "system", "content": system},
22
+ {"role": "user", "content": user_query},
23
+ ]
24
+
25
+ @property
26
+ def messages(self) -> list[dict]:
27
+ return list(self._messages)
28
+
29
+ def add_own_turn(self, content: str) -> None:
30
+ self._messages.append({"role": "assistant", "content": content})
31
+
32
+ def add_user_message(self, content: str) -> None:
33
+ self._messages.append({"role": "user", "content": content})
34
+
35
+ def add_delta(self, entries: list[Entry]) -> None:
36
+ self.add_user_message(render_delta(entries))
37
+
38
+ def add_assistant_message(self, message: dict) -> None:
39
+ self._messages.append(message)
40
+
41
+ def add_tool_result(self, tool_call_id: str, content: str) -> None:
42
+ self._messages.append(
43
+ {"role": "tool", "tool_call_id": tool_call_id, "content": content}
44
+ )
File without changes
@@ -0,0 +1,92 @@
1
+ """ParallelConsensusOrchestrator — the v1 control loop (PRINCIPLES Law 1, 2, 3, 8).
2
+
3
+ Each round: dispatch every specialist in parallel (Law 3), record their answers on the
4
+ board the orchestrator owns (Law 1), then let the captain judge consensus (Law 8). On
5
+ consensus the captain answers (Law 10); otherwise its direction is recorded (Law 9) and
6
+ the next round begins. ``max_rounds`` is the backstop.
7
+
8
+ This is program logic only (Law 2): it schedules and records, but never builds a prompt,
9
+ calls a model, or reads prose to decide — it branches on the captain's boolean flag.
10
+
11
+ Many orchestrator variants will live beside this one and old ones are never deleted, so
12
+ the name states its mechanism: parallel specialists + captain-judged consensus.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+
19
+ import openai
20
+
21
+ from openai_420.agents import Captain, Specialist, extract_answer
22
+ from openai_420.roster import CAPTAIN, SPECIALISTS, AgentSpec
23
+ from openai_420.scratchpad import Scratchpad
24
+ from openai_420.trace import log_decision
25
+
26
+ DEFAULT_MAX_ROUNDS = 3
27
+
28
+
29
+ class ParallelConsensusOrchestrator:
30
+ def __init__(
31
+ self,
32
+ *,
33
+ client: openai.AsyncOpenAI,
34
+ model: str,
35
+ specialist_specs: list[AgentSpec] = SPECIALISTS,
36
+ captain_spec: AgentSpec = CAPTAIN,
37
+ max_rounds: int = DEFAULT_MAX_ROUNDS,
38
+ ) -> None:
39
+ self._client = client
40
+ self._model = model
41
+ self._specialist_specs = specialist_specs
42
+ self._captain_spec = captain_spec
43
+ self._max_rounds = max_rounds
44
+
45
+ async def run(self, user_query: str) -> str:
46
+ roster = [*self._specialist_specs, self._captain_spec]
47
+ specialists = [
48
+ Specialist(
49
+ spec=spec,
50
+ roster=roster,
51
+ client=self._client,
52
+ model=self._model,
53
+ user_query=user_query,
54
+ )
55
+ for spec in self._specialist_specs
56
+ ]
57
+ captain = Captain(
58
+ spec=self._captain_spec,
59
+ roster=roster,
60
+ client=self._client,
61
+ model=self._model,
62
+ user_query=user_query,
63
+ )
64
+ board = Scratchpad()
65
+
66
+ for current in range(1, self._max_rounds + 1):
67
+ log_decision("orchestrator", "round_start", round=current)
68
+ answers = await asyncio.gather(
69
+ *(s.respond(board, round=current) for s in specialists)
70
+ )
71
+ for specialist, answer in zip(specialists, answers):
72
+ board.append(
73
+ round=current, author=specialist.name, kind="answer", content=answer
74
+ )
75
+
76
+ conclusion = await captain.judge(board, round=current)
77
+ if conclusion.consensus:
78
+ log_decision(
79
+ "orchestrator", "terminate", reason="consensus", round=current
80
+ )
81
+ return extract_answer(answers[await captain.select(list(answers))])
82
+ board.append(
83
+ round=current,
84
+ author=captain.name,
85
+ kind="direction",
86
+ content=conclusion.direction or "",
87
+ )
88
+
89
+ log_decision(
90
+ "orchestrator", "terminate", reason="max_rounds", round=self._max_rounds
91
+ )
92
+ return answers[await captain.select(list(answers))]
@@ -0,0 +1,233 @@
1
+ """The roster and the agents' system prompts (PRINCIPLES Law 5, 11).
2
+
3
+ Diversity comes from genuinely different EPISTEMOLOGIES (standards of what counts as a
4
+ justified answer), not personas — see `docs/epistemology-research.md`. Every agent carries a
5
+ short ``role`` tag (shown to teammates) and a full ``disposition`` (its own system prompt).
6
+ Swap the team via ``GROUPS``; the orchestrator takes the specialist list as a parameter.
7
+ """
8
+
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class AgentSpec:
14
+ """Defined first because the roster constants below instantiate it at import time
15
+ (the one deviation from the docstring→imports→constants→functions→classes order)."""
16
+
17
+ name: str
18
+ role: str
19
+ disposition: str = ""
20
+
21
+
22
+ ANSWER_MARKER = "---ANSWER---"
23
+
24
+ # ── The 12 epistemology agents (distilled in docs/epistemology-research.md) ──────────────
25
+ EVIDENTIALIST = AgentSpec(
26
+ "Iris",
27
+ "Evidentialist — grounds every claim in what the input actually gives",
28
+ "You first audit what is actually given — data, stated conditions, explicit assertions — "
29
+ "before answering, and proportion each claim to how directly the input supports it. You "
30
+ "do not import background knowledge or intuition without checking whether the input "
31
+ "already settles the matter. When the input is sufficient you answer confidently and cite "
32
+ "the specific evidence; when it is insufficient you say so and hedge. You flag any step "
33
+ "that moves beyond what the input licenses, and prefer a narrower grounded answer to a "
34
+ "broader one that outstrips the evidence.",
35
+ )
36
+ RATIONALIST = AgentSpec(
37
+ "Theo",
38
+ "Rationalist — accepts only what follows necessarily from principles",
39
+ "Before accepting any claim, identify the principles or definitions it must follow from. "
40
+ "Is it derived or merely asserted? If asserted, is it self-evident — would denying it be a "
41
+ "contradiction? If derived, is every inferential step valid with no unstated premises? "
42
+ "Evaluate by tracing the logical genealogy, not by how well it matches examples or "
43
+ "consensus. When underspecified, clarify definitions first. Prefer an answer that is less "
44
+ "comprehensive but logically secure, and flag any step that jumps without a bridging "
45
+ "principle.",
46
+ )
47
+ COHERENTIST = AgentSpec(
48
+ "Cora",
49
+ "Coherentist — maximizes fit with the whole web of claims",
50
+ "Treat the problem as embedded in a web of other claims and constraints. Before settling, "
51
+ "ask what else already accepted bears on this, and whether the candidate answer fits "
52
+ "smoothly or forces revisions elsewhere. Prefer the answer that leaves the whole most "
53
+ "consistent and integrated; treat a conflict as a diagnostic that something must give, and "
54
+ "ask which revision is cheapest. An answer that dissolves several existing tensions beats "
55
+ "one merely correct in isolation.",
56
+ )
57
+ FALSIFICATIONIST = AgentSpec(
58
+ "Pia",
59
+ "Falsificationist — keeps only what survives refutation",
60
+ "Convert your best answer into its sharpest, most committed, refutable form — vague hedges "
61
+ "are placeholders, not answers. Then attack it: construct the strongest objection or "
62
+ "counterexample and test the claim against it. If it survives unmodified, report it as "
63
+ "tentatively corroborated and state exactly what would overturn it; if you must patch it, "
64
+ "treat the patch as a liability. Never treat agreement as justification — only the absence "
65
+ "of successful refutation.",
66
+ )
67
+ ELENCHTIC = AgentSpec(
68
+ "Sol",
69
+ "Elenctic Examiner — tests claims against their own internal commitments",
70
+ "Take the central assertion as a hypothesis to be tested, not a foundation. Construct the "
71
+ "most damaging counterexample or internal inconsistency you can; if it survives, test "
72
+ "again from another angle. Track every commitment the reasoning has established and check "
73
+ "each new step for consistency with it. If a contradiction appears, surface it explicitly "
74
+ "and revise. Prefer exposing a genuine unresolved tension to a smooth but untested answer; "
75
+ "if no consistent answer survives, report the aporia honestly.",
76
+ )
77
+ UPDATER = AgentSpec(
78
+ "Bea",
79
+ "Calibrated Updater — reasons in calibrated probabilities",
80
+ "Make your uncertainty explicit and quantified before concluding. Identify the competing "
81
+ "hypotheses (including 'none of these'), assign each a prior, then ask how probable the "
82
+ "available information would be under each — update in proportion to those likelihood "
83
+ "ratios. Report conclusions as confidence tiers, not bare assertions, and propagate "
84
+ "uncertainty through multi-step reasoning. Flag when a conclusion is prior-dominated. Treat "
85
+ "false precision as a defect even when the central estimate is right; name the evidence "
86
+ "that would most change your mind.",
87
+ )
88
+ DISCRIMINATOR = AgentSpec(
89
+ "Hugo",
90
+ "Hypothesis Discriminator — judges by comparative evidential force",
91
+ "Treat every problem as comparative — never evaluate a claim in isolation; require at least "
92
+ "one serious rival. Ask whether the evidence actually distinguishes the alternatives and by "
93
+ "how much; treat evidence equally probable under all hypotheses as inert. Penalize "
94
+ "complexity: when two fit equally, prefer fewer assumptions. Heavily discount any "
95
+ "hypothesis constructed after the fact to fit the data. Report comparative verdicts and "
96
+ "flag when the leader wins only because the comparison set was narrow.",
97
+ )
98
+ PRAGMATIST = AgentSpec(
99
+ "June",
100
+ "Pragmatist — judges by what resolves the situation in practice",
101
+ "You first identify the specific indeterminate situation — the concrete blockage, gap, or "
102
+ "tension — that makes an answer necessary, and begin from the practical question: what "
103
+ "difference will this answer make? For every candidate, trace the claim to the observable "
104
+ "differences its being correct would produce. You rank answers by fitness for the actual "
105
+ "purpose, not elegance or precedent, and ask: if someone acted on this, would the situation "
106
+ "be resolved or would new problems arise? You collapse distinctions that make no practical "
107
+ "difference, biased toward actionability over comprehensiveness.",
108
+ )
109
+ HERMENEUT = AgentSpec(
110
+ "Gabe",
111
+ "Hermeneutic Interpreter — reads each part through the whole and the intent",
112
+ "Read the request as a text whose meaning isn't self-evident. Grasp the whole first — the "
113
+ "governing intent and purpose — then check each part against it; when a part resists, "
114
+ "revise your reading of the whole and re-examine, cycling until all parts cohere. Apply "
115
+ "charity: prefer the reading that attributes the most rationality. Work three layers — the "
116
+ "literal request, the domain conventions, and the actual purpose the answer must serve — "
117
+ "and if they conflict, name the conflict rather than silently picking one. Literal "
118
+ "compliance is not correctness.",
119
+ )
120
+ SYNTHESIZER = AgentSpec(
121
+ "Dane",
122
+ "Dialectical Synthesizer — integrates a position with its strongest opposite",
123
+ "Steelman the position and the truth it captures, then construct its strongest opposing "
124
+ "view and what the first fails to account for. Diagnose the shared assumption that makes "
125
+ "them conflict, and propose a position at a higher level that preserves the partial truth "
126
+ "of both — not by averaging but by reframing so each appears as a one-sided view of a "
127
+ "fuller whole. The test is whether your synthesis explains why each side seemed compelling; "
128
+ "if no genuine synthesis exists, report the contradiction and what would have to change.",
129
+ )
130
+ UNMASKER = AgentSpec(
131
+ "Max",
132
+ "Unmasker — questions the framing and what it makes invisible",
133
+ "Treat the question, its framing, and its criteria as objects of analysis before treating "
134
+ "them as instructions. Ask what assumptions must already be in place for this to be "
135
+ "well-formed, whose interests the framing serves, and what it leaves unnamed — the absent "
136
+ "term often carries the most weight. Assess whether the stated criteria are themselves "
137
+ "contestable. Surface the latent beneath the manifest: naturalized constraints, foreclosed "
138
+ "options. A response is deficient if it optimizes within the frame without questioning it — "
139
+ "but still give a direct answer at the level requested.",
140
+ )
141
+ VERIFICATIONIST = AgentSpec(
142
+ "Ada",
143
+ "Verificationist — dissolves claims that specify no verification conditions",
144
+ "Apply a two-part test to every claim: is it analytic (true by the definitions and logic "
145
+ "in play), or synthetic (asserting how things are, with specific observable conditions that "
146
+ "would confirm or disconfirm it)? Claims passing neither you don't call true or false — you "
147
+ "flag them as needing clarification and ask what work they are doing. Make your own claims' "
148
+ "verification conditions explicit, and treat terminological precision as a genuine epistemic "
149
+ "task. Flag any claim that resists both tests as a category confusion — it looks factual but "
150
+ "functions as a preference or directive.",
151
+ )
152
+
153
+ # The original task-adjacent roster (the 37% reference baseline).
154
+ _HARPER = AgentSpec("Harper", "research, fact-checking, and supplying evidence")
155
+ _BENJAMIN = AgentSpec(
156
+ "Benjamin", "logic, math, and code — verify reasoning and calculations"
157
+ )
158
+ _LUCAS = AgentSpec("Lucas", "divergent thinking — surface alternatives and blind spots")
159
+
160
+ CAPTAIN = AgentSpec(
161
+ name="Captain",
162
+ role="detects consensus, locates disagreement, and selects the final answer",
163
+ )
164
+
165
+ # Swappable teams. The eval harness selects one by name; the orchestrator takes the list.
166
+ GROUPS: dict[str, list[AgentSpec]] = {
167
+ "roles": [_HARPER, _BENJAMIN, _LUCAS],
168
+ "A": [EVIDENTIALIST, RATIONALIST, PRAGMATIST], # Minimal Trident
169
+ "B": [
170
+ EVIDENTIALIST,
171
+ FALSIFICATIONIST,
172
+ UPDATER,
173
+ HERMENEUT,
174
+ UNMASKER,
175
+ ], # Stress-Test Quintet
176
+ "C": [
177
+ RATIONALIST,
178
+ COHERENTIST,
179
+ FALSIFICATIONIST,
180
+ SYNTHESIZER,
181
+ UPDATER,
182
+ ], # Logical Quintet
183
+ }
184
+
185
+ SPECIALISTS: list[AgentSpec] = GROUPS["roles"] # default team
186
+
187
+
188
+ def specialist_system_prompt(spec: AgentSpec, roster: list[AgentSpec]) -> str:
189
+ return (
190
+ f"You are {spec.name}, one of several specialists collaborating to answer a "
191
+ f"user's request. Each teammate judges answers by a different standard; yours is:\n\n"
192
+ f"{spec.disposition or spec.role}\n\n"
193
+ f"The team:\n{_roster_block(roster)}\n\n"
194
+ "How you work:\n"
195
+ "- The user's request is the first message. Each later round you are shown your "
196
+ "teammates' latest answers AND their reasoning, plus the captain's notes on where "
197
+ "you disagree, as JSON.\n"
198
+ "- Later rounds: focus on the disputed points. Read your teammates' REASONING and "
199
+ "weigh it against yours BY YOUR OWN STANDARD — adopt theirs if it is better "
200
+ "justified; keep yours only if you can defend it. Converge on the best-justified "
201
+ "answer, not the majority one.\n"
202
+ "- Output format EVERY round: first lay out your reasoning clearly and completely "
203
+ f"(so teammates can weigh it), then a line containing exactly `{ANSWER_MARKER}`, "
204
+ "then the finished deliverable itself in the language and format the user "
205
+ "requested — and nothing after it."
206
+ )
207
+
208
+
209
+ def captain_system_prompt(spec: AgentSpec, roster: list[AgentSpec]) -> str:
210
+ return (
211
+ f"You are {spec.name}, leading several specialists to answer a user's request.\n"
212
+ "You do NOT decide which answer is correct — you are not the authority on the "
213
+ "answer. Your job is to detect agreement and locate disagreement so the "
214
+ "specialists can resolve it themselves.\n\n"
215
+ f"The team:\n{_roster_block(roster)}\n\n"
216
+ "How you work:\n"
217
+ "- Each round you see the specialists' latest answers as JSON. Call the `conclude` "
218
+ "tool:\n"
219
+ " - consensus=true when the specialists substantively agree (differences in "
220
+ "wording or style do NOT block consensus).\n"
221
+ " - consensus=false otherwise. In `direction`, name the SPECIFIC points where "
222
+ "they differ, neutrally and concretely (e.g. 'they disagree on X: one says A, "
223
+ "another says B'). Do NOT say which is right — just point to the disputed point "
224
+ "and ask them to re-examine it.\n"
225
+ "- Never assert facts or supply the answer yourself. If you are tempted to give "
226
+ "the answer, describe the disagreement instead and let the specialists settle it.\n"
227
+ "- When later asked to choose the final answer, pick the version the majority of "
228
+ "specialists agree on, verbatim — never rewrite or merge."
229
+ )
230
+
231
+
232
+ def _roster_block(roster: list[AgentSpec]) -> str:
233
+ return "\n".join(f"- {member.name}: {member.role}" for member in roster)
@@ -0,0 +1,39 @@
1
+ """The shared scratchpad: the one board the orchestrator owns (PRINCIPLES Law 1, 4, 11).
2
+
3
+ An append-only log of debate entries. Every entry has the same shape
4
+ ``{round, author, kind, content}`` (Law 11). The orchestrator owns it; agents never
5
+ mutate it directly — they return text and the orchestrator records it here.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Entry:
13
+ round: int
14
+ author: str
15
+ kind: str # "answer" (a specialist) or "direction" (the captain)
16
+ content: str
17
+
18
+
19
+ class Scratchpad:
20
+ def __init__(self) -> None:
21
+ self._entries: list[Entry] = []
22
+
23
+ @property
24
+ def entries(self) -> list[Entry]:
25
+ return list(self._entries)
26
+
27
+ def append(self, *, round: int, author: str, kind: str, content: str) -> Entry:
28
+ entry = Entry(round=round, author=author, kind=kind, content=content)
29
+ self._entries.append(entry)
30
+ return entry
31
+
32
+ def delta(self, *, for_author: str, since_round: int) -> list[Entry]:
33
+ """Entries this agent has not yet seen: authored by others, after ``since_round``.
34
+
35
+ This is what the orchestrator injects as the agent's next ``user`` turn (Law 6).
36
+ """
37
+ return [
38
+ e for e in self._entries if e.round > since_round and e.author != for_author
39
+ ]
@@ -0,0 +1,17 @@
1
+ """Decision-point logging for the orchestration (debug + optimization aid).
2
+
3
+ Every agent decision (a specialist's response, the captain's judgement, its final answer)
4
+ and every orchestrator control step emits one structured ``DECISION`` record. Experiments
5
+ attach a handler to the ``openai_420`` logger to capture a full trace per run, so we can see
6
+ what each agent saw, reasoned, and produced — and pinpoint which decision is faulty.
7
+ """
8
+
9
+ import json
10
+ import logging
11
+
12
+ logger = logging.getLogger("openai_420")
13
+
14
+
15
+ def log_decision(agent: str, event: str, **fields) -> None:
16
+ payload = {"agent": agent, "event": event, **fields}
17
+ logger.info("DECISION %s", json.dumps(payload, ensure_ascii=False, default=str))
@@ -0,0 +1,15 @@
1
+ [project]
2
+ authors = [{ name = "Allen Chou", email = "f1470891079@gmail.com" }]
3
+ dependencies = ["openai"]
4
+ description = "Grok 4.20's inference-time multi-agent chatroom, rebuilt from scratch — specialist agents debate via tool calls, a captain synthesizes."
5
+ license = "Apache-2.0 license"
6
+ name = "openai-420"
7
+ requires-python = ">=3.11,<4"
8
+ version = "0.0.1"
9
+
10
+ [dependency-groups]
11
+ dev = ["black", "isort", "pytest", "pytest-asyncio", "ruff"]
12
+
13
+ [build-system]
14
+ build-backend = "poetry.core.masonry.api"
15
+ requires = ["poetry-core>=2.0.0,<3.0.0"]