mlx-code 0.0.33__tar.gz → 0.0.35__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.
- {mlx_code-0.0.33 → mlx_code-0.0.35}/PKG-INFO +13 -4
- {mlx_code-0.0.33 → mlx_code-0.0.35}/README.md +10 -1
- mlx_code-0.0.35/mlx_code/apis.py +373 -0
- mlx_code-0.0.35/mlx_code/bare.py +177 -0
- mlx_code-0.0.35/mlx_code/bats.py +262 -0
- mlx_code-0.0.35/mlx_code/bats_plan.py +43 -0
- mlx_code-0.0.35/mlx_code/bench_bats.py +146 -0
- mlx_code-0.0.35/mlx_code/gits.py +293 -0
- mlx_code-0.0.35/mlx_code/lsp_tool.py +326 -0
- mlx_code-0.0.35/mlx_code/main.py +513 -0
- mlx_code-0.0.35/mlx_code/mcb.py +91 -0
- mlx_code-0.0.35/mlx_code/mcb_tool.py +74 -0
- mlx_code-0.0.35/mlx_code/mlxs.py +101 -0
- mlx_code-0.0.35/mlx_code/repl.py +587 -0
- mlx_code-0.0.35/mlx_code/stream_log.py +28 -0
- mlx_code-0.0.35/mlx_code/test_bats_plan.py +69 -0
- mlx_code-0.0.35/mlx_code/tools.py +205 -0
- mlx_code-0.0.35/mlx_code/tui.py +278 -0
- mlx_code-0.0.35/mlx_code/util.py +16 -0
- mlx_code-0.0.35/mlx_code/view_git.py +647 -0
- mlx_code-0.0.35/mlx_code/view_log.py +311 -0
- mlx_code-0.0.35/mlx_code/vlls.py +88 -0
- mlx_code-0.0.35/mlx_code/web.py +114 -0
- {mlx_code-0.0.33 → mlx_code-0.0.35}/mlx_code.egg-info/PKG-INFO +13 -4
- {mlx_code-0.0.33 → mlx_code-0.0.35}/mlx_code.egg-info/SOURCES.txt +5 -0
- {mlx_code-0.0.33 → mlx_code-0.0.35}/mlx_code.egg-info/requires.txt +3 -1
- {mlx_code-0.0.33 → mlx_code-0.0.35}/setup.py +10 -4
- mlx_code-0.0.33/mlx_code/apis.py +0 -574
- mlx_code-0.0.33/mlx_code/bare.py +0 -276
- mlx_code-0.0.33/mlx_code/bats.py +0 -318
- mlx_code-0.0.33/mlx_code/gits.py +0 -379
- mlx_code-0.0.33/mlx_code/lsp_tool.py +0 -599
- mlx_code-0.0.33/mlx_code/main.py +0 -987
- mlx_code-0.0.33/mlx_code/mcb.py +0 -183
- mlx_code-0.0.33/mlx_code/mcb_tool.py +0 -61
- mlx_code-0.0.33/mlx_code/repl.py +0 -874
- mlx_code-0.0.33/mlx_code/stream_log.py +0 -62
- mlx_code-0.0.33/mlx_code/tools.py +0 -425
- mlx_code-0.0.33/mlx_code/tui.py +0 -575
- mlx_code-0.0.33/mlx_code/util.py +0 -30
- mlx_code-0.0.33/mlx_code/view_git.py +0 -995
- mlx_code-0.0.33/mlx_code/view_log.py +0 -625
- mlx_code-0.0.33/mlx_code/web.py +0 -448
- {mlx_code-0.0.33 → mlx_code-0.0.35}/LICENSE +0 -0
- {mlx_code-0.0.33 → mlx_code-0.0.35}/mlx_code/__init__.py +0 -0
- {mlx_code-0.0.33 → mlx_code-0.0.35}/mlx_code.egg-info/dependency_links.txt +0 -0
- {mlx_code-0.0.33 → mlx_code-0.0.35}/mlx_code.egg-info/entry_points.txt +0 -0
- {mlx_code-0.0.33 → mlx_code-0.0.35}/mlx_code.egg-info/top_level.txt +0 -0
- {mlx_code-0.0.33 → mlx_code-0.0.35}/setup.cfg +0 -0
- {mlx_code-0.0.33 → mlx_code-0.0.35}/tests/__init__.py +0 -0
- {mlx_code-0.0.33 → mlx_code-0.0.35}/tests/test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mlx-code
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.35
|
|
4
4
|
Summary: Coding Agent for Mac
|
|
5
5
|
Home-page: https://josefalbers.github.io/mlx-code/
|
|
6
6
|
Author: J Joe
|
|
@@ -8,11 +8,12 @@ Author-email: albersj66@gmail.com
|
|
|
8
8
|
License: Apache-2.0
|
|
9
9
|
Project-URL: Source, https://github.com/JosefAlbers/mlx-code
|
|
10
10
|
Project-URL: Issues, https://github.com/JosefAlbers/mlx-code/issues
|
|
11
|
-
Project-URL: Documentation, https://
|
|
11
|
+
Project-URL: Documentation, https://www.mlx-code.com/
|
|
12
12
|
Requires-Python: >=3.12.8
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: mlx-lm>=0.31.3; platform_system == "Darwin"
|
|
16
|
+
Requires-Dist: vllm; platform_system != "Darwin"
|
|
16
17
|
Requires-Dist: httpx
|
|
17
18
|
Requires-Dist: pydantic
|
|
18
19
|
Provides-Extra: all
|
|
@@ -21,7 +22,6 @@ Requires-Dist: uvicorn; extra == "all"
|
|
|
21
22
|
Requires-Dist: pygments; extra == "all"
|
|
22
23
|
Requires-Dist: textual>=8.2.7; extra == "all"
|
|
23
24
|
Requires-Dist: rich>=15.0.0; extra == "all"
|
|
24
|
-
Requires-Dist: python-lsp-server[all]; extra == "all"
|
|
25
25
|
Dynamic: author
|
|
26
26
|
Dynamic: author-email
|
|
27
27
|
Dynamic: description
|
|
@@ -107,7 +107,7 @@ result = await agent.run('refactor utils.py to use dataclasses')
|
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
109
|
# ephemeral run (no installation)
|
|
110
|
-
uvx --from mlx-code[all] mlc
|
|
110
|
+
uvx --from "mlx-code[all]" mlc
|
|
111
111
|
|
|
112
112
|
# or install into the current environment
|
|
113
113
|
pip install mlx-code[all]
|
|
@@ -563,6 +563,15 @@ podman run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token $JJ_
|
|
|
563
563
|
phone http://jjoe.mlx-code.com
|
|
564
564
|
```
|
|
565
565
|
|
|
566
|
+
#### VLLM
|
|
567
|
+
|
|
568
|
+
```
|
|
569
|
+
curl -fsSL https://raw.githubusercontent.com/vllm-project/vllm-metal/main/install.sh | bash
|
|
570
|
+
source ~/.venv-vllm-metal/bin/activate # or `source ~/.venv-vllm-metal/bin/activate.fish` if fish
|
|
571
|
+
pip install mlx-code[all]
|
|
572
|
+
mlc --backend vllm -m Qwen/Qwen3.5-0.8B
|
|
573
|
+
```
|
|
574
|
+
|
|
566
575
|
---
|
|
567
576
|
|
|
568
577
|
## Credits
|
|
@@ -70,7 +70,7 @@ result = await agent.run('refactor utils.py to use dataclasses')
|
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
72
|
# ephemeral run (no installation)
|
|
73
|
-
uvx --from mlx-code[all] mlc
|
|
73
|
+
uvx --from "mlx-code[all]" mlc
|
|
74
74
|
|
|
75
75
|
# or install into the current environment
|
|
76
76
|
pip install mlx-code[all]
|
|
@@ -526,6 +526,15 @@ podman run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token $JJ_
|
|
|
526
526
|
phone http://jjoe.mlx-code.com
|
|
527
527
|
```
|
|
528
528
|
|
|
529
|
+
#### VLLM
|
|
530
|
+
|
|
531
|
+
```
|
|
532
|
+
curl -fsSL https://raw.githubusercontent.com/vllm-project/vllm-metal/main/install.sh | bash
|
|
533
|
+
source ~/.venv-vllm-metal/bin/activate # or `source ~/.venv-vllm-metal/bin/activate.fish` if fish
|
|
534
|
+
pip install mlx-code[all]
|
|
535
|
+
mlc --backend vllm -m Qwen/Qwen3.5-0.8B
|
|
536
|
+
```
|
|
537
|
+
|
|
529
538
|
---
|
|
530
539
|
|
|
531
540
|
## Credits
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_AN='function_call'
|
|
3
|
+
_AM='Authorization'
|
|
4
|
+
_AL='reasoning_content'
|
|
5
|
+
_AK='output_tokens'
|
|
6
|
+
_AJ='input_tokens'
|
|
7
|
+
_AI='redacted_data'
|
|
8
|
+
_AH='redacted_thinking'
|
|
9
|
+
_AG='response'
|
|
10
|
+
_AF='functionCall'
|
|
11
|
+
_AE='Content-Type'
|
|
12
|
+
_AD='object'
|
|
13
|
+
_AC='parameters'
|
|
14
|
+
_AB='description'
|
|
15
|
+
_AA='tool_calls'
|
|
16
|
+
_A9='tool_choice'
|
|
17
|
+
_A8='stream'
|
|
18
|
+
_A7='thought'
|
|
19
|
+
_A6='reason'
|
|
20
|
+
_A5='done'
|
|
21
|
+
_A4='tool_call'
|
|
22
|
+
_A3='toolcall_end'
|
|
23
|
+
_A2='thinking_delta'
|
|
24
|
+
_A1='index'
|
|
25
|
+
_A0='data: '
|
|
26
|
+
_z='start'
|
|
27
|
+
_y='POST'
|
|
28
|
+
_x='timestamp'
|
|
29
|
+
_w='application/json'
|
|
30
|
+
_v='tools'
|
|
31
|
+
_u='temperature'
|
|
32
|
+
_t='max_tokens'
|
|
33
|
+
_s='model'
|
|
34
|
+
_r='No api key'
|
|
35
|
+
_q='call_id'
|
|
36
|
+
_p='image_url'
|
|
37
|
+
_o='length'
|
|
38
|
+
_n='text_delta'
|
|
39
|
+
_m='cache_write'
|
|
40
|
+
_l='cache_read'
|
|
41
|
+
_k=True
|
|
42
|
+
_j='toolResult'
|
|
43
|
+
_i='redacted'
|
|
44
|
+
_h='mime_type'
|
|
45
|
+
_g='args'
|
|
46
|
+
_f='parts'
|
|
47
|
+
_e='properties'
|
|
48
|
+
_d='tool_call_id'
|
|
49
|
+
_c='tool_use'
|
|
50
|
+
_b='args_buf'
|
|
51
|
+
_a='message'
|
|
52
|
+
_Z='stop'
|
|
53
|
+
_Y='error_message'
|
|
54
|
+
_X='required'
|
|
55
|
+
_W='function'
|
|
56
|
+
_V='toolCall'
|
|
57
|
+
_U='signature'
|
|
58
|
+
_T='data'
|
|
59
|
+
_S='assistant'
|
|
60
|
+
_R='error'
|
|
61
|
+
_Q='delta'
|
|
62
|
+
_P='user'
|
|
63
|
+
_O='output'
|
|
64
|
+
_N='input'
|
|
65
|
+
_M='arguments'
|
|
66
|
+
_L='partial'
|
|
67
|
+
_K='stop_reason'
|
|
68
|
+
_J='id'
|
|
69
|
+
_I='thinking'
|
|
70
|
+
_H='payload'
|
|
71
|
+
_G='usage'
|
|
72
|
+
_F='role'
|
|
73
|
+
_E='name'
|
|
74
|
+
_D='text'
|
|
75
|
+
_C='content'
|
|
76
|
+
_B=None
|
|
77
|
+
_A='type'
|
|
78
|
+
import asyncio,json,os,time,uuid,logging,httpx
|
|
79
|
+
from typing import Any,Literal
|
|
80
|
+
from.tools import Tool
|
|
81
|
+
logger=logging.getLogger(__name__)
|
|
82
|
+
class EventStream:
|
|
83
|
+
def __init__(A)->_B:(A._queue):asyncio.Queue[dict|_B]=asyncio.Queue();(A._result):dict|_B=_B;(A._task):asyncio.Task|_B=_B
|
|
84
|
+
def _attach(A,task:asyncio.Task)->_B:A._task=task
|
|
85
|
+
def push(A,event:dict)->_B:A._queue.put_nowait(event)
|
|
86
|
+
def finish(A,result:dict)->_B:A._result=result;A._queue.put_nowait(_B)
|
|
87
|
+
async def result(A)->dict:
|
|
88
|
+
if A._result is _B:
|
|
89
|
+
async for B in A:0
|
|
90
|
+
assert A._result is not _B;return A._result
|
|
91
|
+
def __aiter__(A)->'EventStream':return A
|
|
92
|
+
async def __anext__(B)->dict:
|
|
93
|
+
A=await B._queue.get()
|
|
94
|
+
if A is _B:raise StopAsyncIteration
|
|
95
|
+
return A
|
|
96
|
+
_REASONING_BUDGET:dict[str,int]={'minimal':512,'low':1024,'medium':8192,'high':16000,'xhigh':32000}
|
|
97
|
+
class ClaudeChat:
|
|
98
|
+
def __init__(A,*,model=_B,api_key=_B,base_url=_B,max_tokens=8192,temperature=_B,reasoning:Literal['off','minimal','low','medium','high','xhigh']='off',tool_choice=_B):
|
|
99
|
+
A.model=model or'claude-haiku-4-5';A.api_key=api_key or os.environ.get('ANTHROPIC_API_KEY');A.base_url=(base_url or'https://api.anthropic.com').rstrip('/');A.max_tokens=max_tokens;A.temperature=temperature;A.reasoning=reasoning;A.tool_choice=tool_choice
|
|
100
|
+
if not A.api_key:logger.debug(_r)
|
|
101
|
+
def _fmt_content(H,content)->str|list:
|
|
102
|
+
G='image';F=content;C='cache_control'
|
|
103
|
+
if isinstance(F,str):return F
|
|
104
|
+
B=[]
|
|
105
|
+
for A in F:
|
|
106
|
+
E=A[_A]
|
|
107
|
+
if E==_D:
|
|
108
|
+
D={_A:_D,_D:A[_D]}
|
|
109
|
+
if A.get(C):D[C]={_A:A[C]}
|
|
110
|
+
B.append(D)
|
|
111
|
+
elif E==G:
|
|
112
|
+
D={_A:G,'source':{_A:'base64','media_type':A[_h],_T:A[_T]}}
|
|
113
|
+
if A.get(C):D[C]={_A:A[C]}
|
|
114
|
+
B.append(D)
|
|
115
|
+
elif E==_I:
|
|
116
|
+
if A[_i]:B.append({_A:_AH,_T:A.get(_I,A.get(_AI,''))})
|
|
117
|
+
else:B.append({_A:_I,_I:A[_I],_U:A.get(_U)or''})
|
|
118
|
+
elif E==_V:B.append({_A:_c,_J:A[_J],_E:A[_E],_N:A[_M]})
|
|
119
|
+
return B
|
|
120
|
+
def _build_messages(E,messages:list[dict])->list[dict]:
|
|
121
|
+
H='is_error';B=messages;D=[];A=0
|
|
122
|
+
while A<len(B):
|
|
123
|
+
C=B[A]
|
|
124
|
+
if C[_F]==_P:D.append({_F:_P,_C:E._fmt_content(C[_C])});A+=1
|
|
125
|
+
elif C[_F]==_S:D.append({_F:_S,_C:E._fmt_content(C[_C])});A+=1
|
|
126
|
+
elif C[_F]==_j:
|
|
127
|
+
G=[]
|
|
128
|
+
while A<len(B)and B[A][_F]==_j:F=B[A];G.append({_A:'tool_result','tool_use_id':F[_d],_C:E._fmt_content(F[_C]),H:F[H]});A+=1
|
|
129
|
+
D.append({_F:_P,_C:G})
|
|
130
|
+
else:A+=1
|
|
131
|
+
return D
|
|
132
|
+
async def stream(C,messages:list[dict],system:str,tools:list[Tool])->EventStream:
|
|
133
|
+
E=tools;B=system;D=EventStream();G:dict[str,Any]={_s:C.model,_t:C.max_tokens,'messages':C._build_messages(messages),_A8:_k}
|
|
134
|
+
if B:G['system']=[{_A:_D,_D:B}]
|
|
135
|
+
if C.temperature:G[_u]=C.temperature
|
|
136
|
+
if C.reasoning!='off':G[_I]={_A:'enabled','budget_tokens':_REASONING_BUDGET[C.reasoning]}
|
|
137
|
+
if E:
|
|
138
|
+
G[_v]=[A.schema()for A in E];A=C.tool_choice
|
|
139
|
+
if A is not _B:G[_A9]={_A:'tool',_E:A[_E]}if isinstance(A,dict)else{_A:'any'}if A==_X else{_A:'none'}if A=='none'else{_A:'auto'}
|
|
140
|
+
W={'x-api-key':str(C.api_key),'anthropic-version':'2023-06-01','content-type':_w};logger.debug(json.dumps(G,indent=2))
|
|
141
|
+
async def F()->_B:
|
|
142
|
+
A={_F:_S,_C:[],_K:_Z,_G:{_N:0,_O:0,_l:0,_m:0},_Y:_B,_x:int(time.time()*1000)}
|
|
143
|
+
try:
|
|
144
|
+
async with httpx.AsyncClient(timeout=12e1)as X:
|
|
145
|
+
async with X.stream(_y,f"{C.base_url}/v1/messages",json=G,headers=W)as N:
|
|
146
|
+
if N.status_code>=400:raise RuntimeError(f"HTTP {N.status_code}: {(await N.aread()).decode()}")
|
|
147
|
+
D.push({_A:_z,_H:{_L:A}});O:dict[int,str]={};H:dict[int,int]={}
|
|
148
|
+
async for T in N.aiter_lines():
|
|
149
|
+
if not T.startswith(_A0):continue
|
|
150
|
+
U=T[6:]
|
|
151
|
+
if U=='[DONE]':break
|
|
152
|
+
E=json.loads(U);I=E.get(_A)
|
|
153
|
+
if I=='message_start':J=E.get(_a,{}).get(_G,{});A[_G][_N]=J.get(_AJ,0);A[_G][_l]=J.get('cache_read_input_tokens',0);A[_G][_m]=J.get('cache_creation_input_tokens',0)
|
|
154
|
+
elif I=='content_block_start':
|
|
155
|
+
B=E.get(_A1,0);P=E.get('content_block',{});Q=P.get(_A)
|
|
156
|
+
if Q==_D:H[B]=len(A[_C]);A[_C].append({_A:_D,_D:''})
|
|
157
|
+
elif Q==_I:H[B]=len(A[_C]);A[_C].append({_A:_I,_I:'',_U:_B,_i:False})
|
|
158
|
+
elif Q==_AH:H[B]=len(A[_C]);A[_C].append({_A:_I,_AI:P.get(_T,''),_U:_B,_i:_k})
|
|
159
|
+
elif Q==_c:H[B]=len(A[_C]);A[_C].append({_A:_V,_J:P[_J],_E:P[_E],_M:{}});O[B]=''
|
|
160
|
+
elif I=='content_block_delta':
|
|
161
|
+
B=E.get(_A1,0);L=E.get(_Q,{});R=L.get(_A);K=H.get(B)
|
|
162
|
+
if K is _B:continue
|
|
163
|
+
F=A[_C][K]
|
|
164
|
+
if R==_n:M=L.get(_D,'');F[_D]+=M;D.push({_A:_n,_H:{_Q:M,_L:A}})
|
|
165
|
+
elif R==_A2:M=L.get(_I,'');F[_I]+=M;D.push({_A:_A2,_H:{_Q:M,_L:A}})
|
|
166
|
+
elif R=='signature_delta':F[_U]=(F.get(_U)or'')+L.get(_U,'')
|
|
167
|
+
elif R=='input_json_delta':O[B]=O.get(B,'')+L.get('partial_json','')
|
|
168
|
+
elif I=='content_block_stop':
|
|
169
|
+
B=E.get(_A1,0);K=H.get(B)
|
|
170
|
+
if K is not _B and K<len(A[_C]):
|
|
171
|
+
F=A[_C][K]
|
|
172
|
+
if F[_A]==_V:
|
|
173
|
+
V=O.pop(B,'')
|
|
174
|
+
if V:
|
|
175
|
+
try:F[_M]=json.loads(V)
|
|
176
|
+
except json.JSONDecodeError:pass
|
|
177
|
+
D.push({_A:_A3,_H:{_A4:F,_L:A}})
|
|
178
|
+
elif I=='message_delta':
|
|
179
|
+
S=E.get(_Q,{}).get(_K)
|
|
180
|
+
if S==_c:A[_K]=_c
|
|
181
|
+
elif S==_t:A[_K]=_o
|
|
182
|
+
elif S:A[_K]=_Z
|
|
183
|
+
J=E.get(_G,{})
|
|
184
|
+
if J:A[_G][_O]=J.get(_AK,A[_G][_O])
|
|
185
|
+
elif I=='message_stop':D.push({_A:_A5,_H:{_A6:A[_K],_a:A}})
|
|
186
|
+
except Exception as Y:A[_K]=_R;A[_Y]=str(Y);D.push({_A:_R,_H:{_R:A}})
|
|
187
|
+
D.finish(A)
|
|
188
|
+
D._attach(asyncio.create_task(F()));return D
|
|
189
|
+
class DefaultChat:
|
|
190
|
+
def __init__(A,*,model=_B,api_key=_B,base_url=_B,max_tokens=8192,temperature=_B,tool_choice=_B):
|
|
191
|
+
C='noapi';B=api_key;A.model=model or C;A.api_key=B or C;A.base_url=(base_url or'http://127.0.0.1:8000').rstrip('/');A.max_tokens=max_tokens;A.temperature=temperature;A.tool_choice=tool_choice
|
|
192
|
+
if not B:logger.debug(_r)
|
|
193
|
+
def _build_messages(K,messages:list[dict],system:str)->list[dict]:
|
|
194
|
+
J='url';F=system;B=[]
|
|
195
|
+
if F:B.append({_F:'system',_C:F})
|
|
196
|
+
for A in messages:
|
|
197
|
+
if A[_F]==_P:
|
|
198
|
+
E=A[_C]
|
|
199
|
+
if isinstance(E,str):B.append({_F:_P,_C:E})
|
|
200
|
+
else:B.append({_F:_P,_C:[{_A:_D,_D:A[_D]}if A[_A]==_D else{_A:_p,_p:{J:f"data:{A[_h]};base64,{A[_T]}"}}for A in E]})
|
|
201
|
+
elif A[_F]==_S:
|
|
202
|
+
G=[A for A in A[_C]if A[_A]==_D];H=[A for A in A[_C]if A[_A]==_V];I=[A for A in A[_C]if A[_A]==_I];C:dict[str,Any]={_F:_S}
|
|
203
|
+
if G:C[_C]=''.join(A[_D]for A in G)
|
|
204
|
+
if I:C[_AL]=''.join(A[_I]for A in I)
|
|
205
|
+
if H:C[_AA]=[{_J:A[_J],_A:_W,_W:{_E:A[_E],_M:json.dumps(A[_M])}}for A in H]
|
|
206
|
+
B.append(C)
|
|
207
|
+
elif A[_F]==_j:D=A[_C];B.append({_F:'tool',_d:A[_d],_C:D[0][_D]if len(D)==1 and D[0][_A]==_D else[{_A:_D,_D:A[_D]}if A[_A]==_D else{_A:_p,_p:{J:f"data:{A[_h]};base64,{A[_T]}"}}for A in D]})
|
|
208
|
+
return B
|
|
209
|
+
async def stream(C,messages:list[dict],system:str,tools:list[Tool])->EventStream:
|
|
210
|
+
D=tools;B=EventStream();E:dict[str,Any]={_s:C.model,_t:C.max_tokens,'messages':C._build_messages(messages,system),_A8:_k}
|
|
211
|
+
if C.temperature:E[_u]=C.temperature
|
|
212
|
+
if D:
|
|
213
|
+
E[_v]=[{_A:_W,_W:{_E:A.name,_AB:A.description,_AC:{_A:_AD,_e:A.parameters.model_json_schema().get(_e,{}),_X:A.parameters.model_json_schema().get(_X,[])}}}for A in D];A=C.tool_choice
|
|
214
|
+
if A is not _B:E[_A9]={_A:_W,_W:{_E:A[_E]}}if isinstance(A,dict)else A
|
|
215
|
+
X={_AM:f"Bearer {C.api_key}",_AE:_w};logger.debug(json.dumps(E,indent=2))
|
|
216
|
+
async def F()->_B:
|
|
217
|
+
A={_F:_S,_C:[],_K:_Z,_G:{_N:0,_O:0,_l:0,_m:0},_Y:_B,_x:int(time.time()*1000)}
|
|
218
|
+
try:
|
|
219
|
+
async with httpx.AsyncClient(timeout=_B)as Y:
|
|
220
|
+
async with Y.stream(_y,f"{C.base_url}/v1/chat/completions",json=E,headers=X)as G:
|
|
221
|
+
if G.status_code>=400:raise RuntimeError(f"HTTP {G.status_code}: {(await G.aread()).decode()}")
|
|
222
|
+
B.push({_A:_z,_H:{_L:A}});F:dict[int,dict]={};L=M='';H=_B
|
|
223
|
+
async for P in G.aiter_lines():
|
|
224
|
+
if not P.startswith(_A0):continue
|
|
225
|
+
N=P[6:].strip()
|
|
226
|
+
if N=='[DONE]'or not N:continue
|
|
227
|
+
Q=json.loads(N);R=(Q.get('choices')or[{}])[0];I=R.get(_Q,{});H=R.get('finish_reason')or H
|
|
228
|
+
if(S:=I.get(_AL)or I.get(_I)):M+=S;B.push({_A:_A2,_H:{_Q:S,_L:A}})
|
|
229
|
+
if(T:=I.get(_C)):L+=T;B.push({_A:_n,_H:{_Q:T,_L:A}})
|
|
230
|
+
for O in I.get(_AA)or[]:
|
|
231
|
+
J=O.get(_A1,0);D=O.get(_W,{})
|
|
232
|
+
if J not in F:F[J]={_J:O.get(_J,str(uuid.uuid4())),_E:D.get(_E,''),_b:D.get(_M,'')}
|
|
233
|
+
else:
|
|
234
|
+
if _E in D:F[J][_E]+=D[_E]
|
|
235
|
+
if _M in D:F[J][_b]+=D[_M]
|
|
236
|
+
if(U:=Q.get(_G)):A[_G][_N]=U.get('prompt_tokens',A[_G][_N]);A[_G][_O]=U.get('completion_tokens',A[_G][_O])
|
|
237
|
+
if M:A[_C].append({_A:_I,_I:M,_U:_B,_i:False})
|
|
238
|
+
if L:A[_C].append({_A:_D,_D:L})
|
|
239
|
+
for(a,K)in sorted(F.items()):
|
|
240
|
+
try:V=json.loads(K[_b])if K[_b]else{}
|
|
241
|
+
except json.JSONDecodeError:V={}
|
|
242
|
+
W={_A:_V,_J:K[_J],_E:K[_E],_M:V};A[_C].append(W);B.push({_A:_A3,_H:{_A4:W,_L:A}})
|
|
243
|
+
A[_K]=_c if H==_AA else _o if H in(_o,_t)else _Z;B.push({_A:_A5,_H:{_A6:A[_K],_a:A}})
|
|
244
|
+
except Exception as Z:A[_K]=_R;A[_Y]=str(Z);B.push({_A:_R,_H:{_R:A}})
|
|
245
|
+
B.finish(A)
|
|
246
|
+
B._attach(asyncio.create_task(F()));return B
|
|
247
|
+
class GeminiChat:
|
|
248
|
+
def __init__(A,*,model=_B,api_key=_B,base_url=_B,max_tokens=8192,temperature=_B,thinking=False,thinking_budget=8192,tool_choice=_B):
|
|
249
|
+
A.model=model or'gemini-3.1-flash-lite-preview';A.api_key=api_key or os.environ.get('GEMINI_API_KEY');A.base_url=(base_url or'https://generativelanguage.googleapis.com').rstrip('/');A.max_tokens=max_tokens;A.temperature=temperature;A.thinking=thinking;A.thinking_budget=thinking_budget;A.tool_choice=tool_choice
|
|
250
|
+
if not A.api_key:logger.debug(_r)
|
|
251
|
+
def _build_contents(M,messages:list[dict])->list[dict]:
|
|
252
|
+
L='tool_name';C=[];E:list[dict]=[]
|
|
253
|
+
def G():
|
|
254
|
+
if E:C.append({_F:_P,_f:list(E)});E.clear()
|
|
255
|
+
for A in messages:
|
|
256
|
+
if A[_F]==_P:
|
|
257
|
+
G();H=A[_C]
|
|
258
|
+
if isinstance(H,str):C.append({_F:_P,_f:[{_D:H}]})
|
|
259
|
+
else:C.append({_F:_P,_f:[{_D:A[_D]}if A[_A]==_D else{'inlineData':{'mimeType':A[_h],_T:A[_T]}}for A in H]})
|
|
260
|
+
elif A[_F]==_S:
|
|
261
|
+
G();D=[]
|
|
262
|
+
for B in A[_C]:
|
|
263
|
+
if B[_A]==_I:D.append({_A7:B[_I]})
|
|
264
|
+
elif B[_A]==_D:D.append({_D:B[_D]})
|
|
265
|
+
elif B[_A]==_V:
|
|
266
|
+
I:dict={_E:B[_E],_g:B[_M]}
|
|
267
|
+
if B[_J]!=B[_E]:I[_J]=B[_J]
|
|
268
|
+
D.append({_AF:I})
|
|
269
|
+
if D:C.append({_F:_s,_f:D})
|
|
270
|
+
elif A[_F]==_j:
|
|
271
|
+
F=A[_C]
|
|
272
|
+
try:J=json.loads(F[0][_D])if F else{}
|
|
273
|
+
except(json.JSONDecodeError,KeyError):J={'result':F[0][_D]if F else''}
|
|
274
|
+
K:dict={_E:A[L],_AG:J}
|
|
275
|
+
if A[_d]!=A[L]:K[_J]=A[_d]
|
|
276
|
+
E.append({'functionResponse':K})
|
|
277
|
+
G();return C
|
|
278
|
+
async def stream(A,messages:list[dict],system:str,tools:list[Tool])->EventStream:
|
|
279
|
+
I='ANY';H=tools;G=system;F='generationConfig';E='mode';B=EventStream();D:dict[str,Any]={'contents':A._build_contents(messages),F:{'maxOutputTokens':A.max_tokens}}
|
|
280
|
+
if G:D['systemInstruction']={_f:[{_D:G}]}
|
|
281
|
+
if A.temperature:D[F][_u]=A.temperature
|
|
282
|
+
if A.thinking:D[F]['thinkingConfig']={'includeThoughts':_k,'thinkingBudget':A.thinking_budget}
|
|
283
|
+
if H:
|
|
284
|
+
D[_v]=[{'functionDeclarations':[{_E:A.name,_AB:A.description,_AC:{_A:_AD,_e:A.parameters.model_json_schema().get(_e,{}),_X:A.parameters.model_json_schema().get(_X,[])}}for A in H]}];C=A.tool_choice
|
|
285
|
+
if C is not _B:D['toolConfig']={'functionCallingConfig':{E:I,'allowedFunctionNames':[C[_E]]}if isinstance(C,dict)else{E:I}if C==_X else{E:'NONE'}if C=='none'else{E:'AUTO'}}
|
|
286
|
+
S=f"{A.base_url}/v1beta/models/{A.model}:streamGenerateContent?alt=sse&key={A.api_key}";logger.debug(json.dumps(D,indent=2))
|
|
287
|
+
async def J()->_B:
|
|
288
|
+
A={_F:_S,_C:[],_K:_Z,_G:{_N:0,_O:0,_l:0,_m:0},_Y:_B,_x:int(time.time()*1000)}
|
|
289
|
+
try:
|
|
290
|
+
async with httpx.AsyncClient(timeout=12e1)as T:
|
|
291
|
+
async with T.stream(_y,S,json=D,headers={_AE:_w})as H:
|
|
292
|
+
if H.status_code>=400:raise RuntimeError(f"HTTP {H.status_code}: {(await H.aread()).decode()}")
|
|
293
|
+
B.push({_A:_z,_H:{_L:A}});I=J='';E:dict[str,dict]={};K=_B
|
|
294
|
+
async for L in H.aiter_lines():
|
|
295
|
+
if not L.startswith(_A0):continue
|
|
296
|
+
M=L[6:].strip()
|
|
297
|
+
if not M:continue
|
|
298
|
+
N=json.loads(M);O=(N.get('candidates')or[{}])[0];K=O.get('finishReason')or K
|
|
299
|
+
for C in O.get(_C,{}).get(_f)or[]:
|
|
300
|
+
if _A7 in C:J+=C[_A7];B.push({_A:_A2,_H:{_Q:C[_A7],_L:A}})
|
|
301
|
+
elif _D in C:I+=C[_D];B.push({_A:_n,_H:{_Q:C[_D],_L:A}})
|
|
302
|
+
elif _AF in C:
|
|
303
|
+
F=C[_AF];G=F.get(_J)or F[_E]
|
|
304
|
+
if G not in E:E[G]={_E:F[_E],_g:F.get(_g,{})}
|
|
305
|
+
else:E[G][_g].update(F.get(_g,{}))
|
|
306
|
+
if(P:=N.get('usageMetadata')):A[_G][_N]=P.get('promptTokenCount',A[_G][_N]);A[_G][_O]=P.get('candidatesTokenCount',A[_G][_O])
|
|
307
|
+
if J:A[_C].append({_A:_I,_I:J,_U:_B,_i:False})
|
|
308
|
+
if I:A[_C].append({_A:_D,_D:I})
|
|
309
|
+
for(G,Q)in E.items():R={_A:_V,_J:G,_E:Q[_E],_M:Q[_g]};A[_C].append(R);B.push({_A:_A3,_H:{_A4:R,_L:A}})
|
|
310
|
+
A[_K]=_o if K=='MAX_TOKENS'else _c if E else _Z;B.push({_A:_A5,_H:{_A6:A[_K],_a:A}});B.finish(A);return
|
|
311
|
+
except Exception as U:A[_K]=_R;A[_Y]=str(U);B.push({_A:_R,_H:{_R:A}})
|
|
312
|
+
B.finish(A)
|
|
313
|
+
B._attach(asyncio.create_task(J()));return B
|
|
314
|
+
class CodexChat:
|
|
315
|
+
def __init__(A,*,model=_B,api_key=_B,base_url=_B,max_tokens=8192,temperature=_B,tool_choice=_B):
|
|
316
|
+
A.model=model or'gpt-5.4-mini';A.api_key=api_key or os.environ.get('OPENAI_API_KEY');A.base_url=(base_url or'https://api.openai.com').rstrip('/');A.max_tokens=max_tokens;A.temperature=temperature;A.tool_choice=tool_choice
|
|
317
|
+
if not A.api_key:logger.debug(_r)
|
|
318
|
+
def _build_input(F,messages:list[dict],system:str)->list[dict]:
|
|
319
|
+
E=system;B=[]
|
|
320
|
+
if E:B.append({_A:_a,_F:'developer',_C:E})
|
|
321
|
+
for A in messages:
|
|
322
|
+
if A[_F]==_P:D=A[_C];B.append({_A:_a,_F:_P,_C:D if isinstance(D,str)else[{_A:'input_text',_D:A[_D]}if A[_A]==_D else{_A:'input_image',_p:f"data:{A[_h]};base64,{A[_T]}"}for A in D]})
|
|
323
|
+
elif A[_F]==_S:
|
|
324
|
+
for C in A[_C]:
|
|
325
|
+
if C[_A]==_D:B.append({_A:_a,_F:_S,_C:[{_A:'output_text',_D:C[_D]}]})
|
|
326
|
+
elif C[_A]==_V:B.append({_A:_AN,_q:C[_J],_E:C[_E],_M:json.dumps(C[_M])})
|
|
327
|
+
elif A[_F]==_j:B.append({_A:'function_call_output',_q:A[_d],_O:A[_C][0][_D]if A[_C]else''})
|
|
328
|
+
return B
|
|
329
|
+
async def stream(B,messages:list[dict],system:str,tools:list[Tool])->EventStream:
|
|
330
|
+
D=tools;C=EventStream();E:dict[str,Any]={_s:B.model,'max_output_tokens':B.max_tokens,_N:B._build_input(messages,system),_A8:_k}
|
|
331
|
+
if B.temperature:E[_u]=B.temperature
|
|
332
|
+
if D:
|
|
333
|
+
E[_v]=[{_A:_W,_E:A.name,_AB:A.description,_AC:{_A:_AD,_e:A.parameters.model_json_schema().get(_e,{}),_X:A.parameters.model_json_schema().get(_X,[])}}for A in D];A=B.tool_choice
|
|
334
|
+
if A is not _B:E[_A9]={_A:_W,_E:A[_E]}if isinstance(A,dict)else A
|
|
335
|
+
T={_AM:f"Bearer {B.api_key}",_AE:_w};logger.debug(json.dumps(E,indent=2))
|
|
336
|
+
async def F()->_B:
|
|
337
|
+
A={_F:_S,_C:[],_K:_Z,_G:{_N:0,_O:0,_l:0,_m:0},_Y:_B,_x:int(time.time()*1000)}
|
|
338
|
+
try:
|
|
339
|
+
async with httpx.AsyncClient(timeout=12e1)as U:
|
|
340
|
+
async with U.stream(_y,f"{B.base_url}/v1/responses",json=E,headers=T)as H:
|
|
341
|
+
if H.status_code>=400:raise RuntimeError(f"HTTP {H.status_code}: {(await H.aread()).decode()}")
|
|
342
|
+
C.push({_A:_z,_H:{_L:A}});L='';F:dict[str,dict]={};M=_B
|
|
343
|
+
async for N in H.aiter_lines():
|
|
344
|
+
if not N.startswith(_A0):continue
|
|
345
|
+
O=N[6:].strip()
|
|
346
|
+
if not O:continue
|
|
347
|
+
D=json.loads(O);I=D.get(_A,'')
|
|
348
|
+
if I=='response.output_text.delta':P=D.get(_Q,'');L+=P;C.push({_A:_n,_H:{_Q:P,_L:A}})
|
|
349
|
+
elif I=='response.output_item.added':
|
|
350
|
+
J=D.get('item',{})
|
|
351
|
+
if J.get(_A)==_AN:G=J[_J];F[G]={_E:J.get(_E,''),_q:J.get(_q,G),_b:''}
|
|
352
|
+
elif I=='response.function_call_arguments.delta':
|
|
353
|
+
G=D.get('item_id','')
|
|
354
|
+
if G in F:F[G][_b]+=D.get(_Q,'')
|
|
355
|
+
elif I=='response.completed':
|
|
356
|
+
M=D.get(_AG,{}).get('status')
|
|
357
|
+
if(Q:=D.get(_AG,{}).get(_G)):A[_G][_N]=Q.get(_AJ,A[_G][_N]);A[_G][_O]=Q.get(_AK,A[_G][_O])
|
|
358
|
+
if L:A[_C].append({_A:_D,_D:L})
|
|
359
|
+
for K in F.values():
|
|
360
|
+
try:R=json.loads(K[_b])if K[_b]else{}
|
|
361
|
+
except json.JSONDecodeError:R={}
|
|
362
|
+
S={_A:_V,_J:K[_q],_E:K[_E],_M:R};A[_C].append(S);C.push({_A:_A3,_H:{_A4:S,_L:A}})
|
|
363
|
+
A[_K]=_c if F else _o if M=='incomplete'else _Z;C.push({_A:_A5,_H:{_A6:A[_K],_a:A}})
|
|
364
|
+
except Exception as V:A[_K]=_R;A[_Y]=str(V);C.push({_A:_R,_H:{_R:A}})
|
|
365
|
+
C.finish(A)
|
|
366
|
+
C._attach(asyncio.create_task(F()));return C
|
|
367
|
+
def resolve_api(api,*,model,api_key,base_url):
|
|
368
|
+
D=base_url;C=api_key;B=model;A=api
|
|
369
|
+
if not isinstance(A,str):return A
|
|
370
|
+
if A=='claude':return ClaudeChat(model=B,api_key=C,base_url=D)
|
|
371
|
+
if A=='gemini':return GeminiChat(model=B,api_key=C,base_url=D)
|
|
372
|
+
if A=='codex':return CodexChat(model=B,api_key=C,base_url=D)
|
|
373
|
+
return DefaultChat(model=B,api_key=C,base_url=D)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_J='commit'
|
|
3
|
+
_I='is_error'
|
|
4
|
+
_H='content'
|
|
5
|
+
_G='thinking_delta'
|
|
6
|
+
_F='type'
|
|
7
|
+
_E='text'
|
|
8
|
+
_D='\n'
|
|
9
|
+
_C=False
|
|
10
|
+
_B=True
|
|
11
|
+
_A=None
|
|
12
|
+
import asyncio,json,logging,select,signal,sys,threading
|
|
13
|
+
from typing import Callable
|
|
14
|
+
from.repl import Agent,TabModel,CommandEngine,UIAdapter,HELP_TEXT,format_exit_summary
|
|
15
|
+
logger=logging.getLogger(__name__)
|
|
16
|
+
_INTERRUPTED=object()
|
|
17
|
+
class BareAdapter(UIAdapter):
|
|
18
|
+
def __init__(A,repl:'BareRepl'):A.repl=repl
|
|
19
|
+
def show_error(A,text:str)->_A:print(f"\n✗ {text}",flush=_B)
|
|
20
|
+
def show_command_result(B,cmd:str,content:str|object)->_A:
|
|
21
|
+
A=content
|
|
22
|
+
if isinstance(A,str):print(A)
|
|
23
|
+
else:print(str(A))
|
|
24
|
+
def show_diff(A,diff_text:str,ref1_label:str,ref2_label:str)->_A:print(diff_text)
|
|
25
|
+
def show_history_list(A,lines:list[str])->_A:print(_D.join(lines))
|
|
26
|
+
def show_history_raw(A,json_text:str)->_A:print(json_text)
|
|
27
|
+
async def add_tab(A,tab:TabModel)->_A:0
|
|
28
|
+
def remove_tab(A,removed_index:int)->_A:
|
|
29
|
+
if A.repl.engine.active_index>=len(A.repl.engine.tabs):A.repl.engine.active_index=len(A.repl.engine.tabs)-1
|
|
30
|
+
def switch_to_tab(A,index:int)->_A:A.repl._render_tab_delimiter();A.repl._print_history_for_tab(A.repl.engine.tabs[index])
|
|
31
|
+
def refresh_chrome(A)->_A:0
|
|
32
|
+
def clear_tab_display(A,tab:TabModel)->_A:0
|
|
33
|
+
def clear_ephemeral(A,tab:TabModel)->_A:0
|
|
34
|
+
def on_agent_event(A,event:dict,tab:TabModel)->_A:
|
|
35
|
+
if tab is not A.repl.engine.active_tab:return
|
|
36
|
+
A.repl._handle_event(event,tab)
|
|
37
|
+
async def run_interactive_shell(B,command:str,cwd:str,env:dict|_A)->int:A=await asyncio.create_subprocess_shell(command,cwd=cwd,stdin=_A,stdout=_A,stderr=_A,env=env);await A.wait();return A.returncode or 0
|
|
38
|
+
def exit_app(B,summary:list[dict])->_A:
|
|
39
|
+
A=format_exit_summary(summary)
|
|
40
|
+
if A:print(A)
|
|
41
|
+
B.repl._should_exit=_B
|
|
42
|
+
def peek_tree_text(A,tab:TabModel)->str:return'Peek is not supported in bare mode.'
|
|
43
|
+
async def peek_open(A,tab:TabModel,path:list[Agent])->_A:0
|
|
44
|
+
def peek_close(A,tab:TabModel,agent_id:str|_A)->bool:return _C
|
|
45
|
+
class BareRepl:
|
|
46
|
+
def __init__(A,engine:CommandEngine,init_prompt:str|_A=_A):A.engine=engine;A.adapter=BareAdapter(A);A.engine.bind(A.adapter);A.engine.attach_agent(A.engine.tabs[0]);A.init_prompt=init_prompt;(A._pending_nls):int=0;(A._awaiting_content):bool=_C;(A._has_output):bool=_C;(A._last_stream_type):str|_A=_A;A._should_exit=_C;A._interrupted=threading.Event()
|
|
47
|
+
async def run(A)->_A:
|
|
48
|
+
C=asyncio.get_running_loop()
|
|
49
|
+
if A.init_prompt:D,A.init_prompt=A.init_prompt,_A;await A.engine.handle_input(D);await A._await_active_task(C)
|
|
50
|
+
while _B:
|
|
51
|
+
if A._should_exit:break
|
|
52
|
+
B=await A._read_input_interruptible(C)
|
|
53
|
+
if B is _INTERRUPTED:continue
|
|
54
|
+
if B is _A:print();break
|
|
55
|
+
B=B.strip()
|
|
56
|
+
if not B:continue
|
|
57
|
+
if B.lower()in{'exit','quit'}:break
|
|
58
|
+
await A.engine.handle_input(B)
|
|
59
|
+
if A._should_exit:break
|
|
60
|
+
await A._await_active_task(C)
|
|
61
|
+
async def _read_input_interruptible(B,loop:asyncio.AbstractEventLoop):
|
|
62
|
+
A=loop
|
|
63
|
+
def D():B._interrupted.set()
|
|
64
|
+
C=_C
|
|
65
|
+
try:A.add_signal_handler(signal.SIGINT,D);C=_B
|
|
66
|
+
except(NotImplementedError,RuntimeError):pass
|
|
67
|
+
try:return await A.run_in_executor(_A,B._read_input)
|
|
68
|
+
finally:
|
|
69
|
+
if C:A.remove_signal_handler(signal.SIGINT)
|
|
70
|
+
async def _await_active_task(C,loop:asyncio.AbstractEventLoop)->_A:
|
|
71
|
+
A=C.engine.active_tab
|
|
72
|
+
if A.running_task is _A:return
|
|
73
|
+
D=A.agent
|
|
74
|
+
def E()->_A:D.abort();print('\n(aborting…)')
|
|
75
|
+
B=_C
|
|
76
|
+
try:loop.add_signal_handler(signal.SIGINT,E);B=_B
|
|
77
|
+
except(NotImplementedError,RuntimeError):pass
|
|
78
|
+
try:await A.running_task
|
|
79
|
+
except asyncio.CancelledError:pass
|
|
80
|
+
finally:
|
|
81
|
+
if B:loop.remove_signal_handler(signal.SIGINT)
|
|
82
|
+
def _read_input(A):
|
|
83
|
+
G=A.engine.active_tab;D=f"[{G.title}] ≫ ";B:list[str]=[]
|
|
84
|
+
while _B:
|
|
85
|
+
sys.stdout.write(D);sys.stdout.flush()
|
|
86
|
+
while _B:
|
|
87
|
+
if A._interrupted.is_set():A._interrupted.clear();print();return _INTERRUPTED
|
|
88
|
+
try:E,H,H=select.select([sys.stdin],[],[],.1)
|
|
89
|
+
except(OSError,ValueError):E=[sys.stdin]
|
|
90
|
+
if E:break
|
|
91
|
+
F=sys.stdin.readline()
|
|
92
|
+
if F=='':return
|
|
93
|
+
C=F.rstrip(_D);B.append(C)
|
|
94
|
+
if C.endswith('\\'):B[-1]=C[:-1];D='... '
|
|
95
|
+
else:break
|
|
96
|
+
return _D.join(B)
|
|
97
|
+
def _handle_event(A,event:dict,tab:TabModel)->_A:
|
|
98
|
+
K='error';H=event;B,C=H[_F],H.get('payload',{})
|
|
99
|
+
if B in('text_delta',_G):
|
|
100
|
+
I=C.get('delta','')
|
|
101
|
+
if I:A._write_delta(I,B)
|
|
102
|
+
elif B=='tool_start':A._pending_nls=0;A._awaiting_content=_C;A._has_output=_B;A._last_stream_type=B
|
|
103
|
+
elif B=='tool_end':
|
|
104
|
+
L=C.get('result',{});E=L.get(_H);M=C.get(_I,_C);D=''
|
|
105
|
+
if E:
|
|
106
|
+
F:list[str]=[]
|
|
107
|
+
if isinstance(E,str):F.append(E)
|
|
108
|
+
elif isinstance(E,list):
|
|
109
|
+
for G in E:
|
|
110
|
+
if isinstance(G,dict)and G.get(_F)==_E:F.append(G.get(_E,''))
|
|
111
|
+
D=_D.join(F).strip(_D)
|
|
112
|
+
if M:
|
|
113
|
+
J='✗ '
|
|
114
|
+
if not D:D=f"{C.get("name","?")} failed"
|
|
115
|
+
else:J='→ 'if D else''
|
|
116
|
+
if D:A._write_delta(J+D,'tool_result')
|
|
117
|
+
A._last_stream_type=B;print()
|
|
118
|
+
elif B==_J:A._pending_nls=0;A._awaiting_content=_C;A._has_output=_B;print(f"\n◇ [{C.get("sha","")}] committed",flush=_B);A._last_stream_type=B
|
|
119
|
+
elif B==K:A._pending_nls=0;A._awaiting_content=_C;A._has_output=_B;N=str(C.get(K,C));print(f"\n✗ {N}",flush=_B);A._last_stream_type=B
|
|
120
|
+
elif B in('agent_start','turn_start'):A._pending_nls=0;A._awaiting_content=_C;A._has_output=_C;A._last_stream_type=_A
|
|
121
|
+
elif B=='agent_end':
|
|
122
|
+
A._pending_nls=0
|
|
123
|
+
if A._has_output:print()
|
|
124
|
+
A._last_stream_type=_A;A._has_output=_C;A._awaiting_content=_C
|
|
125
|
+
def _write_delta(A,text:str,delta_type:str)->_A:
|
|
126
|
+
D=delta_type;B=text
|
|
127
|
+
if D!=A._last_stream_type:A._pending_nls=0;A._awaiting_content=_B;A._last_stream_type=D
|
|
128
|
+
if A._awaiting_content:
|
|
129
|
+
B=B.lstrip(_D)
|
|
130
|
+
if not B:return
|
|
131
|
+
if A._awaiting_content:
|
|
132
|
+
if A._has_output:print()
|
|
133
|
+
A._awaiting_content=_C
|
|
134
|
+
if not A._awaiting_content and A._pending_nls>0:print(_D*A._pending_nls,end='',flush=_B);A._pending_nls=0
|
|
135
|
+
C=B.rstrip(_D)
|
|
136
|
+
if C:
|
|
137
|
+
if D==_G:print(f"[2m{C}[0m",end='',flush=_B)
|
|
138
|
+
else:print(C,end='',flush=_B)
|
|
139
|
+
A._has_output=_B
|
|
140
|
+
A._pending_nls=len(B)-len(C)
|
|
141
|
+
def _render_tab_delimiter(C)->_A:
|
|
142
|
+
A:list[str]=[]
|
|
143
|
+
for(B,D)in enumerate(C.engine.tabs):
|
|
144
|
+
if B==C.engine.active_index:A.append(f"[1m▶ {B+1}. {D.title}[0m")
|
|
145
|
+
else:A.append(f"[2m▷ {B+1}. {D.title}[0m")
|
|
146
|
+
print(_D+'┗━━┫ '+' ┃ '.join(A)+' ┃')
|
|
147
|
+
def _print_history_for_tab(B,tab:TabModel)->_A:
|
|
148
|
+
A=tab
|
|
149
|
+
for C in A.agent.messages:B._print_message(C)
|
|
150
|
+
if A.agent.stream is not _A:B._print_message(A.agent.stream)
|
|
151
|
+
def _print_message(O,msg:dict)->_A:
|
|
152
|
+
M='thinking';L='toolResult';F=msg;C=F.get('role','');D=F.get(_H,'');I=F.get(_I,_C)
|
|
153
|
+
if isinstance(D,list):G=D
|
|
154
|
+
elif isinstance(D,str):G=[{_F:_E,_E:D}]
|
|
155
|
+
else:return
|
|
156
|
+
if C==L:
|
|
157
|
+
H:list[str]=[]
|
|
158
|
+
for A in G:
|
|
159
|
+
if isinstance(A,dict)and A.get(_F)==_E:
|
|
160
|
+
J=A.get(_E,'').strip(_D)
|
|
161
|
+
if J:H.append(J)
|
|
162
|
+
if H:N='✗ 'if I else'→ ';print(N+_D.join(H))
|
|
163
|
+
return
|
|
164
|
+
for A in G:
|
|
165
|
+
K=A.get(_F,_E)
|
|
166
|
+
if K=='toolCall':
|
|
167
|
+
E=A.get('arguments',{})
|
|
168
|
+
if isinstance(E,dict):E=json.dumps(E,ensure_ascii=_C)
|
|
169
|
+
print(f"⚙ {A.get("name","")} {E}");continue
|
|
170
|
+
B=A.get(_E,'')or A.get(M,'')or'';B=B.strip(_D)
|
|
171
|
+
if not B:continue
|
|
172
|
+
if K==M:print(f"[2m{B}[0m")
|
|
173
|
+
elif I:print(f"✗ {B}")
|
|
174
|
+
elif C=='user':print(f"≫ {B}")
|
|
175
|
+
elif C==_J:print(f"◇ {B}")
|
|
176
|
+
elif C==L:print(f"→ {B}")
|
|
177
|
+
else:print(B)
|