Full Code of stepfun-ai/StepDeepResearch for AI

main 8f6798f750fb cached
125 files
695.1 KB
166.5k tokens
741 symbols
1 requests
Download .txt
Showing preview only (763K chars total). Download the full file or copy to clipboard to get everything.
Repository: stepfun-ai/StepDeepResearch
Branch: main
Commit: 8f6798f750fb
Files: 125
Total size: 695.1 KB

Directory structure:
gitextract_gy_2y08y/

├── .env-example
├── .gitignore
├── LICENSE
├── README.md
├── agentkit/
│   └── trace/
│       ├── __init__.py
│       ├── builder.py
│       ├── context.py
│       ├── default.py
│       ├── local_tracer.py
│       ├── remote_tracer.py
│       ├── span.py
│       ├── tracer.py
│       └── types.py
├── config.yaml
├── cortex/
│   ├── __init__.py
│   ├── agents/
│   │   ├── __init__.py
│   │   ├── agent_factory.py
│   │   ├── base_agent.py
│   │   ├── base_step_agent.py
│   │   ├── checkpoint_agent/
│   │   │   ├── checkpoint_agent.py
│   │   │   ├── checkpointer.py
│   │   │   └── react_agent.py
│   │   ├── input/
│   │   │   └── input.py
│   │   ├── react_agent.py
│   │   └── types.py
│   ├── context/
│   │   ├── __init__.py
│   │   ├── base_context.py
│   │   ├── file_context.py
│   │   └── simple_context.py
│   ├── env.py
│   ├── examples/
│   │   ├── agents/
│   │   │   ├── ask_input_agent.py
│   │   │   ├── deep_reasearch_agent.py
│   │   │   ├── main_agent.py
│   │   │   ├── math_agent.py
│   │   │   ├── plan_agent.py
│   │   │   └── search_agent.py
│   │   ├── demo_agent_cli.py
│   │   ├── demo_agent_with_orchestrator.py
│   │   ├── demo_agent_with_tool.py
│   │   ├── demo_checkpoint.py
│   │   ├── demo_toolset_channel.py
│   │   └── server.py
│   ├── model/
│   │   ├── __init__.py
│   │   ├── definition.py
│   │   ├── provider.py
│   │   ├── stepfun_chat.py
│   │   ├── stepfun_provider.py
│   │   └── utils.py
│   ├── orchestrator/
│   │   ├── __init__.py
│   │   ├── local_runner.py
│   │   ├── orchestrator.py
│   │   ├── remote_runner.py
│   │   ├── runner.py
│   │   └── types.py
│   ├── runtime_config.py
│   ├── server/
│   │   ├── channel/
│   │   │   ├── channel.py
│   │   │   ├── error.py
│   │   │   ├── memory_channel.py
│   │   │   └── ws_channel.py
│   │   ├── http_server.py
│   │   └── log/
│   │       ├── log.py
│   │       └── trace.py
│   ├── tools/
│   │   ├── __init__.py
│   │   ├── agent_tool.py
│   │   ├── base.py
│   │   ├── channel.py
│   │   ├── client_tool.py
│   │   ├── function_tool.py
│   │   ├── mcp.py
│   │   ├── mcp_tool.py
│   │   ├── session_tool.py
│   │   ├── toolset.py
│   │   ├── types.py
│   │   ├── ublock_agent_tool.py
│   │   └── unblock_client_tool.py
│   ├── tui/
│   │   ├── __init__.py
│   │   └── tui.py
│   └── utils/
│       ├── __init__.py
│       ├── generator_merger.py
│       └── generator_merger_examples.py
├── cortex-ui/
│   ├── .gitignore
│   ├── .gitlab-ci.yml
│   ├── index.html
│   ├── package.json
│   ├── src/
│   │   ├── App.tsx
│   │   ├── components/
│   │   │   ├── EndpointConfig.tsx
│   │   │   ├── ErrorBoundary.tsx
│   │   │   ├── FilePanel.tsx
│   │   │   ├── FinalAnswer.tsx
│   │   │   ├── SearchResultsPanel.tsx
│   │   │   ├── ShellPanel.tsx
│   │   │   ├── TodoPanel.tsx
│   │   │   └── WebPagePanel.tsx
│   │   ├── index.css
│   │   ├── main.tsx
│   │   ├── pages/
│   │   │   ├── AgentList.tsx
│   │   │   └── ChatPage.tsx
│   │   ├── services/
│   │   │   └── api.ts
│   │   ├── types/
│   │   │   ├── citation.ts
│   │   │   └── index.ts
│   │   ├── utils/
│   │   │   └── citationParser.ts
│   │   └── vite-env.d.ts
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   └── vite.config.ts
├── demo/
│   ├── __init__.py
│   ├── dr_agent/
│   │   ├── __init__.py
│   │   └── dr_agent.py
│   ├── server.py
│   └── tools/
│       ├── __init__.py
│       ├── batch_open.py
│       ├── batch_search.py
│       ├── batch_web_surfer.py
│       ├── file.py
│       ├── open.py
│       ├── search.py
│       ├── shell.py
│       ├── text_truncator.py
│       ├── todo.py
│       └── utils.py
├── pyproject.toml
└── scripts/
    ├── configs/
    │   ├── prompt.py
    │   ├── runner_example.yaml
    │   └── tasks.example.json
    └── runner.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .env-example
================================================
MODEL_PROVIDER=stepfun
MODEL_BASE=https://api.stepfun.com
STEP_MODEL_API_KEY=your-model-api-key
STEP_SEARCH_API_BASE=https://api.stepfun.com
STEP_SEARCH_API_KEY=your-search-api-key


================================================
FILE: .gitignore
================================================
# common file types
*.log
*.ipynb
#*.json
# Allow json files in frontend directory
!agent_cortex/adapter/frontend/**/*.json
*.csv
*.jsonl
*.png
*.jpg

# local dev files/directories
**/_version.py
dist/
.env
.venv
__pycache__/
downloads*
logs
deprecated/
*_debug.yaml
scripts/*_debug.py
tests/*_debug.py
*.png
!assets/**
trash/
traces/
.idea/
.cursor/
*.db

eval/


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
# Step-DeepResearch

<div align="center">
  <img src="assets/logo.png"  height=100>
</div>
<div align="center" style="line-height: 1;">
  <a href="https://stepfun.com/" target="_blank"><img alt="Homepage" src="https://img.shields.io/badge/Homepage-StepFun-white?logo=StepFun&logoColor=white"/></a> &ensp;
  <a href="https://x.com/StepFun_ai" target="_blank"><img alt="Twitter Follow" src="https://img.shields.io/badge/Twitter-StepFun-white?logo=x&logoColor=white"/></a> &ensp;
  <a href="https://discord.com/invite/XHheP5Fn" target="_blank"><img alt="Discord" src="https://img.shields.io/badge/Discord-StepFun-white?logo=discord&logoColor=white"/></a>
</div>
<div align="center">
  <a href="https://arxiv.org/pdf/2512.20491"><img src="https://img.shields.io/static/v1?label=Step-DeepResearch&message=Arxiv&color=red"></a> &ensp;
  <a href="https://platform.stepfun.com/interface-key"><img src="https://img.shields.io/static/v1?label=Step-DeepResearch&message=Model%20API&color=blue"></a>
  <a href="https://github.com/stepfun-ai/StepDeepResearch/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/badge/License-Apache%202.0-blue?&color=blue"/></a>
</div>


## News

* Feb 2, 2026: 👋 We have released **Step 3.5 Flash**, achieving **65.27** on <span style="font-variant: small-caps;">ResearchRubrics</span>. Try it out by setting the environment variable `MODEL_NAME=step-3.5-flash`. [Details](https://static.stepfun.com/blog/step-3.5-flash/)

* Dec 25, 2025: 👋 You can join our group chat to get updates on your beta API application status and the latest project developments.
  <div align="center">
    <img src="assets/wechat_qr_code.jpg" alt="WeChat QR code" width="180" />
    <img src="assets/feishu_qr_code.png" alt="Feishu QR code" width="180" />
  </div>

* Dec 24, 2025: 👋 We have made our technical report available. [Read](https://arxiv.org/pdf/2512.20491)


## Introduction
### Model Summary
  **Step-DeepResearch** is a cost-effective, end-to-end deep research agent model designed for autonomous information exploration and professional report generation in open-ended research scenarios.
  - **Atomic Capability Integration**: By decomposing complex research tasks into trainable atomic capabilities—including planning, information seeking, reflection and cross-validation, and professional report generation—and achieving deep internalization at the model level, the system ensures closed-loop reflection and dynamic correction within a single inference pass.
  - **Progressive Training Pipeline**: We establish a complete optimization path from Agentic Mid-Training to Supervised Fine-Tuning (SFT) and Reinforcement Learning (RL), reshaping the training objective from "predicting the next token" to "deciding the next atomic action." This approach effectively enhances the model's adaptive capabilities and generalization performance in complex environments.
  - **Strong Performance Across Model Scales**: With only 32B parameters, Step-DeepResearch achieves 61.4% on Scale AI <span style="font-variant: small-caps;">ResearchRubrics</span>, matching OpenAI Deep Research and Gemini Deep Research. In expert human evaluations on ADR-Bench, its Elo score significantly outperforms larger models including DeepSeek-v3.2 and GLM-4.6, and rivals top-tier closed-source models.
  - **New model (Step 3.5 Flash)**: **Step 3.5 Flash** reaches **65.27** on <span style="font-variant: small-caps;">ResearchRubrics</span>, delivering research quality that competes with OpenAI and Gemini Deep Research while maintaining significantly higher inference efficiency. [Details](https://static.stepfun.com/blog/step-3.5-flash/)
  - **Superior Cost-Effectiveness**: With extremely low deployment and inference costs while maintaining expert-level research capabilities, Step-DeepResearch stands as the most cost-effective deep research agent solution currently available in the industry.
  - **Access**: Available via StepFun Open Platform API, free for the first month.

  <div align="center">
    <img width="920" alt="Cost-Performance & ADR-Bench comparison" src="assets/combined_comparison.svg"><br/>
    (left) <b>Cost-Efficiency on <span style="font-variant: small-caps;">ResearchRubrics</span>:</b> Step-DeepResearch achieves near-top performance (61.42) while significantly reducing inference costs (RMB), positioned at the high-efficiency frontier. 
    (right) <b>Expert Evaluation on ADR-Bench:</b> Step-DeepResearch consistently leads in Elo ratings across all dimensions, rivaling top-tier closed-source models.
  </div>

  <div align="center">
    <img width="720" alt="Performance on Research Rubrics" src="assets/performance_on_rr.png"><br/>
    <b>Performance on <span style="font-variant: small-caps;">ResearchRubrics</span>:</b> Step 3.5 Flash achieves 65.27 on the benchmark.
  </div>
  
### System Architecture
  Step-DeepResearch adopts a single-agent architecture based on the ReAct paradigm, enabling autonomous deep research through a dynamic cycle of reasoning, action, and reflection.

  Building upon our internal proprietary toolset, we provide a streamlined toolset with local implementation in the framework, including `batch_web_surfer` (batch web search and browsing), `file` (file reading, writing, and editing), `todo` (task state management), and `shell` (interactive command execution), to support the complete research workflow. For a comprehensive introduction to our full toolset, please refer to Section 5.2 of the technical report.

  <div align="center">
  <img width="720" alt="results" src="assets/framework.png">
  <p><b>Step-DeepResearch System Architecture:</b> The agent operates within a ReAct loop, utilizing a specialized toolset (e.g., batch_web_surfer, todo, shell) for planning, execution, and reflection to generate comprehensive research reports. </p>
  </div>

## Case

We provide a sample case below, including the query and the model-generated report.

### Query

```text
我是个 AI 产品经理,正在考虑给产品增加个性化记忆功能。想系统了解一下现在 Agent 里的 Memory 模块都在往哪些方向发展,包括短期和长期记忆是怎么做的、各种技术路线的差异和取舍。你帮我写个调研报告,从工程落地和未来演进的角度分析一下 哪些方向更值得投入
```

### Model Output (Report)

<details style="height: 520px; max-height: 90vh; overflow-y: auto; resize: vertical; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px 16px;">
<summary><b>Click to expand the report</b></summary>


# AI Agent记忆模块深度调研报告:技术演进与产品化路径

## 一、引言与核心问题界定

### 1.1 记忆能力为何成为Agent时代的分水岭

在Agentic AI加速渗透的2025年,智能体已从"能对话、会调用工具"的基础形态,向"具备长期记忆、可自主进化"的高阶阶段迈进[大模型进阶之路:AI Agent记忆能力构建技术详解(值得收藏)](https://blog.csdn.net/xxue345678/article/details/150983939)。正如OpenAI CEO山姆·奥尔特曼在2025年12月的Big Technology访谈中所言:"这是整个系统里我个人最期待的部分之一。AI的下一个重大飞跃并非是更敏锐的推理能力,而是更为根本的记忆"[超级Agent重要拼图?奥尔特曼点名“AI记忆” 存储环节迎来新叙事](https://finance.sina.com.cn/stock/t/2025-12-22/doc-inhcsmfc4526733.shtml)。他进一步指出,"人类本身是有局限的:即使你拥有世界上最好的私人助理,他们也不可能记住你说过的每一句话,不可能读过你的每一封邮件,不可能看过你写的每一份文件"——而这正是AI能够做到的[超级Agent重要拼图?奥尔特曼点名“AI记忆” 存储环节迎来新叙事](http://m.cls.cn/detail/2236511)。

当前,大多数人以为通过更大的上下文窗口或巧妙的提示词工程,AI就拥有了"记忆",但真相是,大多数AI Agent仍是无状态的,无法从过去的交互中学习,也无法随时间适应用户需求[探寻AI Agent 中隐秘的角落:记忆(Memory) - 定义、价值与实践](https://developer.volcengine.com/articles/7540134113190412324)。要从一次性工具迈向真正智能的伙伴,我们需要为AI赋予真正的记忆,而非仅仅依赖更大的提示或更强的检索。

### 1.2 记忆的本质定义

在AI Agent中,记忆是指跨时间、任务和多次用户交互,保留并调用相关信息的能力[探寻AI Agent 中隐秘的角落:记忆(Memory) - 定义、价值与实践](https://developer.volcengine.com/articles/7540134113190412324)。它让AI能记住过去发生的事,并利用这些信息优化未来的行为。记忆不是简单地存储聊天记录,也不是把更多数据塞进提示框——它是一种持久的内部状态,随着每次交互不断进化,哪怕间隔数周或数月,依然能为AI提供连续的上下文[探寻AI Agent 中隐秘的角落:记忆(Memory) - 定义、价值与实践](https://developer.volcengine.com/articles/7540134113190412324)。

记忆的三大支柱包括:**状态**(了解当前情境,掌握正在发生的事情)、**持久性**(跨会话保留知识,确保信息不因对话结束而丢失)、**选择性**(判断哪些信息值得记住,哪些可以忽略)[探寻AI Agent 中隐秘的角落:记忆(Memory) - 定义、价值与实践](https://developer.volcengine.com/articles/7540134113190412324)。这三者共同赋予AI一种前所未有的能力——连续性。

### 1.3 报告研究范围与目标

本报告旨在系统梳理Agent记忆模块的技术演进方向,深入分析短期与长期记忆的实现机制、各类技术路线的差异与取舍,并从业务价值、技术成熟度和工程可行性三个维度,为产品团队提供明确的技术选型建议和投入优先级排序。

---

## 二、Agent记忆模块的分类体系与核心架构

### 2.1 从认知科学到工程实践的记忆分层

人类记忆遵循从感觉记忆到短期记忆再到长期记忆的一般性进程[A survey on large language model based autonomous agents](https://link.springer.com/content/pdf/10.1007/s11704-024-40231-1.pdf)。当设计Agent记忆结构时,研究者从中汲取灵感:

**短期记忆(Working Memory)**:指AI正在进行的对话、脑中即时活跃的上下文。它容量有限(通常为数千tokens),但访问速度极快[从理论到落地:分层记忆架构在AI Agent中的应用实践](https://blog.csdn.net/whitehat_zhou/article/details/150269603)。例如,在MemGPT架构中,短期工作上下文由系统指令、工作上下文和FIFO队列组成[9.4k Star!MemGPT:伯克利大学最新开源、将LLM作为操作系统、无限上下文记忆、服务化部署自定义Agent](http://www.wehelpwin.com/m_article/5363)。

**中期记忆(Episodic Memory)**:指最近读完的书籍核心内容或近期发生的重大事件。它们比短期记忆更持久,但不如长期记忆根深蒂固[从理论到落地:分层记忆架构在AI Agent中的应用实践](https://blog.csdn.net/whitehat_zhou/article/details/150269603)。这类记忆通常通过向量数据库或结构化存储实现语义检索。

**长期记忆(Long-term Memory)**:指用户的个人经历、学到的技能、世界观以及那些已掌握的知识。它容量近乎无限,但检索可能需要更长时间[从理论到落地:分层记忆架构在AI Agent中的应用实践](https://blog.csdn.net/whitehat_zhou/article/details/150269603)。长期记忆是Agent个性化和持续学习的基础。

### 2.2 三种记忆形式的技术实现

根据2025年最新综述研究,Agent记忆可从形式(Forms)、功能(Functions)和动态(Dynamics)三个正交维度进行重构[2025年Memory最全综述!AI Agent记忆统一分类体系](https://zhuanlan.zhihu.com/p/1985435669187825983):

**Token级记忆(Token-level Memory)**:信息被处理在模型的上下文窗口内。这是最直接的记忆方式,包括滑动窗口注意力(Sliding Window Attention)和分块处理(Chunking)等技术[EPISODIC MEMORIES GENERATION AND EVALUATION BENCHMARK FOR LARGE LANGUAGE MODELS](https://arxiv.org/pdf/2501.13121v1.pdf)。优点是实现简单、无额外存储开销;缺点是在长序列上存在"上下文腐烂"问题[AI Agent 性能优化:核心策略与实战技巧(超详细)从零基础入门到精通!](https://m.blog.csdn.net/xiaoganbuaiuk/article/details/154012804)。

**参数级记忆(Parameter-level Memory)**:通过微调将知识直接编码到模型参数中。这种方式使信息成为模型"先天具有的知识",可在任意上下文中激活。优点是检索速度快、无需额外存储;缺点是训练成本高昂、难以增量更新[EPISODIC MEMORIES GENERATION AND EVALUATION BENCHMARK FOR LARGE LANGUAGE MODELS](https://arxiv.org/pdf/2501.13121v1.pdf)。

**潜在级记忆(Latent-level Memory)**:通过检索增强生成(RAG)等外部存储机制实现。信息以向量或结构化形式存储在外存,需要时通过语义检索召回[EPISODIC MEMORIES GENERATION AND EVALUATION BENCHMARK FOR LARGE LANGUAGE MODELS](https://arxiv.org/pdf/2501.13121v1.pdf)。这是当前商业化产品中最广泛采用的方式。

### 2.3 记忆模块在Agent架构中的位置

典型的AI Agent包括LLM用于推理和生成答案、策略或规划模块(如ReAct或AutoGPT风格)、工具或API访问权限以及检索器用于获取文档或历史数据[探寻AI Agent 中隐秘的角落:记忆(Memory) - 定义、价值与实践](https://developer.volcengine.com/articles/7540134113190412324)。然而,这些组件都无法记住昨天发生了什么——它们没有内部状态,也没有随时间进化的理解力。

加入记忆后,AI Agent的架构会发生质的改变:记忆层成为串联其他模块的"数据中枢"[大模型进阶之路:AI Agent记忆能力构建技术详解(值得收藏)](https://blog.csdn.net/xxue345678/article/details/150983939)。Agent架构的四大核心模块(感知、决策、记忆、行动)逐渐清晰,其中记忆模块(Brain-Memory & Knowledge)存储Agent的知识、用户偏好和历史交互记录,是决策与行动的"数据基础"[大模型进阶之路:AI Agent记忆能力构建技术详解(值得收藏)](https://blog.csdn.net/xxue345678/article/details/150983939)。

---

## 三、短期记忆的技术实现与优化策略

### 3.1 上下文窗口的根本限制

大语言模型在单次推理中能够接收和处理的Token数量上限被称为上下文窗口(Context Window),它直接决定了模型的"短期记忆容量"[LLM面试50问精解:从基础到进阶,掌握大语言模型核心知识,助你面试一战成名!大模型面试](https://m.blog.csdn.net/qkh1234567/article/details/155600143)。当前主流模型的上下文长度从4K token(早期GPT系列)发展到128K token(GPT-4o)甚至更长。然而,上下文窗口的扩大存在明显的"性能代价":

由于Transformer的自注意力机制需要计算序列中任意两个位置之间的相关性,其计算复杂度为O(n²),内存复杂度同样为O(n²)[Hi,我是 OceanBase PowerMem,了解一下?](http://cdn.modb.pro/db/1994227212866904064)。这意味着当上下文窗口从4K增加到128K(32倍)时,理论上的计算量和内存需求的增长不是32倍,而是1024倍[Hi,我是 OceanBase PowerMem,了解一下?](http://cdn.modb.pro/db/1994227212866904064)。

更重要的是,研究发现有效上下文长度远低于最大支持长度[LLM的长期记忆系统-成为你的个性化聊天陪伴](https://zhuanlan.zhihu.com/p/1917652340346946388)。已有研究表明,LLM的有效上下文长度在达到约2048 tokens后就开始显著下降,模型精度会随着上下文长度的增加而显著下降[LLM的长期记忆系统-成为你的个性化聊天陪伴](https://zhuanlan.zhihu.com/p/1917652340346946388)。

### 3.2 KV Cache优化技术

KV Cache(Key-Value Cache)的有效利用对提高LLM的运行效率至关重要[AI Agent优化技术深度解析:从Prompt到架构的全面指南(珍藏版)](https://m.blog.csdn.net/2401_85375186/article/details/155227353)。当前KV Cache优化可分为三个层次:

**Token级策略**:包括键值缓存选择、预算分配、合并、量化和低秩分解[【文献阅读】A Survey on Large Language Model Acceleration based on KV Cache Management](https://m.blog.csdn.net/Toky_min/article/details/146019523)。代表性技术包括:
- **SnapKV**:无需微调即可有效最小化KV缓存大小。在处理16K令牌输入时,实现一致的解码速度,与基线相比生成速度提高3.6倍,内存效率提高8.2倍[SnapKV: LLM在生成内容之前就知道您在寻找什么](https://blog.csdn.net/qq_36931982/article/details/139118015)
- **LM-Infinite**和**StreamingLLM**:保留初始token和近期token以实现无限长度上下文
- **H₂O**:基于注意力分数选择重要token进行缓存

**模型级优化**:通过架构创新和注意力机制增强键值重用[【文献阅读】A Survey on Large Language Model Acceleration based on KV Cache Management](https://m.blog.csdn.net/Toky_min/article/details/146019523)。例如Layer-Condensed KV Cache通过减少KV Cache层数而非保留所有层来降低内存消耗[Layer-Condensed KV Cache for Efficient Inference of Large Language Models](https://arxiv.org/pdf/2405.10637v2.pdf)。

**系统级方法**:解决内存管理、调度和硬件感知设计等问题[【文献阅读】A Survey on Large Language Model Acceleration based on KV Cache Management](https://m.blog.csdn.net/Toky_min/article/details/146019523)。NVIDIA Dynamo框架就采用了LLM-aware路由器来跟踪GPU集群中KV Cache的位置,当请求到达时计算新请求与缓存KV块之间的重叠度,将流量导向能最大化缓存复用的GPU[NVIDIA Dynamo Addresses Multi-Node LLM Inference Challenges](https://www.infoq.com/news/2025/12/nvidia-dynamo-kubernetes/?utm_campaign=infoq_content&utm_source=infoq&utm_medium=feed&utm_term=global)。

### 3.3 短期记忆的工程实现方案

**LangChain的记忆实现**提供了多种短期记忆方案\cite{web_bd759108}:
- **ConversationBufferMemory**:存储所有历史消息
- **ConversationBufferWindowMemory**:仅保留最近N轮对话(如k=3表示保留最近3轮)
- **ConversationSummaryMemory**:对对话历史生成摘要以节省Token

这种分级设计体现了"选择性"原则——判断哪些信息值得记住,哪些可以忽略[探寻AI Agent 中隐秘的角落:记忆(Memory) - 定义、价值与实践](https://developer.volcengine.com/articles/7540134113190412324)。

---

## 四、长期记忆的技术路线全景

### 4.1 RAG与Memory的区别与联系

RAG(检索增强生成)是OpenAI、Meta等公司提出的一种框架,用来增强语言模型的知识能力[让大模型“记住”更多:RAG与长期记忆](https://m.blog.csdn.net/2401_82452722/article/details/148744627)。然而,需要明确区分RAG与真正的Agent Memory:

RAG主要用于知识问答场景,在这些场景下知识点之间相对独立,一个用户问题往往只需涉及一两个知识点[LLM的长期记忆系统-成为你的个性化聊天陪伴](https://zhuanlan.zhihu.com/p/1917652340346946388)。企业级RAG系统通常会通过人工干预优化chunk划分,因此能够更好地支持知识类检索。

但在"个人记忆"这一场景下存在独特挑战:首先很难依赖人工完成chunk划分;其次用户的提问往往需要LLM进行推理、联想才能找到真正相关的记忆[LLM的长期记忆系统-成为你的个性化聊天陪伴](https://zhuanlan.zhihu.com/p/1917652340346946388)。当前RAG技术将文本按chunk进行切分,对每个chunk独立生成特征向量,在检索阶段query也是独立与每个chunk进行相似度计算——这种做法不仅容易造成信息"割裂",也难以保证检索到的是上下文完整的语义单元[LLM的长期记忆系统-成为你的个性化聊天陪伴](https://zhuanlan.zhihu.com/p/1917652340346946388)。

### 4.2 主流长期记忆框架对比

#### 4.2.1 MemGPT:操作系统灵感的虚拟内存管理

MemGPT是由UC Berkeley研究团队开发的记忆增强LLM框架,其核心创新是借鉴操作系统内存管理的概念[Meet MemGPT: The Future of Memory-Augmented AI - Medium](https://medium.com/@harshavasana1/meet-memgpt-the-future-of-memory-augmented-ai-9b547bbd3879):

**主上下文(Main Context)**:类比RAM的短期内存,用于即时处理。包含系统指令、工作上下文和FIFO队列[9.4k Star!MemGPT:伯克利大学最新开源、将LLM作为操作系统、无限上下文记忆、服务化部署自定义Agent](http://www.wehelpwin.com/m_article/5363)。

**外部上下文(External Context)**:类比硬盘的长期存储,用于存放不太常用的信息。MemGPT引入两个外部记忆模块——Recall Memory和Archival Memory,分别对应短期交互记录与长期知识存储[LLM的长期记忆系统-成为你的个性化聊天陪伴](https://zhuanlan.zhihu.com/p/1917652340346946388)。

MemGPT通过函数在主上下文和外部上下文之间移动数据,LLM可以通过在其输出中生成特殊关键字参数来请求立即后续推理,以将函数调用链接在一起;函数链允许MemGPT执行多步骤检索来回答用户查询[9.4k Star!MemGPT:伯克利大学最新开源、将LLM作为操作系统、无限上下文记忆、服务化部署自定义Agent](http://www.wehelpwin.com/m_article/5363)。这种架构不仅允许处理更大的数据集和更长的对话,而且还提高了模型在扩展交互中保持一致性能力[9.4k Star!MemGPT:伯克利大学最新开源、将LLM作为操作系统、无限上下文记忆、服务化部署自定义Agent](http://www.wehelpwin.com/m_article/5363)。

MemGPT的核心优势在于它证明了LLM可以被教会管理自己的内存[9.4k Star!MemGPT:伯克利大学最新开源、将LLM作为操作系统、无限上下文记忆、服务化部署自定义Agent](http://www.wehelpwin.com/m_article/5363)。其后商业化的Letta公司已获得1000万美元种子轮融资,在7000万美元估值下由Felicis Ventures领投[Letta, one of UC Berkeley's most anticipated AI startups, has just ...](https://techcrunch.com/2024/09/23/letta-one-of-uc-berkeleys-most-anticipated-ai-startups-has-just-come-out-of-stealth/)。

#### 4.2.2 Mem0:轻量级实用主义路线

Mem0被官方定义为"The Memory Layer for Personalized AI"(个性化AI的记忆层)[Mem0 获2400万美元融资,开发AI内存层技术-搜索](https://cn.bing.com/search?q=Mem0%20%E8%8E%B72400%E4%B8%87%E7%BE%8E%E5%85%83%E8%9E%8D%E8%B5%84%EF%BC%8C%E5%BC%80%E5%8F%91AI%E5%86%85%E5%AD%98%E5%B1%82%E6%8A%80%E6%9C%AF&rdr=1&rdrig=7UV8W6XI2C2HFZFXE23T7R5222E2IX6F&first=1)。其核心设计理念包括:

- **记忆是可搜索和可管理的**:通过自然语言索引+向量化混合检索
- **以用为先**:轻量级实现适合实际部署
- **结构化存储**:使用slot存储结构化长期信息(如角色设定、兴趣偏好)

Mem0在LOCOmo Benchmark中达到了业界开源Memory解决方案的SOTA水平[Hi,我是 OceanBase PowerMem,了解一下?](http://cdn.modb.pro/db/1994227212866904064)。其设计哲学是"足够好而非完美"——通过可恢复的压缩策略(如仅保留URL而非完整内容)在不永久丢失信息的情况下减少上下文开销[AI Agent 性能优化:核心策略与实战技巧(超详细)从零基础入门到精通!](https://m.blog.csdn.net/xiaoganbuaiuk/article/details/154012804)。

#### 4.2.3 OpenMemory:分层记忆分解架构

OpenMemory是一个自托管、模块化的AI记忆引擎,采用Hierarchical Memory Decomposition(HMD)架构[CaviraOSS/OpenMemory: Add long-term memory to any AI ...](https://github.com/CaviraOSS/OpenMemory):

- **单一规范节点每记忆**(无数据复制)
- **多扇区嵌入**(情节、语义、程序性、情感、反思性)
- **单一点位链接**(稀疏、生物启发式图)
- **复合相似性检索**(扇区融合+激活传播)

性能对比数据显示[CaviraOSS/OpenMemory: Add long-term memory to any AI ...](https://github.com/CaviraOSS/OpenMemory):
| 指标 | OpenMemory(自托管) | Zep(云服务) | Supermemory(SaaS) | Mem0 | 向量DB(平均) |
|------|---------------------|---------------|--------------------|-------|----------------|
| 成本 | 自托管 | 按API调用付费 | 按API调用付费 | 按API调用付费 | 按存储+查询付费 |

#### 4.2.4 A-MEM:卡片盒笔记法驱动的记忆系统

A-MEM是一种新型面向LLM Agent的智能记忆系统,遵循卡片盒笔记法(Zettelkasten)的基本原理[A-MEM智能记忆系统,让你的大模型拥有“学习”能力(收藏版)](https://blog.csdn.net/xiaoganbuaiuk/article/details/151215108):

当添加新记忆时,系统会生成包含多个结构化属性的综合笔记,包括上下文描述、关键词和标签。随后分析历史记忆以识别相关联系,并在存在有意义的相似性时建立链接——这一过程还支持记忆进化——随着新记忆的整合,它们会触发对现有历史记忆的上下文表示和属性更新[A-MEM智能记忆系统,让你的大模型拥有“学习”能力(收藏版)](https://blog.csdn.net/xiaoganbuaiuk/article/details/151215108)。

A-MEM的成本效益分析显示:通过选择性top-k检索机制,每次记忆操作约需1200个tokens,与基线方法(LoCoMo和MemGPT为16900个tokens)相比,Token使用量减少了85%-93%[A-MEM智能记忆系统,让你的大模型拥有“学习”能力(收藏版)](https://blog.csdn.net/xiaoganbuaiuk/article/details/151215108)。

### 4.3 多模态记忆的发展前沿

随着多模态LLM的发展,记忆系统也开始向多模态方向演进:

**M3-Agent**是字节跳动Seed团队提出的新型多模态代理框架,配备长期记忆能力。与人类类似,M3-Agent可以处理实时视觉和听觉输入来构建和更新其长期记忆[Research](https://seed.bytedance.com/en/public_papers)。

**VideoAgent**探讨如何将基础模型(大型语言模型和视觉语言模型)与统一记忆机制协调起来解决视频理解问题。其构建结构化内存来存储视频的通用时间事件描述和以对象为中心的跟踪状态,在NExTQA上平均提升6.6%,在EgoSchema上平均提升26.0%[VideoAgent: A Memory-augmented Multimodal Agent for Video Understanding(翻译)](https://m.blog.csdn.net/weixin_56764022/article/details/139862630)。

---

## 五、OpenAI ChatGPT记忆功能解析:行业标杆案例

### 5.1 发展历程与产品演进

OpenAI的记忆功能经历了三个重要阶段:

**第一阶段(2024年2月)**:小范围测试ChatGPT的记忆功能——记住用户在聊天中讨论过的事情,并避免重复信息。用户可以要求它记住特定的内容或让它自行获取详细信息。用得越多,ChatGPT的记忆力就会越好[ChatGPT要有记忆力了,OpenAI宣布小范围测试“记忆”功能!](http://m.nbd.com.cn/articles/2024-02-14/3245164.html)。

**第二阶段(2025年4月)**:向所有ChatGPT Plus和Pro用户推出记忆提升功能。这次更新后,ChatGPT的记忆功能能够参考用户过去的所有聊天以提供更个性化的回复[OpenAI:ChatGPT的记忆提升功能今天起对所有Plus和Pro用户推出](https://finance.jrj.com.cn/2025/04/11072749477951.shtml)。例如,如果用户曾经提到喜欢泰国菜,下次问"中午应该吃什么"时,ChatGPT可能会考虑到这一点[刚刚!OpenAI发布ChatGPT记忆功能,秒变私人助理!](https://m.blog.csdn.net/Leinwin/article/details/148428926)。

**第三阶段(2025年6月)**:为免费版ChatGPT推出轻量级记忆功能。该功能根据用户过往一段时间内的对话习惯、写作风格、提问方式进行个性化回答[OpenAI发布ChatGPT记忆功能](https://c.m.163.com/news/a/K16NIQ1D0511A6N9.html)。

### 5.2 记忆功能的核心机制

ChatGPT的记忆功能通过两个设置来控制[刚刚!OpenAI发布ChatGPT记忆功能,秒变私人助理!](https://m.blog.csdn.net/Leinwin/article/details/148428926):

- **"引用已保存的记忆"**:用户明确要求ChatGPT记住的细节(如名字、喜欢的颜色或饮食偏好)
- **"引用聊天历史"**:ChatGPT可以利用过去的对话信息来使未来的对话更有帮助

OpenAI称用户可以随时关闭新功能,在设置菜单中找到个性化选项即可将记忆设置为关闭。用户也可以在设置中找到"管理内存"选项,在其中查看和删除特定的记忆内容或清除所有记忆[ChatGPT要有记忆力了,OpenAI宣布小范围测试“记忆”功能!](http://m.nbd.com.cn/articles/2024-02-14/3245164.html)。

### 5.3 记忆功能的战略意义

投资人Allie K. Miller在X平台上感叹:"这相当于ChatGPT全天候在'偷听'——不管你有没有叫它记住,它都在默默收集"[OpenAI最近推出的ChatGPT更新简直像给AI打了“记忆芯片”](https://blog.csdn.net/2301_79342058/article/details/147156180)。她还表示,在平台功能越来越趋同的今天,真正拉开差距的关键就是"记忆+个性化"——AI的记忆就是平台的护城河[OpenAI最近推出的ChatGPT更新简直像给AI打了“记忆芯片”](https://blog.csdn.net/2301_79342058/article/details/147156180)。

OpenAI研究还表明,ChatGPT记住什么或不记住什么与人们如何体验ChatGPT的人格密切相关——许多Plus和Pro订阅者告诉OpenAI,更好的记忆是体验中最有价值的部分之一[超越“一刀切”:ChatGPT如何为8亿用户定制个性化体验| 图文 ...](https://www.zhihu.com/pin/1972634281496057035)。

---

## 六、评测基准与性能分析

### 6.1 主要评测基准概述

当前用于评估Agent Memory的主要数据集包括[Agent 又又失忆了!我来做一次记忆体检](https://juejin.cn/post/7564589641258139648)[Survey on Evaluation of LLM-based Agents](http://arxiv.org/pdf/2503.16416v1):

**LoCoMo Benchmark**:专门测试长上下文任务的记忆能力,在该基准中Mem0和Letta曾出现过评测分值分歧[2025 AI 记忆系统大横评:从插件到操作系统,谁在定义下一代Agent Infra?](https://m.toutiao.com/a7578151050135044643/)。

**LongMemEval**:UCLA团队开发的系统性基准,更像是一个"记忆体检表"——从信息提取、跨会话推理、知识更新到拒答未知,一共五项指标[Agent 又又失忆了!我来做一次记忆体检](https://juejin.cn/post/7564589641258139648)。

**Reflection-Bench**:上海人工智能实验室开发的认知心理学导向评测平台,围绕七个认知维度设计了354个任务——预测能力与决策能力、感知能力与记忆能力、反事实思维、信念更新、元反思能力等[AI大模型代理评测全攻略:从入门到精通,一篇就够了!](https://m.blog.csdn.net/m0_56255097/article/details/154832865)。

这些评测指标超越了传统的自然语言处理性能指标——聚焦事实信息的存储与利用,通过准确性指标(基于历史信息生成响应的正确性)和召回率@5指标(前5条检索结果中相关信息的占比)来衡量[上下文工程最新综述A Survey of Context Engineering for ...](https://zhuanlan.zhihu.com/p/1956121336968647834)。

### 6.2 性能数据对比

根据MemOS横向评测数据,在实际任务中的表现差异显著[2025 AI 记忆系统大横评:从插件到操作系统,谁在定义下一代Agent Infra?](https://m.toutiao.com/a7578151050135044643/):

| 技术路线 | 上下文扩展能力 | 检索准确性 | 响应延迟 | 开发复杂度 | 成本效益 |
|---------|--------------|-----------|---------|----------|---------|
| 基础RAG | 中等 | 较低 | 较低 | 低 | 高 |
| MemGPT | 高 | 中等 | 较高 | 高 | 中等 |
| Mem0 | 高 | 较高 | 中等 | 中等 | 高 |
| 向量数据库+自定义 | 可定制 | 高 | 中等 | 高 | 中等 |

---

## 七、工程实践案例与落地挑战

### 7.1 成功案例分析

来自硅谷一线AI创业者的数据显示:只有5%的AI Agent成功部署到生产环境[为什么只有5%的AI Agent落地成功?](https://wwww.huxiu.com/comment/4792610.html)。这5%的成功案例都有一个共同点——都采用了"human-in-the-loop"的设计:
- 将AI定位为辅助工具而非自主决策者
- 构建反馈循环让系统能从人类修正中学习
- 让人类可以轻松验证和否决AI的输出

这些成功案例在记忆设计上的共同特点是:精细调整模型的需求其实非常少见——一个设计完善的RAG系统通常就能满足需求。但大多数RAG系统的设计都太过初级——将所有内容编入索引导致检索信息过量反而迷惑模型;编入索引的内容过少导致模型缺乏有效信号;不加区分地混合结构化与非结构化数据会破坏嵌入向量的语义[为什么只有5%的AI Agent落地成功?](https://wwww.huxiu.com/comment/4792610.html)。

### 7.2 记忆功能的产品化架构

亚马逊云科技发布的Amazon Bedrock AgentCore为开发者打通了AI Agents从概念验证到生产部署的关键环节[跨越“演示”到“生产”鸿沟,亚马逊云科技开启AI Agents新纪元](http://app.myzaker.com/news/article.php?pk=6878d44a8e9f091282757f81)。其核心能力包括:

- **会话管理**:处理多用户并发场景下的状态隔离
- **身份权限控制**:确保AI访问敏感数据时的身份与权限可控
- **记忆系统**:支持分级存储和版本控制
- **可观测性机制**:监控Agent行为并进行调试

该平台的一个关键洞察是:大多数创始人以为自己在打造AI产品,实际上构建的是上下文选择系统[为什么只有5%的AI Agent落地成功?](https://wwww.huxiu.com/comment/4792610.html)。真正的工程工作应该得到应有的重视——精细调整模型的需求其实非常少见。

### 7.3 记忆层级的设计原则

产品化设计中应该考虑多层次的记忆架构[为什么只有5%的AI Agent落地成功?](https://wwww.huxiu.com/comment/4792610.html):

**用户级**:个人偏好设置(如图表类型、风格、写作语气)
**团队级**:高频查询、仪表盘、标准操作手册(runbooks)
**企业级**:知识库、政策文档、历史决策记录

这种分层设计既能满足个性化需求又能保护敏感信息不被泄露给其他用户。

---

## 八、技术路线对比与产品化建议

### 8.1 不同技术路线的核心差异矩阵

基于技术成熟度、落地难度、成本效益和业务价值四个维度进行综合评估:

| 技术路线 | 技术成熟度 | 落地难度 | 成本效益 | 业务价值 |
|---------|----------|---------|---------|---------|
| Token级短期记忆 | ★★★★★ | ★★☆☆☆ | ★★★★★ | ★★★☆☆ |
| 基础RAG方案 | ★★★★☆ | ★★★☆☆ | ★★★★☆ | ★★★★☆ |
| MemGPT虚拟内存 | ★★★☆☆ | ★★★★★ | ★★☆☆☆ | ★★★★★ |
| Mem0轻量级方案 | ★★★★☆ | ★★★☆☆ | ★★★★★ | ★★★★☆ |
| A-MEM卡片盒方案 | ★★★☆☆ | ★★★★☆ | ★★★★☆ | ★★★★★ |

### 8.2 各类技术路线优劣势详解

#### 基础RAG方案(推荐指数:⭐⭐⭐⭐⭐)

**优势**:
- 技术成熟度高,生态系统完善
- 成本可控,可使用开源组件构建
- 易于理解和调试
- 可快速上线验证业务价值

**劣势**:
- 检索准确性受chunk划分影响大
- 难以支持复杂推理关联
- 对非结构化个人记忆支持有限

**适用场景**:知识问答系统、文档检索助手、企业知识库查询

#### Mem0轻量级方案(推荐指数:⭐⭐⭐⭐⭐)

**优势**:
- 实现简单,有现成SDK
- 性能达到开源方案SOTA水平
- 资源占用低适合中小规模部署
- 社区活跃支持良好

**劣势**:
- 功能相对基础,缺乏深度定制能力
- 对复杂多模态场景支持有限

**适用场景**:个性化聊天助手、客服机器人升级、中小型企业AI应用

#### MemGPT虚拟内存方案(推荐指数:⭐⭐⭐★☆)

**优势**:
- 理论架构先进,支持无限扩展
- 内存利用率高
- 可学习管理自身内存

**劣势**:
- 实现复杂度高
- 运维难度大
- 对开发团队要求高
- 生产环境稳定性待验证

**适用场景**:研发型团队探索前沿技术、学术研究、复杂任务型Agent

#### 定制化向量数据库方案(推荐指数:⭐⭐⭐★☆)

**优势**:
- 可高度定制化
- 支持复杂查询逻辑
- 数据完全可控(隐私合规)

**劣势**:
- 开发周期长
- 运维成本高
- 需要专业的数据工程团队

**适用场景**:大型企业私有化部署、高隐私要求行业(医疗、金融)

### 8.3 投入优先级建议

基于您的产品定位和团队能力,建议采取以下投入策略:

#### 第一优先级(立即投入)

**基础RAG+短期记忆组合方案**

理由:
1. 技术风险最低——有成熟的开源框架和丰富的社区支持
2. 成本可控——可使用开源向量数据库(如FAISS、Milvus)
3. 快速验证价值——几周内即可上线原型
4. 底座稳固——为后续升级奠定基础

实施方案建议:
```
短期记忆层 → LangChain/LLamaIndex内置memory模块
长期记忆层 → FAISS/Pinecone向量数据库 + 自定义Chunk策略
检索增强   → 多模态Embedding + Top-k筛选 + 人工反馈闭环
```

#### 第二优先级(3个月内)

**引入Mem0或类似轻量级方案**

理由:
1. 在第一优先级方案验证价值后逐步升级
2. 成本效益比高——开源方案可快速迭代
3. 技术风险可控——有成熟产品参考

#### 第三优先级(6-12个月)

**探索差异化方案**

根据业务场景可选择:
- **如果是知识密集型应用**:深入定制向量数据库方案
- **如果是任务型Agent**:探索MemGPT虚拟内存方案
- **如果是多模态应用**:关注M3-Agent等前沿框架演进

---

## 九、未来演进方向与趋势判断

### 9.1 短期趋势(2025-2026)

根据奥尔特曼的说法:"这是2026年要考虑的事"[超级Agent重要拼图?奥尔特曼点名“AI记忆” 存储环节迎来新叙事](https://finance.sina.com.cn/stock/t/2025-12-22/doc-inhcsmfc4526733.shtml)——真正成熟的长期记忆系统将在明年成为焦点。预计的发展方向包括:

**KV Cache优化技术成熟化**
目前KV Cache优化仍在快速演进中。SnapKV已在单个A100-80GB GPU上实现处理多达38万个上下文令牌的能力[SnapKV: LLM在生成内容之前就知道您在寻找什么](https://blog.csdn.net/qq_36931982/article/details/139118015)。随着技术成熟,这一能力将成为标准配置而非差异化优势。

**多模态记忆标准化**
M3-Agent等多模态记忆框架正在快速发展[Research](https://seed.bytedance.com/en/public_papers)。预计到2026年将出现标准化的多模态记忆接口和评估基准。

### 9.2 中期趋势(2026-2027)

**操作系统级别集成**
Agent的记忆系统将从独立组件演进为操作系统级别的原生能力——类似MemGPT的理念但更加成熟稳定。届时可能出现专门针对AI应用优化的操作系统发行版或云服务层。

**个性化与隐私平衡**
随着监管加强和技术进步,个性化记忆将需要更严格的隐私保护机制。联邦学习、差分隐私等技术将与记忆系统深度整合。

### 9.3 长期展望(2027+)

真正的类人记忆能力将成为区分优秀AI产品与普通工具的关键标志。正如行业共识所言:"在AI都拥有相似模型和工具的未来,记忆将成为决定胜负的关键"[探寻AI Agent 中隐秘的角落:记忆(Memory) - 定义、价值与实践](https://developer.volcengine.com/articles/7540134113190412324)。

---

## 十、结论与行动建议

### 10.1 核心结论

通过对当前Agent Memory模块的技术全景调研,可以得出以下核心结论:

第一,记忆模块已从"锦上添花"转变为"不可或缺"——它是Agent从工具到伙伴的关键分水岭[大模型进阶之路:AI Agent记忆能力构建技术详解(值得收藏)](https://blog.csdn.net/xxue345678/article/details/150983939)[超级Agent重要拼图?奥尔特曼点名“AI记忆” 存储环节迎来新叙事](https://finance.sina.com.cn/stock/t/2025-12-22/doc-inhcsmfc4526733.shtml)。

第二,在技术成熟度和工程可行性方面,基础RAG方案配合短期记忆是最务实的起点;Mem0等轻量级方案提供了良好的平衡点;而MemGPT等前沿方案更适合有充足技术储备的研发型团队。

第三,从成本效益角度分析,A-MEM等优化方案可将Token消耗降低85%-93%[A-MEM智能记忆系统,让你的大模型拥有“学习”能力(收藏版)](https://blog.csdn.net/xiaoganbuaiuk/article/details/151215108)——这说明优化空间巨大但仍需谨慎选择合适的技术路线。

第四,在OpenAI的带领下,个性化记忆已成为行业共识方向[ChatGPT要有记忆力了,OpenAI宣布小范围测试“记忆”功能!](http://m.nbd.com.cn/articles/2024-02-14/3245164.html)[超级Agent重要拼图?奥尔特曼点名“AI记忆” 存储环节迎来新叙事](http://m.cls.cn/detail/2236511)——不跟进将面临竞争劣势风险。

### 10.2 行动建议清单

针对您的产品规划,建议按以下步骤推进:

✅ **Week 1-2**: 明确产品对记忆的具体需求——是知识检索还是个性化对话?需要短期还是长期?对准确性和延迟的要求是多少?

✅ **Week 3**: 基于需求选择技术路线——优先评估LangChain/LlamaIndex+FAISS的基础组合方案

✅ **Week 4**: 构建最小可行原型——用两周时间验证核心功能是否满足业务需求

✅ **Week 5**: 设置评估指标——参考LoCoMo或LongMemEval建立内部评测体系

✅ **Month 2**: 根据原型反馈决定是否引入Mem0或其他轻量级方案进行升级

✅ **Month 3+**: 持续优化并关注行业演进——特别是OpenAI的记忆功能迭代和其他竞品动态

最终建议采取"敏捷验证、快速迭代"策略:不要追求一步到位的理想方案,在验证业务价值的基础上逐步升级技术栈。正如一位资深AI产品经理所言:"大多数创始人以为自己在打造AI产品,但实际上他们构建的是上下文选择系统"[为什么只有5%的AI Agent落地成功?](https://wwww.huxiu.com/comment/4792610.html)——聚焦用户价值而非技术炫技才是成功的关键。
</details>


## Quick Start

You can get beta access to the model API through StepFun Open Platform. 

### Requirements

- Python >= 3.10
- Node.js >= 18 (for frontend)
- npm or yarn

### 1. Environment Setup

**Install dependencies (backend & frontend)**

```bash
# Using uv (recommended)
uv sync
source .venv/bin/activate

# Or using pip
pip install -e .
```

```bash
cd cortex-ui

# Using npm
npm install

# Or using yarn
yarn install
```

**Configure environment variables**

Get your StepFun API key(s) from [StepFun Open Platform](https://platform.stepfun.com/interface-key), the StepFun API key is for both model and search.

Create a `.env` file in the repo root:

```bash
# Model provider
MODEL_PROVIDER=stepfun
# Model name for Deep Research Agent (optional: step-dr-1, step-3.5-flash; default: step-dr-1)
MODEL_NAME=step-dr-1
# Model API base URL (StepFun)
MODEL_BASE=https://api.stepfun.com
# StepFun model API key
STEP_MODEL_API_KEY=your-model-api-key
# Search API base URL (StepFun search service)
STEP_SEARCH_API_BASE=https://api.stepfun.com
# StepFun search API key
STEP_SEARCH_API_KEY=your-search-api-key
```

**Recommended System Prompt**

We recommend using the system prompt in [prompt.py](scripts/configs/prompt.py) to ensure optimal performance.

### 2. Run with Demo UI

**Start backend service**

```bash
python -m demo.server [OPTIONS]
```

Options:
- `--port PORT` Server port (default: `8001`)

The service runs on `http://localhost:8001` by default.

**Main endpoints:**
- `GET /agents` - Get list of all available Agents
- `WebSocket /ws` - Real-time communication endpoint

**Start frontend service**

```bash
cd cortex-ui

# Development mode
npm run dev
# Or
yarn dev
```

The frontend runs on `http://localhost:3000` by default and automatically proxies API requests to the backend service.

### 3. Run with Offline Runner

Use `python -m scripts.runner` to run DeepResearchAgent on ad-hoc tasks without the UI.

- **Direct args**: pass a single prompt or a tasks file.
  ```bash
  python -m scripts.runner \
    --task "列出最近的 AI 安全新闻" \
    --output-dir scripts/results
  ```
- **Config file**: provide defaults via YAML .
  ```bash
  python -m scripts.runner --config scripts/configs/runner_example.yaml
  ```

Inputs:
- `--task` / `--task-id`: one-off prompt and optional id.
- `--tasks-file`: JSON task list (relative to project root if not absolute), e.g. `scripts/configs/tasks.example.json`.
- Optional flags: `--output-dir` (default `scripts/results`), `--mode` (`multi` default), `--no-stream`, `--request-timeout`, `--context-upper-limit`, `--context-lower-limit`, `--overwrite`.

Outputs:
- One JSON trace per task in `output_dir`, containing the final answer, full message/tool events, metadata, and status.


## Our paper

We have made our technical report available. [Read](https://arxiv.org/pdf/2512.20491)

## Contact Us

You can join our group chat to get updates on your beta API application status and the latest project developments.
<div align="center">
  <img src="assets/wechat_qr_code.jpg" alt="WeChat QR code" width="180" />
  <img src="assets/feishu_qr_code.png" alt="Feishu QR code" width="180" />
</div>

## License

The code in the repository is licensed under [Apache 2.0](LICENSE) License.

## Citation

```
@misc{hu2025stepdeepresearchtechnicalreport,
      title={Step-DeepResearch Technical Report}, 
      author={Chen Hu and Haikuo Du and Heng Wang and Lin Lin and Mingrui Chen and Peng Liu and Ruihang Miao and Tianchi Yue and Wang You and Wei Ji and Wei Yuan and Wenjin Deng and Xiaojian Yuan and Xiaoyun Zhang and Xiangyu Liu and Xikai Liu and Yanming Xu and Yicheng Cao and Yifei Zhang and Yongyao Wang and Yubo Shu and Yurong Zhang and Yuxiang Zhang and Zheng Gong and Zhichao Chang and Binyan Li and Dan Ma and Furong Jia and Hongyuan Wang and Jiayu Liu and Jing Bai and Junlan Liu and Manjiao Liu and Na Wang and Qiuping Wu and Qinxin Du and Shiwei Li and Wen Sun and Yifeng Gong and Yonglin Chen and Yuling Zhao and Yuxuan Lin and Ziqi Ren and Zixuan Wang and Aihu Zhang and Brian Li and Buyun Ma and Kang An and Li Xie and Mingliang Li and Pan Li and Shidong Yang and Xi Chen and Xiaojia Liu and Yuchu Luo and Yuan Song and YuanHao Ding and Yuanwei Liang and Zexi Li and Zhaoning Zhang and Zixin Zhang and Binxing Jiao and Daxin Jiang and Jiansheng Chen and Jing Li and Xiangyu Zhang and Yibo Zhu},
      year={2025},
      eprint={2512.20491},
      archivePrefix={arXiv},
      primaryClass={cs.CL},
      url={https://arxiv.org/abs/2512.20491}, 
}
```

## Star History

[![Star History Chart](https://api.star-history.com/svg?repos=stepfun-ai/StepDeepResearch&type=date&legend=top-left)](https://www.star-history.com/#stepfun-ai/StepDeepResearch&type=date&legend=top-left)


================================================
FILE: agentkit/trace/__init__.py
================================================
from .builder import EventBuilder, FunctionSpanBuilder, HTTPSpanBuilder, SpanBuilder
from .context import (
    SpanContext,
    create_span,
    get_current_context,
    get_default_app_name,
    get_default_tracer,
    record_event,
    set_default_app_name,
    set_default_tracer,
    start_trace,
    trace_function,
)
from .local_tracer import LocalStorageTracer
from .remote_tracer import HybridTracer, RemoteTracer
from .span import (
    DeltaEventPayload,
    Error,
    Event,
    EventType,
    FunctionSpanPayload,
    HTTPSpanPayload,
    LLMSpanPayload,
    OtherEventPayload,
    OtherSpanPayload,
    Span,
    SpanType,
    ToolSpanPayload,
)
from .tracer import Tracer

__all__ = [
    # Core types
    "Span",
    "Event",
    "SpanType",
    "EventType",
    "LLMSpanPayload",
    "ToolSpanPayload",
    "FunctionSpanPayload",
    "HTTPSpanPayload",
    "OtherSpanPayload",
    "DeltaEventPayload",
    "OtherEventPayload",
    "Error",
    "Tracer",
    "LocalStorageTracer",
    "RemoteTracer",
    "HybridTracer",
    # Context management
    "SpanContext",
    "get_current_context",
    "get_default_tracer",
    "get_default_app_name",
    "set_default_tracer",
    "set_default_app_name",
    "start_trace",
    "create_span",
    "record_event",
    "trace_function",
    # Builders
    "SpanBuilder",
    "FunctionSpanBuilder",
    "HTTPSpanBuilder",
    "EventBuilder",
]


================================================
FILE: agentkit/trace/builder.py
================================================
"""
Builder pattern for constructing Span and Event objects.
"""

from datetime import datetime
from typing import Any, Optional

from ulid import ULID

from .default import get_default
from .span import (
    Event,
    EventType,
    FunctionSpanPayload,
    HTTPSpanPayload,
    OtherEventPayload,
    Span,
    SpanType,
)


class SpanBuilder:
    """
    Span builder providing a fluent API to construct complex Spans.

    Usage:
        span = (SpanBuilder()
                .with_name("my_operation")
                .with_app_name("my_app")
                .with_type(SpanType.FUNCTION)
                .with_tag("user", "alice")
                .with_parent(parent_span)
                .build())
    """

    def __init__(self, trace_id: Optional[str] = None, app_name: Optional[str] = None):
        self._trace_id = trace_id or str(ULID())
        self._app_name = app_name or get_default("app_name")
        self._id = str(ULID())
        self._name = ""
        self._start_time = datetime.now()
        self._end_time = None
        self._tags: dict[str, str] = {}
        self._payload: Optional[Any] = None
        self._parent_id: Optional[str] = None

    def with_id(self, span_id: str) -> "SpanBuilder":
        """Set span ID."""
        self._id = span_id
        return self

    def with_name(self, name: str) -> "SpanBuilder":
        """Set span name."""
        self._name = name
        return self

    def with_trace_id(self, trace_id: str) -> "SpanBuilder":
        """Set trace ID."""
        self._trace_id = trace_id
        return self

    def with_start_time(self, start_time: datetime) -> "SpanBuilder":
        """Set start time."""
        self._start_time = start_time
        return self

    def with_end_time(self, end_time: datetime) -> "SpanBuilder":
        """Set end time."""
        self._end_time = end_time
        return self

    def with_tag(self, key: str, value: str) -> "SpanBuilder":
        """Add a tag."""
        self._tags[key] = value
        return self

    def with_tags(self, tags: dict[str, str]) -> "SpanBuilder":
        """Add multiple tags."""
        self._tags.update(tags)
        return self

    def with_payload(self, payload: Any) -> "SpanBuilder":
        """Set payload."""
        self._payload = payload
        return self

    def with_parent(self, parent: Span) -> "SpanBuilder":
        """Set parent span."""
        self._parent_id = parent.id
        # Automatically inherit trace_id from parent span
        if parent:
            self._trace_id = parent.trace_id
        return self

    def with_parent_id(self, parent_id: str) -> "SpanBuilder":
        """Set parent span ID."""
        self._parent_id = parent_id
        return self

    def with_app_name(self, app_name: str) -> "SpanBuilder":
        """Set application name."""
        self._app_name = app_name
        return self

    def build(self) -> Span:
        """Build Span object."""
        span = Span(
            id=self._id,
            name=self._name,
            trace_id=self._trace_id,
            app_name=self._app_name,
            start_time=self._start_time,
            end_time=self._end_time,
            tags=self._tags,
            payload=self._payload,
            parent_id=self._parent_id,
        )
        return span


class FunctionSpanBuilder(SpanBuilder):
    """
    Specialized builder for Function Spans.

    Usage:
        span = (FunctionSpanBuilder()
                .with_name("calculate")
                .with_function_name("calculate_total")
                .with_arguments({"x": 1, "y": 2})
                .with_return_value(3)
                .build())
    """

    def __init__(self, trace_id: Optional[str] = None, app_name: Optional[str] = None):
        super().__init__(trace_id, app_name)
        self._function_name = ""
        self._arguments: dict[str, Any] = {}
        self._ret: Any = None
        self._error = None

    def with_function_name(self, name: str) -> "FunctionSpanBuilder":
        """Set function name."""
        self._function_name = name
        if not self._name:
            self._name = name
        return self

    def with_arguments(self, arguments: dict[str, Any]) -> "FunctionSpanBuilder":
        """Set function arguments."""
        self._arguments = arguments
        return self

    def with_return_value(self, ret: Any) -> "FunctionSpanBuilder":
        """Set return value."""
        self._ret = ret
        return self

    def with_error(self, code: int, message: str) -> "FunctionSpanBuilder":
        """Set error information."""
        self._error = {"code": code, "message": message}
        return self

    def build(self) -> Span:
        """Build Function Span."""
        payload = FunctionSpanPayload(
            type=SpanType.FUNCTION,
            name=self._function_name,
            arguments=self._arguments,
            ret=self._ret,
            error=self._error,
        )
        self._payload = payload
        return super().build()


class HTTPSpanBuilder(SpanBuilder):
    """
    Specialized builder for HTTP Spans.

    Usage:
        span = (HTTPSpanBuilder()
                .with_url("https://api.example.com/users")
                .with_method("GET")
                .with_header("Authorization", "Bearer token")
                .with_response('{"users": []}')
                .build())
    """

    def __init__(self, trace_id: Optional[str] = None, app_name: Optional[str] = None):
        super().__init__(trace_id, app_name)
        self._url = ""
        self._method = "GET"
        self._headers: dict[str, list[str]] = {}
        self._body: Optional[str | bytes] = None
        self._response: Optional[str | bytes] = None
        self._error = None

    def with_url(self, url: str) -> "HTTPSpanBuilder":
        """Set URL."""
        self._url = url
        if not self._name:
            self._name = f"{self._method} {url}"
        return self

    def with_method(self, method: str) -> "HTTPSpanBuilder":
        """Set HTTP method."""
        self._method = method
        if self._url:
            self._name = f"{method} {self._url}"
        return self

    def with_header(self, key: str, value: str | list[str]) -> "HTTPSpanBuilder":
        """Add HTTP header."""
        if isinstance(value, str):
            value = [value]
        self._headers[key] = value
        return self

    def with_headers(self, headers: dict[str, list[str]]) -> "HTTPSpanBuilder":
        """Add multiple HTTP headers."""
        self._headers.update(headers)
        return self

    def with_body(self, body: str | bytes) -> "HTTPSpanBuilder":
        """Set request body."""
        self._body = body
        return self

    def with_response(self, response: str | bytes) -> "HTTPSpanBuilder":
        """Set response body."""
        self._response = response
        return self

    def with_error(self, code: int, message: str) -> "HTTPSpanBuilder":
        """Set error information."""
        self._error = {"code": code, "message": message}
        return self

    def build(self) -> Span:
        """Build HTTP Span."""
        payload = HTTPSpanPayload(
            type=SpanType.HTTP,
            url=self._url,
            method=self._method,
            headers=self._headers,
            body=self._body,
            response=self._response,
            error=self._error,
        )
        self._payload = payload
        return super().build()


class EventBuilder:
    """
    Event builder.

    Usage:
        event = (EventBuilder()
                 .with_name("user_input")
                 .with_data({"text": "hello"})
                 .with_parent(span)
                 .build())
    """

    def __init__(self, trace_id: Optional[str] = None, app_name: Optional[str] = None):
        self._trace_id = trace_id or str(ULID())
        self._app_name = app_name or get_default("app_name")
        self._id = str(ULID())
        self._name = ""
        self._timestamp = datetime.now()
        self._tags: dict[str, str] = {}
        self._data: Any = None
        self._parent_id: Optional[str] = None

    def with_id(self, event_id: str) -> "EventBuilder":
        """Set event ID."""
        self._id = event_id
        return self

    def with_name(self, name: str) -> "EventBuilder":
        """Set event name."""
        self._name = name
        return self

    def with_trace_id(self, trace_id: str) -> "EventBuilder":
        """Set trace ID."""
        self._trace_id = trace_id
        return self

    def with_timestamp(self, timestamp: datetime) -> "EventBuilder":
        """Set timestamp."""
        self._timestamp = timestamp
        return self

    def with_tag(self, key: str, value: str) -> "EventBuilder":
        """Add a tag."""
        self._tags[key] = value
        return self

    def with_tags(self, tags: dict[str, str]) -> "EventBuilder":
        """Add multiple tags."""
        self._tags.update(tags)
        return self

    def with_data(self, data: Any) -> "EventBuilder":
        """Set data."""
        self._data = data
        return self

    def with_parent(self, parent: Span) -> "EventBuilder":
        """Set parent span."""
        self._parent_id = parent.id
        # Automatically inherit trace_id from parent span
        if parent:
            self._trace_id = parent.trace_id
        return self

    def with_parent_id(self, parent_id: str) -> "EventBuilder":
        """Set parent span ID."""
        self._parent_id = parent_id
        return self

    def build(self) -> Event:
        """Build Event object."""
        payload = OtherEventPayload(type=EventType.OTHER, data=self._data)

        event = Event(
            id=self._id,
            name=self._name,
            trace_id=self._trace_id,
            timestamp=self._timestamp,
            tags=self._tags,
            payload=payload,
            parent_id=self._parent_id,
            app_name=self._app_name,
        )
        return event


================================================
FILE: agentkit/trace/context.py
================================================
"""
Context management and utility functions for Span and Event.
"""

from contextlib import contextmanager
from contextvars import ContextVar
from datetime import datetime
from functools import wraps
try:  # Python 3.11+ has HTTPMethod in stdlib
    from http import HTTPMethod
except ImportError:  # pragma: no cover - fallback for Python 3.10
    from enum import Enum

    class HTTPMethod(str, Enum):
        GET = "GET"
        POST = "POST"
        PUT = "PUT"
        DELETE = "DELETE"
        PATCH = "PATCH"
        HEAD = "HEAD"
        OPTIONS = "OPTIONS"
        CONNECT = "CONNECT"
        TRACE = "TRACE"
from typing import Any, Callable, Literal, Optional

from ulid import ULID

from .default import get_default, get_default_settings, set_default
from .span import (
    Event,
    EventType,
    FunctionSpanPayload,
    HTTPSpanPayload,
    LLMSpanPayload,
    OtherEventPayload,
    OtherSpanPayload,
    Span,
    SpanType,
    ToolSpanPayload,
)
from .tracer import Tracer
from .types import Error

# Use contextvars to manage the current context
_current_context: ContextVar[Optional["SpanContext"]] = ContextVar(
    "current_context", default=None
)


class SpanContext:
    """
    Manages the current active span context, supporting nested parent-child relationships.

    Supports cross-service span reconstruction: trace_id and parent_id can be specified at creation time.
    """

    def __init__(
        self,
        app_name: Optional[str] = None,
        tags: Optional[dict[str, str]] = None,
        trace_id: Optional[str] = None,
        parent_id: Optional[str] = None,
        tracer: Optional[Tracer] = None,
    ):
        if tracer is None:
            self.tracer = get_default("tracer")
        else:
            self.tracer = tracer
        self.app_name = app_name or get_default("app_name")
        self.tags = tags
        self._span_stack: list[Span] = []
        self._current_trace_id: Optional[str] = trace_id
        self._root_parent_id: Optional[str] = parent_id  # Root parent node for cross-service reconstruction

    def get_current_span(self) -> Optional[Span]:
        """Get the current active span."""
        return self._span_stack[-1] if self._span_stack else None

    def get_current_trace_id(self) -> str:
        """Get or create the current trace_id."""
        if self._current_trace_id is None:
            self._current_trace_id = str(ULID())
        return self._current_trace_id

    def set_trace_id(self, trace_id: str):
        """Set the current trace_id."""
        self._current_trace_id = trace_id

    def merge_tags(self, tags: dict[str, str]) -> dict[str, str]:
        """Merge tags."""
        merged_tags = self.tags or {}
        if tags:
            merged_tags.update(tags)
        return merged_tags

    @contextmanager
    def span(
        self,
        name: str,
        tags: Optional[dict[str, str]] = None,
        payload: Optional[
            HTTPSpanPayload
            | LLMSpanPayload
            | ToolSpanPayload
            | FunctionSpanPayload
            | OtherSpanPayload
        ] = None,
    ):
        """
        Context manager for creating a span.

        Usage:
            with ctx.span("my_operation", SpanType.FUNCTION):
                # your code
                pass
        """
        parent_span = self.get_current_span()
        trace_id = self.get_current_trace_id()

        # Determine parent_id: prioritize span in current stack, otherwise use root parent_id (cross-service scenario)
        parent_id = None
        if parent_span:
            parent_id = parent_span.id
        elif self._root_parent_id:
            parent_id = self._root_parent_id

        span = Span(
            name=name,
            trace_id=trace_id,
            app_name=self.app_name,
            tags=self.merge_tags(tags),
            payload=payload,
            parent_id=parent_id,
        )

        self._span_stack.append(span)
        token = _current_context.set(self)

        try:
            yield span
        except Exception as e:
            # Record error
            span.tags["error"] = str(e)
            raise
        finally:
            # Set end time
            span.end_time = datetime.now()
            self._span_stack.pop()

            # Record span
            if self.tracer:
                self.tracer.record_span(span)
            _current_context.reset(token)

    def record_event(
        self,
        name: str,
        data: Any,
        tags: Optional[dict[str, str]] = None,
    ):
        """
        Record an event to the current span.

        Usage:
            ctx.record_event("user_input", {"text": "hello"})
        """
        parent_span = self.get_current_span()
        trace_id = self.get_current_trace_id()

        payload = OtherEventPayload(type=EventType.OTHER, data=data)

        event = Event(
            name=name,
            trace_id=trace_id,
            tags=self.merge_tags(tags),
            payload=payload,
            parent_id=parent_span.id if parent_span else None,
            app_name=self.app_name,
        )

        if self.tracer:
            self.tracer.record_event(event)

        return event

    @contextmanager
    def function_span(
        self,
        name: str,
        arguments: dict[str, Any],
        tags: Optional[dict[str, str]] = None,
    ):
        """
        Create a span for a function call.

        Usage:
            with ctx.function_span("calculate", {"x": 1, "y": 2}) as span:
                result = calculate(1, 2)
                span.update_payload_data(return_value=result)
        """
        payload = FunctionSpanPayload(
            type=SpanType.FUNCTION,
            name=name,
            arguments=arguments,
            return_value=None,
        )

        with self.span(name, tags, payload) as span:
            try:
                yield span
            except Exception as e:
                if isinstance(span.payload, FunctionSpanPayload):
                    span.payload.error = Error(code=-1, message=str(e))
                raise

    @contextmanager
    def llm_span(
        self,
        name: str = "llm_call",
        request: Any = None,
        tags: Optional[dict[str, str]] = None,
    ):
        """
        Create a span for an LLM call.

        Usage:
            with ctx.llm_span("openai_call", request=messages) as span:
                response = client.chat.completions.create(...)
                span.update_payload_data(response=response)
        """
        payload = LLMSpanPayload(
            type=SpanType.LLM,
            request=request,
        )

        with self.span(name, tags, payload) as span:
            try:
                yield span
            except Exception as e:
                if isinstance(span.payload, LLMSpanPayload):
                    span.payload.error = Error(code=-1, message=str(e))
                raise

    @contextmanager
    def tool_span(
        self,
        name: str = "tool_call",
        request: Any = None,
        tags: Optional[dict[str, str]] = None,
    ):
        """
        Create a span for a tool call.

        Usage:
            with ctx.tool_span("search_tool", request=tool_call) as span:
                result = search(query)
                span.update_payload_data(response=result)
        """
        payload = ToolSpanPayload(type=SpanType.TOOL, request=request)

        with self.span(name, tags, payload) as span:
            try:
                yield span
            except Exception as e:
                if isinstance(span.payload, ToolSpanPayload):
                    span.payload.error = Error(code=-1, message=str(e))
                raise

    @contextmanager
    def http_span(
        self,
        url: str,
        method: Literal[
            HTTPMethod.GET,
            HTTPMethod.POST,
            HTTPMethod.PUT,
            HTTPMethod.DELETE,
            HTTPMethod.PATCH,
            HTTPMethod.HEAD,
            HTTPMethod.OPTIONS,
            HTTPMethod.CONNECT,
            HTTPMethod.TRACE,
        ],
        name: Optional[str] = None,
        headers: Optional[dict[str, list[str]]] = None,
        body: Optional[str | bytes] = None,
        tags: Optional[dict[str, str]] = None,
    ):
        """
        Create a span for an HTTP request.

        Usage:
            with ctx.http_span("https://api.example.com", "POST", body=data) as span:
                response = requests.post(url, data=data)
                span.update_payload_data(response=response.text)
        """
        span_name = name or f"{method} {url}"
        payload = HTTPSpanPayload(
            type=SpanType.HTTP,
            url=url,
            method=method,
            headers=headers or {},
            body=body,
        )

        with self.span(span_name, tags, payload) as span:
            try:
                yield span
            except Exception as e:
                if isinstance(span.payload, HTTPSpanPayload):
                    span.payload.error = Error(code=-1, message=str(e))
                raise


def get_current_context() -> SpanContext:
    """Get the current SpanContext, creating a default one if it doesn't exist."""
    context = _current_context.get()
    if context is None:
        context = SpanContext()
        _current_context.set(context)
    return context


def set_default_tracer(tracer: Tracer):
    """Set the global default Tracer."""
    set_default(tracer=tracer)


def set_default_app_name(app_name: str):
    """Set the global default app_name."""
    set_default(app_name=app_name)


def get_default_tracer() -> Tracer:
    """Get the global default Tracer."""
    return get_default_settings().tracer


def get_default_app_name() -> str:
    """Get the global default app_name."""
    return get_default_settings().app_name


# Convenient global functions
def trace_function(
    name: Optional[str] = None,
    tags: Optional[dict[str, str]] = None,
    context: Optional[SpanContext] = None,
):
    """
    Decorator: automatically trace function calls.

    Usage:
        @trace_function(name="my_function", tags={"category": "business"})
        def my_function(x, y):
            return x + y
    """

    def decorator(func: Callable) -> Callable:
        func_name = name or func.__name__

        @wraps(func)
        def wrapper(*args, **kwargs):
            ctx = context or get_current_context()

            # Build arguments dictionary
            arguments = {"args": args, "kwargs": kwargs}

            with ctx.function_span(func_name, arguments, tags) as span:
                result = func(*args, **kwargs)

                # Record return value
                if isinstance(span.payload, FunctionSpanPayload):
                    span.payload.return_value = result

                return result

        @wraps(func)
        async def async_wrapper(*args, **kwargs):
            ctx = context or get_current_context()

            # Build arguments dictionary
            arguments = {"args": args, "kwargs": kwargs}

            with ctx.function_span(func_name, arguments, tags) as span:
                result = await func(*args, **kwargs)

                # Record return value
                if isinstance(span.payload, FunctionSpanPayload):
                    span.payload.return_value = result

                return result

        # Return corresponding wrapper based on function type
        import asyncio

        if asyncio.iscoroutinefunction(func):
            return async_wrapper
        else:
            return wrapper

    return decorator


def start_trace(trace_id: Optional[str] = None, context: Optional[SpanContext] = None):
    """
    Start a new trace.

    Usage:
        trace_id = start_trace()
    """
    ctx = context or get_current_context()
    if trace_id:
        ctx.set_trace_id(trace_id)
    else:
        trace_id = ctx.get_current_trace_id()
    return trace_id


def record_event(
    name: str,
    data: Any,
    tags: Optional[dict[str, str]] = None,
    context: Optional[SpanContext] = None,
):
    """
    Record an event.

    Usage:
        record_event("user_action", {"action": "click", "button": "submit"})
    """
    ctx = context or get_current_context()
    return ctx.record_event(name, data, tags=tags)


@contextmanager
def create_span(
    name: str,
    tags: Optional[dict[str, str]] = None,
    context: Optional[SpanContext] = None,
):
    """
    Convenient function to create a span.

    Usage:
        with create_span("my_operation"):
            # your code
            pass
    """
    ctx = context or get_current_context()
    with ctx.span(name, tags) as span:
        yield span


================================================
FILE: agentkit/trace/default.py
================================================
from dataclasses import dataclass

from .local_tracer import LocalStorageTracer
from .tracer import Tracer


@dataclass
class DefaultSettings:
    app_name: str = "default"
    tracer: Tracer = LocalStorageTracer(storage_dir="./traces")


_settings = DefaultSettings()


def set_default(**kwargs):
    for key, value in kwargs.items():
        if hasattr(_settings, key):
            setattr(_settings, key, value)
        else:
            raise ValueError(f"Unknown setting: {key}")


def get_default_settings() -> DefaultSettings:
    return _settings


def get_default(key: str):
    """Get a single default value."""
    return getattr(_settings, key)


================================================
FILE: agentkit/trace/local_tracer.py
================================================
import json
from datetime import datetime
from pathlib import Path
from typing import Optional

from .span import Event, Span
from .tracer import Tracer


class LocalStorageTracer(Tracer):
    def __init__(self, storage_dir: str = "./traces"):
        self.storage_dir = Path(storage_dir)
        self.storage_dir.mkdir(parents=True, exist_ok=True)
        self.spans_dir = self.storage_dir / "spans"
        self.events_dir = self.storage_dir / "events"
        self.spans_dir.mkdir(exist_ok=True)
        self.events_dir.mkdir(exist_ok=True)

    def _get_trace_spans_file(self, trace_id: str) -> Path:
        return self.spans_dir / f"{trace_id}.jsonl"

    def _get_trace_events_file(self, trace_id: str) -> Path:
        return self.events_dir / f"{trace_id}.jsonl"

    def record_span(self, span: Span) -> None:
        spans_file = self._get_trace_spans_file(span.trace_id)
        with open(spans_file, "a", encoding="utf-8") as f:
            span_data = span.model_dump_json(exclude_none=True, ensure_ascii=False)
            f.write(span_data + "\n")

    def record_event(self, event: Event) -> None:
        events_file = self._get_trace_events_file(event.trace_id)
        with open(events_file, "a", encoding="utf-8") as f:
            event_data = event.model_dump_json(exclude_none=True, ensure_ascii=False)
            f.write(event_data + "\n")

    def get_spans(self, trace_id: str) -> list[Span]:
        spans_file = self._get_trace_spans_file(trace_id)
        if not spans_file.exists():
            return []

        spans = []
        with open(spans_file, "r", encoding="utf-8") as f:
            for line_num, line in enumerate(f, 1):
                if line.strip():
                    try:
                        span_data = json.loads(line)
                        spans.append(Span(**span_data))
                    except Exception as e:
                        # Log error but continue processing other lines
                        print(
                            f"Warning: Failed to parse span at line {line_num} in {spans_file}: {e}"
                        )
                        continue
        return spans

    def get_events(self, trace_id: str) -> list[Event]:
        events_file = self._get_trace_events_file(trace_id)
        if not events_file.exists():
            return []

        events = []
        with open(events_file, "r", encoding="utf-8") as f:
            for line_num, line in enumerate(f, 1):
                if line.strip():
                    try:
                        event_data = json.loads(line)
                        events.append(Event(**event_data))
                    except Exception as e:
                        # Log error but continue processing other lines
                        print(
                            f"Warning: Failed to parse event at line {line_num} in {events_file}: {e}"
                        )
                        continue
        return events

    def get_trace(self, trace_id: str) -> Optional[dict]:
        spans = self.get_spans(trace_id)
        events = self.get_events(trace_id)

        if not spans and not events:
            return None

        return {
            "trace_id": trace_id,
            "spans": [span.model_dump(mode="json") for span in spans],
            "events": [event.model_dump(mode="json") for event in events],
            "span_count": len(spans),
            "event_count": len(events),
        }

    def get_trace_raw(self, trace_id: str) -> Optional[dict]:
        """
        Get raw trace data (without Pydantic model validation).
        Used for frontend display to avoid serialization/deserialization issues.
        """
        spans_file = self._get_trace_spans_file(trace_id)
        events_file = self._get_trace_events_file(trace_id)

        if not spans_file.exists() and not events_file.exists():
            return None

        spans = []
        events = []

        # Read spans (raw JSON)
        if spans_file.exists():
            with open(spans_file, "r", encoding="utf-8") as f:
                for line in f:
                    if line.strip():
                        try:
                            span_data = json.loads(line)
                            spans.append(span_data)
                        except json.JSONDecodeError as e:
                            # Skip invalid line
                            print(f"Warning: Failed to parse span line: {e}")
                            continue

        # Read events (raw JSON)
        if events_file.exists():
            with open(events_file, "r", encoding="utf-8") as f:
                for line in f:
                    if line.strip():
                        try:
                            event_data = json.loads(line)
                            events.append(event_data)
                        except json.JSONDecodeError as e:
                            # Skip invalid line
                            print(f"Warning: Failed to parse event line: {e}")
                            continue

        return {
            "trace_id": trace_id,
            "spans": spans,
            "events": events,
            "span_count": len(spans),
            "event_count": len(events),
        }

    def list_traces(self, limit: int = 100, offset: int = 0) -> list[dict]:
        trace_files = {}

        for spans_file in self.spans_dir.glob("*.jsonl"):
            trace_id = spans_file.stem
            trace_files[trace_id] = {
                "trace_id": trace_id,
                "spans_file": spans_file,
                "mtime": spans_file.stat().st_mtime,
            }

        for events_file in self.events_dir.glob("*.jsonl"):
            trace_id = events_file.stem
            if trace_id not in trace_files:
                trace_files[trace_id] = {
                    "trace_id": trace_id,
                    "events_file": events_file,
                    "mtime": events_file.stat().st_mtime,
                }
            else:
                trace_files[trace_id]["events_file"] = events_file
                trace_files[trace_id]["mtime"] = max(
                    trace_files[trace_id]["mtime"], events_file.stat().st_mtime
                )

        sorted_traces = sorted(
            trace_files.values(), key=lambda x: x["mtime"], reverse=True
        )

        traces = []
        for trace_info in sorted_traces[offset : offset + limit]:
            trace_id = trace_info["trace_id"]
            spans = self.get_spans(trace_id)
            events = self.get_events(trace_id)

            traces.append(
                {
                    "trace_id": trace_id,
                    "span_count": len(spans),
                    "event_count": len(events),
                    "last_modified": datetime.fromtimestamp(trace_info["mtime"]),
                }
            )

        return traces


================================================
FILE: agentkit/trace/remote_tracer.py
================================================
import logging
from datetime import datetime
from typing import Optional

import httpx
from pydantic import BaseModel

from .span import DataType, Event, Span
from .tracer import Tracer

logger = logging.getLogger(__name__)


class RemoteEvent(BaseModel):
    id: str
    data_type: DataType
    timestamp: datetime
    app_name: str
    data: Span | Event


class RemoteTracer(Tracer):
    """
    A tracer that sends trace data to a remote API.

    According to API docs: /trace/agent/event
    """

    def __init__(
        self,
        base_url: str = "",
        timeout: float = 10.0,
        enable_span: bool = True,
        enable_event: bool = True,
    ):
        """
        Initialize RemoteTracer.

        Args:
            base_url: API base URL.
            timeout: Request timeout in seconds.
            enable_span: Whether to enable span sending (default True).
            enable_event: Whether to enable event sending (default True).
        """
        self.base_url = base_url.rstrip("/")
        self.event_endpoint = f"{self.base_url}/trace/agent/event"
        self.timeout = timeout
        self.enable_span = enable_span
        self.enable_event = enable_event
        self.client = httpx.Client(timeout=timeout)

    def __del__(self):
        """Clean up resources."""
        try:
            self.client.close()
        except Exception:
            pass

    def _send_to_api(self, data: str) -> bool:
        """
        Send data to remote API.

        Args:
            data: Data to send.

        Returns:
            Whether the send was successful.
        """
        try:
            response = self.client.post(
                self.event_endpoint,
                data=data,
                headers={"Content-Type": "application/json"},
            )

            if response.status_code == 200:
                result = response.json()
                if result.get("code") == 0:
                    logger.debug(f"Successfully sent trace data: {data}")
                    return True
                else:
                    logger.error(
                        f"API returned error code {result.get('code')}: {result.get('msg')}"
                    )
                    return False
            else:
                logger.error(f"HTTP error {response.status_code}: {response.text}")
                return False

        except httpx.TimeoutException:
            logger.error(f"Request timeout when sending trace data: {data}")
            return False
        except Exception as e:
            logger.error(f"Failed to send trace data: {e}")
            return False

    def record_span(self, span: Span) -> None:
        """Record a span to the remote service."""
        if not self.enable_span:
            return

        remote_event = RemoteEvent(
            id=span.id,
            data_type=DataType.SPAN,
            timestamp=span.start_time,
            app_name=span.app_name,
            data=span,
        )

        self._send_to_api(remote_event.model_dump_json(exclude_none=True))

    def record_event(self, event: Event) -> None:
        """Record an event to the remote service."""
        if not self.enable_event:
            return

        remote_event = RemoteEvent(
            id=event.id,
            data_type=DataType.EVENT,
            timestamp=event.timestamp,
            app_name=event.app_name,
            data=event,
        )

        self._send_to_api(remote_event.model_dump_json(exclude_none=True))

    def get_spans(self, trace_id: str) -> list[Span]:
        """
        RemoteTracer does not support read operations.

        Note: The current remote API only provides write interface, query is not supported.
        """
        logger.warning("RemoteTracer does not support reading spans")
        return []

    def get_events(self, trace_id: str) -> list[Event]:
        """
        RemoteTracer does not support read operations.

        Note: The current remote API only provides write interface, query is not supported.
        """
        logger.warning("RemoteTracer does not support reading events")
        return []

    def get_trace(self, trace_id: str) -> Optional[dict]:
        """
        RemoteTracer does not support read operations.

        Note: The current remote API only provides write interface, query is not supported.
        """
        logger.warning("RemoteTracer does not support reading traces")
        return None

    def list_traces(self, limit: int = 100, offset: int = 0) -> list[dict]:
        """
        RemoteTracer does not support read operations.

        Note: The current remote API only provides write interface, query is not supported.
        """
        logger.warning("RemoteTracer does not support listing traces")
        return []


class HybridTracer(Tracer):
    """
    Hybrid tracer: sends data to both remote service and local storage.

    Usage:
        from agentkit.trace.local_tracer import LocalStorageTracer
        from agentkit.trace.remote_tracer import RemoteTracer, HybridTracer

        local_tracer = LocalStorageTracer("./traces")
        remote_tracer = RemoteTracer("xxx")
        hybrid_tracer = HybridTracer(local_tracer, remote_tracer)
    """

    def __init__(self, local_tracer: Tracer, remote_tracer: RemoteTracer):
        """
        Initialize hybrid tracer.

        Args:
            local_tracer: Local tracer (for reading and local storage).
            remote_tracer: Remote tracer (for remote reporting).
        """
        self.local_tracer = local_tracer
        self.remote_tracer = remote_tracer

    def record_span(self, span: Span) -> None:
        """Record to both local and remote."""
        self.local_tracer.record_span(span)
        self.remote_tracer.record_span(span)

    def record_event(self, event: Event) -> None:
        """Record to both local and remote."""
        self.local_tracer.record_event(event)
        self.remote_tracer.record_event(event)

    def get_spans(self, trace_id: str) -> list[Span]:
        """Read spans from local."""
        return self.local_tracer.get_spans(trace_id)

    def get_events(self, trace_id: str) -> list[Event]:
        """Read events from local."""
        return self.local_tracer.get_events(trace_id)

    def get_trace(self, trace_id: str) -> Optional[dict]:
        """Read trace from local."""
        return self.local_tracer.get_trace(trace_id)

    def get_trace_raw(self, trace_id: str) -> Optional[dict]:
        """Read raw trace from local."""
        return self.local_tracer.get_trace_raw(trace_id)

    def list_traces(self, limit: int = 100, offset: int = 0) -> list[dict]:
        """List traces from local."""
        return self.local_tracer.list_traces(limit, offset)


================================================
FILE: agentkit/trace/span.py
================================================
from datetime import datetime
from enum import Enum
try:  # Python 3.11+ has HTTPMethod in stdlib
    from http import HTTPMethod
except ImportError:  # pragma: no cover - fallback for Python 3.10
    class HTTPMethod(str, Enum):
        GET = "GET"
        POST = "POST"
        PUT = "PUT"
        DELETE = "DELETE"
        PATCH = "PATCH"
        HEAD = "HEAD"
        OPTIONS = "OPTIONS"
        CONNECT = "CONNECT"
        TRACE = "TRACE"
from typing import Any, Literal, Optional

from pydantic import BaseModel, Field
from ulid import ULID

from .types import Error


class DataType(str, Enum):
    SPAN = "span"
    EVENT = "event"


class SpanType(str, Enum):
    LLM = "llm_span"
    TOOL = "tool_span"
    HTTP = "http_span"
    FUNCTION = "function_span"
    OTHER = "other_span"


class LLMSpanPayload(BaseModel):
    type: Literal[SpanType.LLM]
    request: Any = None
    response: Any = None
    error: Optional[Error] = None


class ToolSpanPayload(BaseModel):
    type: Literal[SpanType.TOOL]
    request: Any = None
    response: Any = None
    error: Optional[Error] = None


class FunctionSpanPayload(BaseModel):
    type: Literal[SpanType.FUNCTION]
    name: str = ""
    arguments: dict[str, Any] = Field(default_factory=dict)
    return_value: Any = None
    error: Optional[Error] = None


class HTTPSpanPayload(BaseModel):
    type: Literal[SpanType.HTTP]
    url: str
    method: Literal[
        HTTPMethod.GET,
        HTTPMethod.POST,
        HTTPMethod.PUT,
        HTTPMethod.DELETE,
        HTTPMethod.PATCH,
        HTTPMethod.HEAD,
        HTTPMethod.OPTIONS,
        HTTPMethod.CONNECT,
        HTTPMethod.TRACE,
    ]
    headers: dict[str, list[str]] = Field(default_factory=dict)
    body: Optional[str | bytes] = None
    response: Optional[str | bytes] = None
    error: Optional[Error] = None


class OtherSpanPayload(BaseModel):
    type: Literal[SpanType.OTHER]
    data: Any


class Span(BaseModel):
    # use ulid
    id: str = Field(default_factory=lambda: str(ULID()))
    name: str = Field(default="")
    data_type: Literal[DataType.SPAN] = DataType.SPAN
    start_time: datetime = Field(default_factory=datetime.now)
    end_time: Optional[datetime] = Field(default=None)
    tags: dict[str, str] = Field(default_factory=dict)
    payload: (
        LLMSpanPayload
        | ToolSpanPayload
        | FunctionSpanPayload
        | OtherSpanPayload
        | HTTPSpanPayload
        | None
    ) = Field(default=None)
    parent_id: Optional[str] = Field(default=None)  # Parent span ID
    trace_id: str
    app_name: str

    def update_payload(
        self,
        payload: (
            LLMSpanPayload
            | ToolSpanPayload
            | FunctionSpanPayload
            | HTTPSpanPayload
            | OtherSpanPayload
            | None
        ),
    ) -> "Span":
        """Update span payload."""
        self.payload = payload
        return self

    def update_payload_data(self, **kwargs) -> "Span":
        """
        Update specific fields in payload.

        Usage:
            span.update_payload_data(ret=result, error=None)
        """
        if self.payload is not None and hasattr(self.payload, "model_copy"):
            # Use pydantic's model_copy to update fields
            self.payload = self.payload.model_copy(update=kwargs)
        return self

    def add_tag(self, key: str, value: str) -> "Span":
        """Add or update a tag."""
        self.tags[key] = value
        return self

    def add_tags(self, tags: dict[str, str]) -> "Span":
        """Add or update multiple tags."""
        self.tags.update(tags)
        return self


class EventType(str, Enum):
    DELTA = "delta_event"
    OTHER = "other_event"


class DeltaEventPayload(BaseModel):
    type: Literal[EventType.DELTA]
    delta: Any


class OtherEventPayload(BaseModel):
    type: Literal[EventType.OTHER]
    data: Any


class Event(BaseModel):
    id: str = Field(default_factory=lambda: str(ULID()))
    name: str = Field(default="")
    data_type: Literal[DataType.EVENT] = DataType.EVENT
    timestamp: datetime = Field(default_factory=datetime.now)
    tags: dict[str, str] = Field(default_factory=dict)
    payload: DeltaEventPayload | OtherEventPayload
    parent_id: Optional[str] = Field(default=None)  # Parent span ID
    trace_id: str
    app_name: str


================================================
FILE: agentkit/trace/tracer.py
================================================
from abc import ABC, abstractmethod
from typing import Optional

from .span import Event, Span


class Tracer(ABC):
    @abstractmethod
    def record_span(self, span: Span) -> None:
        pass

    @abstractmethod
    def record_event(self, event: Event) -> None:
        pass

    @abstractmethod
    def get_spans(self, trace_id: str) -> list[Span]:
        pass

    @abstractmethod
    def get_events(self, trace_id: str) -> list[Event]:
        pass

    @abstractmethod
    def get_trace(self, trace_id: str) -> Optional[dict]:
        pass

    @abstractmethod
    def list_traces(self, limit: int = 100, offset: int = 0) -> list[dict]:
        pass

    def get_trace_raw(self, trace_id: str) -> Optional[dict]:
        """
        Get raw trace data (optional implementation).
        Default behavior is to call get_trace().
        """
        return self.get_trace(trace_id)


================================================
FILE: agentkit/trace/types.py
================================================
from pydantic import BaseModel


class Error(BaseModel):
    code: int
    message: str


# class ModelParams(BaseModel):
#     name: str
#     response_format: Optional[Any] = None
#     toolcall_parser_version: Optional[str] = None
#     parallel_tool_calls: bool = True
#     infer_kwargs: dict[str, Any] = {}
#
#
# class Function(BaseModel):
#     arguments: str | None = None
#     """
#     The arguments to call the function with, as generated by the model in JSON
#     format. Note that the model does not always generate valid JSON, and may
#     hallucinate parameters not defined by your function schema. Validate the
#     arguments in your code before calling your function.
#     """
#
#     name: str | None = None
#     """The name of the function to call."""
#
#
# class ChatToolCall(BaseModel):
#     index: int | None = None
#     """The index of the tool call."""
#
#     id: str | None = None
#     """The ID of the tool call."""
#
#     function: Function
#     """The function that the model called."""
#
#     type: str | None = "function"
#     """The type of the tool. Currently, only `function` is supported."""
#
#
# class ChatMessage(BaseModel):
#     id: Optional[str] = None
#     role: Optional[str] = None
#     content: str | list[dict] | None = None
#     tool_call_id: Optional[str] = None
#     tool_calls: Optional[list[ChatToolCall]] = None
#
#     # for train use
#     extra_info: Optional[dict] = None
#
#
# class LLMRequest(BaseModel):
#     messages: list[ChatMessage]
#     model: ModelParams


================================================
FILE: config.yaml
================================================
# Global runtime defaults for StepDeepResearch.
#
# Used as the default context budget for the force-final-answer workflow in BaseStepAgent.
# `scripts.runner` can override these via its own `--config` YAML or CLI flags.
context_upper_limit: 80000
context_lower_limit: 60000


================================================
FILE: cortex/__init__.py
================================================
from .env import load_env as _load_env

_load_env()


================================================
FILE: cortex/agents/__init__.py
================================================
"""Agent components module."""

from cortex.agents.base_agent import BaseAgent
from cortex.agents.base_step_agent import BaseStepAgent
from cortex.agents.react_agent import ReActAgent
from cortex.agents.types import (
    AgentConfig,
    AgentMessageType,
    AgentResponse,
    AgentRunningStatus,
)

__all__ = [
    "BaseAgent",
    "BaseStepAgent",
    "AgentConfig",
    "AgentResponse",
    "AgentRunningStatus",
    "AgentMessageType",
    "ReActAgent",
]


================================================
FILE: cortex/agents/agent_factory.py
================================================
"""
AgentFactory is the factory class for Agent, responsible for creating and managing Agents
"""

from typing import Awaitable, Callable

from cortex.agents.base_agent import BaseAgent
from cortex.agents.types import AgentConfig


class AgentFactory:
    """
    AgentFactory is the factory class for Agent, responsible for creating and managing Agents
    """

    agent_make_func: dict[str, Callable[[AgentConfig, str], Awaitable[BaseAgent]]] = {}
    default_agent_configs: dict[str, AgentConfig] = {}

    def list_agents(self) -> list[AgentConfig]:
        """
        Return all registered Agent configurations
        """
        return list(self.default_agent_configs.values())

    def get_default_agent_config(self, name: str) -> AgentConfig:
        """
        Get Agent configuration
        """
        config = self.default_agent_configs.get(name)
        if config is None:
            raise ValueError(
                f"AgentConfig not provided, and no default configuration set for '{name}' in factory"
            )
        return config

    def register_agent(
        self,
        name: str,
        make_agent_func: Callable[[AgentConfig, str], Awaitable[BaseAgent]],
        default_config: AgentConfig | None = None,
    ) -> None:
        """
        Register Agent
        """
        self.agent_make_func[name] = make_agent_func
        if default_config is not None:
            self.default_agent_configs[name] = default_config

    async def make_agent(
        self, name: str, context_id: str, agent_config: AgentConfig | None
    ) -> BaseAgent:
        """
        Create Agent
        """
        if name not in self.agent_make_func:
            raise ValueError(f"Agent {name} not found")
        config = agent_config or self.default_agent_configs.get(name)
        if config is None:
            raise ValueError(
                f"AgentConfig not provided, and no default configuration set for '{name}' in factory"
            )
        make_agent_func = self.agent_make_func[name]
        return await make_agent_func(config, context_id)


================================================
FILE: cortex/agents/base_agent.py
================================================
"""Base Agent class, provides run() interface as the base class for all Agents."""

import asyncio
import copy
import logging
from abc import abstractmethod
from typing import Any, AsyncGenerator

from cortex.model.definition import ChatMessage, ChatToolCall, ContentBlockType

from cortex.agents.input.input import InputChannel
from cortex.agents.types import AgentConfig, AgentResponse
from cortex.model import ModelAPI
from cortex.model.provider import ModelProvider
from cortex.model.stepfun_provider import StepFunModelProvider
from cortex.tools.toolset import ToolSet

logger = logging.getLogger(__name__)


class BaseAgent:
    """Base Agent class, provides run() interface."""

    model: Any
    name: str | None = None
    description: str | None = None
    system_prompt: str | None = None
    max_steps: int | None = 5
    _input_channel: InputChannel[ChatMessage] | None = None
    provider: ModelAPI

    def update_from_config(self):
        """Update agent properties from config."""
        # Iterate config attributes and update self if attribute exists
        for key, _ in self.config.model_dump().items():
            setattr(self, key, copy.deepcopy(getattr(self.config, key)))

        self.name = self.name or self.__class__.__name__
        self.description = (
            self.description
            or f"Call the {self.name} sub-agent to handle specific tasks"
        )

    def __init__(
        self,
        provider: ModelProvider | None = None,
        config: AgentConfig | None = None,
        toolset: ToolSet | None = None,
    ):
        self.config = config
        self.update_from_config()

        self._toolset = toolset

        if provider is None:
            provider = StepFunModelProvider(model_params=self.model)
        self.provider = ModelAPI(provider)

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc_value, traceback):
        """Async context manager exit."""

    async def run(
        self,
        messages: list[ChatMessage] | InputChannel[ChatMessage],
        additional_kwargs: dict | None = None,
    ) -> AsyncGenerator[AgentResponse, None]:
        """Run agent, returns AgentResponse generator."""
        async for response in self._run(messages, additional_kwargs):
            if response.agent_name is None:
                response.agent_name = self.name
            yield response

    @abstractmethod
    async def _run(
        self,
        messages: list[ChatMessage] | InputChannel[ChatMessage],
        additional_kwargs: dict | None = None,
    ) -> AsyncGenerator[AgentResponse, None]:
        """
        Run agent, subclasses must implement this method.

        Args:
            messages: Input message list or input channel
            additional_kwargs: Additional parameters

        Yields:
            AgentResponse: Agent response object
        """
        raise NotImplementedError("Subclasses must implement this method")

    def model_api(self) -> ModelAPI:
        return self.provider

    def toolset(self) -> ToolSet | None:
        """
        Get toolset.

        Returns:
            ToolSet | None: Toolset
        """
        return self._toolset

    def as_tool(self, timeout: float | None = None) -> dict[str, Any]:
        """
        Convert Agent to parameters needed for Tool creation.

        Returns a dictionary containing parameters for creating AgentTool:
        - name: Tool name (uses agent's name)
        - description: Tool description (uses agent's description)
        - agent_name: Agent name (used to specify which agent to call, as metadata)
        - timeout: Timeout (optional, if provided)

        Note: When creating AgentTool, channel parameter is also required, which is not included in this method.

        Args:
            timeout: Timeout in seconds, if None it won't be included in the returned dictionary

        Returns:
            dict: Dictionary containing parameters for creating AgentTool:
                {
                    "name": str,           # Tool name
                    "description": str,    # Tool description
                    "agent_name": str,     # Agent name (metadata)
                    "timeout": float,      # Timeout (optional)
                }

        Example:
            >>> agent = MathAgent(config)
            >>> tool_params = agent.as_tool(timeout=60.0)
            >>> # Channel is also required when creating AgentTool
            >>> tool = AgentTool(**tool_params, channel=channel)
            >>> toolset.register(tool)
        """
        agent_name = self.name or self.__class__.__name__

        tool_params: dict[str, Any] = {
            "name": agent_name,
            "description": self.description
            or f"Call {agent_name} Agent to handle specific tasks",
            "agent_name": agent_name,  # As metadata, caller knows which agent this tool corresponds to
        }

        if timeout is not None:
            tool_params["timeout"] = timeout

        logger.debug(
            "BaseAgent.as_tool returns tool params: name=%s, agent_name=%s, has_timeout=%s",
            tool_params["name"],
            tool_params["agent_name"],
            "timeout" in tool_params,
        )

        return tool_params

    @staticmethod
    def has_tool_call(message: ChatMessage) -> bool:
        """
        Check if message contains tool calls.

        Args:
            message: ChatMessage object

        Returns:
            bool: True if message contains tool calls, False otherwise
        """
        if not message:
            return False
        tool_calls = getattr(message, "tool_calls", None)
        return (
            tool_calls is not None and len(tool_calls) > 0
            if isinstance(tool_calls, (list, tuple))
            else tool_calls is not None
        )

    async def _execute_single_tool(self, tool_call: ChatToolCall) -> ChatMessage | None:
        """
        Execute a single tool call.

        Args:
            tool_call: Tool call object containing function.name, function.arguments, id, etc.

        Returns:
            ChatMessage: Tool call result message with role "tool"
        """
        tool_name = tool_call.function.name
        tool_args = tool_call.function.arguments
        tool_call_id = tool_call.id

        try:
            # Execute tool call
            result = await self._toolset.call(
                tool_name=tool_name, parameters=tool_args, tool_call_id=tool_call_id
            )
            logger.info(f"@{self.name} Tool {tool_name} result: {result}")
            if result is None:
                return None

            return ChatMessage(
                role="tool",
                content=result,
                tool_call_id=tool_call_id,
            )

        except Exception as e:
            error_msg = f"Error calling tool {tool_name}: {str(e)}"
            logger.error(f"@{self.name} {error_msg}")

            tool_result_content = [
                {
                    "type": ContentBlockType.TEXT.value,
                    ContentBlockType.TEXT.value: error_msg,
                }
            ]

            return ChatMessage(
                role="tool",
                content=tool_result_content,
                tool_call_id=tool_call_id,
            )

    async def run_tool_call(self, message: ChatMessage) -> list[ChatMessage]:
        """
        Extract tool calls from message and execute them, returning list of tool call result messages.

        Args:
            message: ChatMessage object

        Returns:
            list[ChatMessage]: List of tool call result messages, each with role "tool"
        """
        if not message:
            return []

        if not self._toolset:
            logger.warning(f"@{self.name} run_tool_call: toolset not initialized")
            return []

        tool_calls = getattr(message, "tool_calls", None)
        if not tool_calls:
            return []

        # Ensure tool_calls is a list
        if isinstance(tool_calls, (list, tuple)):
            toolcalls_list = list(tool_calls)
        else:
            toolcalls_list = [tool_calls]

        # Execute all tool calls sequentially
        tool_result_messages = []
        for tool_call in toolcalls_list:
            result_message = await self._execute_single_tool(tool_call)
            if result_message is not None:
                tool_result_messages.append(result_message)

        return tool_result_messages

    async def run_tool_call_concurrency(
        self, message: ChatMessage
    ) -> list[ChatMessage]:
        """
        Extract concurrent tool calls from message and execute them (when there are multiple tool calls).

        Args:
            message: ChatMessage object

        Returns:
            list[ChatMessage]: List of concurrent tool call result messages, only returned when tool call count > 1
        """
        if not message:
            return []

        if not self._toolset:
            logger.warning(f"@{self.name} run_tool_call_concurrency: toolset not initialized")
            return []

        tool_calls = getattr(message, "tool_calls", None)
        if not tool_calls:
            return []

        # Ensure tool_calls is a list
        if isinstance(tool_calls, (list, tuple)):
            toolcalls_list = list(tool_calls)
        else:
            toolcalls_list = [tool_calls]

        # Only return when there are multiple tool calls (concurrent scenario)
        if len(toolcalls_list) <= 1:
            return []

        # Execute all tool calls concurrently
        tool_result_messages = await asyncio.gather(
            *[self._execute_single_tool(tc) for tc in toolcalls_list]
        )

        # Filter out None results
        return [msg for msg in tool_result_messages if msg is not None]


================================================
FILE: cortex/agents/base_step_agent.py
================================================
import copy
import json
import logging
import re
from abc import abstractmethod
from typing import Any, AsyncGenerator, Callable

from agentkit.trace import create_span
from cortex.agents.base_agent import BaseAgent
from cortex.agents.input.input import InputChannel
from cortex.agents.types import (
    AgentConfig,
    AgentMessageType,
    AgentResponse,
    AgentRunningStatus,
)
from cortex.context import BaseContext
from cortex.model.definition import ChatMessage
from cortex.model.provider import ModelProvider
from cortex.tools.toolset import ToolSet
from cortex.runtime_config import get_context_limit_overrides

try:
    import tiktoken
except Exception:  # noqa: BLE001
    tiktoken = None

logger = logging.getLogger(__name__)

DEFAULT_FORCE_FINAL_ANSWER_UPPER_LIMIT = 100_000
DEFAULT_FORCE_FINAL_ANSWER_THRESHOLD = DEFAULT_FORCE_FINAL_ANSWER_UPPER_LIMIT
DEFAULT_FORCE_FINAL_ANSWER_LOWER_LIMIT_RATIO = 0.9
DEFAULT_FORCE_FINAL_ANSWER_LOWER_LIMIT = max(
    int(DEFAULT_FORCE_FINAL_ANSWER_UPPER_LIMIT * DEFAULT_FORCE_FINAL_ANSWER_LOWER_LIMIT_RATIO),
    1,
)
DEFAULT_FORCE_FINAL_ANSWER_PROMPT = (
    "你现在已经达到了你所能处理的最大上下文长度。你应该停止进行工具调用,"
    "并基于以上所有信息重新思考,然后按照以下格式提供你认为最可能的答案:"
    "<think>你的最终思考</think>\n<answer>你的答案</answer>"
)
_AVG_CHARS_PER_TOKEN = 3


def _get_encoding(model_name: str | None):
    if not tiktoken:
        return None
    if not model_name:
        try:
            return tiktoken.get_encoding("cl100k_base")
        except Exception:  # noqa: BLE001
            return None
    try:
        return tiktoken.encoding_for_model(model_name)
    except Exception:  # noqa: BLE001
        try:
            return tiktoken.get_encoding("cl100k_base")
        except Exception:  # noqa: BLE001
            return None


def _estimate_token_length(messages: list[ChatMessage], model_name: str | None) -> int:
    """Token estimator with tiktoken fallback."""
    encoding = _get_encoding(model_name)
    total_tokens = 0
    for message in messages:
        try:
            payload = message.model_dump(exclude_none=True)
        except Exception:
            payload = {
                "role": getattr(message, "role", None),
                "content": getattr(message, "content", None),
            }
        serialized = json.dumps(payload, ensure_ascii=False)
        if encoding:
            try:
                total_tokens += len(encoding.encode(serialized))
                continue
            except Exception:  # noqa: BLE001
                encoding = None
        total_tokens += len(serialized) // _AVG_CHARS_PER_TOKEN
    return total_tokens


def _compress_batch_search_result(content: str) -> str:
    """Strip verbose content and mark compressed."""
    compressed = re.sub(r"<content>.*?</content>\s*", "", content, flags=re.S)
    compressed = compressed.replace(
        "<batch_search_results>", "<batch_search_results_compressed>", 1
    )
    compressed = compressed.replace(
        "</batch_search_results>", "</batch_search_results_compressed>", 1
    )
    return compressed


class BaseStepAgent(BaseAgent):
    """
    Base class for step-based Agent

    Implements run() method, executes tasks by calling step() in a loop
    step() method returns a tuple containing a flag indicating whether to stop
    """

    def __init__(
        self,
        context: BaseContext,
        config: AgentConfig,
        provider: ModelProvider | None = None,
        toolset: ToolSet | None = None,
    ):
        super().__init__(config=config, toolset=toolset, provider=provider)
        self.current_round = 0
        self.context = context
        extra_cfg = config.extra_config if config and config.extra_config else {}
        self._force_final_answer_enabled = extra_cfg.get("force_final_answer", False)
        threshold_override = extra_cfg.get("final_answer_context_threshold")
        upper_override = extra_cfg.get("final_answer_context_upper_limit")
        lower_override = extra_cfg.get("final_answer_context_lower_limit")
        upper_from_extra = upper_override is not None
        lower_from_extra = lower_override is not None
        if not upper_from_extra or not lower_from_extra:
            runtime_upper, runtime_lower = get_context_limit_overrides()
            if not upper_from_extra and runtime_upper is not None:
                upper_override = runtime_upper
            # Only apply runtime lower default when upper isn't explicitly overridden;
            # otherwise keep the existing "derive lower from upper" behavior.
            if (
                not lower_from_extra
                and not upper_from_extra
                and runtime_lower is not None
            ):
                lower_override = runtime_lower

        def _normalize_limit(value: Any) -> int | None:
            if isinstance(value, (int, float)):
                return int(value)
            return None

        upper_limit = _normalize_limit(upper_override)
        if upper_limit is None:
            upper_limit = _normalize_limit(threshold_override)
        if upper_limit is None or upper_limit <= 0:
            upper_limit = DEFAULT_FORCE_FINAL_ANSWER_UPPER_LIMIT
        upper_limit = max(upper_limit, 2)

        lower_limit = _normalize_limit(lower_override)
        if lower_limit is None or lower_limit <= 0:
            derived = int(upper_limit * DEFAULT_FORCE_FINAL_ANSWER_LOWER_LIMIT_RATIO)
            lower_limit = derived if derived > 0 else DEFAULT_FORCE_FINAL_ANSWER_LOWER_LIMIT
        lower_limit = max(lower_limit, 1)
        if lower_limit >= upper_limit:
            lower_limit = max(upper_limit - 1, 1)

        self._force_final_answer_upper_limit = upper_limit
        self._force_final_answer_lower_limit = lower_limit
        self._force_final_answer_prompt = extra_cfg.get(
            "final_answer_prompt", DEFAULT_FORCE_FINAL_ANSWER_PROMPT
        )
        self._force_prompt_inserted = False
        self._model_name = getattr(config.model, "name", None) if config and config.model else None

    def _insert_final_prompt(self) -> None:
        """Activate the force-final-answer prompt for subsequent model calls.

        Kept for backward compatibility; this does not mutate persisted context history.
        """
        if not self._force_final_answer_enabled or self._force_prompt_inserted:
            return
        self._force_prompt_inserted = True
        logger.info("@%s Final answer prompt activated", self.name)

    def _make_force_final_answer_message(self) -> ChatMessage:
        prompt = self._force_final_answer_prompt or DEFAULT_FORCE_FINAL_ANSWER_PROMPT
        return ChatMessage(role="system", content=prompt)

    def _ensure_final_prompt(self, messages: list[ChatMessage]) -> None:
        """Ensure the force-final-answer prompt is present in model input.

        This should not mutate the persisted context history.
        """
        if not self._force_final_answer_enabled or not self._force_prompt_inserted:
            return
        prompt_message = self._make_force_final_answer_message()
        if messages:
            last = messages[-1]
            if (
                getattr(last, "role", None) == prompt_message.role
                and getattr(last, "content", None) == prompt_message.content
            ):
                return
        messages.append(prompt_message)

    @staticmethod
    def _copy_messages(messages: list[ChatMessage]) -> list[ChatMessage]:
        """Deep-copy messages so we can mutate model input without touching stored history."""
        copied: list[ChatMessage] = []
        for message in messages:
            try:
                copied.append(message.model_copy(deep=True))
            except Exception:
                copied.append(copy.deepcopy(message))
        return copied

    @classmethod
    def _compress_batch_search_in_block(cls, block: dict) -> bool:
        """Compress batch_search XML in the first text block found (in-place)."""
        if block.get("type") == "text":
            text_value = block.get("text")
            if (
                isinstance(text_value, str)
                and "<batch_search_results" in text_value
                and "batch_search_results_compressed" not in text_value
            ):
                block["text"] = _compress_batch_search_result(text_value)
                return True

        nested = block.get("content")
        if isinstance(nested, list):
            for item in nested:
                if isinstance(item, dict) and cls._compress_batch_search_in_block(item):
                    return True
        return False

    @classmethod
    def _compress_batch_search_in_content(cls, content: Any) -> tuple[Any, bool]:
        if isinstance(content, str):
            if (
                "<batch_search_results" in content
                and "batch_search_results_compressed" not in content
            ):
                return _compress_batch_search_result(content), True
            return content, False

        if isinstance(content, list):
            for item in content:
                if isinstance(item, dict) and cls._compress_batch_search_in_block(item):
                    return content, True
            return content, False

        return content, False

    def _prepare_messages_for_model(self) -> list[ChatMessage]:
        """Build the message list sent to the model, without mutating stored history."""
        raw_messages = self.context.get_all()
        messages: list[ChatMessage] = list(raw_messages)

        if not self._force_final_answer_enabled:
            return messages

        # If final-answer mode has been activated before, always include the prompt in model input.
        self._ensure_final_prompt(messages)

        token_estimate = _estimate_token_length(messages, self._model_name)
        if token_estimate < self._force_final_answer_upper_limit:
            return messages

        # We are going to mutate messages for context management: deep-copy first.
        messages = self._copy_messages(messages)

        self._handle_context_overflow(messages)
        self._ensure_final_prompt(messages)
        self._ensure_context_within_upper_limit(messages)
        return messages

    def _shrink_batch_search_results(self, messages: list[ChatMessage]) -> bool:
        """Compress earliest uncompressed batch_search_result content (model-input only)."""
        for message in messages:
            content = getattr(message, "content", None)
            new_content, changed = self._compress_batch_search_in_content(content)
            if changed:
                message.content = new_content
                logger.info("@%s Compressed batch_search_results to save tokens", self.name)
                return True
        return False

    @staticmethod
    def _parse_tool_call_arguments(raw_arguments: Any) -> dict[str, Any]:
        """Safely parse tool call arguments JSON."""
        if not isinstance(raw_arguments, str) or not raw_arguments.strip():
            return {}
        try:
            parsed = json.loads(raw_arguments)
            return parsed if isinstance(parsed, dict) else {}
        except Exception:  # noqa: BLE001
            return {}

    def _is_search_tool_call(self, tool_name: str | None, tool_args: dict[str, Any]) -> bool:
        """Identify whether a tool call is search-related."""
        if not tool_name:
            return False
        lowered = tool_name.lower()
        if "search" in lowered:
            return True
        if lowered == "batch_web_surfer":
            action = tool_args.get("action")
            if isinstance(action, str) and action.lower() == "batch_search":
                return True
        return False

    def _drop_oldest_tool_cycle(
        self,
        messages: list[ChatMessage],
        predicate: Callable[[str | None, dict[str, Any]], bool] | None = None,
        log_context: str = "tool",
    ) -> bool:
        """Drop earliest tool call message plus all corresponding tool results."""
        drop_indices: set[int] = set()
        for idx, message in enumerate(messages):
            tool_calls = getattr(message, "tool_calls", None)
            if not tool_calls:
                continue
            matched_any = False
            for tc in tool_calls:
                try:
                    tool_name = tc.function.name
                    raw_arguments = tc.function.arguments
                except Exception:
                    tool_name = None
                    raw_arguments = None
                parsed_args = self._parse_tool_call_arguments(raw_arguments)
                if predicate and not predicate(tool_name, parsed_args):
                    continue
                matched_any = True

            if not matched_any:
                continue

            drop_indices.add(idx)

            # Find corresponding tool results for all tool calls in this message.
            for tc in tool_calls:
                tc_id = getattr(tc, "id", None)
                if not tc_id:
                    continue
                for j in range(idx + 1, len(messages)):
                    tool_msg = messages[j]
                    if getattr(tool_msg, "role", None) != "tool":
                        continue
                    if getattr(tool_msg, "tool_call_id", None) == tc_id:
                        drop_indices.add(j)
                        break

            break

        if not drop_indices:
            return False

        for del_idx in sorted(drop_indices, reverse=True):
            messages.pop(del_idx)

        logger.warning(
            "@%s Dropped earliest %s tool call/results to shrink context", self.name, log_context
        )
        return True

    def _trim_oldest_messages(self, messages: list[ChatMessage]) -> bool:
        """Drop oldest non-system messages until under threshold."""
        if not messages or len(messages) <= 1:
            return False
        removed = False
        idx = 0
        while (
            idx < len(messages)
            and _estimate_token_length(messages, self._model_name)
            > self._force_final_answer_upper_limit
        ):
            if getattr(messages[idx], "role", None) == "system":
                idx += 1
                continue
            messages.pop(idx)
            removed = True
        if removed:
            logger.warning("@%s Trimmed oldest messages to satisfy context budget", self.name)
        return removed

    def _ensure_context_within_upper_limit(self, messages: list[ChatMessage]) -> None:
        """Ensure context is below the configured upper limit before forcing final answer."""
        if not self._force_final_answer_enabled:
            return
        while True:
            token_estimate = _estimate_token_length(messages, self._model_name)
            if token_estimate <= self._force_final_answer_upper_limit:
                return
            if self._drop_oldest_tool_cycle(messages, log_context="any"):
                continue
            if self._trim_oldest_messages(messages):
                continue
            break

        final_tokens = _estimate_token_length(messages, self._model_name)
        if final_tokens > self._force_final_answer_upper_limit:
            logger.warning(
                "@%s Unable to trim context below upper limit (%s tokens remaining)",
                self.name,
                final_tokens,
            )

    def _handle_context_overflow(self, messages: list[ChatMessage]) -> None:
        """Enforce two-threshold hysteresis for search results before triggering final answer."""
        if not self._force_final_answer_enabled:
            return

        token_estimate = _estimate_token_length(messages, self._model_name)
        if token_estimate < self._force_final_answer_upper_limit:
            return

        logger.warning(
            (
                "@%s Context %s tokens reached upper limit %s; processing search tool payloads "
                "until below lower limit %s"
            ),
            self.name,
            token_estimate,
            self._force_final_answer_upper_limit,
            self._force_final_answer_lower_limit,
        )

        while True:
            token_estimate = _estimate_token_length(messages, self._model_name)
            if token_estimate < self._force_final_answer_lower_limit:
                break

            if self._shrink_batch_search_results(messages):
                continue

            if self._drop_oldest_tool_cycle(messages, self._is_search_tool_call, "search"):
                continue

            break

        final_tokens = _estimate_token_length(messages, self._model_name)
        if final_tokens < self._force_final_answer_lower_limit:
            return

        logger.warning(
            (
                "@%s Exhausted search tool cleanup but context still %s tokens (>= lower limit %s); "
                "forcing final answer workflow"
            ),
            self.name,
            final_tokens,
            self._force_final_answer_lower_limit,
        )

        if not self._force_prompt_inserted:
            self._force_prompt_inserted = True
            logger.info("@%s Final answer prompt activated", self.name)

    async def _run(
        self,
        messages: list[ChatMessage] | InputChannel[ChatMessage],
        additional_kwargs: dict | None = None,
    ) -> AsyncGenerator[AgentResponse, None]:
        """
        Run agent, calls step() method in a loop

        Args:
            messages: Input message list or input channel
            additional_kwargs: Additional parameters

        Yields:
            AgentResponse: Agent response object
        """
        if additional_kwargs is None:
            additional_kwargs = {}

        input_messages = []
        # Handle input messages
        if isinstance(messages, list):
            input_messages = messages
        elif isinstance(messages, InputChannel):
            input_messages = await messages.get()

        self.current_round = 0
        # Initialize history messages as member variable
        self.context.add(input_messages)
        should_stop = False

        # Loop calling step() until stop or reach max steps
        while self.current_round < self.max_steps:
            with create_span(
                name=f"@{self.name} Round {self.current_round}/{self.max_steps}"
            ):
                if should_stop and not self.config.unfinished_mode:
                    break
                if should_stop:
                    if isinstance(messages, InputChannel):
                        input_messages = await messages.get()
                        self.context.add(input_messages)
                        should_stop = False
                else:
                    if isinstance(messages, InputChannel):
                        input_messages = await messages.get_no_wait()
                        self.context.add(input_messages)

                self.current_round += 1
                logger.info(f"@{self.name} Round {self.current_round}/{self.max_steps}")

                try:
                    # Call step() method (now an async generator)
                    last_response = None
                    model_messages = self._prepare_messages_for_model()
                    async for response in self._step(model_messages, additional_kwargs):
                        # Update history messages (using member variable)
                        # Only add complete messages to history (has role field and not STREAM type)
                        logger.info(f"@{self.name} Response: {response}")
                        if response is None:
                            continue
                        if response.message:
                            # STREAM type messages are incremental updates, should not be added to history
                            if response.message_type == AgentMessageType.STREAM.value:
                                # Skip streaming incremental messages, they will be handled in accumulated messages
                                pass
                            else:
                                # For non-streaming messages, ensure role field exists
                                message_to_add = None
                                if isinstance(response.message, ChatMessage):
                                    if (
                                        hasattr(response.message, "role")
                                        and response.message.role
                                    ):
                                        message_to_add = response.message
                                elif isinstance(response.message, dict):
                                    if response.message.get("role"):
                                        message_to_add = ChatMessage(**response.message)

                                # Only add message to history when it has a valid role
                                if message_to_add and message_to_add.role:
                                    self.context.add([message_to_add])
                                else:
                                    logger.warning(
                                        f"@{self.name} Skipping message without role: {response.message_type}"
                                    )

                        # Set metadata (only includes round, history_messages uses member variable)
                        if response.metadata is None:
                            response.metadata = {}
                        response.metadata["step_count"] = self.current_round

                        # Return response
                        yield response
                        last_response = response

                        # Check if error occurred
                        if response.status == AgentRunningStatus.ERROR.value:
                            logger.error(
                                f"@{self.name} Error at round {self.current_round}: {response.error_msg}"
                            )
                            should_stop = True
                            break

                    # Check last response status, decide whether to stop
                    if last_response:
                        if last_response.status == AgentRunningStatus.FINISHED.value:
                            should_stop = True
                            logger.info(
                                f"@{self.name} Finished at round {self.current_round}"
                            )
                        elif last_response.status == AgentRunningStatus.ERROR.value:
                            should_stop = True
                        elif last_response.status == AgentRunningStatus.RUNNING.value:
                            # If still running, may need to continue (e.g., has tool calls)
                            # Here can decide whether to stop based on actual situation
                            pass

                except Exception as e:
                    err_text = str(e) or repr(e)
                    logger.error(
                        f"@{self.name} Exception at round {self.current_round}: {err_text}",
                        exc_info=True,
                    )
                    error_response = AgentResponse(
                        status=AgentRunningStatus.ERROR.value,
                        error_msg=err_text,
                        metadata={
                            "step_count": self.current_round,
                        },
                    )
                    yield error_response
                    should_stop = True
                    break

                # If max steps reached and not stopped yet
                if self.current_round >= self.max_steps and not should_stop:
                    logger.warning(
                        f"@{self.name} Reached max steps ({self.max_steps}) without stopping"
                    )
                    # Generate final response
                    final_response = AgentResponse(
                        status=AgentRunningStatus.STOPPED.value,
                        metadata={
                            "step_count": self.current_round,
                        },
                    )
                    yield final_response

    @abstractmethod
    async def _step(
        self,
        messages: list[ChatMessage],
        additional_kwargs: dict | None = None,
    ) -> AsyncGenerator[AgentResponse, None]:
        """
        Execute a single step, subclasses must implement this method.

        Args:
            messages: Current message history.
            additional_kwargs: Additional parameters.

        Yields:
            AgentResponse: Response for current step, can yield multiple responses.
                - Last response with status FINISHED indicates completion.
                - Status ERROR indicates an error occurred.
                - Status RUNNING indicates continue execution.
        """
        raise NotImplementedError("Subclasses must implement this method")


================================================
FILE: cortex/agents/checkpoint_agent/checkpoint_agent.py
================================================
import logging
from abc import abstractmethod
from typing import AsyncGenerator

from cortex.model.definition import ChatMessage, ContentBlockType
from pydantic import BaseModel

from cortex.agents.base_agent import BaseAgent
from cortex.agents.checkpoint_agent.checkpointer import CheckPointer, CheckpointStorage
from cortex.agents.types import (
    AgentConfig,
    AgentMessageType,
    AgentResponse,
    AgentRunningStatus,
)
from cortex.model.provider import ModelProvider
from cortex.tools.toolset import ToolSet

logger = logging.getLogger(__name__)


class PendingToolCall(BaseModel):
    """Pending tool call"""

    request: ChatMessage | None
    results: dict[str, ChatMessage] | None


class CheckpointState(BaseModel):
    """Checkpoint state definition"""

    messages: list[ChatMessage] | None
    pending_tool_calls: list[PendingToolCall] | None
    tool_call_results: list[ChatMessage] | None
    config: AgentConfig | None
    current_step: int
    max_steps: int | None
    finished: bool
    error: str | None


class CheckpointAgent(BaseAgent):
    """Checkpoint-based single-step execution Agent (without LangGraph)"""

    def __init__(
        self,
        config: AgentConfig,
        storage: CheckpointStorage,
        provider: ModelProvider | None = None,
        toolset: ToolSet | None = None,
        thread_id: str | None = None,
    ):
        super().__init__(config=config, toolset=toolset, provider=provider)
        self.thread_id = thread_id or f"{self.name}_main"
        self.storage = storage
        self._state: CheckpointState | None = None

    def _init_state(
        self,
    ) -> CheckpointState:
        """Initialize state"""
        return CheckpointState(
            messages=[],
            pending_tool_calls=[],
            tool_call_results=[],
            config=self.config,
            current_step=0,
            max_steps=getattr(self.config, "max_steps", 10),
            finished=False,
            error=None,
        )

    async def _process_tool_call_results(
        self, state: CheckpointState
    ) -> AsyncGenerator[AgentResponse, None]:
        """Process tool call results"""
        if not state.tool_call_results or len(state.tool_call_results) == 0:
            return

        logger.debug(
            "@%s Processing %d tool call results", self.name, len(state.tool_call_results)
        )
        async for response in self._tool_call_handler(
            state.messages, state.tool_call_results
        ):
            yield response
            if response.message:
                state.messages.append(response.message)

        # Clear processed results
        state.tool_call_results = []

    async def _execute_step(
        self, state: CheckpointState, additional_kwargs: dict | None = None
    ) -> AsyncGenerator[AgentResponse, None]:
        """Execute single step"""
        step_responses = []
        try:
            async for response in self._step(state.messages, additional_kwargs):
                step_responses.append(response)
                yield response

                # Handle message history update
                if response.message:
                    # STREAM type messages are incremental updates, should not be added to history
                    if response.message_type == AgentMessageType.STREAM.value:
                        pass
                    else:
                        # For non-streaming messages, ensure role field exists
                        message_to_add = None
                        if isinstance(response.message, ChatMessage):
                            if (
                                response.message.tool_calls
                                and len(response.message.tool_calls) > 0
                            ):
                                # Execute tool calls
                                results = await self.run_tool_call(response.message)
                                # Convert result list to dict with tool_call_id as key
                                state.pending_tool_calls.append(
                                    PendingToolCall(
                                        request=response.message,
                                        results={
                                            result.tool_call_id: result
                                            for result in results
                                        },
                                    )
                                )
                                message_to_add = response.message
                            elif response.message.tool_call_id:
                                # Tool call result
                                state.tool_call_results.append(response.message)
                            else:
                                message_to_add = response.message

                        elif isinstance(response.message, dict):
                            if response.message.get("role"):
                                message_to_add = ChatMessage(**response.message)

                        # Only add message to history when it has a valid role
                        if message_to_add and message_to_add.role:
                            state.messages.append(message_to_add)
                        else:
                            logger.warning(
                                "@%s Skipping message without role: %s",
                                self.name,
                                response.message_type,
                            )

            state.current_step += 1
            # Check if finished
            if step_responses:
                last_response = step_responses[-1]
                if last_response.status == AgentRunningStatus.FINISHED.value:
                    state.finished = True
                elif last_response.status == AgentRunningStatus.ERROR.value:
                    state.error = last_response.error_msg
                    state.finished = True

        except Exception as e:
            logger.error("@%s Error during step execution: %s", self.name, str(e))
            state.error = str(e)
            state.finished = True
            raise

    async def _update_client_tool_results(self, state: CheckpointState) -> bool:
        """
        Update client tool call results.
        Returns True if there are still pending tool calls.
        """
        if not state.pending_tool_calls or len(state.pending_tool_calls) == 0:
            return False

        new_pending_tool_calls = []
        for pending_item in state.pending_tool_calls:
            request = pending_item.request
            results = pending_item.results
            # Check if each tool_call has a corresponding result
            all_matched = True

            for tool_call in request.tool_calls:
                tool_call_id = tool_call.id
                # Check if result already exists
                if results.get(tool_call_id):
                    continue

                result_content = self.toolset().get_client_tool_call_result(
                    tool_call_id
                )

                if result_content is None:
                    all_matched = False
                    continue
                results[tool_call_id] = ChatMessage(
                    role="tool",
                    content=[
                        {
                            "type": ContentBlockType.TEXT.value,
                            ContentBlockType.TEXT.value: str(result_content),
                        }
                    ],
                    tool_call_id=tool_call_id,
                )

            if all_matched:
                # All tool_calls have corresponding results, move results to tool_call_results
                state.tool_call_results.extend(results.values())
            else:
                # Still have incomplete tool calls, update results and keep in pending
                pending_item.results = results
                new_pending_tool_calls.append(pending_item)

        state.pending_tool_calls = new_pending_tool_calls
        return len(new_pending_tool_calls) > 0

    def _should_continue(self, state: CheckpointState) -> bool:
        """Determine whether execution should continue"""

        if len(state.pending_tool_calls) > 0:
            return False  # Has pending tool calls, need to wait
        if len(state.tool_call_results) > 0:
            return True
        if state.finished:
            return False  # Already finished
        if state.error:
            return False  # Has error
        if state.current_step >= state.max_steps:
            return False  # Reached max steps
        return True

    async def _run(
        self,
        messages: list[ChatMessage],
        additional_kwargs: dict | None = None,
    ) -> AsyncGenerator[AgentResponse, None]:
        """
        Checkpoint-based run method (without LangGraph)

        Args:
            messages: Input message list or input channel
            additional_kwargs: Additional parameters

        Yields:
            AgentResponse: Agent response object
        """
        # Initialize or load state
        async with CheckPointer[CheckpointState](
            self.thread_id, self.storage, self._init_state(), CheckpointState
        ) as state:
            state.messages.extend(messages)

            # Main execution loop
            while True:
                # 1. Update client tool call results and check if there are pending tool calls
                has_pending = await self._update_client_tool_results(state)
                if has_pending:
                    logger.info("@%s Has pending tool calls, waiting for client response", self.name)
                    return

                # 2. Process completed tool call results
                if len(state.tool_call_results) > 0:
                    async for response in self._process_tool_call_results(state):
                        yield response

                # 3. Check whether execution should continue
                if not self._should_continue(state):
                    logger.info("@%s Execution completed", self.name)
                    break

                # 4. Execute next step
                logger.info("@%s Executing step %d", self.name, state.current_step + 1)
                async for response in self._execute_step(state, additional_kwargs):
                    yield response

    @abstractmethod
    async def _step(
        self,
        messages: list[ChatMessage],
        additional_kwargs: dict | None = None,
    ) -> AsyncGenerator[AgentResponse, None]:
        """
        Execute a single step, subclasses must implement this method.

        Args:
            messages: Current message history.
            additional_kwargs: Additional parameters.

        Yields:
            AgentResponse: Response for current step, can yield multiple responses.
                - Last response with status FINISHED indicates completion.
                - Status ERROR indicates an error occurred.
                - Status RUNNING indicates continue execution.
        """
        raise NotImplementedError("Subclasses must implement this method")

    @abstractmethod
    async def _tool_call_handler(
        self,
        messages: list[ChatMessage],
        tool_calls: list[ChatMessage],
    ) -> AsyncGenerator[AgentResponse, None]:
        """
        Handle tool calls, subclasses must implement this method.

        Args:
            messages: Current message history.
            tool_calls: List of tool call messages.

        Yields:
            AgentResponse: Response for current step, can yield multiple responses.
                - Last response with status FINISHED indicates completion.
                - Status ERROR indicates an error occurred.
                - Status RUNNING indicates continue execution.
        """
        raise NotImplementedError("Subclasses must implement this method")


================================================
FILE: cortex/agents/checkpoint_agent/checkpointer.py
================================================
"""Checkpoint storage implementation."""

import json
import logging
import os
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)

from typing import Generic, TypeVar

from pydantic import BaseModel

T = TypeVar("T", bound=BaseModel)


class CheckpointStorage:
    def save_state(self, checkpoint_id: str, state: dict[str, Any]):
        """Save state."""
        raise NotImplementedError("Subclass must implement this method")

    def load_state(self, checkpoint_id: str) -> dict[str, Any] | None:
        """Load state from storage."""
        raise NotImplementedError("Subclass must implement this method")

    def delete_state(self, checkpoint_id: str):
        """Delete the specified checkpoint."""
        raise NotImplementedError("Subclass must implement this method")

    async def asave_state(self, checkpoint_id: str, state: dict[str, Any]):
        """Save state asynchronously."""
        raise NotImplementedError("Subclass must implement this method")

    async def aload_state(self, checkpoint_id: str) -> dict[str, Any] | None:
        """Load state from storage asynchronously."""
        raise NotImplementedError("Subclass must implement this method")

    async def adelete_state(self, checkpoint_id: str):
        """Delete the specified checkpoint asynchronously."""
        raise NotImplementedError("Subclass must implement this method")


class CheckPointer(Generic[T]):
    """Checkpoint manager base class."""

    def __init__(
        self,
        checkpoint_id: str,
        storage: CheckpointStorage,
        init_state: T | None,
        state_type: type[T],
    ):
        """
        Initialize CheckPointer.

        Args:
            checkpoint_id: Checkpoint ID for identifying and loading specific checkpoints
            storage: Storage backend
            init_state: Initial state
            state_type: State type, must be a class inheriting from BaseModel
        """
        self.checkpoint_id = checkpoint_id
        self.storage = storage
        self.init_state = init_state
        self.state_type = state_type
        self._state: T | None = None

    def __enter__(self) -> T:
        state_dict = self.storage.load_state(self.checkpoint_id)
        if state_dict is None:
            self._state = self.init_state
        else:
            # Convert dict to pydantic model
            self._state = self.state_type.model_validate(state_dict)
        return self._state

    def __exit__(self, exc_type, exc_value, traceback):
        # Auto-save state on exit
        if self._state is not None and self.checkpoint_id is not None:
            try:
                # Use pydantic's model_dump method to convert to dict
                state_dict = self._state.model_dump()
                self.storage.save_state(self.checkpoint_id, state_dict)
                logger.debug("Auto-saved state on exit: checkpoint_id=%s", self.checkpoint_id)
            except Exception as e:
                logger.error("Failed to save state on exit: %s", str(e))
            # todo handle cancel error
        return False

    async def __aenter__(self):
        return self.__enter__()

    async def __aexit__(self, exc_type, exc_value, traceback):
        return self.__exit__(exc_type, exc_value, traceback)


class MemoryCheckPointer(CheckpointStorage):
    """Memory-based Checkpoint storage."""

    def __init__(self):
        self._storage: dict[str, dict[str, Any]] = {}

    def save_state(self, checkpoint_id: str, state: dict[str, Any]):
        """Save state to memory."""
        self._storage[checkpoint_id] = state
        logger.debug("State saved to memory: checkpoint_id=%s", checkpoint_id)

    def load_state(self, checkpoint_id: str) -> dict[str, Any] | None:
        """Load state from memory."""
        state = self._storage.get(checkpoint_id)
        if state:
            logger.debug("State loaded from memory: checkpoint_id=%s", checkpoint_id)
        return state

    def delete_state(self, checkpoint_id: str):
        """Delete the specified checkpoint."""
        if checkpoint_id in self._storage:
            del self._storage[checkpoint_id]
            logger.debug("Deleted checkpoint from memory: checkpoint_id=%s", checkpoint_id)

    async def asave_state(self, checkpoint_id: str, state: dict[str, Any]):
        """Save state to memory."""
        self.save_state(checkpoint_id, state)

    async def aload_state(self, checkpoint_id: str) -> dict[str, Any] | None:
        """Load state from memory."""
        return self.load_state(checkpoint_id)

    async def adelete_state(self, checkpoint_id: str):
        """Delete the specified checkpoint."""
        self.delete_state(checkpoint_id)


class FileCheckPointer(CheckpointStorage):
    """File-based Checkpoint storage."""

    def __init__(self, checkpoint_dir: str):
        """
        Initialize file storage.

        Args:
            checkpoint_dir: Checkpoint storage directory
        """
        self.checkpoint_dir = Path(checkpoint_dir)

        # Ensure directory exists
        self.checkpoint_dir.mkdir(parents=True, exist_ok=True)

    def _get_checkpoint_file(self, checkpoint_id: str) -> Path:
        """Get checkpoint file path."""
        return self.checkpoint_dir / f"{checkpoint_id}.json"

    def save_state(self, checkpoint_id: str, state: dict[str, Any]):
        """Save state to file."""
        checkpoint_file = self._get_checkpoint_file(checkpoint_id)
        try:
            # Write to file
            with open(checkpoint_file, "w", encoding="utf-8") as f:
                json.dump(state, f, indent=2, ensure_ascii=False)

            logger.debug("State saved to file: %s", checkpoint_file)

        except Exception as e:
            logger.error("Failed to save state: %s", str(e))
            raise

    def load_state(self, checkpoint_id: str) -> dict[str, Any] | None:
        """Load state from file."""
        checkpoint_file = self._get_checkpoint_file(checkpoint_id)
        if not checkpoint_file.exists():
            logger.debug("Checkpoint file not found: %s", checkpoint_file)
            return None

        try:
            with open(checkpoint_file, "r", encoding="utf-8") as f:
                state_dict = json.load(f)

            logger.debug("State loaded from file: %s", checkpoint_file)
            return state_dict

        except (OSError, json.JSONDecodeError, ValueError) as e:
            logger.error("Failed to load state: %s", str(e))
            return None

    def delete_state(self, checkpoint_id: str):
        """Delete checkpoint file."""
        checkpoint_file = self._get_checkpoint_file(checkpoint_id)
        if checkpoint_file.exists():
            os.remove(checkpoint_file)
            logger.debug("Deleted checkpoint file: %s", checkpoint_file)

    async def asave_state(self, checkpoint_id: str, state: dict[str, Any]):
        """Save state to file."""
        self.save_state(checkpoint_id, state)

    async def aload_state(self, checkpoint_id: str) -> dict[str, Any] | None:
        """Load state from file."""
        return self.load_state(checkpoint_id)

    async def adelete_state(self, checkpoint_id: str):
        """Delete checkpoint file."""
        self.delete_state(checkpoint_id)


class SqliteCheckPointer(CheckpointStorage):
    """SQLite-based Checkpoint storage."""

    def __init__(self, db_path: str):
        """
        Initialize SQLite storage.

        Args:
            db_path: SQLite database file path
        """
        import sqlite3

        self.db_path = db_path
        self.conn: sqlite3.Connection | None = None

        # Ensure database directory exists
        db_dir = Path(db_path).parent
        db_dir.mkdir(parents=True, exist_ok=True)

        # Initialize database table
        self._init_db()

    def _init_db(self):
        """Initialize database table."""
        import sqlite3

        conn = sqlite3.connect(self.db_path)
        cursor = conn.cursor()

        cursor.execute(
            """
            CREATE TABLE IF NOT EXISTS checkpoints (
                checkpoint_id TEXT PRIMARY KEY,
                state TEXT NOT NULL,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """
        )

        conn.commit()
        conn.close()

    def __enter__(self):
        import sqlite3

        self.conn = sqlite3.connect(self.db_path)
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        if self.conn:
            self.conn.close()
            self.conn = None
        return False

    async def __aenter__(self):
        return self.__enter__()

    async def __aexit__(self, exc_type, exc_value, traceback):
        return self.__exit__(exc_type, exc_value, traceback)

    def save_state(self, checkpoint_id: str, state: dict[str, Any]):
        """Save state to SQLite."""
        import sqlite3

        conn = sqlite3.connect(self.db_path)
        try:
            # Convert to JSON string
            state_json = json.dumps(state, ensure_ascii=False)

            # Insert or update
            cursor = conn.cursor()
            cursor.execute(
                """
                INSERT OR REPLACE INTO checkpoints (checkpoint_id, state, updated_at)
                VALUES (?, ?, CURRENT_TIMESTAMP)
            """,
                (checkpoint_id, state_json),
            )

            conn.commit()
            logger.debug("State saved to SQLite: checkpoint_id=%s", checkpoint_id)

        except Exception as e:
            logger.error("Failed to save state: %s", str(e))
            raise
        finally:
            conn.close()

    def load_state(self, checkpoint_id: str) -> dict[str, Any] | None:
        """Load state from SQLite."""
        import sqlite3

        conn = sqlite3.connect(self.db_path)
        try:
            cursor = conn.cursor()
            cursor.execute(
                "SELECT state FROM checkpoints WHERE checkpoint_id = ?",
                (checkpoint_id,),
            )

            row = cursor.fetchone()
            if not row:
                logger.debug("Checkpoint not found: checkpoint_id=%s", checkpoint_id)
                return None

            state_json = row[0]
            state_dict = json.loads(state_json)

            logger.debug("State loaded from SQLite: checkpoint_id=%s", checkpoint_id)
            return state_dict

        except (json.JSONDecodeError, ValueError) as e:
            logger.error("Failed to load state: %s", str(e))
            return None
        finally:
            conn.close()

    def delete_state(self, checkpoint_id: str):
        """Delete checkpoint."""
        import sqlite3

        conn = sqlite3.connect(self.db_path)
        try:
            cursor = conn.cursor()
            cursor.execute(
                "DELETE FROM checkpoints WHERE checkpoint_id = ?", (checkpoint_id,)
            )
            conn.commit()
            logger.debug("Deleted checkpoint: checkpoint_id=%s", checkpoint_id)
        finally:
            conn.close()

    async def asave_state(self, checkpoint_id: str, state: dict[str, Any]):
        """Save state to SQLite."""
        self.save_state(checkpoint_id, state)

    async def aload_state(self, checkpoint_id: str) -> dict[str, Any] | None:
        """Load state from SQLite."""
        return self.load_state(checkpoint_id)

    async def adelete_state(self, checkpoint_id: str):
        """Delete checkpoint."""
        self.delete_state(checkpoint_id)


================================================
FILE: cortex/agents/checkpoint_agent/react_agent.py
================================================
"""ReActAgent - An Agent specifically designed to execute tasks in ReAct (Reasoning + Acting) mode, capable of calling tools to complete tasks."""

import logging
from typing import AsyncGenerator
from uuid import uuid4

from cortex.model.definition import ChatMessage

from cortex.agents.checkpoint_agent.checkpoint_agent import CheckpointAgent
from cortex.agents.checkpoint_agent.checkpointer import CheckpointStorage
from cortex.agents.react_agent import process_messages
from cortex.agents.types import (
    AgentConfig,
    AgentMessageType,
    AgentResponse,
    AgentRunningStatus,
)
from cortex.model.provider import ModelProvider
from cortex.tools.toolset import ToolSet

logger = logging.getLogger(__name__)


class CheckpointReActAgent(CheckpointAgent):
    """ReActAgent - An Agent specifically designed to execute tasks in ReAct (Reasoning + Acting) mode.

    Features:
    - Can call tools to complete tasks
    - Can call tools multiple times to complete complex tasks
    - Can provide detailed task execution process description
    - Can provide detailed task execution results
    """

    def __init__(
        self,
        storage: CheckpointStorage,
        context_id: str | None = None,
        provider: ModelProvider | None = None,
        config: AgentConfig | None = None,
        toolset: ToolSet | None = None,
    ):
        # If no toolset is provided, create a default math toolset
        if toolset is None:
            # Note: Cannot call async functions directly here, need to initialize externally
            raise ValueError(
                "ReActAgent requires a toolset, please use init_react_tools() to create one"
            )

        if not context_id:
            context_id = uuid4().hex
        super().__init__(
            storage=storage,
            thread_id=context_id,
            provider=provider,
            config=config,
            toolset=toolset,
        )

    async def _step(
        self,
        messages: list[ChatMessage],
        additional_kwargs: dict | None = None,
    ) -> AsyncGenerator[AgentResponse, None]:
        """
        Execute a single step, can yield multiple responses.

        Args:
            messages: Current message history
            additional_kwargs: Additional parameters

        Yields:
            AgentResponse: Response for the current step
        """
        try:
            async for response_message in process_messages(
                self.system_prompt,
                messages,
                self.toolset(),
                self.model_api(),
                getattr(self.model, "infer_kwargs", {}).get("stream", False),
                trace_messages=list(messages) if messages else [],
            ):
                yield response_message
        except Exception as e:
            err_text = str(e) or repr(e)
            logger.error("@%s execution error: %s", self.name, err_text, exc_info=True)
            error_response = AgentResponse(
                message=None,
                status=AgentRunningStatus.ERROR.value,
                error_msg=err_text,
                message_type=AgentMessageType.FINAL.value,
            )
            yield error_response

    async def _tool_call_handler(
        self, messages: list[ChatMessage], tool_results: list[ChatMessage]
    ) -> AsyncGenerator[AgentResponse, None]:
        """
        Handle tool calls.
        """

        for result in tool_results:
            yield AgentResponse(
                message=result,
                status=AgentRunningStatus.RUNNING.value,
                message_type=AgentMessageType.ACCUMULATED.value,
            )


================================================
FILE: cortex/agents/input/input.py
================================================
import asyncio
import logging
from typing import Generic, TypeVar

from pydantic import BaseModel

logger = logging.getLogger(__name__)

T = TypeVar("T", bound=BaseModel)


class InputChannel(Generic[T]):
    queue: asyncio.Queue[T]

    def __init__(self, queue: asyncio.Queue[T]) -> None:
        self.queue = queue

    async def get(self) -> list[T]:
        """Blocks until at least one message is available, then returns all available messages."""
        logger.debug("InputChannel waiting for first data")
        first = await self.queue.get()
        data_list: list[T] = [first]
        count = 1
        while True:
            try:
                data = self.queue.get_nowait()
                data_list.append(data)
                count += 1
            except asyncio.QueueEmpty:
                break
        logger.debug(f"InputChannel returning {count} data(s)")
        return data_list

    async def get_no_wait(self) -> list[T]:
        data_list: list[T] = []
        count = 0
        while True:
            try:
                data = self.queue.get_nowait()
                data_list.append(data)
                count += 1
            except asyncio.QueueEmpty:
                break
        logger.debug(f"InputChannel returning {count} data(s)")
        return data_list


================================================
FILE: cortex/agents/react_agent.py
================================================
"""ReActAgent - Specialized Agent for executing ReAct pattern, capable of calling tools to complete tasks."""

import logging
from typing import AsyncGenerator
from uuid import uuid4

from cortex.agents.base_agent import BaseAgent
from cortex.agents.base_step_agent import BaseStepAgent
from cortex.agents.types import (
    AgentConfig,
    AgentMessageType,
    AgentResponse,
    AgentRunningStatus,
)
from cortex.context import BaseContext
from cortex.context.simple_context import SimpleContext
from cortex.model import ChatMessage, MessageType, ModelAPI
from cortex.model.provider import ModelProvider
from cortex.tools.toolset import ToolSet

logger = logging.getLogger(__name__)


def _check_if_finished(response_message: ChatMessage | None) -> bool:
    """Check if task is finished based on model output."""
    if not response_message:
        return False

    # If there are tool calls, continue execution
    if BaseAgent.has_tool_call(response_message):
        return False

    return True


async def process_messages(
    system_prompt: str | None,
    messages: list[ChatMessage],
    toolset: ToolSet,
    model_api: ModelAPI,
    use_stream: bool,
    trace_messages: list[ChatMessage] | None = None,
) -> AsyncGenerator[AgentResponse, None]:
    if not system_prompt:
        system_prompt = """You are a professional task execution assistant capable of calling tools to complete tasks.
Your task is:
1. Understand the task proposed by the user
2. Use the provided tools to complete the task
3. Call tools multiple times to complete complex tasks
4. Provide detailed explanations of task execution process
5. Provide detailed task execution results

Please ensure:
- For complex tasks, use tools step by step
- Provide clear explanations of task execution steps
- Verify the correctness of task execution results
- When the task is complete, clearly state "Task completed"
"""

    # Prepare message list (only insert system if none present, to avoid duplicate when runner/caller already added one)
    infer_messages = messages.copy()
    if system_prompt and not any(msg.role == "system" for msg in infer_messages):
        infer_messages.insert(0, ChatMessage(role="system", content=system_prompt))

    # Get all tool schemas
    tool_schemas = toolset.get_all_schemas()

    # Convert tool schemas to model-usable format
    tools_for_model = []
    for tool_name, schema in tool_schemas.items():
        tools_for_model.append(
            {
                "type": "function",
                "function": {
                    "name": tool_name,
                    "description": schema.description,
                    "parameters": schema.parameters,
                },
            }
        )

    # Call model
    trace_request = None
    if trace_messages is not None:
        trace_infer_messages = trace_messages.copy()
        if system_prompt and not any(msg.role == "system" for msg in trace_infer_messages):
            trace_infer_messages.insert(
                0, ChatMessage(role="system", content=system_prompt)
            )
        trace_request = {
            "messages": trace_infer_messages,
            "sent_messages": infer_messages,
            "tools": tools_for_model if tools_for_model else None,
        }

    if use_stream:
        # Streaming output mode
        delta_count = 0
        response_message = None

        async for model_msg in model_api.chat_completion_stream(
            messages=infer_messages,
            tools=tools_for_model if tools_for_model else None,
            trace_request=trace_request,
        ):
            delta_count += 1
            event = model_msg.message
            response_message = (
                event  # Save last message (model yields accumulated_message at end)
            )

            if model_msg.message_type == MessageType.DELTA:
                # This is a delta event, yield directly
                delta_response = AgentResponse(
                    message=event,
                    status=AgentRunningStatus.RUNNING.value,
                    message_type=AgentMessageType.STREAM.value,
                )
                yield delta_response

    else:
        # Non-streaming output mode
        model_msg = await model_api.chat_completion(
            messages=infer_messages,
            tools=tools_for_model if tools_for_model else None,
            trace_request=trace_request,
        )
        response_message = model_msg.message

    is_finished = _check_if_finished(response_message)
    response_status = (
        AgentRunningStatus.FINISHED.value
        if is_finished
        else AgentRunningStatus.RUNNING.value
    )
    message_type = (
        AgentMessageType.FINAL.value
        if is_finished
        else AgentMessageType.ACCUMULATED.value
    )

    model_response = AgentResponse(
        message=response_message,
        status=response_status,
        message_type=message_type,
    )
    yield model_response


class ReActAgent(BaseStepAgent):
    """ReActAgent - Specialized Agent for executing ReAct pattern, capable of calling tools to complete tasks.

    Features:
    - Can call tools to complete tasks
    - Can call tools multiple times to complete complex tasks
    - Can provide detailed explanations of task execution process
    - Can provide detailed task execution results
    """

    def __init__(
        self,
        context: BaseContext | None = None,
        provider: ModelProvider | None = None,
        config: AgentConfig | None = None,
        toolset: ToolSet | None = None,
    ):
        if context is None:
            context = SimpleContext(uuid4().hex)
        # If toolset is not provided, create default math toolset
        if toolset is None:
            # Note: Cannot call async functions directly here, need to initialize externally
            raise ValueError(
                "ReActAgent requires a toolset, please use init_react_tools() to create one"
            )

        super().__init__(
            context=context, provider=provider, config=config, toolset=toolset
        )

    async def _step(
        self,
        messages: list[ChatMessage],
        additional_kwargs: dict | None = None,
    ) -> AsyncGenerator[AgentResponse, None]:
        """
        Execute single step operation, can yield multiple responses.

        Args:
            messages: Current message history
            additional_kwargs: Additional parameters

        Yields:
            AgentResponse: Response for current step
        """

        trace_messages: list[ChatMessage] | None = None
        try:
            trace_messages = list(self.context.get_all())
        except Exception:  # noqa: BLE001
            trace_messages = None

        if trace_messages is not None and self._force_final_answer_enabled and self._force_prompt_inserted:
            prompt_message = self._make_force_final_answer_message()
            if not trace_messages or not (
                getattr(trace_messages[-1], "role", None) == prompt_message.role
                and getattr(trace_messages[-1], "content", None) == prompt_message.content
            ):
                trace_messages.append(prompt_message)

        async for response_message in process_messages(
            self.system_prompt,
            messages,
            self.toolset(),
            self.model_api(),
            getattr(self.model, "infer_kwargs", {}).get("stream", False),
            trace_messages=trace_messages,
        ):
            try:
                yield response_message

                # Only check and execute tool calls for non-STREAM types (i.e., ACCUMULATED or FINAL)
                # In streaming output, STREAM type delta messages don't contain complete tool_call
                if response_message.message_type == AgentMessageType.STREAM.value:
                    continue

                # Check for tool calls and execute
                tool_result_messages = []
                message = response_message.message
                if self.has_tool_call(message):
                    tool_result_messages = await self.run_tool_call(message)
                    if tool_result_messages:
                        logger.info(
                            "@%s Detected %s tool call results",
                            self.name,
                            len(tool_result_messages),
                        )

                    # Yield tool result responses
                    for tool_result_msg in tool_result_messages:
                        tool_response = AgentResponse(
                            message=tool_result_msg,
                            status=AgentRunningStatus.RUNNING.value,
                            message_type=AgentMessageType.FINAL.value,
                        )
                        yield tool_response

            except Exception as e:
                err_text = str(e) or repr(e)
                logger.error("@%s Execution error: %s", self.name, err_text, exc_info=True)
                error_response = AgentResponse(
                    message=None,
                    status=AgentRunningStatus.ERROR.value,
                    error_msg=err_text,
                    message_type=AgentMessageType.FINAL.value,
                )
                yield error_response


================================================
FILE: cortex/agents/types.py
================================================
from enum import Enum

from cortex.model.definition import ChatMessage, ModelParams
from cortex.model.utils import merge_delta_message
from pydantic import BaseModel, Field

from cortex.tools.base import ToolSchema


class RunnerType(str, Enum):
    """Agent runner type enum."""

    LOCAL = "local"
    REMOTE = "remote"


class AgentRunningStatus(str, Enum):
    """Agent running status enum."""

    FINISHED = "finished"
    STOPPED = "stopped"
    ERROR = "error"
    RUNNING = "running"


class AgentMessageType(str, Enum):
    """Agent message type enum."""

    STREAM = "stream"  # Streaming output
    ACCUMULATED = "accumulated"  # Accumulated output
    FINAL = "final"  # Final output


class AgentResponseType(str, Enum):
    """Agent response type enum."""

    RESPONSE = "response"
    TOOL_CALL = "tool_call"
    TOOL_RESULT = "tool_result"


class AgentConfig(BaseModel):
    """Declarative configuration for Agent."""

    model: ModelParams
    name: str = Field(default="")
    agent_type: str | None = None
    system_prompt: str | None = None
    description: str | None = None
    tools: list[ToolSchema | str] = Field(default_factory=list)
    max_steps: int = 10
    extra_config: dict | None = None
    runner_type: RunnerType = RunnerType.LOCAL
    endpoint: str | None = None
    unfinished_mode: bool = False
    use_share_context: bool = False


class AgentResponse(BaseModel):
    """Agent response model."""

    agent_name: str | None = None
    message: ChatMessage | None = None
    message_type: AgentMessageType = (
        AgentMessageType.FINAL
    )  # delta: streaming output, accumulated: accumulated output, final: final output
    status: AgentRunningStatus = AgentRunningStatus.RUNNING
    error_msg: str | None = None
    metadata: dict[str, object] | None = None

    def get_type(self) -> AgentResponseType:
        """Get response type."""
        if self.message is None:
            return AgentResponseType.RESPONSE
        if self.message.tool_call_id is not None:
            return AgentResponseType.TOOL_CALL
        if self.message.tool_calls is not None:
            return AgentResponseType.TOOL_CALL
        return AgentResponseType.RESPONSE

    def __add__(self, other: "AgentResponse") -> "AgentResponse":
        if not isinstance(other, AgentResponse):
            return NotImplemented

        # Merge delta_message dictionaries
        merged_delta_dict = merge_delta_message(
            self.message.model_dump() if self.message else None,
            other.message.model_dump() if other.message else None,
        )
        merged_delta = ChatMessage(**merged_delta_dict)

        # Create new field dictionary, default to self's fields
        new_fields = self.model_dump()
        new_fields["message"] = merged_delta

        # Iterate other fields, override with other's value if present
        for field, _ in self.model_dump().items():
            if field == "message":
                continue  # Already processed
            other_value = getattr(other, field)
            if other_value:
                new_fields[field] = other_value

        return AgentResponse(**new_fields)


================================================
FILE: cortex/context/__init__.py
================================================
"""Context module for managing conversation and session context."""

from cortex.context.base_context import BaseContext
from cortex.context.file_context import FileContext
from cortex.context.simple_context import SimpleContext

__all__ = ["BaseContext", "make_simple_context", "make_file_context"]


def make_simple_context(session_id: str) -> BaseContext:
    return SimpleContext(session_id)


def make_file_context(path: str, session_id: str) -> BaseContext:
    return FileContext(session_id, path)


================================================
FILE: cortex/context/base_context.py
================================================
"""Base context management class."""

from abc import ABC, abstractmethod
from typing import List

from cortex.model.definition import ChatMessage


class BaseContext(ABC):
    """Base context management class, providing basic interface for session message management."""

    def __init__(self, session_id: str):
        """
        Initialize base context.

        Args:
            session_id: Session ID
        """
        self.session_id = session_id

    @abstractmethod
    def add(self, messages: list[ChatMessage]) -> None:
        """
        Add chat messages to context.

        Args:
            messages: List of chat messages to add
        """
        ...

    @abstractmethod
    def get_all(self) -> List[ChatMessage]:
        """
        Get all chat messages.

        Returns:
            List[ChatMessage]: List of all chat messages
        """
        ...


================================================
FILE: cortex/context/file_context.py
================================================
"""File-based context management class."""

import asyncio
import json
import os
from pathlib import Path
from typing import List

from cortex.model.definition import ChatMessage

from cortex.context.base_context import BaseContext


class FileContext(BaseContext):
    """File-based context management class, each session_id corresponds to a file."""

    def __init__(
        self,
        session_id: str,
        storage_dir: str = "contexts",
        batch_size: int = 5,
        delay_seconds: float = 2.0,
    ):
        """
        Initialize file context.

        Args:
            session_id: Session ID
            storage_dir: Storage directory, defaults to "contexts"
            batch_size: Batch write size, write immediately when this count is reached
            delay_seconds: Delay write time (seconds)
        """
        super().__init__(session_id)
        self.storage_dir = Path(storage_dir)
        self.storage_dir.mkdir(exist_ok=True)
        self.file_path = self.storage_dir / f"{session_id}.jsonl"
        self._messages: List[ChatMessage] = []
        self._pending_messages: List[ChatMessage] = []
        self._batch_size = batch_size
        self._delay_seconds = delay_seconds
        self._write_task = None
        self._load_messages()

    def __del__(self):
        """Automatically call flush when object is garbage collected"""
        try:
            self.flush()
        except Exception:
            # Ignore exceptions in destructor to avoid affecting garbage collection
            pass

    def _load_messages(self) -> None:
        """Load messages from file"""
        if self.file_path.exists():
            try:
                with open(self.file_path, "r", encoding="utf-8") as f:
                    self._messages = []
                    for line in f:
                        line = line.strip()
                        if line:
                            msg_data = json.loads(line)
                            self._messages.append(ChatMessage(**msg_data))
            except (json.JSONDecodeError, KeyError, TypeError):
                # If file is corrupted or format is incorrect, start fresh
                self._messages = []

    def _save_messages(self) -> None:
        """Save messages to file"""
        # Rewrite entire file
        all_messages = self._messages + self._pending_messages
        with open(self.file_path, "w", encoding="utf-8") as f:
            for msg in all_messages:
                json_line = json.dumps(msg.model_dump(), ensure_ascii=False)
                f.write(json_line + "\n")

        # Move pending messages to main message list
        self._messages.extend(self._pending_messages)
        self._pending_messages = []

    async def _delayed_write(self) -> None:
        """Delayed write task"""
        await asyncio.sleep(self._delay_seconds)
        if self._pending_messages:
            self._save_messages()
        self._write_task = None

    def _schedule_write(self) -> None:
        """Schedule write task"""
        # If batch size is reached, write immediately
        if len(self._pending_messages) >= self._batch_size:
            if self._write_task:
                self._write_task.cancel()
                self._write_task = None
            self._save_messages()
        else:
            # If no write task in progress, create a delayed write task
            if not self._write_task:
                self._write_task = asyncio.create_task(self._delayed_write())

    def add(self, messages: list[ChatMessage]) -> None:
        """Add chat messages to context

        Args:
            messages: List of chat messages to add
        """
        self._pending_messages.extend(messages)
        self._schedule_write()

    def get_all(self) -> List[ChatMessage]:
        """Get all chat messages

        Returns:
            List[ChatMessage]: List of all chat messages
        """
        return (self._messages + self._pending_messages).copy()

    def clear(self) -> None:
        """Clear context messages"""
        if self._write_task:
            self._write_task.cancel()
            self._write_task = None
        self._messages = []
        self._pending_messages = []
        if self.file_path.exists():
            os.remove(self.file_path)

    def flush(self) -> None:
        """Force write all pending messages"""
        if self._write_task:
            self._write_task.cancel()
            self._write_task = None
        if self._pending_messages:
            self._save_messages()


================================================
FILE: cortex/context/simple_context.py
================================================
from typing import List

from cortex.model.definition import ChatMessage

from cortex.context import BaseContext

simple_contexts: dict[str, list[ChatMessage]] = {}


class SimpleContext(BaseContext):
    """Simple context management class for managing session messages."""

    def __init__(self, session_id: str):
        super().__init__(session_id)

    def add(self, msg: list[ChatMessage]) -> None:
        """Add chat message list to context."""
        if self.session_id in simple_contexts:
            simple_contexts[self.session_id].extend(msg)
        else:
            simple_contexts[self.session_id] = msg

    def get_all(self) -> List[ChatMessage]:
        """Get all chat messages."""
        return simple_contexts.get(self.session_id, [])


================================================
FILE: cortex/env.py
================================================
from __future__ import annotations

import os
from pathlib import Path


def _repo_root() -> Path:
    return Path(__file__).resolve().parent.parent


def _parse_env_line(line: str) -> tuple[str, str] | None:
    stripped = line.strip()
    if not stripped or stripped.startswith("#"):
        return None
    if stripped.startswith("export "):
        stripped = stripped[7:].lstrip()
    if "=" not in stripped:
        return None
    key, value = stripped.split("=", 1)
    key = key.strip()
    if not key:
        return None
    value = value.strip()
    if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
        value = value[1:-1]
    return key, value


def _load_env_fallback(path: Path) -> None:
    try:
        content = path.read_text(encoding="utf-8")
    except Exception:
        return
    for line in content.splitlines():
        parsed = _parse_env_line(line)
        if not parsed:
            continue
        key, value = parsed
        os.environ.setdefault(key, value)


def load_env() -> None:
    env_path = _repo_root() / ".env"
    if not env_path.exists():
        return
    try:
        from dotenv import load_dotenv  # type: ignore
    except Exception:
        _load_env_fallback(env_path)
        return
    load_dotenv(env_path, override=False)


================================================
FILE: cortex/examples/agents/ask_input_agent.py
================================================
"""AskInputAgent - Fixed-flow Agent for asking user input and repeating what the user says"""

import json
import logging
import uuid
from typing import AsyncGenerator
from uuid import uuid4

from cortex.model.definition import ChatMessage, ChatToolCall, ContentBlockType, Function

from cortex.agents.base_agent import BaseAgent
from cortex.agents.input.input import InputChannel
from cortex.agents.types import (
    AgentConfig,
    AgentMessageType,
    AgentResponse,
    AgentRunningStatus,
)
from cortex.context import BaseContext, make_simple_context
from cortex.model import ModelParams
from cortex.tools.client_tool import ClientTool
from cortex.tools.toolset import ToolSet
from cortex.tools.types import ToolType

logger = logging.getLogger(__name__)


async def init_ask_input_tools() -> ToolSet:
    """Initialize ask_input toolset"""
    toolset = ToolSet()

    # Register ask_input tool (ClientTool)
    ask_input_tool = ClientTool(
        name="ask_input",
        description="Ask user for input. Used for scenarios requiring user interaction such as obtaining user feedback, confirmation, modification suggestions, etc. Parameters: prompt (required) - prompt message to display to the user; context (optional) - context information to help users understand the current situation.",
        tool_type=ToolType.ASK_INPUT,
        channel=toolset.channel,
        timeout=300.0,  # User input may take a long time
        client_params={
            "properties": {
                "prompt": {
                    "type": "string",
                    "description": "Prompt message to display to the user, explaining what the user needs to do (confirm, modify, provide information, etc.)",
                },
                "context": {
                    "type": "string",
                    "description": "Context information to help users understand the current situation, such as current plan content, items that need confirmation, etc.",
                },
            },
            "required": ["prompt"],
        },
    )
    toolset.register(ask_input_tool)
    logger.info("Registered ask_input tool")

    return toolset


class AskInputAgent(BaseAgent):
    """AskInputAgent - Fixed-flow Agent for asking user input and repeating what the user says

    Fixed flow:
    1. Send a function call with tool_name as ask_input
    2. After receiving ask_input result, send a message repeating what the user said
    """

    def __init__(
        self, context: BaseContext, config: AgentConfig, toolset: ToolSet | None = None
    ):
        super().__init__(config=config, toolset=toolset)
        self.context = context

    async def _run(
        self,
        messages: list[ChatMessage] | InputChannel[ChatMessage],
        additional_kwargs: dict | None = None,
    ) -> AsyncGenerator[AgentResponse, None]:
        """
        Run agent, execute fixed flow

        Args:
            messages: Input message list or input channel
            additional_kwargs: Additional parameters

        Yields:
            AgentResponse: Agent response object
        """
        try:
            # Step 1: Create a ChatMessage containing tool_calls, call ask_input tool
            tool_call_id = f"call_{uuid.uuid4().hex[:8]}"

            # Create tool call arguments
            tool_args = json.dumps({"prompt": "Please enter some content"})

            # Create ChatToolCall
            tool_call = ChatToolCall(
                id=tool_call_id,
                function=Function(
                    name="ask_input",
                    arguments=tool_args,
                ),
            )

            # Create ChatMessage containing tool_calls
            tool_call_message = ChatMessage(
                role="assistant",
                tool_calls=[tool_call],
            )

            # Yield tool call response
            tool_call_response = AgentResponse(
                message=tool_call_message,
                status=AgentRunningStatus.RUNNING.value,
                message_type=AgentMessageType.FINAL.value,
            )
            yield tool_call_response

            # Execute tool call
            tool_result_messages = await self.run_tool_call(tool_call_message)

            if not tool_result_messages:
                error_response = AgentResponse(
                    message=None,
                    status=AgentRunningStatus.ERROR.value,
                    error_msg="ask_input tool call failed, no result returned",
                    message_type=AgentMessageType.FINAL.value,
                )
                yield error_response
                return

            # Yield tool result response
            tool_result_message = tool_result_messages[0]
            tool_result_response = AgentResponse(
                message=tool_result_message,
                status=AgentRunningStatus.RUNNING.value,
                message_type=AgentMessageType.FINAL.value,
            )
            yield tool_result_response

            # Step 2: Get user input from tool result and repeat what the user said
            user_input = None
            if tool_result_message.content:
                # Extract user input
                if isinstance(tool_result_message.content, list):
                    for block in tool_result_message.content:
                        if isinstance(block, dict):
                            text_content = block.get(
                                ContentBlockType.TEXT.value
                            ) or block.get("text")
                            if text_content:
                                user_input = str(text_content)
                                break
                        elif isinstance(block, str):
                            user_input = block
                            break
                elif isinstance(tool_result_message.content, str):
                    user_input = tool_result_message.content

            if user_input is None:
                user_input = "Failed to get user input"

            # Create message repeating what the user said
            repeat_message = ChatMessage(
                role="assistant",
                content=[
                    {
                        "type": ContentBlockType.TEXT.value,
                        ContentBlockType.TEXT.value: f"You said: {user_input}",
                    }
                ],
            )

            # Yield final response
            final_response = AgentResponse(
                message=repeat_message,
                status=AgentRunningStatus.FINISHED.value,
                message_type=AgentMessageType.FINAL.value,
            )
            yield final_response

        except Exception as e:
            logger.error(f"@{self.name} execution error: {e}", exc_info=True)
            error_response = AgentResponse(
                message=None,
                status=AgentRunningStatus.ERROR.value,
                error_msg=str(e),
                message_type=AgentMessageType.FINAL.value,
            )
            yield error_response


async def make_ask_input_agent(
    config: AgentConfig, context_id: str | None = None
) -> BaseAgent:
    """Create AskInputAgent"""
    toolset = await init_ask_input_tools()
    context = make_simple_context(context_id)
    if context_id is None:
        context_id = uuid4().hex
    return AskInputAgent(context=context, config=config, toolset=toolset)


def get_ask_input_agent_config() -> AgentConfig:
    """Get AskInputAgent configuration"""
    return AgentConfig(
        name="AskInputAgent",
        description="Fixed-flow Agent for asking user input and repeating what the user says. Does not require model calls.",
        system_prompt=None,  # No system prompt needed
        model=ModelParams(
            name="gpt-4o-mini",  # Although model calls are not needed, configuration requires it
            infer_kwargs={"max_tokens": 100, "temperature": 0.7, "stream": False},
        ),
    )


================================================
FILE: cortex/examples/agents/deep_reasearch_agent.py
================================================
"""DeepResearchAgent - Deep research Agent"""

from cortex.agents.base_agent import BaseAgent
from cortex.agents.react_agent import ReActAgent
from cortex.agents.types import AgentConfig
from cortex.model import ModelParams
from cortex.tools.agent_tool import AgentTool
from cortex.tools.toolset import ToolSet


async def init_deep_research_tools() -> ToolSet:
    """Initialize deep research tools"""
    # Register web_search tool
    toolset = ToolSet()
    await toolset.register_from_mcp_server(
        mcp_server="http://xxx/mcp",
        tool_names=["web_search"],
    )

    # plan agent tool
    plan_agent_tool = AgentTool(
        name="PlanAgent",
        description="PlanAgent - Planning Agent responsible for creating plans",
        channel=toolset.channel,
        timeout=3000.0,
    )
    toolset.register(plan_agent_tool)

    return toolset


def get_deep_research_agent_config() -> AgentConfig:
    """Get deep research Agent configuration"""
    return AgentConfig(
        name="DeepResearchAgent",
        description="DeepResearchAgent - Deep research Agent responsible for deep research tasks",
        system_prompt="""You are a powerful "Deep Search Agent", an intelligent system with capabilities for initial thinking, reflection, and adaptation. When facing a complex problem, you don't simply execute searches, but rather think deeply, plan, and flexibly utilize tools (such as web search, webpage access, code execution) to find answers like a smart assistant.
The key lies in "depth" and "intelligence", which means the Agent needs to have the following characteristics:
1. **Tool Coordination**: Able to carefully analyze problems, reference plans already made in history, and strategically use tools to collect, process, and present information.
2. **Data Extraction and Visualization**: Able to extract data relevant to the problem from massive amounts of information, and use the visualization (visualize_data) tool to transform data into intuitive charts.
3. **Reflection and Adaptation**: This is the most important capability! During the search process, if problems are encountered (such as unsatisfactory search results, insufficient information, or uncertainty), the Agent won't give up easily, but will proactively reflect and adjust strategies. For example:
 - Change search keywords.
 - View more search results.
 - Determine whether current information is sufficient to answer the question; if not, continue searching for missing information.
 - Evaluate the reliability of information sources.
 - Use different tools or information sources for cross-validation.
 - Determine whether there is critical data that needs to be displayed through charts; if so, use visualization tools to present it.
 - Check whether the collected information fully meets all requirements of the original question.


You must always follow these rules to complete tasks:
1. Always provide tool calls, otherwise it will fail
2. Always use correct tool parameters. Don't use variable names in action parameters, use specific values instead
3. Only call tools when needed: if information is not needed, don't call search tools, try to solve the problem yourself
4. Never repeat a tool call with exactly the same parameters that has already been used
5. Only use the visualize_data tool for visualization, while the execute_python_code tool can only be used for complex data calculations or file processing
6. If information involves key numerical values, data presentation, data comparison, process chains, multiple stages, entity relationships, timelines, etc., you must use the visualize_data tool to generate charts
7. For data that has already been visualized, don't call the visualization tool repeatedly
8. Do not use the execute_python_code tool to output large amounts of text, do not use the execute_python_code tool to output reports
9. Never express gratitude for any tool call results (such as search results)
10. Multi-language support: You support responding in Chinese, English, Japanese, Korean, Traditional Chinese, Spanish, and Portuguese, automatically identifying the "user's input language" and matching the output.
11. Search additional requirements: When the problem you need to search is related to travel/public opinion, you need to generate queries in the language corresponding to the travel/public opinion location for searching, and also generate an identical query in the user's language for searching.


Start now! If you complete the task correctly, you will receive a $1,000,000 reward.
        """,
        model=ModelParams(
            name="gpt-5.1",
            infer_kwargs={"max_tokens": 2000, "temperature": 0.7, "stream": False},
        ),
        max_steps=10,
    )


async def make_deep_research_agent(session_id: str, config: AgentConfig) -> BaseAgent:
    """Create deep research Agent"""
    toolset = await init_deep_research_tools()
    return ReActAgent(config=config, toolset=toolset)


================================================
FILE: cortex/examples/agents/main_agent.py
================================================
"""MainAgent - Main coordination Agent."""

import logging
from uuid import uuid4

from cortex.agents.base_agent import BaseAgent
from cortex.agents.react_agent import ReActAgent
from cortex.agents.types import AgentConfig
from cortex.context import make_simple_context
from cortex.examples.agents.math_agent import get_math_agent_config, make_math_agent
from cortex.examples.agents.search_agent import (
    get_search_agent_config,
    make_search_agent,
)
from cortex.model import ModelParams
from cortex.tools.agent_tool import AgentTool
from cortex.tools.toolset import ToolSet

logger = logging.getLogger(__name__)


async def init_main_tools() -> ToolSet:
    """Initialize main coordination tools."""
    toolset = ToolSet()
    search_agent = await make_search_agent(config=get_search_agent_config())
    math_agent = await make_math_agent(config=get_math_agent_config())

    search_tool_params = search_agent.as_tool()
    math_tool_params = math_agent.as_tool()
    toolset.register(
        AgentTool(
            name=search_tool_params["name"],
            description=search_tool_params["description"],
            timeout=search_tool_params["timeout"]
            if "timeout" in search_tool_params
            else 300,
            channel=toolset.channel,
        )
    )
    toolset.register(
        AgentTool(
            name=math_tool_params["name"],
            description=math_tool_params["description"],
            timeout=math_tool_params["timeout"]
            if "timeout" in math_tool_params
            else 300,
            channel=toolset.channel,
        )
    )
    return toolset


async def make_main_agent(
    config: AgentConfig, context_id: str | None = None
) -> BaseAgent:
    """Create MainAgent."""
    toolset = await init_main_tools()
    if context_id is None:
        context_id = uuid4().hex
    context = make_simple_context(context_id)
    return ReActAgent(context=context, config=config, toolset=toolset)


def get_main_agent_config() -> AgentConfig:
    """Get MainAgent configuration."""
    return AgentConfig(
        name="MainAgent",
        description="Main coordination Agent responsible for coordinating and calling other specialized Agents to complete tasks. Can select appropriate Agents based on task requirements (e.g., MathAgent for mathematical calculations, SearchAgent for information search) and coordinate multiple Agents to complete complex tasks.",
        system_prompt="You are a main coordination Agent responsible for coordinating and calling other specialized Agents to complete tasks.",
        model=ModelParams(
            name="gpt-4o-mini",
            infer_kwargs={"max_tokens": 2000, "temperature": 0.7, "stream": False},
        ),
    )


================================================
FILE: cortex/examples/agents/math_agent.py
================================================
"""MathAgent - An Agent that can solve mathematical problems."""

import logging
import math
from uuid import uuid4

from cortex.agents.base_agent import BaseAgent
from cortex.agents.react_agent import ReActAgent
from cortex.agents.types import AgentConfig
from cortex.context import make_simple_context
from cortex.model import ModelParams
from cortex.tools.function_tool import FunctionTool
from cortex.tools.toolset import ToolSet

logger = logging.getLogger(__name__)


# Basic math operation tool functions
def add(a: float, b: float) -> float:
    """Add two numbers.

    Args:
        a: First number
        b: Second number

    Returns:
        Sum of the two numbers
    """
    return a + b


def subtract(a: float, b: float) -> float:
    """Subtract two numbers.

    Args:
        a: Minuend
        b: Subtrahend

    Returns:
        Difference of the two numbers
    """
    return a - b


def multiply(a: float, b: float) -> float:
    """Multiply two numbers.

    Args:
        a: First number
        b: Second number

    Returns:
        Product of the two numbers
    """
    return a * b


def divide(a: float, b: float) -> float:
    """Divide two numbers.

    Args:
        a: Dividend
        b: Divisor (cannot be 0)

    Returns:
        Quotient of the two numbers

    Raises:
        ValueError: When divisor is 0
    """
    if b == 0:
        raise ValueError("Divisor cannot be 0")
    return a / b


def power(base: float, exponent: float) -> float:
    """Calculate power operation.

    Args:
        base: Base number
        exponent: Exponent

    Returns:
        Base raised to the power of exponent
    """
    return base**exponent


def sqrt(number: float) -> float:
    """Calculate square root.

    Args:
        number: Number to calculate square root (must be >= 0)

    Returns:
        Square root of the number

    Raises:
        ValueError: When number is less than 0
    """
    if number < 0:
        raise ValueError("Cannot calculate square root of negative number")
    return math.sqrt(number)


def calculate_expression(expression: str) -> float:
    """Calculate a mathematical expression (using eval, for simple expressions only).

    Args:
        expression: Mathematical expression string, e.g., "2 + 3 * 4"

    Returns:
        Calculation result

    Warning:
        This function uses eval, only for simple mathematical expressions, do not use for untrusted input
    """
    # Only allow numbers, operators, and parentheses
    allowed_chars = set("0123456789+-*/.() ")
    if not all(c in allowed_chars for c in expression):
        raise ValueError("Expression contains disallowed characters")
    try:
        return float(eval(expression))
    except Exception as e:
        raise ValueError(f"Expression calculation error: {str(e)}")


async def init_math_tools() -> ToolSet:
    """Initialize math toolset."""
    toolset = ToolSet()

    # Register basic math operation tools
    math_functions = [
        ("add", add, "Addition: add two numbers"),
        ("subtract", subtract, "Subtraction: subtract two numbers"),
        ("multiply", multiply, "Multiplication: multiply two numbers"),
        ("divide", divide, "Division: divide two numbers"),
        ("power", power, "Power: calculate a number raised to a power"),
        ("sqrt", sqrt, "Square root: calculate the square root of a number"),
        (
            "calculate_expression",
            calculate_expression,
            "Calculate expression: evaluate a simple mathematical expression string",
        ),
    ]

    for tool_name, tool_func, tool_desc in math_functions:
        function_tool = FunctionTool(
            name=tool_name,
            func=tool_func,
            description=tool_desc,
        )
        toolset.register(function_tool)
        logger.info(f"Registered math tool: {tool_name}")

    return toolset


async def make_math_agent(
    config: AgentConfig, context_id: str | None = None
) -> BaseAgent:
    """Create MathAgent."""
    toolset = await init_math_tools()
    if context_id is None:
        context_id = uuid4().hex
    context = make_simple_context(context_id)
    return ReActAgent(context=context, config=config, toolset=toolset)


def get_math_agent_config() -> AgentConfig:
    """Get MathAgent configuration."""
    return AgentConfig(
        name="MathAgent",
        description="An Agent specialized for mathematical calculations. Supports basic math operations (addition, subtraction, multiplication, division, power, square root, etc.), can handle complex mathematical expressions, supports multi-step calculations, and provides detailed calculation process explanations. Suitable for arithmetic, algebra, geometry calculations, and mathematical expression solving.",
        system_prompt="You are a professional mathematical calculation assistant.",
        model=ModelParams(
            name="gpt-4o-mini",
            infer_kwargs={"max_tokens": 2000, "temperature": 0.7, "stream": False},
        ),
    )


================================================
FILE: cortex/examples/agents/plan_agent.py
================================================
"""PlanAgent - Creates plans based on task objectives, and finalizes plans after user confirmation or modification"""

import logging

from cortex.agents.base_agent import BaseAgent
from cortex.agents.react_agent import ReActAgent
from cortex.agents.types import AgentConfig
from cortex.context import make_simple_context
from cortex.model import ModelPara
Download .txt
gitextract_gy_2y08y/

├── .env-example
├── .gitignore
├── LICENSE
├── README.md
├── agentkit/
│   └── trace/
│       ├── __init__.py
│       ├── builder.py
│       ├── context.py
│       ├── default.py
│       ├── local_tracer.py
│       ├── remote_tracer.py
│       ├── span.py
│       ├── tracer.py
│       └── types.py
├── config.yaml
├── cortex/
│   ├── __init__.py
│   ├── agents/
│   │   ├── __init__.py
│   │   ├── agent_factory.py
│   │   ├── base_agent.py
│   │   ├── base_step_agent.py
│   │   ├── checkpoint_agent/
│   │   │   ├── checkpoint_agent.py
│   │   │   ├── checkpointer.py
│   │   │   └── react_agent.py
│   │   ├── input/
│   │   │   └── input.py
│   │   ├── react_agent.py
│   │   └── types.py
│   ├── context/
│   │   ├── __init__.py
│   │   ├── base_context.py
│   │   ├── file_context.py
│   │   └── simple_context.py
│   ├── env.py
│   ├── examples/
│   │   ├── agents/
│   │   │   ├── ask_input_agent.py
│   │   │   ├── deep_reasearch_agent.py
│   │   │   ├── main_agent.py
│   │   │   ├── math_agent.py
│   │   │   ├── plan_agent.py
│   │   │   └── search_agent.py
│   │   ├── demo_agent_cli.py
│   │   ├── demo_agent_with_orchestrator.py
│   │   ├── demo_agent_with_tool.py
│   │   ├── demo_checkpoint.py
│   │   ├── demo_toolset_channel.py
│   │   └── server.py
│   ├── model/
│   │   ├── __init__.py
│   │   ├── definition.py
│   │   ├── provider.py
│   │   ├── stepfun_chat.py
│   │   ├── stepfun_provider.py
│   │   └── utils.py
│   ├── orchestrator/
│   │   ├── __init__.py
│   │   ├── local_runner.py
│   │   ├── orchestrator.py
│   │   ├── remote_runner.py
│   │   ├── runner.py
│   │   └── types.py
│   ├── runtime_config.py
│   ├── server/
│   │   ├── channel/
│   │   │   ├── channel.py
│   │   │   ├── error.py
│   │   │   ├── memory_channel.py
│   │   │   └── ws_channel.py
│   │   ├── http_server.py
│   │   └── log/
│   │       ├── log.py
│   │       └── trace.py
│   ├── tools/
│   │   ├── __init__.py
│   │   ├── agent_tool.py
│   │   ├── base.py
│   │   ├── channel.py
│   │   ├── client_tool.py
│   │   ├── function_tool.py
│   │   ├── mcp.py
│   │   ├── mcp_tool.py
│   │   ├── session_tool.py
│   │   ├── toolset.py
│   │   ├── types.py
│   │   ├── ublock_agent_tool.py
│   │   └── unblock_client_tool.py
│   ├── tui/
│   │   ├── __init__.py
│   │   └── tui.py
│   └── utils/
│       ├── __init__.py
│       ├── generator_merger.py
│       └── generator_merger_examples.py
├── cortex-ui/
│   ├── .gitignore
│   ├── .gitlab-ci.yml
│   ├── index.html
│   ├── package.json
│   ├── src/
│   │   ├── App.tsx
│   │   ├── components/
│   │   │   ├── EndpointConfig.tsx
│   │   │   ├── ErrorBoundary.tsx
│   │   │   ├── FilePanel.tsx
│   │   │   ├── FinalAnswer.tsx
│   │   │   ├── SearchResultsPanel.tsx
│   │   │   ├── ShellPanel.tsx
│   │   │   ├── TodoPanel.tsx
│   │   │   └── WebPagePanel.tsx
│   │   ├── index.css
│   │   ├── main.tsx
│   │   ├── pages/
│   │   │   ├── AgentList.tsx
│   │   │   └── ChatPage.tsx
│   │   ├── services/
│   │   │   └── api.ts
│   │   ├── types/
│   │   │   ├── citation.ts
│   │   │   └── index.ts
│   │   ├── utils/
│   │   │   └── citationParser.ts
│   │   └── vite-env.d.ts
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   └── vite.config.ts
├── demo/
│   ├── __init__.py
│   ├── dr_agent/
│   │   ├── __init__.py
│   │   └── dr_agent.py
│   ├── server.py
│   └── tools/
│       ├── __init__.py
│       ├── batch_open.py
│       ├── batch_search.py
│       ├── batch_web_surfer.py
│       ├── file.py
│       ├── open.py
│       ├── search.py
│       ├── shell.py
│       ├── text_truncator.py
│       ├── todo.py
│       └── utils.py
├── pyproject.toml
└── scripts/
    ├── configs/
    │   ├── prompt.py
    │   ├── runner_example.yaml
    │   └── tasks.example.json
    └── runner.py
Download .txt
SYMBOL INDEX (741 symbols across 95 files)

FILE: agentkit/trace/builder.py
  class SpanBuilder (line 22) | class SpanBuilder:
    method __init__ (line 36) | def __init__(self, trace_id: Optional[str] = None, app_name: Optional[...
    method with_id (line 47) | def with_id(self, span_id: str) -> "SpanBuilder":
    method with_name (line 52) | def with_name(self, name: str) -> "SpanBuilder":
    method with_trace_id (line 57) | def with_trace_id(self, trace_id: str) -> "SpanBuilder":
    method with_start_time (line 62) | def with_start_time(self, start_time: datetime) -> "SpanBuilder":
    method with_end_time (line 67) | def with_end_time(self, end_time: datetime) -> "SpanBuilder":
    method with_tag (line 72) | def with_tag(self, key: str, value: str) -> "SpanBuilder":
    method with_tags (line 77) | def with_tags(self, tags: dict[str, str]) -> "SpanBuilder":
    method with_payload (line 82) | def with_payload(self, payload: Any) -> "SpanBuilder":
    method with_parent (line 87) | def with_parent(self, parent: Span) -> "SpanBuilder":
    method with_parent_id (line 95) | def with_parent_id(self, parent_id: str) -> "SpanBuilder":
    method with_app_name (line 100) | def with_app_name(self, app_name: str) -> "SpanBuilder":
    method build (line 105) | def build(self) -> Span:
  class FunctionSpanBuilder (line 121) | class FunctionSpanBuilder(SpanBuilder):
    method __init__ (line 134) | def __init__(self, trace_id: Optional[str] = None, app_name: Optional[...
    method with_function_name (line 141) | def with_function_name(self, name: str) -> "FunctionSpanBuilder":
    method with_arguments (line 148) | def with_arguments(self, arguments: dict[str, Any]) -> "FunctionSpanBu...
    method with_return_value (line 153) | def with_return_value(self, ret: Any) -> "FunctionSpanBuilder":
    method with_error (line 158) | def with_error(self, code: int, message: str) -> "FunctionSpanBuilder":
    method build (line 163) | def build(self) -> Span:
  class HTTPSpanBuilder (line 176) | class HTTPSpanBuilder(SpanBuilder):
    method __init__ (line 189) | def __init__(self, trace_id: Optional[str] = None, app_name: Optional[...
    method with_url (line 198) | def with_url(self, url: str) -> "HTTPSpanBuilder":
    method with_method (line 205) | def with_method(self, method: str) -> "HTTPSpanBuilder":
    method with_header (line 212) | def with_header(self, key: str, value: str | list[str]) -> "HTTPSpanBu...
    method with_headers (line 219) | def with_headers(self, headers: dict[str, list[str]]) -> "HTTPSpanBuil...
    method with_body (line 224) | def with_body(self, body: str | bytes) -> "HTTPSpanBuilder":
    method with_response (line 229) | def with_response(self, response: str | bytes) -> "HTTPSpanBuilder":
    method with_error (line 234) | def with_error(self, code: int, message: str) -> "HTTPSpanBuilder":
    method build (line 239) | def build(self) -> Span:
  class EventBuilder (line 254) | class EventBuilder:
    method __init__ (line 266) | def __init__(self, trace_id: Optional[str] = None, app_name: Optional[...
    method with_id (line 276) | def with_id(self, event_id: str) -> "EventBuilder":
    method with_name (line 281) | def with_name(self, name: str) -> "EventBuilder":
    method with_trace_id (line 286) | def with_trace_id(self, trace_id: str) -> "EventBuilder":
    method with_timestamp (line 291) | def with_timestamp(self, timestamp: datetime) -> "EventBuilder":
    method with_tag (line 296) | def with_tag(self, key: str, value: str) -> "EventBuilder":
    method with_tags (line 301) | def with_tags(self, tags: dict[str, str]) -> "EventBuilder":
    method with_data (line 306) | def with_data(self, data: Any) -> "EventBuilder":
    method with_parent (line 311) | def with_parent(self, parent: Span) -> "EventBuilder":
    method with_parent_id (line 319) | def with_parent_id(self, parent_id: str) -> "EventBuilder":
    method build (line 324) | def build(self) -> Event:

FILE: agentkit/trace/context.py
  class HTTPMethod (line 14) | class HTTPMethod(str, Enum):
  class SpanContext (line 50) | class SpanContext:
    method __init__ (line 57) | def __init__(
    method get_current_span (line 75) | def get_current_span(self) -> Optional[Span]:
    method get_current_trace_id (line 79) | def get_current_trace_id(self) -> str:
    method set_trace_id (line 85) | def set_trace_id(self, trace_id: str):
    method merge_tags (line 89) | def merge_tags(self, tags: dict[str, str]) -> dict[str, str]:
    method span (line 97) | def span(
    method record_event (line 155) | def record_event(
    method function_span (line 187) | def function_span(
    method llm_span (line 217) | def llm_span(
    method tool_span (line 245) | def tool_span(
    method http_span (line 270) | def http_span(
  function get_current_context (line 315) | def get_current_context() -> SpanContext:
  function set_default_tracer (line 324) | def set_default_tracer(tracer: Tracer):
  function set_default_app_name (line 329) | def set_default_app_name(app_name: str):
  function get_default_tracer (line 334) | def get_default_tracer() -> Tracer:
  function get_default_app_name (line 339) | def get_default_app_name() -> str:
  function trace_function (line 345) | def trace_function(
  function start_trace (line 405) | def start_trace(trace_id: Optional[str] = None, context: Optional[SpanCo...
  function record_event (line 420) | def record_event(
  function create_span (line 437) | def create_span(

FILE: agentkit/trace/default.py
  class DefaultSettings (line 8) | class DefaultSettings:
  function set_default (line 16) | def set_default(**kwargs):
  function get_default_settings (line 24) | def get_default_settings() -> DefaultSettings:
  function get_default (line 28) | def get_default(key: str):

FILE: agentkit/trace/local_tracer.py
  class LocalStorageTracer (line 10) | class LocalStorageTracer(Tracer):
    method __init__ (line 11) | def __init__(self, storage_dir: str = "./traces"):
    method _get_trace_spans_file (line 19) | def _get_trace_spans_file(self, trace_id: str) -> Path:
    method _get_trace_events_file (line 22) | def _get_trace_events_file(self, trace_id: str) -> Path:
    method record_span (line 25) | def record_span(self, span: Span) -> None:
    method record_event (line 31) | def record_event(self, event: Event) -> None:
    method get_spans (line 37) | def get_spans(self, trace_id: str) -> list[Span]:
    method get_events (line 57) | def get_events(self, trace_id: str) -> list[Event]:
    method get_trace (line 77) | def get_trace(self, trace_id: str) -> Optional[dict]:
    method get_trace_raw (line 92) | def get_trace_raw(self, trace_id: str) -> Optional[dict]:
    method list_traces (line 140) | def list_traces(self, limit: int = 100, offset: int = 0) -> list[dict]:

FILE: agentkit/trace/remote_tracer.py
  class RemoteEvent (line 14) | class RemoteEvent(BaseModel):
  class RemoteTracer (line 22) | class RemoteTracer(Tracer):
    method __init__ (line 29) | def __init__(
    method __del__ (line 52) | def __del__(self):
    method _send_to_api (line 59) | def _send_to_api(self, data: str) -> bool:
    method record_span (line 97) | def record_span(self, span: Span) -> None:
    method record_event (line 112) | def record_event(self, event: Event) -> None:
    method get_spans (line 127) | def get_spans(self, trace_id: str) -> list[Span]:
    method get_events (line 136) | def get_events(self, trace_id: str) -> list[Event]:
    method get_trace (line 145) | def get_trace(self, trace_id: str) -> Optional[dict]:
    method list_traces (line 154) | def list_traces(self, limit: int = 100, offset: int = 0) -> list[dict]:
  class HybridTracer (line 164) | class HybridTracer(Tracer):
    method __init__ (line 177) | def __init__(self, local_tracer: Tracer, remote_tracer: RemoteTracer):
    method record_span (line 188) | def record_span(self, span: Span) -> None:
    method record_event (line 193) | def record_event(self, event: Event) -> None:
    method get_spans (line 198) | def get_spans(self, trace_id: str) -> list[Span]:
    method get_events (line 202) | def get_events(self, trace_id: str) -> list[Event]:
    method get_trace (line 206) | def get_trace(self, trace_id: str) -> Optional[dict]:
    method get_trace_raw (line 210) | def get_trace_raw(self, trace_id: str) -> Optional[dict]:
    method list_traces (line 214) | def list_traces(self, limit: int = 100, offset: int = 0) -> list[dict]:

FILE: agentkit/trace/span.py
  class HTTPMethod (line 6) | class HTTPMethod(str, Enum):
  class DataType (line 24) | class DataType(str, Enum):
  class SpanType (line 29) | class SpanType(str, Enum):
  class LLMSpanPayload (line 37) | class LLMSpanPayload(BaseModel):
  class ToolSpanPayload (line 44) | class ToolSpanPayload(BaseModel):
  class FunctionSpanPayload (line 51) | class FunctionSpanPayload(BaseModel):
  class HTTPSpanPayload (line 59) | class HTTPSpanPayload(BaseModel):
  class OtherSpanPayload (line 79) | class OtherSpanPayload(BaseModel):
  class Span (line 84) | class Span(BaseModel):
    method update_payload (line 104) | def update_payload(
    method update_payload_data (line 119) | def update_payload_data(self, **kwargs) -> "Span":
    method add_tag (line 131) | def add_tag(self, key: str, value: str) -> "Span":
    method add_tags (line 136) | def add_tags(self, tags: dict[str, str]) -> "Span":
  class EventType (line 142) | class EventType(str, Enum):
  class DeltaEventPayload (line 147) | class DeltaEventPayload(BaseModel):
  class OtherEventPayload (line 152) | class OtherEventPayload(BaseModel):
  class Event (line 157) | class Event(BaseModel):

FILE: agentkit/trace/tracer.py
  class Tracer (line 7) | class Tracer(ABC):
    method record_span (line 9) | def record_span(self, span: Span) -> None:
    method record_event (line 13) | def record_event(self, event: Event) -> None:
    method get_spans (line 17) | def get_spans(self, trace_id: str) -> list[Span]:
    method get_events (line 21) | def get_events(self, trace_id: str) -> list[Event]:
    method get_trace (line 25) | def get_trace(self, trace_id: str) -> Optional[dict]:
    method list_traces (line 29) | def list_traces(self, limit: int = 100, offset: int = 0) -> list[dict]:
    method get_trace_raw (line 32) | def get_trace_raw(self, trace_id: str) -> Optional[dict]:

FILE: agentkit/trace/types.py
  class Error (line 4) | class Error(BaseModel):

FILE: cortex-ui/src/components/EndpointConfig.tsx
  type EndpointConfigProps (line 8) | interface EndpointConfigProps {

FILE: cortex-ui/src/components/ErrorBoundary.tsx
  type Props (line 4) | interface Props {
  type State (line 8) | interface State {
  class ErrorBoundary (line 13) | class ErrorBoundary extends Component<Props, State> {
    method constructor (line 14) | constructor(props: Props) {
    method getDerivedStateFromError (line 19) | static getDerivedStateFromError(error: Error): State {
    method componentDidCatch (line 23) | componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    method render (line 27) | render() {

FILE: cortex-ui/src/components/FilePanel.tsx
  type FileResult (line 25) | interface FileResult {
  type FilePanelProps (line 37) | interface FilePanelProps {

FILE: cortex-ui/src/components/FinalAnswer.tsx
  type FinalAnswerProps (line 20) | interface FinalAnswerProps {
  function extractAnswerContent (line 28) | function extractAnswerContent(content: string): string {
  function preprocessCitations (line 41) | function preprocessCitations(content: string, citations: CitationStore):...

FILE: cortex-ui/src/components/SearchResultsPanel.tsx
  type SearchResultsPanelProps (line 19) | interface SearchResultsPanelProps {

FILE: cortex-ui/src/components/ShellPanel.tsx
  type ShellResult (line 20) | interface ShellResult {
  type ShellPanelProps (line 27) | interface ShellPanelProps {

FILE: cortex-ui/src/components/TodoPanel.tsx
  type TaskStatus (line 20) | type TaskStatus = 'completed' | 'in_progress' | 'pending' | 'not_started';
  type TodoItem (line 23) | interface TodoItem {
  type TodoPanelProps (line 35) | interface TodoPanelProps {
  function parseTodoResult (line 47) | function parseTodoResult(content: string): TodoItem[] {
  function isTodoResult (line 122) | function isTodoResult(content: string): boolean {
  function getStatusConfig (line 133) | function getStatusConfig(status: TaskStatus) {
  function getPriorityTag (line 149) | function getPriorityTag(priority?: 'high' | 'medium' | 'low') {
  function formatTime (line 162) | function formatTime(timeStr?: string) {

FILE: cortex-ui/src/components/WebPagePanel.tsx
  type WebPageResult (line 26) | interface WebPageResult {
  type WebPagePanelProps (line 37) | interface WebPagePanelProps {
  function parseBatchOpenResult (line 319) | function parseBatchOpenResult(resultStr: string): WebPageResult[] {
  function isBatchOpenResult (line 377) | function isBatchOpenResult(resultStr: string): boolean {

FILE: cortex-ui/src/pages/ChatPage.tsx
  type ToolCall (line 50) | interface ToolCall {
  type ToolCallStatus (line 59) | interface ToolCallStatus {
  type MessageItem (line 65) | interface MessageItem {
  type ContentItem (line 93) | interface ContentItem {

FILE: cortex-ui/src/services/api.ts
  constant DEFAULT_API_BASE_URL (line 4) | const DEFAULT_API_BASE_URL = 'http://localhost:8001';
  constant API_BASE_URL (line 13) | const API_BASE_URL =
  class WebSocketService (line 36) | class WebSocketService {
    method connect (line 42) | connect(url: string): Promise<void> {
    method send (line 83) | send(data: AgentEvent): void {
    method onMessage (line 91) | onMessage(handler: (event: AgentEvent) => void): void {
    method onError (line 95) | onError(handler: (error: Event) => void): void {
    method onClose (line 99) | onClose(handler: () => void): void {
    method close (line 103) | close(): void {
    method isConnected (line 119) | isConnected(): boolean {

FILE: cortex-ui/src/types/citation.ts
  type SearchResultItem (line 6) | interface SearchResultItem {
  type QueryResult (line 17) | interface QueryResult {
  type BatchSearchResults (line 24) | interface BatchSearchResults {
  type CitationInfo (line 31) | interface CitationInfo {
  type CitationStore (line 39) | type CitationStore = Map<string, CitationInfo>;

FILE: cortex-ui/src/types/index.ts
  type ModelParams (line 3) | interface ModelParams {
  type ComponentConfig (line 10) | interface ComponentConfig {
  type AgentConfig (line 15) | interface AgentConfig {
  type ChatMessage (line 30) | interface ChatMessage {
  type AgentEventType (line 39) | enum AgentEventType {
  type AgentMessageType (line 48) | enum AgentMessageType {
  type AgentRunningStatus (line 54) | enum AgentRunningStatus {
  type AgentResponse (line 61) | interface AgentResponse {
  type AgentRequest (line 69) | interface AgentRequest {
  type ClientToolCall (line 75) | interface ClientToolCall {
  type AgentEvent (line 83) | interface AgentEvent {

FILE: cortex-ui/src/utils/citationParser.ts
  function parseSearchResultsXML (line 11) | function parseSearchResultsXML(xmlContent: string): CitationStore {
  function extractXMLValue (line 44) | function extractXMLValue(content: string, tagName: string): string | und...
  function isBatchSearchResult (line 53) | function isBatchSearchResult(content: string): boolean {
  function extractCitationsFromToolResult (line 61) | function extractCitationsFromToolResult(result: string): CitationStore {
  function parseQueryResults (line 71) | function parseQueryResults(xmlContent: string): QueryResult[] {

FILE: cortex-ui/src/vite-env.d.ts
  type ImportMetaEnv (line 3) | interface ImportMetaEnv {
  type ImportMeta (line 7) | interface ImportMeta {

FILE: cortex/agents/agent_factory.py
  class AgentFactory (line 11) | class AgentFactory:
    method list_agents (line 19) | def list_agents(self) -> list[AgentConfig]:
    method get_default_agent_config (line 25) | def get_default_agent_config(self, name: str) -> AgentConfig:
    method register_agent (line 36) | def register_agent(
    method make_agent (line 49) | async def make_agent(

FILE: cortex/agents/base_agent.py
  class BaseAgent (line 21) | class BaseAgent:
    method update_from_config (line 32) | def update_from_config(self):
    method __init__ (line 44) | def __init__(
    method __aenter__ (line 59) | async def __aenter__(self):
    method __aexit__ (line 62) | async def __aexit__(self, exc_type, exc_value, traceback):
    method run (line 65) | async def run(
    method _run (line 77) | async def _run(
    method model_api (line 94) | def model_api(self) -> ModelAPI:
    method toolset (line 97) | def toolset(self) -> ToolSet | None:
    method as_tool (line 106) | def as_tool(self, timeout: float | None = None) -> dict[str, Any]:
    method has_tool_call (line 159) | def has_tool_call(message: ChatMessage) -> bool:
    method _execute_single_tool (line 178) | async def _execute_single_tool(self, tool_call: ChatToolCall) -> ChatM...
    method run_tool_call (line 224) | async def run_tool_call(self, message: ChatMessage) -> list[ChatMessage]:
    method run_tool_call_concurrency (line 260) | async def run_tool_call_concurrency(

FILE: cortex/agents/base_step_agent.py
  function _get_encoding (line 45) | def _get_encoding(model_name: str | None):
  function _estimate_token_length (line 62) | def _estimate_token_length(messages: list[ChatMessage], model_name: str ...
  function _compress_batch_search_result (line 85) | def _compress_batch_search_result(content: str) -> str:
  class BaseStepAgent (line 97) | class BaseStepAgent(BaseAgent):
    method __init__ (line 105) | def __init__(
    method _insert_final_prompt (line 163) | def _insert_final_prompt(self) -> None:
    method _make_force_final_answer_message (line 173) | def _make_force_final_answer_message(self) -> ChatMessage:
    method _ensure_final_prompt (line 177) | def _ensure_final_prompt(self, messages: list[ChatMessage]) -> None:
    method _copy_messages (line 195) | def _copy_messages(messages: list[ChatMessage]) -> list[ChatMessage]:
    method _compress_batch_search_in_block (line 206) | def _compress_batch_search_in_block(cls, block: dict) -> bool:
    method _compress_batch_search_in_content (line 226) | def _compress_batch_search_in_content(cls, content: Any) -> tuple[Any,...
    method _prepare_messages_for_model (line 243) | def _prepare_messages_for_model(self) -> list[ChatMessage]:
    method _shrink_batch_search_results (line 266) | def _shrink_batch_search_results(self, messages: list[ChatMessage]) ->...
    method _parse_tool_call_arguments (line 278) | def _parse_tool_call_arguments(raw_arguments: Any) -> dict[str, Any]:
    method _is_search_tool_call (line 288) | def _is_search_tool_call(self, tool_name: str | None, tool_args: dict[...
    method _drop_oldest_tool_cycle (line 301) | def _drop_oldest_tool_cycle(
    method _trim_oldest_messages (line 357) | def _trim_oldest_messages(self, messages: list[ChatMessage]) -> bool:
    method _ensure_context_within_upper_limit (line 377) | def _ensure_context_within_upper_limit(self, messages: list[ChatMessag...
    method _handle_context_overflow (line 399) | def _handle_context_overflow(self, messages: list[ChatMessage]) -> None:
    method _run (line 450) | async def _run(
    method _step (line 599) | async def _step(

FILE: cortex/agents/checkpoint_agent/checkpoint_agent.py
  class PendingToolCall (line 22) | class PendingToolCall(BaseModel):
  class CheckpointState (line 29) | class CheckpointState(BaseModel):
  class CheckpointAgent (line 42) | class CheckpointAgent(BaseAgent):
    method __init__ (line 45) | def __init__(
    method _init_state (line 58) | def _init_state(
    method _process_tool_call_results (line 73) | async def _process_tool_call_results(
    method _execute_step (line 93) | async def _execute_step(
    method _update_client_tool_results (line 165) | async def _update_client_tool_results(self, state: CheckpointState) ->...
    method _should_continue (line 215) | def _should_continue(self, state: CheckpointState) -> bool:
    method _run (line 230) | async def _run(
    method _step (line 275) | async def _step(
    method _tool_call_handler (line 296) | async def _tool_call_handler(

FILE: cortex/agents/checkpoint_agent/checkpointer.py
  class CheckpointStorage (line 18) | class CheckpointStorage:
    method save_state (line 19) | def save_state(self, checkpoint_id: str, state: dict[str, Any]):
    method load_state (line 23) | def load_state(self, checkpoint_id: str) -> dict[str, Any] | None:
    method delete_state (line 27) | def delete_state(self, checkpoint_id: str):
    method asave_state (line 31) | async def asave_state(self, checkpoint_id: str, state: dict[str, Any]):
    method aload_state (line 35) | async def aload_state(self, checkpoint_id: str) -> dict[str, Any] | None:
    method adelete_state (line 39) | async def adelete_state(self, checkpoint_id: str):
  class CheckPointer (line 44) | class CheckPointer(Generic[T]):
    method __init__ (line 47) | def __init__(
    method __enter__ (line 69) | def __enter__(self) -> T:
    method __exit__ (line 78) | def __exit__(self, exc_type, exc_value, traceback):
    method __aenter__ (line 91) | async def __aenter__(self):
    method __aexit__ (line 94) | async def __aexit__(self, exc_type, exc_value, traceback):
  class MemoryCheckPointer (line 98) | class MemoryCheckPointer(CheckpointStorage):
    method __init__ (line 101) | def __init__(self):
    method save_state (line 104) | def save_state(self, checkpoint_id: str, state: dict[str, Any]):
    method load_state (line 109) | def load_state(self, checkpoint_id: str) -> dict[str, Any] | None:
    method delete_state (line 116) | def delete_state(self, checkpoint_id: str):
    method asave_state (line 122) | async def asave_state(self, checkpoint_id: str, state: dict[str, Any]):
    method aload_state (line 126) | async def aload_state(self, checkpoint_id: str) -> dict[str, Any] | None:
    method adelete_state (line 130) | async def adelete_state(self, checkpoint_id: str):
  class FileCheckPointer (line 135) | class FileCheckPointer(CheckpointStorage):
    method __init__ (line 138) | def __init__(self, checkpoint_dir: str):
    method _get_checkpoint_file (line 150) | def _get_checkpoint_file(self, checkpoint_id: str) -> Path:
    method save_state (line 154) | def save_state(self, checkpoint_id: str, state: dict[str, Any]):
    method load_state (line 168) | def load_state(self, checkpoint_id: str) -> dict[str, Any] | None:
    method delete_state (line 186) | def delete_state(self, checkpoint_id: str):
    method asave_state (line 193) | async def asave_state(self, checkpoint_id: str, state: dict[str, Any]):
    method aload_state (line 197) | async def aload_state(self, checkpoint_id: str) -> dict[str, Any] | None:
    method adelete_state (line 201) | async def adelete_state(self, checkpoint_id: str):
  class SqliteCheckPointer (line 206) | class SqliteCheckPointer(CheckpointStorage):
    method __init__ (line 209) | def __init__(self, db_path: str):
    method _init_db (line 228) | def _init_db(self):
    method __enter__ (line 248) | def __enter__(self):
    method __exit__ (line 254) | def __exit__(self, exc_type, exc_value, traceback):
    method __aenter__ (line 260) | async def __aenter__(self):
    method __aexit__ (line 263) | async def __aexit__(self, exc_type, exc_value, traceback):
    method save_state (line 266) | def save_state(self, checkpoint_id: str, state: dict[str, Any]):
    method load_state (line 294) | def load_state(self, checkpoint_id: str) -> dict[str, Any] | None:
    method delete_state (line 323) | def delete_state(self, checkpoint_id: str):
    method asave_state (line 338) | async def asave_state(self, checkpoint_id: str, state: dict[str, Any]):
    method aload_state (line 342) | async def aload_state(self, checkpoint_id: str) -> dict[str, Any] | None:
    method adelete_state (line 346) | async def adelete_state(self, checkpoint_id: str):

FILE: cortex/agents/checkpoint_agent/react_agent.py
  class CheckpointReActAgent (line 24) | class CheckpointReActAgent(CheckpointAgent):
    method __init__ (line 34) | def __init__(
    method _step (line 59) | async def _step(
    method _tool_call_handler (line 95) | async def _tool_call_handler(

FILE: cortex/agents/input/input.py
  class InputChannel (line 12) | class InputChannel(Generic[T]):
    method __init__ (line 15) | def __init__(self, queue: asyncio.Queue[T]) -> None:
    method get (line 18) | async def get(self) -> list[T]:
    method get_no_wait (line 34) | async def get_no_wait(self) -> list[T]:

FILE: cortex/agents/react_agent.py
  function _check_if_finished (line 24) | def _check_if_finished(response_message: ChatMessage | None) -> bool:
  function process_messages (line 36) | async def process_messages(
  class ReActAgent (line 150) | class ReActAgent(BaseStepAgent):
    method __init__ (line 160) | def __init__(
    method _step (line 180) | async def _step(

FILE: cortex/agents/types.py
  class RunnerType (line 10) | class RunnerType(str, Enum):
  class AgentRunningStatus (line 17) | class AgentRunningStatus(str, Enum):
  class AgentMessageType (line 26) | class AgentMessageType(str, Enum):
  class AgentResponseType (line 34) | class AgentResponseType(str, Enum):
  class AgentConfig (line 42) | class AgentConfig(BaseModel):
  class AgentResponse (line 59) | class AgentResponse(BaseModel):
    method get_type (line 71) | def get_type(self) -> AgentResponseType:
    method __add__ (line 81) | def __add__(self, other: "AgentResponse") -> "AgentResponse":

FILE: cortex/context/__init__.py
  function make_simple_context (line 10) | def make_simple_context(session_id: str) -> BaseContext:
  function make_file_context (line 14) | def make_file_context(path: str, session_id: str) -> BaseContext:

FILE: cortex/context/base_context.py
  class BaseContext (line 9) | class BaseContext(ABC):
    method __init__ (line 12) | def __init__(self, session_id: str):
    method add (line 22) | def add(self, messages: list[ChatMessage]) -> None:
    method get_all (line 32) | def get_all(self) -> List[ChatMessage]:

FILE: cortex/context/file_context.py
  class FileContext (line 14) | class FileContext(BaseContext):
    method __init__ (line 17) | def __init__(
    method __del__ (line 44) | def __del__(self):
    method _load_messages (line 52) | def _load_messages(self) -> None:
    method _save_messages (line 67) | def _save_messages(self) -> None:
    method _delayed_write (line 80) | async def _delayed_write(self) -> None:
    method _schedule_write (line 87) | def _schedule_write(self) -> None:
    method add (line 100) | def add(self, messages: list[ChatMessage]) -> None:
    method get_all (line 109) | def get_all(self) -> List[ChatMessage]:
    method clear (line 117) | def clear(self) -> None:
    method flush (line 127) | def flush(self) -> None:

FILE: cortex/context/simple_context.py
  class SimpleContext (line 10) | class SimpleContext(BaseContext):
    method __init__ (line 13) | def __init__(self, session_id: str):
    method add (line 16) | def add(self, msg: list[ChatMessage]) -> None:
    method get_all (line 23) | def get_all(self) -> List[ChatMessage]:

FILE: cortex/env.py
  function _repo_root (line 7) | def _repo_root() -> Path:
  function _parse_env_line (line 11) | def _parse_env_line(line: str) -> tuple[str, str] | None:
  function _load_env_fallback (line 29) | def _load_env_fallback(path: Path) -> None:
  function load_env (line 42) | def load_env() -> None:

FILE: cortex/examples/agents/ask_input_agent.py
  function init_ask_input_tools (line 28) | async def init_ask_input_tools() -> ToolSet:
  class AskInputAgent (line 59) | class AskInputAgent(BaseAgent):
    method __init__ (line 67) | def __init__(
    method _run (line 73) | async def _run(
  function make_ask_input_agent (line 192) | async def make_ask_input_agent(
  function get_ask_input_agent_config (line 203) | def get_ask_input_agent_config() -> AgentConfig:

FILE: cortex/examples/agents/deep_reasearch_agent.py
  function init_deep_research_tools (line 11) | async def init_deep_research_tools() -> ToolSet:
  function get_deep_research_agent_config (line 32) | def get_deep_research_agent_config() -> AgentConfig:
  function make_deep_research_agent (line 75) | async def make_deep_research_agent(session_id: str, config: AgentConfig)...

FILE: cortex/examples/agents/main_agent.py
  function init_main_tools (line 22) | async def init_main_tools() -> ToolSet:
  function make_main_agent (line 53) | async def make_main_agent(
  function get_main_agent_config (line 64) | def get_main_agent_config() -> AgentConfig:

FILE: cortex/examples/agents/math_agent.py
  function add (line 19) | def add(a: float, b: float) -> float:
  function subtract (line 32) | def subtract(a: float, b: float) -> float:
  function multiply (line 45) | def multiply(a: float, b: float) -> float:
  function divide (line 58) | def divide(a: float, b: float) -> float:
  function power (line 76) | def power(base: float, exponent: float) -> float:
  function sqrt (line 89) | def sqrt(number: float) -> float:
  function calculate_expression (line 106) | def calculate_expression(expression: str) -> float:
  function init_math_tools (line 128) | async def init_math_tools() -> ToolSet:
  function make_math_agent (line 159) | async def make_math_agent(
  function get_math_agent_config (line 170) | def get_math_agent_config() -> AgentConfig:

FILE: cortex/examples/agents/plan_agent.py
  function init_plan_tools (line 17) | async def init_plan_tools() -> ToolSet:
  function make_plan_agent (line 54) | async def make_plan_agent(context_id: str, config: AgentConfig) -> BaseA...
  function get_plan_agent_config (line 61) | def get_plan_agent_config() -> AgentConfig:

FILE: cortex/examples/agents/search_agent.py
  function init_search_tools (line 16) | async def init_search_tools() -> ToolSet:
  function make_search_agent (line 26) | async def make_search_agent(
  function get_search_agent_config (line 37) | def get_search_agent_config() -> AgentConfig:

FILE: cortex/examples/demo_agent_cli.py
  function main (line 31) | async def main():

FILE: cortex/examples/demo_agent_with_orchestrator.py
  function main (line 24) | async def main(

FILE: cortex/examples/demo_agent_with_tool.py
  function add_numbers (line 24) | def add_numbers(a: int, b: int) -> int:
  function multiply_numbers (line 37) | async def multiply_numbers(a: int, b: int) -> int:
  function get_random_string (line 51) | def get_random_string(length: int = 10) -> str:
  function calculate_area (line 63) | def calculate_area(radius: float) -> float:
  function init_tools (line 75) | async def init_tools():
  function main (line 102) | async def main(user_input: str):

FILE: cortex/examples/demo_checkpoint.py
  function make_storage (line 26) | def make_storage() -> CheckpointStorage:
  function get_search_agent_config (line 30) | def get_search_agent_config() -> AgentConfig:
  function get_math_agent_config (line 42) | def get_math_agent_config() -> AgentConfig:
  function make_search_agent (line 54) | async def make_search_agent(
  function make_math_agent (line 70) | async def make_math_agent(
  function make_main_agent (line 85) | async def make_main_agent(
  function main (line 150) | async def main():

FILE: cortex/examples/demo_toolset_channel.py
  class MockExternalServer (line 16) | class MockExternalServer:
    method __init__ (line 19) | def __init__(self):
    method process_request (line 23) | async def process_request(self, tool_name: str, request_id: str, data:...
  function demo_basic_usage (line 69) | async def demo_basic_usage():
  function demo_custom_request_id (line 133) | async def demo_custom_request_id():
  function demo_manual_response (line 179) | async def demo_manual_response():
  function demo_error_handling (line 248) | async def demo_error_handling():
  function demo_concurrent_requests (line 310) | async def demo_concurrent_requests():
  function main (line 362) | async def main():

FILE: cortex/examples/server.py
  function main (line 21) | async def main():

FILE: cortex/model/__init__.py
  class MessageType (line 21) | class MessageType(str, Enum):
  class ModelMessage (line 28) | class ModelMessage(BaseModel):
  class ModelAPI (line 35) | class ModelAPI:
    method __init__ (line 38) | def __init__(self, provider: ModelProvider):
    method chat_completion (line 41) | async def chat_completion(
    method chat_completion_stream (line 70) | async def chat_completion_stream(

FILE: cortex/model/definition.py
  class MessageRole (line 8) | class MessageRole(Enum):
  class ContentBlockType (line 18) | class ContentBlockType(Enum):
  class ModelParams (line 34) | class ModelParams(BaseModel):
  class Function (line 47) | class Function(BaseModel):
  class ChatToolCall (line 60) | class ChatToolCall(BaseModel):
  class ExtraInfo (line 74) | class ExtraInfo(TypedDict, total=False):
  class ChatMessage (line 97) | class ChatMessage(BaseModel):
    method from_dict (line 108) | def from_dict(cls, data: dict) -> "ChatMessage":
    method to_dict (line 111) | def to_dict(self) -> dict:

FILE: cortex/model/provider.py
  class ModelProvider (line 7) | class ModelProvider(ABC):
    method chat_completion_stream (line 9) | async def chat_completion_stream(
    method chat_completion (line 18) | async def chat_completion(

FILE: cortex/model/stepfun_chat.py
  class Function (line 25) | class Function(BaseModel):
  class ToolCall (line 31) | class ToolCall(BaseModel):
  class Message (line 39) | class Message(BaseModel):
  class Delta (line 47) | class Delta(BaseModel):
  class Choice (line 55) | class Choice(BaseModel):
  class StreamChoice (line 62) | class StreamChoice(BaseModel):
  class Usage (line 69) | class Usage(BaseModel):
  class ChatCompletion (line 76) | class ChatCompletion(BaseModel):
  class ChatCompletionChunk (line 86) | class ChatCompletionChunk(BaseModel):
  class StepFunClient (line 98) | class StepFunClient:
    method __init__ (line 107) | def __init__(
    method _build_headers (line 117) | def _build_headers(self) -> dict[str, str]:
    method _build_request_body (line 123) | def _build_request_body(
    method chat_completion (line 149) | async def chat_completion(
    method chat_completion_stream (line 181) | async def chat_completion_stream(

FILE: cortex/model/stepfun_provider.py
  class StepFunModelProvider (line 27) | class StepFunModelProvider(ModelProvider):
    method __init__ (line 34) | def __init__(self, model_params: ModelParams):
    method _call_with_retry (line 42) | async def _call_with_retry(
    method _stream_with_retry (line 77) | async def _stream_with_retry(
    method _create_client (line 107) | def _create_client(self) -> StepFunClient:
    method _chat_messages_to_openai (line 126) | def _chat_messages_to_openai(
    method _extract_think_from_content (line 232) | def _extract_think_from_content(self, content: str | None) -> tuple[st...
    method _message_to_chat_message (line 259) | def _message_to_chat_message(self, message: Message) -> ChatMessage:
    method _process_stream_content_for_think (line 320) | def _process_stream_content_for_think(self, content: str | None) -> tu...
    method _delta_to_chat_message (line 396) | def _delta_to_chat_message(
    method chat_completion (line 458) | async def chat_completion(
    method chat_completion_stream (line 500) | async def chat_completion_stream(

FILE: cortex/model/utils.py
  function merge_delta_message (line 4) | def merge_delta_message(d1: dict | None, d2: dict | None) -> dict:

FILE: cortex/orchestrator/local_runner.py
  class LocalRunner (line 29) | class LocalRunner(Runner):
    method __init__ (line 34) | def __init__(
    method init (line 64) | async def init(
    method send (line 95) | async def send(self, event: AgentEvent) -> None:
    method run (line 118) | async def run(self) -> AsyncGenerator[AgentEvent, None]:
    method _on_agent_finished (line 151) | def _on_agent_finished(self, response: AgentResponse) -> None:
    method _on_generator_complete (line 184) | async def _on_generator_complete(
    method _on_client_tool_send (line 196) | async def _on_client_tool_send(
    method get_result (line 240) | def get_result(self) -> AgentEvent:

FILE: cortex/orchestrator/orchestrator.py
  class OrchMode (line 24) | class OrchMode(str, Enum):
  class Orchestrator (line 31) | class Orchestrator:
    method __init__ (line 34) | def __init__(self, agent_factory: AgentFactory):
    method list_agents (line 52) | def list_agents(self) -> list[AgentConfig]:
    method run (line 56) | async def run(
    method run_root_runner (line 138) | async def run_root_runner(
    method send_event (line 159) | async def send_event(self, event: AgentEvent) -> None:
    method _create_runner (line 172) | async def _create_runner(
    method _handle_client_tool_call (line 203) | async def _handle_client_tool_call(
    method _on_generator_complete (line 284) | async def _on_generator_complete(
    method _cleanup_runner (line 352) | def _cleanup_runner(self, task_id: str) -> None:

FILE: cortex/orchestrator/remote_runner.py
  class RemoteRunner (line 9) | class RemoteRunner(Runner):
    method __init__ (line 12) | def __init__(self, channel: Channel):
    method init (line 23) | async def init(
    method send (line 40) | async def send(self, event: AgentEvent) -> None:
    method run (line 55) | async def run(self) -> AsyncGenerator[AgentEvent, None]:

FILE: cortex/orchestrator/runner.py
  class Runner (line 10) | class Runner(ABC):
    method __init__ (line 13) | def __init__(
    method get_parent_task_id (line 31) | def get_parent_task_id(self) -> str | None:
    method get_root_task_id (line 40) | def get_root_task_id(self) -> str | None:
    method init (line 50) | async def init(
    method send (line 67) | async def send(self, event: AgentEvent) -> None:
    method run (line 77) | async def run(self) -> AsyncGenerator[AgentEvent, None]:
    method get_result (line 87) | def get_result(self) -> AgentEvent:

FILE: cortex/orchestrator/types.py
  class AgentEventType (line 10) | class AgentEventType(str, Enum):
  class AgentRequest (line 21) | class AgentRequest(BaseModel):
  class ClientToolCallType (line 29) | class ClientToolCallType(str, Enum):
  class ClientToolCall (line 37) | class ClientToolCall(BaseModel):
  class AgentEvent (line 46) | class AgentEvent(BaseModel):

FILE: cortex/runtime_config.py
  function _repo_root (line 15) | def _repo_root() -> Path:
  function _pick_first_present (line 20) | def _pick_first_present(cfg: dict[str, Any], keys: tuple[str, ...]) -> Any:
  function _as_int (line 27) | def _as_int(value: Any) -> int | None:
  function load_runtime_config (line 45) | def load_runtime_config() -> dict[str, Any]:
  function get_context_limit_overrides (line 82) | def get_context_limit_overrides() -> tuple[int | None, int | None]:

FILE: cortex/server/channel/channel.py
  class Channel (line 4) | class Channel(ABC):
    method __init__ (line 5) | def __init__(self, context_id: str) -> None:
    method send (line 9) | async def send(self, event: dict[str, object]):
    method receive (line 13) | async def receive(self) -> dict[str, object]:
    method heartbeat (line 17) | async def heartbeat(self):
    method close (line 21) | async def close(self):

FILE: cortex/server/channel/error.py
  class ChannelClosedError (line 1) | class ChannelClosedError(Exception):

FILE: cortex/server/channel/memory_channel.py
  class MemoryChannel (line 10) | class MemoryChannel(Channel):
    method __init__ (line 11) | def __init__(self, context_id: str) -> None:
    method send (line 18) | async def send(self, event: Dict[str, object]):
    method receive (line 24) | async def receive(self) -> Dict[str, object]:
    method heartbeat (line 32) | async def heartbeat(self):
    method close (line 35) | async def close(self):

FILE: cortex/server/channel/ws_channel.py
  class WebSocketChannel (line 16) | class WebSocketChannel(Channel):
    method __init__ (line 20) | def __init__(self, ws: WebSocket) -> None:
    method send (line 23) | async def send(self, event: dict[str, object]) -> None:
    method receive (line 28) | async def receive(self) -> dict[str, object]:
    method heartbeat (line 46) | async def heartbeat(self) -> None:
    method close (line 53) | async def close(self) -> None:

FILE: cortex/server/http_server.py
  function extract_and_set_trace_id (line 25) | def extract_and_set_trace_id(headers: dict) -> str:
  class TraceMiddleware (line 44) | class TraceMiddleware(BaseHTTPMiddleware):
    method dispatch (line 47) | async def dispatch(self, request: Request, call_next):
  class HttpServer (line 54) | class HttpServer:
    method __init__ (line 58) | def __init__(self, orch: Orchestrator, tracer: Tracer):
    method start (line 62) | async def start(self, host: str = "0.0.0.0", port: int = 8001) -> None:
    method _build_app (line 75) | def _build_app(self) -> FastAPI:

FILE: cortex/server/log/log.py
  function setup_logging (line 13) | def setup_logging(log_dir: str = "./logs", log_level: int = logging.WARN...

FILE: cortex/server/log/trace.py
  function set_trace_id (line 7) | def set_trace_id(trace_id: str):
  function get_trace_id (line 11) | def get_trace_id() -> str | None:
  class TraceIdFilter (line 15) | class TraceIdFilter(logging.Filter):
    method filter (line 18) | def filter(self, record: logging.LogRecord) -> bool:

FILE: cortex/tools/agent_tool.py
  class AgentTool (line 16) | class AgentTool(Tool):
    method __init__ (line 21) | def __init__(
    method _define_schema (line 55) | def _define_schema(self) -> ToolSchema:
    method _call (line 101) | async def _call(self, parameters: str, **kwargs) -> Any:
    method parse_messages (line 176) | def parse_messages(parameters: str) -> list[ChatMessage]:

FILE: cortex/tools/base.py
  class ToolSchema (line 11) | class ToolSchema:
  class Tool (line 21) | class Tool(ABC):
    method __init__ (line 24) | def __init__(
    method get_schema (line 45) | def get_schema(self) -> ToolSchema:
    method _define_schema (line 57) | def _define_schema(self) -> ToolSchema:
    method call (line 66) | async def call(self, parameters: str, **kwargs) -> Any:
    method _call (line 80) | async def _call(self, parameters: str, **kwargs) -> Any:

FILE: cortex/tools/channel.py
  class MessageType (line 12) | class MessageType(Enum):
  class ChannelMessage (line 21) | class ChannelMessage:
  class Channel (line 31) | class Channel:
    method __init__ (line 34) | def __init__(
    method set_on_send (line 51) | def set_on_send(
    method create_request_id (line 64) | def create_request_id(self) -> str:
    method send_request (line 74) | async def send_request(
    method set_response (line 145) | def set_response(self, request_id: str, data: Any, error: Optional[str...

FILE: cortex/tools/client_tool.py
  class ClientTool (line 13) | class ClientTool(Tool):
    method __init__ (line 16) | def __init__(
    method _define_schema (line 52) | def _define_schema(self) -> ToolSchema:
    method _call (line 69) | async def _call(self, parameters: str, **kwargs) -> Any:

FILE: cortex/tools/function_tool.py
  class FunctionTool (line 13) | class FunctionTool(Tool):
    method __init__ (line 16) | def __init__(
    method _define_schema (line 35) | def _define_schema(self) -> ToolSchema:
    method _call (line 47) | async def _call(self, parameters: str, **kwargs) -> Any:

FILE: cortex/tools/mcp.py
  class MCPNotInitializedError (line 12) | class MCPNotInitializedError(Exception):
    method __init__ (line 15) | def __init__(self) -> None:
  class MCPClient (line 20) | class MCPClient:
    method __init__ (line 21) | def __init__(self, server_url: str) -> None:
    method initialize (line 26) | async def initialize(self) -> None:
    method aclose (line 37) | async def aclose(self) -> None:
    method list_tools (line 48) | async def list_tools(self) -> list[Tool]:
    method call_tool (line 55) | async def call_tool(

FILE: cortex/tools/mcp_tool.py
  class MCPTool (line 11) | class MCPTool(Tool):
    method __init__ (line 14) | def __init__(
    method _define_schema (line 33) | def _define_schema(self) -> ToolSchema:
    method _call (line 46) | async def _call(self, parameters: str, **kwargs) -> Any:

FILE: cortex/tools/session_tool.py
  class SessionTool (line 9) | class SessionTool(Tool):
    method __init__ (line 12) | def __init__(
    method _define_schema (line 34) | def _define_schema(self) -> ToolSchema:
    method _call (line 55) | async def _call(self, parameters: str, **kwargs) -> Any:

FILE: cortex/tools/toolset.py
  class ToolSet (line 21) | class ToolSet:
    method __init__ (line 26) | def __init__(
    method set_on_send (line 60) | def set_on_send(
    method register (line 75) | def register(self, tool: Tool, name: Optional[str] = None) -> None:
    method register_from_mcp_server (line 95) | async def register_from_mcp_server(
    method register_from_config (line 129) | def register_from_config(self, config: ToolConfig) -> Tool:
    method get_tool (line 175) | def get_tool(self, name: str) -> Optional[Tool]:
    method list_tools (line 187) | def list_tools(self) -> List[str]:
    method call (line 196) | async def call(self, tool_name: str, parameters: str, **kwargs) -> Any:
    method get_schema (line 230) | def get_schema(self, tool_name: str) -> Any:
    method get_all_schemas (line 249) | def get_all_schemas(self) -> Dict[str, Any]:
    method get_client_tool_call_result (line 258) | def get_client_tool_call_result(self, tool_call_id: str) -> Any:
    method set_client_tool_call_result (line 273) | def set_client_tool_call_result(
    method set_response (line 281) | def set_response(self, request_id: str, data: Any, error: Optional[str...

FILE: cortex/tools/types.py
  class ToolParameters (line 10) | class ToolParameters(BaseModel):
  class ToolType (line 17) | class ToolType(Enum):
  class ExecutionType (line 28) | class ExecutionType(Enum):
  class ToolConfig (line 36) | class ToolConfig:

FILE: cortex/tools/ublock_agent_tool.py
  class UnblockAgentTool (line 16) | class UnblockAgentTool(Tool):
    method __init__ (line 21) | def __init__(
    method _define_schema (line 55) | def _define_schema(self) -> ToolSchema:
    method _call (line 101) | async def _call(self, parameters: str, **kwargs) -> Any:
    method parse_messages (line 150) | def parse_messages(parameters: str) -> list[ChatMessage]:

FILE: cortex/tools/unblock_client_tool.py
  class UnblockClientTool (line 13) | class UnblockClientTool(Tool):
    method __init__ (line 16) | def __init__(
    method _define_schema (line 51) | def _define_schema(self) -> ToolSchema:
    method _call (line 68) | async def _call(self, parameters: str, **kwargs) -> Any:

FILE: cortex/tui/tui.py
  function _content_to_string (line 30) | def _content_to_string(content) -> str:
  class EventItem (line 81) | class EventItem(ListItem):
    method __init__ (line 84) | def __init__(self, event: AgentEvent, index: int):
    method render (line 89) | def render(self) -> RenderableType:
  class CommandItem (line 407) | class CommandItem(ListItem):
    method __init__ (line 410) | def __init__(self, command: str):
    method render (line 414) | def render(self) -> RenderableType:
  class AgentItem (line 419) | class AgentItem(ListItem):
    method __init__ (line 422) | def __init__(self, agent_config: AgentConfig):
    method render (line 426) | def render(self) -> RenderableType:
  class ProcessView (line 445) | class ProcessView(Container):
    method __init__ (line 448) | def __init__(self, *args, **kwargs):
    method compose (line 454) | def compose(self) -> ComposeResult:
    method add_event (line 463) | def add_event(self, event: AgentEvent) -> None:
    method on_list_item_selected (line 473) | def on_list_item_selected(self, event: ListView.Selected) -> None:
    method clear_events (line 486) | def clear_events(self) -> None:
  class AgentsListView (line 492) | class AgentsListView(Container):
    method __init__ (line 495) | def __init__(self, *args, **kwargs):
    method set_selection_callback (line 501) | def set_selection_callback(self, callback: Callable[[str], None]) -> N...
    method compose (line 505) | def compose(self) -> ComposeResult:
    method update_agents (line 514) | def update_agents(self, agents: list[AgentConfig]) -> None:
    method on_list_item_selected (line 522) | def on_list_item_selected(self, event: ListView.Selected) -> None:
  class TaskItem (line 539) | class TaskItem(ListItem):
    method __init__ (line 542) | def __init__(self, task_id: str, request_data: dict):
    method render (line 547) | def render(self) -> RenderableType:
  class TasksListView (line 577) | class TasksListView(Container):
    method __init__ (line 580) | def __init__(self, *args, **kwargs):
    method set_selection_callback (line 586) | def set_selection_callback(self, callback: Callable[[str], None]) -> N...
    method compose (line 590) | def compose(self) -> ComposeResult:
    method update_tasks (line 599) | def update_tasks(self, tasks: list[tuple[str, dict]]) -> None:
    method on_list_item_selected (line 611) | def on_list_item_selected(self, event: ListView.Selected) -> None:
  class CommandSelector (line 625) | class CommandSelector(Container):
    method __init__ (line 628) | def __init__(self, *args, **kwargs):
    method compose (line 633) | def compose(self) -> ComposeResult:
    method update_commands (line 636) | def update_commands(self, commands: list[str]) -> None:
    method get_selected_command (line 647) | def get_selected_command(self) -> Optional[str]:
  class AgentTUIApp (line 664) | class AgentTUIApp(App):
    method __init__ (line 752) | def __init__(
    method compose (line 790) | def compose(self) -> ComposeResult:
    method on_mount (line 809) | def on_mount(self) -> None:
    method on_input_changed (line 838) | def on_input_changed(self, event: Input.Changed) -> None:
    method on_command_selected (line 865) | def on_command_selected(self, event: ListView.Selected) -> None:
    method _focus_input (line 875) | def _focus_input(self) -> None:
    method _update_placeholder_by_focus (line 881) | def _update_placeholder_by_focus(self) -> None:
    method on_key (line 892) | def on_key(self, event: Key) -> None:
    method on_input_submitted (line 937) | def on_input_submitted(self, event: Input.Submitted) -> None:
    method _handle_command (line 979) | def _handle_command(self, command: str) -> None:
    method _switch_view (line 1020) | def _switch_view(self, view_name: str) -> None:
    method _on_agent_selected (line 1047) | def _on_agent_selected(self, agent_name: str) -> None:
    method _send_ask_input_result (line 1053) | def _send_ask_input_result(self, user_input: str) -> None:
    method _on_task_selected (line 1100) | def _on_task_selected(self, task_id: str) -> None:
    method _load_tasks (line 1107) | def _load_tasks(self) -> None:
    method _load_task_events (line 1131) | def _load_task_events(self, task_id: str) -> None:
    method _save_request (line 1163) | def _save_request(self, root_task_id: str, request: AgentRequest) -> N...
    method _save_event (line 1176) | def _save_event(self, root_task_id: str, event: AgentEvent) -> None:
    method _run_agent (line 1190) | async def _run_agent(self, user_input: str) -> None:
    method action_quit (line 1280) | async def action_quit(self) -> None:

FILE: cortex/utils/generator_merger.py
  class GeneratorMerger (line 8) | class GeneratorMerger:
    method __init__ (line 11) | def __init__(
    method add_generator (line 35) | def add_generator(
    method add_async_generator (line 58) | def add_async_generator(
    method _get_next_item (line 79) | def _get_next_item(self, generator: Generator[Any, None, None]) -> tup...
    method _run_generator_with_wrapper (line 95) | async def _run_generator_with_wrapper(
    method _run_generator (line 131) | async def _run_generator(
    method _run_async_generator (line 164) | async def _run_async_generator(
    method _notify_generator_complete (line 189) | async def _notify_generator_complete(
    method merge (line 229) | async def merge(self) -> AsyncGenerator[Any, None]:
    method __aiter__ (line 349) | async def __aiter__(self):
  function example_usage (line 355) | async def example_usage():

FILE: cortex/utils/generator_merger_examples.py
  function example_1_basic_usage (line 9) | async def example_1_basic_usage():
  function example_2_dynamic_addition (line 34) | async def example_2_dynamic_addition():
  function example_3_different_data_types (line 66) | async def example_3_different_data_types():
  function example_4_data_streams (line 100) | async def example_4_data_streams():
  function example_5_file_processing (line 145) | async def example_5_file_processing():
  function example_6_batch_processing (line 175) | async def example_6_batch_processing():
  function example_7_error_handling (line 211) | async def example_7_error_handling():
  function example_8_callback_usage (line 249) | async def example_8_callback_usage():
  function example_9_callback_vs_event (line 321) | async def example_9_callback_vs_event():
  function run_all_examples (line 369) | async def run_all_examples():

FILE: demo/dr_agent/dr_agent.py
  function init_dr_tools (line 28) | async def init_dr_tools(context_id: str | None = None) -> ToolSet:
  function make_dr_agent (line 67) | async def make_dr_agent(
  function get_dr_agent_config (line 99) | def get_dr_agent_config() -> AgentConfig:

FILE: demo/server.py
  function parse_args (line 14) | def parse_args():
  function main (line 25) | async def main():

FILE: demo/tools/batch_open.py
  function _generate_unique_name (line 17) | def _generate_unique_name() -> str:
  class BatchOpenResult (line 22) | class BatchOpenResult(TypedDict):
  function batch_open (line 31) | async def batch_open(urls: list[str]) -> BatchOpenResult:
  function format_open_results (line 74) | def format_open_results(result: BatchOpenResult) -> str:

FILE: demo/tools/batch_search.py
  function _generate_cite_index (line 19) | def _generate_cite_index(query_idx: int, item_idx: int, url: str) -> str:
  class BatchSearchResult (line 25) | class BatchSearchResult(TypedDict):
  function _deduplicate_results (line 30) | def _deduplicate_results(search_results: list[SearchResult]) -> None:
  function _rerank (line 48) | def _rerank(search_results: list[SearchResult], topk: int) -> None:
  function batch_search (line 92) | async def batch_search(
  function _generate_unique_name (line 130) | def _generate_unique_name() -> str:
  function format_search_results (line 135) | def format_search_results(result: BatchSearchResult) -> str:

FILE: demo/tools/batch_web_surfer.py
  function _ensure_list (line 12) | def _ensure_list(value: list[str] | str | None) -> list[str]:
  function batch_web_surfer (line 31) | async def batch_web_surfer(
  function create_batch_web_surfer_tool (line 102) | def create_batch_web_surfer_tool():

FILE: demo/tools/file.py
  function _wrap_tool_result (line 22) | def _wrap_tool_result(text: str) -> list[dict]:
  function _safe_decode (line 36) | def _safe_decode(data: bytes, encoding: str) -> tuple[str, bool]:
  function _format_bytes (line 45) | def _format_bytes(size: int) -> str:
  function _format_read_result (line 55) | def _format_read_result(
  function _format_write_result (line 79) | def _format_write_result(path: Path, action: str, size: int) -> str:
  function _format_stat_result (line 89) | def _format_stat_result(path: Path, stat_result: os.stat_result) -> str:
  function _format_list_result (line 106) | def _format_list_result(path: Path, entries: list[os.DirEntry]) -> str:
  function file (line 129) | async def file(
  function create_file_tool (line 236) | def create_file_tool() -> FunctionTool:

FILE: demo/tools/open.py
  class Page (line 19) | class Page(TypedDict, total=False):
  class OpenResult (line 32) | class OpenResult(TypedDict):
  function _make_error_result (line 39) | def _make_error_result(code: int, message: str, url: str) -> OpenResult:
  function _download_http (line 48) | async def _download_http(url: str, timeout: int = _OPEN_URL_TIMEOUT) -> ...
  function _download_s3 (line 64) | def _download_s3(url: str) -> bytes:
  function _get_url_content_bytes (line 70) | async def _get_url_content_bytes(url: str, timeout: int = _OPEN_URL_TIME...
  function _parse_content_to_markdown (line 83) | def _parse_content_to_markdown(content_bytes: bytes, url: str) -> str:
  function open (line 94) | async def open(url: str) -> OpenResult:

FILE: demo/tools/search.py
  class SearchResultItem (line 8) | class SearchResultItem(TypedDict):
  class SearchResult (line 17) | class SearchResult(TypedDict):
  function search (line 28) | async def search(

FILE: demo/tools/shell.py
  class ShellOutput (line 28) | class ShellOutput:
  function _truncate_from_end (line 36) | def _truncate_from_end(content: str, max_length: int = MAX_OUTPUT_LENGTH...
  function _format_shell_output (line 43) | def _format_shell_output(
  function _execute_shell (line 91) | async def _execute_shell(
  function shell (line 147) | async def shell(
  function create_shell_tool (line 187) | def create_shell_tool() -> FunctionTool:

FILE: demo/tools/text_truncator.py
  function truncate_text (line 11) | def truncate_text(
  function _find_sentence_boundary (line 67) | def _find_sentence_boundary(text: str, min_position: float) -> int:
  function clean_text_whitespace (line 92) | def clean_text_whitespace(text: str, max_consecutive_newlines: int = 3) ...
  function clean_markdown_content (line 116) | def clean_markdown_content(text: str) -> str:
  class FormatResult (line 141) | class FormatResult:
  function format_content (line 148) | def format_content(

FILE: demo/tools/todo.py
  function _ensure_todo_dir (line 81) | def _ensure_todo_dir():
  class TodoItemHint (line 87) | class TodoItemHint(TypedDict):
  class TodoItem (line 111) | class TodoItem:
    method __post_init__ (line 117) | def __post_init__(self):
    method update (line 126) | def update(self, updates: dict[str, Any]) -> None:
  class TodoCollection (line 134) | class TodoCollection:
    method merge_updates (line 139) | def merge_updates(self, updates: UPDATE_TYPE) -> None:
    method to_dict (line 149) | def to_dict(self) -> dict[str, dict[str, Any]]:
    method from_dict (line 154) | def from_dict(cls, data: dict[str, dict[str, Any]]) -> "TodoCollection":
  function format_todo_result (line 162) | def format_todo_result(todos: TodoCollection) -> tuple[str, dict[str, An...
  function _append_field_line (line 236) | def _append_field_line(lines: list[str], field_name: str, value: Any) ->...
  function seems_like_complete_rewrite (line 260) | def seems_like_complete_rewrite(updates: UPDATE_TYPE, current: dict[str,...
  function create_todo_tool (line 272) | def create_todo_tool(context_id: str = "default"):

FILE: demo/tools/utils.py
  function json_dumps (line 11) | def json_dumps(data: Any, indent: int = 2, ensure_ascii: bool = False) -...

FILE: scripts/runner.py
  class EvalTask (line 44) | class EvalTask:
  function slugify (line 54) | def slugify(value: str | None, fallback_prefix: str = "task") -> str:
  function _truncate (line 65) | def _truncate(text: str, limit: int | None = 600) -> str:
  function _stringify_content (line 73) | def _stringify_content(content: Any) -> str:
  function _format_content_blocks (line 102) | def _format_content_blocks(
  function _format_tool_calls (line 132) | def _format_tool_calls(tool_calls: list[ChatToolCall] | None) -> list[di...
  function _extract_answer_text (line 147) | def _extract_answer_text(content: Any) -> str | None:
  function _format_message (line 159) | def _format_message(
  function _format_event (line 180) | def _format_event(event: AgentEvent, index: int, max_block_chars: int | ...
  function load_tasks_from_file (line 224) | def load_tasks_from_file(path: Path) -> list[EvalTask]:
  function merge_tasks (line 269) | def merge_tasks(
  function build_orchestrator (line 301) | def build_orchestrator(agent_name: str) -> Orchestrator:
  function build_agent_config (line 312) | def build_agent_config(
  function run_single_task (line 337) | async def run_single_task(
  function save_result (line 469) | def save_result(
  function run_tasks (line 513) | async def run_tasks(
  function parse_args (line 553) | def parse_args() -> argparse.Namespace:
  function configure_logging (line 626) | def configure_logging(level: str) -> None:
  function load_config_file (line 633) | def load_config_file(path: str | Path) -> dict[str, Any]:
  function merge_with_config (line 642) | def merge_with_config(
  function main (line 664) | def main() -> None:
Condensed preview — 125 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (779K chars).
[
  {
    "path": ".env-example",
    "chars": 181,
    "preview": "MODEL_PROVIDER=stepfun\nMODEL_BASE=https://api.stepfun.com\nSTEP_MODEL_API_KEY=your-model-api-key\nSTEP_SEARCH_API_BASE=htt"
  },
  {
    "path": ".gitignore",
    "chars": 363,
    "preview": "# common file types\n*.log\n*.ipynb\n#*.json\n# Allow json files in frontend directory\n!agent_cortex/adapter/frontend/**/*.j"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 30461,
    "preview": "# Step-DeepResearch\n\n<div align=\"center\">\n  <img src=\"assets/logo.png\"  height=100>\n</div>\n<div align=\"center\" style=\"li"
  },
  {
    "path": "agentkit/trace/__init__.py",
    "chars": 1403,
    "preview": "from .builder import EventBuilder, FunctionSpanBuilder, HTTPSpanBuilder, SpanBuilder\nfrom .context import (\n    SpanCont"
  },
  {
    "path": "agentkit/trace/builder.py",
    "chars": 9990,
    "preview": "\"\"\"\nBuilder pattern for constructing Span and Event objects.\n\"\"\"\n\nfrom datetime import datetime\nfrom typing import Any, "
  },
  {
    "path": "agentkit/trace/context.py",
    "chars": 12703,
    "preview": "\"\"\"\nContext management and utility functions for Span and Event.\n\"\"\"\n\nfrom contextlib import contextmanager\nfrom context"
  },
  {
    "path": "agentkit/trace/default.py",
    "chars": 657,
    "preview": "from dataclasses import dataclass\n\nfrom .local_tracer import LocalStorageTracer\nfrom .tracer import Tracer\n\n\n@dataclass\n"
  },
  {
    "path": "agentkit/trace/local_tracer.py",
    "chars": 6891,
    "preview": "import json\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Optional\n\nfrom .span import Event,"
  },
  {
    "path": "agentkit/trace/remote_tracer.py",
    "chars": 6763,
    "preview": "import logging\nfrom datetime import datetime\nfrom typing import Optional\n\nimport httpx\nfrom pydantic import BaseModel\n\nf"
  },
  {
    "path": "agentkit/trace/span.py",
    "chars": 4332,
    "preview": "from datetime import datetime\nfrom enum import Enum\ntry:  # Python 3.11+ has HTTPMethod in stdlib\n    from http import H"
  },
  {
    "path": "agentkit/trace/tracer.py",
    "chars": 890,
    "preview": "from abc import ABC, abstractmethod\nfrom typing import Optional\n\nfrom .span import Event, Span\n\n\nclass Tracer(ABC):\n    "
  },
  {
    "path": "agentkit/trace/types.py",
    "chars": 1539,
    "preview": "from pydantic import BaseModel\n\n\nclass Error(BaseModel):\n    code: int\n    message: str\n\n\n# class ModelParams(BaseModel)"
  },
  {
    "path": "config.yaml",
    "chars": 275,
    "preview": "# Global runtime defaults for StepDeepResearch.\n#\n# Used as the default context budget for the force-final-answer workfl"
  },
  {
    "path": "cortex/__init__.py",
    "chars": 52,
    "preview": "from .env import load_env as _load_env\n\n_load_env()\n"
  },
  {
    "path": "cortex/agents/__init__.py",
    "chars": 463,
    "preview": "\"\"\"Agent components module.\"\"\"\n\nfrom cortex.agents.base_agent import BaseAgent\nfrom cortex.agents.base_step_agent import"
  },
  {
    "path": "cortex/agents/agent_factory.py",
    "chars": 2082,
    "preview": "\"\"\"\nAgentFactory is the factory class for Agent, responsible for creating and managing Agents\n\"\"\"\n\nfrom typing import Aw"
  },
  {
    "path": "cortex/agents/base_agent.py",
    "chars": 9849,
    "preview": "\"\"\"Base Agent class, provides run() interface as the base class for all Agents.\"\"\"\n\nimport asyncio\nimport copy\nimport lo"
  },
  {
    "path": "cortex/agents/base_step_agent.py",
    "chars": 25075,
    "preview": "import copy\nimport json\nimport logging\nimport re\nfrom abc import abstractmethod\nfrom typing import Any, AsyncGenerator, "
  },
  {
    "path": "cortex/agents/checkpoint_agent/checkpoint_agent.py",
    "chars": 11989,
    "preview": "import logging\nfrom abc import abstractmethod\nfrom typing import AsyncGenerator\n\nfrom cortex.model.definition import Cha"
  },
  {
    "path": "cortex/agents/checkpoint_agent/checkpointer.py",
    "chars": 11514,
    "preview": "\"\"\"Checkpoint storage implementation.\"\"\"\n\nimport json\nimport logging\nimport os\nfrom pathlib import Path\nfrom typing impo"
  },
  {
    "path": "cortex/agents/checkpoint_agent/react_agent.py",
    "chars": 3643,
    "preview": "\"\"\"ReActAgent - An Agent specifically designed to execute tasks in ReAct (Reasoning + Acting) mode, capable of calling t"
  },
  {
    "path": "cortex/agents/input/input.py",
    "chars": 1303,
    "preview": "import asyncio\nimport logging\nfrom typing import Generic, TypeVar\n\nfrom pydantic import BaseModel\n\nlogger = logging.getL"
  },
  {
    "path": "cortex/agents/react_agent.py",
    "chars": 9308,
    "preview": "\"\"\"ReActAgent - Specialized Agent for executing ReAct pattern, capable of calling tools to complete tasks.\"\"\"\n\nimport lo"
  },
  {
    "path": "cortex/agents/types.py",
    "chars": 3167,
    "preview": "from enum import Enum\n\nfrom cortex.model.definition import ChatMessage, ModelParams\nfrom cortex.model.utils import merge"
  },
  {
    "path": "cortex/context/__init__.py",
    "chars": 505,
    "preview": "\"\"\"Context module for managing conversation and session context.\"\"\"\n\nfrom cortex.context.base_context import BaseContext"
  },
  {
    "path": "cortex/context/base_context.py",
    "chars": 882,
    "preview": "\"\"\"Base context management class.\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom typing import List\n\nfrom cortex.model.def"
  },
  {
    "path": "cortex/context/file_context.py",
    "chars": 4533,
    "preview": "\"\"\"File-based context management class.\"\"\"\n\nimport asyncio\nimport json\nimport os\nfrom pathlib import Path\nfrom typing im"
  },
  {
    "path": "cortex/context/simple_context.py",
    "chars": 760,
    "preview": "from typing import List\n\nfrom cortex.model.definition import ChatMessage\n\nfrom cortex.context import BaseContext\n\nsimple"
  },
  {
    "path": "cortex/env.py",
    "chars": 1307,
    "preview": "from __future__ import annotations\n\nimport os\nfrom pathlib import Path\n\n\ndef _repo_root() -> Path:\n    return Path(__fil"
  },
  {
    "path": "cortex/examples/agents/ask_input_agent.py",
    "chars": 7989,
    "preview": "\"\"\"AskInputAgent - Fixed-flow Agent for asking user input and repeating what the user says\"\"\"\n\nimport json\nimport loggin"
  },
  {
    "path": "cortex/examples/agents/deep_reasearch_agent.py",
    "chars": 4963,
    "preview": "\"\"\"DeepResearchAgent - Deep research Agent\"\"\"\n\nfrom cortex.agents.base_agent import BaseAgent\nfrom cortex.agents.react_a"
  },
  {
    "path": "cortex/examples/agents/main_agent.py",
    "chars": 2732,
    "preview": "\"\"\"MainAgent - Main coordination Agent.\"\"\"\n\nimport logging\nfrom uuid import uuid4\n\nfrom cortex.agents.base_agent import "
  },
  {
    "path": "cortex/examples/agents/math_agent.py",
    "chars": 5023,
    "preview": "\"\"\"MathAgent - An Agent that can solve mathematical problems.\"\"\"\n\nimport logging\nimport math\nfrom uuid import uuid4\n\nfro"
  },
  {
    "path": "cortex/examples/agents/plan_agent.py",
    "chars": 4665,
    "preview": "\"\"\"PlanAgent - Creates plans based on task objectives, and finalizes plans after user confirmation or modification\"\"\"\n\ni"
  },
  {
    "path": "cortex/examples/agents/search_agent.py",
    "chars": 1801,
    "preview": "\"\"\"SearchAgent - An Agent that can integrate search information\"\"\"\n\nimport logging\nfrom uuid import uuid4\n\nfrom cortex.a"
  },
  {
    "path": "cortex/examples/demo_agent_cli.py",
    "chars": 3080,
    "preview": "\"\"\"Demo Agent CLI - TUI interface built using TUI module to display AgentEvent.\"\"\"\n\nimport asyncio\nimport logging\nfrom p"
  },
  {
    "path": "cortex/examples/demo_agent_with_orchestrator.py",
    "chars": 5324,
    "preview": "\"\"\"Demo Agent with Orchestrator - Demonstrates how to use the Orchestrator pattern to coordinate multiple Agents\"\"\"\n\nimp"
  },
  {
    "path": "cortex/examples/demo_agent_with_tool.py",
    "chars": 4335,
    "preview": "\"\"\"Demo Agent with Tool implementation.\"\"\"\n\nimport argparse\nimport asyncio\nimport logging\nimport math\nimport random\nimpo"
  },
  {
    "path": "cortex/examples/demo_checkpoint.py",
    "chars": 7694,
    "preview": "import asyncio\nimport logging\nfrom uuid import uuid4\n\nfrom cortex.agents.agent_factory import AgentFactory\nfrom cortex.a"
  },
  {
    "path": "cortex/examples/demo_toolset_channel.py",
    "chars": 13393,
    "preview": "\"\"\"Channel usage example: Demonstrates complete async communication flow.\"\"\"\n\nimport asyncio\nimport logging\nimport time\n"
  },
  {
    "path": "cortex/examples/server.py",
    "chars": 2044,
    "preview": "import asyncio\nimport logging\n\nfrom agentkit.trace import LocalStorageTracer\n\nfrom cortex.agents.agent_factory import Ag"
  },
  {
    "path": "cortex/model/__init__.py",
    "chars": 3492,
    "preview": "\"\"\"Model system for Agent components.\"\"\"\n\nfrom enum import Enum\nfrom typing import AsyncGenerator\n\nfrom agentkit.trace i"
  },
  {
    "path": "cortex/model/definition.py",
    "chars": 3072,
    "preview": "from enum import Enum\nfrom typing import Any, Dict, List, Optional\nfrom typing_extensions import TypedDict\n\nfrom pydanti"
  },
  {
    "path": "cortex/model/provider.py",
    "chars": 615,
    "preview": "from abc import ABC, abstractmethod\nfrom typing import AsyncGenerator\n\nfrom .definition import ChatMessage\n\n\nclass Model"
  },
  {
    "path": "cortex/model/stepfun_chat.py",
    "chars": 6445,
    "preview": "\"\"\"StepFun Chat API Client with reasoning support.\"\"\"\n\nimport json\nfrom typing import Any, AsyncGenerator, Iterable\n\nimp"
  },
  {
    "path": "cortex/model/stepfun_provider.py",
    "chars": 21661,
    "preview": "\"\"\"StepFun Model Provider implementation.\"\"\"\n\nimport asyncio\nimport logging\nimport re\nfrom typing import Any, AsyncGener"
  },
  {
    "path": "cortex/model/utils.py",
    "chars": 3852,
    "preview": "import copy\n\n\ndef merge_delta_message(d1: dict | None, d2: dict | None) -> dict:\n    \"\"\"\n    Merge two message dictionar"
  },
  {
    "path": "cortex/orchestrator/__init__.py",
    "chars": 492,
    "preview": "\"\"\"Orchestrator module - Provides generator merging and agent coordination functionality.\"\"\"\n\nfrom .local_runner import "
  },
  {
    "path": "cortex/orchestrator/local_runner.py",
    "chars": 8298,
    "preview": "\"\"\"Local Runner implementation\"\"\"\n\nimport asyncio\nfrom typing import AsyncGenerator, Optional\nfrom uuid import uuid4\n\nfr"
  },
  {
    "path": "cortex/orchestrator/orchestrator.py",
    "chars": 12627,
    "preview": "\"\"\"Orchestrator for coordinating execution of multiple Agents.\"\"\"\n\nimport logging\nfrom enum import Enum\nfrom typing impo"
  },
  {
    "path": "cortex/orchestrator/remote_runner.py",
    "chars": 2940,
    "preview": "from typing import AsyncGenerator\n\nfrom cortex.agents.types import AgentConfig, AgentRunningStatus\nfrom cortex.orchestra"
  },
  {
    "path": "cortex/orchestrator/runner.py",
    "chars": 2176,
    "preview": "\"\"\"Runner interface definition.\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom typing import AsyncGenerator\n\nfrom cortex.a"
  },
  {
    "path": "cortex/orchestrator/types.py",
    "chars": 1529,
    "preview": "from enum import Enum\nfrom uuid import uuid4\n\nfrom cortex.model.definition import ChatMessage, Function\nfrom pydantic im"
  },
  {
    "path": "cortex/runtime_config.py",
    "chars": 2561,
    "preview": "from __future__ import annotations\n\nimport os\nfrom pathlib import Path\nfrom typing import Any\n\ntry:\n    import yaml\nexce"
  },
  {
    "path": "cortex/server/channel/channel.py",
    "chars": 445,
    "preview": "from abc import ABC, abstractmethod\n\n\nclass Channel(ABC):\n    def __init__(self, context_id: str) -> None:\n        self."
  },
  {
    "path": "cortex/server/channel/error.py",
    "chars": 46,
    "preview": "class ChannelClosedError(Exception):\n    pass\n"
  },
  {
    "path": "cortex/server/channel/memory_channel.py",
    "chars": 1182,
    "preview": "import asyncio\nfrom typing import Dict\n\nfrom loguru import logger\n\nfrom cortex.server.channel.channel import Channel\nfro"
  },
  {
    "path": "cortex/server/channel/ws_channel.py",
    "chars": 1757,
    "preview": "\"\"\"WebSocket channel implementation for agent communication.\"\"\"\n\nimport asyncio\nimport logging\nimport time\nfrom typing i"
  },
  {
    "path": "cortex/server/http_server.py",
    "chars": 8241,
    "preview": "import asyncio\nimport logging\nimport uuid\n\nimport uvicorn\nfrom agentkit.trace import SpanContext, Tracer\nfrom fastapi im"
  },
  {
    "path": "cortex/server/log/log.py",
    "chars": 1721,
    "preview": "# context.py\n\nimport logging\nimport os\nfrom datetime import datetime\nfrom pathlib import Path\n\nfrom pythonjsonlogger imp"
  },
  {
    "path": "cortex/server/log/trace.py",
    "chars": 497,
    "preview": "import logging\nfrom contextvars import ContextVar\n\ntrace_id_var: ContextVar[str | None] = ContextVar(\"trace_id\", default"
  },
  {
    "path": "cortex/tools/__init__.py",
    "chars": 586,
    "preview": "\"\"\"Tool system for Agent components.\"\"\"\n\nfrom .agent_tool import AgentTool\nfrom .base import Tool, ToolSchema\nfrom .chan"
  },
  {
    "path": "cortex/tools/agent_tool.py",
    "chars": 7167,
    "preview": "\"\"\"Agent Tool implementation - calls agents through Orchestrator.\"\"\"\n\nimport json\nimport logging\nfrom typing import Any,"
  },
  {
    "path": "cortex/tools/base.py",
    "chars": 2216,
    "preview": "\"\"\"Base Tool class.\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass, field\nfrom typing import "
  },
  {
    "path": "cortex/tools/channel.py",
    "chars": 5083,
    "preview": "\"\"\"Channel for async tool execution communication.\"\"\"\n\nimport asyncio\nfrom dataclasses import dataclass\nfrom enum import"
  },
  {
    "path": "cortex/tools/client_tool.py",
    "chars": 3296,
    "preview": "\"\"\"Client Tool implementation - uses Channel for communication.\"\"\"\n\nimport logging\nfrom typing import Any, Optional\n\nfro"
  },
  {
    "path": "cortex/tools/function_tool.py",
    "chars": 2006,
    "preview": "\"\"\"Function Tool implementation.\"\"\"\n\nimport inspect\nimport json\nfrom typing import Any, Callable, Optional\n\nfrom agents."
  },
  {
    "path": "cortex/tools/mcp.py",
    "chars": 1891,
    "preview": "import logging\nfrom contextlib import AsyncExitStack\nfrom typing import Any, final\n\nfrom mcp import ClientSession, Tool\n"
  },
  {
    "path": "cortex/tools/mcp_tool.py",
    "chars": 1842,
    "preview": "\"\"\"MCP (Model Context Protocol) Tool implementation.\"\"\"\n\nimport json\nfrom typing import Any\n\nfrom .base import Tool, Too"
  },
  {
    "path": "cortex/tools/session_tool.py",
    "chars": 2768,
    "preview": "\"\"\"Session Tool implementation.\"\"\"\n\nfrom typing import Any, Dict, Optional\n\nfrom .base import Tool, ToolSchema\nfrom .typ"
  },
  {
    "path": "cortex/tools/toolset.py",
    "chars": 9692,
    "preview": "\"\"\"ToolSet for managing and executing tools.\"\"\"\n\nimport logging\nfrom typing import Any, Awaitable, Callable, Dict, List,"
  },
  {
    "path": "cortex/tools/types.py",
    "chars": 861,
    "preview": "\"\"\"Tool types and enums.\"\"\"\n\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom typing import Any, Dict, Optio"
  },
  {
    "path": "cortex/tools/ublock_agent_tool.py",
    "chars": 6440,
    "preview": "\"\"\"Agent Tool implementation - calls agents through Orchestrator.\"\"\"\n\nimport json\nimport logging\nfrom typing import Any,"
  },
  {
    "path": "cortex/tools/unblock_client_tool.py",
    "chars": 2964,
    "preview": "\"\"\"Client Tool implementation - uses Channel for communication.\"\"\"\n\nimport logging\nfrom typing import Any, Optional\n\nfro"
  },
  {
    "path": "cortex/tui/__init__.py",
    "chars": 129,
    "preview": "\"\"\"Agent TUI module - Provides Textual-based TUI interface\"\"\"\n\nfrom cortex.tui.tui import AgentTUIApp\n\n__all__ = [\"Agent"
  },
  {
    "path": "cortex/tui/tui.py",
    "chars": 49398,
    "preview": "\"\"\"Agent TUI - TUI interface built with Textual and Rich, displaying AgentEvent\"\"\"\n\nimport asyncio\nimport json\nimport lo"
  },
  {
    "path": "cortex/utils/__init__.py",
    "chars": 77,
    "preview": "from .generator_merger import GeneratorMerger\n\n__all__ = [\"GeneratorMerger\"]\n"
  },
  {
    "path": "cortex/utils/generator_merger.py",
    "chars": 15534,
    "preview": "\"\"\"Generator merger implementation using asyncio.\"\"\"\n\nimport asyncio\nfrom collections.abc import Generator\nfrom typing i"
  },
  {
    "path": "cortex/utils/generator_merger_examples.py",
    "chars": 12722,
    "preview": "\"\"\"GeneratorMerger usage examples collection.\"\"\"\n\nimport asyncio\nimport time\n\nfrom .generator_merger import GeneratorMer"
  },
  {
    "path": "cortex-ui/.gitignore",
    "chars": 254,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
  },
  {
    "path": "cortex-ui/.gitlab-ci.yml",
    "chars": 1113,
    "preview": "variables:\n  APP: $CI_PROJECT_NAMESPACE\n  SERVER: $CI_PROJECT_NAME\n  VERSION: $CI_COMMIT_SHA\n  IMAGE_TAG_PROD: ${CI_COMM"
  },
  {
    "path": "cortex-ui/index.html",
    "chars": 365,
    "preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href"
  },
  {
    "path": "cortex-ui/package.json",
    "chars": 646,
    "preview": "{\n  \"name\": \"agent-cortex-frontend\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"bu"
  },
  {
    "path": "cortex-ui/src/App.tsx",
    "chars": 695,
    "preview": "import React from 'react';\nimport { BrowserRouter as Router, Routes, Route } from 'react-router-dom';\nimport { ConfigPro"
  },
  {
    "path": "cortex-ui/src/components/EndpointConfig.tsx",
    "chars": 2101,
    "preview": "import React, { useState } from 'react';\nimport { Input, Button, Typography, message } from 'antd';\nimport { EditOutline"
  },
  {
    "path": "cortex-ui/src/components/ErrorBoundary.tsx",
    "chars": 1158,
    "preview": "import React, { Component, ReactNode } from 'react';\nimport { Result, Button } from 'antd';\n\ninterface Props {\n  childre"
  },
  {
    "path": "cortex-ui/src/components/FilePanel.tsx",
    "chars": 16516,
    "preview": "/**\n * FilePanel Component\n * Display file tool execution results with elegant UI\n */\n\nimport React, { useState } from '"
  },
  {
    "path": "cortex-ui/src/components/FinalAnswer.tsx",
    "chars": 14171,
    "preview": "/**\n * FinalAnswer Component\n * Renders final answer with citations, supports Markdown and \\cite{} format\n */\n\nimport Re"
  },
  {
    "path": "cortex-ui/src/components/SearchResultsPanel.tsx",
    "chars": 9047,
    "preview": "/**\n * SearchResultsPanel Component\n * Display search results from batch_search tool\n */\n\nimport React, { useState } fro"
  },
  {
    "path": "cortex-ui/src/components/ShellPanel.tsx",
    "chars": 9784,
    "preview": "/**\n * ShellPanel Component\n * Display shell command execution results with elegant terminal-style UI\n */\n\nimport React,"
  },
  {
    "path": "cortex-ui/src/components/TodoPanel.tsx",
    "chars": 11400,
    "preview": "/**\n * TodoPanel Component\n * Display task list results from todo tool\n */\n\nimport React from 'react';\nimport { Card, Ty"
  },
  {
    "path": "cortex-ui/src/components/WebPagePanel.tsx",
    "chars": 19507,
    "preview": "/**\n * WebPagePanel Component\n * Display web page content from batch_open tool\n */\n\nimport React, { useState } from 'rea"
  },
  {
    "path": "cortex-ui/src/index.css",
    "chars": 469,
    "preview": "* {\n  margin: 0;\n  padding: 0;\n  box-sizing: border-box;\n}\n\nbody {\n  margin: 0;\n  font-family: -apple-system, BlinkMacSy"
  },
  {
    "path": "cortex-ui/src/main.tsx",
    "chars": 165,
    "preview": "import ReactDOM from 'react-dom/client';\nimport App from './App';\nimport './index.css';\n\nReactDOM.createRoot(document.ge"
  },
  {
    "path": "cortex-ui/src/pages/AgentList.tsx",
    "chars": 7188,
    "preview": "import React, { useEffect, useState } from 'react';\nimport { Card, Row, Col, Modal, Button, Spin, Typography, Alert, Dro"
  },
  {
    "path": "cortex-ui/src/pages/ChatPage.tsx",
    "chars": 62930,
    "preview": "import React, { useEffect, useState, useRef } from 'react';\nimport { useParams, useNavigate, useSearchParams } from 'rea"
  },
  {
    "path": "cortex-ui/src/services/api.ts",
    "chars": 4935,
    "preview": "import axios from 'axios';\nimport { AgentConfig, AgentEvent, ChatMessage } from '../types';\n\nconst DEFAULT_API_BASE_URL "
  },
  {
    "path": "cortex-ui/src/types/citation.ts",
    "chars": 826,
    "preview": "/**\n * Search citation related type definitions\n */\n\n// Single search result item\nexport interface SearchResultItem {\n  "
  },
  {
    "path": "cortex-ui/src/types/index.ts",
    "chars": 1899,
    "preview": "// Type definitions corresponding to backend\n\nexport interface ModelParams {\n  model_name: string;\n  api_key?: string;\n "
  },
  {
    "path": "cortex-ui/src/utils/citationParser.ts",
    "chars": 3591,
    "preview": "/**\n * Search citation parsing utilities\n * Parse citation info from batch_search tool XML results\n */\n\nimport { Citatio"
  },
  {
    "path": "cortex-ui/src/vite-env.d.ts",
    "chars": 163,
    "preview": "/// <reference types=\"vite/client\" />\n\ninterface ImportMetaEnv {\n  readonly VITE_API_BASE_URL?: string;\n}\n\ninterface Imp"
  },
  {
    "path": "cortex-ui/tsconfig.json",
    "chars": 563,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM."
  },
  {
    "path": "cortex-ui/tsconfig.node.json",
    "chars": 214,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\""
  },
  {
    "path": "cortex-ui/vite.config.ts",
    "chars": 519,
    "preview": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\nexport default defineConfig({\n  envDir: '."
  },
  {
    "path": "demo/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "demo/dr_agent/__init__.py",
    "chars": 150,
    "preview": "\"\"\"Demo agents package.\"\"\"\n\nfrom demo.dr_agent.dr_agent import get_dr_agent_config, make_dr_agent\n\n__all__ = [\"make_dr_a"
  },
  {
    "path": "demo/dr_agent/dr_agent.py",
    "chars": 6823,
    "preview": "\"\"\"Deep Research Agent\n\nConfigured with shell, web_surfer, and todo tools for executing deep research tasks.\n\"\"\"\n\nimport"
  },
  {
    "path": "demo/server.py",
    "chars": 1292,
    "preview": "import argparse\nimport asyncio\nimport logging\n\nfrom agentkit.trace import LocalStorageTracer\nfrom cortex.agents.agent_fa"
  },
  {
    "path": "demo/tools/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "demo/tools/batch_open.py",
    "chars": 5813,
    "preview": "import asyncio\nimport traceback\nimport uuid\nfrom datetime import datetime\nfrom pathlib import Path\nfrom typing import Ty"
  },
  {
    "path": "demo/tools/batch_search.py",
    "chars": 7619,
    "preview": "import hashlib\nimport uuid\nfrom datetime import datetime\nfrom pathlib import Path\nfrom urllib.parse import urlparse\n\nfro"
  },
  {
    "path": "demo/tools/batch_web_surfer.py",
    "chars": 3883,
    "preview": "import json\nfrom typing import Annotated, Literal\n\nfrom loguru import logger\n\nfrom cortex.model.definition import Conten"
  },
  {
    "path": "demo/tools/file.py",
    "chars": 7908,
    "preview": "\"\"\"File tool for local sandbox.\n\nProvides read/write/append/list/stat operations with an action + path schema.\n\"\"\"\n\nfrom"
  },
  {
    "path": "demo/tools/open.py",
    "chars": 3675,
    "preview": "import asyncio\nimport os\nfrom typing import Optional\nfrom urllib.parse import urlparse\n\nimport aiofiles\nimport aiohttp\ni"
  },
  {
    "path": "demo/tools/search.py",
    "chars": 3028,
    "preview": "import os\n\nimport aiohttp\nfrom loguru import logger\nfrom typing_extensions import NotRequired, TypedDict\n\n\nclass SearchR"
  },
  {
    "path": "demo/tools/shell.py",
    "chars": 6130,
    "preview": "\"\"\"Shell Tool - Execute Shell Commands\n\nSupports executing shell commands on macOS and Linux, returning structured outpu"
  },
  {
    "path": "demo/tools/text_truncator.py",
    "chars": 4406,
    "preview": "\"\"\"文本截断工具模块\n\n提供统一的文本截断功能,优化为默认使用句子或段落边界截断策略。\n\"\"\"\n\nimport re\nfrom dataclasses import dataclass\n\nDEFAULT_SUFFIX = \" ... [T"
  },
  {
    "path": "demo/tools/todo.py",
    "chars": 14226,
    "preview": "\"\"\"\nTodo Tool - Agent Task and Plan Management\n\nMinimalist design: read, write, and update operations through a single i"
  },
  {
    "path": "demo/tools/utils.py",
    "chars": 1512,
    "preview": "\"\"\"\nUtility functions for demo tools.\n\"\"\"\n\nimport json\nfrom typing import Any\n\nfrom pydantic import BaseModel\n\n\ndef json"
  },
  {
    "path": "pyproject.toml",
    "chars": 2100,
    "preview": "[project]\nname = \"stepdeepresearch\"\ndynamic = [\"version\"]\ndescription = \"\"\nauthors = [\n    {name = \"xieli\", email = \"xie"
  },
  {
    "path": "scripts/configs/prompt.py",
    "chars": 2177,
    "preview": "\"\"\"System prompt used by the runner scripts.\"\"\"\n\nSYSTEM_PROMPT = \"\"\"**任务目标:** 针对以下问题,请进行深入、详尽的调查与分析,并提供一个经过充分验证的、全面的答案。\n"
  },
  {
    "path": "scripts/configs/runner_example.yaml",
    "chars": 591,
    "preview": "# task: \"列出最近的 AI 安全新闻\"\n# task_id: custom_id           # optional override for the single task\ntasks_file: scripts/confi"
  },
  {
    "path": "scripts/configs/tasks.example.json",
    "chars": 186,
    "preview": "[\n  {\n    \"id\": \"demo_task_1\",\n    \"prompt\": \"我是个 AI 产品经理,正在考虑给产品增加个性化记忆功能。想系统了解一下现在 Agent 里的 Memory 模块都在往哪些方向发展,包括短期和长期"
  },
  {
    "path": "scripts/runner.py",
    "chars": 24494,
    "preview": "\"\"\"\nUtility script for running DeepResearchAgent on ad-hoc tasks or task lists.\n\nUsage examples:\n    # Run a single ad-h"
  }
]

About this extraction

This page contains the full source code of the stepfun-ai/StepDeepResearch GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 125 files (695.1 KB), approximately 166.5k tokens, and a symbol index with 741 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!