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.
Files changed (62) hide show
  1. qi_agent/__init__.py +18 -0
  2. qi_agent/abort.py +40 -0
  3. qi_agent/auth.py +216 -0
  4. qi_agent/cli.py +2178 -0
  5. qi_agent/compaction.py +475 -0
  6. qi_agent/config.py +409 -0
  7. qi_agent/docs/cli.md +188 -0
  8. qi_agent/docs/compaction.md +148 -0
  9. qi_agent/docs/configuration.md +89 -0
  10. qi_agent/docs/containerization.md +80 -0
  11. qi_agent/docs/docs.json +1 -0
  12. qi_agent/docs/environment-variables.md +47 -0
  13. qi_agent/docs/extensions.md +382 -0
  14. qi_agent/docs/how-qi-works.md +93 -0
  15. qi_agent/docs/index.md +61 -0
  16. qi_agent/docs/json.md +112 -0
  17. qi_agent/docs/keybindings.md +83 -0
  18. qi_agent/docs/models.md +138 -0
  19. qi_agent/docs/packages.md +155 -0
  20. qi_agent/docs/providers.md +288 -0
  21. qi_agent/docs/quickstart.md +109 -0
  22. qi_agent/docs/security.md +69 -0
  23. qi_agent/docs/session-format.md +299 -0
  24. qi_agent/docs/sessions.md +127 -0
  25. qi_agent/docs/settings.md +138 -0
  26. qi_agent/docs/skills.md +124 -0
  27. qi_agent/docs/slash-commands.md +55 -0
  28. qi_agent/docs/themes.md +78 -0
  29. qi_agent/docs/tui.md +109 -0
  30. qi_agent/docs/usage.md +112 -0
  31. qi_agent/export_html.py +95 -0
  32. qi_agent/extensions.py +2267 -0
  33. qi_agent/llm.py +609 -0
  34. qi_agent/loader.py +299 -0
  35. qi_agent/model_catalog.py +108 -0
  36. qi_agent/models.py +103 -0
  37. qi_agent/packages.py +430 -0
  38. qi_agent/paths.py +242 -0
  39. qi_agent/pep723.py +74 -0
  40. qi_agent/presets.py +293 -0
  41. qi_agent/prompt.py +150 -0
  42. qi_agent/registry.py +485 -0
  43. qi_agent/runner.py +584 -0
  44. qi_agent/runtime.py +1940 -0
  45. qi_agent/session.py +739 -0
  46. qi_agent/settings.py +501 -0
  47. qi_agent/share.py +111 -0
  48. qi_agent/system_prompt.py +278 -0
  49. qi_agent/theme.py +389 -0
  50. qi_agent/themes/dark.json +90 -0
  51. qi_agent/themes/light.json +89 -0
  52. qi_agent/titling.py +90 -0
  53. qi_agent/tools/__init__.py +349 -0
  54. qi_agent/tools/shell.py +276 -0
  55. qi_agent/trust.py +106 -0
  56. qi_agent/tui.py +6224 -0
  57. qi_agent/workspaces.py +116 -0
  58. qi_coding_agent-0.1.0.dist-info/METADATA +151 -0
  59. qi_coding_agent-0.1.0.dist-info/RECORD +62 -0
  60. qi_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  61. qi_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  62. qi_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,382 @@
1
+ # 扩展
2
+
3
+ 扩展是**一个 Python 模块 + 一个 `register(api)` 入口**。装进 qi 之后,它可以给模型加工具、订阅事件、加命令,以及自己画界面。
4
+
5
+ 能做什么:
6
+
7
+ | 想做 | 用什么 | 见 |
8
+ | --- | --- | --- |
9
+ | 给模型加工具 | `api.register_tool(...)` | §7 |
10
+ | 拦或改工具调用 | `api.on("tool_call" / "tool_result" / "tool_execution_*")` | §4 |
11
+ | 改系统提示词 / 注入消息 | `before_agent_start`、`api.send_message` / `send_user_message` | §4、§5 |
12
+ | 加斜杠命令 / 快捷键 / CLI 旗标 / 子命令 | `register_command` / `register_shortcut` / `register_flag` / `register_cli_command` | §5 |
13
+ | 问用户(确认 / 选择 / 输入 / 编辑器) | `ctx.ui.*` | §8 |
14
+ | 自定义界面 | `ctx.ui.custom` / `set_widget` / `set_footer`,以及工具与消息的渲染回调 | §8 |
15
+ | 存自己的状态 | `api.append_entry` / `set_label` / `set_session_name` | §5 |
16
+ | 起一个受管的子运行 | `api.run_agent(spec, task)` | §5 |
17
+ | 声明消费型配置(按作用域) | `provides_config` / `register_resolver` / `resolve_tools` | §9 |
18
+
19
+ 可跑的样例:[`examples/extensions/hello/`](../examples/extensions/hello/extension.py) —— 一份 `extension.py` 把主要的面各走一遍,复制它改名字就是你的扩展。`extensions/` 下的三个官方扩展是更完整的参考实现。
20
+
21
+ > 相关文档:[packages.md](packages.md)(安装与声明)、[how-qi-works.md](how-qi-works.md)(内置工具)、[settings.md](settings.md)(`extensions` / `packages` 字段)。
22
+
23
+ ## 1. 快速开始
24
+
25
+ ```python
26
+ # ~/.qi/agent/extensions/my-ext/extension.py
27
+ from qi_agent.extensions import Tool
28
+
29
+ def register(api):
30
+ # 1) 订阅事件:会话开始说一句(没有界面时自动落到启动提示)
31
+ api.on("session_start", lambda payload, ctx: ctx.ui.notify("my-ext 已装载"))
32
+
33
+ # 2) 拦住危险命令
34
+ def check_tool(payload, ctx):
35
+ if payload["tool_name"] == "bash" and "rm -rf" in str(payload["input"].get("command", "")):
36
+ return {"block": True, "reason": "被 my-ext 拦下"}
37
+ api.on("tool_call", check_tool)
38
+
39
+ # 3) 注册一个模型能调用的工具
40
+ async def greet(args, ctx):
41
+ return f"你好,{args['name']}!(工作目录:{ctx.workdir})"
42
+ api.register_tool(Tool(
43
+ name="greet",
44
+ description="跟某人打个招呼。",
45
+ parameters={"type": "object",
46
+ "properties": {"name": {"type": "string", "description": "称呼"}},
47
+ "required": ["name"]},
48
+ execute=greet,
49
+ ))
50
+
51
+ # 4) 加一条斜杠命令
52
+ api.register_command("hello", lambda args, ctx: ctx.ui.notify(f"hello {args}"),
53
+ description="示例命令")
54
+ ```
55
+
56
+ 试跑与确认:
57
+
58
+ ```bash
59
+ qi -e ./my-ext # 只本次运行(scope=temporary);TUI 与无头都生效
60
+ qi list # 看装了哪些扩展,以及与 settings.packages 的声明比对
61
+ qi doctor # 诊断:装载失败 / 依赖契约 / 声明不一致
62
+ ```
63
+
64
+ `register` 可以是 async —— 返回 awaitable 时宿主会等它跑完再继续启动。
65
+
66
+ **别在 `register` 里起后台资源**(进程、socket、watcher、定时器):有些调用根本不进会话,那些资源会没人收。要在 `session_start` 里起,并在 `session_shutdown` 里幂等收尾。
67
+
68
+ ## 2. 扩展放在哪、怎么加载
69
+
70
+ 一个扩展 = 一个目录 + 固定入口 `extension.py`(文件名固定,里面必须有 `register(api)`)。
71
+
72
+ | 通道 | 位置 |
73
+ | --- | --- |
74
+ | 用户级 | `~/.qi/agent/extensions/<名>/extension.py` |
75
+ | 项目级 | `<项目>/.qi/extensions/<名>/extension.py`(未信任不加载,见下) |
76
+ | 附加路径 | `settings.json` 的 `extensions[]`:每项可以是扩展目录的**父目录**,也可以直接指向**单个扩展目录**;支持 `-<路径>` 排除 |
77
+ | 单次试用 | `qi -e <目录>` / `--extension`(只本进程,`scope=temporary`) |
78
+ | pip 包 | entry point 组 `qi.extensions` |
79
+
80
+ 装载顺序:**项目级 → 用户级 → `settings.extensions[]` → entry point**,各组内按名排序。顺序确定,但**不是稳定 API** —— 需要保证先后关系的扩展应当写成一个扩展。
81
+
82
+ **装载失败是整体中断**:任何一个扩展在 `register` 里抛异常,这一轮装载就停下(不会"坏一个、其余照跑")。所以 `register` 要薄,重活留给事件。
83
+
84
+ **项目级要过信任门控**:项目目录与项目级 `settings.extensions[]` 只有在项目被信任后才装载 —— 扩展是仓库控制的任意代码。
85
+
86
+ - `qi -a` 信任本项目(本次运行),`qi -na` 明确不信任;两者同给报错退出码 2。
87
+ - 没表态时看**用户级** `settings.defaultProjectTrust`(`ask` / `always` / `never`);`ask` 在没有 UI(如 `-p`)时**保守判不信任**,并在 stderr 提示用 `-a`。
88
+ - 用户级扩展(以及 `-e` 给的)不受项目信任影响;`project_trust` 事件让它们参与这次投票 —— 项目级扩展自己不参与(否则仓库能给自己背书)。
89
+ - 跳过项目级资源时会有启动提示(`qi doctor` 也能看到原因)。
90
+
91
+ **关掉发现**:`--no-extensions`(=`-ne`)关掉 entry point + 两个内置目录 + `settings.extensions[]`;`-e` 显式给的仍然生效。
92
+
93
+ 安装、声明(`settings.packages`)与两条装法的失效面见 [packages.md](packages.md)。
94
+
95
+ ## 3. 可用的 import(公开面)
96
+
97
+ 扩展能 import 的公开面只有 `qi_agent.extensions`:
98
+
99
+ `ExtensionBus` · `ExtensionApi` · `ExtensionContext` · `EmitResult` · `ExecResult` · `ExtensionUi` · `SessionView` · `ModelView` · `ModelRegistryView` · `RendererRegistry` · `Tool` · `ToolError` · `ToolExecutor` · `ToolOutcome` · `register_tool` · `tool_info` · `CommandRegistry` · `FlagRegistry`
100
+
101
+ - `Tool` 就住在这一层 —— 要写工具就必然要这个类型;`registry` 仍**转发导入**它,所以旧写法不会断。
102
+ - 调用方的接口是 `register(api)` 拿到的那个 `ExtensionApi`;上面这些类型只在"要写工具 / 要造类型"时才需要 import。
103
+ - 其它 `qi_agent.*` 模块是内部实现,不承诺兼容。
104
+
105
+ ## 4. 事件(`api.on`)
106
+
107
+ `api.on(事件名, handler)`:handler 收 `(payload, ctx)`,可以同步或 async。返回值按事件分三类 —— **通知型**(忽略)、**可改**(补丁)、**裁决**(停链)。
108
+
109
+ **事件共 36 个。**
110
+
111
+ | 事件 | 时机 | 契约 |
112
+ | --- | --- | --- |
113
+ | `project_trust` | 首次会话绑定时(信任判定之前) | 返回 `{trusted: "yes"\|"no"\|"undecided"}` —— **首个 yes/no 拥有决定权**;`remember: true` 写进 `trust.json`;`undecided` 继续走 `trust.json` → `defaultProjectTrust`。**只有用户级/CLI 扩展参与**,给了 `-a`/`-na` 时不发 |
114
+ | `session_start` | 会话建立 / 载入 / 重载 | 通知。`{reason, session, cwd}`;`reason` ∈ `startup`/`reload`/`new`/`resume`/`fork`。**幂等**(同一会话至多一次) |
115
+ | `resources_discover` | `session_start` 之后 | 返回 `{skillPaths, promptPaths, themePaths}`。**qi 真的加载**:技能进 `top_skills`(给技能根或直接给 `<name>/` 都认),另两类路径进 `runtime.extra_resource_paths` |
116
+ | `input` | 收到用户输入(自动压缩之前) | `continue` / `transform`(改 **`text`**)/ `handled`(**首胜**,链停,`reply` = 给用户的答复) |
117
+ | `before_agent_start` | 用户消息**已落盘**后、agent loop 前 | `{prompt, system_prompt, agent}`;可链式改 `system_prompt`;`message` = 注入一条**持久**消息(字符串或 `{content}`) |
118
+ | `agent_start` / `agent_end` | 回合起止 | 通知。`{agent}` / `{agent, text, turns, aborted}` |
119
+ | `turn_start` / `turn_end` | 每轮(一次 LLM 响应 + 工具) | **通知**。`{turn_index, timestamp}` / `{turn_index, text, tool_calls}` |
120
+ | `context` | 每次 LLM 调用前 | `{messages}`:可换列表 / 裁剪。**要改就返回新列表**;就地改消息对象会污染跨轮上下文 |
121
+ | `message_start` / `message_update` / `message_end` | 消息生命周期(user / assistant / tool 都会发) | `message_end` 可改 `{message}` 的 **`content`**;`role` 与工具调用不可改(改了会让模型看到的与跑过的不一致) |
122
+ | `tool_call` | 工具执行前 | `{tool_name, tool_call_id, input}`;可改 `input`(真生效、**不重校**);`{block, reason?}` 拦住;handler 抛异常 **fail-safe 拦住** |
123
+ | `tool_execution_start` / `tool_execution_update` / `tool_execution_end` | 工具执行的生命周期 | **通知**。`{tool_call_id, tool_name, args}` / `+ partial_result`(工具调 `ctx.on_update(partial)` 触发)/ `{result, is_error, details}` |
124
+ | `tool_result` | 工具执行后 | `{tool_name, tool_call_id, input, result, details, status, exit_code}`;patch 语义:返回 `{result?, details?, status?}` |
125
+ | `before_provider_request` | 请求发出前 | `{payload}` —— **可整个替换**(返回 `{payload: <新 dict>}`) |
126
+ | `before_provider_headers` | 请求头组装后 | `{headers}` —— handler **原地改**(值为 `None` 则删掉该头) |
127
+ | `after_provider_response` | 拿到响应、还没消费流 | 通知。`{status, headers}`(取不到元数据时 `0` / `{}`) |
128
+ | `model_select` / `thinking_level_select` | 模型 / 思考级别变化 | 通知。`{model, previous, source}` / `{level, previous_level, source}`。**只从 runtime 发**(TUI 与扩展共用那一处) |
129
+ | `session_info_changed` | 会话改名 | 通知。`{name, source}`;`source` ∈ `user` / `auto` / `extension` |
130
+ | `session_before_switch` / `session_before_fork` / `session_before_tree` | 切会话 / fork / 树跳转**之前** | `{cancel: true}` 拦下。扩展调 `ctx.new_session()/fork()/navigate_tree()/switch_session()` **以及** TUI 的 `/new` `/resume <id>` `/fork` `/tree` 都走这道闸门 |
131
+ | `session_tree` | 树跳转之后 | 通知。`{newLeafId}` |
132
+ | `session_before_compact` / `session_compact` / `session_compact_failed` | 压缩 | `session_before_compact` 可 `{cancel}` 拦下、或 `{summary}` 自带摘要(自带时**不调模型**);成功后 `session_compact`;失败先发 `session_compact_failed`(`{error}`)再抛 |
133
+ | `session_shutdown` | 退出 / 换会话之前 | 通知。`{reason}` ∈ `quit`/`new`/`resume`。TUI 退出时发;`emit_session_shutdown()` 也可手动调 |
134
+ | `agent_settled` | 回合真的结束(无排队消息) | 通知。**保守近似**:provider 级重试在客户端内部发生,那时本方法还没跑完 |
135
+ | `ui_prompt_start` / `ui_prompt_end` | 阻塞式 UI 问答开始 / 结束 | 通知。`{reason: "ui_prompt", kind, title}`;`kind` ∈ `confirm`/`select`/`input`/`editor`/`custom` |
136
+ | `user_bash` | 用户 `!` / `!!` 命令 | `{command, excludeFromContext, cwd}`;返回 `{operations: {command}}` 换命令,或 `{result}` 直接给结果(不执行)。**TUI 专属**(无头模式没有 `!`) |
137
+
138
+ 被拦下的工具调用以 `status=error` + `error="denied"` 的结果还给模型(不是抛异常):
139
+ 模型看到"被拦了 + 原因"才能换个思路,而抛异常会把整个回合打成失败。
140
+
141
+ > **顺序**:`tool_execution_start`(带**未被闸门改过**的原始参数)→ `tool_call`(闸门在这里改)→ 真正执行。
142
+ > 两者不矛盾是因为**持久化来源是内部 `AgentEvent(kind="tool_start")`**(在闸门**之后**发),
143
+ > 不是这条扩展事件 —— 所以"回放里看到的参数不是跑过的"那个顾虑不成立。
144
+ >
145
+ > **payload 的键名用 snake_case**(`tool_name` / `system_prompt` / `turn_index`),方法名用
146
+ > snake_case、驼峰写法同样可用 —— 见 §5 开头的命名规则。写扩展时以本节为准。
147
+ >
148
+ > **兼容别名(两套都收)**:总线在派发前给每个 payload 补上 `type`(= 事件名,判别字段),
149
+ > 以及**同名同义**键的驼峰别名(`tool_name` → `toolName`、`tool_call_id` → `toolCallId`、
150
+ > `system_prompt` → `systemPrompt`、`turn_index` → `turnIndex` …)。qi 自己的键**一个不少**,
151
+ > 所以两套写法(`event.type` / `event.toolName` 或 snake_case)同时成立。
152
+ > payload **结构**不假造:如 `turn_end` 给的是 `text`/`tool_calls`。
153
+
154
+ ### 4.1 事件链语义(硬规则)
155
+
156
+ 不把这几条定死,每个扩展作者都会发明一套。规则如下(每条都有测试钉住):
157
+
158
+ | 规则 | 内容 | 实现 |
159
+ | --- | --- | --- |
160
+ | **顺序** | 按**扩展装载顺序**(项目目录 → 全局目录 → `settings.extensions[]` → entry point,各自按名排序)。写明“顺序确定,但不是稳定 API” | `ExtensionBus._handlers` 追加序;派发**串行**(不是 gather) |
161
+ | **快照遍历** | handler 里再 `on()` **不影响本次派发**(否则“注册一个自己”会让派发越跑越多) | 派发前取 `list(...)` |
162
+ | **链式 + patch** | 同类 handler 依次执行,后者看到前者**改过之后**的值;返回 `dict` 即浅合入 payload;payload 是**同一个 dict**,原地改也对后续可见 | `base.update(returned)` + 共享同一个 `base` |
163
+ | **裁决有两种形状** ⭐ | ① **键真值**:`tool_call` 的 `{block: True}`(`{block: False}` 不算,不能被假值短路)② **值在集合里**:`input` 的 `{action: "handled"}`。**必须按值判** —— `transform` 走的是同一个 `action` 键,但它是**链式**的(多级改写要接着往下走) | `emit_until(stop_keys=…, stop_values=…)` |
164
+ | **不重新校验** | `tool_call` 改完参数**不再过一遍 schema/权限** —— 这是**刻意的**(写进文档,否则会有人以为改了会被拦) | `tool_call` 闸门与执行之间没有第二道校验 |
165
+ | **不认识的返回值** | 忽略,不报错(扩展可能比宿主新 —— “扩展发了东西但没人看见”比多一个无害返回值难诊断得多) | 非 dict 返回只记录进 `returns` |
166
+ | **失败隔离** | 单个 handler 抛异常 → **记进 `EmitResult.errors` 并继续链,不中断会话** | `except Exception` + `continue` |
167
+ | **fail-safe(拦截器专用)** | 安全类事件用 `on_error_result` 兜底:**闸门自己崩了就拦住**。放行等于“装了闸门反而更不安全” | `on_error_result` 命中即裁决停链 |
168
+ | **异常要看得见** | `errors` 不能只存在对象里:`QiRuntime.start_session` 把它转成 `notes`(界面上的一行字)— 否则扩展坏掉的表现是“啥都没发生” | `notes` 通道 |
169
+ | **通知型事件** | `turn_start` / `model_select` 等的返回值被忽略 —— 写进文档,避免“我返回了为什么不生效” | 用 `emit()`(不给 `stop_*`);runtime 侧走 `_emit_notice`(后台任务,同步入口也能发) |
170
+
171
+ ### 4.2 事件粒度
172
+
173
+ - 每个用户 prompt 一次 `before_agent_start`(可换 system prompt、可注入消息)。
174
+ - 每轮 `turn_start` 只是通知。
175
+ - 每次 LLM 调用前 `context` 可改 messages。
176
+
177
+ 需要更细的粒度(比如"每轮换一套提示词 / 工具集")就用 `api.run_agent(spec, task)` 起一个**子运行**,而不是指望 hook —— 子运行有自己的提示词、工具集与模型。
178
+
179
+ ## 5. 方法(`api.*`)
180
+
181
+ **命名:snake_case 是正式名,驼峰是别名**(两套都指向同一个函数对象,
182
+ `api.registerTool is api.register_tool`)。**参数形状两边都收**:options 对象写法
183
+ (`registerCommand(name, {handler, description, getArgumentCompletions})`、
184
+ `registerFlag(name, {type, default})`、`registerTool({name, label, ..., execute})`)
185
+ 与关键字写法都认。
186
+
187
+ | 方法 | 作用 |
188
+ | --- | --- |
189
+ | `register_tool(tool)` / `registerTool(def)` | 注册工具(含**动态注册**:load 之后、事件里、命令里都能调,下一轮即可调)。收 `Tool`,也收 dict 形状(见 §7) |
190
+ | `add_tool(tool)` | v1 旧名,`register_tool` 的别名 |
191
+ | `get_all_tools()` / `get_active_tools()` / `set_active_tools(names)` | 读全部工具元数据(含 `source_info` + `promptGuidelines`/`sourceInfo` 键)/ 读启用集 / 改运行时工具集。**未知名字被过滤但会记进 `notes`** |
192
+ | `exec(cmd, args, opts)` | 起子进程(**不经 shell**,`args` 原样进 argv;`signal` / `timeout` 任一命中即 kill)。结果带 `command/args/code/stdout/stderr/killed/duration_ms` |
193
+ | `register_command(name, handler, opts)` | 斜杠命令;handler 收 `(args, ctx)`。**重名不覆盖**:变成 `name:1` / `name:2`。`getArgumentCompletions` **会收下并出现在 `get_commands()` 里**,但 TUI 补全尚未消费它 |
194
+ | `register_shortcut(key, handler, opts)` | 快捷键(key 用 textual 写法)。坏键名只提示,不让启动失败 |
195
+ | `get_commands()` | 命令清单。形状:`{name, description, source, sourceInfo}` + qi 的 `has_argument_completions` |
196
+ | `register_flag(name, opts)` / `get_flag(name)` | CLI 旗标。**值走 `--ext name=value`**。装载后对账:未注册名 / 字符串旗标缺值 → 退出码 2 |
197
+ | `send_message(msg, opts)` | 注入消息(**进 LLM 上下文**)。收字符串或 dict 形状 `{customType, content, display, details}`;`deliver_as` 三档(`steer`/`follow_up`/`next_turn`,驼峰别名 `followUp`/`nextTurn` 也认);`trigger_turn=True` = 空闲时也开一轮(前端领 `take_turn_request()`) |
198
+ | `send_user_message(content, opts)` | 同上,落盘标成"用户说的"(`injected_by`)。`trigger_turn` 缺省 **True** |
199
+ | `append_entry(customType, data)` | 落盘扩展自定义 entry(**不进 LLM 上下文**)。写口**只此一个**,章由宿主盖 |
200
+ | `set_session_name(name)` / `get_session_name()` / `set_label(entryId, label)` | 会话名与 entry label(`set_label` 落成 `{type: "label", targetId, label}`) |
201
+ | `set_model(model)` / `get_thinking_level()` / `set_thinking_level(level)` | 模型与思考级别。`set_model` 收 `"provider/model"` / `ModelView` / `{provider, id}`,返回 bool。**唯一入口是 runtime**(UI 与扩展共用,事件只发一次) |
202
+ | `register_provider(name, cfg)` / `unregister_provider(name)` | 动态注册/注销 provider。**只改内存,不写 `models.json`**;覆盖同名会记 note |
203
+ | `register_message_renderer` / `register_entry_renderer` / `register_markdown_transformer` | 渲染回调登记(`RendererRegistry`)。契约见 §8 |
204
+ | `events.on(channel, handler)` / `events.emit(channel, data)` | **扩展之间**的消息频道。`on` **返回退订函数**。与宿主事件是两套 API、同一总线对象 |
205
+ | `run_agent(spec, task, opts)` | 受管的**子运行**(不碰当前会话)。`spec = {system_prompt(必填), tools?(缺省**继承父**), model?, name?}` |
206
+ | `provides_config(kind, types)` | 消费型配置声明(见 §10) |
207
+ | `register_resolver(kind, fn)` / `resolve_tools(kind, scope=…)` | 配置种类 → 工具的一般机制(见 §10)。没人提供 → `[]` |
208
+ | `register_cli_command(name, handler)` | `qi <name> …` 子命令(选项表静态,各扩展自己解析 argv) |
209
+
210
+ ## 6. 上下文(`ctx`)
211
+
212
+ `ctx` 的 snake_case 是正式名,驼峰写法是别名(`ctx.hasUI` / `ctx.isIdle()` /
213
+ `ctx.getSystemPrompt()` / `ctx.newSession()` …),两套指向同一份实现。
214
+
215
+ | 成员 | 说明 |
216
+ | --- | --- |
217
+ | `ctx.ui` | **总是存在**(见 §8) |
218
+ | `ctx.mode` | `tui` / `rpc` / `json` / `print`。组件层 UI 用 `ctx.mode == "tui"` 门控 |
219
+ | `ctx.cwd` | 工作目录 |
220
+ | `ctx.model` | `ModelView` —— **`str` 子类**:`ctx.model == "provider/model"` 与 `ctx.model.id` / `.contextWindow` **同时**成立 |
221
+ | `ctx.thinkingLevel` / `ctx.thinking_level` | 当前思考级别 |
222
+ | `ctx.hasUI` / `ctx.has_ui` | 是否有人在看(TUI/web=真,`-p`=假) |
223
+ | `ctx.isProjectTrusted()` | 信任状态 |
224
+ | `ctx.signal` | **协作式中断信号** —— 扩展做异步时必须传它(Esc 才能取消 `fetch`/子进程) |
225
+ | `ctx.abort()` | 中断当前回合(`ctx.signal` 是**信号对象**,这里的是**方法** —— 两者名字接近,别取错;`ctx` 在两个位置上形状不同,见 §6.1) |
226
+ | `ctx.session_manager` | 当前会话的**只读**视图。读:`entries`/`custom_entries`/`get_entry`/`get_leaf_id`/`get_leaf_entry`/`get_branch`/`build_context_entries`/`get_header`/`get_tree`/`get_label`/`get_cwd`/`get_session_dir`/`get_session_file`/`get_session_name`;写只走 `api.append_entry`(以及 `api.set_label` / `api.set_session_name`) |
227
+ | `ctx.model_registry` | `ModelRegistryView`:`get_all` / `get_available` / `find` / `has_configured_auth` / `get_provider_display_name` / `register_provider` / `unregister_provider` |
228
+ | `ctx.scoped_models` / `scopedModels` | `--models` / `enabledModels` 圈定的模型(空 = 不限制) |
229
+ | `ctx.abort()` / `isIdle()` / `hasPendingMessages()` / `shutdown()` | 运行控制。`shutdown()` 需要前端注入退出实现,否则记一条 note |
230
+ | `ctx.compact(opts)` / `getContextUsage()` | 压缩(**触发即返回**,不等完成)/ 上下文占用 `{tokens, contextWindow, percent}` |
231
+ | `ctx.getSystemPrompt()` / `getSystemPromptOptions()` | 读当前提示词(**优先给本回合真正建好的那一份**,含 `before_agent_start` 的改写) |
232
+ | `ctx.waitForIdle()` | 等当前回合跑完 |
233
+ | `ctx.newSession()` / `fork(entryId)` / `navigateTree(targetId)` / `switchSession(path)` / `reload()` | 会话操作。前四个都过**可取消**的 `session_before_*` 闸门,返回 `{cancelled}`。`reload()` 重扫技能/提示词并重发 `resources_discover` + `session_start(reason="reload")`,**不重新 import 扩展模块**(Python 不保证安全重载) |
234
+
235
+ ### 6.1 `ctx` 有两个形状
236
+
237
+ 同一个 `ctx`,在两条路径上不是同一个类 —— 而它们**字段名不同**,只认其中一个的话,
238
+ 另一个形状上会静默拿到 `None`(不报错、不失效地"看起来在跑")。
239
+
240
+ | 路径 | 类型 | 中断信号 | 工作目录 | 问人 | 信任 |
241
+ | --- | --- | --- | --- | --- | --- |
242
+ | **handler**(`api.on(...)` / 命令) | `ExtensionContext` | `ctx.signal` | `ctx.cwd` | `ctx.ui.confirm`(async) | `ctx.project_trusted` |
243
+ | **工具**(`Tool.execute(args, ctx)` 的第 2 个参数) | `ToolContext` | `ctx.abort` | `ctx.workdir` | `ctx.ui.confirm`(async) | `ctx.project_trusted` |
244
+
245
+ 三条由此而来的写法规矩(都是踩过的):
246
+
247
+ 1. **取信号要两边都认**:`ctx.abort` 在 handler 侧是**方法**(`ctx.abort()`),`ctx.signal`
248
+ 在工具侧不存在。判据落在形状上(可调用的一律不认),不要只 `getattr` 一个名字 ——
249
+ 两次写错的版本都不抛错,症状只是"Esc 杀不掉子 agent"。
250
+ 2. **取目录同理**:`ctx.cwd` / `ctx.workdir` 各认一半。
251
+ 3. **项目级 prompt / 角色正文这类"仓库控制的内容"必须先过 `project_trusted`** ——
252
+ 两条路径都要。扩展工具想设这道门就得读 `ctx.project_trusted`。
253
+
254
+ 参考实现:`extensions/qi-agents/qi_agents/subagent.py` 的 `_abort_signal()` / `_cwd_of()`。
255
+
256
+ ## 7. 写一个工具
257
+
258
+ 工具 = 一个 `Tool`(或等价 dict),注册后进入模型的工具清单(下一轮生效)。
259
+
260
+ | 字段 | 必填 | 说明 |
261
+ | --- | --- | --- |
262
+ | `name` | 是 | 工具名(模型看到的名字;同一 catalog 内唯一) |
263
+ | `description` | 是 | 进 tool schema 的说明(模型"要不要调它"时看的就是这段,可以长一些) |
264
+ | `parameters` | 是 | 参数的 JSON Schema |
265
+ | `execute` | 是 | `async def execute(args, ctx) -> str \| ToolOutcome`。也收 `(tool_call_id, params, signal, on_update, ctx)` 那种五参形状(按形参个数适配) |
266
+ | `prompt_snippet` | 否 | 系统提示词「可用工具」那一行的摘要(省略则回落到 `description`) |
267
+ | `prompt_guidelines` | 否 | 该工具**被启用时**追加的指南 bullet;**必须自带工具名**(指南是平铺追加的) |
268
+ | `label` | 否 | 界面显示名(回落 `name`) |
269
+ | `prepare_arguments` | 否 | 拿到**原始**参数后、执行前的整理钩子;返回的 dict 才是执行用的那一份(也进 `tool_call` 事件的 `input`) |
270
+ | `render_call` / `render_result` / `render_shell` | 否 | TUI 自己画工具卡片(见 §8) |
271
+ | `execution_mode` | 否 | `"parallel"` = 可与同批其它 parallel 工具并发(见 §7.1) |
272
+ | `constrained_sampling` | 否 | **收下但忽略**(注册时记一条 note);逐工具受限采样要 provider 配合,liteLLM 没有可移植的对应物 |
273
+
274
+ ```python
275
+ from qi_agent.extensions import Tool, ToolError, ToolOutcome
276
+
277
+ async def read_issue(args, ctx):
278
+ if not args.get("id"):
279
+ raise ToolError("缺少 id") # → 以 error 结果返回给模型,不中断会话
280
+ ...
281
+ return ToolOutcome( # 结构化结果(可选)
282
+ result="…模型可见的文本…",
283
+ details={"issue": {...}}, # 给前端渲染用;**不进** LLM 上下文
284
+ exit_code=0,
285
+ )
286
+
287
+ api.register_tool(Tool(
288
+ name="read_issue",
289
+ description="按 id 读一个 issue。",
290
+ parameters={"type": "object", "properties": {"id": {"type": "string"}}, "required": ["id"]},
291
+ execute=read_issue,
292
+ prompt_snippet="读 issue",
293
+ prompt_guidelines=["需要 issue 内容时用 read_issue,不要猜。"],
294
+ ))
295
+ ```
296
+
297
+ - **错误**:抛 `ToolError`(或返回 `ToolOutcome(status="error", …)`)→ 变成模型可读的 error 结果,回合继续。
298
+ - **进度**:`ctx.on_update(partial)` 推 `tool_execution_update` 事件(TUI 会更新卡片)。
299
+ - **`ctx`**:`workdir`(会话目录;`ctx.guard(path)` 能校验"不越界")、`abort`、`ui`、`project_trusted`、`tool_call_id`、`session_env`。
300
+ - **动态注册**:装载后、事件里、命令里都能 `register_tool`,**下一轮**就能调。
301
+
302
+ ### 7.1 并发:`executionMode`
303
+
304
+ 工具的 `executionMode` 声明 `"sequential" | "parallel"`(默认顺序),语义如下:
305
+
306
+ | 规则 | 内容 |
307
+ | --- | --- |
308
+ | 默认 | 不声明 = 顺序 |
309
+ | 成批 | **相邻**的 parallel 工具并成一批并发跑(`asyncio.gather`) |
310
+ | 打断 | 一个顺序工具把批次**切断** —— 顺序工具永远不会与并行工具重叠(否则“我声明了顺序”就没意义) |
311
+ | 并发安全 | **由声明方负责**:写 `"parallel"` 等于说“我的实现是并发安全的”。qi 不做文件变更队列 |
312
+ | 顺序不变 | 事件与上下文顺序仍然确定:先把整批的 `tool_execution_start` 发完,再按**声明序**发 `tool_execution_end` / `tool_result` / 消息 |
313
+ | 没有 session 级默认 | 只认**逐工具**声明 |
314
+
315
+ 内部 `AgentEvent(kind="tool_start"/"tool_end")` **总是带 `tool_call_id`** —— 并发时前端工具卡片与落盘靠它把 start/end 配对;想自己配对的事件订阅者也照这个键来。
316
+
317
+ ## 8. 终端 UI 与渲染
318
+
319
+ 扩展能问用户、改状态、往界面里塞组件,以及换掉工具卡片 / 消息 / markdown 的渲染 —— 这些都走
320
+ `ctx.ui` 与渲染回调,**契约、方法清单、无后端行为与例子在 [终端 UI 组件](tui.md)**。
321
+
322
+ 一句话:**`ctx.ui` 总是存在**,没有前端时按调用方给的 `default` 回答(`notify` 落进启动提示),
323
+ 所以"交互"永远不会把回合挂住。
324
+
325
+ ## 9. 配置种类与作用域
326
+
327
+ 扩展之间共享同一份**目录语义**时,不必各自实现一遍目录遍历:
328
+ **定义 scope 的是目录的主家,读配置的是配置种类提供者。**
329
+
330
+ ```text
331
+ 定义 scope 的扩展: 扫 <scope 目录> → 得到一个 scope(名字 + 目录 + 信任状态)
332
+ 配置种类提供者: 按 scope 自取 —— "给我一个 scope,我读我要的那个文件"
333
+ ```
334
+
335
+ - **目录遍历只一处**(定义 scope 的那个扩展),其余扩展只实现"读自己那个文件";
336
+ - 声明方式:`api.provides_config(kind, types)`;没人提供该种类时**整个种类不装载**(静默跳过);
337
+ - core 侧的一般机制:`api.register_resolver(kind, fn)` / `api.resolve_tools(kind, scope=…)`,
338
+ 没人提供 → `[]`;
339
+ - 边界:**只在 scope 目录内**。全局(`~/.qi/agent/`)与项目(`<项目>/.qi/`)那两层的声明表沿用
340
+ 按作用域分层的旧规则(它们不是任何 scope 私有的)。
341
+
342
+ ## 10. 依赖契约
343
+
344
+ **安装目标:统一装进 qi 自己的解释器环境。** 目标是 qi 所在的那个 venv,**不是**用户 cwd 的项目 venv(否则就是那个经典失败:"我在项目里 pip install 了,qi 就是 import 不到")。装法就是往 `sys.executable` 对应的环境里 pip —— **这个动作 qi 不替你做**(见下)。
345
+
346
+ - **公开 import 白名单**:只能 import §3 列的那些名字。别的 `qi_agent.*` 模块算内部实现。
347
+ - **扩展不得把 `qi-coding-agent` 写进 `dependencies`**(否则 pip 会在解析时把 qi 自己降级 —— 宿主被自己的扩展踢掉)。
348
+ 装载时会读 pip 通道的 `requires()`(目录通道读 PEP 723 的 `# /// script` 声明),命中就**报告**(带版本满足判定与修复动作)。**只报告不拒绝** —— 声明本身不危险,危险的是被 pip 解成一棵冲突的树;拒载会让本来能跑的扩展直接不可用。判定按 PEP 503 归一(`Qi.Coding.Agent` / `qi_coding_agent` 都算),但 `qi-coding-agent-extra` 不算。
349
+ 另一条会单独报出来:**旧名 `qi-agent`** —— 那个名字在 PyPI 上属于别的项目,写进依赖不是"多写一条",
350
+ 而是会真的装一个不相干的包(还可能与 qi 的 import 包在 `site-packages` 里撞车)。
351
+ - **接受的代价**:所有扩展 + 宿主共用一棵解析树 → **版本冲突无处躲**。三层缓解:① 装载时比对已装版本,**不一致就报告**(不静默);② 冲突就拆成独立进程(自己的环境);③ 重依赖优先走外部进程/服务。
352
+ - **uv tool / pipx 的坑**:uv 文档原话 —— tool 环境 "may be upgraded via `uv tool upgrade`, or **re-created entirely** via subsequent `uv tool install`",所以 pip 装进去的扩展**会被重建抹掉**。规矩:声明永远在 `settings.packages`,用 `qi doctor` / `qi list` 在每次重建后**发现**它丢了(并给出装法);或者这类用户直接用 `uv tool install qi-coding-agent --with <包>`(写进 uv 的托管依赖,升级不丢 —— 这条更省事,推荐)。
353
+ - **宿主环境只读时**(系统 Python / Homebrew 管理的解释器):pip 会自己报错;出路是 `uv tool install qi-coding-agent --with <ext>`,或换一个可写的安装方式。qi 不检测这件事 —— 它不调 pip,所以也不该假装知道装不装得进。
354
+ - **`qi install <来源> [-l]`** 帮你走一步:调 pip + 写进 `settings.packages`(带 `-l` 写项目)。它**不替代**“想清楚装到哪个环境”这件事,所以每次都先把要跑的命令打出来。`qi remove` / `uninstall` 只从声明里移除(**不卸包**)。详见 [packages.md](packages.md) 与 [cli.md](cli.md)。
355
+
356
+ ## 11. 细节与已知限制
357
+
358
+ 已经全齐的面:事件 36 项、`api.*` 方法、`ctx` 成员、**`ctx.ui` 的全部方法**
359
+ (含组件层的 `set_widget` / `custom` / `set_footer` / `set_header` /
360
+ `set_editor_component` / `add_autocomplete_provider` / `on_terminal_input`)、
361
+ **渲染回调与工具渲染钩子(TUI 已消费)**、命令参数补全、`/resume` 选择器过闸门。
362
+
363
+ | 项 | 契约 |
364
+ | --- | --- |
365
+ | `ctx.ui.set_editor_component` 的**边界** | 要求组件是 **`TextArea`(或子类)**;不是则记 note 并拒绝。qi 的编辑器是承重的(历史环 / kill-ring / 补全 / 光标定位),把边界画在这里,15 处调用点就不必各自做兼容 —— 写一个 `TextArea` 子类仍然能做 vim 式模态编辑(拦截按键、自己改文本)。契约见 §8 |
366
+ | `ctx.ui.add_autocomplete_provider` 的**形状** | `factory(text, cursor_offset) -> list[str \| {value, label, description}]`;**返回退订函数**;与命令参数补全**同一套元素形状**,两种补全只学一次 |
367
+ | `ctx.ui.on_terminal_input` 的 `data` 改写 | 只支持 `{"consume": True}`(吃掉按键);不支持"换成另一个键" —— 需要改写按键的场景用 `set_editor_component` 接管编辑器(Textual 的 `events.Key` 不是为改写设计的) |
368
+ | `register_command` 的 `get_argument_completions` / `add_autocomplete_provider` 返回 **async** | **支持**(只是晚一拍):结果回来时若文本与光标未动就回填,动了就丢掉(补全主路径是同步的,async 走 worker + 一次重刷) |
369
+ | `constrainedSampling` | **收下但忽略**(注册时记一条 note):逐工具受限采样要 provider 配合(JSON schema / grammar),litellm 没有可移植的对应物。**收而不报才是最坏的**:扩展会以为参数被约束住了 |
370
+ | `user_bash` | 只在 TUI 发(无头模式没有 `!` 命令这回事) |
371
+ | `agent_settled` | 保守近似:qi 没有回合级重试/重压机制;provider 级重试在客户端内部,那时 `stream()` 还没返回 |
372
+ | 事件 payload 的**结构** | 键名双双可用(snake_case + 驼峰别名,见 §4),但**结构不假造**:如 `turn_end` 给 `{turn_index, text, tool_calls}` —— payload 面向本仓自己的前端词汇 |
373
+ | `deliver_as` 取值 | `follow_up` / `next_turn`(驼峰别名也收) |
374
+ | 设置类 entry 的键名 | `model_id`(本仓 entry 一律 snake_case:`custom_type` / `agent_id` / `duration_ms`) |
375
+ | `register_flag` 的取值通道 | 只能 `--ext name=value`(typer 的选项表静态,放宽未知长旗标会把用户的笔误变成一句 prompt) |
376
+ | 入口文件名 | 固定 `extension.py` |
377
+ | `add_route` / `add_static` | 未实现(HTTP 宿主的挂载点,不属现行扩展面) |
378
+
379
+ **另外几件已落地**:`renderShell`(见 §9)、`executionMode`(见 §7.1)、payload 的 `type` 字段与
380
+ 驼峰键别名、async 参数/补全提供者、**模型/级别的落盘 + 环境变量 + 续会话还原**(见
381
+ [session-format.md §6.1](session-format.md) —— 换模型是界面状态,不进上下文,
382
+ 所以 agent 只能靠 `QI_*` 环境变量自查)。
@@ -0,0 +1,93 @@
1
+ # qi 是怎么工作的
2
+
3
+ qi 把**模型请求、工具执行、上下文拼装、会话落盘**串成一条链。一次会话就是这条链留下的记录:
4
+ 消息、工具调用与结果、模型切换、压缩摘要都在里面。
5
+
6
+ 会话里的消息构成一棵**树**,树上每一条从根到某个节点的路径就是一条**分支**。当前节点所在的那条
7
+ 就是**活动分支**,它提供下一次模型请求的历史。
8
+
9
+ ## Agent loop
10
+
11
+ 用户消息落进活动分支。qi 用**系统提示词 + 活动分支 + 可用工具 + 模型设置**拼出一次请求,
12
+ 发给选定的 provider。
13
+
14
+ provider 流式返回助手回复(可能包含文本与工具调用)。qi 记录回复、执行每个工具调用、记录结果 ——
15
+ 这就是**一轮(turn)**。如果工具结果或排队消息还需要再问一次模型,就开下一轮;否则本次运行结束。
16
+
17
+ - **steer**(回合中按 `Enter`):当前回合结束后立刻作为下一回合发出。
18
+ - **follow-up**(回合中按 `Alt+Enter`):排在所有 steer 之后。
19
+ - **中止**(`escape`):协作式中断 —— 未执行的工具补上"已中断"结果、半截回答照常落盘,排队的消息退回编辑器。
20
+
21
+ ## 上下文
22
+
23
+ 活动分支提供对话历史:qi 把会话 entry 转成模型能吃的 user / assistant / tool-result 消息。
24
+ 系统提示词由**基座**加**动态块**拼成:
25
+
26
+ ```text
27
+ 基座 代码内默认,或被 <项目>/.qi/SYSTEM.md / ~/.qi/agent/SYSTEM.md 整体替换
28
+ 动态块 项目上下文(AGENTS.md…) → 技能清单 → 手册索引 → 当前工作目录
29
+ ```
30
+
31
+ - **技能**只把名字、描述、位置放进提示词;正文在任务匹配时由模型自己 `read`(渐进披露)。见 [skills.md](skills.md)。
32
+ - 没有能读文件的工具(`read` / `bash`)时,技能清单与手册索引**整块不注入**。
33
+ - 什么时候压、压哪一段、摘要长什么样,见 [compaction.md](compaction.md)。
34
+ - 自定义基座要注意:它会**整体替换**基座,连带「可用工具 / 指南」两块一起带走(见 [configuration.md](configuration.md))。
35
+
36
+ ## 会话
37
+
38
+ 持久会话是 JSONL 文件,每条 entry 带 `id` 与 `parentId`;当前节点决定活动分支。
39
+ 字段、entry 类型与落盘顺序见 [session-format.md](session-format.md)。
40
+
41
+ - **接着**旧节点继续提问 = 同一个文件里再开一条分支;`--fork` / `/fork` / `/clone` 则是**新文件**。
42
+ - 模型上下文**只从活动分支重建**;压缩会插入一条摘要 entry,在后续请求里替掉更早的消息 —— 原始 entry 仍留在树里。
43
+ - 会话默认**懒建**:`qi` / `/new` 先有对象、不写文件,第一条助手回答才落盘;`--no-session` 则是内存会话。见 [sessions.md](sessions.md)。
44
+
45
+ ## 界面
46
+
47
+ | 形态 | 命令 | 输出 |
48
+ | --- | --- | --- |
49
+ | 交互 TUI | `qi` / `qi "问题"` | 界面(默认 `fullscreen`:qi 拥有视口) |
50
+ | 无头一次 | `qi -p "问题"` | **只有答案**(进度走 stderr,`--verbose` 才显示) |
51
+ | 事件流 | `qi --mode json "问题"` | 一行一个 JSON 事件 → [json.md](json.md) |
52
+
53
+ 三者用的是**同一套** agent 与会话机制。qi 没有 RPC 模式与 SDK。
54
+
55
+ ## 工具
56
+
57
+ 工具是**代码**(全局注册进 `ToolCatalog`),技能与资产是**内容**。内置 9 个:
58
+
59
+ | 工具 | 作用 |
60
+ | --- | --- |
61
+ | `read` | 读文件(路径 + 行范围;超长截断并提示续读) |
62
+ | `ls` | 列目录 |
63
+ | `find` | 文件名搜索(名称 / glob) |
64
+ | `grep` | 文本搜索(正则 / 关键字 + 行号 + 上下文) |
65
+ | `write` | 整写文件(新建 / 覆盖) |
66
+ | `edit` | 精确替换(`old_text → new_text`;多处匹配报错) |
67
+ | `bash` | 执行命令(工作目录 = 会话 cwd;真正的 bash;输出截断 + timeout) |
68
+ | `powershell` | 执行 PowerShell(**仅 Windows**;非 Windows 调用时返回可照做的错误) |
69
+ | `clarify` | 向用户提一个澄清问题;没人可问时不挂住,返回"需要用户澄清"给模型 |
70
+
71
+ **工具集三态**(对齐 Claude Code):省略或 `["*"]` = 全部可用;显式名单 = allowlist 收窄;
72
+ 名单里有未知名 → 启动报错。会话级有四个旗标:`-t` 严格白名单 / `-xt` 排除 / `-nt` 全禁 /
73
+ `-nbt` 只去内置;`settings.defaultTools` 只挑**内置**那一档。见 [cli.md](cli.md)。
74
+
75
+ `bash` / `powershell` 的子进程还会拿到一组**会话环境变量**(`QI_SESSION_ID` / `QI_MODEL` …),
76
+ 这是 agent 自查"我现在跑的是什么模型"的唯一通道 —— 见 [environment-variables.md](environment-variables.md)。
77
+
78
+ `bash` **不做命令级过滤**;真边界只能来自容器 / VM / 受限用户,见 [security.md](security.md) 与
79
+ [containerization.md](containerization.md)。扩展可以注册自己的工具(收 `Tool`,也收 dict 形状),
80
+ 见 [extensions.md](extensions.md)。
81
+
82
+ ## 扩展与资源
83
+
84
+ 扩展是加载进 qi 进程的 Python 模块,能注册工具、命令、快捷键、旗标、provider、事件处理器与渲染回调。
85
+ 技能提供按需说明与附带文件;主题提供终端配色;扩展通过 pip 或本地目录分发(声明在 `settings.packages`)。
86
+
87
+ 见 [extensions.md](extensions.md)、[skills.md](skills.md)、[themes.md](themes.md)、[packages.md](packages.md)。
88
+
89
+ ## 信任与权限
90
+
91
+ 项目级资源(`.qi/extensions/`、项目级 `settings.extensions[]`)要**先过信任门控**才加载;项目级**技能**与
92
+ `AGENTS.md` 等文本指令不受门控。工具与扩展都以 **qi 进程本身的系统权限**运行 —— 门控管的是"加载什么",
93
+ 不是"能干什么"。见 [security.md](security.md)。
qi_agent/docs/index.md ADDED
@@ -0,0 +1,61 @@
1
+ # qi 文档
2
+
3
+ qi 是**可扩展的编码 agent 框架**:裸 core 就是单 agent,跑在终端里;角色、MCP、Web 都是扩展。
4
+ 给它一个目标和一个工作目录,它会读文件、跑命令、改内容,并一步步把任务做完。
5
+
6
+ 机器可读的导航清单是同目录的 [`docs.json`](docs.json)(与下面这些分类同源)。
7
+
8
+ ## 开始用 qi
9
+
10
+ 新装?跟着 [快速开始](quickstart.md) 装上、接一个模型、跑完第一个任务。
11
+
12
+ 已经装好了,按你想做的事选:
13
+
14
+ - [在终端里用 qi](usage.md) —— 交互模式、消息队列、导出结果。
15
+ - [选一个模型](models.md) —— 接厂商、接本地模型或兼容端点。
16
+ - [接着或分叉一条会话](sessions.md) —— 恢复工作,或在不丢历史的前提下换条路。
17
+ - [配置 qi](configuration.md) —— 偏好、工作目录、指令与可复用资源放在哪。
18
+ - [理解 qi 怎么工作](how-qi-works.md) —— 工具、上下文、会话与 agent loop。
19
+
20
+ ## 定制 qi
21
+
22
+ qi 能加载按需说明、加可执行集成、换配色,并把这些打包分发。
23
+
24
+ | 想做 | 看 |
25
+ | --- | --- |
26
+ | 改偏好(渲染模式、压缩、思考级别…) | [设置](settings.md)、[配置](configuration.md) |
27
+ | 加一份按需加载的能力说明 | [技能](skills.md) |
28
+ | 写扩展(工具 / 事件 / 命令 / 界面) | [写扩展](extensions.md)、[终端 UI 组件](tui.md) |
29
+ | 换配色 | [主题](themes.md) |
30
+ | 安装、声明、分发 | [扩展的安装与声明](packages.md) |
31
+
32
+ ## 自动化与嵌入
33
+
34
+ - [命令行](cli.md):`qi -p "…"` 无头一次拿结果,进度走 stderr。
35
+ - [JSON 事件流](json.md):把一轮里的每个事件按行喂给上层程序。
36
+
37
+ qi 没有 RPC 模式与 SDK —— `--mode rpc` 会明确报未实现(见 [PLAN.md](../design/PLAN.md) 的 v2 清单)。
38
+
39
+ ## 参考
40
+
41
+ 查表用:[命令行](cli.md) · [斜杠命令](slash-commands.md) · [设置](settings.md) ·
42
+ [环境变量](environment-variables.md) · [键位](keybindings.md) · [供应商与凭证](providers.md) ·
43
+ [会话文件格式](session-format.md) · [上下文压缩](compaction.md) · [JSON 事件流](json.md)。
44
+
45
+ ## 安全地使用
46
+
47
+ qi 的工具与扩展以 **qi 进程本身的权限**运行。信任门控只决定"要不要加载项目里的资源",**不是沙箱**。
48
+ 动不认识的仓库、扩展或无人值守的自动化之前,先读 [安全地运行](security.md);要真隔离看
49
+ [隔离运行](containerization.md)。
50
+
51
+ ## 读完还想改 qi 本身
52
+
53
+ | 去处 | 内容 |
54
+ | --- | --- |
55
+ | [development.md](../design/development.md) | 拉起来、测试、静态检查、仓库布局 |
56
+ | [PLAN.md](../design/PLAN.md) | 开发计划、阶段表、未决清单 |
57
+ | [overview.md](../design/overview.md) | 总设计:架构总览、既定决策索引、技术选型、目录布局 |
58
+ | [internals.md](../design/internals.md) | 实现细节与决策记录(手册之外的部分) |
59
+ | [extensions-design.md](../design/extensions-design.md) | 扩展化的阶段拆解、未定清单、决策编号表(E1–E28) |
60
+ | [pi-alignment.md](../design/pi-alignment.md) | 与上游 pi 的逐节对照 |
61
+ | [bash-allowlist.md](../design/bash-allowlist.md) | bash 策略的变更记录(为什么删掉首词白名单) |