qi-coding-agent 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qi_agent/__init__.py +18 -0
- qi_agent/abort.py +40 -0
- qi_agent/auth.py +216 -0
- qi_agent/cli.py +2178 -0
- qi_agent/compaction.py +475 -0
- qi_agent/config.py +409 -0
- qi_agent/docs/cli.md +188 -0
- qi_agent/docs/compaction.md +148 -0
- qi_agent/docs/configuration.md +89 -0
- qi_agent/docs/containerization.md +80 -0
- qi_agent/docs/docs.json +1 -0
- qi_agent/docs/environment-variables.md +47 -0
- qi_agent/docs/extensions.md +382 -0
- qi_agent/docs/how-qi-works.md +93 -0
- qi_agent/docs/index.md +61 -0
- qi_agent/docs/json.md +112 -0
- qi_agent/docs/keybindings.md +83 -0
- qi_agent/docs/models.md +138 -0
- qi_agent/docs/packages.md +155 -0
- qi_agent/docs/providers.md +288 -0
- qi_agent/docs/quickstart.md +109 -0
- qi_agent/docs/security.md +69 -0
- qi_agent/docs/session-format.md +299 -0
- qi_agent/docs/sessions.md +127 -0
- qi_agent/docs/settings.md +138 -0
- qi_agent/docs/skills.md +124 -0
- qi_agent/docs/slash-commands.md +55 -0
- qi_agent/docs/themes.md +78 -0
- qi_agent/docs/tui.md +109 -0
- qi_agent/docs/usage.md +112 -0
- qi_agent/export_html.py +95 -0
- qi_agent/extensions.py +2267 -0
- qi_agent/llm.py +609 -0
- qi_agent/loader.py +299 -0
- qi_agent/model_catalog.py +108 -0
- qi_agent/models.py +103 -0
- qi_agent/packages.py +430 -0
- qi_agent/paths.py +242 -0
- qi_agent/pep723.py +74 -0
- qi_agent/presets.py +293 -0
- qi_agent/prompt.py +150 -0
- qi_agent/registry.py +485 -0
- qi_agent/runner.py +584 -0
- qi_agent/runtime.py +1940 -0
- qi_agent/session.py +739 -0
- qi_agent/settings.py +501 -0
- qi_agent/share.py +111 -0
- qi_agent/system_prompt.py +278 -0
- qi_agent/theme.py +389 -0
- qi_agent/themes/dark.json +90 -0
- qi_agent/themes/light.json +89 -0
- qi_agent/titling.py +90 -0
- qi_agent/tools/__init__.py +349 -0
- qi_agent/tools/shell.py +276 -0
- qi_agent/trust.py +106 -0
- qi_agent/tui.py +6224 -0
- qi_agent/workspaces.py +116 -0
- qi_coding_agent-0.1.0.dist-info/METADATA +151 -0
- qi_coding_agent-0.1.0.dist-info/RECORD +62 -0
- qi_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- qi_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- qi_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# 会话文件格式
|
|
2
|
+
|
|
3
|
+
一次会话 = **一个 JSONL 文件**,一行一个 JSON 对象。格式是**树**(v2):每条 entry 带 `id` 与
|
|
4
|
+
`parentId`,「历史」= 从某个节点沿 `parentId` 回溯到根的那条链。所以同一个文件里可以并存多条
|
|
5
|
+
分支 —— 回到旧节点继续提问,新内容成为它的子节点,旧分支原样保留。
|
|
6
|
+
|
|
7
|
+
**事实源:`src/qi_agent/session.py`。**本文任何一处与代码不一致,以代码为准。
|
|
8
|
+
|
|
9
|
+
## 1. 文件与读写约定
|
|
10
|
+
|
|
11
|
+
- **位置**:`~/.qi/agent/sessions/<YYYYmmddTHHMMSS>_<id>.jsonl`
|
|
12
|
+
(`settings.json` 的 `sessionDir` 可覆盖目录;全局一份,不按项目分目录 —— 按项目分组靠 header 的 `cwd`)。
|
|
13
|
+
- **文件名里的 `<id>`** = header 的 `id`,12 位小写 hex(`uuid4().hex[:12]`)。
|
|
14
|
+
- **编码**:UTF-8,`ensure_ascii=False` —— 中文原样落盘,不是 `\uXXXX`。
|
|
15
|
+
- **读取**(`SessionStore._read`):逐行 `json.loads`;**空行跳过,坏行也跳过**。
|
|
16
|
+
一行损坏不会让整条会话读不出来 —— 「会话读不出来」比「丢一行」严重得多。
|
|
17
|
+
- **写入有三种**,语义不同:
|
|
18
|
+
- `append()` —— 追加一行,不重写文件;
|
|
19
|
+
- `save()` —— 整文件重写(改名、改标题、迁移、分叉都走它);
|
|
20
|
+
- `flush()` —— **整文件写出**("预留但还没落盘"的会话首次落盘,见 §9.1)。
|
|
21
|
+
例外:如果一个读入的老会话刚被内存补过链(`Session.migrated`),`append()` 也会先
|
|
22
|
+
**整文件重写** —— 否则新节点的 `parentId` 指向的那条链只存在于内存里,重开即断。
|
|
23
|
+
|
|
24
|
+
## 2. header(`entries[0]`)
|
|
25
|
+
|
|
26
|
+
元数据,**不参与树**:
|
|
27
|
+
|
|
28
|
+
| 键 | 说明 |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `type` | 恒为 `"session"` |
|
|
31
|
+
| `version` | `2` = 树格式;`1` = 线性旧格式。**坏值按 2 处理**(不因一行脏数据报错) |
|
|
32
|
+
| `id` | 会话 id |
|
|
33
|
+
| `title` | 会话标题 |
|
|
34
|
+
| `created_at` | `YYYY-MM-DDTHH:MM:SS`(本地时区) |
|
|
35
|
+
| `cwd` | 会话工作目录(绝对路径)。旧会话可能没有 → 首次使用时由 `ensure_cwd()` 回填并整文件重写 |
|
|
36
|
+
| `parentSession` | 本会话从哪个会话文件分叉出来(**只有** `/fork` `/clone` / `qi --fork` 会写)。会话选择器的树状视图按它缩进;老会话没有这个键 |
|
|
37
|
+
|
|
38
|
+
> **标题有两处**:`Session.title`(运行时读的)与 header entry 的 `title`(磁盘上的真相)。
|
|
39
|
+
> 只改一处会出现「列表里是新名、重开又变回旧的」,所以改名统一走 `SessionStore.set_title()`
|
|
40
|
+
> —— 它同时改内存、header、磁盘,写盘失败还会把内存**回滚**。
|
|
41
|
+
>
|
|
42
|
+
> **空标题与历史默认名 `tui` 都算「没有名字」**(`SessionStore`/`has_title()` 同一口径):
|
|
43
|
+
> 自动命名据此决定要不要起标题,会话选择器据此决定显示标题还是**回落第一句话**
|
|
44
|
+
> (`Session.display_label`:没名字就显示第一句话)。见 [sessions.md](sessions.md) §7。
|
|
45
|
+
|
|
46
|
+
## 3. entry 的公共字段
|
|
47
|
+
|
|
48
|
+
`append()` 会自动补三个字段(调用方显式给了就不覆盖):
|
|
49
|
+
|
|
50
|
+
| 字段 | 由谁补 | 说明 |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| `ts` | `append()` | 写入时间戳 |
|
|
53
|
+
| `id` | `append()` | 12 位 hex,树节点标识 |
|
|
54
|
+
| `parentId` | `append()` | 父节点 = **写入那一刻的当前节点**;根节点为 `null` |
|
|
55
|
+
|
|
56
|
+
## 4. 树语义
|
|
57
|
+
|
|
58
|
+
| 概念 | 定义 | 代码 |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| `tree_entries` | 参与树的 entry = 除 header 外的全部 | `Session.tree_entries` |
|
|
61
|
+
| `leaf` | 文件里**最后一条**带 `id` 的非 header entry | `Session.leaf` |
|
|
62
|
+
| `current` | 当前节点:**优先 `position`,否则 `leaf`** | `Session.current` |
|
|
63
|
+
| `branch(leaf=None)` | 从 `leaf`(默认 `current`)沿 `parentId` 回溯到根,**正序**返回 | `Session.branch()` |
|
|
64
|
+
| `children(id)` | 某节点的直接子节点(`id=None` = 根层),用于树渲染 | `Session.children()` |
|
|
65
|
+
| `branch_points` | 分叉点数 = 有 >1 个子节点的节点数 | `Session.branch_points` |
|
|
66
|
+
|
|
67
|
+
**`position` 是内存态,不落盘**:它表示「下一次提问挂在哪」,默认 `None`(即用 `leaf`)。
|
|
68
|
+
`/tree` 跳到旧节点时通过 `SessionStore.set_position()` 设定;此后新 entry 就挂在那个节点下,
|
|
69
|
+
于是文件里出现第二条分支。
|
|
70
|
+
|
|
71
|
+
**历史只走 `branch()`** —— 会话上下文、消息计数、回放都只看这条链,不看整个文件。
|
|
72
|
+
`entries` 则是**文件里的全部 entry**(含 header 与其它分支),两者不要混用。
|
|
73
|
+
|
|
74
|
+
**损坏时的退化**:悬空 `parentId`、成环、`current` 指向不存在的节点,`branch()` 都**不抛异常**
|
|
75
|
+
—— 成环时截断,认不出目标时退回文件顺序。理由同上:「读不出来」比「丢几条分支」严重。
|
|
76
|
+
|
|
77
|
+
## 5. entry 类型
|
|
78
|
+
|
|
79
|
+
qi 目前**写入** 8 类,另有 1 类**只读**。
|
|
80
|
+
|
|
81
|
+
| `type` | 谁写 | 关键键 | 说明 |
|
|
82
|
+
| --- | --- | --- | --- |
|
|
83
|
+
| `session` | `SessionStore.create()` | 见 §2 | header,不参与树 |
|
|
84
|
+
| `message` | `runtime._persist_final()` / 用户消息落盘 | `role` · `content` · `agent_id` · `usage`(仅助手) | 唯一**进 LLM 上下文**的类型 |
|
|
85
|
+
| `tool` | `runtime._persist_tool()` | `tool` · `args` · `status` · `duration_ms` · `exit_code` · `error` · `result` · `details` | 一次工具往返;结构化字段来自 `AgentRunner`,前端不必解析 `result` 字符串 |
|
|
86
|
+
| `custom` | `runtime._persist_thinking()` / `_persist_narration()` / `append_extension_entry()` | `custom_type` · `content` 或 `data` · `source`+`agent`(扩展写) | **不进上下文**(见 §6) |
|
|
87
|
+
| `compaction` | `compaction.compact()` → `Runtime.compact_session()` | `summary` · `firstKeptEntryId` · `tokensBefore` · `usage` | 一次压缩的产物;`firstKeptEntryId` 决定窗口起点 |
|
|
88
|
+
| `branch_summary` | `Runtime.summarize_branch_for_jump()` | `summary` · `fromId` · `usage` | `/tree` 跳分支时,把「被放弃的那段」压成摘要挂到新位置 |
|
|
89
|
+
| `model_change` | `Runtime.set_model()` → `SessionStore.set_context_setting()` | `provider` · `model_id` | **设置类**:记录"这时换成了哪个模型"。续会话时按它还原(见 §6.1);`_history()` 不读它 |
|
|
90
|
+
| `thinking_level_change` | `Runtime.set_thinking_level()` | `thinking_level` | **设置类**:同上,记思考级别。只在级别**真的变了**时才写 |
|
|
91
|
+
| `dispatch` | **无人写**(遗留) | `agent` · `display_name` · `confidence` · `reasoning` · `source` | v3 取消 auto 分派后不再产生;`cli.py` / `tui.py` / qi-web 仍**读取**它,以便旧会话能正确回放 |
|
|
92
|
+
|
|
93
|
+
`custom` 的两个来源形状不同,`source` 字段是区分点:
|
|
94
|
+
|
|
95
|
+
```jsonc
|
|
96
|
+
// core 写的(思考、叙述)
|
|
97
|
+
{"type": "custom", "custom_type": "assistant_thinking", "agent": "general", "content": "…"}
|
|
98
|
+
|
|
99
|
+
// 扩展写的(api.appendEntry;章由宿主盖,扩展只给 custom_type 与 data)
|
|
100
|
+
{"type": "custom", "custom_type": "plan_steps", "source": "qi-plan", "agent": "general", "data": {…}}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`append_extension_entry()` 是**扩展自定义 entry 的唯一写口**。**没有活动会话时它直接报错**
|
|
104
|
+
(回合外的写往往是"想存但存错地方"的第一步)。扩展自己拼 entry 迟早会出现两种形状,而回放与
|
|
105
|
+
诊断都得同时认两种,所以章由宿主盖。
|
|
106
|
+
|
|
107
|
+
> 用户消息的 `agent_id` 是"**将处理它的** agent",不是发言者(`"extension"` 表示这条是扩展注入的)。
|
|
108
|
+
|
|
109
|
+
## 6. 上下文组装:`custom` 为什么不进上下文
|
|
110
|
+
|
|
111
|
+
`Runtime._history()` 只看当前分支,且**只读 `message`**,装配规则是:
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
最近一次 compaction 的 summary(若有)
|
|
115
|
+
→ 从它的 firstKeptEntryId 起头的 message
|
|
116
|
+
→ 沿途的 branch_summary
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
思考与叙述做成 `custom` 而不是 `message` 是**刻意的**:它们在界面上要看得见(否则刷新/回放里
|
|
120
|
+
整段消失,直播与回放不一致),但**不能进 LLM 上下文**,否则每一轮都在给模型灌过程文本
|
|
121
|
+
(零提示词回归风险)。
|
|
122
|
+
|
|
123
|
+
### 6.1 设置类 entry:模型与思考级别
|
|
124
|
+
|
|
125
|
+
`model_change` / `thinking_level_change` 走的是**同一条取舍**:
|
|
126
|
+
|
|
127
|
+
- **不进上下文** —— `_history()` 只读 `message`,所以切换模型不会往提示词里加任何东西。
|
|
128
|
+
- **不在 `/tree` 默认视图里显示** —— 每一行都重复一遍"模型: x/y"只会淹没对话;
|
|
129
|
+
`all` 过滤档仍能看到它们。
|
|
130
|
+
- **但要在文件里** —— 两个理由:
|
|
131
|
+
1. **续会话时按它还原**。`Runtime.bind_session()` 读当前分支上最后一条同类 entry
|
|
132
|
+
(`session.context_settings()`),把模型与级别恢复过来,而不是回 settings 默认
|
|
133
|
+
(恢复失败就退回默认)。
|
|
134
|
+
还原前先问一句"拿得到 key 吗"(`resolve_key(...).ok`):
|
|
135
|
+
provider 还在、但密钥已删时**退回默认 + 记一条 note**,而不是发一次注定 401 的请求。
|
|
136
|
+
CLI 显式给的 `--model` / `--thinking` 是当次覆盖,**优先级高于**会话里记的旧值。
|
|
137
|
+
2. **时间线上留下事实**。回放/诊断里能看出"这一轮开始换的模型",而不是只有结果没有原因。
|
|
138
|
+
|
|
139
|
+
**为什么 agent 看不见切换动作**:换模型是**界面状态**,不作为消息进对话 —— 所以 agent
|
|
140
|
+
"知道自己现在是什么"却"不知道刚才换过"。要让它能自查,走会话环境变量
|
|
141
|
+
(`QI_PROVIDER` / `QI_MODEL` …),见 [environment-variables.md](environment-variables.md)。
|
|
142
|
+
|
|
143
|
+
**写入时机**(三处,都由 `Runtime` 收口,所以两个入口 —— TUI 的 `/model` 与扩展的
|
|
144
|
+
`api.setModel` —— 不会一个漏写一个重写):
|
|
145
|
+
|
|
146
|
+
| 时机 | 写什么 |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| 新会话绑定时 | 起点两条(只在**新**会话写) |
|
|
149
|
+
| 换模型 / 换级别 | 各自的 entry;**同值不重写**(否则每次启动都加两行) |
|
|
150
|
+
| 换模型触发级别自动调整 | 那条 `thinking_level_change` 也写(它是一次真实的切换) |
|
|
151
|
+
|
|
152
|
+
**回合外也能写**:`_active_session` 只在一个回合内有效,而 `/model` 发生在两次提问之间 ——
|
|
153
|
+
所以前端选完会话要调 `bind_session()`,写入点取 `_active_session or _bound_session`。
|
|
154
|
+
|
|
155
|
+
### 6.2 "哪个会话"必须按**会话**记,不能按"来过没有"
|
|
156
|
+
|
|
157
|
+
`bind_session()` 要服务一种 CLI/TUI 里少见、而 web 是常态的形态:**一个 runtime 服务
|
|
158
|
+
同一 cwd 下的多条会话**(`WebState.runtime_for` 按 cwd 缓存)。为此有三条记账规则,
|
|
159
|
+
每一条早先都写错过,而症状都是"**静默串会话**"(不报错,只是拿着另一条会话的设置跑):
|
|
160
|
+
|
|
161
|
+
| 记账 | 判据 | 写错的症状 |
|
|
162
|
+
| --- | --- | --- |
|
|
163
|
+
| 还原与否 | `_restored_session` = **上一轮绑的那个 id** | 用 `set[str]` 记"还原过的 id" → A → B → A 切回来**不再还原**,留在 B 的模型上 |
|
|
164
|
+
| 显式级别 | `_level_pins: set[session_id]`(**会话级**)+ `_level_pinned_cli`(整场运行) | 用一个 `bool` → 在 A 上设过一次,之后**任何**会话里记的级别都不再生效 |
|
|
165
|
+
| 还原失败 | 退回 `_default_llm_exec`(构造期那份) | 只记一条 note → 留在**上一个会话**的模型上,而 note 写着"已用默认模型继续" |
|
|
166
|
+
|
|
167
|
+
三条的判据可以合成一句:**"当前设置"是会话的属性,不是 runtime 的属性**。
|
|
168
|
+
runtime 上那份只是一个**缓存**,每次换会话都要用会话里记的 entry 重填。
|
|
169
|
+
|
|
170
|
+
两条边界(与上面三条配对,别把它们一起改坏):
|
|
171
|
+
|
|
172
|
+
- **同级重绑不还原**。web 每一轮开跑前都会 `bind_session(same_session)`,若同级也还原,
|
|
173
|
+
用户刚换的模型会被每一轮悄悄洗掉。
|
|
174
|
+
- **会话级的"显式设过"不拦还原**。会话里记的级别**就是**用户在那个会话上显式选的那个 ——
|
|
175
|
+
还原它不是在覆盖选择,而是在装回来。所以 `_level_pins` 只用在一处:换模型时的
|
|
176
|
+
`modelThinkingLevels` 自动联动(不该覆盖用户的显式选择)。**只有命令行那档**(`--thinking`
|
|
177
|
+
/ `--model p/m:级别`)才拦还原 —— 那是当次的显式覆盖,与 `--model` 同级。
|
|
178
|
+
|
|
179
|
+
**绑定必须是同步的**(TUI 这条线踩过一次):`_select_session()` 之后紧跟一句**同步**的
|
|
180
|
+
`_sync_model_from_runtime()`,而 `session_start` 走的是 async worker —— 若只靠 worker 里的
|
|
181
|
+
bind,续会话时界面读到的是 restore **之前**的模型。表现是"footer 显示 settings 默认、
|
|
182
|
+
请求却发会话里记的那个"(见 `tests/test_tui_model_restore.py`)。所以 `_select_session()`
|
|
183
|
+
自己先同步 bind(`_bind_current_session`),`/resume` / `/fork` / `/import` 也各自 bind。
|
|
184
|
+
web 端同理:每轮开跑前 `WebState.bind()`(`bind_session` + `start_session`),而
|
|
185
|
+
`POST /api/model` 只做同步的 `bind_session`(换一次模型不该顺手派发 `session_start`)。
|
|
186
|
+
|
|
187
|
+
**键名约定**:设置类 entry 的键是 `model_id`(本仓 entry 一律 snake_case,与 `custom_type` /
|
|
188
|
+
`agent_id` / `duration_ms` 一致)。
|
|
189
|
+
|
|
190
|
+
## 7. 封顶(会话文件不无界增长)
|
|
191
|
+
|
|
192
|
+
| 常量 | 值 | 作用于 |
|
|
193
|
+
| --- | --- | --- |
|
|
194
|
+
| `MAX_TOOL_ENTRY_CHARS` | 8000 | `tool.result` · 思考正文 · 叙述正文(超出后追加 `…(落盘已截断)`) |
|
|
195
|
+
| `MAX_TOOL_DETAILS_CHARS` | 8000 | `tool.details` |
|
|
196
|
+
|
|
197
|
+
`details` 超限时**不切字符串**(切 JSON 会得到非法 JSON),而是换成可渲染的标记:
|
|
198
|
+
`{"_truncated": true, "_full_chars": <原长度>}`。
|
|
199
|
+
|
|
200
|
+
## 8. v1 → v2 迁移与容错
|
|
201
|
+
|
|
202
|
+
`version: 1` 的线性文件(entry 无 `id`/`parentId`)在**读入时**于内存里补链:按文件顺序串成一条
|
|
203
|
+
链、补 `id`、把 header 的 `version` 改成 2(`SessionStore.migrate()`)。
|
|
204
|
+
|
|
205
|
+
- **读路径不写文件**:TUI 里的会话列表之类的只读操作不会改动老会话。
|
|
206
|
+
- 首次写入(`append` / `save`)时才会整文件落盘(加载阶段只在内存里迁移)。
|
|
207
|
+
- `migrated` 标志就是为此存在:它让 `append()` 知道"必须重写"(见 §1)。
|
|
208
|
+
|
|
209
|
+
## 9. 写路径与落盘顺序的不变量
|
|
210
|
+
|
|
211
|
+
**关于写路径:**
|
|
212
|
+
|
|
213
|
+
- **`position` 不落盘**。它只在内存里;重启后 `current` 退回 `leaf`。要持久化"当前在哪"就得
|
|
214
|
+
靠 `branch_summary` 这类 entry 本身挂在目标位置下(这也是 `summarize_branch_for_jump()`
|
|
215
|
+
先 `set_position()` 再 `append()` 的原因)。
|
|
216
|
+
- **分叉(`fork_at`)复制分支到新文件,并保留原 `id`/`parentId`**。链在新文件里自洽,
|
|
217
|
+
将来互相引用时不会指到另一个文件去。`entry_id=None` = 空历史(从第一条消息之前开始)。
|
|
218
|
+
|
|
219
|
+
**关于落盘时机**(§9.1 详述):新会话**预留时没有文件**,要到第一条助手回答才真正写出。
|
|
220
|
+
|
|
221
|
+
**关于落盘顺序**(回放器可以直接依赖这四条;它们都是为了"文件里的顺序 = 真实因果"):
|
|
222
|
+
|
|
223
|
+
- **用户消息立即落盘**,不等回合结束 —— 否则运行中刷新/断线就看不到自己说了什么。
|
|
224
|
+
|
|
225
|
+
### 9.1 会话的三态:落盘 / 预留 / 内存
|
|
226
|
+
|
|
227
|
+
`Session` 有两个独立标志位,组合出三种会话(不要拿 `path.exists()` 判 —— 那会在"文件刚被删掉"
|
|
228
|
+
时给出错误答案):
|
|
229
|
+
|
|
230
|
+
| | `ephemeral` | `unflushed` | 什么时候出现 | 行为 |
|
|
231
|
+
| --- | --- | --- | --- | --- |
|
|
232
|
+
| **已落盘** | False | False | `create()` / `--fork` / 从磁盘读入 | 正常追加/重写 |
|
|
233
|
+
| **预留** | False | True | 裸 `qi` 启动、`/new`(`reserve()`) | entries 照常攒;**`append` 只在出现第一条 assistant 时 `flush()`**;`set_title`(改名)也立刻 `flush()` —— 见下 |
|
|
234
|
+
| **内存** | True | — | `--no-session`(`ephemeral()`) | 永不写盘(append/save/set_title 全跳过) |
|
|
235
|
+
|
|
236
|
+
**为什么要"预留"而不是"启动就建文件"**:本机真实事故 —— `~/.qi/agent/sessions/` 攒了
|
|
237
|
+
3000+ 个只有一行的空会话,全部来自"进来看一眼就走"。同一根因在 web 端也有(见
|
|
238
|
+
[design/web.md](../design/web.md) §18.28 的"懒创建")。
|
|
239
|
+
|
|
240
|
+
**为什么留住会话对象、只懒文件**:构造时就 `newSession()`,
|
|
241
|
+
于是"当前会话"这个不变量始终成立 —— 历史、用量、扩展的 `ctx.session_manager`、`/session`
|
|
242
|
+
信息行、回合链路都照常工作。若改成"启动时没有会话",下游每一处都要判 None,而那些判空迟早漏一个。
|
|
243
|
+
|
|
244
|
+
**落盘用独占创建、拒绝覆盖**:`flush()` 以 `"x"`(即 `O_EXCL`)打开文件 —— 预留期间**不该**
|
|
245
|
+
存在文件,真有就说明路径撞了。会话是不可再生的用户数据,宁可报错也不能静默盖掉别人的会话。
|
|
246
|
+
(一次 `write` 写出全部 entry;中途崩溃会留下一个截断的文件,这与 `save()` 的风险相同,
|
|
247
|
+
这里没有额外加 fsync/临时文件 —— 不为一个极罕见的窗口加一层复杂度。)
|
|
248
|
+
|
|
249
|
+
**只问不答不落盘**:判据是"有没有 assistant 回答"(`_has_assistant`),不是"有没有 entry"。
|
|
250
|
+
一个没有回答的会话在 `/resume` 列表里没有价值,而文件一出现就会进列表。
|
|
251
|
+
|
|
252
|
+
**改名会立刻落盘(唯一一个「没有回答也写文件」的口子)**:`/name`(以及扩展的
|
|
253
|
+
`ctx.setSessionName` / 会话选择器的 `ctrl+r`)走 `set_title()`,而它承诺"内存 + header +
|
|
254
|
+
落盘三处一起改" —— 打在一个**预留中**的会话上时,那个 `save()` 曾经是空操作(文件推迟到第一条
|
|
255
|
+
回答),于是名字只活在内存里:`/name` 之后 `/new`(或退出)会连**会话带名字**一起消失,
|
|
256
|
+
而界面刚说过"会话名已设为 X"。所以命名一律 `flush()`:`create()` 那条"显式意图立刻落盘"的
|
|
257
|
+
规矩同样适用于"我给这个会话起了名" —— 一个名字总得有文件可落(`qi -n <名字>` 本来就立刻落盘)。
|
|
258
|
+
自动命名不会走这条路:它在回合末尾才落标题,而那会儿第一条 assistant 回答已经把会话写出去了。
|
|
259
|
+
|
|
260
|
+
**显式创建照旧立即落盘**:`-c` / `--session` / `--fork` / `-n` / `--session-id` /
|
|
261
|
+
web 的 `POST /api/sessions` 都走 `create()` —— 那是调用方明确要的东西,懒建只针对"什么都没说"。
|
|
262
|
+
|
|
263
|
+
- **取上下文在用户消息落盘之前**(`history = self._history(session)` 在前,`append` 在后)。
|
|
264
|
+
反过来写,本轮输入会在上下文里出现**两次**(两条一样的 user)。
|
|
265
|
+
- **思考先于回答落盘**:思考产生在这条助手消息之前,顺序反了回放就成了"先回答、再思考"。
|
|
266
|
+
- **宣布了工具调用的助手消息立刻落成 `custom`(叙述),不缓到下一轮**。缓冲的话它会被写在
|
|
267
|
+
它触发的工具卡片**之后**,回放顺序变成"工具卡 → 叙述",与真实因果相反。不带工具调用那条
|
|
268
|
+
由回合末尾的 `message` 代表,所以不会重复。
|
|
269
|
+
|
|
270
|
+
这四条与 §6 的上下文组装是两回事:那说的是"哪些 entry 进模型",这里说的是"它们按什么顺序
|
|
271
|
+
写进文件"。
|
|
272
|
+
|
|
273
|
+
## 10. 典型文件
|
|
274
|
+
|
|
275
|
+
```jsonc
|
|
276
|
+
{"type":"session","version":2,"id":"9f3a1c2b7d4e","title":"修一个解析 bug","created_at":"2026-09-20T10:28:03","cwd":"/Users/x/proj"}
|
|
277
|
+
{"type":"model_change","provider":"deepseek","model_id":"deepseek-chat","ts":"…","id":"9a1b…","parentId":null}
|
|
278
|
+
{"type":"thinking_level_change","thinking_level":"medium","ts":"…","id":"ab2c…","parentId":"9a1b…"}
|
|
279
|
+
{"type":"message","role":"user","content":"看看 src/parse.py 的 bug","agent_id":"general","ts":"2026-09-20T10:28:11","id":"a1b2c3d4e5f6","parentId":"ab2c…"}
|
|
280
|
+
{"type":"custom","custom_type":"assistant_thinking","agent":"general","content":"先读文件…","ts":"…","id":"b2c3…","parentId":"a1b2c3d4e5f6"}
|
|
281
|
+
{"type":"tool","agent":"general","tool":"read","args":{"path":"src/parse.py"},"status":"ok","duration_ms":12,"exit_code":null,"error":null,"details":null,"result":"…","ts":"…","id":"c3d4…","parentId":"b2c3…"}
|
|
282
|
+
{"type":"message","role":"assistant","content":"问题在偏移量计算。","agent_id":"general","usage":{"turns":1,"llm_calls":2,"prompt_tokens":3120,"completion_tokens":140,"total_tokens":3260,"context_tokens":3120},"ts":"…","id":"d4e5…","parentId":"c3d4…"}
|
|
283
|
+
{"type":"model_change","provider":"zhipu","model_id":"glm-4.6","ts":"…","id":"e5f6…","parentId":"d4e5…"}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
(字段顺序与省略号仅为可读性;真实文件里 `append()` 补的三个字段位置由调用点决定,解析不要依赖顺序。)
|
|
287
|
+
|
|
288
|
+
## 11. 稳定性
|
|
289
|
+
|
|
290
|
+
**可以依赖的**:`type` 取值集合(§5)· header 的 `id`/`version`/`cwd` · entry 的 `id`/`parentId`/`ts`
|
|
291
|
+
· `message` 的 `role`/`content` · `compaction` 的 `summary`/`firstKeptEntryId`
|
|
292
|
+
· `model_change` 的 `provider`/`model_id` · `thinking_level_change` 的 `thinking_level`(§6.1)。
|
|
293
|
+
|
|
294
|
+
**不要依赖的**:字段顺序 · 未知 `type` 的存在与否(旧会话里有已废弃的类型,新类型也会加进来)
|
|
295
|
+
· `custom_type` 的完整清单(扩展可以自定义)· `usage` 里字段是否齐全(provider 差异大;
|
|
296
|
+
缺失就是缺失,不补估值 —— 见 [sessions.md](sessions.md) §5)。
|
|
297
|
+
|
|
298
|
+
**读到不认识的 `type` 时按"照原样保留、能显示就显示"处理,绝不丢弃** —— 这是 qi 各处一致的原则
|
|
299
|
+
(与 `details["ui"]` 同一条),旧会话在新版本里仍要能回放。
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# 会话
|
|
2
|
+
|
|
3
|
+
会话是 qi 的工作单元:一次对话的历史、分支、工具往返、用量都在里面。**一条会话 = 一个 JSONL
|
|
4
|
+
文件**,格式与字段见 [session-format.md](session-format.md);本文讲怎么用它。
|
|
5
|
+
|
|
6
|
+
## 1. 存在哪
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
~/.qi/agent/sessions/<YYYYmmddTHHMMSS>_<12位id>.jsonl
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- **全局一份,不按项目分目录。** 按项目分组靠 header 里的 `cwd`(见 §4)。
|
|
13
|
+
- 目录可被 `settings.json` 的 `sessionDir` 覆盖。
|
|
14
|
+
- 会话 id = header 的 `id`,也是文件名的一部分。
|
|
15
|
+
|
|
16
|
+
## 2. 常用操作
|
|
17
|
+
|
|
18
|
+
| 目的 | 命令 |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| 新开一条(**说话才落盘**) | `qi` 或 `qi "第一个问题"` |
|
|
21
|
+
| 接着最近一条继续 | `qi -c` / `qi --continue` |
|
|
22
|
+
| 指定某条会话 | `qi --session <路径 / id 或前缀>` |
|
|
23
|
+
| 用精确 id(不存在则建) | `qi --session-id <id>` |
|
|
24
|
+
| 浏览并选一条恢复 | `qi -r` / `qi --resume`(**需要 TTY**;与 TUI 的 `/resume` 同一条路) |
|
|
25
|
+
| 从某条会话分叉出新会话 | `qi --fork <路径 / id 或前缀>` |
|
|
26
|
+
| 换会话目录 | `qi --session-dir <目录>`(优先于 `settings.sessionDir`) |
|
|
27
|
+
| 给会话起名 | `qi -n "名字"`(或 `--name`) |
|
|
28
|
+
| 不落盘(临时试一把) | `qi --no-session` |
|
|
29
|
+
| 导出成 JSONL 文件 | `qi --export out.jsonl`(可配 `--session`;省略则导出最近一条) |
|
|
30
|
+
| 列出 / 查看 / 重命名 / 删除 | TUI 的 `/resume` 选择器(`qi -r` 直接进;**没有对应的 CLI 子命令**)。选择器默认只看**当前目录**,`tab` 切全部 |
|
|
31
|
+
|
|
32
|
+
**`<id 或前缀>` 两种前缀都认**:header 的 `id` 前缀,以及文件名 stem 前缀(即含
|
|
33
|
+
`<时间戳>_` 的那一截)。**也接受文件路径** —— `qi --session ./some.jsonl` 会直接读那个文件。
|
|
34
|
+
`qi -c` 取的是**按文件修改时间**最新的一条,不是按 header 的 `created_at`。
|
|
35
|
+
|
|
36
|
+
## 3. 一条会话的边界
|
|
37
|
+
|
|
38
|
+
- **新会话懒建**:`qi` / `/new` 会**立刻有一个会话对象,但不建文件** —— 文件推迟到**第一条助手回答**
|
|
39
|
+
(只算路径、不写盘,真写盘在第一条助手回答)。所以"进来看看就退出"、
|
|
40
|
+
"问了句就被 Ctrl+C"都不留空会话。带显式意图的创建(`-c` / `--session` / `--fork` / `-n` /
|
|
41
|
+
`--session-id` / web 的 `POST /api/sessions`)**照旧立刻落文件** —— 懒建只针对"什么都没说"。
|
|
42
|
+
- `--no-session` 的会话**不落盘**:是**内存会话**(`session.ephemeral`),只在内存里跑完这一轮,
|
|
43
|
+
磁盘上一个字节都不留(拿它做一次性脚本很合适)。
|
|
44
|
+
- `qi --export` 是**导出后退出**:不会跑模型,只是把现有会话原样拷成一个可带走的 JSONL。
|
|
45
|
+
还没落盘的会话没有文件可导,TUI 会明说而不是抛 `Errno 2`。
|
|
46
|
+
- 删除会**直接 unlink 文件**,不可恢复(网页端删工作区时是同一语义:目录不动,只删会话文件)。
|
|
47
|
+
|
|
48
|
+
## 4. 工作目录与按项目分组
|
|
49
|
+
|
|
50
|
+
会话在被创建时把**工作目录**(绝对路径)写进 header 的 `cwd`。这是恢复时选对目录、以及前端
|
|
51
|
+
按项目分组列表的依据。
|
|
52
|
+
|
|
53
|
+
老会话(早期版本)的 header 里没有 `cwd`:首次被使用时由 `ensure_cwd()` **回填**并整文件重写一次,
|
|
54
|
+
此后不再进这个分支。回填时若 header 不合法(空文件 / 类型不对)就**不猜**,直接跳过 ——
|
|
55
|
+
给一个历史会话硬塞一个错误的工作目录,比留着空更糟。
|
|
56
|
+
|
|
57
|
+
## 5. 用量统计
|
|
58
|
+
|
|
59
|
+
TUI 里回放会话、以及网页端显示的用量,都来自 `session.py` 的 `usage_summary()`,它**读当前分支**
|
|
60
|
+
计算。口径如下:
|
|
61
|
+
|
|
62
|
+
| 字段 | 含义 |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| `turns` | 用户轮数 = 当前分支上 `role=user` 的消息条数 |
|
|
65
|
+
| `tools` / `tool_failures` | 工具往返次数,以及其中 `status=error` 的次数 |
|
|
66
|
+
| `steps` / `llm_calls` | 累计值,**只统计带 `usage` 的助手消息** |
|
|
67
|
+
| `prompt_tokens` / `completion_tokens` / `total_tokens` | 累计值,同上 |
|
|
68
|
+
| `context_tokens` | **最后一条**带该键的轮的值 —— 即"现在上下文里装着多少",**不是各轮相加** |
|
|
69
|
+
|
|
70
|
+
三条容易误解的地方,都是刻意的:
|
|
71
|
+
|
|
72
|
+
1. **`context_tokens` 不求和。** 它是那一轮最后一次 LLM 调用看到的 prompt 大小。把各轮相加会
|
|
73
|
+
得到一个随轮数虚增的天文数字。
|
|
74
|
+
2. **没有 usage 就不算 token。** 写入 usage 之前的老会话、以及被硬取消的轮只计入 `turns`,
|
|
75
|
+
token 记 0 而**不补估值** —— 没有就是没有。
|
|
76
|
+
3. **求和在后端做。** 前端一次只拿得到一个窗口(会话接口是分页的),长会话窗口前面还有几千条,
|
|
77
|
+
前端求和会**静默少算**。后端手上有整条分支,所以由它算。
|
|
78
|
+
|
|
79
|
+
`total_tokens` 在 provider 没给时用 `prompt + completion` 兜底(好过显示 0)。
|
|
80
|
+
|
|
81
|
+
## 6. 分支、`/tree` 与 fork
|
|
82
|
+
|
|
83
|
+
同一个文件里可以并存多条分支:**回到旧节点继续提问**,新内容成为它的子节点,旧分支原样保留。
|
|
84
|
+
机制(节点、`parentId`、`position`)见 [session-format.md](session-format.md) §4。
|
|
85
|
+
|
|
86
|
+
- **TUI 里 `/tree`** 跳到任意节点:此后提问挂在那个节点下,形成新分支。
|
|
87
|
+
- **跳分支时**,被放弃的那段会被压成一条 `branch_summary` 挂到新位置 —— 这样"跳回来"之后模型
|
|
88
|
+
仍知道另一条路上发生过什么,而不是凭空断掉。
|
|
89
|
+
- **`qi --fork <id>`** 把「源会话当前分支」**复制成一个新文件**,不是在同一文件里再开分支。
|
|
90
|
+
复制过去的 entry 保留原 `id`/`parentId`,链在新文件里自洽。
|
|
91
|
+
|
|
92
|
+
## 6.1 模型与思考级别跟着会话走
|
|
93
|
+
|
|
94
|
+
会话文件里记着**模型是什么时候被换掉的**(`model_change` / `thinking_level_change`,
|
|
95
|
+
见 [session-format.md](session-format.md) §6.1),所以:
|
|
96
|
+
|
|
97
|
+
- **续接会话(`-c` / `--session` / `/resume`)会恢复那次切换** —— 上次切到 glm,再打开还是 glm,
|
|
98
|
+
而不是回 `settings.json` 的默认模型。
|
|
99
|
+
- **还原前先看凭证**:记的那个 provider 已经没密钥(退出登录 / 环境变量没了)时,退回默认并
|
|
100
|
+
在启动提示里说明 —— 不会拿着一个注定 401 的模型继续跑。
|
|
101
|
+
- **CLI 显式给的赢**:`--model` / `--thinking`(含 `--model provider/id:级别`)是当次覆盖,
|
|
102
|
+
不会被会话里的旧值顶掉。
|
|
103
|
+
- **换模型与换级别在界面上照旧是即时生效的**(下一回合用到请求里);落 entry 是为了"下次打开"
|
|
104
|
+
与"回放里看得出切换点",不影响本回合行为。
|
|
105
|
+
- **界面显示的与发出去的一致**:TUI 的 footer 缓存(`_model` / `_thinking_level`)在选会话后
|
|
106
|
+
会跟 runtime 真在用的那一份**同步**;启动路径尤其要注意 —— 绑定必须是同步的,不能只依赖
|
|
107
|
+
`session_start` 那个 async worker(见 [session-format.md](session-format.md) §6.1)。
|
|
108
|
+
|
|
109
|
+
## 7. 自动命名
|
|
110
|
+
|
|
111
|
+
会话默认叫「未命名」。左栏一列「未命名」等于没有左栏 —— 分不清哪条是哪条。所以 qi 会用
|
|
112
|
+
**模型**给会话起一个短标题。机制如下(全在 `Runtime._stream_inner`):
|
|
113
|
+
|
|
114
|
+
| 项 | 行为 |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| 触发条件 | **这个会话还没有标题**(`not has_title(session.title)`)—— 新建的、以及本功能上线前建的老会话都算。注意 TUI 现在**不再**给新会话写默认名 `tui`(写死默认名的后果:那 2000+ 条老会话一律叫 `tui`,而自动命名因为"看起来有名字"永远不跑),`has_title()` 把历史默认名 `tui` 也算作"没有名字",所以存量会话同样会被补上标题 |
|
|
117
|
+
| 输入 | 会话**原本的第一句话**,不是这一轮说的话。否则老会话续聊时,一句「接着再补个测试」会把一个讲仓库结构的会话命名成「补充测试」 |
|
|
118
|
+
| 时机 | 与回合**并行**起(`asyncio.create_task`)—— **不拖首字延迟**;回合末尾才套用标题 |
|
|
119
|
+
| 被取消时 | 硬取消时连标题任务一起取消 —— 不能把它漏在后台,它还会往会话里写 |
|
|
120
|
+
| 用户改过名 | 不覆盖:`/name` 或选择器里 `ctrl+r` 改的名字(`source="user"`)优先级最高 |
|
|
121
|
+
| 还没起名时显示什么 | 会话选择器显示**第一句话**(`Session.display_label` 的回落)—— 命名调用失败也不会剩一排「未命名」 |
|
|
122
|
+
|
|
123
|
+
标题的读写都走 `SessionStore.set_title()`:它同时改内存、header entry、磁盘,**写盘失败还会把
|
|
124
|
+
内存回滚**。只改一处会出现"列表里是新名、重开又变回旧的",而那种不一致最难查。
|
|
125
|
+
|
|
126
|
+
手动改名会发 `session_info_changed` 事件,`source` 区分 `user`(`/name`)与 `auto`(自动命名)——
|
|
127
|
+
前端可以据此决定要不要覆盖用户已经改过的名字。
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# 设置(`settings.json`)
|
|
2
|
+
|
|
3
|
+
> 文件位置、分层与合并规则见下。
|
|
4
|
+
> 读写入口:`qi config`(见 [cli.md](cli.md));扩展声明在 `settings.packages` 与 `settings.extensions`(见 [extensions.md](extensions.md) §10)。
|
|
5
|
+
|
|
6
|
+
## 1. 位置与分层
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
<git根>/.qi/settings.json 项目覆盖(可提交共享)
|
|
10
|
+
~/.qi/agent/settings.json 全局
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
配对关系:全局比项目深一层(`~/.qi/agent/` ↔ `<项目>/.qi/`),因为全局侧是
|
|
14
|
+
"全部用户级状态的挂载点"。`QI_AGENT_HOME` 指向的就是全局 agent 目录本身。
|
|
15
|
+
|
|
16
|
+
## 2. 合并规则
|
|
17
|
+
|
|
18
|
+
- 键级**深合并**:项目覆盖全局;嵌套对象递归合并。
|
|
19
|
+
- **数组整体替换**(不逐项合并)—— 避免"想删一项却删不掉"。
|
|
20
|
+
- 未知字段原样保留(`extra=allow`),方便扩展与前后版本共存。
|
|
21
|
+
- 文件损坏/JSON 非法 → 启动报错(不静默降级),`qi doctor` 会指出具体文件。
|
|
22
|
+
|
|
23
|
+
```jsonc
|
|
24
|
+
// ~/.qi/agent/settings.json
|
|
25
|
+
{ "theme": "dark", "compaction": { "enabled": true, "reserveTokens": 16384 } }
|
|
26
|
+
|
|
27
|
+
// <项目>/.qi/settings.json
|
|
28
|
+
{ "compaction": { "reserveTokens": 8192 } }
|
|
29
|
+
|
|
30
|
+
// 结果:theme=dark, compaction={enabled: true, reserveTokens: 8192}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 3. 字段
|
|
34
|
+
|
|
35
|
+
### 已生效
|
|
36
|
+
|
|
37
|
+
| 字段 | 类型 | 默认 | 说明 |
|
|
38
|
+
| --- | --- | --- | --- |
|
|
39
|
+
| `defaultProvider` | string | — | 默认 provider(**唯一来源**;`models.json` 里的同名键不参与) |
|
|
40
|
+
| `defaultModel` | string | — | 默认模型 id,同上;缺任一项则启动失败并给出迁移命令 |
|
|
41
|
+
| `sessionDir` | string | — | 会话目录;相对路径按**各自 settings.json 所在目录**解析,支持 `~`;项目优先于全局 |
|
|
42
|
+
| `shellPath` | string | — | `bash` 用哪个 shell;空 = 按平台解析:`shellPath` → Windows 上 Git Bash 已知路径 → PATH 的 bash → `/bin/bash` → `sh`。指向不存在的文件会直接报错(不静默回退)。见 [how-qi-works.md](how-qi-works.md) |
|
|
43
|
+
| `defaultProjectTrust` | string | `ask` | 项目信任:`ask`(默认,现保守判**不信任**)/ `always` / `never`。**只从用户级读** —— 项目级写了会被忽略并提示(仓库不能自称可信) |
|
|
44
|
+
| `skills` | string[] | `[]` | 追加技能路径;支持 glob、`~`、相对路径与排除项(见 §4) |
|
|
45
|
+
| `skillsEnabled` | boolean | `true` | qi 扩展:关闭技能自动发现(等价 `--no-skills` CLI 开关) |
|
|
46
|
+
| `enableSkillCommands` | boolean | `true` | 把技能注册成 `/skill:<名>` 命令 —— 技能平时只进描述,这条是**强制加载全文**的入口。关掉则这批命令不存在 |
|
|
47
|
+
| `doubleEscapeAction` | string | `tree` | 空编辑器连按两次 `escape`:`tree`(默认)/`fork`/`none`;非法值按 `tree`。见 [slash-commands.md](slash-commands.md) |
|
|
48
|
+
| `hideThinkingBlock` | boolean | `false` | 启动时不展示思考块(`ctrl+t` 仍可切换) |
|
|
49
|
+
| `editorPaddingX` | number | `1` | 编辑器左右内边距(qi 视觉基线用 `1`) |
|
|
50
|
+
| `outputPad` | number | `1` | 助手输出的左侧缩进(`0` = 顶格) |
|
|
51
|
+
| `autocompleteMaxVisible` | number | `5` | 补全面板最多显示几行(候选本身不裁,面板内滚动) |
|
|
52
|
+
| `tuiMode` | string | `fullscreen` | TUI 渲染模式:`fullscreen`(qi 默认)= 备用屏、qi 拥有视口,滚轮只滚 transcript;`regular` = inline(不占全屏、滚动交给终端)。非法/缺失值按 `fullscreen`。理由见 [usage.md](usage.md)。启动时读;改完要重启(不想重启就用 `--tui-mode`) |
|
|
53
|
+
|
|
54
|
+
### 收下但按实际状态落地
|
|
55
|
+
|
|
56
|
+
这些字段都收下;下表逐条说明落地状态(**写了会不会生效**):
|
|
57
|
+
|
|
58
|
+
| 字段 | 用途 | 现状 |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| `theme` | 主题名(dark/light/auto) | TUI 已接:自动探测终端背景(OSC 11),失败落 dark;`QI_THEME` 可覆盖。见 [usage.md](usage.md) |
|
|
61
|
+
| `branchSummary` | 分支摘要:/tree 跳转时被放弃那段要不要压成摘要;`skipPrompt: true` = **不问也不摘要** | **已接**(`skipPrompt`):默认**先问一句**,默认答案「不摘要」;无前端时同样不摘要。`reserveTokens` **未接** —— qi 的摘要预算固定,不加不生效的旋钮 |
|
|
62
|
+
| `thinkingBudgets` | 每个思考级别的 token 预算;Anthropic/Google/Bedrock 原生用,OpenAI 兼容形态要靠每模型的 `compat.thinkingTokenBudgetField` | **部分已接**:**只对 Anthropic 形态**生效(litellm 只在那里有对应参数),且**不配就不带**;钳制到至少留 1024 token 给答案。OpenAI 兼容那半不做 —— qi 没有 compat 层 |
|
|
63
|
+
| `modelThinkingLevels` | `{}` | 按模型的思考级别:`{"provider/模型": "high"}`(也认裸模型 id)。四级优先:`--thinking` > `--model provider/id:<级别>` > 这里 > `defaultThinkingLevel`;换模型时自动采纳,但本会话显式设过(`/thinking`)就不覆盖 |
|
|
64
|
+
| `defaultThinkingLevel` | 默认思考级别 | 已接:`off`/`minimal`/`low`/`medium`/`high`/`xhigh`/`max`;**不配默认 `medium`**;TUI `shift+tab` 可运行时切换(`--thinking` 可覆盖)。见 [slash-commands.md](slash-commands.md) |
|
|
65
|
+
| `enabledModels` | 模型轮换(Ctrl+P) | 已接:`/scoped-models` 勾选后写回(全局设置);空 = 轮换 models.json 里全部。已记进 settings 但**现已不可用**的条目(provider 删了 / 凭证没了)在面板里画成删除线 + `[unavailable]`,取消勾选才移出 |
|
|
66
|
+
| `quietStartup` | 隐藏启动头 | 已接:不写 banner(含快捷键提示);启动提示(会话不存在之类)仍会出 |
|
|
67
|
+
| `defaultTools` | 初始内置工具集 | **已接**:只挑**内置**那一档,扩展装的工具照旧全留 —— 所以 `[]` = "不要内置、只留扩展"(与"没配"是两件事)。`--tools` / `-t` 等 CLI 旗标**压过**它 |
|
|
68
|
+
| `compaction` | `{enabled, reserveTokens(默认 16384), keepRecentTokens(默认 20000)}` | 已接:超 `contextWindow - reserveTokens` 自动压缩;`/compact` 手动压缩。见 [slash-commands.md](slash-commands.md) |
|
|
69
|
+
| `retry.provider` | `{timeoutMs, maxRetries}` | **已接**(请求级):→ litellm 的 `timeout`(秒)/ `num_retries`。毫秒→秒在代码里换算。这是**唯一**该管请求超时的地方 —— agent 层不再套 `asyncio.timeout`(旧行为会把整轮打成“执行超时”) |
|
|
70
|
+
| `retry.enabled` / `retry.maxRetries` / `retry.baseDelayMs` | 回合级重试 | 未接(失败回合退避重试)。已接的只是上面的 `retry.provider.*`;`maxRetryDelayMs` 无对应 litellm 参数,未映射 |
|
|
71
|
+
| `packages` | **扩展声明层**:这个环境该装哪些扩展 | **已接**:`qi list` / `qi doctor` 读它做「声明了没装 / 装了没声明」的双向比对,并输出可复制的装法。qi **不替你调 pip**(理由见 [extensions.md §11](extensions.md)),所以这里只是声明 |
|
|
72
|
+
| `extensions` | 额外扩展路径 | **已接**(P-E1):每一条可以是扩展目录的**父目录**,也可以直接指向**单个扩展目录**;支持 `!排除` / `-排除`(见 §4);项目级那份受信任门控 |
|
|
73
|
+
|
|
74
|
+
> 上表是**诚实清单**:写在文档里的是"已收下但未生效",避免用户以为写了就有效。
|
|
75
|
+
|
|
76
|
+
## 4. 资源路径解析
|
|
77
|
+
|
|
78
|
+
`skills`(以及将来其它资源字段)里的每一条按下列规则解析:
|
|
79
|
+
|
|
80
|
+
- 相对路径 → 相对**该条所在的 settings.json 目录**:`~/.qi/agent/settings.json` 里的
|
|
81
|
+
相对路径相对 `~/.qi/agent`,`<项目>/.qi/settings.json` 里的相对 `<项目>/.qi`。
|
|
82
|
+
→ 所以两个作用域各自写 `"skills": ["x"]` 指的是**两个不同的目录**,互不影响。
|
|
83
|
+
- `~` 展开为 home;绝对路径直接用;支持 glob(`*` / `?` / `[`)。
|
|
84
|
+
- 排除项:前缀 `!` 或 `-`;强制纳入:前缀 `+`。
|
|
85
|
+
|
|
86
|
+
同一套语法对 **`extensions`** 也生效(排除项作用于整个发现集)—— 所以内建目录
|
|
87
|
+
(`~/.qi/agent/extensions/`、`<项目>/.qi/extensions/`)里扫出来的扩展也能被单独关掉:
|
|
88
|
+
|
|
89
|
+
```jsonc
|
|
90
|
+
{ "extensions": ["-~/.qi/agent/extensions/some-ext"] } // 只关这一个
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
这正是 `qi config` 面板里“关”的写法(加一条否定项,**不是删条目**)—— 状态因此被记住,
|
|
94
|
+
面板才能区分“没配过”与“主动关了”。pip 装的包没有路径可否定,它用 `packages` 的**对象形态**:
|
|
95
|
+
|
|
96
|
+
```jsonc
|
|
97
|
+
{ "packages": [{ "source": "qi-mcp", "extensions": [] }] } // 这个包不贡献扩展
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
(注:glob 相对**该条所在的 settings.json 目录**解析 —— 写绝对路径可以,写**绝对 glob**不行。)
|
|
101
|
+
|
|
102
|
+
排除项作用于**整个发现集**,不只是数组里纳入的根 —— 因此默认目录里扫出来的技能也能单独关掉:
|
|
103
|
+
|
|
104
|
+
```jsonc
|
|
105
|
+
{
|
|
106
|
+
"skills": [
|
|
107
|
+
"~/my-extra-skills", // 追加一个根
|
|
108
|
+
"-skills/experimental", // 关掉 ~/.qi/agent/skills/experimental
|
|
109
|
+
"!~/noisy-skills" // 同样效果,`!` 与 `-` 等价
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
不存在的排除路径会被丢弃(运行时解析,没有目标可排除)。
|
|
115
|
+
|
|
116
|
+
## 5. 与技能发现的关系
|
|
117
|
+
|
|
118
|
+
`settings.json` 的 `skills` 只是技能来源之一,完整优先级与发现规则见
|
|
119
|
+
[skills.md](skills.md) §1。
|
|
120
|
+
|
|
121
|
+
## 6. `models-store.json`:qi 不使用
|
|
122
|
+
|
|
123
|
+
模型清单全部来自静态配置(`models.json` 加 litellm 的模型表),qi **没有**远程模型目录缓存 ——
|
|
124
|
+
`~/.qi/agent/models-store.json` 这个文件不存在,也不需要存在。
|
|
125
|
+
|
|
126
|
+
后果只有一条:新模型不会自己出现。`qi init --refresh <名>` 是拿厂商 `/models` 接口对齐本地清单的
|
|
127
|
+
入口(见 [providers.md](providers.md) §2.1)。
|
|
128
|
+
|
|
129
|
+
## 7. `qi config` 速查
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
qi config # 看:合并后的设置、来源文件、默认模型来源、顶层技能
|
|
133
|
+
qi config --json # 机器可读
|
|
134
|
+
qi config --set theme=light # 写全局(JSON 值解析,失败则当字符串)
|
|
135
|
+
qi config --set 'skills=["~/x"]'
|
|
136
|
+
qi config -l --set theme=dark # 写项目 <git根>/.qi/settings.json
|
|
137
|
+
qi config --unset theme # 删键(支持点号路径,如 compaction.enabled)
|
|
138
|
+
```
|