flowing-agent 0.1.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.
Files changed (89) hide show
  1. flowing/__init__.py +188 -0
  2. flowing/_unstable/__init__.py +30 -0
  3. flowing/_unstable/logging.py +412 -0
  4. flowing/agent.py +4945 -0
  5. flowing/agent_registry.py +463 -0
  6. flowing/builtins/__init__.py +101 -0
  7. flowing/builtins/agents.py +82 -0
  8. flowing/builtins/tools.py +833 -0
  9. flowing/compiler.py +958 -0
  10. flowing/composables/__init__.py +92 -0
  11. flowing/composables/compact.py +522 -0
  12. flowing/composables/prompt_until.py +268 -0
  13. flowing/composables/reminder.py +269 -0
  14. flowing/composables/retry.py +364 -0
  15. flowing/context.py +979 -0
  16. flowing/errors.py +2299 -0
  17. flowing/hooks.py +1150 -0
  18. flowing/interfaces/__init__.py +350 -0
  19. flowing/interfaces/cli.py +328 -0
  20. flowing/interfaces/controls.py +238 -0
  21. flowing/interfaces/oneshot.py +360 -0
  22. flowing/interfaces/repl.py +602 -0
  23. flowing/interfaces/repl_debug.py +220 -0
  24. flowing/interfaces/run.py +190 -0
  25. flowing/interfaces/serve.py +753 -0
  26. flowing/interfaces/web.py +319 -0
  27. flowing/interfaces/webui-dist/index.html +189 -0
  28. flowing/lists.py +387 -0
  29. flowing/media.py +551 -0
  30. flowing/message.py +2008 -0
  31. flowing/model.py +445 -0
  32. flowing/params.py +801 -0
  33. flowing/parsable.py +1151 -0
  34. flowing/parser.py +548 -0
  35. flowing/paths.py +561 -0
  36. flowing/persistence.py +871 -0
  37. flowing/plugins/__init__.py +280 -0
  38. flowing/plugins/clipboard/__init__.py +25 -0
  39. flowing/plugins/clipboard/clipboard.py +193 -0
  40. flowing/plugins/clipboard/tools.py +394 -0
  41. flowing/plugins/comm/__init__.py +24 -0
  42. flowing/plugins/comm/comm.py +1230 -0
  43. flowing/plugins/comm/models.py +154 -0
  44. flowing/plugins/cron/__init__.py +27 -0
  45. flowing/plugins/cron/cron.py +151 -0
  46. flowing/plugins/cron/jobs.py +467 -0
  47. flowing/plugins/cron/models.py +159 -0
  48. flowing/plugins/cron/tools.py +154 -0
  49. flowing/plugins/skills/__init__.py +51 -0
  50. flowing/plugins/skills/models.py +469 -0
  51. flowing/plugins/skills/registry.py +488 -0
  52. flowing/plugins/skills/skills.py +1006 -0
  53. flowing/plugins/workflow/__init__.py +23 -0
  54. flowing/plugins/workflow/loader.py +212 -0
  55. flowing/plugins/workflow/plugin.py +246 -0
  56. flowing/plugins/workflow/workflow.py +502 -0
  57. flowing/provide.py +167 -0
  58. flowing/providers/__init__.py +238 -0
  59. flowing/providers/anthropic.py +50 -0
  60. flowing/providers/anthropic_messages.py +650 -0
  61. flowing/providers/bedrock.py +66 -0
  62. flowing/providers/deepseek.py +99 -0
  63. flowing/providers/deepseek_anthropic.py +59 -0
  64. flowing/providers/deepseek_responses.py +68 -0
  65. flowing/providers/groq.py +48 -0
  66. flowing/providers/kimi_coding.py +74 -0
  67. flowing/providers/kimi_coding_anthropic.py +68 -0
  68. flowing/providers/moonshot.py +59 -0
  69. flowing/providers/moonshot_anthropic.py +61 -0
  70. flowing/providers/moonshot_responses.py +60 -0
  71. flowing/providers/openai_completions.py +691 -0
  72. flowing/providers/openai_responses.py +713 -0
  73. flowing/providers/openrouter.py +107 -0
  74. flowing/providers/provider.py +1134 -0
  75. flowing/runtime.py +2518 -0
  76. flowing/snapshot.py +598 -0
  77. flowing/subagents.py +680 -0
  78. flowing/tool/__init__.py +190 -0
  79. flowing/tool/_env.py +44 -0
  80. flowing/tool/cli.py +184 -0
  81. flowing/tool/core.py +1226 -0
  82. flowing/tool/mcp.py +326 -0
  83. flowing/tool/registry.py +729 -0
  84. flowing/tool/request.py +237 -0
  85. flowing/tool/script.py +310 -0
  86. flowing_agent-0.1.0.dist-info/METADATA +156 -0
  87. flowing_agent-0.1.0.dist-info/RECORD +89 -0
  88. flowing_agent-0.1.0.dist-info/WHEEL +4 -0
  89. flowing_agent-0.1.0.dist-info/entry_points.txt +3 -0
flowing/__init__.py ADDED
@@ -0,0 +1,188 @@
1
+ """``flowing`` —— 轻量式 Agent 框架:最终 API 规约(顶层导出)。
2
+
3
+ .. rubric:: 功能介绍
4
+
5
+ Flowing 的核心立场是“框架只提供机制,不提供策略”。包结构三层:
6
+
7
+ - **框架核心**:``runtime`` / ``agent`` / ``agent_registry`` /
8
+ ``subagents`` / ``message`` / ``context`` /
9
+ ``parsable`` / ``params`` / ``tool``(子包)/ ``media`` / ``model`` /
10
+ ``hooks`` / ``lists`` / ``errors`` /
11
+ ``snapshot`` / ``providers`` / ``persistence`` / ``parser`` / ``paths`` /
12
+ ``provide`` / ``compiler`` —— 本模块顶层导出其中面向日常使用的符号。
13
+ - **内置扩展**:``flowing.plugins``(``skills`` / ``comm`` / ``cron`` /
14
+ ``workflow`` / ``clipboard``)——随包发布、显式 ``runtime.install(...)`` 启用。
15
+ - **应用层**:``flowing.composables``(以 ``use_xxx(agent, ...)`` 函数向
16
+ Agent 装配应用逻辑与策略)。
17
+
18
+ 运行模型锚点:消息级树(``Message.id`` + ``parent_id`` 链,
19
+ ``current_head_id`` 指向消息 id);Turn 仅为逻辑执行阶段(执行期载体
20
+ :class:`flowing.agent.TurnContext`,不落盘、不进树);三层能力描述
21
+ (可执行对象 / LLM 可见声明 / Agent 级绑定,推广到 Tool / 子 Agent /
22
+ Skill);provide-inject 沿 ``_parent_id`` 链上溯;实例级钩子系统。
23
+
24
+ .. rubric:: 设计动机
25
+
26
+ 顶层导出收敛到“写一个 Agent 项目一定会 import”的最小集合;其余符号
27
+ (异常明细、快照视图、内部容器)经子模块显式导入,保持顶层命名空间
28
+ 可读、可记忆。
29
+
30
+ .. rubric:: 使用示例
31
+
32
+ .. code-block:: python
33
+
34
+ # @/main.py —— 子项目入口约定
35
+ import flowing
36
+ from flowing import Runtime, on
37
+
38
+ async def main(**kwargs) -> Runtime:
39
+ runtime = flowing.Runtime() # @ 由 launch 上下文自动绑定
40
+ runtime.install(...) # 阶段一:安装扩展
41
+ await runtime.mount("@/root.fya") # 创建根 Agent
42
+ return runtime
43
+
44
+ .. code-block:: bash
45
+
46
+ flowing run <path> # launch(path) → await runtime → SIGINT → shutdown
47
+
48
+ .. rubric:: 行为规约
49
+
50
+ - ``flowing.launch(path, **kwargs)`` 是 Runtime 的**唯一创建入口**;
51
+ 绕过它直接 ``Runtime()`` 因无 ``@`` 上下文抛 ``RuntimeError``。
52
+ - 本模块只做 re-export,不定义任何新符号;各符号的完整契约见所属
53
+ 子模块的规约。
54
+ - 稳定性:本模块导出的全部符号属跨版本稳定契约;``_`` 前缀符号与
55
+ ``flowing.interfaces.cli`` / ``flowing.interfaces.web`` 的细节不属稳定边界。
56
+ """
57
+
58
+ from flowing.agent import (
59
+ Agent,
60
+ CancelContext,
61
+ Execution,
62
+ FieldUpdate,
63
+ ProviderErrorContext,
64
+ TurnContext,
65
+ TurnResult,
66
+ )
67
+ from flowing.agent_registry import AgentRegistry
68
+ from flowing.context import Context, PromptBlock, PromptBlockList, PromptSegment
69
+ from flowing.errors import FlowingError, Intercepted
70
+ from flowing.hooks import HookRegistry, on
71
+ from flowing.lists import ManagedList
72
+ from flowing.message import (
73
+ ContentBlock,
74
+ FileBlock,
75
+ ImageBlock,
76
+ MediaBlock,
77
+ Message,
78
+ MessageChain,
79
+ MessageKind,
80
+ MessagePriority,
81
+ MessageQueue,
82
+ StructBlock,
83
+ TextBlock,
84
+ ThinkingBlock,
85
+ ToolCallBlock,
86
+ )
87
+ from flowing.model import (
88
+ ModelConfig,
89
+ )
90
+ from flowing.params import ConfigKey, InjectionKey
91
+ from flowing.parsable import PENDING, Parsable
92
+ from flowing.plugins import Plugin
93
+ from flowing.provide import ProvideNode
94
+ from flowing.providers import (
95
+ Provider,
96
+ ProviderConfig,
97
+ ProviderDelta,
98
+ ProviderResponse,
99
+ Usage,
100
+ )
101
+ from flowing.builtins import FinishTool, SubagentInvokeTool
102
+ from flowing.runtime import Runtime, launch, resolve
103
+ from flowing.subagents import SubagentEntry, SubagentInvocation, SubagentResult
104
+ from flowing.tool import (
105
+ Audio,
106
+ File,
107
+ Image,
108
+ ScriptTool,
109
+ Tool,
110
+ ToolCall,
111
+ ToolDefinition,
112
+ ToolEntry,
113
+ ToolRegistry,
114
+ ToolResult,
115
+ Video,
116
+ flowing_tool,
117
+ normalize_output,
118
+ output_to_blocks,
119
+ register_media_converter,
120
+ )
121
+
122
+ __all__ = [
123
+ "Agent",
124
+ "AgentRegistry",
125
+ "Audio",
126
+ "CancelContext",
127
+ "ConfigKey",
128
+ "ContentBlock",
129
+ "Context",
130
+ "Execution",
131
+ "FieldUpdate",
132
+ "File",
133
+ "FileBlock",
134
+ "FinishTool",
135
+ "FlowingError",
136
+ "ScriptTool",
137
+ "HookRegistry",
138
+ "Image",
139
+ "ImageBlock",
140
+ "InjectionKey",
141
+ "Intercepted",
142
+ "ManagedList",
143
+ "MediaBlock",
144
+ "Message",
145
+ "MessageChain",
146
+ "MessageKind",
147
+ "MessagePriority",
148
+ "MessageQueue",
149
+ "ModelConfig",
150
+ "PENDING",
151
+ "Parsable",
152
+ "Plugin",
153
+ "PromptBlock",
154
+ "PromptBlockList",
155
+ "PromptSegment",
156
+ "ProvideNode",
157
+ "Provider",
158
+ "ProviderConfig",
159
+ "ProviderDelta",
160
+ "ProviderResponse",
161
+ "ProviderErrorContext",
162
+ "Runtime",
163
+ "StructBlock",
164
+ "SubagentEntry",
165
+ "SubagentInvocation",
166
+ "SubagentInvokeTool",
167
+ "SubagentResult",
168
+ "TextBlock",
169
+ "ThinkingBlock",
170
+ "Tool",
171
+ "ToolCall",
172
+ "ToolCallBlock",
173
+ "ToolDefinition",
174
+ "ToolEntry",
175
+ "ToolRegistry",
176
+ "ToolResult",
177
+ "TurnContext",
178
+ "TurnResult",
179
+ "Usage",
180
+ "Video",
181
+ "flowing_tool",
182
+ "launch",
183
+ "normalize_output",
184
+ "on",
185
+ "output_to_blocks",
186
+ "register_media_converter",
187
+ "resolve",
188
+ ]
@@ -0,0 +1,30 @@
1
+ """``flowing._unstable`` —— 实验性命名空间:可导入,但不冻结。
2
+
3
+ .. rubric:: 功能介绍
4
+
5
+ 本包收容“功能已确定、策略 / 接口形态未确定”的自用设施。与
6
+ :mod:`flowing.plugins` 的区别在稳定性承诺:
7
+
8
+ - ``flowing.plugins.*``:内置扩展,契约随框架版本冻结(签名、语义、
9
+ 持久化格式按正常版本纪律演进)。
10
+ - ``flowing._unstable.*``:允许在任何版本中改签名、改名、改语义、
11
+ 整体删除或迁出,不视为 breaking change,不发迁移通告。下划线
12
+ 前缀同时挡住自动导入与“稳定 API”的心理预期。
13
+
14
+ .. rubric:: 毕业规则
15
+
16
+ 某设施的策略确定后,整体迁往正式包(如 ``flowing.plugins``)并在本
17
+ 包留一个 re-export 过渡(仅当下一个 minor 版本);迁入正式包后按
18
+ 正常稳定性纪律管理。
19
+
20
+ .. rubric:: 使用约定
21
+
22
+ - 仅自用 / 内部 debug 场景;下游发布物(插件、workflow、技能包)
23
+ 禁止依赖本包。
24
+ - 本包模块的持久化产物(如日志文件)不构成恢复依赖——框架任何恢复
25
+ 路径不得读取 ``_unstable`` 设施写出的文件。
26
+
27
+ 当前内容::mod:`flowing._unstable.logging` (钩子链路日志插件)。
28
+ """
29
+
30
+ __all__: list[str]
@@ -0,0 +1,412 @@
1
+ """``flowing._unstable.logging`` —— 钩子链路日志插件(LoggingPlugin / use_logging)。
2
+
3
+ .. rubric:: 功能介绍
4
+
5
+ 实验性、自用 debug 辅助(见 :mod:`flowing._unstable` 的稳定性承诺:
6
+ 接口与落盘格式均不冻结)。功能范围是确定的:把 Agent 钩子链路的触发
7
+ 事实按等级写入每 Agent 一个的 ``logging.jsonl``;默认日志策略未定
8
+ (记录哪些字段、摘要如何截断、是否滚动都是开放项,本文件只固定最小
9
+ 骨架)。
10
+
11
+ 完全在框架核心之外,只复用 ``runtime.install()`` 安装、``agent.hooks``
12
+ 实例级钩子、``Runtime.get_plugin()`` 插件探测等公开或半公开机制,
13
+ 核心不感知本插件的存在。
14
+
15
+ .. rubric:: 注册面清单
16
+
17
+ - 启用方式(双层启用,与内置扩展同构):
18
+
19
+ - 阶段一(Runtime 安装):``runtime.install(LoggingPlugin(level="INFO"))``
20
+ —— ``install()`` 经 ``runtime.provide(logging_plugin_key, self)``
21
+ 提供自身,并在安装期一次性探测四个内置扩展是否已装(运行期不
22
+ 重复探测)。重复安装同名插件按框架既有规则报错(一个 Runtime
23
+ 同时只装一个同名插件,见 :meth:`flowing.runtime.Runtime.install`)。
24
+ - 阶段二(Agent 启用):``setup()`` 中 ``use_logging(self)`` —— 经
25
+ ``agent.inject(logging_plugin_key)`` 取回插件实例并按当前等级挂
26
+ handler。未安装本插件时调用抛
27
+ :class:`flowing.errors.MissingProvideError` (不静默跳过)。未调用
28
+ ``use_logging()`` 的 Agent 零开销、零输出。
29
+
30
+ - 注册的资源:provide key ``logging_plugin_key`` (字符串
31
+ ``"logging:plugin"``),provide 到 Runtime 根,消费方式为
32
+ ``agent.inject(logging_plugin_key)`` 沿亲代链上溯查找;未安装时抛
33
+ :class:`flowing.errors.MissingProvideError`。
34
+
35
+ - 声明的钩子点:无——本插件不声明任何新钩子点,只向既有钩子点挂
36
+ 观察 handler。
37
+
38
+ - 挂载的钩子:按等级向各钩子点挂纯观察 handler,统一 ``by="logging"``
39
+ (整组可经 ``remove_by_owner("logging")`` 移除):
40
+
41
+ - INFO 关键节点:``use_logging`` 立即挂钩(21 点清单见“等级语义”);
42
+ - DEBUG 全集与扩展钩子点:延迟到内部 ``after_create`` /
43
+ ``after_recover`` handler 中挂(此时其它 ``use_*()`` 已声明完各自
44
+ 钩子点,枚举才能覆盖全集;recover 由 ``after_recover`` 兜底重挂,
45
+ 幂等去重)。
46
+
47
+ - 作用域与副作用:只影响启用它的 Agent 实例——落盘到该 Agent 的
48
+ session 目录;不写 ``tree.jsonl`` / ``state.jsonl``,不参与恢复;不写
49
+ Runtime 级全局日志。写盘失败静默降级(见“落盘”),绝不打断业务
50
+ 管线。
51
+
52
+ .. rubric:: 等级语义(全局单档,非标准 logging 级别体系)
53
+
54
+ 等级是插件实例级全局开关(``LoggingPlugin.level``,运行期可改,下一
55
+ 次钩子触发即生效——已挂的 handler 每次触发时读当前等级,不缓存):
56
+
57
+ - ``"OFF"``:什么都不输出。
58
+ - ``"INFO"`` (默认):关键节点各记一行——生命周期三对(``before_create`` /
59
+ ``after_create`` / ``before_recover`` / ``after_recover`` /
60
+ ``before_destroy`` / ``after_destroy``)、逻辑 turn 边界(``before_turn`` /
61
+ ``after_turn``)、消息出入队(``on_enqueue`` / ``on_dequeue``)、
62
+ LLM 调用边界(``before_provider_gen`` / ``after_provider_gen`` /
63
+ ``on_provider_error``)、工具调用边界(``before_tool_call`` /
64
+ ``after_tool_call``)、子 Agent(``on_subagent_invoke`` /
65
+ ``on_subagent_returns``)、取消与 fork(``before_cancel`` /
66
+ ``after_cancel`` / ``on_fork``)——共 20 点。
67
+ value 只记摘要(类型名 + 标识字段,如工具名 / 消息 id)。
68
+ - ``"DEBUG"``:在 INFO 基础上,该 Agent 实例上存在的全部钩子点都
69
+ 输出(枚举 ``agent.hooks`` 的已声明点),value 记 ``repr`` 截断
70
+ (默认 500 字符,构造参数可调);流式增量 ``on_provider_delta``
71
+ 在 DEBUG 下同样逐条输出(高频,自用场景自负)。
72
+
73
+ .. rubric:: 插件探测
74
+
75
+ ``install()`` 时经 ``runtime.get_plugin()`` 检测 ``"skill"`` /
76
+ ``"comm"`` / ``"cron"`` / ``"workflow"`` 四个内置扩展是否已安装,
77
+ 结果记入 :attr:`LoggingPlugin.detected`。语义只是“白名单放行”:
78
+ 被探测到的扩展的钩子点(``before_skill_load`` / ``after_skill_load`` /
79
+ ``on_signal`` / ``on_event`` / ``on_cron_trigger``;workflow 无实例级
80
+ 钩子点)在该 Agent 已声明的前提下纳入 DEBUG 全集与 INFO 关键节点表;
81
+ 未安装的扩展对应点名不登记、不访问(避免对未声明的点挂钩抛
82
+ :class:`flowing.errors.UnknownHookPointError`)。探测只回答“能不能
83
+ 理”,不替 Agent 声明钩子点。
84
+
85
+ .. rubric:: 落盘
86
+
87
+ 每个启用 Agent 一个文件:``<该 Agent session 目录>/logging.jsonl``
88
+ (session 目录 = ``tree.jsonl`` / ``state.jsonl`` 所在目录)。
89
+ append-only、一行一个 JSON object::
90
+
91
+ {"ts": "2026-08-18T06:47:56.565Z", "agent_id": "...", "level": "INFO",
92
+ "hook": "before_tool_call", "value": "ToolCall(name='search', id='t1')",
93
+ "handler_tag": null}
94
+
95
+ - 写入失败(磁盘满 / 目录被删)静默降级:打印一次 stderr 警告后,
96
+ 该 Agent 后续不再尝试写盘——日志插件绝不打断业务管线。
97
+ - 不做滚动 / 压缩 / 清理(策略未定,自用阶段手工处理)。
98
+
99
+ .. seealso:: :mod:`flowing._unstable`、:class:`LoggingPlugin`、:func:`use_logging`
100
+ """
101
+
102
+ import json
103
+ import sys
104
+ from datetime import datetime, timezone
105
+ from enum import Enum
106
+ from typing import Any, ClassVar, Literal
107
+
108
+ from flowing.agent import Agent
109
+ from flowing.errors import FlowingError
110
+ from flowing.plugins import Plugin
111
+ from flowing.runtime import Runtime
112
+
113
+ logging_plugin_key: str = "logging:plugin"
114
+ """provide key:``use_logging`` 经 ``inject`` 取回
115
+ :class:`LoggingPlugin` 实例的键,字符串值即 ``"logging:plugin"``。
116
+ """
117
+
118
+ LogLevel = Literal["OFF", "INFO", "DEBUG"]
119
+ """全局等级字面量:``"OFF"`` / ``"INFO"`` / ``"DEBUG"``。
120
+ 语义见模块 docstring「等级语义」。
121
+ """
122
+
123
+ _LEVELS: tuple[str, ...] = ("OFF", "INFO", "DEBUG")
124
+ """合法等级集合(``LoggingPlugin.level`` setter 的校验依据)。"""
125
+
126
+ _INFO_HOOK_POINTS: tuple[str, ...] = (
127
+ # 生命周期三对(before_create/before_recover 在 setup 前 dispatch,
128
+ # 挂载发生于 setup 内,创建/恢复管线上实际观察不到,挂载仅为对称完整)
129
+ "before_create", "after_create",
130
+ "before_recover", "after_recover",
131
+ "before_destroy", "after_destroy",
132
+ # 逻辑 turn 边界
133
+ "before_turn", "after_turn",
134
+ # 消息出入队
135
+ "on_enqueue", "on_dequeue",
136
+ # LLM 调用边界
137
+ "before_provider_gen", "after_provider_gen", "on_provider_error",
138
+ # 工具调用边界
139
+ "before_tool_call", "after_tool_call",
140
+ # 子 Agent
141
+ "on_subagent_invoke", "on_subagent_returns",
142
+ # 取消与 fork
143
+ "before_cancel", "after_cancel",
144
+ "on_fork",
145
+ )
146
+ """INFO 级关键节点清单(20 点,``use_logging`` 立即挂钩;模块 docstring
147
+ 「等级语义」的落地名表)。"""
148
+
149
+ _EXTENSION_HOOK_POINTS: dict[str, tuple[str, ...]] = {
150
+ "skill": ("before_skill_load", "after_skill_load"),
151
+ "comm": ("on_signal", "on_event"),
152
+ "cron": ("on_cron_trigger",),
153
+ # workflow 无实例级钩子点声明(install 仅注册 run-workflow 工具,
154
+ # Workflow 自持独立 HookRegistry,不经 Agent.hooks 枚举抵达)
155
+ "workflow": (),
156
+ }
157
+ """插件探测白名单:已安装内置扩展名 → 其声明的扩展钩子点。``detected``
158
+ 命中且该 Agent 已 ``declare`` 时,``_attach_debug`` 才挂钩。"""
159
+
160
+
161
+ def _utc_ts() -> str:
162
+ """ISO 8601 UTC 毫秒精度时间戳(``Z`` 后缀,与落盘样例行同形态)。"""
163
+ return (datetime.now(timezone.utc).isoformat(timespec="milliseconds")
164
+ .replace("+00:00", "Z"))
165
+
166
+
167
+ class LoggingPlugin(Plugin):
168
+ """钩子链路日志插件:等级开关 + 插件探测 + 落盘器。
169
+
170
+ ``runtime.install(LoggingPlugin(level=...))`` 安装(阶段一)。持有全局
171
+ 等级与探测结果;``use_logging()`` 挂的 handler 每次触发时回读本
172
+ 实例的当前等级。
173
+
174
+ .. rubric:: 使用示例
175
+
176
+ .. code-block:: python
177
+
178
+ runtime.install(LoggingPlugin(level="DEBUG"))
179
+ # Agent 侧:setup() 中 use_logging(self)
180
+ # 运行期调级:
181
+ runtime.get_plugin("logging").level = "INFO"
182
+
183
+ .. rubric:: 行为要点
184
+
185
+ - ``install()`` 只注册(provide 自身 + 记录探测结果),不挂钩——
186
+ 钩子是 per-agent 的,属 :func:`use_logging` 的职责。
187
+ - ``level`` 运行期可写(property setter 校验),写后下一次钩子
188
+ 触发即生效;非法值抛 :class:`flowing.errors.FlowingError`。
189
+ - 重复安装(再次 ``runtime.install(LoggingPlugin())``)→ 同名插件
190
+ 冲突,按框架既有规则报错(一个 Runtime 同时只装一个同名插件)。
191
+
192
+ .. seealso:: :func:`use_logging`、模块 docstring“注册面清单”。
193
+ """
194
+
195
+ name: ClassVar[str] = "logging"
196
+ """注册名(显式声明;推荐格式见 ``flowing.plugins`` 命名约定)。
197
+ """
198
+
199
+ namespace: ClassVar[str] = "logging"
200
+ """provide key 的命名空间前缀(与 :data:`logging_plugin_key` 的
201
+ ``"logging:"`` 前缀一致)。
202
+ """
203
+
204
+ dependencies: ClassVar[list[str]] = [] # 与基类 Plugin 的 ClassVar 对齐
205
+ """依赖声明(类属性元数据)。本插件无依赖,为空列表。
206
+ """
207
+
208
+ detected: frozenset[str]
209
+ """install 时探测到的已安装内置扩展名集合(``"skill"`` 等的子集)。
210
+ 只读观测面,供 ``use_logging`` 与调试者查询。
211
+ """
212
+
213
+ max_value_repr: int
214
+ """DEBUG 级 value ``repr`` 的截断长度(默认 500)。
215
+ """
216
+
217
+ def __init__(self, level: LogLevel = "INFO", *, max_value_repr: int = 500) -> None:
218
+ """构造插件实例。
219
+
220
+ :param level: 初始全局等级,默认 ``"INFO"`` (经 setter 校验,
221
+ 非法值抛 :class:`flowing.errors.FlowingError`)。
222
+ :param max_value_repr: DEBUG 级 value 摘要的 repr 截断长度。
223
+ """
224
+ self.level = level # 走 setter:初始等级同样过校验
225
+ self.max_value_repr = max_value_repr
226
+ # self.detected 由 install() 探测后写入(时序见模块 docstring)
227
+
228
+ @property
229
+ def level(self) -> LogLevel:
230
+ """全局等级。运行期可改;handler 每次触发时回读,不缓存。
231
+
232
+ 写入非法值(非 ``"OFF"`` / ``"INFO"`` / ``"DEBUG"``)抛
233
+ :class:`flowing.errors.FlowingError`。
234
+ """
235
+ return self._level
236
+
237
+ @level.setter
238
+ def level(self, value: LogLevel) -> None:
239
+ if value not in _LEVELS:
240
+ raise FlowingError(
241
+ f"invalid log level: {value!r} (valid values: {', '.join(_LEVELS)})")
242
+ self._level = value
243
+
244
+ def install(self, runtime: Runtime) -> None:
245
+ """provide 自身(``logging_plugin_key``)并一次性探测四个内置扩展。
246
+
247
+ 只注册,不做任何 per-agent 挂钩——钩子是 per-agent 的,属
248
+ :func:`use_logging` 的职责。探测须显式 ``strict=False``
249
+ (默认形态下未安装的扩展会抛 ``KeyError``)。
250
+
251
+ :param runtime: 当前安装的 Runtime。
252
+ """
253
+ runtime.provide(logging_plugin_key, self)
254
+ # 探测 skill/comm/cron/workflow 四扩展(有意探测 → 显式 strict=False),
255
+ # 结果记入 self.detected(时序见模块 docstring);未安装的点名不登记
256
+ self.detected = frozenset(
257
+ name for name in _EXTENSION_HOOK_POINTS
258
+ if runtime.get_plugin(name, strict=False) is not None)
259
+
260
+
261
+ def use_logging(agent: Agent) -> None:
262
+ """阶段二启用:在 ``setup()`` 中调用,按插件当前等级为本 Agent 挂钩。
263
+
264
+ 同步函数——``inject`` 查表与钩子注册均为同步操作,无 ``await``
265
+ 需求。
266
+
267
+ .. rubric:: 功能介绍
268
+
269
+ 经 ``agent.inject(logging_plugin_key)`` 取回 :class:`LoggingPlugin`
270
+ 实例;未安装本插件时抛 :class:`flowing.errors.MissingProvideError`
271
+ (不静默跳过)。
272
+
273
+ .. rubric:: 挂钩策略
274
+
275
+ - INFO 关键节点(清单见模块 docstring“等级语义”):立即挂到各
276
+ 钩子点,``by="logging"``;handler 内早退判断当前等级。
277
+ - DEBUG 全集与扩展钩子点:延迟到一个内部 ``after_create``
278
+ handler(``by="logging"``)中执行——此刻其它 ``use_*()`` 已
279
+ 声明完各自钩子点,枚举该实例已声明的全部钩子点才能覆盖全集;
280
+ 同时消除“``use_logging`` 必须在其它 ``use_*`` 之后调用”的
281
+ 顺序约束。recover 管线同理由 ``after_recover`` 内部 handler
282
+ 兜底重挂(幂等:已挂过的点名不重复挂钩)。
283
+ - 所有 handler 为纯观察:不修改 value(原样透传返回)、不
284
+ ``raise Intercepted``;自身异常捕获后置为 stderr 警告(不打断
285
+ 管线)。
286
+
287
+ .. rubric:: 行为要点
288
+
289
+ - ``after_provider_gen`` 的 value(``ProviderResponse``)的
290
+ ``usage`` 概要以 :attr:`flowing.message.Message.usage` 为准,
291
+ 不在摘要中复述;``on_provider_error`` 记异常类型名。
292
+ - ``on_enqueue`` 被 ``Intercepted`` 拦截的结果无从观察属
293
+ 已知限制(拦截发生在 dispatch 内,观察点只见到“没触发
294
+ 后续 handler”)。
295
+ - 不声明任何新钩子点;不写 state(无恢复义务)。
296
+
297
+ :param agent: 启用日志的 Agent(``setup()`` 中的 ``self``)。
298
+
299
+ .. seealso:: :class:`LoggingPlugin`、模块 docstring“落盘”。
300
+ """
301
+ # 经 inject 沿链上溯取回插件实例;未安装本插件时抛
302
+ # flowing.errors.MissingProvideError(不静默跳过)
303
+ plugin: LoggingPlugin = agent.inject(logging_plugin_key)
304
+ # session 目录取 Agent 管线预绑的 _session_dir(tree.jsonl /
305
+ # state.jsonl 所在目录),不在本插件重写路径拼接
306
+ log_path = agent._session_dir / "logging.jsonl"
307
+ # per-agent 闭包状态:写盘降级标记 + 已挂点名集(use_logging 随
308
+ # setup 在每实例上跑一次,天然按实例隔离;恢复换新实例后
309
+ # setup 在新实例上执行,闭包随之重建)
310
+ state: dict[str, bool] = {"write_failed": False}
311
+ attached: set[str] = set()
312
+
313
+ def _summarize(value: Any) -> Any:
314
+ # INFO 摘要:类型名 + 标识字段(如 tool 名 / 消息 id);
315
+ # 无 value 钩子点记 None;on_provider_error 记异常类型名
316
+ if value is None:
317
+ return None
318
+ if isinstance(value, (str, int, float, bool)):
319
+ return value
320
+ parts: list[str] = []
321
+ for field_name in ("id", "name", "kind", "type", "topic", "source", "reason"):
322
+ field_value = getattr(value, field_name, None)
323
+ if field_value is None:
324
+ continue
325
+ if isinstance(field_value, Enum):
326
+ field_value = field_value.value
327
+ parts.append(f"{field_name}={field_value!r}")
328
+ error = getattr(value, "error", None)
329
+ if isinstance(error, BaseException):
330
+ parts.append(f"error={type(error).__name__}")
331
+ if not parts:
332
+ return type(value).__name__
333
+ return f"{type(value).__name__}({', '.join(parts)})"
334
+
335
+ def _make_observer(hook_name: str):
336
+ # 按点名构造观察 handler(落盘行的 "hook" 字段需要点名,
337
+ # dispatch 不透传钩子点名,故逐点绑定)
338
+
339
+ async def _observe(host: Agent, value: Any = None) -> Any:
340
+ # 纯观察 handler:每次触发回读 plugin.level 当前值(不缓存),
341
+ # "OFF" 早退;INFO 记摘要,DEBUG 记 repr 截断 max_value_repr;
342
+ # 追加 logging.jsonl 一行;写盘失败静默降级(一次性 stderr
343
+ # 警告后不再尝试);自身异常置 stderr 警告,不打断管线
344
+ try:
345
+ level = plugin.level
346
+ if level == "OFF" or state["write_failed"]:
347
+ return value
348
+ rendered = (repr(value)[: plugin.max_value_repr]
349
+ if level == "DEBUG" else _summarize(value))
350
+ line = json.dumps(
351
+ {"ts": _utc_ts(), "agent_id": host.node_id,
352
+ "level": level, "hook": hook_name,
353
+ "value": rendered, "handler_tag": None},
354
+ ensure_ascii=False, default=repr)
355
+ try:
356
+ with open(log_path, "a", encoding="utf-8") as f:
357
+ f.write(line + "\n")
358
+ except OSError as exc:
359
+ # 写盘失败(磁盘满 / 目录被删)静默降级:一次性
360
+ # stderr 警告后该 Agent 后续不再尝试写盘
361
+ state["write_failed"] = True
362
+ print(f"[flowing._unstable.logging] agent {host.node_id} "
363
+ f"failed to write log, will not retry {log_path}: {exc!r}",
364
+ file=sys.stderr)
365
+ except Exception as exc:
366
+ # handler 自身异常:stderr 警告,不传播(日志插件绝不能
367
+ # 打断业务管线)
368
+ print(f"[flowing._unstable.logging] agent {host.node_id} "
369
+ f"observer handler raised (hook={hook_name}, swallowed): {exc!r}",
370
+ file=sys.stderr)
371
+ return value
372
+
373
+ return _observe
374
+
375
+ def _attach(hook_name: str) -> None:
376
+ if hook_name in attached:
377
+ return # 幂等去重:已挂过的点名不重复挂钩
378
+ attached.add(hook_name)
379
+ agent.hooks._hook_points[hook_name](
380
+ _make_observer(hook_name), by="logging")
381
+
382
+ # INFO 关键节点立即挂钩(by="logging",清单见模块 docstring「等级语义」;
383
+ # handler 内早退判断当前等级)
384
+ for _name in _INFO_HOOK_POINTS:
385
+ _attach(_name)
386
+
387
+ async def _attach_debug(host: Agent, _value: Any = None) -> None:
388
+ # DEBUG 全集与插件钩子点的延迟挂钩:挂载面按当前等级分流
389
+ # (只影响「挂不挂」;等级判定统一收敛在 handler 触发时回读,
390
+ # 与早退逻辑叠加不冲突)
391
+ level = plugin.level
392
+ if level == "OFF":
393
+ return
394
+ if level == "DEBUG":
395
+ # 枚举 agent.hooks._hook_points(内部 API),对该实例已存在
396
+ # 的全部钩子点挂观察 handler(含 on_provider_delta 逐条输出;
397
+ # 幂等去重由 _attach 承担)
398
+ for name in agent.hooks._hook_points:
399
+ _attach(name)
400
+ return
401
+ # INFO:只挂 detected 白名单放行的扩展钩子点,且以该 Agent
402
+ # 已 declare 为前提(枚举自然跳过未声明的点,不触发
403
+ # UnknownHookPointError)
404
+ for ext_name in plugin.detected:
405
+ for name in _EXTENSION_HOOK_POINTS[ext_name]:
406
+ if name in agent.hooks._hook_points:
407
+ _attach(name)
408
+
409
+ # 延迟到 after_create / after_recover:此刻其它 use_*() 已 declare 完
410
+ # 各自钩子点,枚举才能覆盖全集;recover 管线由 after_recover 兜底重挂
411
+ agent.hooks.after_create(_attach_debug, by="logging")
412
+ agent.hooks.after_recover(_attach_debug, by="logging")