FazBrowse GitHub Viewer
|
Trending
|
URL:
|
Home
Tools:
[Download Repo ZIP]
[View Raw Code]
[Original HTTPS Page]
cecli/cecli/coders/agent_coder.py at main · cecli-dev/cecli · GitHub
cecli-dev
cecli
Repository navigation
Code
Issues
13
(13)
Pull requests
8
(8)
Actions
Projects
Security and quality
Insights
Expand file tree
Breadcrumbs
cecli
/
cecli
/
coders
/
agent_coder.py
Copy path
More file actions
More file actions
Latest commit
History
History
History
2039 lines (1799 loc) · 86.5 KB
Breadcrumbs
cecli
/
cecli
/
coders
/
agent_coder.py
Copy path
File metadata and controls
2039 lines (1799 loc) · 86.5 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import
asyncio
import
base64
import
json
import
locale
import
logging
import
os
import
platform
import
random
import
time
import
traceback
from
collections
import
defaultdict
from
datetime
import
datetime
from
pathlib
import
Path
import
xxhash
from
cecli
.
args
import
AGENT_CONFIG_LIST_FIELDS
from
cecli
.
change_tracker
import
ChangeTracker
from
cecli
.
helpers
import
nested
,
responses
from
cecli
.
helpers
.
agents
.
service
import
AgentService
from
cecli
.
helpers
.
background_commands
import
BackgroundCommandManager
from
cecli
.
helpers
.
conversation
import
ConversationService
,
MessageTag
from
cecli
.
helpers
.
similarity
import
(
cosine_similarity
,
create_bigram_vector
,
normalize_vector
,
)
from
cecli
.
helpers
.
skills
import
SkillsManager
from
cecli
.
hooks
import
HookIntegration
from
cecli
.
mcp
import
LocalServer
,
McpServerManager
from
cecli
.
tools
.
utils
.
base_tool
import
BaseTool
from
cecli
.
tools
.
utils
.
registry
import
ToolRegistry
from
cecli
.
utils
import
copy_tool_call
,
tool_call_to_dict
from
.
base_coder
import
Coder
from
cecli
.
helpers
.
coroutines
import
interruptible
# isort:skip
logger
=
logging
.
getLogger
(
__name__
)
class
AgentCoder
(
Coder
):
"""Mode where the LLM autonomously manages which files are in context."""
edit_format
=
"agent"
prompt_format
=
"agent"
context_management_enabled
=
True
hashlines
=
True
stop_on_empty
=
False
def
__init__
(
self
,
*
args
,
**
kwargs
):
if
kwargs
.
get
(
"uuid"
,
None
):
self
.
uuid
=
kwargs
.
get
(
"uuid"
)
self
.
start_up_errors
=
[]
self
.
recently_removed
=
{}
self
.
tool_usage_history
=
[]
self
.
loaded_custom_tools
=
[]
self
.
tool_usage_retries
=
20
self
.
last_round_tools
=
[]
self
.
tool_call_vectors
=
[]
self
.
tool_similarity_threshold
=
0.95
self
.
max_tool_vector_history
=
20
self
.
read_tools
=
{
"command"
,
"explorecode"
,
"ls"
,
"readfile"
,
"grep"
,
"thinking"
,
"updatetodolist"
,
"searchfacts"
,
}
self
.
write_tools
=
{
"command"
,
"editfile"
,
"undochange"
,
"replacefacts"
,
}
self
.
edit_allowed
=
True
self
.
max_tool_calls
=
10000
self
.
large_file_token_threshold
=
8192
self
.
skills_manager
=
None
self
.
change_tracker
=
ChangeTracker
()
self
.
args
=
kwargs
.
get
(
"args"
)
self
.
files_added_in_exploration
=
set
()
self
.
file_read_cache
=
set
()
self
.
tool_call_count
=
0
self
.
turn_count
=
0
self
.
use_enhanced_context
=
True
self
.
_last_edited_file
=
None
self
.
_cur_message_divider
=
None
self
.
allowed_context_blocks
=
set
()
self
.
context_block_tokens
=
{}
self
.
context_blocks_cache
=
{}
self
.
current_tasks
=
[]
self
.
hot_reload_enabled
=
False
self
.
tokens_calculated
=
False
self
.
skip_cli_confirmations
=
False
self
.
agent_finished
=
False
init_metadata
=
kwargs
.
pop
(
"init_metadata"
, {})
self
.
agent_config
=
self
.
_get_agent_config
(
config
=
init_metadata
)
self
.
max_sub_agents
=
self
.
agent_config
.
get
(
"max_sub_agents"
,
30
)
self
.
sub_agent_paths
=
self
.
agent_config
.
get
(
"subagent_paths"
, [])
self
.
_setup_agent
()
# primary_root is needed early so the skills and tool registries can
# resolve relative paths against the primary workspace root rather than
# this coder's (possibly overridden) local root. base_coder re-affirms
# and keeps this value in super().__init__().
self
.
primary_root
=
kwargs
.
get
(
"primary_root"
)
or
getattr
(
self
,
"primary_root"
,
None
)
# Allow a sub-agent to target a different working root (multi-project workspace).
if
not
kwargs
.
get
(
"root"
):
configured_root
=
self
.
agent_config
.
get
(
"root"
)
if
configured_root
:
kwargs
[
"root"
]
=
configured_root
self
.
_initialize_skills_manager
(
self
.
agent_config
,
root
=
kwargs
.
get
(
"root"
,
self
.
primary_root
)
)
AgentService
.
build_registry
(
self
.
sub_agent_paths
,
root
=
kwargs
.
get
(
"root"
,
self
.
primary_root
)
)
ToolRegistry
.
build_registry
(
agent_config
=
self
.
agent_config
,
root
=
kwargs
.
get
(
"root"
,
self
.
primary_root
)
)
self
.
loaded_custom_tools
=
ToolRegistry
.
loaded_custom_tools
super
().
__init__
(
*
args
,
**
kwargs
)
def
post_init
(
self
):
super
().
post_init
()
if
not
self
.
_inherited_tools
:
# Populate per-instance tool and server filters from config
self
.
registered_tools
[
"included"
]
=
set
(
map
(
str
.
lower
,
self
.
agent_config
.
get
(
"tools_includelist"
, []))
)
self
.
registered_tools
[
"excluded"
]
=
set
(
map
(
str
.
lower
,
self
.
agent_config
.
get
(
"tools_excludelist"
, []))
)
self
.
registered_servers
[
"included"
]
=
set
(
map
(
str
.
lower
,
self
.
agent_config
.
get
(
"servers_includelist"
, []))
)
self
.
registered_servers
[
"excluded"
]
=
set
(
map
(
str
.
lower
,
self
.
agent_config
.
get
(
"servers_excludelist"
, []))
)
for
err
in
self
.
start_up_errors
:
self
.
io
.
tool_warning
(
err
)
self
.
start_up_errors
=
[]
# Preserve the original 10000 setting now that max tool calls is configurable
self
.
max_tool_calls
=
self
.
max_tool_calls
*
400
def
_setup_agent
(
self
):
os
.
makedirs
(
".cecli/temp"
,
exist_ok
=
True
)
def
_get_agent_config
(
self
,
config
=
None
):
"""
Parse and return agent configuration from args.agent_config.
Args:
config: Optional dict of metadata (e.g. sub-agent front matter)
that is merged on top of args.agent_config. Parameter
takes priority over args.agent_config values.
Returns:
dict: Agent configuration with defaults for missing values
"""
config
=
config
or
{}
# init_metadata provides agent-config directly, replacing args
if
"agent_config"
in
config
and
config
[
"agent_config"
]:
config
=
config
[
"agent_config"
]
# Primary agent: load from args.agent_config
elif
(
hasattr
(
self
,
"args"
)
and
self
.
args
and
hasattr
(
self
.
args
,
"agent_config"
)
and
self
.
args
.
agent_config
):
try
:
args_config
=
json
.
loads
(
self
.
args
.
agent_config
)
# Validate that array fields are lists, wrap scalars in lists
list_fields
=
AGENT_CONFIG_LIST_FIELDS
for
field
in
list_fields
:
if
field
in
args_config
:
if
args_config
[
field
]
is
None
:
# YAML null / JSON null -> treat as empty list
args_config
[
field
]
=
[]
elif
not
isinstance
(
args_config
[
field
],
list
):
self
.
start_up_errors
.
append
(
f"agent-config field '
{
field
}
' should be a list but got "
f"
{
type
(
args_config
[
field
]).
__name__
}
, wrapping in list"
)
args_config
[
field
]
=
[
args_config
[
field
]]
# Merge metadata (caller-provided config) on top of
# args.agent_config so sub-agent front matter takes priority.
args_config
.
update
(
config
)
config
=
args_config
except
(
json
.
JSONDecodeError
,
TypeError
)
as
e
:
self
.
start_up_errors
.
append
(
f"Failed to parse agent-config JSON:
{
e
}
"
)
return
{}
config
[
"large_file_token_threshold"
]
=
nested
.
getter
(
config
,
"large_file_token_threshold"
,
8192
)
config
[
"show_lint_errors"
]
=
nested
.
getter
(
config
,
"show_lint_errors"
,
False
)
config
[
"skip_cli_confirmations"
]
=
nested
.
getter
(
config
,
"skip_cli_confirmations"
,
nested
.
getter
(
config
,
"yolo"
, [])
)
config
[
"command_timeout"
]
=
nested
.
getter
(
config
,
"command_timeout"
,
30
)
config
[
"allowed_commands"
]
=
nested
.
getter
(
config
,
"allowed_commands"
, [])
config
[
"hot_reload"
]
=
nested
.
getter
(
config
,
"hot_reload"
,
False
)
config
[
"diff_colors"
]
=
nested
.
getter
(
config
,
"diff_colors"
,
True
)
config
[
"allow_nested_delegation"
]
=
nested
.
getter
(
config
,
"allow_nested_delegation"
,
False
)
config
[
"allow_orchestration"
]
=
nested
.
getter
(
config
,
"allow_orchestration"
,
True
)
config
[
"max_sub_agents"
]
=
nested
.
getter
(
config
,
"max_sub_agents"
,
30
)
if
config
[
"max_sub_agents"
]
==
-
1
:
config
[
"max_sub_agents"
]
=
2
^
31
-
1
config
[
"tools_paths"
]
=
nested
.
getter
(
config
, [
"tools_paths"
,
"tool_paths"
], [])
config
[
"tools_includelist"
]
=
nested
.
getter
(
config
, [
"tools_includelist"
,
"tools_whitelist"
], []
)
config
[
"tools_excludelist"
]
=
nested
.
getter
(
config
,
[
"tools_excludelist"
,
"tools_blacklist"
],
[
"gitbranch"
,
"gitdiff"
,
"gitlog"
,
"gitremote"
,
"gitshow"
,
"gitstatus"
,
"searchfacts"
,
"replacefacts"
,
],
)
if
config
[
"tools_includelist"
]:
config
[
"tools_includelist"
].
append
(
"yield"
)
config
[
"servers_includelist"
]
=
nested
.
getter
(
config
, [
"servers_includelist"
,
"servers_whitelist"
], []
)
config
[
"servers_excludelist"
]
=
nested
.
getter
(
config
, [
"servers_excludelist"
,
"servers_blacklist"
], []
)
config
[
"include_context_blocks"
]
=
set
(
nested
.
getter
(
config
,
"include_context_blocks"
,
{
# "context_summary",
# "directory_structure",
"environment_info"
,
# "git_status",
# "symbol_outline",
"todo_list"
,
"sub_agents"
,
"skills"
,
"servers"
,
},
)
)
config
[
"exclude_context_blocks"
]
=
set
(
nested
.
getter
(
config
,
"exclude_context_blocks"
, []))
if
config
[
"allow_orchestration"
]:
config
[
"include_context_blocks"
].
add
(
"orchestration"
)
config
[
"exclude_context_blocks"
].
discard
(
"orchestration"
)
else
:
config
[
"include_context_blocks"
].
discard
(
"orchestration"
)
config
[
"exclude_context_blocks"
].
add
(
"orchestration"
)
config
[
"tools_excludelist"
].
append
(
"orchestrate"
)
self
.
large_file_token_threshold
=
config
[
"large_file_token_threshold"
]
self
.
skip_cli_confirmations
=
config
[
"skip_cli_confirmations"
]
self
.
hot_reload_enabled
=
config
[
"hot_reload"
]
self
.
allowed_context_blocks
=
config
[
"include_context_blocks"
]
for
context_block
in
config
[
"exclude_context_blocks"
]:
try
:
self
.
allowed_context_blocks
.
remove
(
context_block
)
except
KeyError
:
pass
if
"skills"
in
self
.
allowed_context_blocks
:
config
[
"skills_paths"
]
=
nested
.
getter
(
config
,
"skills_paths"
, [])
config
[
"skills_includelist"
]
=
nested
.
getter
(
config
, [
"skills_includelist"
,
"skills_whitelist"
], []
)
config
[
"skills_excludelist"
]
=
nested
.
getter
(
config
, [
"skills_excludelist"
,
"skills_blacklist"
], []
)
config
[
"skills_init"
]
=
nested
.
getter
(
config
, [
"skills_init"
,
"skills_startup"
], [])
if
"skills"
not
in
self
.
allowed_context_blocks
or
not
nested
.
getter
(
config
,
"skills_paths"
, []
):
config
[
"tools_excludelist"
].
append
(
"loadskill"
)
config
[
"tools_excludelist"
].
append
(
"removeskill"
)
return
config
def
_initialize_skills_manager
(
self
,
config
,
root
=
None
):
"""
Initialize the skills manager with the configured directory paths and filters.
"""
try
:
self
.
skills_manager
=
SkillsManager
(
directory_paths
=
config
.
get
(
"skills_paths"
, []),
include_list
=
config
.
get
(
"skills_includelist"
, []),
exclude_list
=
config
.
get
(
"skills_excludelist"
, []),
initialize_list
=
config
.
get
(
"skills_init"
, []),
root
=
root
or
self
.
root
or
self
.
primary_root
,
coder
=
self
,
)
except
Exception
as
e
:
self
.
io
.
tool_warning
(
f"Failed to initialize skills manager:
{
str
(
e
)
}
"
)
def
show_announcements
(
self
):
super
().
show_announcements
()
if
self
.
loaded_custom_tools
:
self
.
io
.
tool_output
(
f"Loaded custom tools:
{
', '
.
join
(
self
.
loaded_custom_tools
)
}
"
)
def
get_local_tool_schemas
(
self
):
"""Returns the JSON schemas for all local tools using the tool registry."""
schemas
=
[]
for
tool_name
in
ToolRegistry
.
get_registered_tools
():
tool_module
=
ToolRegistry
.
get_tool
(
tool_name
)
if
hasattr
(
tool_module
,
"SCHEMA"
):
schemas
.
append
(
tool_module
.
SCHEMA
)
return
schemas
async
def
initialize_mcp_tools
(
self
):
if
not
self
.
mcp_manager
:
verbose
=
getattr
(
self
.
args
,
"verbose"
,
False
)
if
self
.
args
else
False
self
.
mcp_manager
=
McpServerManager
([],
self
.
io
,
verbose
)
server_name
=
"Local"
server
=
self
.
mcp_manager
.
get_server
(
server_name
)
# We have already initialized local server and its connected
# then no need to duplicate work
if
server
is
not
None
and
server
.
is_connected
:
return
# If we dont have any tools for local server to use, no point in creating it then
local_tools
=
self
.
get_local_tool_schemas
()
if
not
local_tools
:
return
if
server
is
None
:
local_server_config
=
{
"name"
:
server_name
}
local_server
=
LocalServer
(
local_server_config
)
await
self
.
mcp_manager
.
add_server
(
local_server
,
connect
=
True
)
else
:
await
self
.
mcp_manager
.
connect_server
(
server_name
)
async
def
_execute_mcp_tool
(
self
,
server
,
tool_name
,
params
):
"""Helper to execute a single MCP tool call, created from legacy format."""
from
cecli
.
tools
.
utils
.
responses
import
ToolResponse
async
def
_exec_async
():
function_dict
=
{
"name"
:
tool_name
,
"arguments"
:
json
.
dumps
(
params
)}
tool_call_dict
=
{
"id"
:
f"mcp-tool-call-
{
time
.
time
()
}
"
,
"function"
:
function_dict
,
"type"
:
"function"
,
}
try
:
session
=
await
server
.
connect
()
call_result
=
await
self
.
call_mcp_tool_from_session
(
session
,
tool_call_dict
)
except
Exception
as
e
:
if
server
.
is_session_expired_error
(
e
):
try
:
session
=
await
server
.
reconnect
()
call_result
=
await
self
.
call_mcp_tool_from_session
(
session
,
tool_call_dict
)
except
Exception
as
retry_exc
:
self
.
io
.
tool_warning
(
f"Executing
{
tool_name
}
on
{
server
.
name
}
failed after reconnect:
\n
"
f"Error:
{
retry_exc
}
"
)
return
f"Error executing tool call
{
tool_name
}
:
{
retry_exc
}
"
else
:
self
.
io
.
tool_warning
(
f"Executing
{
tool_name
}
on
{
server
.
name
}
failed:
\n
Error:
{
e
}
"
)
return
f"Error executing tool call
{
tool_name
}
:
{
e
}
"
try
:
content_parts
=
[]
if
call_result
.
content
:
for
item
in
call_result
.
content
:
if
hasattr
(
item
,
"resource"
):
resource
=
item
.
resource
if
hasattr
(
resource
,
"text"
):
content_parts
.
append
(
resource
.
text
)
elif
hasattr
(
resource
,
"blob"
):
try
:
decoded_blob
=
base64
.
b64decode
(
resource
.
blob
).
decode
(
"utf-8"
)
content_parts
.
append
(
decoded_blob
)
except
(
UnicodeDecodeError
,
TypeError
):
name
=
getattr
(
resource
,
"name"
,
"unnamed"
)
mime_type
=
getattr
(
resource
,
"mimeType"
,
"unknown mime type"
)
content_parts
.
append
(
f"[embedded binary resource:
{
name
}
(
{
mime_type
}
)]"
)
elif
hasattr
(
item
,
"text"
):
content_parts
.
append
(
item
.
text
)
return
""
.
join
(
content_parts
)
except
Exception
as
e
:
self
.
io
.
tool_warning
(
f"Executing
{
tool_name
}
on
{
server
.
name
}
failed:
\n
Error:
{
e
}
"
)
return
f"Error executing tool call
{
tool_name
}
:
{
e
}
"
result
,
interrupted
=
await
interruptible
(
_exec_async
(),
self
.
interrupt_event
)
response
=
ToolResponse
(
tool_name
)
if
interrupted
:
response
.
append_error
(
"Tool execution interrupted by user."
)
elif
isinstance
(
result
,
str
)
and
result
.
startswith
(
"Error executing tool call"
):
response
.
append_error
(
result
)
else
:
response
.
append_result
(
result
)
return
response
def
_calculate_context_block_tokens
(
self
,
force
=
False
):
"""
Calculate token counts for all enhanced context blocks.
This is the central method for calculating token counts,
ensuring they're consistent across all parts of the code.
This method populates the cache for context blocks and calculates tokens.
Args:
force: If True, recalculate tokens even if already calculated
"""
if
hasattr
(
self
,
"tokens_calculated"
)
and
self
.
tokens_calculated
and
not
force
:
return
self
.
context_block_tokens
=
{}
if
not
hasattr
(
self
,
"context_blocks_cache"
):
self
.
context_blocks_cache
=
{}
if
not
self
.
use_enhanced_context
:
return
try
:
self
.
context_blocks_cache
=
{}
block_types
=
[
"environment_info"
,
"directory_structure"
,
"git_status"
,
"symbol_outline"
,
"skills"
,
"servers"
,
"sub_agents"
,
"orchestration"
,
]
for
block_type
in
block_types
:
if
block_type
in
self
.
allowed_context_blocks
:
block_content
=
self
.
_generate_context_block
(
block_type
)
if
block_content
:
self
.
context_block_tokens
[
block_type
]
=
self
.
get_active_model
().
token_count
(
block_content
)
self
.
tokens_calculated
=
True
except
Exception
:
pass
def
_generate_context_block
(
self
,
block_name
):
"""
Generate a specific context block and cache it.
This is a helper method for get_cached_context_block.
"""
content
=
None
if
block_name
==
"environment_info"
:
content
=
self
.
get_environment_info
()
elif
block_name
==
"directory_structure"
:
content
=
self
.
get_directory_structure
()
elif
block_name
==
"git_status"
:
content
=
self
.
get_git_status
()
elif
block_name
==
"symbol_outline"
:
content
=
self
.
get_context_symbol_outline
()
elif
block_name
==
"context_summary"
:
content
=
self
.
get_context_summary
()
elif
block_name
==
"todo_list"
:
content
=
self
.
get_todo_list
()
elif
block_name
==
"skills"
:
content
=
self
.
get_skills_context
()
elif
block_name
==
"servers"
:
content
=
self
.
get_servers_context
()
elif
block_name
==
"orchestration"
:
content
=
self
.
get_orchestration_context
()
elif
block_name
==
"sub_agents"
and
(
not
self
.
parent_uuid
or
self
.
agent_config
.
get
(
"allow_nested_delegation"
,
False
)
):
content
=
self
.
get_sub_agents_context
()
if
content
is
not
None
:
self
.
context_blocks_cache
[
block_name
]
=
content
return
content
def
get_cached_context_block
(
self
,
block_name
):
"""
Get a context block from the cache, or generate it if not available.
This should be used by format_chat_chunks to avoid regenerating blocks.
This will ensure tokens are calculated if they haven't been yet.
"""
if
not
hasattr
(
self
,
"tokens_calculated"
)
or
not
self
.
tokens_calculated
:
self
.
_calculate_context_block_tokens
()
if
hasattr
(
self
,
"context_blocks_cache"
)
and
block_name
in
self
.
context_blocks_cache
:
return
self
.
context_blocks_cache
[
block_name
]
return
self
.
_generate_context_block
(
block_name
)
def
get_context_symbol_outline
(
self
):
"""
Generate a symbol outline for files currently in context using Tree-sitter,
bypassing the cache for freshness.
"""
if
not
self
.
use_enhanced_context
or
not
self
.
repo_map
:
return
None
try
:
result
=
'<context name="symbol_outline" from="agent">
\n
'
result
+=
"## Symbol Outline (Current Context)
\n
\n
"
result
+=
(
"Code definitions (classes, functions, methods, etc.) found in files currently in"
" chat context."
)
result
+=
"
\n
\n
"
files_to_outline
=
list
(
self
.
abs_fnames
)
+
list
(
self
.
abs_read_only_fnames
)
if
not
files_to_outline
:
result
+=
"No files currently in context.
\n
"
result
+=
"</context>"
return
result
all_tags_by_file
=
defaultdict
(
list
)
has_symbols
=
False
if
not
self
.
repo_map
:
self
.
io
.
tool_warning
(
"RepoMap not initialized, cannot generate symbol outline."
)
return
None
for
abs_fname
in
sorted
(
files_to_outline
):
rel_fname
=
self
.
get_rel_fname
(
abs_fname
)
try
:
tags
=
list
(
self
.
repo_map
.
get_tags_raw
(
abs_fname
,
rel_fname
))
if
tags
:
all_tags_by_file
[
rel_fname
].
extend
(
tags
)
has_symbols
=
True
except
Exception
as
e
:
self
.
io
.
tool_warning
(
f"Could not get symbols for
{
rel_fname
}
:
{
e
}
"
)
if
not
has_symbols
:
result
+=
"No symbols found in the current context files.
\n
"
else
:
for
rel_fname
in
sorted
(
all_tags_by_file
.
keys
()):
tags
=
sorted
(
all_tags_by_file
[
rel_fname
],
key
=
lambda
t
: (
t
.
line
,
t
.
name
))
definition_tags
=
[]
for
tag
in
tags
:
kind_to_check
=
tag
.
specific_kind
or
tag
.
kind
if
(
kind_to_check
and
kind_to_check
.
lower
()
in
self
.
repo_map
.
definition_kinds
):
definition_tags
.
append
(
tag
)
if
definition_tags
:
result
+=
f"###
{
rel_fname
}
\n
"
for
tag
in
definition_tags
:
line_info
=
(
f", lines
{
tag
.
start_line
+
1
}
-
{
tag
.
end_line
+
1
}
"
if
tag
.
line
>=
0
else
""
)
kind_to_check
=
tag
.
specific_kind
or
tag
.
kind
result
+=
f"-
{
tag
.
name
}
(
{
kind_to_check
}
{
line_info
}
)
\n
"
result
+=
"
\n
"
result
+=
"</context>"
return
result
.
strip
()
except
Exception
as
e
:
self
.
io
.
tool_error
(
f"Error generating symbol outline:
{
str
(
e
)
}
"
)
return
None
def
format_chat_chunks
(
self
):
"""
Override parent's format_chat_chunks to include enhanced context blocks with a
cleaner, more hierarchical structure for better organization.
Optimized for prompt caching by placing context blocks strategically:
1. Relatively static blocks (directory structure, environment info) before done_messages
2. Dynamic blocks (context summary, symbol outline, git status) after chat_files
This approach preserves prefix caching while providing fresh context information.
"""
if
not
self
.
use_enhanced_context
:
# Use parent's implementation which may use conversation system if flag is enabled
return
super
().
format_chat_chunks
()
# Choose appropriate fence based on file content
self
.
choose_fence
()
ConversationService
.
get_chunks
(
self
).
initialize_conversation_system
()
# Clean up ConversationFiles and remove corresponding messages
ConversationService
.
get_chunks
(
self
).
cleanup_files
()
# Add reminder message with list of readonly and editable files
ConversationService
.
get_chunks
(
self
).
add_file_list_reminder
()
# Add system messages (including examples and reminder)
# For sub-agents, use their specific system prompt via AgentService lookup
# For primary agents, use the default system messages flow
needs_system_prompts
=
True
if
hasattr
(
self
,
"parent_uuid"
)
and
self
.
parent_uuid
:
service
=
AgentService
.
get_instance
(
self
)
info
=
service
.
sub_agents
.
get
(
self
.
uuid
)
if
info
:
config
=
AgentService
.
get_registry
().
get
(
info
.
name
)
if
config
and
config
.
prompt
and
config
.
prompt
.
strip
():
ConversationService
.
get_chunks
(
self
).
add_system_message
(
config
.
prompt
)
needs_system_prompts
=
False
if
needs_system_prompts
:
ConversationService
.
get_chunks
(
self
).
add_system_messages
()
# Add static context blocks (priority 50 - between SYSTEM and EXAMPLES)
ConversationService
.
get_chunks
(
self
).
add_static_context_blocks
()
# Add rules messages
ConversationService
.
get_chunks
(
self
).
add_rules_messages
()
# Handle file messages using conversation module helper methods
# These methods will add messages to ConversationManager
ConversationService
.
get_chunks
(
self
).
add_repo_map_messages
()
# Add pre-message context blocks (priority 125 - between REPO and READONLY_FILES)
ConversationService
.
get_chunks
(
self
).
add_pre_message_context_blocks
()
ConversationService
.
get_chunks
(
self
).
add_active_skills_messages
(
getattr
(
self
,
"skills_manager"
,
None
)
)
ConversationService
.
get_chunks
(
self
).
add_readonly_files_messages
()
ConversationService
.
get_chunks
(
self
).
add_chat_files_messages
()
ConversationService
.
get_manager
(
self
).
flush_queue
()
# Add post-message context blocks (priority 250 - between CUR and REMINDER)
ConversationService
.
get_chunks
(
self
).
add_post_message_context_blocks
()
# Background command output is debounced independently so it is not
# re-dumped every turn as its contents mutate slightly
ConversationService
.
get_chunks
(
self
).
add_background_command_output
()
# Add sub-agent states context block (same priority as post-message blocks)
ConversationService
.
get_chunks
(
self
).
add_sub_agent_states
()
ConversationService
.
get_chunks
(
self
).
add_randomized_cta
()
return
ConversationService
.
get_manager
(
self
).
get_messages_dict
()
def
get_context_summary
(
self
):
"""
Generate a summary of the current context, including file content tokens and additional context blocks,
with an accurate total token count.
"""
if
not
self
.
use_enhanced_context
:
return
None
if
hasattr
(
self
,
"context_blocks_cache"
)
and
"context_summary"
in
self
.
context_blocks_cache
:
return
self
.
context_blocks_cache
[
"context_summary"
]
try
:
if
not
hasattr
(
self
,
"context_block_tokens"
)
or
not
self
.
context_block_tokens
:
self
.
_calculate_context_block_tokens
()
result
=
'<context name="context_summary" from="agent">
\n
'
result
+=
"## Current Context Overview
\n
\n
"
max_input_tokens
=
self
.
get_active_model
().
info
.
get
(
"max_input_tokens"
)
or
0
if
max_input_tokens
:
result
+=
f"Model context limit:
{
max_input_tokens
:,
}
tokens
\n
\n
"
total_file_tokens
=
0
editable_tokens
=
0
readonly_tokens
=
0
editable_files
=
[]
readonly_files
=
[]
if
self
.
abs_fnames
:
result
+=
"### Editable Files
\n
\n
"
for
fname
in
sorted
(
self
.
abs_fnames
):
rel_fname
=
self
.
get_rel_fname
(
fname
)
content
=
self
.
io
.
read_text
(
fname
)
if
content
is
not
None
:
tokens
=
self
.
get_active_model
().
token_count
(
content
)
total_file_tokens
+=
tokens
editable_tokens
+=
tokens
size_indicator
=
(
"Large"
if
tokens
>
5000
else
"Medium"
if
tokens
>
1000
else
"Small"
)
editable_files
.
append
(
f"-
{
rel_fname
}
:
{
tokens
:,
}
tokens (
{
size_indicator
}
)"
)
if
editable_files
:
result
+=
"
\n
"
.
join
(
editable_files
)
+
"
\n
\n
"
result
+=
(
f"**Total editable:
{
len
(
editable_files
)
}
files,"
f"
{
editable_tokens
:,
}
tokens**
\n
\n
"
)
else
:
result
+=
"No editable files in context
\n
\n
"
if
self
.
abs_read_only_fnames
:
result
+=
"### Read-Only Files
\n
\n
"
for
fname
in
sorted
(
self
.
abs_read_only_fnames
):
rel_fname
=
self
.
get_rel_fname
(
fname
)
content
=
self
.
io
.
read_text
(
fname
)
if
content
is
not
None
:
tokens
=
self
.
get_active_model
().
token_count
(
content
)
total_file_tokens
+=
tokens
readonly_tokens
+=
tokens
size_indicator
=
(
"Large"
if
tokens
>
5000
else
"Medium"
if
tokens
>
1000
else
"Small"
)
readonly_files
.
append
(
f"-
{
rel_fname
}
:
{
tokens
:,
}
tokens (
{
size_indicator
}
)"
)
if
readonly_files
:
result
+=
"
\n
"
.
join
(
readonly_files
)
+
"
\n
\n
"
result
+=
(
f"**Total read-only:
{
len
(
readonly_files
)
}
files,"
f"
{
readonly_tokens
:,
}
tokens**
\n
\n
"
)
else
:
result
+=
"No read-only files in context
\n
\n
"
extra_tokens
=
sum
(
self
.
context_block_tokens
.
values
())
total_tokens
=
total_file_tokens
+
extra_tokens
result
+=
f"**Total files usage:
{
total_file_tokens
:,
}
tokens**
\n
\n
"
result
+=
f"**Additional context usage:
{
extra_tokens
:,
}
tokens**
\n
\n
"
result
+=
f"**Total context usage:
{
total_tokens
:,
}
tokens**"
if
max_input_tokens
:
percentage
=
total_tokens
/
max_input_tokens
*
100
result
+=
f" (
{
percentage
:.1f
}
% of limit)"
if
percentage
>
80
:
result
+=
"
\n
\n
⚠ **Context is getting full!**
\n
"
result
+=
"- Remove non-essential files, skills and tool servers via the `ResourceManager` tool.
\n
"
result
+=
"- Keep only essential files, skills, and MCP servers in context for best performance"
result
+=
"
\n
</context>"
if
not
hasattr
(
self
,
"context_blocks_cache"
):
self
.
context_blocks_cache
=
{}
self
.
context_blocks_cache
[
"context_summary"
]
=
result
return
result
except
Exception
as
e
:
self
.
io
.
tool_error
(
f"Error generating context summary:
{
str
(
e
)
}
"
)
return
None
def
get_environment_info
(
self
):
"""
Generate an environment information context block with key system details.
Returns formatted string with working directory, platform, date, and other relevant environment details,
including the agent's own identity and the primary agent's UUID.
"""
if
not
self
.
use_enhanced_context
:
return
None
try
:
current_date
=
datetime
.
now
().
strftime
(
"%Y-%m-%d"
)
platform_info
=
platform
.
platform
()
language
=
self
.
chat_language
or
locale
.
getlocale
()[
0
]
or
"en-US"
result
=
'<context name="environment_info" from="agent">
\n
'
result
+=
"## Environment Information
\n
\n
"
result
+=
f"- Working directory:
{
self
.
root
}
\n
"
result
+=
f"- Current date:
{
current_date
}
\n
"
result
+=
f"- Platform:
{
platform_info
}
\n
"
result
+=
f"- Language preference:
{
language
}
\n
"
# Agent identity so the model can recognise itself and the primary
# agent. Kept here (a static context block) rather than in the
# sub-agent states block to avoid re-injecting an agent's own
# identity whenever sub-agent state changes mid-conversation.
service
=
AgentService
.
get_instance
(
self
)
self_uuid
=
str
(
self
.
uuid
)
primary_uuid
=
str
(
service
.
coder
.
uuid
)
self_name
=
service
.
get_agent_name
(
self
)
or
"primary"
result
+=
f"- Agent:
{
self_name
}
(
{
self_uuid
}
) [self]
\n
"
result
+=
f"- Primary agent: primary (
{
primary_uuid
}
)
\n
"
if
self
.
repo
:
try
:
rel_repo_dir
=
self
.
repo
.
get_rel_repo_dir
()
num_files
=
len
(
self
.
repo
.
get_tracked_files
())
result
+=
f"- Git repository:
{
rel_repo_dir
}
with
{
num_files
:,
}
files
\n
"
except
Exception
:
result
+=
"- Git repository: active but details unavailable
\n
"
if
self
.
mcp_manager
and
self
.
mcp_manager
.
connected_servers
:
num_mcp_servers
=
len
(
self
.
mcp_manager
.
connected_servers
)
result
+=
f"- Connected MCP servers:
{
num_mcp_servers
}
\n
"
result
+=
"- Git repository: none
\n
"
result
+=
"</context>"
return
result
except
Exception
as
e
:
self
.
io
.
tool_error
(
f"Error generating environment info:
{
str
(
e
)
}
"
)
return
None
async
def
process_tool_calls
(
self
,
tool_call_response
):
"""
Track tool usage before calling the base implementation.
"""
await
self
.
auto_save_session
()
self
.
last_round_tools
=
[]
self
.
turn_count
+=
1
if
self
.
partial_response_tool_calls
:
for
tool_call
in
self
.
partial_response_tool_calls
:
tool_name
=
getattr
(
tool_call
.
function
,
"name"
,
None
)
tool_call_copy
=
tool_call_to_dict
(
copy_tool_call
(
tool_call
))
if
"id"
in
tool_call_copy
:
del
tool_call_copy
[
"id"
]
if
tool_name
:
self
.
last_round_tools
.
append
(
tool_name
)
content
=
(
str
(
self
.
partial_response_content
)
if
self
.
partial_response_content
else
""
)
tool_call_str
=
str
(
tool_call_copy
)
tool_vector
=
create_bigram_vector
((
tool_call_str
,
content
))
tool_vector_norm
=
normalize_vector
(
tool_vector
)
self
.
tool_call_vectors
.
append
(
tool_vector_norm
)
if
self
.
last_round_tools
:
self
.
tool_usage_history
+=
self
.
last_round_tools
self
.
tool_usage_history
=
list
(
filter
(
None
,
self
.
tool_usage_history
))
if
len
(
self
.
tool_usage_history
)
>
self
.
tool_usage_retries
:
self
.
tool_usage_history
.
pop
(
0
)
if
len
(
self
.
tool_call_vectors
)
>
self
.
max_tool_vector_history
:
self
.
tool_call_vectors
.
pop
(
0
)
# Ensure we call base implementation to trigger execution of all tools (native + extracted)
return
await
super
().
process_tool_calls
(
tool_call_response
)
async
def
_execute_local_tools
(
self
,
tool_calls_list
):
"""Execute local tools via ToolRegistry."""
tool_responses
=
[]
used_write_tool
=
False
for
tool_call
in
tool_calls_list
:
tool_name
=
tool_call
.
function
.
name
result_message
=
""
self
.
io
.
tool_call_event
(
tool_call
.
id
,
"in_progress"
)
try
:
if
responses
.
unprefix_tool_name
(
tool_name
)[
1
].
lower
()
in
self
.
write_tools
:
used_write_tool
=
True
args_string
=
tool_call
.
function
.
arguments
.
strip
()
parsed_args_list
=
[]
if
not
await
HookIntegration
.
call_pre_tool_hooks
(
self
,
tool_name
,
args_string
):
tool_responses
.
append
(
{
"role"
:
"tool"
,
"tool_call_id"
:
tool_call
.
id
,
"content"
:
"Tool Request Aborted."
,
}
)
continue
if
args_string
:
parsed
=
responses
.
parse_tool_arguments
(
args_string
)
if
isinstance
(
parsed
,
dict
)
and
"@error"
in
parsed
:
self
.
io
.
tool_warning
(
f"Malformed JSON arguments in tool
{
tool_name
}
:
{
parsed
[
'@error'
]
}
"
)
tool_responses
.
append
(
{
"role"
:
"tool"
,
"tool_call_id"
:
tool_call
.
id
,
"content"
: (
f"Malformed JSON arguments in tool
{
tool_name
}
:
{
parsed
[
'@error'
]
}
"
),
}
)
continue
parsed_args_list
=
[
parsed
]
if
not
parsed_args_list
and
not
args_string
:
parsed_args_list
.
append
({})
all_results_content
=
[]
norm_tool_name
=
tool_name
.
lower
()
tasks
=
[]
if
norm_tool_name
in
ToolRegistry
.
get_registered_tools
():
tool_module
=
ToolRegistry
.
get_tool
(
norm_tool_name
)
for
params
in
parsed_args_list
:
result
=
tool_module
.
process_response
(
self
,
params
)
if
asyncio
.
iscoroutine
(
result
):
tasks
.
append
(
result
)
else
:
tasks
.
append
(
asyncio
.
to_thread
(
lambda
:
result
))
else
:
all_results_content
.
append
(
f"Error: Unknown tool name '
{
tool_name
}
'"
)
if
tasks
:
async
def
gather_and_await
():
return
await
asyncio
.
gather
(
*
tasks
,
return_exceptions
=
True
)
task_results
,
interrupted
=
await
interruptible
(
gather_and_await
(),
self
.
interrupt_event
)
if
interrupted
:
self
.
io
.
tool_warning
(
"Tool execution interrupted."
)
all_results_content
.
append
(
"Tool execution interrupted by user."
)
elif
task_results
:
for
res
in
task_results
:
if
isinstance
(
res
,
Exception
):
all_results_content
.
append
(
f"Error in tool execution:
{
res
}
"
)
else
:
all_results_content
.
append
(
str
(
res
))
if
not
await
HookIntegration
.
call_post_tool_hooks
(
self
,
tool_name
,
args_string
,
"
\n
\n
"
.
join
(
all_results_content
)
):
tool_responses
.
append
(
{
"role"
:
"tool"
,
"tool_call_id"
:
tool_call
.
id
,
"content"
:
"Tool Response Redacted."
,
}
)
continue
result_message
=
"
\n
\n
"
.
join
(
all_results_content
)
except
Exception
as
e
:
self
.
model_kwargs
=
{}
result_message
=
f"Error executing
{
tool_name
}
:
{
e
}
"
self
.
io
.
tool_error
(
f"Error during
{
tool_name
}
execution:
{
e
}
\n
{
traceback
.
format_exc
()
}
"
)
tool_responses
.
append
(
{
"role"
:
"tool"
,
"tool_call_id"
:
tool_call
.
id
,
"content"
:
result_message
}
)
if
self
.
auto_lint
and
used_write_tool
:
edited
=
list
(
self
.
files_edited_by_tools
)
lint_coro
=
self
.
lint_edited
(
edited
,
show_output
=
False
)
lint_errors
,
interrupted
=
await
interruptible
(
lint_coro
,
self
.
interrupt_event
)
if
interrupted
:
# Abort the turn with CancelledError, which the linear run loop
# handles by re-prompting for input. KeyboardInterrupt is a
# BaseException that would escape the worker's event loop and
# leave it unable to accept further prompts.
raise
asyncio
.
CancelledError
(
"Interrupted during linting"
)
has_errors
=
False
if
self
.
lint_outcome
is
False
:
has_errors
=
True
self
.
lint_outcome
=
not
lint_errors
if
lint_errors
:
lint_errors
=
lint_errors
.
replace
(
"# Fix any linting errors below, if possible."
,
"# Fix the linting errors below, and then continue with your task."
,
1
,
)
lint_hash
=
xxhash
.
xxh3_128_hexdigest
(
lint_errors
.
encode
(
"utf-8"
,
errors
=
"replace"
))
ConversationService
.
get_manager
(
self
).
add_message
(
message_dict
=
dict
(
role
=
"user"
,
content
=
lint_errors
),
tag
=
MessageTag
.
LINT
,
hash_key
=
(
"lint_errors"
,
"agent"
,
lint_hash
),
)
ConversationService
.
get_manager
(
self
).
queue_message
(
message_dict
=
dict
(
role
=
"user"
,
content
=
"Please address the latest linting errors."
),
tag
=
MessageTag
.
LINT
,
hash_key
=
(
"lint_errors"
,
"agent"
,
lint_hash
,
"cta"
),
promotion
=
ConversationService
.
get_manager
(
self
).
DEFAULT_TAG_PROMOTION_VALUE
,
mark_for_demotion
=
1
,
)
else
:
if
has_errors
:
ConversationService
.
get_manager
(
self
).
add_message
(
message_dict
=
dict
(
role
=
"user"
,
content
=
(
'<context name="linting_confirmation" from="agent">'
"All linting errors resolved."
"</context>"
),
),
tag
=
MessageTag
.
LINT
,
hash_key
=
(
"lint_errors"
,
"agent"
,
str
(
time
.
monotonic_ns
())),
)
return
tool_responses
async
def
_execute_mcp_tools
(
self
,
server
,
tool_calls
):
"""Execute MCP tools via LiteLLM."""
tool_responses
=
[]
for
tool_call
in
tool_calls
:
# Use existing _execute_mcp_tool logic
result
=
await
self
.
_execute_mcp_tool
(
server
,
View remainder of file in raw view
Back
|
FazBrowse Home
|
New Git URL