pi-shadow-mind 0.1.15 → 0.1.17
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/DESIGN.md +8 -6
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/dist/index.js +178 -70
- package/dist/index.js.map +4 -4
- package/package.json +13 -1
package/DESIGN.md
CHANGED
|
@@ -111,7 +111,7 @@ active_for_models:
|
|
|
111
111
|
| `run_with_model` | Shadow 自己使用的模型;省略时使用插件默认模型 |
|
|
112
112
|
| `thinking_level` | Shadow 使用的 thinking level;省略时使用插件默认值,再回退到 Main 会话当前生效等级 |
|
|
113
113
|
| `timeout_seconds` | Shadow 单次运行超时;省略时使用插件默认超时 |
|
|
114
|
-
| `tools` | 在 Pi SDK `readOnlyTools`
|
|
114
|
+
| `tools` | 在 Pi SDK `readOnlyTools` 之上追加的工具白名单;`["*"]` 授权当前 Session 的全部工具;默认 `[]` |
|
|
115
115
|
|
|
116
116
|
`name` 只用于 `shadow-report` 和状态界面展示,不参与身份判断;省略时回退到最终解析出的 `id`。Markdown 正文就是 Shadow 的认知定义、长期职责和行为要求。
|
|
117
117
|
|
|
@@ -131,9 +131,9 @@ Pi SDK readOnlyTools
|
|
|
131
131
|
|
|
132
132
|
默认名称集合包含 `read`、`grep`、`find` 和 `ls`。激活时插件按名称从当前 Main Session 的最终工具 registry 解析实际实现,因此会继承其他插件对同名工具的覆盖以及当前环境适配。这里的 `readOnlyTools` 表示默认名称约定,不保证被覆盖后的实现仍然没有副作用。
|
|
133
133
|
|
|
134
|
-
`tools` 是在该集合之上追加的显式白名单,省略时视为 `[]`。例如 `tools: [write]` 会获得当前 registry 中的默认四个工具、`write` 和内置 `report_to_main
|
|
134
|
+
`tools` 是在该集合之上追加的显式白名单,省略时视为 `[]`。例如 `tools: [write]` 会获得当前 registry 中的默认四个工具、`write` 和内置 `report_to_main`。使用 `tools: ["*"]` 时,Shadow 会获得激活时 Main Session registry 中的全部工具以及 `report_to_main`;之后新增或改名的工具会自动随 registry 解析,不需要同步维护定义文件。
|
|
135
135
|
|
|
136
|
-
Pi 的通用 `ToolDefinition` 没有可供插件查询的只读属性,因此自定义工具不会被自动归类或加入;编辑文件、执行 Shell 或其他能力也不会被隐式继承自 Main
|
|
136
|
+
Pi 的通用 `ToolDefinition` 没有可供插件查询的只读属性,因此自定义工具不会被自动归类或加入;编辑文件、执行 Shell 或其他能力也不会被隐式继承自 Main,除非显式列出名称或使用 `"*"`。配置的追加工具在当前 Session 不存在时,插件忽略该项、写入轻量运行事件,其余工具继续正常提供。
|
|
137
137
|
|
|
138
138
|
`tools` 白名单本身就是用户对该 Shadow 的执行授权。列入白名单的写入、Shell 或其他工具可以由后台 Shadow 直接调用,插件不增加逐次确认层。
|
|
139
139
|
|
|
@@ -340,9 +340,11 @@ Main 在会话中切换模型后,后续 heartbeat 直接依据新模型重新
|
|
|
340
340
|
|
|
341
341
|
配置了 `final_response` 的 Shadow 在 Main 发出非空最终文字,并进入 `agent_settled` 状态后参与调度。该模式仍应用 `enabled` 和 `active_for_models`,但绕过 `heartbeat_probability` 与 `activation_probability`,所有匹配项都必须获得一次运行机会。
|
|
342
342
|
|
|
343
|
-
最终回复检查使用当时的完整净化轨迹快照。`max_parallel_shadows` 仍是硬并发上限;没有空闲槽位或同一 Shadow
|
|
343
|
+
最终回复检查使用当时的完整净化轨迹快照。`max_parallel_shadows` 仍是硬并发上限;没有空闲槽位或同一 Shadow 正在运行时,检查进入专用队列,槽位释放后继续执行。旧 epoch 的运行释放槽位时同样继续泵送当前队列,避免跨 epoch 的异步收尾把检查永久卡住。
|
|
344
344
|
|
|
345
|
-
|
|
345
|
+
每次最终回复检查拥有独立、递增的 review generation。独立的 completion-review 协调器统一拥有 generation、等待队列、运行完成计数和报告聚合,Runtime 只转发宿主生命周期事件与 Shadow 启停通知。同一 generation 的报告先暂存,所有匹配 Shadow 都进入终态后才合并为一次 `shadow-report`;因此较慢的同批检查不会在 Main 已开始修订后单独 steer。新的最终回复、新用户输入、其他报告触发的修订或 Session 关闭都会使旧 generation 失效,并清空尚未启动的项目;仍在收尾的旧运行只能记录 lifetime usage,不能再投递报告。异步刷新配置和 Registry 时还会同时校验启动请求与 epoch,防止等待 I/O 的旧最终回复在新任务中恢复调度。
|
|
346
|
+
|
|
347
|
+
检查结果沿用普通 `shadow-report` 的 steer/follow-up 机制。因此 Main 根据聚合报告修正并再次给出最终回复后,可以再次触发新一代完成检查;Shadow 没有发现时保持沉默,循环自然结束。
|
|
346
348
|
|
|
347
349
|
## 6. Shadow 的输出与介入
|
|
348
350
|
|
|
@@ -362,7 +364,7 @@ Shadow 返回
|
|
|
362
364
|
└── 丢弃旧结果
|
|
363
365
|
```
|
|
364
366
|
|
|
365
|
-
|
|
367
|
+
Heartbeat Shadow 不会因为 Main 给出最终回复而立即终止,迟到结果仍可在下一条用户消息到来前触发补充回复。Final-response Shadow 则按 review generation 聚合,只有整批检查结束后才允许触发一次补充回复。
|
|
366
368
|
|
|
367
369
|
每个用户任务拥有递增的 `epoch`:
|
|
368
370
|
|
package/README.md
CHANGED
|
@@ -66,13 +66,13 @@ Report only concrete, actionable issues grounded in the visible trajectory or
|
|
|
66
66
|
repository. If the current work is unrelated, do not intervene.
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
This Shadow is read-only. It reviews the implementation in parallel and reports concrete architectural concerns without taking control of the main task.
|
|
69
|
+
This Shadow is read-only. It reviews the implementation in parallel and reports concrete architectural concerns without taking control of the main task. Tool names extend the default read-only set; use `tools: ["*"]` only when the Shadow should inherit every tool currently registered in the main session, including tools added later.
|
|
70
70
|
|
|
71
71
|
## How it works
|
|
72
72
|
|
|
73
73
|
Each Shadow chooses one or both activation triggers with `trigger`. The default is `[heartbeat]`: after a main-agent `turn_end` that completed at least one tool call, the extension evaluates the global heartbeat probability, then eligible Shadows roll independently using `activation_probability`. Pure text-only conversation turns are skipped.
|
|
74
74
|
|
|
75
|
-
Use `trigger: [final_response]` for completion review. It activates after the main agent has emitted its final text
|
|
75
|
+
Use `trigger: [final_response]` for completion review. It activates after the main agent has emitted its final text and bypasses both heartbeat and activation probability. All checks for that final response finish before their findings are sent together through one `shadow-report` follow-up, so a slow sibling cannot leak into a revised answer. `trigger: [heartbeat, final_response]` enables both modes. `max_parallel_shadows` remains the concurrency limit; excess final-response checks are queued rather than skipped.
|
|
76
76
|
|
|
77
77
|
Each activation starts a fresh temporary session. It inherits the main agent's unchanged system prompt but receives only a sanitized plain-text trajectory: assistant thinking is removed, while tool calls retain compact, deterministic result summaries.
|
|
78
78
|
|
|
@@ -83,7 +83,7 @@ Shadow definitions are ordinary Markdown files. They can be created and adjusted
|
|
|
83
83
|
## Installation
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
|
-
pi install npm:pi-shadow-mind
|
|
86
|
+
pi install npm:pi-shadow-mind
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
On the first session start, the extension creates:
|
package/README.zh-CN.md
CHANGED
|
@@ -63,13 +63,13 @@ tools: [read, grep]
|
|
|
63
63
|
无关,不要介入。
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
这个 Shadow 默认只读。它会在实现过程中并行审阅架构,并向主 Agent
|
|
66
|
+
这个 Shadow 默认只读。它会在实现过程中并行审阅架构,并向主 Agent 报告具体问题,但不会接管主任务。`tools` 中的名称会追加到默认只读工具集;只有确实需要继承 Main Session 当前注册的全部工具(包括未来新增工具)时,才使用 `tools: ["*"]`。
|
|
67
67
|
|
|
68
68
|
## 工作方式
|
|
69
69
|
|
|
70
70
|
每个 Shadow 可以通过 `trigger` 选择一种或两种激活方式。默认值是 `[heartbeat]`:主 Agent 的一次 `turn_end` 只有在该轮至少完成过一个工具调用时,扩展才进行全局 heartbeat 概率判断,符合条件的 Shadow 再按照各自的 `activation_probability` 独立抽选。纯文本对话轮次不会触发 heartbeat。
|
|
71
71
|
|
|
72
|
-
使用 `trigger: [final_response]` 可以进行完成后审查。它在主 Agent 发出最终文字并完全 settled 后激活,不受 heartbeat 和 `activation_probability`
|
|
72
|
+
使用 `trigger: [final_response]` 可以进行完成后审查。它在主 Agent 发出最终文字并完全 settled 后激活,不受 heartbeat 和 `activation_probability` 影响。同一最终回复的全部检查结束后,发现会合并为一次 `shadow-report` follow-up,较慢的同批检查不会介入已经修订的回复。`trigger: [heartbeat, final_response]` 会同时启用两种模式。`max_parallel_shadows` 仍然限制并发数;超出并发槽位的最终回复检查会排队,而不会被跳过。
|
|
73
73
|
|
|
74
74
|
每次激活都会创建一个全新的临时 Session。它继承主 Agent 原封不动的 system prompt,但只接收净化后的文本轨迹:思考内容会被移除,工具调用后仅保留简洁、确定性的结果概述。
|
|
75
75
|
|
|
@@ -80,7 +80,7 @@ Shadow 定义只是普通 Markdown 文件,可以由用户创建和调整,也
|
|
|
80
80
|
## 安装
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
pi install npm:pi-shadow-mind
|
|
83
|
+
pi install npm:pi-shadow-mind
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
首次启动 Session 时,扩展会创建:
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,121 @@ import {
|
|
|
12
12
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { Text } from "@earendil-works/pi-tui";
|
|
14
14
|
|
|
15
|
+
// src/final-response-queue.ts
|
|
16
|
+
var FinalResponseQueue = class {
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.options = options;
|
|
19
|
+
}
|
|
20
|
+
pending = [];
|
|
21
|
+
enqueue(items) {
|
|
22
|
+
this.pending.push(...items);
|
|
23
|
+
this.pump();
|
|
24
|
+
}
|
|
25
|
+
slotAvailable() {
|
|
26
|
+
this.pump();
|
|
27
|
+
}
|
|
28
|
+
clear() {
|
|
29
|
+
this.pending.length = 0;
|
|
30
|
+
}
|
|
31
|
+
pump() {
|
|
32
|
+
while (this.options.activeCount() < this.options.maxParallel() && this.pending.length > 0) {
|
|
33
|
+
const epoch = this.options.currentEpoch();
|
|
34
|
+
const activeShadowIds = this.options.activeShadowIds();
|
|
35
|
+
const index = this.pending.findIndex(
|
|
36
|
+
(item2) => item2.epoch !== epoch || !activeShadowIds.has(item2.shadowId)
|
|
37
|
+
);
|
|
38
|
+
if (index < 0) return;
|
|
39
|
+
const [item] = this.pending.splice(index, 1);
|
|
40
|
+
if (!item || item.epoch !== epoch) continue;
|
|
41
|
+
this.options.launch(item);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/completion-review.ts
|
|
47
|
+
var CompletionReview = class {
|
|
48
|
+
constructor(host) {
|
|
49
|
+
this.host = host;
|
|
50
|
+
this.queue = new FinalResponseQueue({
|
|
51
|
+
currentEpoch: () => this.host.currentEpoch(),
|
|
52
|
+
maxParallel: () => this.host.maxParallel(),
|
|
53
|
+
activeCount: () => this.host.activeCount(),
|
|
54
|
+
activeShadowIds: () => this.host.activeShadowIds(),
|
|
55
|
+
launch: ({ job, run }) => host.launch(job, run)
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
requestId = 0;
|
|
59
|
+
state;
|
|
60
|
+
requestByRun = /* @__PURE__ */ new WeakMap();
|
|
61
|
+
queue;
|
|
62
|
+
begin(epoch) {
|
|
63
|
+
this.invalidate();
|
|
64
|
+
return { id: this.requestId, epoch };
|
|
65
|
+
}
|
|
66
|
+
isCurrent(request) {
|
|
67
|
+
return request.id === this.requestId && request.epoch === this.host.currentEpoch();
|
|
68
|
+
}
|
|
69
|
+
schedule(request, jobs) {
|
|
70
|
+
if (!this.isCurrent(request) || this.state?.requestId === request.id || jobs.some((job) => job.epoch !== request.epoch)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (jobs.length === 0) {
|
|
74
|
+
this.retire(request.id);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
this.state = {
|
|
78
|
+
requestId: request.id,
|
|
79
|
+
epoch: request.epoch,
|
|
80
|
+
remaining: jobs.length,
|
|
81
|
+
reports: []
|
|
82
|
+
};
|
|
83
|
+
this.queue.enqueue(
|
|
84
|
+
jobs.map((job) => {
|
|
85
|
+
const run = {
|
|
86
|
+
accept: (report) => this.accept(request.id, report)
|
|
87
|
+
};
|
|
88
|
+
this.requestByRun.set(run, request.id);
|
|
89
|
+
return { epoch: job.epoch, shadowId: job.shadowId, job, run };
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
slotAvailable() {
|
|
95
|
+
this.queue.slotAvailable();
|
|
96
|
+
}
|
|
97
|
+
complete(run) {
|
|
98
|
+
if (!run) return;
|
|
99
|
+
const requestId = this.requestByRun.get(run);
|
|
100
|
+
this.requestByRun.delete(run);
|
|
101
|
+
if (requestId === void 0 || this.state?.requestId !== requestId) return;
|
|
102
|
+
this.state.remaining -= 1;
|
|
103
|
+
if (this.state.remaining > 0) return;
|
|
104
|
+
const reports = this.state.reports;
|
|
105
|
+
this.retire(requestId);
|
|
106
|
+
if (reports.length > 0) this.host.deliver(reports);
|
|
107
|
+
}
|
|
108
|
+
cancel(request) {
|
|
109
|
+
if (this.isCurrent(request)) this.retire(request.id);
|
|
110
|
+
}
|
|
111
|
+
invalidate() {
|
|
112
|
+
this.requestId += 1;
|
|
113
|
+
this.state = void 0;
|
|
114
|
+
this.queue.clear();
|
|
115
|
+
}
|
|
116
|
+
retire(requestId) {
|
|
117
|
+
if (requestId !== this.requestId) return;
|
|
118
|
+
this.requestId += 1;
|
|
119
|
+
this.state = void 0;
|
|
120
|
+
this.queue.clear();
|
|
121
|
+
}
|
|
122
|
+
accept(requestId, report) {
|
|
123
|
+
if (this.state?.requestId !== requestId || report.epoch !== this.state.epoch) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.state.reports.push(report);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
15
130
|
// src/config.ts
|
|
16
131
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
17
132
|
import { dirname, join } from "node:path";
|
|
@@ -7007,37 +7122,6 @@ function definedOnly(value) {
|
|
|
7007
7122
|
);
|
|
7008
7123
|
}
|
|
7009
7124
|
|
|
7010
|
-
// src/final-response-queue.ts
|
|
7011
|
-
var FinalResponseQueue = class {
|
|
7012
|
-
constructor(options) {
|
|
7013
|
-
this.options = options;
|
|
7014
|
-
}
|
|
7015
|
-
pending = [];
|
|
7016
|
-
enqueue(items) {
|
|
7017
|
-
this.pending.push(...items);
|
|
7018
|
-
this.pump();
|
|
7019
|
-
}
|
|
7020
|
-
slotAvailable() {
|
|
7021
|
-
this.pump();
|
|
7022
|
-
}
|
|
7023
|
-
clear() {
|
|
7024
|
-
this.pending.length = 0;
|
|
7025
|
-
}
|
|
7026
|
-
pump() {
|
|
7027
|
-
while (this.options.activeCount() < this.options.maxParallel() && this.pending.length > 0) {
|
|
7028
|
-
const epoch = this.options.currentEpoch();
|
|
7029
|
-
const activeShadowIds = this.options.activeShadowIds();
|
|
7030
|
-
const index = this.pending.findIndex(
|
|
7031
|
-
(item2) => item2.epoch !== epoch || !activeShadowIds.has(item2.shadowId)
|
|
7032
|
-
);
|
|
7033
|
-
if (index < 0) return;
|
|
7034
|
-
const [item] = this.pending.splice(index, 1);
|
|
7035
|
-
if (!item || item.epoch !== epoch) continue;
|
|
7036
|
-
this.options.launch(item);
|
|
7037
|
-
}
|
|
7038
|
-
}
|
|
7039
|
-
};
|
|
7040
|
-
|
|
7041
7125
|
// src/management-tools.ts
|
|
7042
7126
|
import { Type } from "typebox";
|
|
7043
7127
|
var ID = Type.String({ pattern: "^[a-z0-9][a-z0-9_-]*$" });
|
|
@@ -7663,7 +7747,7 @@ function compactJson(value) {
|
|
|
7663
7747
|
// src/shadow-runner.ts
|
|
7664
7748
|
function resolveShadowTools(whitelist, available, defaults = DEFAULT_READ_TOOLS) {
|
|
7665
7749
|
const builtin = /* @__PURE__ */ new Set(["report_to_main"]);
|
|
7666
|
-
const requested = [.../* @__PURE__ */ new Set([...defaults, ...whitelist, ...builtin])];
|
|
7750
|
+
const requested = whitelist.includes("*") ? [.../* @__PURE__ */ new Set([...available, ...builtin])] : [.../* @__PURE__ */ new Set([...defaults, ...whitelist, ...builtin])];
|
|
7667
7751
|
const tools = [];
|
|
7668
7752
|
const missing = [];
|
|
7669
7753
|
for (const name of requested) {
|
|
@@ -7685,13 +7769,13 @@ function buildRunResult(options) {
|
|
|
7685
7769
|
const usage = usageMetrics(ownMessages);
|
|
7686
7770
|
return {
|
|
7687
7771
|
reason,
|
|
7688
|
-
...error
|
|
7772
|
+
...error === void 0 ? {} : { error },
|
|
7689
7773
|
durationMs,
|
|
7690
7774
|
toolNames: session?.getActiveToolNames() ?? [],
|
|
7691
7775
|
missingTools,
|
|
7692
7776
|
...metrics,
|
|
7693
7777
|
usage,
|
|
7694
|
-
...thinkingLevel
|
|
7778
|
+
...thinkingLevel === void 0 ? {} : { thinkingLevel },
|
|
7695
7779
|
sessionFile: session?.sessionFile
|
|
7696
7780
|
};
|
|
7697
7781
|
}
|
|
@@ -8280,7 +8364,10 @@ var ShadowMindRuntime = class {
|
|
|
8280
8364
|
this.pi = pi;
|
|
8281
8365
|
this.batcher = new ReportBatcher(
|
|
8282
8366
|
this.configStore.current.resultBatchWindowMs,
|
|
8283
|
-
(reports) =>
|
|
8367
|
+
(reports) => {
|
|
8368
|
+
this.completionReview.invalidate();
|
|
8369
|
+
void this.deliverReports(reports);
|
|
8370
|
+
}
|
|
8284
8371
|
);
|
|
8285
8372
|
}
|
|
8286
8373
|
agentDir = getAgentDir();
|
|
@@ -8293,22 +8380,14 @@ var ShadowMindRuntime = class {
|
|
|
8293
8380
|
runner = new ShadowRunner();
|
|
8294
8381
|
usageStore = new UsageStore(this.agentDir);
|
|
8295
8382
|
active = /* @__PURE__ */ new Map();
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
pending.shadow,
|
|
8305
|
-
pending.mainModel,
|
|
8306
|
-
pending.fullModelId,
|
|
8307
|
-
pending.context,
|
|
8308
|
-
pending.availableTools
|
|
8309
|
-
)
|
|
8310
|
-
}
|
|
8311
|
-
);
|
|
8383
|
+
completionReview = new CompletionReview({
|
|
8384
|
+
currentEpoch: () => this.epoch,
|
|
8385
|
+
maxParallel: () => this.configStore.current.maxParallelShadows,
|
|
8386
|
+
activeCount: () => this.active.size,
|
|
8387
|
+
activeShadowIds: () => new Set([...this.active.values()].map(({ shadow }) => shadow.id)),
|
|
8388
|
+
launch: (pending, review) => this.launchShadow({ ...pending, completionReview: review }),
|
|
8389
|
+
deliver: (reports) => void this.deliverReports(reports)
|
|
8390
|
+
});
|
|
8312
8391
|
recentEvents = [];
|
|
8313
8392
|
recentRuns = [];
|
|
8314
8393
|
batcher;
|
|
@@ -8458,7 +8537,7 @@ var ShadowMindRuntime = class {
|
|
|
8458
8537
|
);
|
|
8459
8538
|
}
|
|
8460
8539
|
async onHeartbeat(ctx) {
|
|
8461
|
-
await this.refresh(ctx);
|
|
8540
|
+
const snapshot = await this.refresh(ctx);
|
|
8462
8541
|
if (this.paused || !ctx.model) {
|
|
8463
8542
|
this.record("heartbeat-skipped", {
|
|
8464
8543
|
reason: this.paused ? "paused" : "no-model",
|
|
@@ -8466,7 +8545,6 @@ var ShadowMindRuntime = class {
|
|
|
8466
8545
|
});
|
|
8467
8546
|
return;
|
|
8468
8547
|
}
|
|
8469
|
-
const snapshot = await this.registry.load();
|
|
8470
8548
|
const fullModelId = `${ctx.model.provider}/${ctx.model.id}`;
|
|
8471
8549
|
const decision = decideHeartbeat({
|
|
8472
8550
|
heartbeatProbability: this.configStore.current.heartbeatProbability,
|
|
@@ -8501,25 +8579,30 @@ var ShadowMindRuntime = class {
|
|
|
8501
8579
|
this.pi.getAllTools().map((tool2) => tool2.name)
|
|
8502
8580
|
);
|
|
8503
8581
|
for (const { shadow } of decision.activated) {
|
|
8504
|
-
this.launchShadow(
|
|
8582
|
+
this.launchShadow({
|
|
8505
8583
|
ctx,
|
|
8506
8584
|
shadow,
|
|
8507
|
-
ctx.model,
|
|
8585
|
+
mainModel: ctx.model,
|
|
8508
8586
|
fullModelId,
|
|
8509
8587
|
context,
|
|
8510
8588
|
availableTools
|
|
8511
|
-
);
|
|
8589
|
+
});
|
|
8512
8590
|
}
|
|
8513
8591
|
}
|
|
8514
8592
|
async onFinalResponse(ctx) {
|
|
8515
|
-
|
|
8593
|
+
const request = this.completionReview.begin(this.epoch);
|
|
8594
|
+
const snapshot = await this.refresh(ctx);
|
|
8595
|
+
if (!this.completionReview.isCurrent(request)) {
|
|
8596
|
+
this.record("final-response-skipped", { reason: "superseded" });
|
|
8597
|
+
return;
|
|
8598
|
+
}
|
|
8516
8599
|
if (this.paused || !ctx.model) {
|
|
8600
|
+
this.completionReview.cancel(request);
|
|
8517
8601
|
this.record("final-response-skipped", {
|
|
8518
8602
|
reason: this.paused ? "paused" : "no-model"
|
|
8519
8603
|
});
|
|
8520
8604
|
return;
|
|
8521
8605
|
}
|
|
8522
|
-
const snapshot = await this.registry.load();
|
|
8523
8606
|
const mainModel = ctx.model;
|
|
8524
8607
|
const fullModelId = `${mainModel.provider}/${mainModel.id}`;
|
|
8525
8608
|
const decision = decideFinalResponse({
|
|
@@ -8532,7 +8615,10 @@ var ShadowMindRuntime = class {
|
|
|
8532
8615
|
...decision.modelFiltered.length ? { modelFiltered: decision.modelFiltered } : {},
|
|
8533
8616
|
...decision.runningExcluded.length ? { runningExcluded: decision.runningExcluded } : {}
|
|
8534
8617
|
});
|
|
8535
|
-
if (!decision.activated.length)
|
|
8618
|
+
if (!decision.activated.length) {
|
|
8619
|
+
this.completionReview.schedule(request, []);
|
|
8620
|
+
return;
|
|
8621
|
+
}
|
|
8536
8622
|
const context = buildSessionContext(
|
|
8537
8623
|
ctx.sessionManager.getEntries(),
|
|
8538
8624
|
ctx.sessionManager.getLeafId()
|
|
@@ -8540,9 +8626,10 @@ var ShadowMindRuntime = class {
|
|
|
8540
8626
|
const availableTools = new Set(
|
|
8541
8627
|
this.pi.getAllTools().map((tool2) => tool2.name)
|
|
8542
8628
|
);
|
|
8543
|
-
this.
|
|
8629
|
+
this.completionReview.schedule(
|
|
8630
|
+
request,
|
|
8544
8631
|
decision.activated.map(({ shadow }) => ({
|
|
8545
|
-
epoch:
|
|
8632
|
+
epoch: request.epoch,
|
|
8546
8633
|
shadowId: shadow.id,
|
|
8547
8634
|
ctx,
|
|
8548
8635
|
shadow,
|
|
@@ -8553,11 +8640,22 @@ var ShadowMindRuntime = class {
|
|
|
8553
8640
|
}))
|
|
8554
8641
|
);
|
|
8555
8642
|
}
|
|
8556
|
-
launchShadow(
|
|
8643
|
+
launchShadow(options) {
|
|
8644
|
+
const {
|
|
8645
|
+
ctx,
|
|
8646
|
+
shadow,
|
|
8647
|
+
mainModel,
|
|
8648
|
+
fullModelId,
|
|
8649
|
+
context,
|
|
8650
|
+
availableTools,
|
|
8651
|
+
completionReview
|
|
8652
|
+
} = options;
|
|
8557
8653
|
const runId = randomUUID3();
|
|
8558
|
-
const runEpoch = this.epoch;
|
|
8654
|
+
const runEpoch = options.epoch ?? this.epoch;
|
|
8559
8655
|
const { tools, missing } = resolveShadowTools(shadow.tools, availableTools);
|
|
8560
|
-
|
|
8656
|
+
const activeRun = { shadow, epoch: runEpoch };
|
|
8657
|
+
if (completionReview) activeRun.completionReview = completionReview;
|
|
8658
|
+
this.active.set(runId, activeRun);
|
|
8561
8659
|
this.record("run-start", {
|
|
8562
8660
|
runId,
|
|
8563
8661
|
shadowId: shadow.id,
|
|
@@ -8573,30 +8671,39 @@ var ShadowMindRuntime = class {
|
|
|
8573
8671
|
cwd: ctx.cwd,
|
|
8574
8672
|
agentDir: this.agentDir,
|
|
8575
8673
|
mainSystemPrompt: ctx.getSystemPrompt(),
|
|
8576
|
-
// SAFETY: buildSessionContext returns Pi
|
|
8674
|
+
// SAFETY: buildSessionContext returns Pi messages; ShadowRunner treats them as read-only records.
|
|
8577
8675
|
messages: context.messages,
|
|
8578
8676
|
mainModel,
|
|
8579
8677
|
tools,
|
|
8580
8678
|
resolveModel: (id) => resolveModel(ctx, id),
|
|
8581
8679
|
modelAuthOk: (model) => ctx.modelRegistry.hasConfiguredAuth(model) || ctx.modelRegistry.isUsingOAuth(model),
|
|
8582
8680
|
mainThinkingLevel: ctx.thinkingLevel,
|
|
8583
|
-
onReport: (report) =>
|
|
8681
|
+
onReport: (report) => {
|
|
8682
|
+
if (completionReview) completionReview.accept(report);
|
|
8683
|
+
else this.acceptReport(report);
|
|
8684
|
+
}
|
|
8584
8685
|
}).then((result) => this.handleRunEnd(runId, shadow, result)).catch((error) => {
|
|
8686
|
+
const activeRun2 = this.active.get(runId);
|
|
8585
8687
|
this.active.delete(runId);
|
|
8688
|
+
this.completionReview.slotAvailable();
|
|
8689
|
+
this.completionReview.complete(activeRun2?.completionReview);
|
|
8586
8690
|
this.record("run-end", {
|
|
8587
8691
|
runId,
|
|
8588
8692
|
shadowId: shadow.id,
|
|
8589
8693
|
reason: "error",
|
|
8590
8694
|
error: error instanceof Error ? error.message : String(error)
|
|
8591
8695
|
});
|
|
8592
|
-
this.finalResponseQueue.slotAvailable();
|
|
8593
8696
|
});
|
|
8594
8697
|
}
|
|
8595
8698
|
handleRunEnd(runId, shadow, result) {
|
|
8596
8699
|
const activeRun = this.active.get(runId);
|
|
8597
8700
|
this.active.delete(runId);
|
|
8701
|
+
this.completionReview.slotAvailable();
|
|
8598
8702
|
const persisted = this.usageStore.add(result.usage);
|
|
8599
|
-
if (activeRun?.epoch !== this.epoch)
|
|
8703
|
+
if (activeRun?.epoch !== this.epoch) {
|
|
8704
|
+
this.completionReview.complete(activeRun?.completionReview);
|
|
8705
|
+
return;
|
|
8706
|
+
}
|
|
8600
8707
|
this.sessionUsage = addUsage(this.sessionUsage, result.usage);
|
|
8601
8708
|
this.recentRuns.push({
|
|
8602
8709
|
shadowName: shadow.name,
|
|
@@ -8607,7 +8714,7 @@ var ShadowMindRuntime = class {
|
|
|
8607
8714
|
this.record("run-end", { runId, shadowId: shadow.id, ...result });
|
|
8608
8715
|
if (this.latestContext && this.sessionLifetime.isActive)
|
|
8609
8716
|
this.updateStatus(this.latestContext);
|
|
8610
|
-
this.
|
|
8717
|
+
this.completionReview.complete(activeRun.completionReview);
|
|
8611
8718
|
void persisted.then(() => {
|
|
8612
8719
|
if (this.latestContext && this.sessionLifetime.isActive)
|
|
8613
8720
|
this.updateStatus(this.latestContext);
|
|
@@ -8655,10 +8762,11 @@ var ShadowMindRuntime = class {
|
|
|
8655
8762
|
)
|
|
8656
8763
|
];
|
|
8657
8764
|
this.updateStatus(ctx);
|
|
8765
|
+
return registry;
|
|
8658
8766
|
}
|
|
8659
8767
|
abortAll(reason) {
|
|
8660
8768
|
this.runner.abortAll();
|
|
8661
|
-
this.
|
|
8769
|
+
this.completionReview.invalidate();
|
|
8662
8770
|
this.batcher.clear();
|
|
8663
8771
|
this.record("runs-aborted", { reason, count: this.active.size });
|
|
8664
8772
|
}
|