Embeddable agent execution kernel — LLM loop, hooks, events, tools, dynamic sub-agents.
Project description
power-loop
可嵌入的、有状态的 Agent 执行内核。 调用方只管「给一段最新输入 + session_id」, power-loop 自治管理:LLM 多轮循环、工具调用、上下文压缩、子代理、消息持久化(SQLite)、 悬挂态恢复。
Python ≥ 3.10 · MIT · OpenAI 兼容 + Anthropic 双家底 · 零强依赖(SQLite 用 stdlib)
from power_loop import StatefulAgentLoop, AgentLoopConfig
loop = StatefulAgentLoop(
llm=my_llm,
db_path="./sessions.db",
config=AgentLoopConfig(system_prompt="You are helpful.", max_rounds=8),
)
r1 = await loop.send("hello") # 自动新建 session
r2 = await loop.send("more please", session_id=r1.session_id) # 续话
loop.close_session(r1.session_id) # 物理删除
目录
- 1. 它是什么 / 不是什么
- 2. 安装
- 3. Quickstart
- 4. 核心概念
- 5. API 参考
- 6. Examples
- 7. 配置(环境变量)
- 8. 内部机制
- 9. 测试
- 10. Roadmap & Changelog
1. 它是什么 / 不是什么
是:
- 一个 Python 库(非服务),可被任意后端 / CLI / 测试
import使用。 - 唯一公开入口
StatefulAgentLoop:有状态、send/resume/abort_pending/close_session四个动词。 - 持久化层
SessionStore:SQLite,5 张表,承诺消息顺序、悬挂态、压缩审计的所有不变量。 - 工具循环 + 生命周期 Hook + 事件总线 + 子代理(命令式 + 声明式
AgentSpec)。 - 上下文压缩:可插拔
Compactor协议,自带DefaultCompactor默认开启。
不是:
- 不是 IM / 业务服务。不感知会话、用户、Kafka、HTTP;这些放在调用方。
- 不是大而全的 Agent Framework。没有内置 RAG / 向量库 / Planner / DAG。
- 不绑死任何模型厂商:
base_url/api_key/model都通过配置传入。
2. 安装
pip install -e . # 开发安装
pip install -e ".[dev]" # 含 pytest / ruff / mypy
依赖(见 pyproject.toml):openai、anthropic、socksio、python-dotenv、pyyaml、rich、pypdf。SQLite 用 Python 标准库,零新增。
3. Quickstart
本节按由浅入深的顺序排:每一步只引入一个新概念。每个代码片段都对应 examples/
下一个可独立运行的文件。先按顺序读完,再回头看 §4 的核心概念会很顺。
3.1 第一次发送(最小用法)
最少的样板:构造 LLM → 构造 loop → send。
import asyncio
from power_loop import StatefulAgentLoop
async def main():
loop = StatefulAgentLoop(llm=my_llm, db_path=":memory:")
result = await loop.send("In one sentence: what is HTTP?")
print(result.final_text)
asyncio.run(main())
要点:
db_path=":memory:"是临时 store;生产换成文件路径就能跨进程保留。- 不传
session_id→ 自动创建新 session。返回的result.session_id是后续续话的钥匙。 - 没传
AgentLoopConfig也行:全部用默认值(max_rounds=24、DefaultCompactor()等)。
→ 完整版:examples/00_minimal.py
3.2 多轮对话
唯一新东西:把上一轮返回的 session_id 传回去。
r1 = await loop.send("My favorite color is teal.")
r2 = await loop.send("What did I just say?", session_id=r1.session_id)
# r2.final_text 会引用 "teal"
power-loop 会自动从 store 加载历史,模型每轮看到的都是完整上下文。你不用维护 "messages list",只管最新的一句输入。
→ 完整版:examples/01_multi_turn.py
3.3 工具调用
要让模型调用你的 Python 函数:写 ToolDefinition + handler,注册到 ToolRegistry,
传给 loop。
from power_loop import ToolDefinition, ToolRegistry, AgentLoopConfig
def lookup_dish(**kwargs) -> str:
return {"lima": "ceviche", "tokyo": "sushi"}.get(kwargs["city"].lower(), "?")
registry = ToolRegistry()
registry.register(
ToolDefinition(
name="lookup_dish",
description="Return the local dish for a city.",
input_schema={"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]},
required_params=("city",),
),
lookup_dish,
)
loop = StatefulAgentLoop(
llm=my_llm, db_path=":memory:", tool_registry=registry,
config=AgentLoopConfig(max_rounds=4),
)
result = await loop.send("What's Lima's signature dish?")
为什么 max_rounds ≥ 2:工具调用本质是两步——第 1 轮 LLM 决定调工具,第 2 轮看
到工具结果给最终答案。max_rounds=1 跑不通工具。
→ 完整版:examples/02_tool_use.py
3.4 子代理
register_spawn_agent(registry) 一行注入两个 meta-tool:spawn_agent(命令式)
和 run_agent(声明式 AgentSpec)。父 LLM 自主决定调用,子会话在同一个
SessionStore 里独立跑完,把 final_text 当 tool 结果回灌给父。
from power_loop import register_spawn_agent
registry = ToolRegistry()
register_spawn_agent(registry)
loop = StatefulAgentLoop(
llm=my_llm, db_path=":memory:", tool_registry=registry,
config=AgentLoopConfig(
system_prompt="Delegate factual Qs via spawn_agent.",
max_rounds=5,
),
)
result = await loop.send("Delegate this: capital of Japan?")
→ 完整版:examples/03_subagent.py
3.5 上下文压缩
历史变长后默认压缩自动触发:被折叠的消息在 store 里标 compacted_out,
插入一条 compact_note 摘要替代。开关、阈值、保留条数都在 DefaultCompactor
构造参数里。完整版演示了如何检查 store.list_compactions(sid) 看审计行。
3.6 悬挂态恢复
如果进程在 assistant(tool_calls) 已落库、tool 消息还没全部落库时挂掉,session
处于悬挂态。下次 send 会抛 SessionPendingError,由调用方选 resume 还是
abort_pending。
→ examples/05_pending_resume.py
4. 核心概念
Session
一次 send/resume 循环跑在一个 session 上。session 是 SessionStore 里 sessions 表的一行,承载 system prompt、AgentLoopConfig 快照、metadata 与所有 messages。send(user_input) 不传 session_id 自动新建;传则续话。
SessionStore
power-loop 的唯一持久化入口(SQLite)。5 张表:
| 表 | 作用 |
|---|---|
sessions |
元数据 + 父子链接 + 生命周期 |
messages |
每条消息 + state ∈ {active, compacted_out} + seq |
compactions |
每次压缩的审计行(覆盖了哪些 seq → 哪个 note) |
usage_rounds |
每轮 token 用量 |
session_state |
next_seq / round_index / pending |
并发:单连接 + threading.RLock;SQLite WAL 模式,允许多读者跨进程共享文件。
Sink
MessageSink 是 pipeline 与持久化之间的协议。StatefulAgentLoop 默认装 SQLiteSink,把每条消息、压缩、usage 持久化到 store。NullSink 是测试用的 no-op。
Compactor
可插拔的上下文压缩策略。AgentLoopConfig.compactor 默认为 DefaultCompactor();传 None 关闭。
触发:estimate_tokens(history) ≥ max_tokens × trigger_ratio(默认 0.75);
环境变量 CONTEXT_COMPACT_THRESHOLD 可设绝对值覆盖。
不变量(DefaultCompactor 实现,自定义 Compactor 应遵守):
- 保留所有
role=system消息(含先前compact_note); - 保留尾部
keep_last_n个 user 段(默认 4); - 绝不切开
assistant(tool_calls) ↔ tool(tool_call_id=…)原子对; - 摘要 LLM 抛错 → 返回
None→ 主循环用未压缩 history 继续(软降级)。
Pending 状态机
一轮工具调用的协议是:assistant(tool_calls=[A,B]) → tool(tool_call_id=A) + tool(tool_call_id=B)。
如果进程在 assistant 已落库、tool 还没全部落库时挂掉,session 处于悬挂态。下次 send 会抛 SessionPendingError。调用方两个选项:
await loop.resume(sid)— 把剩余 tool_calls 跑完,继续循环;loop.abort_pending(sid, reason="…")— 给每个未完成 tool_call 写一条<aborted: reason>tool 消息,恢复协议合法性,再send即可继续。
子代理
spawn_agent(task, ...)— 命令式 meta-tool,LLM 用 kwargs 调用,自动包成AgentSpec。run_agent(spec, input)— 声明式 meta-tool,LLM 提交完整 spec(严格 schema,未知字段拒绝)。
两者都走同一份内部实现 run_agent_spec,差异只在入口形态。子会话与父共享同一个 SessionStore,建立 parent_session_id / spawn_tool_call_id 链接,spawn_depth ≤ 3 强校验。
生命周期 SubagentLifecycle:
EPHEMERAL(默认):子 session 完成时物理删除;非完成态(hit_round_limit / cancelled)保留供 debug;LINKED:保留;父close_session(cascade=True)时随之级联删;DETACHED:保留;父 close 时不影响(解链)。
Hooks & Events
两条互不污染的通道:
- Hooks(控制流):15 个
HookPoint,每个 hook 返回HookDirective ∈ {CONTINUE/SKIP/BREAK/SHORT_CIRCUIT}。改 LLM 请求、注入 mock 结果、安全门、提前终止都靠它。完整 hook 表与示例:docs/hooks.md。 - Events(旁路只读):
AgentEventBus发布 token usage / stream delta / tool call started / completed / 等。指标、审计、UI 推送都订阅它。完整 event 表与示例:docs/events.md。
5. API 参考
StatefulAgentLoop
唯一公开入口。一个实例可并发驱动多个 session(每 session 一把 asyncio.Lock)。
StatefulAgentLoop(
*,
llm: LLMService,
store: SessionStore | None = None, # 传 None → 用 db_path 自建
db_path: str = "./power_loop_sessions.db",
config: AgentLoopConfig | None = None,
tool_registry: ToolRegistry | None = None,
hooks: AgentHooks | None = None,
event_bus: AgentEventBus | None = None,
)
| 方法 | 说明 |
|---|---|
await send(user_input, session_id=None, *, metadata=None, stop_event=None) -> StatefulResult |
主入口。无 session_id 自动新建;悬挂态 → SessionPendingError。 |
send_sync(...) |
同上的同步壳。 |
await resume(session_id) |
把悬挂的 tool_calls 跑完,继续循环。 |
abort_pending(session_id, *, reason="aborted") -> int |
给悬挂 tool_calls 写 <aborted> tool 消息,返回 abort 数量。 |
close_session(session_id, *, cascade=True) -> int |
物理删除 session(含 LINKED 子树)。 |
close() |
关 store(若 owned)。不删数据。 |
get_messages(session_id, *, include_compacted=False) -> list[dict] |
取当前 active history。 |
get_pending(session_id) -> dict | None |
查悬挂态。 |
StatefulResult
@dataclass
class StatefulResult:
session_id: str
status: str # "completed" / "hit_round_limit" / "cancelled" / "pending_tools"
final_text: str = ""
rounds: int = 0
pending_tool_calls: list[dict] = []
AgentLoopConfig
@dataclass
class AgentLoopConfig:
system_prompt: str | None = None
max_rounds: int = 24
temperature: float | None = 0.0
max_tokens: int | None = 8000
compactor: Compactor | None = DefaultCompactor() # 传 None 关闭压缩
SessionStore
SessionStore.open(path="./power_loop_sessions.db") -> SessionStore
store.close()
store.create_session(*, system_prompt=None, model=None, config=None,
parent_session_id=None, spawn_tool_call_id=None,
kind=SessionKind.ROOT,
lifecycle=SubagentLifecycle.EPHEMERAL,
metadata=None, session_id=None) -> str
store.get_session(sid) -> SessionRow | None
store.list_children(parent_sid) -> list[SessionRow]
store.close_session(sid, *, cascade=True) -> int # 物理删除,返回行数
store.archive_session(sid) # 改 status,不删
store.append_message(sid, *, role, content=None, tool_calls=None,
tool_call_id=None, name=None, round_index=None,
meta=None) -> int # 返回新 seq
store.load_active_messages(sid) -> list[MessageRow]
store.load_all_messages(sid) -> list[MessageRow] # 含 compacted_out
store.record_compaction(sid, *, from_seq, to_seq, note_content,
before_tokens, after_tokens, round_index) -> tuple[int, int]
store.list_compactions(sid) -> list[CompactionRow]
store.record_usage(sid, *, round_index, prompt_tokens,
completion_tokens, total_tokens, model=None)
store.get_state(sid) -> SessionStateRow | None
store.set_pending(sid, pending: dict | None)
子代理:AgentSpec + 工具
@dataclass(frozen=True)
class AgentSpec:
name: str
system_prompt: str
tools: list[str] | None = None # parent tool whitelist, None=inherit all
max_rounds: int = 8 # 1..50
max_tokens: int = 4000
temperature: float = 0.0
model: str | None = None
lifecycle: str = "ephemeral" # "ephemeral" / "linked" / "detached"
metadata: dict[str, Any] = {}
# 工厂:AgentSpec.from_dict(d) / AgentSpec.from_json(s)
# 严格 schema:未知字段 / 非法 lifecycle / max_rounds 越界 → AgentSpecError
from power_loop import register_spawn_agent
register_spawn_agent(registry, *, include_run_agent=True, overwrite=False)
直接 API(绕过 meta-tool):
from power_loop import run_agent_spec
result = await run_agent_spec(spec, "user input", parent_loop=loop)
# → {"session_id", "status", "final_text", "rounds", "depth"}
Errors
class PowerLoopError(Exception): ...
class SessionNotFoundError(PowerLoopError):
session_id: str
class SessionPendingError(PowerLoopError):
session_id: str
assistant_seq: int
pending_tool_calls: list[dict]
Compactor
@dataclass(frozen=True)
class CompactionPlan:
fold_start_idx: int # inclusive
fold_end_idx: int # inclusive
summary_text: str
before_tokens: int
after_tokens: int
class Compactor(Protocol):
async def maybe_compact(self, messages, *, llm, max_tokens, round_index) -> CompactionPlan | None: ...
class DefaultCompactor:
def __init__(self, *, trigger_ratio=0.75, keep_last_n=4,
summary_max_tokens=512, summary_llm=None, absolute_threshold=None): ...
自定义 compactor 实现上面的 Protocol 即可注入 AgentLoopConfig.compactor=YourCompactor()。
Public API 稳定性约定
power-loop 采用 三层分级,与 power_loop/__init__.py 的 STABLE_API 元组同步:
STABLE(跨 minor 保证向后兼容)
破坏性变更必须升 minor 版本号(0.x → 0.x+1)+ CHANGELOG 独立条目。业务方只应依赖这些符号。
| 符号 | 一句话 |
|---|---|
StatefulAgentLoop |
主入口:send() / resume() / abort_pending() |
StatefulResult |
send() 返回值:session_id / status / final_text / rounds |
AgentLoopConfig |
配置单:system_prompt / max_rounds / compactor / retry_policy / memory / … |
AgentLoopResult |
Pipeline 内部返回值(status / final_text / rounds / messages) |
SessionStore |
SQLite 持久化:open(path) / create_session() / append_message() / … |
SubagentLifecycle |
Enum:EPHEMERAL / LINKED / DETACHED |
PowerLoopError |
所有异常的基类,except PowerLoopError 一把抓 |
SessionNotFoundError |
session_id 不在 store 里 |
SessionPendingError |
上次崩溃留下未完成的 tool_calls |
LLMTimeout |
LLM 调用(或一系列 retry)超 total_timeout |
LLMRetryExhausted |
max_attempts 次重试仍未成功 |
CancellationRequested |
CancellationToken 已 flip |
ToolNotFound |
调用了一个未注册的 tool 名字 |
ToolValidationError |
tool args 未通过 schema / required 校验 |
SpecValidationError |
AgentSpec 严格 schema 拒绝(AgentSpecError 的父类) |
LLMRetryPolicy |
重试策略:max_attempts / backoff_* / total_timeout / retry_on |
CancellationToken |
统一 cancel 形状:from_any(ev) / cancel(reason) |
AgentHooks |
Hook 管理器:register(pt, fn) / register_async(pt, async_fn) |
AgentEventBus |
事件总线:subscribe(type, fn) / publish(event) |
HookPoint |
Enum:SESSION_START … MEMORY_RECALLED(18 个) |
HookDirective |
Enum:CONTINUE / SKIP / BREAK / SHORT_CIRCUIT |
ToolRegistry |
工具注册表:register(def, handler) / invoke_async(name, args) |
ToolDefinition |
工具声明:name / description / input_schema / required_params |
PROVISIONAL(0.x 阶段可能调整)
从 power_loop 顶层导入,但不在 STABLE 列表中。生产代码引用前确认版本号。
例:MessageSink / SQLiteSink / AgentSpec / run_agent_spec / MemoryProvider / MemorySnapshot / StructuredOutputSpec / parse_structured / trim_history / LLMProviderConfig / 全部 *Payload / 全部 *Ctx 等。
INTERNAL(无版本承诺)
从 power_loop.core.* / power_loop.runtime.* 等子模块导入的符号视为 internal,可随时变更或删除。Pipeline / Runner / ContextManager 等都在这一层。
6. Examples
examples/ 下每个文件可独立 python examples/NN_*.py 运行,并由 tests/real/test_examples.py 持续验证。
推荐按编号顺序读:每个文件只引入一个新概念。
| 文件 | 你会学到 |
|---|---|
00_minimal.py |
最小用法:StatefulAgentLoop(llm=…).send(text) |
01_multi_turn.py |
用 session_id 续话 + get_messages / close_session |
02_tool_use.py |
自定义 ToolDefinition + 多轮工具调用 |
03_subagent.py |
spawn_agent meta-tool + EPHEMERAL 自动清理 |
04_compaction.py |
DefaultCompactor 自动折叠 + 查看 store 审计行 |
05_pending_resume.py |
SessionPendingError + resume / abort_pending |
06_declarative_subagent.py |
AgentSpec 严格 schema + run_agent meta-tool + 直接调 run_agent_spec |
07_user_confirmation.py |
用 async TOOL_BEFORE hook 实现「执行前问用户」中断 |
08_streaming.py |
订阅 STREAM_DELTA event 做打字机渲染 |
09_audit_log.py |
bus.subscribe(None, …) 全量审计写 JSONL |
10_async_approval_queue.py |
多并发 session + asyncio.Queue 审批 worker |
11_persistence.py |
db_path 跨进程恢复:子进程拿同一个 SQLite 文件续上 |
12_retry_and_cancel.py |
LLMRetryPolicy + 注入失败 → 重试 / degraded / cancel 三条路径 |
13_memory_sqlite.py |
MemoryProvider 跨 session SQLite 事实记忆 |
14_structured_card.py |
StructuredOutputSpec + parse_structured 抽取 JSON 卡片 |
examples/_helpers.py 是共享的 .env 读取 + LLM 构造辅助,每个示例 from _helpers import make_llm,省掉 boilerplate。复制到自己项目时把那两行内联即可。
7. 配置(环境变量)
LLM 凭证与端点 不入代码,统一走环境变量(建议 .env + python-dotenv):
推荐(POWER_LOOP_*,M1.4 起):
| 变量 | 说明 |
|---|---|
POWER_LOOP_BASE_URL |
OpenAI 兼容端点 |
POWER_LOOP_API_KEY |
API key |
POWER_LOOP_MODEL |
默认模型名 |
POWER_LOOP_PROVIDER |
标签(openai / dashscope / deepseek / …) |
向后兼容 OPENAI_COMPAT_*(旧 .env 不改名继续工作)。详见 docs/providers.md。
构造 LLM 一行:
from power_loop import create_llm_service_from_env
llm = create_llm_service_from_env() # 读 POWER_LOOP_*(回退到 OPENAI_COMPAT_*)
旧方式(仍可用):
from llm_client.interface import OpenAICompatibleChatConfig
from llm_client.llm_factory import OpenAICompatibleChatLLMService
import os
cfg = OpenAICompatibleChatConfig(
base_url=os.environ["OPENAI_COMPAT_BASE_URL"],
api_key=os.environ["OPENAI_COMPAT_API_KEY"],
model=os.environ["OPENAI_COMPAT_MODEL"],
max_tokens=512, temperature=0.2,
)
llm = OpenAICompatibleChatLLMService(cfg)
8. 内部机制
完整版(含 Mermaid 架构图、序列图、状态机):docs/architecture.md。
本节是浓缩。
Pipeline 一回合
session.start
↓
for round in 0..max_rounds:
├ round.start → sink.on_round_started
├ prepare_round
│ ├ todo reminder(每 5 轮)
│ ├ microcompact(大 tool 输出溢盘到 .cache/)
│ └ compactor.maybe_compact → 命中则 sink.on_compaction → 持久化
├ llm.before → LLM.complete → llm.after
├ assistant 消息落 sink(带 tool_calls 时立即 set_pending)
├ 若无 tool_calls → round.end → 返回 "completed"
├ round.decide
├ tools.batch.before
│ ├ tool.before → tool.invoke → tool.after / tool.error
│ └ tool 消息落 sink(同 tool_call_id 解 pending)
├ tools.batch.after
└ round.end → sink.on_round_ended(usage=…)
session.end
15 个 HookPoint:见 power_loop/contracts/hooks.py。
消息持久化与 seq
每条消息在 store 里有唯一 (session_id, seq)。SQLiteSink 在内存里维护一份 _history_seqs 与 pipeline.history 一一对应:
- 加载老 session:
init_history_seqs([row.seq for row in active_rows]) - 新追加:
store.append_message返回 seq,追加到尾 - 压缩:
on_compaction(fold_start_idx, fold_end_idx, …)→ 用索引转 seq →store.record_compaction→ 重写_history_seqs(折叠区间替换为 note 的 seq)
Pending 状态机
LLM 返回 tool_calls
↓
assistant 消息落库 → set_pending({assistant_seq, tool_call_ids, tool_calls})
↓
tool A 落库 → 自动从 _unresolved 移除 A → set_pending(剩余)
↓ (process killed here)
进程重启 → send() 检测 pending → SessionPendingError
├ resume() → 跑剩余 tool_calls,pending 清零,继续
└ abort_pending() → 写 <aborted> tool 消息,pending 清零,下次 send 即可继续
9. 测试
# 全跑(含真实 LLM,要 .env 配 OPENAI_COMPAT_*)
pytest
# 只跑单元测试(不连真实 LLM)
pytest -m "not real_llm"
# 跳过真实 LLM
pytest --no-real
| 目录 | 内容 |
|---|---|
tests/unit/ |
纯控制流 / 契约测试,fake LLM |
tests/integration/ |
多组件场景,fake LLM |
tests/real/ |
跑真实 DashScope;缺 env 自动 skip |
tests/real/judge.py 提供 LLM-as-judge:业务方在测试里调 assert_passes(question, answer, rubric),
内部 spawn 一个 power-loop 作 evaluator,按 rubric 返回 {passed, reason} JSON。
专门解决 LLM 输出非确定性下的断言难题。
10. Roadmap & Changelog
- 详细路线:
ROADMAP.md - 版本记录:
CHANGELOG.md
Issues / PRs welcome.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file power_loop-0.2.0.tar.gz.
File metadata
- Download URL: power_loop-0.2.0.tar.gz
- Upload date:
- Size: 126.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9b18ecfe756ae34b0ff05bb85378227c0724b2b42a4e02776cef4f16f21a10b3
|
|
| MD5 |
471099d3facd9762e6d95b1fd99b063f
|
|
| BLAKE2b-256 |
b25be3a30fd6e0feea31a01ddba3302733a155d6812926c6d157560e9bfcbd66
|
File details
Details for the file power_loop-0.2.0-py3-none-any.whl.
File metadata
- Download URL: power_loop-0.2.0-py3-none-any.whl
- Upload date:
- Size: 134.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fd2f0f432600844f4c16a7bf47ae4cd23ed8a6bb2b2144a4256b0f803b4f0d29
|
|
| MD5 |
bae3079112fb3bec19ffe10bba6f5391
|
|
| BLAKE2b-256 |
082fbda8cddab83f075b5dddac9e175771b92a530084285bbd771ed6375b4d58
|