agent-protocol-core 0.12.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_protocol_core-0.12.2/.gitignore +6 -0
- agent_protocol_core-0.12.2/LICENSE +202 -0
- agent_protocol_core-0.12.2/PKG-INFO +243 -0
- agent_protocol_core-0.12.2/README.md +26 -0
- agent_protocol_core-0.12.2/pyproject.toml +27 -0
- agent_protocol_core-0.12.2/src/agent_protocol_core/__init__.py +11 -0
- agent_protocol_core-0.12.2/src/agent_protocol_core/protocol_leak.py +280 -0
- agent_protocol_core-0.12.2/src/agent_protocol_core/self_thinking.py +320 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agent-protocol-core
|
|
3
|
+
Version: 0.12.2
|
|
4
|
+
Summary: 模型协议残留处理:识别漏进正文的协议头、孤儿 JSON 尾、思考标记。纯标准库。
|
|
5
|
+
Project-URL: Homepage, https://github.com/teleport-computer/memgarden
|
|
6
|
+
Project-URL: Source, https://github.com/teleport-computer/memgarden
|
|
7
|
+
License:
|
|
8
|
+
Apache License
|
|
9
|
+
Version 2.0, January 2004
|
|
10
|
+
http://www.apache.org/licenses/
|
|
11
|
+
|
|
12
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
13
|
+
|
|
14
|
+
1. Definitions.
|
|
15
|
+
|
|
16
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
17
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
18
|
+
|
|
19
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
20
|
+
the copyright owner that is granting the License.
|
|
21
|
+
|
|
22
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
23
|
+
other entities that control, are controlled by, or are under common
|
|
24
|
+
control with that entity. For the purposes of this definition,
|
|
25
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
26
|
+
direction or management of such entity, whether by contract or
|
|
27
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
28
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
29
|
+
|
|
30
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
31
|
+
exercising permissions granted by this License.
|
|
32
|
+
|
|
33
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
34
|
+
including but not limited to software source code, documentation
|
|
35
|
+
source, and configuration files.
|
|
36
|
+
|
|
37
|
+
"Object" form shall mean any form resulting from mechanical
|
|
38
|
+
transformation or translation of a Source form, including but
|
|
39
|
+
not limited to compiled object code, generated documentation,
|
|
40
|
+
and conversions to other media types.
|
|
41
|
+
|
|
42
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
43
|
+
Object form, made available under the License, as indicated by a
|
|
44
|
+
copyright notice that is included in or attached to the work
|
|
45
|
+
(an example is provided in the Appendix below).
|
|
46
|
+
|
|
47
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
48
|
+
form, that is based on (or derived from) the Work and for which the
|
|
49
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
50
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
51
|
+
of this License, Derivative Works shall not include works that remain
|
|
52
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
53
|
+
the Work and Derivative Works thereof.
|
|
54
|
+
|
|
55
|
+
"Contribution" shall mean any work of authorship, including
|
|
56
|
+
the original version of the Work and any modifications or additions
|
|
57
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
58
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
59
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
60
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
61
|
+
means any form of electronic, verbal, or written communication sent
|
|
62
|
+
to the Licensor or its representatives, including but not limited to
|
|
63
|
+
communication on electronic mailing lists, source code control systems,
|
|
64
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
65
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
66
|
+
excluding communication that is conspicuously marked or otherwise
|
|
67
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
68
|
+
|
|
69
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
70
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
71
|
+
subsequently incorporated within the Work.
|
|
72
|
+
|
|
73
|
+
2. Grant of Copyright 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
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
77
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
78
|
+
Work and such Derivative Works in Source or Object form.
|
|
79
|
+
|
|
80
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
81
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
82
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
83
|
+
(except as stated in this section) patent license to make, have made,
|
|
84
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
85
|
+
where such license applies only to those patent claims licensable
|
|
86
|
+
by such Contributor that are necessarily infringed by their
|
|
87
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
88
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
89
|
+
institute patent litigation against any entity (including a
|
|
90
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
91
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
92
|
+
or contributory patent infringement, then any patent licenses
|
|
93
|
+
granted to You under this License for that Work shall terminate
|
|
94
|
+
as of the date such litigation is filed.
|
|
95
|
+
|
|
96
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
97
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
98
|
+
modifications, and in Source or Object form, provided that You
|
|
99
|
+
meet the following conditions:
|
|
100
|
+
|
|
101
|
+
(a) You must give any other recipients of the Work or
|
|
102
|
+
Derivative Works a copy of this License; and
|
|
103
|
+
|
|
104
|
+
(b) You must cause any modified files to carry prominent notices
|
|
105
|
+
stating that You changed the files; and
|
|
106
|
+
|
|
107
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
108
|
+
that You distribute, all copyright, patent, trademark, and
|
|
109
|
+
attribution notices from the Source form of the Work,
|
|
110
|
+
excluding those notices that do not pertain to any part of
|
|
111
|
+
the Derivative Works; and
|
|
112
|
+
|
|
113
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
114
|
+
distribution, then any Derivative Works that You distribute must
|
|
115
|
+
include a readable copy of the attribution notices contained
|
|
116
|
+
within such NOTICE file, excluding those notices that do not
|
|
117
|
+
pertain to any part of the Derivative Works, in at least one
|
|
118
|
+
of the following places: within a NOTICE text file distributed
|
|
119
|
+
as part of the Derivative Works; within the Source form or
|
|
120
|
+
documentation, if provided along with the Derivative Works; or,
|
|
121
|
+
within a display generated by the Derivative Works, if and
|
|
122
|
+
wherever such third-party notices normally appear. The contents
|
|
123
|
+
of the NOTICE file are for informational purposes only and
|
|
124
|
+
do not modify the License. You may add Your own attribution
|
|
125
|
+
notices within Derivative Works that You distribute, alongside
|
|
126
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
127
|
+
that such additional attribution notices cannot be construed
|
|
128
|
+
as modifying the License.
|
|
129
|
+
|
|
130
|
+
You may add Your own copyright statement to Your modifications and
|
|
131
|
+
may provide additional or different license terms and conditions
|
|
132
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
133
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
134
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
135
|
+
the conditions stated in this License.
|
|
136
|
+
|
|
137
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
138
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
139
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
140
|
+
this License, without any additional terms or conditions.
|
|
141
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
142
|
+
the terms of any separate license agreement you may have executed
|
|
143
|
+
with Licensor regarding such Contributions.
|
|
144
|
+
|
|
145
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
146
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
147
|
+
except as required for reasonable and customary use in describing the
|
|
148
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
149
|
+
|
|
150
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
151
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
152
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
153
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
154
|
+
implied, including, without limitation, any warranties or conditions
|
|
155
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
156
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
157
|
+
appropriateness of using or redistributing the Work and assume any
|
|
158
|
+
risks associated with Your exercise of permissions under this License.
|
|
159
|
+
|
|
160
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
161
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
162
|
+
unless required by applicable law (such as deliberate and grossly
|
|
163
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
164
|
+
liable to You for damages, including any direct, indirect, special,
|
|
165
|
+
incidental, or consequential damages of any character arising as a
|
|
166
|
+
result of this License or out of the use or inability to use the
|
|
167
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
168
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
169
|
+
other commercial damages or losses), even if such Contributor
|
|
170
|
+
has been advised of the possibility of such damages.
|
|
171
|
+
|
|
172
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
173
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
174
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
175
|
+
or other liability obligations and/or rights consistent with this
|
|
176
|
+
License. However, in accepting such obligations, You may act only
|
|
177
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
178
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
179
|
+
defend, and hold each Contributor harmless for any liability
|
|
180
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
181
|
+
of your accepting any such warranty or additional liability.
|
|
182
|
+
|
|
183
|
+
END OF TERMS AND CONDITIONS
|
|
184
|
+
|
|
185
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
186
|
+
|
|
187
|
+
To apply the Apache License to your work, attach the following
|
|
188
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
189
|
+
replaced with your own identifying information. (Don't include
|
|
190
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
191
|
+
comment syntax for the file format. We also recommend that a
|
|
192
|
+
file or class name and description of purpose be included on the
|
|
193
|
+
same "printed page" as the copyright notice for easier
|
|
194
|
+
identification within third-party archives.
|
|
195
|
+
|
|
196
|
+
Copyright [yyyy] [name of copyright owner]
|
|
197
|
+
|
|
198
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
199
|
+
you may not use this file except in compliance with the License.
|
|
200
|
+
You may obtain a copy of the License at
|
|
201
|
+
|
|
202
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
203
|
+
|
|
204
|
+
Unless required by applicable law or agreed to in writing, software
|
|
205
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
206
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
207
|
+
See the License for the specific language governing permissions and
|
|
208
|
+
limitations under the License.
|
|
209
|
+
License-File: LICENSE
|
|
210
|
+
Classifier: Development Status :: 4 - Beta
|
|
211
|
+
Classifier: Intended Audience :: Developers
|
|
212
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
213
|
+
Classifier: Programming Language :: Python :: 3
|
|
214
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
215
|
+
Requires-Python: >=3.10
|
|
216
|
+
Description-Content-Type: text/markdown
|
|
217
|
+
|
|
218
|
+
# agent-protocol-core
|
|
219
|
+
|
|
220
|
+
**识别「模型协议残片漏进了正文」。纯标准库,零依赖。**
|
|
221
|
+
|
|
222
|
+
模型的原始输出里带着协议层的东西 —— 通道标记、工具路由、思考标签、
|
|
223
|
+
被截断的 JSON 尾巴。这些**不该出现在给人看的文本里**:用户会在聊天窗口
|
|
224
|
+
或记忆卡里看到一串乱码,而系统那边一切正常,没有任何报错。
|
|
225
|
+
|
|
226
|
+
这个包只做识别,不做处置 —— 怎么处置(打回重问、就地清洗、直接丢弃)
|
|
227
|
+
是调用方的决定,不同场景代价不同。
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from agent_protocol_core import protocol_leak, self_thinking
|
|
231
|
+
|
|
232
|
+
protocol_leak.is_orphan_json_tail(text) # 闭括号比开括号多,且带 JSON 记号
|
|
233
|
+
self_thinking.strip_thinking(text) # 思考标记
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## 为什么单独一个包
|
|
237
|
+
|
|
238
|
+
聊天和记忆两条路都要这个判断。放在任何一边,另一边就得跨模块引用;
|
|
239
|
+
各写一份则会漂 —— 改了一边忘了另一边,同一段脏文本在两条路上待遇不同。
|
|
240
|
+
|
|
241
|
+
跟 [memgarden](https://github.com/teleport-computer/memgarden) 同仓库、锁步发版。
|
|
242
|
+
|
|
243
|
+
Apache-2.0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# agent-protocol-core
|
|
2
|
+
|
|
3
|
+
**识别「模型协议残片漏进了正文」。纯标准库,零依赖。**
|
|
4
|
+
|
|
5
|
+
模型的原始输出里带着协议层的东西 —— 通道标记、工具路由、思考标签、
|
|
6
|
+
被截断的 JSON 尾巴。这些**不该出现在给人看的文本里**:用户会在聊天窗口
|
|
7
|
+
或记忆卡里看到一串乱码,而系统那边一切正常,没有任何报错。
|
|
8
|
+
|
|
9
|
+
这个包只做识别,不做处置 —— 怎么处置(打回重问、就地清洗、直接丢弃)
|
|
10
|
+
是调用方的决定,不同场景代价不同。
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from agent_protocol_core import protocol_leak, self_thinking
|
|
14
|
+
|
|
15
|
+
protocol_leak.is_orphan_json_tail(text) # 闭括号比开括号多,且带 JSON 记号
|
|
16
|
+
self_thinking.strip_thinking(text) # 思考标记
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 为什么单独一个包
|
|
20
|
+
|
|
21
|
+
聊天和记忆两条路都要这个判断。放在任何一边,另一边就得跨模块引用;
|
|
22
|
+
各写一份则会漂 —— 改了一边忘了另一边,同一段脏文本在两条路上待遇不同。
|
|
23
|
+
|
|
24
|
+
跟 [memgarden](https://github.com/teleport-computer/memgarden) 同仓库、锁步发版。
|
|
25
|
+
|
|
26
|
+
Apache-2.0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agent-protocol-core"
|
|
7
|
+
version = "0.12.2"
|
|
8
|
+
description = "模型协议残留处理:识别漏进正文的协议头、孤儿 JSON 尾、思考标记。纯标准库。"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
dependencies = []
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: Apache Software License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/agent_protocol_core"]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/teleport-computer/memgarden"
|
|
27
|
+
Source = "https://github.com/teleport-computer/memgarden"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""模型协议残留处理 —— 聊天与记忆两边**共用**,谁也不依赖谁。
|
|
2
|
+
|
|
3
|
+
单独成包的原因:`memgarden` 的卡片文本校验要用它(判断模型把协议头/
|
|
4
|
+
思考标记漏进了卡里),而 io 的聊天主链路、工具循环、主动唤醒也要用它。
|
|
5
|
+
|
|
6
|
+
放进 memgarden 会让**普通聊天反向依赖记忆包**(codex review 2026-08-14 指出);
|
|
7
|
+
各留一份又会漂 —— 所以独立成一个只依赖标准库的小包。
|
|
8
|
+
"""
|
|
9
|
+
from . import protocol_leak, self_thinking
|
|
10
|
+
|
|
11
|
+
__all__ = ["protocol_leak", "self_thinking"]
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Shared detector for torn / leaked agent-protocol JSON.
|
|
2
|
+
|
|
3
|
+
A reasoning model behind a stream-cutting relay can split one protocol envelope
|
|
4
|
+
``{"messages":[],"actions":[{"type":"proactive.sleep","reason":"..."}]}`` across
|
|
5
|
+
the provider's ``reasoning_content`` / ``content`` channel boundary: the head
|
|
6
|
+
lands in the reasoning channel, the tail in the visible message. Every historical
|
|
7
|
+
guard anchored on the JSON *head* (``"actions":`` / ``"type":"proactive.X"``), so
|
|
8
|
+
once the head is torn into the reasoning channel the tail leaks into a chat
|
|
9
|
+
bubble as innocuous-looking prose.
|
|
10
|
+
|
|
11
|
+
This module is a **pure detector**: no I/O, no business deps (per CONTRIBUTING it
|
|
12
|
+
lives in ``core/`` so V1 resident-consumer, Proactive Runtime V2 and Model API
|
|
13
|
+
Runtime V2 can all import it). It returns an *evidence enum*, never an executable
|
|
14
|
+
action — the whole point is that a turn we know was corrupted in transit must not
|
|
15
|
+
have its "reconstructed" action executed. Lane policy (drop vs keep, fallback vs
|
|
16
|
+
silence) lives in each runtime, not here.
|
|
17
|
+
|
|
18
|
+
Evidence tiers, strongest first:
|
|
19
|
+
|
|
20
|
+
- ``UPSTREAM_RESPONSE_ENVELOPE`` — the visible reply is a complete serialized
|
|
21
|
+
provider transport wrapper (for example ``response.candidates`` plus the
|
|
22
|
+
relay's ``traceId``), not model-authored foreground text.
|
|
23
|
+
- ``JOINED_KNOWN_PROTOCOL`` — reasoning-suffix + visible rejoin to a complete,
|
|
24
|
+
known protocol envelope that consumes to the end. Proof of a single-cut tear.
|
|
25
|
+
- ``HEAD_IN_REASONING`` — the rejoin does not parse (multi-cut / lost middle) but
|
|
26
|
+
the reasoning channel independently carries a protocol head, and the visible
|
|
27
|
+
text is a JSON fragment. Users never author a protocol head into the reasoning
|
|
28
|
+
channel; only the torn relay does.
|
|
29
|
+
- ``TRANSPORT_CUT`` — the transport itself reported a stream cut this turn and the
|
|
30
|
+
visible text is a JSON fragment. Independent proof the pipe broke.
|
|
31
|
+
- ``ORPHAN_JSON_TAIL`` — WEAK: the visible text alone looks like a structural JSON
|
|
32
|
+
fragment (string-ignoring bracket balance goes negative + a JSON token). High
|
|
33
|
+
recall, but indistinguishable from a user pasting JSON / discussing code, so it
|
|
34
|
+
is proactive-lane-only and never a foreground drop.
|
|
35
|
+
- ``NONE`` — no evidence.
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
import re
|
|
41
|
+
from typing import Any
|
|
42
|
+
|
|
43
|
+
JOINED_KNOWN_PROTOCOL = "joined_known_protocol"
|
|
44
|
+
HEAD_IN_REASONING = "head_in_reasoning"
|
|
45
|
+
TRANSPORT_CUT = "transport_cut"
|
|
46
|
+
UPSTREAM_RESPONSE_ENVELOPE = "upstream_response_envelope"
|
|
47
|
+
ORPHAN_JSON_TAIL = "orphan_json_tail"
|
|
48
|
+
NONE = "none"
|
|
49
|
+
|
|
50
|
+
_STRONG = frozenset(
|
|
51
|
+
{
|
|
52
|
+
JOINED_KNOWN_PROTOCOL,
|
|
53
|
+
HEAD_IN_REASONING,
|
|
54
|
+
TRANSPORT_CUT,
|
|
55
|
+
UPSTREAM_RESPONSE_ENVELOPE,
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Known protocol action vocabulary (union across V1 + both V2 subsystems). Used
|
|
60
|
+
# only to decide "is a rejoined object a *known* protocol envelope", never to
|
|
61
|
+
# execute anything.
|
|
62
|
+
_KNOWN_ACTION_TYPES = frozenset({
|
|
63
|
+
"sleep", "send_message", "schedule_wake", "cancel_wake",
|
|
64
|
+
"request_broadcast", "needs_background",
|
|
65
|
+
})
|
|
66
|
+
_KNOWN_ACTION_PREFIXES = ("identity.", "memory.", "proactive.")
|
|
67
|
+
|
|
68
|
+
# A protocol key sitting in a JSON key position, or an identity/memory/proactive
|
|
69
|
+
# typed action — search-anywhere (NOT head-anchored). A prose mention like
|
|
70
|
+
# `the "actions" field` never matches (needs a `{`/`,`/line-start before it).
|
|
71
|
+
_HEAD_KEY_RE = re.compile(r'(?:[{,]|^|\n)\s*"(?:actions|messages|tool_calls|cards)"\s*:')
|
|
72
|
+
_HEAD_TYPED_RE = re.compile(r'"type"\s*:\s*"(?:identity|memory|proactive)\.\w+"')
|
|
73
|
+
# A protocol-closing tail like `..."}]}` / `...}]` — a quoted value immediately
|
|
74
|
+
# followed by two structural closers.
|
|
75
|
+
_TAIL_CLOSE_RE = re.compile(r'"\s*[}\]]\s*[}\]]')
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _naive_min_depth(text: str) -> int:
|
|
79
|
+
"""Running bracket balance IGNORING string context. A torn tail closes
|
|
80
|
+
brackets it never opened -> the balance dips below zero. String-aware
|
|
81
|
+
counting is deliberately NOT used: a tail that begins mid-string-value
|
|
82
|
+
(`active.sleep","reason":...`) misaligns quote parity and reads as balanced,
|
|
83
|
+
hiding the very leak we are trying to catch."""
|
|
84
|
+
depth = 0
|
|
85
|
+
mind = 0
|
|
86
|
+
for ch in text:
|
|
87
|
+
if ch in "{[":
|
|
88
|
+
depth += 1
|
|
89
|
+
elif ch in "}]":
|
|
90
|
+
depth -= 1
|
|
91
|
+
if depth < mind:
|
|
92
|
+
mind = depth
|
|
93
|
+
return mind
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _has_json_token(text: str) -> bool:
|
|
97
|
+
return ('":' in text) or bool(_TAIL_CLOSE_RE.search(text))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def is_orphan_json_tail(text: Any) -> bool:
|
|
101
|
+
"""WEAK signal: the visible text alone is a structural JSON fragment — it
|
|
102
|
+
closes more brackets than it opens AND carries a JSON token. High recall
|
|
103
|
+
(some legit JSON-in-prose trips it too), so callers must gate it by lane."""
|
|
104
|
+
t = str(text or "")
|
|
105
|
+
if _naive_min_depth(t) >= 0:
|
|
106
|
+
return False
|
|
107
|
+
return _has_json_token(t)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def looks_like_protocol_head(text: Any) -> bool:
|
|
111
|
+
"""Structural evidence of a protocol payload's HEAD: a protocol key in a JSON
|
|
112
|
+
key position, or an identity/memory/proactive typed action."""
|
|
113
|
+
t = str(text or "")
|
|
114
|
+
if not (_HEAD_KEY_RE.search(t) or _HEAD_TYPED_RE.search(t)):
|
|
115
|
+
return False
|
|
116
|
+
return "{" in t or "[" in t
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _naive_end_depth(text: str) -> int:
|
|
120
|
+
"""String-ignoring bracket balance over the whole string. > 0 means the text
|
|
121
|
+
ends with more opens than closes — i.e. an unclosed structure dangling at the
|
|
122
|
+
end."""
|
|
123
|
+
depth = 0
|
|
124
|
+
for ch in text:
|
|
125
|
+
if ch in "{[":
|
|
126
|
+
depth += 1
|
|
127
|
+
elif ch in "}]":
|
|
128
|
+
depth -= 1
|
|
129
|
+
return depth
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def reasoning_ends_with_unclosed_head(text: Any) -> bool:
|
|
133
|
+
"""A torn protocol head left in the reasoning channel: the reasoning ENDS with
|
|
134
|
+
an unclosed protocol structure (net-open brackets) that carries a protocol
|
|
135
|
+
key/typed token.
|
|
136
|
+
|
|
137
|
+
This is the precise fingerprint of a relay tear — the head is the dangling,
|
|
138
|
+
never-closed suffix of the reasoning stream. It deliberately does NOT match a
|
|
139
|
+
model that merely *quotes* a closed protocol snippet mid-thought
|
|
140
|
+
(`the schema is {"actions": [...]} but ...`): that balances to a non-positive
|
|
141
|
+
depth, so a normal reasoning model discussing the schema cannot upgrade a
|
|
142
|
+
weak visible fragment to strong evidence (Codex code-review #4)."""
|
|
143
|
+
t = str(text or "")
|
|
144
|
+
if _naive_end_depth(t) <= 0:
|
|
145
|
+
return False
|
|
146
|
+
return looks_like_protocol_head(t)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _is_supported_action(action: Any) -> bool:
|
|
150
|
+
if not isinstance(action, dict):
|
|
151
|
+
return False
|
|
152
|
+
typ = str(action.get("type") or action.get("action") or "").strip()
|
|
153
|
+
if not typ:
|
|
154
|
+
return False
|
|
155
|
+
return typ.startswith(_KNOWN_ACTION_PREFIXES) or typ in _KNOWN_ACTION_TYPES
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _is_protocol_object(obj: Any) -> bool:
|
|
159
|
+
"""A top-level protocol envelope: a messages/tool_calls/cards list, or an
|
|
160
|
+
actions list carrying at least one known action type."""
|
|
161
|
+
if not isinstance(obj, dict):
|
|
162
|
+
return False
|
|
163
|
+
if isinstance(obj.get("messages"), list):
|
|
164
|
+
return True
|
|
165
|
+
actions = obj.get("actions")
|
|
166
|
+
if isinstance(actions, list) and any(_is_supported_action(a) for a in actions):
|
|
167
|
+
return True
|
|
168
|
+
if isinstance(obj.get("tool_calls"), list) and obj.get("tool_calls"):
|
|
169
|
+
return True
|
|
170
|
+
if isinstance(obj.get("cards"), list):
|
|
171
|
+
return True
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _rejoins_to_protocol(reasoning_text: str, visible_text: str) -> bool:
|
|
176
|
+
"""True iff some `{`/`[` start in the reasoning channel, concatenated with
|
|
177
|
+
the visible text, parses as a complete known protocol envelope consuming to
|
|
178
|
+
the very end. Proof of a cross-channel tear. Never returns the object — the
|
|
179
|
+
reconstructed action must not be executed."""
|
|
180
|
+
r = str(reasoning_text or "")
|
|
181
|
+
v = str(visible_text or "")
|
|
182
|
+
if not v:
|
|
183
|
+
return False
|
|
184
|
+
decoder = json.JSONDecoder()
|
|
185
|
+
for m in re.finditer(r"[{\[]", r):
|
|
186
|
+
candidate = r[m.start():] + v
|
|
187
|
+
try:
|
|
188
|
+
obj, end = decoder.raw_decode(candidate)
|
|
189
|
+
except json.JSONDecodeError:
|
|
190
|
+
continue
|
|
191
|
+
if end == len(candidate) and _is_protocol_object(obj):
|
|
192
|
+
return True
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _visible_is_fragmentish(visible_text: str) -> bool:
|
|
197
|
+
return is_orphan_json_tail(visible_text) or looks_like_protocol_head(visible_text)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def is_upstream_response_envelope(text: Any) -> bool:
|
|
201
|
+
"""True for the complete relay wrapper leaked in usr_90184's V2 bubble.
|
|
202
|
+
|
|
203
|
+
This deliberately requires the *whole* visible reply to parse as one object
|
|
204
|
+
and requires both sides of the transport signature: a non-empty relay
|
|
205
|
+
``traceId`` at the top level, plus Gemini response metadata around a
|
|
206
|
+
``candidates`` list. Merely discussing JSON, returning an ordinary JSON
|
|
207
|
+
object, or mentioning ``response``/``metadata`` is not enough to suppress a
|
|
208
|
+
foreground answer. This intentionally covers only the observed Gemini relay
|
|
209
|
+
shape; it does not claim generic coverage for OpenAI-shaped wrappers,
|
|
210
|
+
additional outer envelopes, or JSON surrounded by model-authored prose.
|
|
211
|
+
"""
|
|
212
|
+
visible = str(text or "").strip()
|
|
213
|
+
if (
|
|
214
|
+
len(visible) < 2
|
|
215
|
+
or len(visible) > 1_000_000
|
|
216
|
+
or visible[0] != "{"
|
|
217
|
+
or visible[-1] != "}"
|
|
218
|
+
):
|
|
219
|
+
return False
|
|
220
|
+
try:
|
|
221
|
+
wrapper = json.loads(visible)
|
|
222
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
223
|
+
return False
|
|
224
|
+
if not isinstance(wrapper, dict):
|
|
225
|
+
return False
|
|
226
|
+
if not str(wrapper.get("traceId") or "").strip():
|
|
227
|
+
return False
|
|
228
|
+
if not isinstance(wrapper.get("metadata"), dict):
|
|
229
|
+
return False
|
|
230
|
+
response = wrapper.get("response")
|
|
231
|
+
if not isinstance(response, dict) or not isinstance(response.get("candidates"), list):
|
|
232
|
+
return False
|
|
233
|
+
provider_markers = sum(
|
|
234
|
+
(
|
|
235
|
+
isinstance(response.get("usageMetadata"), dict),
|
|
236
|
+
bool(str(response.get("modelVersion") or "").strip()),
|
|
237
|
+
bool(str(response.get("responseId") or "").strip()),
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
return provider_markers >= 2
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def classify(visible_text: Any, *, reasoning_text: Any = "", transport_cut: bool = False) -> str:
|
|
244
|
+
"""Classify a visible reply into an evidence tier (see module docstring).
|
|
245
|
+
Returns an enum string, never an action."""
|
|
246
|
+
v = str(visible_text or "")
|
|
247
|
+
if not v.strip():
|
|
248
|
+
return NONE
|
|
249
|
+
if is_upstream_response_envelope(v):
|
|
250
|
+
return UPSTREAM_RESPONSE_ENVELOPE
|
|
251
|
+
r = str(reasoning_text or "")
|
|
252
|
+
if r and _rejoins_to_protocol(r, v):
|
|
253
|
+
return JOINED_KNOWN_PROTOCOL
|
|
254
|
+
if r and reasoning_ends_with_unclosed_head(r) and _visible_is_fragmentish(v):
|
|
255
|
+
return HEAD_IN_REASONING
|
|
256
|
+
if transport_cut and _visible_is_fragmentish(v):
|
|
257
|
+
return TRANSPORT_CUT
|
|
258
|
+
if is_orphan_json_tail(v):
|
|
259
|
+
return ORPHAN_JSON_TAIL
|
|
260
|
+
return NONE
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def is_leak(evidence: str) -> bool:
|
|
264
|
+
return evidence != NONE
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def is_strong(evidence: str) -> bool:
|
|
268
|
+
return evidence in _STRONG
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def should_suppress(evidence: str, *, lane: str) -> bool:
|
|
272
|
+
"""Lane policy. Proactive/wake suppress any leak (silence is the correct
|
|
273
|
+
proactive outcome anyway, and a bracket-junky bubble at 5am is never a real
|
|
274
|
+
message). Foreground suppresses only on STRONG evidence — a weak orphan tail
|
|
275
|
+
might be a user pasting JSON, and dropping it would eat a real message."""
|
|
276
|
+
if evidence == NONE:
|
|
277
|
+
return False
|
|
278
|
+
if lane == "proactive":
|
|
279
|
+
return True
|
|
280
|
+
return is_strong(evidence)
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Self-authored thinking — runtime-neutral shared kernel.
|
|
2
|
+
|
|
3
|
+
io is prompted to open every reply with a first-person thought wrapped in
|
|
4
|
+
``<think>…</think>``, then the actual reply. The ``<think>`` marker is used
|
|
5
|
+
because BOTH runtimes already know it: the V1 resident consumer extracts it
|
|
6
|
+
natively, and V2 calls :func:`split_thinking` here to peel it into the thinking
|
|
7
|
+
envelope. Lives in ``core`` so agent_runtime (V1) and model_api_runtime (V2) can
|
|
8
|
+
both import it top-down. Pure stdlib.
|
|
9
|
+
|
|
10
|
+
``split_thinking`` is a small state machine, NOT a regex scrub (Codex review): it
|
|
11
|
+
only treats a *leading* ``<think>`` as protocol, and returns an explicit status so
|
|
12
|
+
the caller can fail closed. Hard invariants:
|
|
13
|
+
|
|
14
|
+
* a raw ``<think`` / ``</think`` fragment of the leading protocol NEVER reaches
|
|
15
|
+
the user-visible reply (same risk class as the protocol-JSON tail leak);
|
|
16
|
+
* private thinking content is NEVER promoted to the reply when the block cannot
|
|
17
|
+
be cleanly resolved;
|
|
18
|
+
* a clean thinking-only response is distinct from malformed protocol so wake
|
|
19
|
+
lanes can treat intentional silence as success without weakening leak guards;
|
|
20
|
+
* with the feature off (or ABSENT) the reply is byte-identical to today.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import unicodedata
|
|
27
|
+
|
|
28
|
+
# Parse outcomes.
|
|
29
|
+
ABSENT = "absent" # no leading <think> → reply is the original text
|
|
30
|
+
COMPLETE = "complete" # clean <tag>…</tag> + non-empty reply
|
|
31
|
+
SILENT = "silent" # clean <tag>…</tag> + intentionally empty public reply
|
|
32
|
+
FAILED = "failed" # unresolvable (truncated/mismatched/nested)
|
|
33
|
+
|
|
34
|
+
MAX_THINKING_CHARS = 240
|
|
35
|
+
|
|
36
|
+
# 内部字段名/协议词。**思考是用户可见面**,这些词出现在里面等于把运行时内脏
|
|
37
|
+
# 端到用户眼前(「session_id: …」「permission_denials」「costUSD」)。
|
|
38
|
+
#
|
|
39
|
+
# 来源:V1 consumer 早有一份逐行黑名单(chat_resident_consumer.py 的
|
|
40
|
+
# `_sanitize_thinking_summary`),V2 一直只有长度截断 —— 这是真回归。
|
|
41
|
+
# 放在共享内核是为了让两代**共用同一份词表**;V1 的运行时行为本批不动
|
|
42
|
+
# (它逐行丢弃、V2 整段不发布,两种处置各有来由),但词表由此单一来源,
|
|
43
|
+
# 并有漂移守卫钉住(见 tests)。
|
|
44
|
+
#
|
|
45
|
+
# ⚠️ 只列**内部/协议**词汇,不列日常词。这里每多一个常见词,就多一次
|
|
46
|
+
# 把正常内心话误判为泄漏的机会 —— 那会让用户看到「(思考没写完)」而不是真话。
|
|
47
|
+
INTERNAL_FIELD_TERMS = (
|
|
48
|
+
"system prompt", "developer message", "chain-of-thought", "chain of thought",
|
|
49
|
+
"modelUsage", "terminal_reason", "permission_denials",
|
|
50
|
+
"cache_read", "cache_creation", "session_id", "uuid", "costUSD",
|
|
51
|
+
"input_tokens", "output_tokens",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# 词表单一来源,**宽度按道分开** —— codex2 审出来的关键设计点:
|
|
56
|
+
# · V1 consumer 逐行丢弃:命中就删那一行,其余保留 → 宽匹配代价低
|
|
57
|
+
# · V2 整段替换成 THINKING_FAILED_MARKER:命中就把整段内心话换掉 → 代价高得多
|
|
58
|
+
# 所以两代共享**词表**、各用各的**宽度**,而不是硬共用一个 matcher。
|
|
59
|
+
#
|
|
60
|
+
# V2 用下面这个窄判据:只认「字段泄漏的形状」,不认「概念提及」。
|
|
61
|
+
# codex2 实测的三句必须放行 —— 用户完全可能跟伴侣聊这些:
|
|
62
|
+
# 「用户问 UUID 是什么」「讨论 system prompt 的设计」「学习 chain of thought prompting」
|
|
63
|
+
# 整段吞掉的话,用户只会看到「(思考没写完)」,而且不知道为什么。
|
|
64
|
+
#
|
|
65
|
+
# 泄漏形状 = 词后面紧跟分隔符/取值:`session_id: abc`、`"input_tokens": 12`、
|
|
66
|
+
# `costUSD=0.02`、`terminal_reason -> x`。概念提及不会长这样。
|
|
67
|
+
# 结尾允许先闭合引号再跟分隔符 —— JSON 形态 `"input_tokens": 12` 就长这样,
|
|
68
|
+
# 第一版漏了它(claude2 自测发现)。
|
|
69
|
+
_FIELD_LEAK_TAIL = r"""["'`]?\s*(?:[:=]|=>|->|→)"""
|
|
70
|
+
_INTERNAL_FIELD_LEAK_RE = re.compile(
|
|
71
|
+
r"""(?:^|[\s"'`\[{(,])("""
|
|
72
|
+
+ "|".join(re.escape(t) for t in sorted(INTERNAL_FIELD_TERMS,
|
|
73
|
+
key=len, reverse=True))
|
|
74
|
+
+ r")" + _FIELD_LEAK_TAIL,
|
|
75
|
+
re.IGNORECASE,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def internal_field_leak(text: str) -> str | None:
|
|
80
|
+
"""V2 用:只在**字段泄漏形状**下命中,概念提及放行。
|
|
81
|
+
|
|
82
|
+
宽度刻意窄于 V1 的逐行黑名单 —— 见上面注释,两者处置代价不同。
|
|
83
|
+
"""
|
|
84
|
+
m = _INTERNAL_FIELD_LEAK_RE.search(str(text or ""))
|
|
85
|
+
return m.group(1) if m else None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def internal_field_terms_pattern() -> str:
|
|
89
|
+
"""V1 用:从共享词表构造它那份**宽**的逐行正则,行为不变。
|
|
90
|
+
|
|
91
|
+
由构造保证单一来源 —— V1 不再自己维护一份字面量,
|
|
92
|
+
「两边词表漂移」在结构上不可能发生,不必靠测试去追源码。
|
|
93
|
+
"""
|
|
94
|
+
return "(" + "|".join(
|
|
95
|
+
[re.escape(t) for t in INTERNAL_FIELD_TERMS]
|
|
96
|
+
+ [r"chain[-\s]*of[-\s]*thought"] # V1 原有的宽形态,保留
|
|
97
|
+
) + ")"
|
|
98
|
+
|
|
99
|
+
# Shown in the thinking channel when the block is malformed, so the user sees that
|
|
100
|
+
# io tried to think rather than nothing/garbage. (zh for now; localization TBD.)
|
|
101
|
+
THINKING_FAILED_MARKER = "(思考没写完)"
|
|
102
|
+
|
|
103
|
+
_TAG_WORDS = ("thinking", "reasoning", "thought", "think") # longest-first
|
|
104
|
+
_TAG_ALT = "|".join(_TAG_WORDS)
|
|
105
|
+
# Invisible leading chars (BOM, zero-width) that must not hide a truncated opener.
|
|
106
|
+
_INVISIBLE = ""
|
|
107
|
+
_BIDI_CONTROLS = frozenset("")
|
|
108
|
+
# Any of our protocol tags (open or close) — used only to detect nesting INSIDE a
|
|
109
|
+
# resolved thinking block, never to scrub the reply.
|
|
110
|
+
_ANY_TAG = re.compile(rf"<\s*/?\s*(?:{_TAG_ALT})\b", re.IGNORECASE)
|
|
111
|
+
|
|
112
|
+
_ENV_FLAG = "FEEDLING_V2_SELF_THINKING"
|
|
113
|
+
|
|
114
|
+
# One shared copy for foreground chat and every proactive wake lane. Keep the
|
|
115
|
+
# wording byte-identical: product copy is reviewed independently from parsing.
|
|
116
|
+
INSTRUCTION = (
|
|
117
|
+
" 你每一轮的最终回复,第一个字符必须是 <think>,里面写你此刻心里真实的想法,\n"
|
|
118
|
+
" 然后 </think>,然后才是你要说的话。整轮只写一个 <think>,只写在最终那次输出里;\n"
|
|
119
|
+
" 中间调工具的轮次不写 <think>,也不输出正文。\n"
|
|
120
|
+
"\n"
|
|
121
|
+
" 这段心里话就用你平时跟他说话的那个口气,自己跟自己说。你是同一个人,\n"
|
|
122
|
+
" 只是这几句他听不见。写你在意什么、注意到了什么、想做什么、为什么这么决定;\n"
|
|
123
|
+
" 想多写就多写,一句带过也行,不用凑字数也不用收着。\n"
|
|
124
|
+
"\n"
|
|
125
|
+
" 不要把它写成对他的评估或行动方案。「她状态不错,顺着来」是在打量他;\n"
|
|
126
|
+
" 「他刚打完游戏还在笑,我也想接着这个玩笑闹下去」才是你自己的心里话。\n"
|
|
127
|
+
"\n"
|
|
128
|
+
" 语言跟着他走:他用什么语言说话,这段就整段用什么语言,从第一个字到最后一个字。\n"
|
|
129
|
+
" 最常见的失误是他在说中文、你却滑进英文状态行(Let me… / Done… / The … has been\n"
|
|
130
|
+
" updated)。别这样;哪句话先在别的语言里成形了,先换成他的语言再写下来。\n"
|
|
131
|
+
" 好例子(用户在说中文,所以整块是中文):\n"
|
|
132
|
+
" '<think>他想改叫999、还说喜欢说大话,那我先把名字这些存好,回复也顺着这个爱吹的人设、语气夸张点才对味</think>'。\n"
|
|
133
|
+
" 坏例子(同一个用户说的是中文;这个英文块语言错了,而且机械地报了步骤):\n"
|
|
134
|
+
" '<think>Let me update the name and match a boastful tone</think>'。\n"
|
|
135
|
+
"\n"
|
|
136
|
+
" 只说日常意图:不出现工具名、参数、字段名、服务器、「身份卡」这类内部或技术说法,\n"
|
|
137
|
+
" 也不要在正文里提到这条规则本身。"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Screen-watch adds this immediately after the shared instruction. It narrows
|
|
141
|
+
# the no-narration rule to visible speech without silencing private thoughts.
|
|
142
|
+
SCREEN_WATCH_INSTRUCTION = (
|
|
143
|
+
" 「不要叙述你在看屏幕」这条只管你说出口的话。心里话里,你看到了什么、屏幕上在发生什么,\n"
|
|
144
|
+
" 该写就写。那本来就是你此刻在想的事。"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def enabled() -> bool:
|
|
149
|
+
return os.environ.get(_ENV_FLAG, "1").strip().lower() not in {"0", "false", "no", "off"}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _sanitize(value: str) -> str:
|
|
153
|
+
out: list[str] = []
|
|
154
|
+
for ch in str(value or ""):
|
|
155
|
+
if ch in _BIDI_CONTROLS or ch in _INVISIBLE:
|
|
156
|
+
continue
|
|
157
|
+
if unicodedata.category(ch) == "Cc": # control incl \x00 \t \n
|
|
158
|
+
out.append(" ")
|
|
159
|
+
continue
|
|
160
|
+
out.append(ch)
|
|
161
|
+
return " ".join("".join(out).split()).strip()[:MAX_THINKING_CHARS]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _lstrip_invisible(s: str) -> str:
|
|
165
|
+
i = 0
|
|
166
|
+
while i < len(s) and (s[i].isspace() or s[i] in _INVISIBLE):
|
|
167
|
+
i += 1
|
|
168
|
+
return s[i:]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def split_thinking(text: str) -> tuple[str, str, str]:
|
|
172
|
+
"""Return ``(status, thinking, reply)`` — see module docstring for the contract."""
|
|
173
|
+
raw = str(text or "")
|
|
174
|
+
head = _lstrip_invisible(raw)
|
|
175
|
+
if not head.startswith("<"):
|
|
176
|
+
return ABSENT, "", raw # no leading protocol candidate → reply untouched
|
|
177
|
+
|
|
178
|
+
# Parse the leading tag token: '<' ws '/'? ws letters ws '>'?
|
|
179
|
+
m = re.match(r"<\s*(/?)\s*([A-Za-z]*)\s*(>?)", head)
|
|
180
|
+
slash, word, gt = m.group(1), (m.group(2) or "").lower(), m.group(3)
|
|
181
|
+
|
|
182
|
+
is_full = word in _TAG_WORDS
|
|
183
|
+
is_prefix = bool(word) and any(w.startswith(word) for w in _TAG_WORDS)
|
|
184
|
+
|
|
185
|
+
# Not one of our tags at all (e.g. <div>, <3) → leave as ordinary reply.
|
|
186
|
+
if not is_prefix:
|
|
187
|
+
return ABSENT, "", raw
|
|
188
|
+
|
|
189
|
+
# A leading close tag, or a truncated / partial-word opener → cannot be a clean
|
|
190
|
+
# opener. Fail closed; never leak the fragment.
|
|
191
|
+
if slash or not is_full or not gt:
|
|
192
|
+
return FAILED, "", ""
|
|
193
|
+
|
|
194
|
+
# Full '<tag>' opener with '>'. Find its matching close.
|
|
195
|
+
rest = head[m.end():]
|
|
196
|
+
close = re.search(rf"<\s*/\s*{word}\s*>", rest, re.IGNORECASE)
|
|
197
|
+
if not close:
|
|
198
|
+
return FAILED, "", "" # truncated or mismatched close
|
|
199
|
+
inner = rest[: close.start()]
|
|
200
|
+
reply = rest[close.end():].strip()
|
|
201
|
+
|
|
202
|
+
# Nesting or an extra protocol tag inside the thinking block → ambiguous.
|
|
203
|
+
if _ANY_TAG.search(inner):
|
|
204
|
+
return FAILED, "", ""
|
|
205
|
+
if not reply:
|
|
206
|
+
return SILENT, _sanitize(inner), ""
|
|
207
|
+
return COMPLETE, _sanitize(inner), reply
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
# 全文剥离闸(2026-08-08)。split_thinking 只认开头第一块——那是当初 Codex review
|
|
212
|
+
# 要求的保守设计,为了不误剥正文里被引用的标签。线上证明它漏了两种形状:
|
|
213
|
+
# * 开头剥完后面还有一整块(gpt-5.4 一轮写了两个块)
|
|
214
|
+
# * 开标签被上游吃掉,只剩孤立闭标签(pi + 中转站)
|
|
215
|
+
# 两种都从「不认识就原样放行」这个 fail-open 缺口漏进了用户气泡。
|
|
216
|
+
# 本节改为 fail-CLOSED,并由四个对外出口 + 一个历史入口共用。
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
_GATE_ENV_FLAG = "FEEDLING_THINK_GATE"
|
|
220
|
+
|
|
221
|
+
# 标签名边界。**不能用 `\b`**:`\b` 在 `t` 和 `-` 之间成立,于是 `<thought-process>`
|
|
222
|
+
# 这种合法 XML/JSX 标签会被 _RESIDUE 判成残留、又不被 _PAIRED_BLOCK 接受,整条回复
|
|
223
|
+
# 白白失败关闭(Codex review 2026-08-08 实测)。XML 名称允许 `-` `.` `:`,所以边界
|
|
224
|
+
# 必须显式排掉这些字符。
|
|
225
|
+
_NAME_END = r"(?![\w:.-])"
|
|
226
|
+
# 一整对同名标签。开闭必须同名(`(?P=tag)`),否则 <think>…</reasoning> 这种
|
|
227
|
+
# 错配会被当成一块合法协议剥掉。
|
|
228
|
+
_PAIRED_BLOCK = re.compile(
|
|
229
|
+
rf"<\s*(?P<tag>{_TAG_ALT}){_NAME_END}\s*>(?P<body>.*?)<\s*/\s*(?P=tag){_NAME_END}\s*>",
|
|
230
|
+
re.IGNORECASE | re.DOTALL,
|
|
231
|
+
)
|
|
232
|
+
# 剥完之后判定「还有没有残留」。任何开或闭标签都算。
|
|
233
|
+
_RESIDUE = re.compile(rf"<\s*/?\s*(?:{_TAG_ALT}){_NAME_END}", re.IGNORECASE)
|
|
234
|
+
# 孤立闭标签:按本协议思考永远写在最前面,所以一个配不上对的 </think> 说明它
|
|
235
|
+
# 前面的全是思考(开标签在上游某处被吃掉了)。
|
|
236
|
+
_LONE_CLOSE = re.compile(rf"<\s*/\s*(?:{_TAG_ALT}){_NAME_END}\s*>", re.IGNORECASE)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def gate_enabled() -> bool:
|
|
240
|
+
"""泄漏闸的 kill switch。默认开——关掉只用于线上出问题时立刻止血,
|
|
241
|
+
不是灰度门。关掉后调用方必须逐字回到本次改动前的行为。"""
|
|
242
|
+
return os.environ.get(_GATE_ENV_FLAG, "1").strip().lower() not in {
|
|
243
|
+
"0", "false", "no", "off",
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# 只删标签壳、保留文字。给「历史入口」的 FAILED 兜底用:那种行结构已经乱到
|
|
248
|
+
# 分不清哪段是思考,但它的文字本来就已经发到用户眼前过了,删掉整行会平白打断
|
|
249
|
+
# 对话连贯性。入口真正要断的是「模型看到可抄的格式」,删掉标签壳就够了。
|
|
250
|
+
_TAG_MARKER = re.compile(rf"<\s*/?\s*(?:{_TAG_ALT})(?![\w:.-])\s*>", re.IGNORECASE)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def strip_tag_markers(text: str) -> str:
|
|
254
|
+
"""删掉 think 类标签本身,保留标签之间的文字。"""
|
|
255
|
+
return re.sub(r"\n{3,}", "\n\n", _TAG_MARKER.sub("", str(text or ""))).strip()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def strip_all_thinking(text: str, *, sanitize: bool = True) -> tuple[str, str, str]:
|
|
259
|
+
"""全文剥离版,返回 ``(status, thinking, reply)``,状态常量与
|
|
260
|
+
:func:`split_thinking` 完全相同,方便调用点按 kill switch 二选一。
|
|
261
|
+
|
|
262
|
+
与 split_thinking 的唯一区别是扫描范围:那个只认开头第一块,这个扫全文并
|
|
263
|
+
在结尾复查残留。剥完只要正文里还剩任何 think 类标签,就返回 ``FAILED``
|
|
264
|
+
(thinking/reply 都为空),由调用方决定发兜底话还是静默——绝不把带标签的
|
|
265
|
+
残文端给用户。
|
|
266
|
+
|
|
267
|
+
``sanitize=False`` 时思考按原样(保留换行、不截断)返回,交给调用方自己
|
|
268
|
+
格式化。V1 consumer 用这条:它有自己的摘要器(保留换行、上限 700),本次
|
|
269
|
+
统一剥离**判据**,不该顺带改掉它的展示格式。
|
|
270
|
+
"""
|
|
271
|
+
raw = str(text or "")
|
|
272
|
+
if not _RESIDUE.search(raw):
|
|
273
|
+
# 逐字节不变的快路径。没有标签就绝不碰,是 kill switch 之外的第二道保险。
|
|
274
|
+
return ABSENT, "", raw
|
|
275
|
+
|
|
276
|
+
blocks: list[str] = []
|
|
277
|
+
|
|
278
|
+
def _take(match: "re.Match[str]") -> str:
|
|
279
|
+
body = match.group("body") or ""
|
|
280
|
+
# 块里还有别的标签,说明结构已经乱了,不当作可信思考内容——留在原地,
|
|
281
|
+
# 由下面的残留检查失败关闭。
|
|
282
|
+
if _ANY_TAG.search(body):
|
|
283
|
+
return match.group(0)
|
|
284
|
+
if body.strip():
|
|
285
|
+
blocks.append(body.strip())
|
|
286
|
+
return "\n"
|
|
287
|
+
|
|
288
|
+
reply = _PAIRED_BLOCK.sub(_take, raw)
|
|
289
|
+
|
|
290
|
+
# 孤立闭标签:它之前的一切当思考。只处理第一个——出现多个说明结构已乱,
|
|
291
|
+
# 同样交给残留检查失败关闭。
|
|
292
|
+
lone = _LONE_CLOSE.search(reply)
|
|
293
|
+
if lone is not None:
|
|
294
|
+
head = reply[: lone.start()].strip()
|
|
295
|
+
# head 里还带标签 = 开闭错配(<think>…</reasoning>)或多层残骸,不是
|
|
296
|
+
# 「开标签被上游吃掉」那种可救的形状。失败关闭,别把带标签的文本当思考。
|
|
297
|
+
if _RESIDUE.search(head):
|
|
298
|
+
return FAILED, "", ""
|
|
299
|
+
if blocks and head:
|
|
300
|
+
# 已经剥出过完整块,却还剩一个带内容的孤立闭标签——这不是「开标签被
|
|
301
|
+
# 吃掉」,而是结构本身就乱了。此时把 head 当思考会把真正的正文吞进
|
|
302
|
+
# 推理过程(`<think>A</think>正文甲</think>正文乙` → 正文甲消失,
|
|
303
|
+
# Codex review 2026-08-08 实测)。失败关闭。
|
|
304
|
+
return FAILED, "", ""
|
|
305
|
+
if head:
|
|
306
|
+
blocks.insert(0, head)
|
|
307
|
+
reply = reply[lone.end():]
|
|
308
|
+
|
|
309
|
+
if _RESIDUE.search(reply):
|
|
310
|
+
return FAILED, "", ""
|
|
311
|
+
if not blocks:
|
|
312
|
+
# 有标签、却一块内容都没剥出来(例如只有一个空标签对)——同样不可信。
|
|
313
|
+
return FAILED, "", ""
|
|
314
|
+
|
|
315
|
+
reply = re.sub(r"\n{3,}", "\n\n", reply).strip()
|
|
316
|
+
joined = "\n".join(blocks)
|
|
317
|
+
thinking = _sanitize(joined) if sanitize else joined.strip()
|
|
318
|
+
if not reply:
|
|
319
|
+
return SILENT, thinking, ""
|
|
320
|
+
return COMPLETE, thinking, reply
|