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.
- openai_420-0.0.1/LICENSE +201 -0
- openai_420-0.0.1/PKG-INFO +16 -0
- openai_420-0.0.1/openai_420/__init__.py +1 -0
- openai_420-0.0.1/openai_420/agents.py +216 -0
- openai_420-0.0.1/openai_420/conclude.py +52 -0
- openai_420-0.0.1/openai_420/conversation.py +44 -0
- openai_420-0.0.1/openai_420/orchestrators/__init__.py +0 -0
- openai_420-0.0.1/openai_420/orchestrators/parallel_consensus.py +92 -0
- openai_420-0.0.1/openai_420/roster.py +233 -0
- openai_420-0.0.1/openai_420/scratchpad.py +39 -0
- openai_420-0.0.1/openai_420/trace.py +17 -0
- openai_420-0.0.1/pyproject.toml +15 -0
openai_420-0.0.1/LICENSE
ADDED
|
@@ -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"]
|