lite-agent 0.12.0__py3-none-any.whl → 0.14.0__py3-none-any.whl

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.

Potentially problematic release.


This version of lite-agent might be problematic. Click here for more details.

lite_agent/__init__.py CHANGED
@@ -1,8 +1,8 @@
1
1
  """Lite Agent - A lightweight AI agent framework."""
2
2
 
3
3
  from .agent import Agent
4
- from .chat_display import display_chat_summary, display_messages
4
+ from .chat_display import chat_summary_to_string, display_chat_summary, display_messages, messages_to_string
5
5
  from .message_transfers import consolidate_history_transfer
6
6
  from .runner import Runner
7
7
 
8
- __all__ = ["Agent", "Runner", "consolidate_history_transfer", "display_chat_summary", "display_messages"]
8
+ __all__ = ["Agent", "Runner", "chat_summary_to_string", "consolidate_history_transfer", "display_chat_summary", "display_messages", "messages_to_string"]
@@ -10,6 +10,7 @@ import json
10
10
  import time
11
11
  from collections.abc import Callable
12
12
  from datetime import datetime, timedelta, timezone
13
+ from io import StringIO
13
14
 
14
15
  try:
15
16
  from zoneinfo import ZoneInfo
@@ -46,6 +47,7 @@ class DisplayConfig:
46
47
  console: Console | None = None
47
48
  show_indices: bool = True
48
49
  show_timestamps: bool = True
50
+ show_metadata: bool = True
49
51
  max_content_length: int = 1000
50
52
  local_timezone: timezone | str | None = None
51
53
 
@@ -57,6 +59,7 @@ class MessageContext:
57
59
  console: Console
58
60
  index_str: str
59
61
  timestamp_str: str
62
+ show_metadata: bool
60
63
  max_content_length: int
61
64
  truncate_content: Callable[[str, int], str]
62
65
 
@@ -441,6 +444,7 @@ def display_messages(
441
444
  console=console,
442
445
  max_content_length=config.max_content_length,
443
446
  show_timestamp=config.show_timestamps,
447
+ show_metadata=config.show_metadata,
444
448
  local_timezone=local_timezone,
445
449
  )
446
450
 
@@ -452,6 +456,7 @@ def _display_single_message_compact(
452
456
  console: Console,
453
457
  max_content_length: int = 100,
454
458
  show_timestamp: bool = False,
459
+ show_metadata: bool = True,
455
460
  local_timezone: timezone | None = None,
456
461
  ) -> None:
457
462
  """以列式格式打印单个消息,类似 rich log。"""
@@ -469,7 +474,7 @@ def _display_single_message_compact(
469
474
  timestamp = _format_timestamp(message_time, local_timezone=local_timezone)
470
475
 
471
476
  # 创建列式显示
472
- _display_message_in_columns(message, console, index, timestamp, max_content_length, truncate_content)
477
+ _display_message_in_columns(message, console, index, timestamp, show_metadata=show_metadata, max_content_length=max_content_length, truncate_content=truncate_content)
473
478
 
474
479
 
475
480
  def _display_message_in_columns(
@@ -477,6 +482,8 @@ def _display_message_in_columns(
477
482
  console: Console,
478
483
  index: int | None,
479
484
  timestamp: str | None,
485
+ *,
486
+ show_metadata: bool,
480
487
  max_content_length: int,
481
488
  truncate_content: Callable[[str, int], str],
482
489
  ) -> None:
@@ -490,12 +497,9 @@ def _display_message_in_columns(
490
497
  if isinstance(message, NewUserMessage):
491
498
  _display_user_message_with_columns(message, console, time_str, index_str, max_content_length, truncate_content)
492
499
  elif isinstance(message, NewAssistantMessage):
493
- _display_assistant_message_with_columns(message, console, time_str, index_str, max_content_length, truncate_content)
500
+ _display_assistant_message_with_columns(message, console, time_str, index_str, show_metadata=show_metadata, max_content_length=max_content_length, truncate_content=truncate_content)
494
501
  elif isinstance(message, NewSystemMessage):
495
502
  _display_system_message_with_columns(message, console, time_str, index_str, max_content_length, truncate_content)
496
- else:
497
- # 处理旧格式消息
498
- _display_legacy_message_with_columns(message, console, time_str, index_str, max_content_length, truncate_content)
499
503
 
500
504
 
501
505
  def _display_user_message_with_columns(
@@ -523,27 +527,48 @@ def _display_user_message_with_columns(
523
527
  content = " ".join(content_parts)
524
528
  content = truncate_content(content, max_content_length)
525
529
 
526
- # 创建表格来确保对齐
530
+ # 创建表格来确保对齐,根据配置动态调整列宽
527
531
  table = Table.grid(padding=0)
528
- table.add_column(width=8, justify="left") # 时间列
529
- table.add_column(width=4, justify="left") # 序号列
532
+
533
+ # 只有在显示时间戳时才添加时间列
534
+ time_width = 8 if time_str.strip() else 0
535
+ if time_width > 0:
536
+ table.add_column(width=time_width, justify="left") # 时间列
537
+
538
+ # 只有在显示序号时才添加序号列
539
+ index_width = 4 if index_str.strip() else 0
540
+ if index_width > 0:
541
+ table.add_column(width=index_width, justify="left") # 序号列
542
+
530
543
  table.add_column(min_width=0) # 内容列
531
544
 
545
+ # 辅助函数:根据列数构建行
546
+ def build_table_row(*content_parts: str) -> tuple[str, ...]:
547
+ row_parts = []
548
+ if time_width > 0:
549
+ row_parts.append(content_parts[0] if len(content_parts) > 0 else "")
550
+ if index_width > 0:
551
+ row_parts.append(content_parts[1] if len(content_parts) > 1 else "")
552
+ row_parts.append(content_parts[-1] if content_parts else "") # 内容列总是最后一个
553
+ return tuple(row_parts)
554
+
532
555
  lines = content.split("\n")
533
556
  for i, line in enumerate(lines):
534
557
  if i == 0:
535
558
  # 第一行显示 User: 标签
536
559
  table.add_row(
537
- f"[dim]{time_str:8}[/dim]",
538
- f"[dim]{index_str:4}[/dim]",
539
- "[blue]User:[/blue]",
560
+ *build_table_row(
561
+ f"[dim]{time_str:8}[/dim]",
562
+ f"[dim]{index_str:4}[/dim]",
563
+ "[blue]User:[/blue]",
564
+ ),
540
565
  )
541
566
  # 如果有内容,添加内容行
542
567
  if line:
543
- table.add_row("", "", line)
568
+ table.add_row(*build_table_row("", "", line))
544
569
  else:
545
570
  # 续行只在内容列显示
546
- table.add_row("", "", line)
571
+ table.add_row(*build_table_row("", "", line))
547
572
 
548
573
  console.print(table)
549
574
 
@@ -559,24 +584,45 @@ def _display_system_message_with_columns(
559
584
  """使用列布局显示系统消息。"""
560
585
  content = truncate_content(message.content, max_content_length)
561
586
 
562
- # 创建表格来确保对齐
587
+ # 创建表格来确保对齐,根据配置动态调整列宽
563
588
  table = Table.grid(padding=0)
564
- table.add_column(width=8, justify="left") # 时间列
565
- table.add_column(width=4, justify="left") # 序号列
589
+
590
+ # 只有在显示时间戳时才添加时间列
591
+ time_width = 8 if time_str.strip() else 0
592
+ if time_width > 0:
593
+ table.add_column(width=time_width, justify="left") # 时间列
594
+
595
+ # 只有在显示序号时才添加序号列
596
+ index_width = 4 if index_str.strip() else 0
597
+ if index_width > 0:
598
+ table.add_column(width=index_width, justify="left") # 序号列
599
+
566
600
  table.add_column(min_width=0) # 内容列
567
601
 
602
+ # 辅助函数:根据列数构建行
603
+ def build_table_row(*content_parts: str) -> tuple[str, ...]:
604
+ row_parts = []
605
+ if time_width > 0:
606
+ row_parts.append(content_parts[0] if len(content_parts) > 0 else "")
607
+ if index_width > 0:
608
+ row_parts.append(content_parts[1] if len(content_parts) > 1 else "")
609
+ row_parts.append(content_parts[-1] if content_parts else "") # 内容列总是最后一个
610
+ return tuple(row_parts)
611
+
568
612
  lines = content.split("\n")
569
613
  for i, line in enumerate(lines):
570
614
  if i == 0:
571
615
  # 第一行显示完整信息
572
616
  table.add_row(
573
- f"[dim]{time_str:8}[/dim]",
574
- f"[dim]{index_str:4}[/dim]",
575
- f"[yellow]System:[/yellow] {line}",
617
+ *build_table_row(
618
+ f"[dim]{time_str:8}[/dim]",
619
+ f"[dim]{index_str:4}[/dim]",
620
+ f"[yellow]System:[/yellow] {line}",
621
+ ),
576
622
  )
577
623
  else:
578
624
  # 续行只在内容列显示
579
- table.add_row("", "", line)
625
+ table.add_row(*build_table_row("", "", line))
580
626
 
581
627
  console.print(table)
582
628
 
@@ -586,6 +632,8 @@ def _display_assistant_message_with_columns(
586
632
  console: Console,
587
633
  time_str: str,
588
634
  index_str: str,
635
+ *,
636
+ show_metadata: bool,
589
637
  max_content_length: int,
590
638
  truncate_content: Callable[[str, int], str],
591
639
  ) -> None:
@@ -605,7 +653,7 @@ def _display_assistant_message_with_columns(
605
653
 
606
654
  # 构建元信息
607
655
  meta_info = ""
608
- if message.meta:
656
+ if show_metadata and message.meta:
609
657
  meta_parts = []
610
658
  if message.meta.model is not None:
611
659
  meta_parts.append(f"Model:{message.meta.model}")
@@ -620,12 +668,31 @@ def _display_assistant_message_with_columns(
620
668
  if meta_parts:
621
669
  meta_info = f" [dim]({' | '.join(meta_parts)})[/dim]"
622
670
 
623
- # 创建表格来确保对齐
671
+ # 创建表格来确保对齐,根据配置动态调整列宽
624
672
  table = Table.grid(padding=0)
625
- table.add_column(width=8, justify="left") # 时间列
626
- table.add_column(width=4, justify="left") # 序号列
673
+
674
+ # 只有在显示时间戳时才添加时间列
675
+ time_width = 8 if time_str.strip() else 0
676
+ if time_width > 0:
677
+ table.add_column(width=time_width, justify="left") # 时间列
678
+
679
+ # 只有在显示序号时才添加序号列
680
+ index_width = 4 if index_str.strip() else 0
681
+ if index_width > 0:
682
+ table.add_column(width=index_width, justify="left") # 序号列
683
+
627
684
  table.add_column(min_width=0) # 内容列
628
685
 
686
+ # 辅助函数:根据列数构建行
687
+ def build_table_row(*content_parts: str) -> tuple[str, ...]:
688
+ row_parts = []
689
+ if time_width > 0:
690
+ row_parts.append(content_parts[0] if len(content_parts) > 0 else "")
691
+ if index_width > 0:
692
+ row_parts.append(content_parts[1] if len(content_parts) > 1 else "")
693
+ row_parts.append(content_parts[-1] if content_parts else "") # 内容列总是最后一个
694
+ return tuple(row_parts)
695
+
629
696
  # 处理文本内容
630
697
  first_row_added = False
631
698
  if text_parts:
@@ -636,24 +703,28 @@ def _display_assistant_message_with_columns(
636
703
  if i == 0:
637
704
  # 第一行显示 Assistant: 标签
638
705
  table.add_row(
639
- f"[dim]{time_str:8}[/dim]",
640
- f"[dim]{index_str:4}[/dim]",
641
- f"[green]Assistant:[/green]{meta_info}",
706
+ *build_table_row(
707
+ f"[dim]{time_str:8}[/dim]",
708
+ f"[dim]{index_str:4}[/dim]",
709
+ f"[green]Assistant:[/green]{meta_info}",
710
+ ),
642
711
  )
643
712
  # 如果有内容,添加内容行
644
713
  if line:
645
- table.add_row("", "", line)
714
+ table.add_row(*build_table_row("", "", line))
646
715
  first_row_added = True
647
716
  else:
648
717
  # 续行只在内容列显示
649
- table.add_row("", "", line)
718
+ table.add_row(*build_table_row("", "", line))
650
719
 
651
720
  # 如果没有文本内容,只显示助手消息头
652
721
  if not first_row_added:
653
722
  table.add_row(
654
- f"[dim]{time_str:8}[/dim]",
655
- f"[dim]{index_str:4}[/dim]",
656
- f"[green]Assistant:[/green]{meta_info}",
723
+ *build_table_row(
724
+ f"[dim]{time_str:8}[/dim]",
725
+ f"[dim]{index_str:4}[/dim]",
726
+ f"[green]Assistant:[/green]{meta_info}",
727
+ ),
657
728
  )
658
729
 
659
730
  # 添加工具调用
@@ -667,7 +738,7 @@ def _display_assistant_message_with_columns(
667
738
  args_str = f" {tool_call.arguments}"
668
739
 
669
740
  args_display = truncate_content(args_str, max_content_length - len(tool_call.name) - 10)
670
- table.add_row("", "", f"[magenta]Call:[/magenta] {tool_call.name}{args_display}")
741
+ table.add_row(*build_table_row("", "", f"[magenta]Call:[/magenta] {tool_call.name}{args_display}"))
671
742
 
672
743
  # 添加工具结果
673
744
  for tool_result in tool_results:
@@ -676,10 +747,10 @@ def _display_assistant_message_with_columns(
676
747
  if tool_result.execution_time_ms is not None:
677
748
  time_info = f" [dim]({tool_result.execution_time_ms}ms)[/dim]"
678
749
 
679
- table.add_row("", "", f"[cyan]Output:[/cyan]{time_info}")
750
+ table.add_row(*build_table_row("", "", f"[cyan]Output:[/cyan]{time_info}"))
680
751
  lines = output.split("\n")
681
752
  for line in lines:
682
- table.add_row("", "", line)
753
+ table.add_row(*build_table_row("", "", line))
683
754
 
684
755
  console.print(table)
685
756
 
@@ -689,6 +760,8 @@ def _display_legacy_message_with_columns(
689
760
  console: Console,
690
761
  time_str: str,
691
762
  index_str: str,
763
+ *,
764
+ show_metadata: bool, # noqa: ARG001
692
765
  max_content_length: int,
693
766
  truncate_content: Callable[[str, int], str],
694
767
  ) -> None:
@@ -735,6 +808,7 @@ def _create_message_context(context_config: dict[str, FlexibleRunnerMessage | Co
735
808
  max_content_length = max_content_length_val
736
809
  truncate_content = context_config["truncate_content"]
737
810
  show_timestamp = context_config.get("show_timestamp", False)
811
+ show_metadata = bool(context_config.get("show_metadata", True))
738
812
  local_timezone = context_config.get("local_timezone")
739
813
 
740
814
  # 类型检查
@@ -773,6 +847,7 @@ def _create_message_context(context_config: dict[str, FlexibleRunnerMessage | Co
773
847
  console=console,
774
848
  index_str=index_str,
775
849
  timestamp_str=timestamp_str,
850
+ show_metadata=show_metadata,
776
851
  max_content_length=max_content_length,
777
852
  truncate_content=truncate_content, # type: ignore[arg-type]
778
853
  )
@@ -1048,3 +1123,103 @@ def _display_new_assistant_message_compact(message: NewAssistantMessage, context
1048
1123
  # Always use indented format for better hierarchy
1049
1124
  context.console.print(f" [cyan]Output:[/cyan]{time_info}")
1050
1125
  context.console.print(f" {output}")
1126
+
1127
+
1128
+ def messages_to_string(
1129
+ messages: RunnerMessages,
1130
+ *,
1131
+ show_indices: bool = False,
1132
+ show_timestamps: bool = False,
1133
+ show_metadata: bool = False,
1134
+ max_content_length: int = 1000,
1135
+ local_timezone: timezone | str | None = None,
1136
+ ) -> str:
1137
+ """
1138
+ 将消息列表转换为纯文本字符串,默认简洁格式(不显示时间、序号、元数据)。
1139
+
1140
+ Args:
1141
+ messages: 要转换的消息列表
1142
+ show_indices: 是否显示消息序号(默认False)
1143
+ show_timestamps: 是否显示时间戳(默认False)
1144
+ show_metadata: 是否显示元数据(如模型、延迟、token使用等,默认False)
1145
+ max_content_length: 内容最大长度限制(默认1000)
1146
+ local_timezone: 本地时区设置(可选)
1147
+
1148
+ Returns:
1149
+ 包含所有消息的纯文本字符串
1150
+ """
1151
+ # 创建一个没有颜色的 Console 来捕获输出
1152
+ string_buffer = StringIO()
1153
+ plain_console = Console(file=string_buffer, force_terminal=False, no_color=True, width=120)
1154
+
1155
+ # 使用配置
1156
+ config = DisplayConfig(
1157
+ console=plain_console,
1158
+ show_indices=show_indices,
1159
+ show_timestamps=show_timestamps,
1160
+ show_metadata=show_metadata,
1161
+ max_content_length=max_content_length,
1162
+ local_timezone=local_timezone,
1163
+ )
1164
+
1165
+ # 调用现有的 display_messages 函数,但输出到字符串缓冲区
1166
+ display_messages(messages, config=config)
1167
+
1168
+ # 获取结果并清理尾随空格
1169
+ result = string_buffer.getvalue()
1170
+ string_buffer.close()
1171
+
1172
+ # 清理每行的尾随空格
1173
+ lines = result.split("\n")
1174
+ cleaned_lines = [line.rstrip() for line in lines]
1175
+ return "\n".join(cleaned_lines)
1176
+
1177
+
1178
+ def chat_summary_to_string(messages: RunnerMessages, *, include_performance: bool = False) -> str:
1179
+ """
1180
+ 将聊天摘要转换为纯文本字符串,默认只显示基本统计信息。
1181
+
1182
+ Args:
1183
+ messages: 要分析的消息列表
1184
+ include_performance: 是否包含性能统计信息(默认False)
1185
+
1186
+ Returns:
1187
+ 包含聊天摘要的纯文本字符串
1188
+ """
1189
+ string_buffer = StringIO()
1190
+ plain_console = Console(file=string_buffer, force_terminal=False, no_color=True, width=120)
1191
+
1192
+ if include_performance:
1193
+ # 调用现有的 display_chat_summary 函数,包含所有信息
1194
+ display_chat_summary(messages, console=plain_console)
1195
+ else:
1196
+ # 只显示基本的消息统计信息
1197
+ _display_basic_message_stats(messages, plain_console)
1198
+
1199
+ # 获取结果并清理
1200
+ result = string_buffer.getvalue()
1201
+ string_buffer.close()
1202
+
1203
+ return result
1204
+
1205
+
1206
+ def _display_basic_message_stats(messages: RunnerMessages, console: Console) -> None:
1207
+ """显示基本的消息统计信息,不包含性能数据。"""
1208
+ message_counts, _ = _analyze_messages(messages)
1209
+
1210
+ # 创建简化的统计表格
1211
+ table = Table(title="Message Summary", show_header=True, header_style="bold blue")
1212
+ table.add_column("Message Type", justify="left")
1213
+ table.add_column("Count", justify="right")
1214
+
1215
+ # 添加消息类型统计
1216
+ for msg_type, count in message_counts.items():
1217
+ if msg_type != "Total": # 跳过总计,单独处理
1218
+ table.add_row(msg_type, str(count))
1219
+
1220
+ # 添加总计行
1221
+ if "Total" in message_counts:
1222
+ table.add_row("", "") # 空行分隔
1223
+ table.add_row("[bold]Total[/bold]", f"[bold]{message_counts['Total']}[/bold]")
1224
+
1225
+ console.print(table)
lite_agent/context.py CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any, Generic, TypeVar
5
+ from typing import TYPE_CHECKING, Generic, TypeVar
6
6
 
7
7
  from pydantic import BaseModel
8
8
 
9
+ if TYPE_CHECKING:
10
+ from lite_agent.types import NewMessage
11
+
9
12
  T = TypeVar("T")
10
13
 
11
14
 
@@ -33,5 +36,5 @@ class HistoryContext(BaseModel, Generic[T]):
33
36
  ... return f"用户 {user_id} 有 {len(messages)} 条消息"
34
37
  """
35
38
 
36
- history_messages: list[Any]
39
+ history_messages: list[NewMessage]
37
40
  data: T | None = None
lite_agent/runner.py CHANGED
@@ -1,9 +1,10 @@
1
+ import inspect
1
2
  import json
2
3
  from collections.abc import AsyncGenerator, Sequence
3
4
  from datetime import datetime, timedelta, timezone
4
5
  from os import PathLike
5
6
  from pathlib import Path
6
- from typing import Any, Literal, cast
7
+ from typing import Any, Literal, cast, get_args, get_origin
7
8
 
8
9
  from funcall import Context
9
10
 
@@ -112,6 +113,59 @@ class Runner:
112
113
  """Normalize record_to parameter to Path object if provided."""
113
114
  return Path(record_to) if record_to else None
114
115
 
116
+ def _tool_expects_history_context(self, tool_calls: Sequence["ToolCall"]) -> bool:
117
+ """Check if any of the tool calls expect HistoryContext in their signatures.
118
+
119
+ Returns True if any tool function has a Context[HistoryContext[...]] parameter,
120
+ False if they expect Context[...] without HistoryContext wrapper.
121
+ """
122
+ if not tool_calls:
123
+ return False
124
+
125
+ for tool_call in tool_calls:
126
+ tool_func = self.agent.fc.function_registry.get(tool_call.function.name)
127
+ if not tool_func:
128
+ continue
129
+
130
+ # Get function signature
131
+ sig = inspect.signature(tool_func)
132
+
133
+ # Check each parameter for Context annotation
134
+ for param in sig.parameters.values():
135
+ if param.annotation == inspect.Parameter.empty:
136
+ continue
137
+
138
+ # Check if parameter is Context[...]
139
+ origin = get_origin(param.annotation)
140
+ if origin is not None:
141
+ # Check if it's Context type (compare by string name to handle import differences)
142
+ origin_name = getattr(origin, "__name__", str(origin))
143
+ if "Context" in origin_name:
144
+ args = get_args(param.annotation)
145
+ if args:
146
+ # Check if the Context contains HistoryContext
147
+ inner_type = args[0]
148
+ inner_origin = get_origin(inner_type)
149
+ if inner_origin is not None:
150
+ inner_name = getattr(inner_origin, "__name__", str(inner_origin))
151
+ if "HistoryContext" in inner_name:
152
+ logger.debug(f"Tool {tool_call.function.name} expects HistoryContext")
153
+ return True
154
+ # Also check for direct HistoryContext class
155
+ elif hasattr(inner_type, "__name__") and "HistoryContext" in inner_type.__name__:
156
+ logger.debug(f"Tool {tool_call.function.name} expects HistoryContext")
157
+ return True
158
+
159
+ # Also handle direct annotation checking
160
+ if hasattr(param.annotation, "__name__"):
161
+ annotation_str = str(param.annotation)
162
+ if "HistoryContext" in annotation_str:
163
+ logger.debug(f"Tool {tool_call.function.name} expects HistoryContext (direct)")
164
+ return True
165
+
166
+ logger.debug("No tools expect HistoryContext")
167
+ return False
168
+
115
169
  async def _handle_tool_calls(self, tool_calls: "Sequence[ToolCall] | None", includes: Sequence[AgentChunkType], context: "Any | None" = None) -> AsyncGenerator[AgentChunk, None]: # noqa: ANN401
116
170
  """Handle tool calls and yield appropriate chunks."""
117
171
  if not tool_calls:
@@ -184,23 +238,33 @@ class Runner:
184
238
  )
185
239
  return # Stop processing other tool calls after transfer
186
240
 
187
- # Auto-inject history messages into context
188
- if context is not None and not isinstance(context, Context):
189
- # If user provided a plain object, wrap it in Context first
241
+ # Check if tools expect HistoryContext wrapper
242
+ expects_history = self._tool_expects_history_context(tool_calls)
243
+
244
+ if expects_history:
245
+ # Auto-inject history messages into context for tools that expect HistoryContext
246
+ if context is not None and not isinstance(context, Context):
247
+ # If user provided a plain object, wrap it in Context first
248
+ context = Context(context)
249
+
250
+ if isinstance(context, Context):
251
+ # Extract original value and wrap in HistoryContext
252
+ original_value = context.value
253
+ wrapped = HistoryContext(
254
+ history_messages=self.messages.copy(),
255
+ data=original_value,
256
+ )
257
+ context = Context(wrapped)
258
+ else:
259
+ # No context provided, create HistoryContext with only history messages
260
+ wrapped = HistoryContext(history_messages=self.messages.copy())
261
+ context = Context(wrapped)
262
+ elif context is not None and not isinstance(context, Context):
263
+ # Tools don't expect HistoryContext, wrap user object in Context
190
264
  context = Context(context)
191
-
192
- if isinstance(context, Context):
193
- # Extract original value and wrap in HistoryContext
194
- original_value = context.value
195
- wrapped = HistoryContext(
196
- history_messages=self.messages.copy(),
197
- data=original_value,
198
- )
199
- context = Context(wrapped)
200
- else:
201
- # No context provided, create HistoryContext with only history messages
202
- wrapped = HistoryContext(history_messages=self.messages.copy())
203
- context = Context(wrapped)
265
+ elif context is None:
266
+ # Provide empty context for tools that don't expect HistoryContext
267
+ context = Context(None)
204
268
 
205
269
  async for tool_call_chunk in self.agent.handle_tool_calls(tool_calls, context=context):
206
270
  # if tool_call_chunk.type == "function_call" and tool_call_chunk.type in includes:
@@ -289,6 +353,12 @@ class Runner:
289
353
  # Determine completion condition based on agent configuration
290
354
  completion_condition = getattr(self.agent, "completion_condition", CompletionMode.STOP)
291
355
 
356
+ # If termination_tools are set but completion_condition is still STOP,
357
+ # automatically switch to CALL mode to enable custom termination
358
+ if completion_condition == CompletionMode.STOP and hasattr(self.agent, "termination_tools") and self.agent.termination_tools:
359
+ completion_condition = CompletionMode.CALL
360
+ logger.debug(f"Auto-switching to CALL mode due to termination_tools: {self.agent.termination_tools}")
361
+
292
362
  def is_finish() -> bool:
293
363
  if completion_condition == CompletionMode.CALL:
294
364
  # Check if any termination tool was called in the last assistant message
@@ -511,9 +581,10 @@ class Runner:
511
581
  max_steps: int = 20,
512
582
  includes: list[AgentChunkType] | None = None,
513
583
  record_to: PathLike | str | None = None,
584
+ context: Context | None = None,
514
585
  ) -> list[AgentChunk]:
515
586
  """Run the agent until it completes and return the final message."""
516
- resp = self.run(user_input, max_steps, includes, record_to=record_to)
587
+ resp = self.run(user_input, max_steps, includes, record_to=record_to, context=context)
517
588
  return await self._collect_all_chunks(resp)
518
589
 
519
590
  def _analyze_last_assistant_message(self) -> tuple[list[AssistantToolCall], dict[str, str]]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lite-agent
3
- Version: 0.12.0
3
+ Version: 0.14.0
4
4
  Summary: A lightweight, extensible framework for building AI agent.
5
5
  Author-email: Jianqi Pan <jannchie@gmail.com>
6
6
  License: MIT
@@ -1,13 +1,13 @@
1
- lite_agent/__init__.py,sha256=Swuefee0etSiaDnn30K2hBNV9UI3hIValW3A-pRE7e0,338
1
+ lite_agent/__init__.py,sha256=uN7fMz4MIO8sc8mMV-MLFZNXsW3PgzzWJVHZSlJ2jT4,430
2
2
  lite_agent/agent.py,sha256=gMn-NeQxl0298uKmlJph_c9WP3QN8WeKMEEDuv5RdVw,18681
3
- lite_agent/chat_display.py,sha256=6gPgutMj7hCUOH6QBcC2f4Bhc93Gdq4vBa_1y6QKt2g,40793
3
+ lite_agent/chat_display.py,sha256=bSJvYmrhc6xdSTyF8Tg3wiydnl4aL9R30QfkijDx8O8,47390
4
4
  lite_agent/client.py,sha256=-9BXLhAp3bGJsdKJ02lLpPJeHQKyHKQwhebZ6WCYh_k,9988
5
5
  lite_agent/constants.py,sha256=_xIDdQwaJrWk8N_62o-KYEo3jj1waPJ0ZOd3hHybKNo,718
6
- lite_agent/context.py,sha256=qnqj4JVYL0pkle1YptwM_CXJUWTeLaYtI1_MGtJYaHI,1225
6
+ lite_agent/context.py,sha256=gN5Ner_ntMNwnUqraOhc9P7JpQ1lv174tIrreWBLjhQ,1305
7
7
  lite_agent/loggers.py,sha256=XkNkdqwD_nQGfhQJ-bBWT7koci_mMkNw3aBpyMhOICw,57
8
8
  lite_agent/message_transfers.py,sha256=N9ViK7Gxqqa1sd3V_hkNuQ9fUipg7M95l-sVBBG2Id4,5357
9
9
  lite_agent/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- lite_agent/runner.py,sha256=p-Dw4RNYfRnIwnbt8oJv8e4qI84X7OUWLxgyITMnGe8,45090
10
+ lite_agent/runner.py,sha256=XbQO5vD1LUfqq_XPNBOxZEYp3s_98sfDQXnnMWyFIsI,48908
11
11
  lite_agent/processors/__init__.py,sha256=ybpAzpMBIE9v5I24wIBZRXeaOaPNTmoKH13aofgNI6Q,234
12
12
  lite_agent/processors/completion_event_processor.py,sha256=zoWvs8dfrIkCSITGtS-4Hpve3WFCA0UUsMvYifL2fw0,13010
13
13
  lite_agent/processors/response_event_processor.py,sha256=Jr3cj1ItJ8aq9UBhEEjDwWDnPNOZ2ZXjWJ3-g4ghkhM,8514
@@ -30,6 +30,6 @@ lite_agent/utils/message_builder.py,sha256=J-yycL9pXSO9MbgC5NEGqvoP1LC2Nxe9r2YRW
30
30
  lite_agent/utils/message_converter.py,sha256=5HmNncTl71TD2M_6Ezz1Tnfavzna8DQYb4-D47Du7mA,9165
31
31
  lite_agent/utils/message_state_manager.py,sha256=rFUyqyd_7NdJRtyqsAWGcfwrDIlD6gK2dBDSDx1eGBs,5766
32
32
  lite_agent/utils/metrics.py,sha256=RzOEhCWxbLmmIEkzaxOJ6tAdthI8dv2Foc98Lq8afOQ,1915
33
- lite_agent-0.12.0.dist-info/METADATA,sha256=CvsMgd3DNYoZTvD0bTirNibch5S9eH8K-hjwWvEjFZo,3538
34
- lite_agent-0.12.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
35
- lite_agent-0.12.0.dist-info/RECORD,,
33
+ lite_agent-0.14.0.dist-info/METADATA,sha256=JLMNgR-lomFvzGuH_SbisiZwz3ORo-axVg6sCkVQeSc,3538
34
+ lite_agent-0.14.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
35
+ lite_agent-0.14.0.dist-info/RECORD,,