ni.agentkit 0.6.0__tar.gz → 0.6.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/PKG-INFO +3 -3
  2. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/README.md +2 -2
  3. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/__init__.py +1 -1
  4. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/agents/agent.py +44 -7
  5. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/agents/base_agent.py +31 -22
  6. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/agents/orchestrators.py +18 -14
  7. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/docs/Architecture.md +8 -5
  8. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/docs/README.md +1 -1
  9. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/docs/Reference.md +6 -4
  10. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/docs/TestReport.md +25 -25
  11. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/17_checkpoint_handoff_resume.py +11 -3
  12. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/17_checkpoint_handoff_resume.py +11 -4
  13. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/ni.agentkit.egg-info/PKG-INFO +3 -3
  14. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/ni.agentkit.egg-info/SOURCES.txt +3 -0
  15. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/pyproject.toml +1 -1
  16. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/runner/context.py +63 -2
  17. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/runner/runner.py +82 -9
  18. ni_agentkit-0.6.2/tests/test_after_callback.py +69 -0
  19. ni_agentkit-0.6.2/tests/test_quickstart_core.py +125 -0
  20. ni_agentkit-0.6.2/tests/test_runner_checkpoint_resume.py +152 -0
  21. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/tools/skill_toolset.py +4 -0
  22. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/LICENSE +0 -0
  23. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/_cli.py +0 -0
  24. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/agents/__init__.py +0 -0
  25. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/docs/QuickStart.md +0 -0
  26. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/__init__.py +0 -0
  27. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/01_basic_chat.py +0 -0
  28. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/02_tool_calling.py +0 -0
  29. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/03_skill_usage.py +0 -0
  30. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/04_multi_agent.py +0 -0
  31. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/05_guardrail.py +0 -0
  32. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/06_orchestration.py +0 -0
  33. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/07_sync_async_stream.py +0 -0
  34. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/08_memory.py +0 -0
  35. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/09a_structured_data_sql.py +0 -0
  36. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/09b_structured_data_graph.py +0 -0
  37. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/10_skill_lifecycle.py +0 -0
  38. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/11_orchestration_enhancement.py +0 -0
  39. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/12_run_context_serialization.py +0 -0
  40. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/13_human_in_the_loop.py +0 -0
  41. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/14_event_standardization.py +0 -0
  42. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/15_multi_tenant_isolation.py +0 -0
  43. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/16_lifecycle_hooks.py +0 -0
  44. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/18_model_cosplay.py +0 -0
  45. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/README.md +0 -0
  46. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/ollama/__init__.py +0 -0
  47. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/quickstart.py +0 -0
  48. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/01_basic_chat.py +0 -0
  49. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/02_tool_calling.py +0 -0
  50. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/03_skill_usage.py +0 -0
  51. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/04_multi_agent.py +0 -0
  52. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/05_guardrail.py +0 -0
  53. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/06_orchestration.py +0 -0
  54. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/07_sync_async_stream.py +0 -0
  55. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/08_memory.py +0 -0
  56. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/09a_structured_data_sql.py +0 -0
  57. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/09b_structured_data_graph.py +0 -0
  58. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/10_skill_lifecycle.py +0 -0
  59. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/11_orchestration_enhancement.py +0 -0
  60. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/12_run_context_serialization.py +0 -0
  61. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/13_human_in_the_loop.py +0 -0
  62. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/14_event_standardization.py +0 -0
  63. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/15_multi_tenant_isolation.py +0 -0
  64. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/16_lifecycle_hooks.py +0 -0
  65. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/18_model_cosplay.py +0 -0
  66. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/README.md +0 -0
  67. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/standard/__init__.py +0 -0
  68. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/examples/test_ollama.py +0 -0
  69. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/llm/__init__.py +0 -0
  70. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/llm/adapters/__init__.py +0 -0
  71. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/llm/adapters/anthropic_adapter.py +0 -0
  72. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/llm/adapters/google_adapter.py +0 -0
  73. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/llm/adapters/ollama_adapter.py +0 -0
  74. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/llm/adapters/openai_adapter.py +0 -0
  75. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/llm/adapters/openai_compatible.py +0 -0
  76. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/llm/base.py +0 -0
  77. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/llm/cache.py +0 -0
  78. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/llm/middleware.py +0 -0
  79. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/llm/registry.py +0 -0
  80. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/llm/types.py +0 -0
  81. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/memory/__init__.py +0 -0
  82. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/memory/base.py +0 -0
  83. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/memory/mem0_provider.py +0 -0
  84. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/ni.agentkit.egg-info/dependency_links.txt +0 -0
  85. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/ni.agentkit.egg-info/entry_points.txt +0 -0
  86. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/ni.agentkit.egg-info/requires.txt +0 -0
  87. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/ni.agentkit.egg-info/top_level.txt +0 -0
  88. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/runner/__init__.py +0 -0
  89. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/runner/context_store.py +0 -0
  90. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/runner/events.py +0 -0
  91. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/safety/__init__.py +0 -0
  92. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/safety/guardrails.py +0 -0
  93. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/safety/permissions.py +0 -0
  94. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/setup.cfg +0 -0
  95. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/skills/__init__.py +0 -0
  96. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/skills/loader.py +0 -0
  97. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/skills/models.py +0 -0
  98. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/skills/registry.py +0 -0
  99. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/tools/__init__.py +0 -0
  100. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/tools/base_tool.py +0 -0
  101. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/tools/function_tool.py +0 -0
  102. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/tools/nebula_tool.py +0 -0
  103. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/tools/sqlite_tool.py +0 -0
  104. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/tools/structured_data.py +0 -0
  105. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/utils/__init__.py +0 -0
  106. {ni_agentkit-0.6.0 → ni_agentkit-0.6.2}/utils/schema.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ni.agentkit
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: A Python-native Agent framework with first-class Skill support and multi-LLM adapter
5
5
  Author-email: Krix Tam <krix.tam@qq.com>
6
6
  License: MIT
@@ -153,8 +153,8 @@ python "$(python -c "import agentkit, os; print(os.path.join(agentkit.get_exampl
153
153
 
154
154
  ```bash
155
155
  dist/
156
- ├── ni_agentkit-0.6.0-py3-none-any.whl # pip install 用这个
157
- └── ni_agentkit-0.6.0.tar.gz # 源码分发
156
+ ├── ni_agentkit-0.6.2-py3-none-any.whl # pip install 用这个
157
+ └── ni_agentkit-0.6.2.tar.gz # 源码分发
158
158
  ```
159
159
 
160
160
  ## 📄 License
@@ -111,8 +111,8 @@ python "$(python -c "import agentkit, os; print(os.path.join(agentkit.get_exampl
111
111
 
112
112
  ```bash
113
113
  dist/
114
- ├── ni_agentkit-0.6.0-py3-none-any.whl # pip install 用这个
115
- └── ni_agentkit-0.6.0.tar.gz # 源码分发
114
+ ├── ni_agentkit-0.6.2-py3-none-any.whl # pip install 用这个
115
+ └── ni_agentkit-0.6.2.tar.gz # 源码分发
116
116
  ```
117
117
 
118
118
  ## 📄 License
@@ -31,7 +31,7 @@ from .tools.function_tool import FunctionTool, function_tool
31
31
  from .tools.structured_data import ResultFormatter, StructuredDataTool
32
32
  from .tools.sqlite_tool import SQLiteTool, SQLiteResultFormatter
33
33
 
34
- __version__ = "0.6.0"
34
+ __version__ = "0.6.2"
35
35
 
36
36
 
37
37
  def get_docs_dir() -> str:
@@ -190,6 +190,7 @@ class Agent(BaseAgent):
190
190
  llm = self._resolve_model()
191
191
  memory_injection = ""
192
192
  skill_prompt_injection = ""
193
+ runtime_skill_toolset: SkillToolset | None = None
193
194
  history_messages: list[Message] = []
194
195
  parsed_history_len = 0
195
196
  last_tool_signature: tuple[tuple[str, int], ...] = tuple()
@@ -206,8 +207,8 @@ class Agent(BaseAgent):
206
207
  logger.warning("检索记忆失败: %s", e)
207
208
 
208
209
  if self.skills:
209
- skill_toolset = SkillToolset(skills=self.skills)
210
- skill_prompt_injection = "\n\n" + skill_toolset.get_system_prompt_injection()
210
+ runtime_skill_toolset = SkillToolset(skills=self.skills)
211
+ skill_prompt_injection = "\n\n" + runtime_skill_toolset.get_system_prompt_injection()
211
212
 
212
213
  try:
213
214
  while round_count < self.max_tool_rounds:
@@ -219,7 +220,32 @@ class Agent(BaseAgent):
219
220
  instructions += skill_prompt_injection
220
221
 
221
222
  # 2. 获取工具
222
- tools = await self.get_all_tools(ctx)
223
+ tools: list[BaseTool] = []
224
+ for tool_union in self.tools:
225
+ if isinstance(tool_union, BaseTool):
226
+ tools.append(tool_union)
227
+ elif isinstance(tool_union, BaseToolset):
228
+ tools.extend(await tool_union.get_tools(ctx))
229
+ elif callable(tool_union):
230
+ fn_key = id(tool_union)
231
+ cached_tool = self._callable_tool_cache.get(fn_key)
232
+ if cached_tool is None:
233
+ cached_tool = FunctionTool.from_function(tool_union)
234
+ self._callable_tool_cache[fn_key] = cached_tool
235
+ tools.append(cached_tool)
236
+
237
+ if runtime_skill_toolset is not None:
238
+ runtime_skill_toolset.set_additional_tools(tools.copy())
239
+ tools.extend(await runtime_skill_toolset.get_tools(ctx))
240
+
241
+ for target in self.handoffs:
242
+ if isinstance(target, BaseAgent):
243
+ target_key = id(target)
244
+ cached_handoff_tool = self._handoff_tool_cache.get(target_key)
245
+ if cached_handoff_tool is None:
246
+ cached_handoff_tool = self._create_handoff_tool(target)
247
+ self._handoff_tool_cache[target_key] = cached_handoff_tool
248
+ tools.append(cached_handoff_tool)
223
249
  tool_signature = tuple((tool.name, id(tool)) for tool in tools)
224
250
  tool_defs_start = time.perf_counter()
225
251
  if tool_signature != last_tool_signature:
@@ -388,13 +414,24 @@ class Agent(BaseAgent):
388
414
  try:
389
415
  result = await tool.execute(ctx, tool_call.arguments)
390
416
  except HumanInputRequested as e:
391
- # 触发挂起事件,并记录挂起的工具信息
392
- ctx.state["__suspended_tool_call_id__"] = tool_call.id
393
- ctx.state["__suspended_tool_name__"] = tool_call.name
417
+ suspension = ctx.register_suspension(
418
+ tool_call_id=tool_call.id,
419
+ tool_name=tool_call.name,
420
+ prompt=e.prompt,
421
+ form_schema=e.kwargs.get("form_schema"),
422
+ resume_strategy=e.kwargs.get("resume_strategy", "as_tool_result"),
423
+ )
394
424
  yield Event(
395
425
  agent=self.name,
396
426
  type=EventType.SUSPEND_REQUESTED,
397
- data={"prompt": e.prompt, "tool": tool_call.name, "tool_call_id": tool_call.id, **e.kwargs}
427
+ data={
428
+ "suspension_id": suspension.suspension_id,
429
+ "prompt": e.prompt,
430
+ "tool": tool_call.name,
431
+ "tool_call_id": tool_call.id,
432
+ "resume_strategy": suspension.resume_strategy,
433
+ **e.kwargs,
434
+ },
398
435
  )
399
436
  return
400
437
  except Exception as e:
@@ -5,7 +5,9 @@ agentkit/agents/base_agent.py — 所有 Agent 的基类
5
5
  """
6
6
  from __future__ import annotations
7
7
 
8
+ import asyncio
8
9
  from abc import abstractmethod
10
+ from contextlib import aclosing
9
11
  from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable, Optional
10
12
 
11
13
  from pydantic import BaseModel, ConfigDict, Field
@@ -62,30 +64,37 @@ class BaseAgent(BaseModel):
62
64
 
63
65
  async def run(self, ctx: "RunContext") -> AsyncGenerator[Event, None]:
64
66
  """运行入口 — 子类不可覆盖"""
65
- # 1. before callback
66
- if self.before_agent_callback:
67
- result, duration, err = await self._run_hook(self.before_agent_callback, "before_agent_callback", ctx)
68
- if err:
69
- yield Event(agent=self.name, type="error", data={"hook": "before_agent", "error": str(err), "duration": duration})
70
- if self.fail_fast_on_hook_error:
67
+ emit_after_events = True
68
+ try:
69
+ # 1. before callback
70
+ if self.before_agent_callback:
71
+ result, duration, err = await self._run_hook(self.before_agent_callback, "before_agent_callback", ctx)
72
+ if err:
73
+ yield Event(agent=self.name, type="error", data={"hook": "before_agent", "error": str(err), "duration": duration})
74
+ if self.fail_fast_on_hook_error:
75
+ return
76
+ elif result is not None:
77
+ yield Event(agent=self.name, type="callback", data={"result": result, "duration": duration})
71
78
  return
72
- elif result is not None:
73
- yield Event(agent=self.name, type="callback", data={"result": result, "duration": duration})
74
- return
75
-
76
- # 2. 核心逻辑(子类实现)
77
- async for event in self._run_impl(ctx):
78
- yield event
79
79
 
80
- # 3. after callback
81
- if self.after_agent_callback:
82
- result, duration, err = await self._run_hook(self.after_agent_callback, "after_agent_callback", ctx)
83
- if err:
84
- yield Event(agent=self.name, type="error", data={"hook": "after_agent", "error": str(err), "duration": duration})
85
- if self.fail_fast_on_hook_error:
86
- return
87
- elif result is not None:
88
- yield Event(agent=self.name, type="callback", data={"result": result, "duration": duration})
80
+ # 2. 核心逻辑(子类实现)
81
+ impl_stream = self._run_impl(ctx)
82
+ async with aclosing(impl_stream):
83
+ async for event in impl_stream:
84
+ yield event
85
+ except (GeneratorExit, asyncio.CancelledError):
86
+ # 外部提前中断时不再尝试向外 yield 事件,但必须执行 after 回调逻辑
87
+ emit_after_events = False
88
+ raise
89
+ finally:
90
+ # 3. after callback(保证执行)
91
+ if self.after_agent_callback:
92
+ result, duration, err = await self._run_hook(self.after_agent_callback, "after_agent_callback", ctx)
93
+ if emit_after_events:
94
+ if err:
95
+ yield Event(agent=self.name, type="error", data={"hook": "after_agent", "error": str(err), "duration": duration})
96
+ elif result is not None:
97
+ yield Event(agent=self.name, type="callback", data={"result": result, "duration": duration})
89
98
 
90
99
  @abstractmethod
91
100
  async def _run_impl(self, ctx: "RunContext") -> AsyncGenerator[Event, None]:
@@ -8,6 +8,7 @@ agentkit/agents/loop_agent.py — 循环执行子 Agent
8
8
  from __future__ import annotations
9
9
 
10
10
  import asyncio
11
+ from contextlib import aclosing
11
12
  from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable
12
13
 
13
14
  from ..runner.events import Event
@@ -22,10 +23,11 @@ class SequentialAgent(BaseAgent):
22
23
 
23
24
  async def _run_impl(self, ctx: "RunContext") -> AsyncGenerator[Event, None]:
24
25
  for sub in self.sub_agents:
25
- async for event in sub.run(ctx):
26
- yield event
27
- if event.type == "escalate":
28
- return
26
+ async with aclosing(sub.run(ctx)) as stream:
27
+ async for event in stream:
28
+ yield event
29
+ if event.type == "escalate":
30
+ return
29
31
 
30
32
 
31
33
  class ParallelAgent(BaseAgent):
@@ -60,12 +62,13 @@ class ParallelAgent(BaseAgent):
60
62
  async def run_branch(agent: BaseAgent, branch_ctx: "RunContext") -> None:
61
63
  branch_status[agent.name] = "running"
62
64
  try:
63
- async for event in agent.run(branch_ctx):
64
- all_events[agent.name].append(event)
65
- await queue.put((agent.name, event))
66
- if event.type == "escalate":
67
- branch_status[agent.name] = "escalated"
68
- return
65
+ async with aclosing(agent.run(branch_ctx)) as stream:
66
+ async for event in stream:
67
+ all_events[agent.name].append(event)
68
+ await queue.put((agent.name, event))
69
+ if event.type == "escalate":
70
+ branch_status[agent.name] = "escalated"
71
+ return
69
72
  branch_status[agent.name] = "completed"
70
73
  except asyncio.CancelledError:
71
74
  branch_status[agent.name] = "cancelled"
@@ -112,10 +115,11 @@ class LoopAgent(BaseAgent):
112
115
  async def _run_impl(self, ctx: "RunContext") -> AsyncGenerator[Event, None]:
113
116
  for iteration in range(self.max_iterations):
114
117
  for sub in self.sub_agents:
115
- async for event in sub.run(ctx):
116
- yield event
117
- if event.type == "escalate":
118
- return
118
+ async with aclosing(sub.run(ctx)) as stream:
119
+ async for event in stream:
120
+ yield event
121
+ if event.type == "escalate":
122
+ return
119
123
 
120
124
  if self.loop_condition is not None:
121
125
  # Provide ctx and current iteration index (0-based) as state
@@ -157,6 +157,8 @@ Runner.run()
157
157
 
158
158
  **9 个回调点**标记为 ❶ ~ ❾,覆盖了 Agent、Model、Tool、Handoff 四个层面的前后拦截及错误处理(含 `on_error_callback`)。
159
159
 
160
+ 补充说明:`after_agent_callback` 通过 `finally` 语义保证执行。即使上游编排发生提前 `return`(如 `escalate`)或外部关闭流式生成器(`aclose`),也会触发该回调;但在生成器关闭路径下,不保证回调事件继续对外 `yield`。
161
+
160
162
  ---
161
163
 
162
164
  ## Human-in-the-loop (HITL) 与状态管理
@@ -170,18 +172,19 @@ AgentKit 原生支持在任务执行过程中安全地挂起(Suspend)并持
170
172
 
171
173
  ├─ 遇到需要人工确认的工具 (抛出 HumanInputRequested)
172
174
 
173
- ├─ Runner 捕获异常,触发 suspend_requested 事件
175
+ ├─ Agent 捕获异常,注册 SuspensionRecord 并触发 suspend_requested 事件(含 suspension_id)
174
176
 
175
- ├─ ContextStore 保存当前 RunContext (包含会话状态、记忆、执行分支等)
177
+ ├─ ContextStore 保存当前 RunContext (包含会话状态、执行分支、suspensions、resume_idempotency 等)
176
178
 
177
179
 
178
180
  [进程可完全退出 / 释放资源]
179
181
 
180
182
  ├─ 外部人工提供输入
181
183
 
182
- ├─ Runner.resume(session_id, user_input, context_store)
184
+ ├─ Runner.resume(session_id, user_input, context_store, suspension_id=None, idempotency_key=None)
183
185
 
184
- ├─ ContextStore 恢复 RunContext,将人工输入作为挂起工具的结果
186
+ ├─ ContextStore 恢复 RunContext,按 suspension_id(或最新 pending)定位挂起点
187
+ │ └─ resume_strategy=as_tool_result 时将 user_input 注入对应 tool_call 结果
185
188
 
186
189
 
187
190
  [Agent 恢复运行,继续未完成的对话与工具链]
@@ -540,7 +543,7 @@ agent = Agent(memory=my_memory, memory_async_write=False, ...)
540
543
 
541
544
  #### 生命周期 Hooks / Callbacks
542
545
  AgentKit 提供细粒度的执行拦截点,支持同步/异步回调,并允许请求与响应改写:
543
- - **`before_agent_callback` / `after_agent_callback`**: 拦截整个 Agent 会话。
546
+ - **`before_agent_callback` / `after_agent_callback`**: 拦截整个 Agent 会话。`after_agent_callback` 在提前中断/外部关闭流的场景下也会执行(收尾保证),但关闭路径不保证继续外发 callback 事件。
544
547
  - **`before_model_callback` / `after_model_callback`**: 在 LLM 调用前后拦截,允许改写 Prompt 和模型输出。
545
548
  - **`before_tool_callback` / `after_tool_callback`**: 在工具执行前后拦截,允许改写执行结果。
546
549
  - **`before_handoff_callback` / `after_handoff_callback`**: 在触发 Handoff 转移前拦截,可动态修改目标 Agent。
@@ -3,7 +3,7 @@
3
3
  > Python 原生的 Agent 开发框架,内置一等公民级别的 Skill 支持和自研多模型适配层。
4
4
 
5
5
  [![Python](https://img.shields.io/badge/Python-≥3.11-blue.svg)](https://python.org)
6
- [![Version](https://img.shields.io/badge/Version-0.6.0-green.svg)]()
6
+ [![Version](https://img.shields.io/badge/Version-0.6.2-green.svg)]()
7
7
  [![License](https://img.shields.io/badge/License-MIT-yellow.svg)]()
8
8
 
9
9
  ---
@@ -72,7 +72,7 @@ from agentkit import Agent
72
72
  | `memory_async_write` | `bool` | `True` | 记忆写入模式。`True`=fire-and-forget 异步写入(不阻塞返回);`False`=同步等待写入完成(多轮串行对话推荐) |
73
73
  | `model_cosplay_enabled` | `bool` | `False` | 是否开启 ModelCosplay。关闭时如果类已预设 `model`,实例化时不允许覆盖;开启后允许实例化和运行时改写模型 |
74
74
  | `before_agent_callback` | `Callable \| None` | `None` | Agent 运行前回调 |
75
- | `after_agent_callback` | `Callable \| None` | `None` | Agent 运行后回调 |
75
+ | `after_agent_callback` | `Callable \| None` | `None` | Agent 运行后回调。通过 `finally` 语义保证执行;即使上游提前中断或外部关闭流(`aclose`)也会触发,但关闭路径不保证继续外发 callback 事件 |
76
76
  | `before_model_callback` | `Callable \| None` | `None` | LLM 调用前回调(可拦截/改写) |
77
77
  | `after_model_callback` | `Callable \| None` | `None` | LLM 调用后回调(可改写响应) |
78
78
  | `before_tool_callback` | `Callable \| None` | `None` | 工具调用前回调(可拦截执行) |
@@ -116,7 +116,7 @@ agent = Agent(
116
116
  | `description` | `str` | 描述 |
117
117
  | `sub_agents` | `list[BaseAgent]` | 子 Agent(自动建立父子关系) |
118
118
  | `before_agent_callback` | `Callable \| None` | 运行前回调 |
119
- | `after_agent_callback` | `Callable \| None` | 运行后回调 |
119
+ | `after_agent_callback` | `Callable \| None` | 运行后回调。支持提前中断/流关闭场景下的收尾触发(关闭路径不保证事件外发) |
120
120
  | `model_cosplay_enabled` | `bool` | 是否开启 ModelCosplay(默认 `False`) |
121
121
 
122
122
  **子类化**:
@@ -181,7 +181,7 @@ from agentkit import Runner
181
181
  | `run_sync` | `(agent, **kwargs) -> RunResult` | 同步运行(内部调用 asyncio.run) |
182
182
  | `run_streamed` | `async (agent, *, input, user_id=None, session_id=None, **kwargs) -> AsyncGenerator[Event, None]` | 流式运行,实时产出 Event |
183
183
  | `run_with_checkpoint` | `async (agent, *, input, session_id, context_store, ..., max_turns=10) -> AsyncGenerator[Event, None]` | 流式运行(支持挂起),遇到 `suspend_requested` 时自动保存上下文与执行指针(turn/current_agent/agent_path),并追加发出 `suspended` 事件 |
184
- | `resume` | `async (agent, *, session_id, user_input, context_store, ...) -> AsyncGenerator[Event, None]` | 恢复被挂起会话;优先按 checkpoint 的 `agent_path` 定位恢复节点,再将 `user_input` 作为挂起工具结果继续流转 |
184
+ | `resume` | `async (agent, *, session_id, user_input, context_store, ..., suspension_id=None, idempotency_key=None) -> AsyncGenerator[Event, None]` | 恢复被挂起会话;优先按 checkpoint 的 `agent_path` 定位恢复节点,再按 `suspension_id`(或最新 pending)恢复挂起点;支持 `idempotency_key` 幂等 |
185
185
 
186
186
  **参数**:
187
187
 
@@ -211,7 +211,9 @@ from agentkit.runner.context import RunContext
211
211
  | `user_id` | `str \| None` | 用户 ID(用于多租户和记忆隔离) |
212
212
  | `session_id` | `str` | 会话 ID,默认随机生成(用于断点续跑和上下文隔离) |
213
213
  | `messages` | `list[dict]` | 当前完整的对话消息链 |
214
- | `state` | `dict` | 运行期间的内部状态,挂起工具等信息暂存于此 |
214
+ | `state` | `dict` | 运行期间的内部状态(不再承载挂起协议字段) |
215
+ | `suspensions` | `list[SuspensionRecord]` | 框架托管的挂起记录列表(含 `suspension_id/tool_call_id/tool_name/prompt/form_schema/resume_strategy`) |
216
+ | `resume_idempotency` | `dict[str, dict]` | resume 幂等记录,避免重复提交人工输入 |
215
217
 
216
218
  **方法**:
217
219
 
@@ -1,9 +1,9 @@
1
1
  # AgentKit 示例测试报告
2
2
 
3
- > 测试时间:2026-04-19
4
- > 测试环境:macOS (Apple Silicon)
3
+ > 测试时间:`2026-04-22`
4
+ > 测试环境:`macOS (Apple Silicon)`
5
5
  > 模型:Ollama `qwen3.5:cloud`
6
- > AgentKit 版本:v0.6.0
6
+ > AgentKit 版本:v0.6.2
7
7
  > Thinking 模式:开启(默认)
8
8
  > LLM 调用模式:非流式(默认)
9
9
  > 缓存:开启(默认)
@@ -15,31 +15,31 @@
15
15
 
16
16
  | # | 示例 | 文件 | 耗时 | 状态 | 说明 |
17
17
  |---|------|------|-----:|:----:|------|
18
- | 1 | 基础对话 | `01_basic_chat.py` | 30.52s | ✅ | 运行通过 |
19
- | 2 | 工具调用 | `02_tool_calling.py` | 42.10s | ✅ | 运行通过 |
20
- | 3 | Skill 使用 | `03_skill_usage.py` | 200.30s | ✅ | 运行通过 |
21
- | 4 | 多 Agent 协作 | `04_multi_agent.py` | 110.67s | ✅ | 运行通过 |
22
- | 5 | 安全护栏 | `05_guardrail.py` | 7.19s | ✅ | 运行通过 |
23
- | 6 | 编排 Agent | `06_orchestration.py` | 54.30s | ✅ | 运行通过 |
24
- | 7 | 同步/异步/流式 | `07_sync_async_stream.py` | 32.54s | ✅ | 运行通过 |
25
- | 8 | 记忆系统 | `08_memory.py` | 778.88s | ✅ | 运行通过(本轮最慢) |
26
- | 9A | 结构化数据(SQL) | `09a_structured_data_sql.py` | 148.74s | ✅ | 运行通过 |
27
- | 9B | 结构化数据(图) | `09b_structured_data_graph.py` | 31.25s | ✅ | 运行通过 |
28
- | 10 | Skill 生命周期 | `10_skill_lifecycle.py` | 72.92s | ✅ | 运行通过 |
29
- | 11 | 编排增强 | `11_orchestration_enhancement.py` | 139.09s | ✅ | 运行通过 |
30
- | 12 | 序列化协议 | `12_run_context_serialization.py` | 0.33s | ✅ | 运行通过 |
31
- | 13 | Human in the Loop | `13_human_in_the_loop.py` | 9.75s | ✅ | 运行通过 |
32
- | 14 | Event 标准化 | `14_event_standardization.py` | 11.32s | ✅ | 运行通过 |
33
- | 15 | 多租户隔离 | `15_multi_tenant_isolation.py` | 0.39s | ✅ | 运行通过 |
34
- | 16 | 生命周期 Hooks | `16_lifecycle_hooks.py` | 0.29s | ✅ | 运行通过 |
35
- | 17 | Checkpoint + Handoff + Resume | `17_checkpoint_handoff_resume.py` | 0.30s | ✅ | 运行通过 |
36
- | 18 | ModelCosplay | `18_model_cosplay.py` | 0.21s | ✅ | 运行通过 |
37
- | | **合计** | | **1671.09s** | **19/19** | |
18
+ | 1 | 基础对话 | `01_basic_chat.py` | 124.36s | ✅ | 运行通过 |
19
+ | 2 | 工具调用 | `02_tool_calling.py` | 30.32s | ✅ | 运行通过 |
20
+ | 3 | Skill 使用 | `03_skill_usage.py` | 258.00s | ✅ | 运行通过(本轮最慢) |
21
+ | 4 | 多 Agent 协作 | `04_multi_agent.py` | 183.07s | ✅ | 运行通过 |
22
+ | 5 | 安全护栏 | `05_guardrail.py` | 21.82s | ✅ | 运行通过 |
23
+ | 6 | 编排 Agent | `06_orchestration.py` | 156.18s | ✅ | 运行通过 |
24
+ | 7 | 同步/异步/流式 | `07_sync_async_stream.py` | 17.57s | ✅ | 运行通过 |
25
+ | 8 | 记忆系统 | `08_memory.py` | 148.30s | ✅ | 运行通过 |
26
+ | 9A | 结构化数据(SQL) | `09a_structured_data_sql.py` | 3.99s | ✅ | 运行通过 |
27
+ | 9B | 结构化数据(图) | `09b_structured_data_graph.py` | 8.48s | ✅ | 运行通过 |
28
+ | 10 | Skill 生命周期 | `10_skill_lifecycle.py` | 1.77s | ✅ | 运行通过 |
29
+ | 11 | 编排增强 | `11_orchestration_enhancement.py` | 86.13s | ✅ | 运行通过 |
30
+ | 12 | 序列化协议 | `12_run_context_serialization.py` | 0.31s | ✅ | 运行通过 |
31
+ | 13 | Human in the Loop | `13_human_in_the_loop.py` | 6.21s | ✅ | 运行通过 |
32
+ | 14 | Event 标准化 | `14_event_standardization.py` | 50.10s | ✅ | 运行通过 |
33
+ | 15 | 多租户隔离 | `15_multi_tenant_isolation.py` | 0.44s | ✅ | 运行通过 |
34
+ | 16 | 生命周期 Hooks | `16_lifecycle_hooks.py` | 0.27s | ✅ | 运行通过 |
35
+ | 17 | Checkpoint + Handoff + Resume | `17_checkpoint_handoff_resume.py` | 0.17s | ✅ | 运行通过(本轮最快) |
36
+ | 18 | ModelCosplay | `18_model_cosplay.py` | 0.18s | ✅ | 运行通过 |
37
+ | | **合计** | | **1097.67s** | **19/19** | |
38
38
 
39
39
  ## 耗时分析
40
40
 
41
- - **最快示例**:18 ModelCosplay(0.21s
42
- - **最慢示例**:8 记忆系统(778.88s
41
+ - **最快示例**:17 Checkpoint + Handoff + Resume(0.17s
42
+ - **最慢示例**:3 Skill 使用(258.00s
43
43
  - **耗时集中区间**:涉及多轮推理/记忆写入/编排循环的示例耗时显著更高
44
44
 
45
45
  ## 各示例 LLM 调用次数估算
@@ -22,12 +22,20 @@ class ReviewAgent(BaseAgent):
22
22
  async def _run_impl(self, ctx: RunContext) -> AsyncGenerator[Event, None]:
23
23
  if not ctx.state.get("review_suspended_once"):
24
24
  ctx.state["review_suspended_once"] = True
25
- ctx.state["__suspended_tool_call_id__"] = "manual-review-1"
26
- ctx.state["__suspended_tool_name__"] = "manual_review"
25
+ suspension = ctx.register_suspension(
26
+ tool_call_id="manual-review-1",
27
+ tool_name="manual_review",
28
+ prompt="请审批该任务(approve/reject)",
29
+ )
27
30
  yield Event(
28
31
  agent=self.name,
29
32
  type=EventType.SUSPEND_REQUESTED,
30
- data={"prompt": "请审批该任务(approve/reject)", "tool": "manual_review", "tool_call_id": "manual-review-1"},
33
+ data={
34
+ "suspension_id": suspension.suspension_id,
35
+ "prompt": "请审批该任务(approve/reject)",
36
+ "tool": "manual_review",
37
+ "tool_call_id": "manual-review-1",
38
+ },
31
39
  )
32
40
  return
33
41
 
@@ -23,13 +23,20 @@ class ReviewAgent(BaseAgent):
23
23
  # 第一次进入:请求人工输入并挂起
24
24
  if not ctx.state.get("review_suspended_once"):
25
25
  ctx.state["review_suspended_once"] = True
26
- # 用于 Runner.resume 注入 user_input 到 tool message
27
- ctx.state["__suspended_tool_call_id__"] = "manual-review-1"
28
- ctx.state["__suspended_tool_name__"] = "manual_review"
26
+ suspension = ctx.register_suspension(
27
+ tool_call_id="manual-review-1",
28
+ tool_name="manual_review",
29
+ prompt="请审批该任务(approve/reject)",
30
+ )
29
31
  yield Event(
30
32
  agent=self.name,
31
33
  type=EventType.SUSPEND_REQUESTED,
32
- data={"prompt": "请审批该任务(approve/reject)", "tool": "manual_review", "tool_call_id": "manual-review-1"},
34
+ data={
35
+ "suspension_id": suspension.suspension_id,
36
+ "prompt": "请审批该任务(approve/reject)",
37
+ "tool": "manual_review",
38
+ "tool_call_id": "manual-review-1",
39
+ },
33
40
  )
34
41
  return
35
42
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ni.agentkit
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: A Python-native Agent framework with first-class Skill support and multi-LLM adapter
5
5
  Author-email: Krix Tam <krix.tam@qq.com>
6
6
  License: MIT
@@ -153,8 +153,8 @@ python "$(python -c "import agentkit, os; print(os.path.join(agentkit.get_exampl
153
153
 
154
154
  ```bash
155
155
  dist/
156
- ├── ni_agentkit-0.6.0-py3-none-any.whl # pip install 用这个
157
- └── ni_agentkit-0.6.0.tar.gz # 源码分发
156
+ ├── ni_agentkit-0.6.2-py3-none-any.whl # pip install 用这个
157
+ └── ni_agentkit-0.6.2.tar.gz # 源码分发
158
158
  ```
159
159
 
160
160
  ## 📄 License
@@ -182,6 +182,9 @@ skills/__init__.py
182
182
  skills/loader.py
183
183
  skills/models.py
184
184
  skills/registry.py
185
+ tests/test_after_callback.py
186
+ tests/test_quickstart_core.py
187
+ tests/test_runner_checkpoint_resume.py
185
188
  tools/__init__.py
186
189
  tools/base_tool.py
187
190
  tools/function_tool.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ni.agentkit"
3
- version = "0.6.0"
3
+ version = "0.6.2"
4
4
  description = "A Python-native Agent framework with first-class Skill support and multi-LLM adapter"
5
5
  authors = [
6
6
  { name = "Krix Tam", email = "krix.tam@qq.com" }
@@ -6,12 +6,27 @@ from __future__ import annotations
6
6
  import copy
7
7
  import json
8
8
  import logging
9
- from dataclasses import dataclass, field
9
+ import time
10
+ from dataclasses import asdict, dataclass, field
10
11
  from typing import Any, Optional
11
12
  from uuid import uuid4
12
13
 
13
14
  logger = logging.getLogger(__name__)
14
15
 
16
+
17
+ @dataclass
18
+ class SuspensionRecord:
19
+ suspension_id: str
20
+ tool_call_id: str
21
+ tool_name: str
22
+ prompt: str
23
+ form_schema: dict[str, Any] | None = None
24
+ resume_strategy: str = "as_tool_result"
25
+ created_at: float = field(default_factory=time.time)
26
+ resolved_at: float | None = None
27
+ resolved_input: str | None = None
28
+
29
+
15
30
  @dataclass
16
31
  class RunContext:
17
32
  """一次运行的完整上下文"""
@@ -30,6 +45,10 @@ class RunContext:
30
45
 
31
46
  # 分支(并行 Agent 用)
32
47
  branch: Optional[str] = None
48
+ # 挂起记录(框架托管,业务侧无需关注内部协议)
49
+ suspensions: list[SuspensionRecord] = field(default_factory=list)
50
+ # resume 幂等记录:idempotency_key -> 已处理结果
51
+ resume_idempotency: dict[str, dict[str, Any]] = field(default_factory=dict)
33
52
 
34
53
  def add_message(self, role: str, content: Any) -> None:
35
54
  self.messages.append({"role": role, "content": content})
@@ -49,6 +68,45 @@ class RunContext:
49
68
  branch_ctx.branch = branch_name
50
69
  return branch_ctx
51
70
 
71
+ def register_suspension(
72
+ self,
73
+ *,
74
+ tool_call_id: str,
75
+ tool_name: str,
76
+ prompt: str,
77
+ form_schema: dict[str, Any] | None = None,
78
+ resume_strategy: str = "as_tool_result",
79
+ ) -> SuspensionRecord:
80
+ record = SuspensionRecord(
81
+ suspension_id=str(uuid4()),
82
+ tool_call_id=tool_call_id,
83
+ tool_name=tool_name,
84
+ prompt=prompt,
85
+ form_schema=form_schema,
86
+ resume_strategy=resume_strategy,
87
+ )
88
+ self.suspensions.append(record)
89
+ return record
90
+
91
+ def get_pending_suspension(self, suspension_id: str | None = None) -> SuspensionRecord | None:
92
+ pending = [s for s in self.suspensions if s.resolved_at is None]
93
+ if not pending:
94
+ return None
95
+ if suspension_id:
96
+ for s in pending:
97
+ if s.suspension_id == suspension_id:
98
+ return s
99
+ return None
100
+ return pending[-1]
101
+
102
+ def resolve_suspension(self, suspension_id: str, user_input: str) -> SuspensionRecord | None:
103
+ for s in self.suspensions:
104
+ if s.suspension_id == suspension_id and s.resolved_at is None:
105
+ s.resolved_at = time.time()
106
+ s.resolved_input = user_input
107
+ return s
108
+ return None
109
+
52
110
  def to_dict(self) -> dict[str, Any]:
53
111
  """将 RunContext 序列化为字典,支持 shared_context 自定义协议"""
54
112
  serialized_shared = None
@@ -74,6 +132,8 @@ class RunContext:
74
132
  "messages": copy.deepcopy(self.messages),
75
133
  "state": copy.deepcopy(self.state),
76
134
  "branch": self.branch,
135
+ "suspensions": [asdict(s) for s in self.suspensions],
136
+ "resume_idempotency": copy.deepcopy(self.resume_idempotency),
77
137
  }
78
138
 
79
139
  @classmethod
@@ -94,6 +154,8 @@ class RunContext:
94
154
  messages=data.get("messages", []),
95
155
  state=data.get("state", {}),
96
156
  branch=data.get("branch"),
157
+ suspensions=[SuspensionRecord(**s) for s in data.get("suspensions", [])],
158
+ resume_idempotency=data.get("resume_idempotency", {}),
97
159
  )
98
160
 
99
161
  def to_json(self) -> str:
@@ -104,4 +166,3 @@ class RunContext:
104
166
  def from_json(cls, json_str: str, shared_context_cls: Optional[Any] = None) -> "RunContext":
105
167
  """从 JSON 字符串反序列化 RunContext"""
106
168
  return cls.from_dict(json.loads(json_str), shared_context_cls)
107
-