@tea-agent/loop-agent 0.39.0-next.10 → 0.39.0-next.12
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.
- package/CHANGELOG.md +14 -0
- package/dist/build-stamp.json +2 -2
- package/dist/worker/console/chat/mcp-inventory.js +221 -0
- package/dist/worker/console/chat/pi-runtime.js +144 -69
- package/dist/worker/console/chat/repo-walk.js +10 -1
- package/dist/worker/console/chat/routes.js +19 -8
- package/dist/worker/console/chat/turn-process.js +10 -10
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B1E-cK2K.js → abnfDiagram-N423BO3Z-CRpBFiPS.js} +1 -1
- package/dist/worker/console/static/assets/{arc-BGzjO9rd.js → arc-CFq00qel.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DLdhmjAc.js → architectureDiagram-T3A2C74G-Dk_urLIc.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-MYV8D865.js → blockDiagram-VBNYF7ZC-CLM5ocwZ.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BzDorEwd.js → c4Diagram-5PPSVZJV-CWYcPCa3.js} +1 -1
- package/dist/worker/console/static/assets/channel-Dpjb6BMm.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C1X8DD9m.js → chunk-2GRJ4B5K-XILOozrE.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-_b3zpUSR.js → chunk-2Q5K7J3B-M4A5lfjn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-B1nq6qNo.js → chunk-5RXB4S5H-CHq6wHgP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BHy2jQUr.js → chunk-5VM5RSS4-COevfhfp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BDoLBPGY.js → chunk-6Q2QTUOP-CN1D-iO4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-SDhf5BHa.js → chunk-GF5L2VYU-C6qAAYuy.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-B2X9WLQ5.js → chunk-JWPE2WC7-DzjF9LjT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Y1pEsL6r.js → chunk-KBJHAD2P-B4vicTaj.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BhlwtGnw.js → chunk-RYQCIY6F-D5nkzM8K.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-lFslfIU9.js → chunk-XXDRQBXY-CUgkbah9.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cv4CdaZH.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cv4CdaZH.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ms_QSyzO.js → cose-bilkent-JH36ORCC-faupYrw-.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DVBoJPNo.js → cynefin-VYW2F7L2-Bev9OTKA.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-ClesFZ8l.js → cynefinDiagram-MW4NZA55-A0iHnoJm.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CwcFFdXS.js → dagre-VZM6K2ZE-Bboc-8dL.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CK6fAQka.js → diagram-7IWD3JNH-CZcV9H_a.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CxBy8J_n.js → diagram-B4RE2ZJO-I1kV6LGQ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Cn6E66dO.js → diagram-LBJQPF4R-BaSON7dV.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Cep_FINW.js → diagram-Q27KOJAE-KUeIAdtX.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-gzv9ZY8Y.js → diagram-UB23O5K3-DylPuZu-.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-CZVlOlaj.js → ebnfDiagram-BXEA7PRR-DcWoMdm7.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-EGJvKAH5.js → erDiagram-JOGREHBK-BxJOdF_H.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DhfPgO79.js → flowDiagram-UKHOOZJN-DHxQgLL0.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BzJfLMVN.js → ganttDiagram-PKOTCBZU-CVz9xzIE.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-C5uHg3cm.js → gitGraphDiagram-DS77QQ5N-D4C2Yvbd.js} +1 -1
- package/dist/worker/console/static/assets/index-BA5-N5ER.css +1 -0
- package/dist/worker/console/static/assets/index-amy_wpJL.js +325 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BFEs1rSc.js → infoDiagram-6WML65LV-Dr3ejwyd.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DOY3aNcc.js → ishikawaDiagram-WSZJBQD7-2kJbNO0g.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CvEVBETJ.js → journeyDiagram-NVQOT4AX-CMfF4VjJ.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CvRuqo46.js → kanban-definition-27J2QSJJ-DNN-Q1z3.js} +1 -1
- package/dist/worker/console/static/assets/{linear-B8zO1HYx.js → linear-OB4Y8pOn.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-FLK4IOPu.js → mermaid.core-DFSjahzL.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CwQ-RWrj.js → mindmap-definition-FAOFIHXS-D1WNklJu.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-H_S6wWna.js → pegDiagram-VL7TDLO6-Be2wdQXD.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CZUOcDQQ.js → pieDiagram-7S7Q4E2Y-hhWe5f6C.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CeaPaSv9.js → quadrantDiagram-CIZ2JOQS-BT597AoO.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Cpcs9ua-.js → railroadDiagram-AXF67PYL-BjqVRkNc.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D0_Aeyk6.js → requirementDiagram-LRYGKXZP-D964n5d3.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-73kWjUq_.js → sankeyDiagram-W5VNT64P-_aPpr_hL.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-41K29Ot4.js → sequenceDiagram-SI44F4Z6-BY0YUKL1.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-D1nO0oh9.js → sizeCapture-X5ZJPWSS-CgpnvQ-_.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-C77XHgDB.js → stateDiagram-OKZ733FA-BfWR9Od2.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI--1Trd-FO.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DC0D7jEO.js → swimlanes-SLNWSIFB-CWDqlFt_.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BepA9ids.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BLDHd_Uf.js → timeline-definition-Z64GVDOM-DjURY-Nw.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-eh6Y_q96.js → vennDiagram-T6HMQDX7-D1nP5tpF.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Cj4Cy9tr.js → wardleyDiagram-T6FBY63Y-BfRbqp6h.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DUIjXY1m.js → xychartDiagram-ELKLHX3M-D8FzmnEz.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +36 -25
- package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +17 -4
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +28 -22
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
- package/dist/workflows/dag/contract-validator-registrations.js +2 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +40 -0
- package/dist/workflows/dag/init-hybrid.js +1 -1
- package/dist/workflows/dag/node-execution.js +15 -3
- package/dist/workflows/dag/types.js +1 -0
- package/package.json +1 -1
- package/dist/worker/console/static/assets/channel-jVvdIfCV.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-VqdMg0Mg.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-VqdMg0Mg.js +0 -1
- package/dist/worker/console/static/assets/index-0DWC8Vk9.js +0 -325
- package/dist/worker/console/static/assets/index-DAouGklf.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B9GimbZV.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-yxfxvdqm.js +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### 改进
|
|
6
|
+
|
|
7
|
+
- Operator Chat 对齐 pi-web:建会话不再传封闭 `tools` 白名单(避免 SDK 把扩展 MCP 工具滤出 registry),创建后 `bindExtensions` 发 `session_start`,再钉激活集。扩展注册的 MCP 工具可被模型调用;`apply_patch` / `full-tools` / `shell` / `coding-chat` 仍排除。
|
|
8
|
+
- 对话侧栏 Plugins 右侧新增只读 MCP 弹窗:展示 `mcp.json` 配置的服务,以及当前会话真正注册/激活的 MCP 工具;没有配置也没有工具时保持真实空态。
|
|
9
|
+
- 对话侧栏原 Packages 入口改名为 Plugins,与 pi-web / Pi 用户面一致;内部仍是 package inventory。
|
|
10
|
+
- 对话进行中底部上下文占用百分比保持常驻:token 用量事件不再冲掉上一帧的 contextUsage,并在回合中持续刷新。
|
|
11
|
+
- 上一轮大量 bash 结束后再发新消息,旧工具条不再整批挂到新气泡下面:无主工具不再丢进最新一组,历史按完整回合裁切,空助手气泡若还挂着工具会留下来当锚点。
|
|
12
|
+
- 侧栏文件树点击目录行直接插入 `@目录/` 引用(箭头单独负责展开/收起);输入框 @ 补全也列出目录,排在文件前面。
|
|
13
|
+
- 对话进行中点「停止」立即安静下来(乐观停止,对齐 pi-web):不再等 POST /abort 往返;后端 abort 等待从 8s 降到 2.5s,未终结时由状态对账收敛。
|
|
14
|
+
- 侧栏文件树实时刷新:agent 回合结束、浏览器标签页回到前台、手动点「刷新」时重拉根目录+全部已展开目录(此前手动刷新只拉根目录,子目录永远旧缓存)。
|
|
15
|
+
- Skills / Packages / Models 面板不再把 Pi 的 warning / collision 原文堆在列表上方:改成标题条一颗摘要芯片(如「14 条提示」),点开按「需要处理 / 名称不规范 / 同名未生效」分组,文案用自然语言,路径只显示末两段;名称不规范的技能在列表旁标「提示」,详情里说明「现在这样也能用」。加载失败仍单独成组,可点「再读一次」。
|
|
16
|
+
- Skills / Packages 双栏改为左右各自滚动;资源弹窗挂到 `document.body`,按顶部对话导航的实时高度在剩余视口居中,不再被 sticky 顶栏挡住上沿。
|
|
17
|
+
|
|
5
18
|
### 新增
|
|
6
19
|
|
|
7
20
|
- 前端 plan 唯一机器契约化(JSON-only):`frontend-plan-pi` / `frontend-plan-revision-pi` 输出收敛为「一行导语 + 唯一 fenced `json` 契约 + 唯一 `openspec-citations` 引用块」,彻底移除「可选 Markdown 说明」;plan.md(`contracts/frontend-plan.md`)改由 prewrite gate 用纯函数 `renderFrontendPlanMarkdown` 确定性渲染(不新增 DAG 节点/不新增 Pi 调用)
|
|
@@ -23,6 +36,7 @@
|
|
|
23
36
|
|
|
24
37
|
### 修复
|
|
25
38
|
|
|
39
|
+
- `frontend-plan-revision-pi` 节点级校验与 merge-patch 输出协议矛盾修复:新增 schemaId `frontend-implementation-contract-revision-patch-v1` 与格式级宽松校验 `validateFrontendRevisionPatchNodeOutput`(至少一个 `openspec-citations` 块 + 剥离引用块后恰好一个 JSON 对象,不做完整契约 schema 校验),revision 节点 `structuredContractOutput.schemaId` 切换(plan-pi 仍为 v1);node-execution 的 invalid-output retry 指令按 schemaId 分支,patch 协议提示 merge-patch delta + 引用块,消除「prompt 要求 patch、retry 要求完整契约」的 retryable 死循环;gate 仍是唯一权威合并/校验点(零改动)。ADR 0015 补「节点级校验」决策 7
|
|
26
40
|
- `frontend-implementation-contract.schema.json` 的 `uiComponentChoices` items 补 `if/then` 决策语义(`specified` ⇒ `specReference` 必填非 null;`new` ⇒ `specReference` 为 null/缺失);`frontend-plan-render.ts` 空 `section` 不再渲染尾随 `#`;ADR 0016 增补「门禁边界」小节
|
|
27
41
|
|
|
28
42
|
### 文档
|
package/dist/build-stamp.json
CHANGED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only MCP inventory for Operator Chat.
|
|
3
|
+
*
|
|
4
|
+
* Pi has no built-in MCP catalog. Console projects two real facts:
|
|
5
|
+
* 1. mcp.json server specs (configured, may or may not have registered tools)
|
|
6
|
+
* 2. tools already on the live session that look like MCP registrations
|
|
7
|
+
*
|
|
8
|
+
* Never fabricate servers or tools. Missing files stay empty.
|
|
9
|
+
*/
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
const MCP_CONFIG_NAMES = ["mcp.json"];
|
|
13
|
+
export function isMcpToolName(name) {
|
|
14
|
+
const lower = name.trim().toLowerCase();
|
|
15
|
+
return lower === "mcp" || lower.startsWith("mcp_") || lower.startsWith("mcp-");
|
|
16
|
+
}
|
|
17
|
+
export function inferMcpServerName(toolName, knownServers = []) {
|
|
18
|
+
const name = toolName.trim();
|
|
19
|
+
if (/^mcp$/i.test(name))
|
|
20
|
+
return undefined;
|
|
21
|
+
const rest = name.replace(/^mcp[_-]/i, "");
|
|
22
|
+
if (!rest || rest === name || knownServers.length === 0)
|
|
23
|
+
return undefined;
|
|
24
|
+
const ranked = [...knownServers].sort((left, right) => right.length - left.length);
|
|
25
|
+
const lower = rest.toLowerCase();
|
|
26
|
+
return ranked.find((server) => {
|
|
27
|
+
const key = server.toLowerCase();
|
|
28
|
+
return lower === key || lower.startsWith(`${key}_`) || lower.startsWith(`${key}-`);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function looksLikeMcpTool(tool) {
|
|
32
|
+
const name = typeof tool.name === "string" ? tool.name : "";
|
|
33
|
+
if (!name)
|
|
34
|
+
return false;
|
|
35
|
+
if (isMcpToolName(name))
|
|
36
|
+
return true;
|
|
37
|
+
const source = `${tool.sourceInfo?.source ?? ""} ${tool.sourceInfo?.path ?? ""}`.toLowerCase();
|
|
38
|
+
return source.includes("mcp");
|
|
39
|
+
}
|
|
40
|
+
function asRecord(value) {
|
|
41
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
42
|
+
? value
|
|
43
|
+
: undefined;
|
|
44
|
+
}
|
|
45
|
+
function readEnabled(entry) {
|
|
46
|
+
return entry.enabled !== false;
|
|
47
|
+
}
|
|
48
|
+
function readTransport(entry) {
|
|
49
|
+
if (typeof entry.transport === "string" && entry.transport.trim()) {
|
|
50
|
+
return entry.transport.trim();
|
|
51
|
+
}
|
|
52
|
+
if (typeof entry.type === "string" && entry.type.trim()) {
|
|
53
|
+
return entry.type.trim();
|
|
54
|
+
}
|
|
55
|
+
if (typeof entry.url === "string")
|
|
56
|
+
return "http";
|
|
57
|
+
if (Array.isArray(entry.command) || typeof entry.command === "string") {
|
|
58
|
+
return "stdio";
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
function readCommand(entry) {
|
|
63
|
+
if (typeof entry.command === "string" && entry.command.trim()) {
|
|
64
|
+
const args = Array.isArray(entry.args)
|
|
65
|
+
? entry.args.filter((item) => typeof item === "string").join(" ")
|
|
66
|
+
: "";
|
|
67
|
+
return args ? `${entry.command} ${args}` : entry.command;
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(entry.command)) {
|
|
70
|
+
return entry.command
|
|
71
|
+
.filter((item) => typeof item === "string")
|
|
72
|
+
.join(" ")
|
|
73
|
+
.trim();
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
function extractServerMap(parsed) {
|
|
78
|
+
const root = asRecord(parsed);
|
|
79
|
+
if (!root)
|
|
80
|
+
return {};
|
|
81
|
+
const mcpServers = asRecord(root.mcpServers);
|
|
82
|
+
if (mcpServers)
|
|
83
|
+
return mcpServers;
|
|
84
|
+
const mcp = asRecord(root.mcp);
|
|
85
|
+
if (mcp)
|
|
86
|
+
return mcp;
|
|
87
|
+
const servers = asRecord(root.servers);
|
|
88
|
+
if (servers)
|
|
89
|
+
return servers;
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
export function parseMcpConfig(raw, scope, configPath) {
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(raw);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const servers = [];
|
|
101
|
+
for (const [name, value] of Object.entries(extractServerMap(parsed))) {
|
|
102
|
+
if (!name.trim())
|
|
103
|
+
continue;
|
|
104
|
+
const entry = asRecord(value) ?? {};
|
|
105
|
+
const command = readCommand(entry);
|
|
106
|
+
const url = typeof entry.url === "string" ? entry.url.trim() : undefined;
|
|
107
|
+
servers.push({
|
|
108
|
+
name: name.trim(),
|
|
109
|
+
scope,
|
|
110
|
+
enabled: readEnabled(entry),
|
|
111
|
+
...(readTransport(entry) ? { transport: readTransport(entry) } : {}),
|
|
112
|
+
...(command ? { command } : {}),
|
|
113
|
+
...(url ? { url } : {}),
|
|
114
|
+
configPath,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return servers;
|
|
118
|
+
}
|
|
119
|
+
export function projectSessionMcpTools(tools, activeToolNames = [], knownServers = []) {
|
|
120
|
+
const active = new Set(activeToolNames.map((name) => name.toLowerCase()));
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
const projected = [];
|
|
123
|
+
for (const tool of tools) {
|
|
124
|
+
if (!looksLikeMcpTool(tool) || typeof tool.name !== "string")
|
|
125
|
+
continue;
|
|
126
|
+
const name = tool.name.trim();
|
|
127
|
+
if (!name || seen.has(name))
|
|
128
|
+
continue;
|
|
129
|
+
seen.add(name);
|
|
130
|
+
const server = inferMcpServerName(name, knownServers);
|
|
131
|
+
projected.push({
|
|
132
|
+
name,
|
|
133
|
+
description: typeof tool.description === "string" ? tool.description : "",
|
|
134
|
+
...(server ? { server } : {}),
|
|
135
|
+
source: tool.sourceInfo?.source ?? "",
|
|
136
|
+
scope: tool.sourceInfo?.scope ?? "session",
|
|
137
|
+
...(tool.sourceInfo?.path ? { path: tool.sourceInfo.path } : {}),
|
|
138
|
+
active: active.has(name.toLowerCase()),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return projected;
|
|
142
|
+
}
|
|
143
|
+
export function assembleMcpInventory(servers, tools) {
|
|
144
|
+
const byServer = new Map();
|
|
145
|
+
const unassigned = [];
|
|
146
|
+
for (const tool of tools) {
|
|
147
|
+
const key = tool.server?.toLowerCase();
|
|
148
|
+
if (!key) {
|
|
149
|
+
unassigned.push(tool);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const list = byServer.get(key) ?? [];
|
|
153
|
+
list.push(tool);
|
|
154
|
+
byServer.set(key, list);
|
|
155
|
+
}
|
|
156
|
+
const views = servers.map((server) => {
|
|
157
|
+
const matched = byServer.get(server.name.toLowerCase()) ?? [];
|
|
158
|
+
byServer.delete(server.name.toLowerCase());
|
|
159
|
+
const status = !server.enabled
|
|
160
|
+
? "disabled"
|
|
161
|
+
: matched.length > 0
|
|
162
|
+
? "loaded"
|
|
163
|
+
: "configured";
|
|
164
|
+
return {
|
|
165
|
+
name: server.name,
|
|
166
|
+
scope: server.scope,
|
|
167
|
+
status,
|
|
168
|
+
enabled: server.enabled,
|
|
169
|
+
...(server.transport ? { transport: server.transport } : {}),
|
|
170
|
+
...(server.command ? { command: server.command } : {}),
|
|
171
|
+
...(server.url ? { url: server.url } : {}),
|
|
172
|
+
configPath: server.configPath,
|
|
173
|
+
tools: matched,
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
for (const [name, matched] of byServer) {
|
|
177
|
+
views.push({
|
|
178
|
+
name,
|
|
179
|
+
scope: "session",
|
|
180
|
+
status: "loaded",
|
|
181
|
+
enabled: true,
|
|
182
|
+
tools: matched,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return { servers: views, unassignedTools: unassigned };
|
|
186
|
+
}
|
|
187
|
+
async function readConfigFile(filePath) {
|
|
188
|
+
try {
|
|
189
|
+
return await readFile(filePath, "utf8");
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
export async function loadMcpServerSpecs(options) {
|
|
196
|
+
const candidates = [
|
|
197
|
+
{ path: path.join(options.agentDir, ...MCP_CONFIG_NAMES), scope: "user" },
|
|
198
|
+
{ path: path.join(options.cwd, ".pi", ...MCP_CONFIG_NAMES), scope: "project" },
|
|
199
|
+
{ path: path.join(options.cwd, ".mcp.json"), scope: "project" },
|
|
200
|
+
];
|
|
201
|
+
const servers = [];
|
|
202
|
+
const seen = new Set();
|
|
203
|
+
for (const candidate of candidates) {
|
|
204
|
+
const raw = await readConfigFile(candidate.path);
|
|
205
|
+
if (!raw)
|
|
206
|
+
continue;
|
|
207
|
+
for (const server of parseMcpConfig(raw, candidate.scope, candidate.path)) {
|
|
208
|
+
const key = `${server.scope}:${server.name.toLowerCase()}`;
|
|
209
|
+
if (seen.has(key))
|
|
210
|
+
continue;
|
|
211
|
+
seen.add(key);
|
|
212
|
+
servers.push(server);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return servers;
|
|
216
|
+
}
|
|
217
|
+
export async function readMcpInventory(options) {
|
|
218
|
+
const servers = await loadMcpServerSpecs(options);
|
|
219
|
+
const tools = projectSessionMcpTools(options.tools ?? [], options.activeToolNames ?? [], servers.map((server) => server.name));
|
|
220
|
+
return assembleMcpInventory(servers, tools);
|
|
221
|
+
}
|
|
@@ -151,6 +151,63 @@ export function computeOperatorChatActiveToolNames(session) {
|
|
|
151
151
|
}
|
|
152
152
|
return extras.length > 0 ? [...baseline, ...extras] : baseline;
|
|
153
153
|
}
|
|
154
|
+
const OPERATOR_CHAT_SESSION_EXCLUDE_TOOLS = Object.freeze([
|
|
155
|
+
"apply_patch",
|
|
156
|
+
"apply-patch",
|
|
157
|
+
"full-tools",
|
|
158
|
+
"full_tools",
|
|
159
|
+
"coding-chat",
|
|
160
|
+
"coding_chat",
|
|
161
|
+
"shell",
|
|
162
|
+
]);
|
|
163
|
+
function createConsoleExtensionBindings() {
|
|
164
|
+
return {
|
|
165
|
+
mode: "rpc",
|
|
166
|
+
onError: (error) => {
|
|
167
|
+
const message = error instanceof Error
|
|
168
|
+
? error.message
|
|
169
|
+
: typeof error === "object" &&
|
|
170
|
+
error &&
|
|
171
|
+
"error" in error &&
|
|
172
|
+
typeof error.error === "string"
|
|
173
|
+
? String(error.error)
|
|
174
|
+
: String(error);
|
|
175
|
+
process.stderr.write(`[console] extension: ${message}\n`);
|
|
176
|
+
},
|
|
177
|
+
uiContext: {
|
|
178
|
+
select: async () => undefined,
|
|
179
|
+
confirm: async () => false,
|
|
180
|
+
input: async () => undefined,
|
|
181
|
+
editor: async () => undefined,
|
|
182
|
+
notify: () => undefined,
|
|
183
|
+
onTerminalInput: () => () => undefined,
|
|
184
|
+
setStatus: () => undefined,
|
|
185
|
+
setWorkingMessage: () => undefined,
|
|
186
|
+
setWorkingVisible: () => undefined,
|
|
187
|
+
setWorkingIndicator: () => undefined,
|
|
188
|
+
setHiddenThinkingLabel: () => undefined,
|
|
189
|
+
setWidget: () => undefined,
|
|
190
|
+
setFooter: () => undefined,
|
|
191
|
+
setHeader: () => undefined,
|
|
192
|
+
setTitle: () => undefined,
|
|
193
|
+
custom: async () => undefined,
|
|
194
|
+
pasteToEditor: () => undefined,
|
|
195
|
+
setEditorText: () => undefined,
|
|
196
|
+
getEditorText: () => "",
|
|
197
|
+
addAutocompleteProvider: () => undefined,
|
|
198
|
+
setEditorComponent: () => undefined,
|
|
199
|
+
getEditorComponent: () => undefined,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async function bindSessionExtensions(session) {
|
|
204
|
+
if (typeof session.bindExtensions !== "function")
|
|
205
|
+
return;
|
|
206
|
+
await session.bindExtensions(createConsoleExtensionBindings());
|
|
207
|
+
}
|
|
208
|
+
function pinOperatorChatTools(session) {
|
|
209
|
+
session.setActiveToolsByName(computeOperatorChatActiveToolNames(session));
|
|
210
|
+
}
|
|
154
211
|
/**
|
|
155
212
|
* Build Pi custom ToolDefinition objects for every whitelisted operator action.
|
|
156
213
|
* Each tool's execute() routes into the operator action dispatcher (Gate 3).
|
|
@@ -505,6 +562,29 @@ export function projectRuntimeSnapshot(input) {
|
|
|
505
562
|
})),
|
|
506
563
|
diagnostics: pkg.diagnostics.map((diag) => field(diag, 400) ?? ""),
|
|
507
564
|
});
|
|
565
|
+
const projectMcpTool = (tool) => ({
|
|
566
|
+
name: field(tool.name, 200) ?? "",
|
|
567
|
+
description: field(tool.description, 600) ?? "",
|
|
568
|
+
...(tool.server ? { server: field(tool.server, 200) } : {}),
|
|
569
|
+
source: field(tool.source, 200) ?? "",
|
|
570
|
+
scope: tool.scope,
|
|
571
|
+
...(tool.path ? { path: field(tool.path, 400) } : {}),
|
|
572
|
+
active: tool.active,
|
|
573
|
+
});
|
|
574
|
+
const projectMcp = (mcp) => ({
|
|
575
|
+
servers: mcp.servers.map((server) => ({
|
|
576
|
+
name: field(server.name, 200) ?? "",
|
|
577
|
+
scope: server.scope,
|
|
578
|
+
status: server.status,
|
|
579
|
+
enabled: server.enabled,
|
|
580
|
+
...(server.transport ? { transport: field(server.transport, 80) } : {}),
|
|
581
|
+
...(server.command ? { command: field(server.command, 400) } : {}),
|
|
582
|
+
...(server.url ? { url: field(server.url, 400) } : {}),
|
|
583
|
+
...(server.configPath ? { configPath: field(server.configPath, 400) } : {}),
|
|
584
|
+
tools: server.tools.map(projectMcpTool),
|
|
585
|
+
})),
|
|
586
|
+
unassignedTools: mcp.unassignedTools.map(projectMcpTool),
|
|
587
|
+
});
|
|
508
588
|
const projectDiagnostic = (diag) => ({
|
|
509
589
|
type: diag.type,
|
|
510
590
|
...(diag.code ? { code: diag.code } : {}),
|
|
@@ -560,6 +640,7 @@ export function projectRuntimeSnapshot(input) {
|
|
|
560
640
|
skills: input.skills.map(projectSkill),
|
|
561
641
|
extensions: input.extensions.map(projectExtension),
|
|
562
642
|
packages: input.packages.map(projectPackage),
|
|
643
|
+
mcp: projectMcp(input.mcp ?? { servers: [], unassignedTools: [] }),
|
|
563
644
|
diagnostics: input.diagnostics.map(projectDiagnostic),
|
|
564
645
|
hasBash: input.hasBash,
|
|
565
646
|
};
|
|
@@ -903,36 +984,16 @@ export class ConsolePiRuntime {
|
|
|
903
984
|
const customTools = await this.loadCustomTools();
|
|
904
985
|
if (this.disposedSessions.has(sessionId))
|
|
905
986
|
return;
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
//
|
|
912
|
-
// operationWait / safe-read / git-status / ...) from the registry, so the
|
|
913
|
-
// model could only ever see bash/read/write/edit/grep/find/ls. The
|
|
914
|
-
// allowlist must cover the full tool surface that is later activated via
|
|
915
|
-
// setActiveToolsByName(computeOperatorChatActiveToolNames(session)) —
|
|
916
|
-
// operator actions + built-in explore tools + safe explore tools.
|
|
917
|
-
// Hard write/coding channels stay blocked via excludeTools below (they
|
|
918
|
-
// are removed even when present in the allowed list).
|
|
987
|
+
// pi-web parity: do NOT pass `tools`. A closed allowlist becomes
|
|
988
|
+
// SDK allowedToolNames and filters extension MCP tools out of the
|
|
989
|
+
// registry before Gate 2 can re-add them. Operator/safe customTools
|
|
990
|
+
// still register via customTools; non-Pi write channels stay blocked
|
|
991
|
+
// by excludeTools. After create, bindExtensions emits session_start
|
|
992
|
+
// so MCP adapters can register, then pin the active set.
|
|
919
993
|
const { session } = await this.bindings.createSessionFromServices({
|
|
920
994
|
services,
|
|
921
995
|
sessionManager: init.sessionManager,
|
|
922
|
-
|
|
923
|
-
...OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS,
|
|
924
|
-
...OPERATOR_CHAT_ALLOWED_TOOLS,
|
|
925
|
-
...OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS,
|
|
926
|
-
],
|
|
927
|
-
excludeTools: [
|
|
928
|
-
"apply_patch",
|
|
929
|
-
"apply-patch",
|
|
930
|
-
"full-tools",
|
|
931
|
-
"full_tools",
|
|
932
|
-
"coding-chat",
|
|
933
|
-
"coding_chat",
|
|
934
|
-
"shell",
|
|
935
|
-
],
|
|
996
|
+
excludeTools: [...OPERATOR_CHAT_SESSION_EXCLUDE_TOOLS],
|
|
936
997
|
...(customTools.length > 0 ? { customTools } : {}),
|
|
937
998
|
...(resolvedModel ? { model: resolvedModel } : {}),
|
|
938
999
|
});
|
|
@@ -945,7 +1006,8 @@ export class ConsolePiRuntime {
|
|
|
945
1006
|
}
|
|
946
1007
|
return;
|
|
947
1008
|
}
|
|
948
|
-
|
|
1009
|
+
await bindSessionExtensions(session);
|
|
1010
|
+
pinOperatorChatTools(session);
|
|
949
1011
|
// Register then re-check dispose race: a concurrent dispose must win.
|
|
950
1012
|
this.sessions.set(sessionId, session);
|
|
951
1013
|
this.sessionManagers.set(sessionId, init.sessionManager);
|
|
@@ -1616,6 +1678,24 @@ export class ConsolePiRuntime {
|
|
|
1616
1678
|
retryable: true,
|
|
1617
1679
|
});
|
|
1618
1680
|
}
|
|
1681
|
+
const { readMcpInventory } = await import("./mcp-inventory.js");
|
|
1682
|
+
let mcp = { servers: [], unassignedTools: [] };
|
|
1683
|
+
try {
|
|
1684
|
+
mcp = await readMcpInventory({
|
|
1685
|
+
agentDir: await safeGetAgentDir(this.bindings),
|
|
1686
|
+
cwd: this.options.cwd,
|
|
1687
|
+
tools: session.getAllTools?.() ?? [],
|
|
1688
|
+
activeToolNames: session.getActiveToolNames(),
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
catch (error) {
|
|
1692
|
+
diagnostics.push({
|
|
1693
|
+
type: "error",
|
|
1694
|
+
code: "MCP_INVENTORY_FAILED",
|
|
1695
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1696
|
+
retryable: true,
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1619
1699
|
return projectRuntimeSnapshot({
|
|
1620
1700
|
sessionId,
|
|
1621
1701
|
revision: this.revisions.get(sessionId) ?? 0,
|
|
@@ -1651,6 +1731,7 @@ export class ConsolePiRuntime {
|
|
|
1651
1731
|
skills,
|
|
1652
1732
|
extensions,
|
|
1653
1733
|
packages,
|
|
1734
|
+
mcp,
|
|
1654
1735
|
diagnostics,
|
|
1655
1736
|
hasBash: true,
|
|
1656
1737
|
});
|
|
@@ -1833,29 +1914,15 @@ export class ConsolePiRuntime {
|
|
|
1833
1914
|
: undefined;
|
|
1834
1915
|
assertNoWriteToolInList(OPERATOR_CHAT_ALLOWED_TOOLS);
|
|
1835
1916
|
const customTools = await this.loadCustomTools();
|
|
1836
|
-
// Same allowlist rationale as materializeSession: the SDK filters
|
|
1837
|
-
// customTools against `tools`, so the allowed names must cover the full
|
|
1838
|
-
// operator + explore surface or every custom tool is silently dropped.
|
|
1839
1917
|
const { session } = await this.bindings.createSessionFromServices({
|
|
1840
1918
|
services,
|
|
1841
1919
|
sessionManager: init.sessionManager,
|
|
1842
|
-
|
|
1843
|
-
...OPERATOR_CHAT_BUILTIN_EXPLORE_TOOLS,
|
|
1844
|
-
...OPERATOR_CHAT_ALLOWED_TOOLS,
|
|
1845
|
-
...OPERATOR_CHAT_SAFE_EXPLORE_TOOL_IDS,
|
|
1846
|
-
],
|
|
1847
|
-
excludeTools: [
|
|
1848
|
-
"apply_patch",
|
|
1849
|
-
"apply-patch",
|
|
1850
|
-
"full-tools",
|
|
1851
|
-
"full_tools",
|
|
1852
|
-
"coding-chat",
|
|
1853
|
-
"coding_chat",
|
|
1854
|
-
"shell",
|
|
1855
|
-
],
|
|
1920
|
+
excludeTools: [...OPERATOR_CHAT_SESSION_EXCLUDE_TOOLS],
|
|
1856
1921
|
...(customTools.length > 0 ? { customTools } : {}),
|
|
1857
1922
|
...(resolvedModel ? { model: resolvedModel } : {}),
|
|
1858
1923
|
});
|
|
1924
|
+
await bindSessionExtensions(session);
|
|
1925
|
+
pinOperatorChatTools(session);
|
|
1859
1926
|
return {
|
|
1860
1927
|
session,
|
|
1861
1928
|
modelRuntime,
|
|
@@ -1950,6 +2017,32 @@ export class ConsolePiRuntime {
|
|
|
1950
2017
|
if (options?.signal?.aborted) {
|
|
1951
2018
|
return;
|
|
1952
2019
|
}
|
|
2020
|
+
const emitContextUsage = () => {
|
|
2021
|
+
try {
|
|
2022
|
+
const contextUsage = session.getSessionStats?.()?.contextUsage;
|
|
2023
|
+
if (!contextUsage)
|
|
2024
|
+
return;
|
|
2025
|
+
if (contextUsage.contextWindow <= 0 &&
|
|
2026
|
+
contextUsage.percent === null &&
|
|
2027
|
+
contextUsage.tokens === null) {
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
onEvent({
|
|
2031
|
+
type: "usage",
|
|
2032
|
+
sessionId,
|
|
2033
|
+
usage: {
|
|
2034
|
+
inputTokens: 0,
|
|
2035
|
+
outputTokens: 0,
|
|
2036
|
+
totalTokens: 0,
|
|
2037
|
+
contextUsage,
|
|
2038
|
+
},
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
catch {
|
|
2042
|
+
// Stats getter is best-effort; never break the event stream.
|
|
2043
|
+
}
|
|
2044
|
+
};
|
|
2045
|
+
emitContextUsage();
|
|
1953
2046
|
unsub = session.subscribe((event) => {
|
|
1954
2047
|
const mapped = mapSdkEvent(sessionId, event);
|
|
1955
2048
|
if (mapped)
|
|
@@ -1962,31 +2055,13 @@ export class ConsolePiRuntime {
|
|
|
1962
2055
|
const usage = extractUsageSample(event);
|
|
1963
2056
|
if (usage)
|
|
1964
2057
|
onEvent({ type: "usage", sessionId, usage });
|
|
2058
|
+
emitContextUsage();
|
|
1965
2059
|
}
|
|
1966
|
-
//
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
const contextUsage = stats?.contextUsage;
|
|
1972
|
-
if (contextUsage) {
|
|
1973
|
-
onEvent({
|
|
1974
|
-
type: "usage",
|
|
1975
|
-
sessionId,
|
|
1976
|
-
usage: {
|
|
1977
|
-
inputTokens: 0,
|
|
1978
|
-
outputTokens: 0,
|
|
1979
|
-
totalTokens: 0,
|
|
1980
|
-
...(contextUsage.tokens === null
|
|
1981
|
-
? {}
|
|
1982
|
-
: { contextUsage }),
|
|
1983
|
-
},
|
|
1984
|
-
});
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
catch {
|
|
1988
|
-
// Stats getter is best-effort; never break the event stream.
|
|
1989
|
-
}
|
|
2060
|
+
// Keep the context fill bar live through the turn (pi-web SessionInfoBar).
|
|
2061
|
+
if (event.type === "agent_start" ||
|
|
2062
|
+
event.type === "agent_settled" ||
|
|
2063
|
+
event.type === "tool_execution_end") {
|
|
2064
|
+
emitContextUsage();
|
|
1990
2065
|
}
|
|
1991
2066
|
});
|
|
1992
2067
|
options?.signal?.addEventListener("abort", onAbort);
|
|
@@ -77,6 +77,7 @@ function isBinaryExt(p) {
|
|
|
77
77
|
export async function walkRepoFiles(repoRoot, options) {
|
|
78
78
|
const skipSensitive = options?.skipSensitive ?? true;
|
|
79
79
|
const maxFiles = options?.maxFiles ?? 5000;
|
|
80
|
+
const includeDirs = options?.includeDirs ?? false;
|
|
80
81
|
const filter = options?.glob ? globToRe(options.glob) : undefined;
|
|
81
82
|
const out = [];
|
|
82
83
|
async function walk(dir, prefix) {
|
|
@@ -96,7 +97,15 @@ export async function walkRepoFiles(repoRoot, options) {
|
|
|
96
97
|
if (entry.isDirectory()) {
|
|
97
98
|
if (SKIP_DIRS.has(name))
|
|
98
99
|
continue;
|
|
99
|
-
|
|
100
|
+
const dirRelative = `${prefix}${name}/`;
|
|
101
|
+
if (includeDirs) {
|
|
102
|
+
const dirEntry = {
|
|
103
|
+
abs: path.join(dir, name),
|
|
104
|
+
repoRelative: dirRelative,
|
|
105
|
+
};
|
|
106
|
+
out.push(dirEntry);
|
|
107
|
+
}
|
|
108
|
+
await walk(path.join(dir, name), dirRelative);
|
|
100
109
|
continue;
|
|
101
110
|
}
|
|
102
111
|
if (!entry.isFile())
|
|
@@ -51,7 +51,7 @@ function turnSummary(turn) {
|
|
|
51
51
|
function isTerminalTurnState(state) {
|
|
52
52
|
return state === "settled" || state === "aborted" || state === "failed";
|
|
53
53
|
}
|
|
54
|
-
const DEFAULT_ABORT_WAIT_MS =
|
|
54
|
+
const DEFAULT_ABORT_WAIT_MS = 2_500;
|
|
55
55
|
/** Parse Chat's `<epoch>:<seq>` SSE cursor. Legacy `<sessionId>:<seq>` has
|
|
56
56
|
* the same wire shape; its prefix intentionally fails the current process
|
|
57
57
|
* epoch comparison and triggers safe snapshot recovery (M5.1). */
|
|
@@ -1642,16 +1642,27 @@ async function handleChatFiles(res, deps, sessionId, query) {
|
|
|
1642
1642
|
if (!(await requireChatSession(res, deps, sessionId)))
|
|
1643
1643
|
return;
|
|
1644
1644
|
const q = query.trim().toLowerCase();
|
|
1645
|
-
const
|
|
1645
|
+
const walked = await walkRepoFiles(deps.appData.repoRoot, {
|
|
1646
1646
|
skipSensitive: true,
|
|
1647
1647
|
maxFiles: 5000,
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
.
|
|
1648
|
+
includeDirs: true,
|
|
1649
|
+
});
|
|
1650
|
+
const entries = walked
|
|
1651
|
+
.filter((entry) => !isSensitivePath(entry.repoRelative) &&
|
|
1652
|
+
(!q || entry.repoRelative.toLowerCase().includes(q)))
|
|
1653
|
+
.sort((a, b) => {
|
|
1654
|
+
const aDir = a.repoRelative.endsWith("/");
|
|
1655
|
+
const bDir = b.repoRelative.endsWith("/");
|
|
1656
|
+
if (aDir !== bDir)
|
|
1657
|
+
return aDir ? -1 : 1;
|
|
1658
|
+
return a.repoRelative.localeCompare(b.repoRelative);
|
|
1659
|
+
})
|
|
1652
1660
|
.slice(0, 50)
|
|
1653
|
-
.map((
|
|
1654
|
-
|
|
1661
|
+
.map((entry) => ({
|
|
1662
|
+
path: entry.repoRelative,
|
|
1663
|
+
isDir: entry.repoRelative.endsWith("/"),
|
|
1664
|
+
}));
|
|
1665
|
+
sendJson(res, 200, { ok: true, files: entries });
|
|
1655
1666
|
}
|
|
1656
1667
|
export async function handleChatCommands(res, deps, sessionId) {
|
|
1657
1668
|
try {
|
|
@@ -216,9 +216,6 @@ export function buildTurnProcessGroups(input) {
|
|
|
216
216
|
current = startGroup(`assistant-${message.id}`);
|
|
217
217
|
current.assistants.push(message);
|
|
218
218
|
}
|
|
219
|
-
if (groups.length === 0 && input.toolCalls.length > 0) {
|
|
220
|
-
groups.push(emptyGroup("orphan-tools"));
|
|
221
|
-
}
|
|
222
219
|
const assigned = new Set();
|
|
223
220
|
const assistantToGroup = new Map();
|
|
224
221
|
for (const group of groups) {
|
|
@@ -265,17 +262,12 @@ export function buildTurnProcessGroups(input) {
|
|
|
265
262
|
const end = next?.user?.createdAt ?? Number.POSITIVE_INFINITY;
|
|
266
263
|
return tool.createdAt >= start && tool.createdAt < end;
|
|
267
264
|
});
|
|
265
|
+
// Time window only. Never dump unmatched tools onto the latest group:
|
|
266
|
+
// that is how a previous turn's bash cards jump under a new prompt.
|
|
268
267
|
if (windowGroup) {
|
|
269
268
|
windowGroup.tools.push(tool);
|
|
270
269
|
assigned.add(tool.id);
|
|
271
270
|
}
|
|
272
|
-
else {
|
|
273
|
-
const lastGroup = groups.at(-1);
|
|
274
|
-
if (lastGroup) {
|
|
275
|
-
lastGroup.tools.push(tool);
|
|
276
|
-
assigned.add(tool.id);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
271
|
}
|
|
280
272
|
for (const group of groups) {
|
|
281
273
|
group.tools.sort((a, b) => {
|
|
@@ -292,6 +284,14 @@ export function buildTurnProcessGroups(input) {
|
|
|
292
284
|
}
|
|
293
285
|
return groups;
|
|
294
286
|
}
|
|
287
|
+
/** Keep the most recent complete turns. Slice groups, never raw messages. */
|
|
288
|
+
export function selectRecentTurnGroups(groups, limit) {
|
|
289
|
+
if (limit <= 0)
|
|
290
|
+
return [];
|
|
291
|
+
if (groups.length <= limit)
|
|
292
|
+
return [...groups];
|
|
293
|
+
return groups.slice(-limit);
|
|
294
|
+
}
|
|
295
295
|
export function processGroupSummary(tools, options) {
|
|
296
296
|
const count = tools.length;
|
|
297
297
|
const thinkingCount = options?.thinkingCount ?? 0;
|