pi-shadow-mind 0.1.20 → 0.1.22
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-Evolution.md +12 -0
- package/DESIGN.md +6 -3
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/index.js +109 -11
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
package/DESIGN-Evolution.md
CHANGED
|
@@ -55,3 +55,15 @@
|
|
|
55
55
|
- 发生:PR #9 将报告正文挂到最近运行下;审计复现报告被后续沉默运行挤出视图、长报告生成超高常驻组件,以及面板关闭时显示命令只提示成功。
|
|
56
56
|
- 分析:最近运行与最近交付报告是不同的保留口径;常驻摘要面板也不适合承载完整正文。继续在 Runtime 中叠加可见性状态、命令解析和正文排版会扩大入口职责。
|
|
57
57
|
- 改变:ReportHistory 独立按交付保留 5 份报告,ReportBrowser 管理命令与会话生命周期,ReportViewer 提供限高滚动快照。定向回归经过实际交付、命令和组件渲染,验证 200 行正文完整可达、终端缩放、关闭重开、会话重置及非 TUI 模式。
|
|
58
|
+
|
|
59
|
+
## 2026-09-12 · 为完成检查增加按 Shadow 的轮次上限
|
|
60
|
+
|
|
61
|
+
- 发生:PR #11 与 issue #10 记录到同一用户任务内的反复整改:终检报告触发 Main 修订后,新的最终回复再次触发同一 Shadow,缺少停止条件;heartbeat 报告接力也会持续唤醒整改轮。
|
|
62
|
+
- 分析:跳过由 shadow-report 触发的修复轮会失去对修订结果的复核,统一预算又涉及整个调度层、不适合放进单个 Shadow 定义。因此停止条件放在单个 Shadow 的完成检查上:整批审查进入终态后才提交轮次,被新输入、新报告或会话切换失效的检查不消耗配额;超时或错误的运行同样提交一次尝试,避免对同一用户任务重复重试。存在待交付的 heartbeat 报告时先交付已知问题,避免在旧文本上启动终检并浪费有限的验收轮次。缺省与 0 都表示不限制,维持现有默认行为。
|
|
63
|
+
- 改变:新增 `final_response_rounds` frontmatter 字段与调度过滤;独立的 FinalResponseBudget 拥有轮次语义、计数与重置,Runtime 只接线 Session 生命周期与整批完成回调;CompletionReview 增加完成回调,FinalResponseQueue 增加启动门禁,batcher 有待交付报告时优先刷新。heartbeat 接力、跨 Shadow 统一预算与 stop 后不自动 follow-up 仍留在 issue #10。
|
|
64
|
+
|
|
65
|
+
## 2026-09-15 · active_for_models 支持排除语法与模型短名
|
|
66
|
+
|
|
67
|
+
- 发生:用户希望仅对特定主力模型(如 `gpt-6-astra`)关闭某 Shadow,而对其余模型保持启用。
|
|
68
|
+
- 分析:此前仅支持全量 `*` 或显式枚举白名单,针对特定模型禁用必须穷举所有其他合法模型,配置维护成本高;应支持以 `!` 开头的排除规则,支持模型短名与 glob 通配,并在纯排除配置时默认全量模型候选。
|
|
69
|
+
- 改变:`matchesModel` 区分正向与否定规则;命中任一排除规则立即过滤;纯排除列表默认继承 `*`;补充短名、通配、格式破坏与调度过滤回归。
|
package/DESIGN.md
CHANGED
|
@@ -109,8 +109,9 @@ frontmatter 包含以下运行字段:
|
|
|
109
109
|
| `debug` | 是否保存完整 Shadow Session 日志;默认 `false` |
|
|
110
110
|
| `activation_probability` | 每次 heartbeat 时独立激活的概率,范围为 `0` 到 `1`;默认 `0.3`,不影响 final_response |
|
|
111
111
|
| `trigger` | 激活方式,可包含 `heartbeat`、`final_response` 或两者;默认 `[heartbeat]` |
|
|
112
|
+
| `final_response_rounds` | 主 Agent 输出结束后的最大运行轮数。设为 `1` 时仅运行一次,避免反馈循环导致持续对话;缺省或 `0` 表示不限制 |
|
|
112
113
|
| `activation_tools` | heartbeat 候选工具过滤;该 Main 轮次命中任意指定工具名即可,默认 `[]` 表示不限制;final_response 独立于此过滤 |
|
|
113
|
-
| `active_for_models` | 适用于哪些 Main 模型;`"*"` 表示全部模型,省略时默认 `["*"]` |
|
|
114
|
+
| `active_for_models` | 适用于哪些 Main 模型;`"*"` 表示全部模型,省略时默认 `["*"]`;支持 `"!pattern"` 排除语法及通配符 |
|
|
114
115
|
| `run_with_model` | Shadow 自己使用的模型;省略时使用插件默认模型 |
|
|
115
116
|
| `thinking_level` | Shadow 使用的 thinking level;省略时使用插件默认值,再回退到 Main 会话当前生效等级 |
|
|
116
117
|
| `timeout_seconds` | Shadow 单次运行超时;省略时使用插件默认超时 |
|
|
@@ -118,7 +119,7 @@ frontmatter 包含以下运行字段:
|
|
|
118
119
|
|
|
119
120
|
`name` 只用于 `shadow-report` 和状态界面展示,不参与身份判断;省略时回退到最终解析出的 `id`。Markdown 正文就是 Shadow 的认知定义、长期职责和行为要求。
|
|
120
121
|
|
|
121
|
-
`active_for_models` 绑定的是被观察的 Main 模型,`run_with_model` 则指定 Shadow 自己运行时使用的模型。匹配前由 Pi 将 Main 的别名或简写解析为完整 `provider/model-id`;模型过滤使用完整 ID
|
|
122
|
+
`active_for_models` 绑定的是被观察的 Main 模型,`run_with_model` 则指定 Shadow 自己运行时使用的模型。匹配前由 Pi 将 Main 的别名或简写解析为完整 `provider/model-id`;模型过滤使用完整 ID、纯模型短名、精确值 `"*"` 及通配符进行匹配;支持以 `!` 开头的排除规则(如 `"!gpt-6-astra"` 或 `"!openai-codex/*"`),命中任一排除项即跳过该 Shadow;当列表仅包含排除规则时,默认正向范围为全量 `*`。模型选择优先级为:Shadow 的 `run_with_model` → 插件的 `default_shadow_model` → 激活时的当前 Main 模型。
|
|
122
123
|
|
|
123
124
|
如果显式配置的 `run_with_model` 当前不存在、未认证或不可用,本次激活失败并写入轻量运行事件,不自动换用其他模型。只有省略该字段时才使用插件默认 Shadow 模型。
|
|
124
125
|
|
|
@@ -346,7 +347,9 @@ Main 在会话中切换模型后,后续 heartbeat 直接依据新模型重新
|
|
|
346
347
|
|
|
347
348
|
每次最终回复检查拥有独立、递增的 review generation。独立的 completion-review 协调器统一拥有 generation、等待队列、运行完成计数和报告聚合,Runtime 只转发宿主生命周期事件与 Shadow 启停通知。同一 generation 的报告先暂存,所有匹配 Shadow 都进入终态后才合并为一次 `shadow-report`;因此较慢的同批检查不会在 Main 已开始修订后单独 steer。新的最终回复、新用户输入、其他报告触发的修订或 Session 关闭都会使旧 generation 失效,并清空尚未启动的项目;仍在收尾的旧运行只能记录 lifetime usage,不能再投递报告。异步刷新配置和 Registry 时还会同时校验启动请求与 epoch,防止等待 I/O 的旧最终回复在新任务中恢复调度。
|
|
348
349
|
|
|
349
|
-
检查结果沿用普通 `shadow-report` 的 steer/follow-up
|
|
350
|
+
检查结果沿用普通 `shadow-report` 的 steer/follow-up 机制。Main 根据聚合报告修正并再次给出最终回复后,默认会再次触发完成检查。若需避免反馈循环导致持续对话,可设置 `final_response_rounds: 1` 限制仅运行一轮(缺省或 `0` 表示不限制)。该轮次仅在整批审查进入终态且未被失效打断时提交;无报告以及超时或错误的运行同样计为一次尝试,被中途失效打断的审查不消耗轮次。轮次预算由独立的 FinalResponseBudget 持有,Runtime 只提交整批完成结果,并在新用户输入与 Session 开始时重置。
|
|
351
|
+
|
|
352
|
+
若在触发最终回复检查时,缓冲队列中已有待交付的过程巡检报告(如 heartbeat 产出的报告),系统会优先刷新交付已有报告以驱动 Main 修复已知问题,暂不启动针对旧文本的最终检查。这避免了终检门禁在尚未修复过程缺陷的半成品上被提前唤醒并白白消耗唯一的验收轮次。Shadow 没有发现时保持沉默,循环自然结束。
|
|
350
353
|
|
|
351
354
|
## 6. Shadow 的输出与介入
|
|
352
355
|
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ This Shadow is read-only. It reviews the implementation in parallel and reports
|
|
|
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. The global configuration `heartbeat_tools` restricts which Main tools can trigger a heartbeat evaluation; individual Shadows can use `activation_tools` to restrict their own heartbeat eligibility. Both default to `[]` (no filtering) and match any listed tool by exact name, e.g. `[edit, write]`. Config is refreshed before filtering each tool-bearing turn. These filters apply only to heartbeat, not `final_response`, and are separate from `tools`, which grants tools to the Shadow itself.
|
|
74
74
|
|
|
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.
|
|
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. Set the per-Shadow `final_response_rounds` to limit execution rounds after completion: `1` allows at most one round to prevent feedback loops, while omitted or `0` means unlimited. `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
|
|
package/README.zh-CN.md
CHANGED
|
@@ -69,7 +69,7 @@ tools: [read, grep]
|
|
|
69
69
|
|
|
70
70
|
每个 Shadow 可以通过 `trigger` 选择一种或两种激活方式。默认值是 `[heartbeat]`:主 Agent 的一次 `turn_end` 只有在该轮至少完成过一个工具调用时,扩展才进行全局 heartbeat 概率判断,符合条件的 Shadow 再按照各自的 `activation_probability` 独立抽选。纯文本对话轮次不会触发 heartbeat。全局配置 `heartbeat_tools` 限定哪些 Main 工具可以触发心跳判断;单个 Shadow 的 `activation_tools` 限定它自己的 heartbeat 候选资格。两者默认 `[]`(不限制),按工具名精确匹配,命中列表中任意一个即可,例如 `[edit, write]`。每个工具轮次先刷新配置再过滤。这两个过滤项只影响 heartbeat,不影响 `final_response`,也不同于授权 Shadow 自己使用工具的 `tools`。
|
|
71
71
|
|
|
72
|
-
使用 `trigger: [final_response]` 可以进行完成后审查。它在主 Agent 发出最终文字并完全 settled 后激活,不受 heartbeat 和 `activation_probability` 影响。同一最终回复的全部检查结束后,发现会合并为一次 `shadow-report` follow-up
|
|
72
|
+
使用 `trigger: [final_response]` 可以进行完成后审查。它在主 Agent 发出最终文字并完全 settled 后激活,不受 heartbeat 和 `activation_probability` 影响。同一最终回复的全部检查结束后,发现会合并为一次 `shadow-report` follow-up,较慢的同批检查不会介入已经修订的回复。可用 Shadow 级 `final_response_rounds` 限制主 Agent 输出结束后的运行轮数:设为 `1` 表示仅运行一轮,避免反馈循环导致持续对话;缺省或设为 `0` 表示不限制。`trigger: [heartbeat, final_response]` 会同时启用两种模式。`max_parallel_shadows` 仍然限制并发数;超出并发槽位的最终回复检查会排队,而不会被跳过。
|
|
73
73
|
|
|
74
74
|
每次激活都会创建一个全新的临时 Session。它继承主 Agent 原封不动的 system prompt,但只接收净化后的文本轨迹:思考内容会被移除,工具调用后仅保留简洁、确定性的结果概述。
|
|
75
75
|
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ var FinalResponseQueue = class {
|
|
|
26
26
|
this.pending.length = 0;
|
|
27
27
|
}
|
|
28
28
|
pump() {
|
|
29
|
+
if (this.options.canLaunch && !this.options.canLaunch()) return;
|
|
29
30
|
while (this.options.activeCount() < this.options.maxParallel() && this.pending.length > 0) {
|
|
30
31
|
const epoch = this.options.currentEpoch();
|
|
31
32
|
const activeShadowIds = this.options.activeShadowIds();
|
|
@@ -49,6 +50,7 @@ var CompletionReview = class {
|
|
|
49
50
|
maxParallel: () => this.host.maxParallel(),
|
|
50
51
|
activeCount: () => this.host.activeCount(),
|
|
51
52
|
activeShadowIds: () => this.host.activeShadowIds(),
|
|
53
|
+
canLaunch: () => this.host.canLaunch ? this.host.canLaunch() : true,
|
|
52
54
|
launch: ({ job, run }) => host.launch(job, run)
|
|
53
55
|
});
|
|
54
56
|
}
|
|
@@ -75,7 +77,8 @@ var CompletionReview = class {
|
|
|
75
77
|
requestId: request.id,
|
|
76
78
|
epoch: request.epoch,
|
|
77
79
|
remaining: jobs.length,
|
|
78
|
-
reports: []
|
|
80
|
+
reports: [],
|
|
81
|
+
shadowIds: jobs.map((job) => job.shadowId)
|
|
79
82
|
};
|
|
80
83
|
this.queue.enqueue(
|
|
81
84
|
jobs.map((job) => {
|
|
@@ -99,7 +102,9 @@ var CompletionReview = class {
|
|
|
99
102
|
this.state.remaining -= 1;
|
|
100
103
|
if (this.state.remaining > 0) return;
|
|
101
104
|
const reports = this.state.reports;
|
|
105
|
+
const shadowIds = this.state.shadowIds;
|
|
102
106
|
this.retire(requestId);
|
|
107
|
+
this.host.onReviewCompleted?.(shadowIds);
|
|
103
108
|
if (reports.length > 0) this.host.deliver(reports);
|
|
104
109
|
}
|
|
105
110
|
cancel(request) {
|
|
@@ -6986,6 +6991,10 @@ function parseShadowMarkdown(source, filePath) {
|
|
|
6986
6991
|
value.timeout_seconds,
|
|
6987
6992
|
"timeout_seconds"
|
|
6988
6993
|
),
|
|
6994
|
+
finalResponseRounds: optionalNonNegativeInteger(
|
|
6995
|
+
value.final_response_rounds,
|
|
6996
|
+
"final_response_rounds"
|
|
6997
|
+
),
|
|
6989
6998
|
activationTools: cleanStringArray(
|
|
6990
6999
|
value.activation_tools,
|
|
6991
7000
|
[],
|
|
@@ -7022,6 +7031,12 @@ function optionalPositiveNumber(value, name) {
|
|
|
7022
7031
|
throw new Error(`${name} must be positive`);
|
|
7023
7032
|
return value;
|
|
7024
7033
|
}
|
|
7034
|
+
function optionalNonNegativeInteger(value, name) {
|
|
7035
|
+
if (value === void 0) return void 0;
|
|
7036
|
+
if (!isFiniteNumber(value) || !Number.isInteger(value) || value < 0)
|
|
7037
|
+
throw new Error(`${name} must be a non-negative integer`);
|
|
7038
|
+
return value;
|
|
7039
|
+
}
|
|
7025
7040
|
function triggerArray(value, fallback, name) {
|
|
7026
7041
|
const values = typeof value === "string" ? [value] : value;
|
|
7027
7042
|
const items = cleanStringArray(values, fallback, name);
|
|
@@ -7110,6 +7125,7 @@ function serializeShadow(shadow) {
|
|
|
7110
7125
|
...shadow.runWithModel ? { run_with_model: shadow.runWithModel } : {},
|
|
7111
7126
|
...shadow.thinkingLevel ? { thinking_level: shadow.thinkingLevel } : {},
|
|
7112
7127
|
...shadow.timeoutSeconds !== void 0 ? { timeout_seconds: shadow.timeoutSeconds } : {},
|
|
7128
|
+
...shadow.finalResponseRounds !== void 0 ? { final_response_rounds: shadow.finalResponseRounds } : {},
|
|
7113
7129
|
...shadow.activationTools && shadow.activationTools.length ? { activation_tools: shadow.activationTools } : {},
|
|
7114
7130
|
tools: shadow.tools ?? []
|
|
7115
7131
|
};
|
|
@@ -7122,7 +7138,8 @@ ${(shadow.prompt ?? "").trim()}
|
|
|
7122
7138
|
}
|
|
7123
7139
|
function describeShadow(shadow) {
|
|
7124
7140
|
const actTools = shadow.activationTools.length ? ` act_tools=${shadow.activationTools.join(",")}` : "";
|
|
7125
|
-
|
|
7141
|
+
const finalRounds = shadow.finalResponseRounds ? ` final_rounds=${shadow.finalResponseRounds}` : "";
|
|
7142
|
+
return `${shadow.enabled ? "enabled" : "disabled"} ${shadow.id} (${shadow.name}) p=${shadow.activationProbability} trigger=${shadow.trigger.join(",")} models=${shadow.activeForModels.join(",")}${finalRounds}${actTools} tools=${shadow.tools.join(",") || "default"} file=${basename2(shadow.filePath)}`;
|
|
7126
7143
|
}
|
|
7127
7144
|
function definedOnly(value) {
|
|
7128
7145
|
return Object.fromEntries(
|
|
@@ -7130,6 +7147,27 @@ function definedOnly(value) {
|
|
|
7130
7147
|
);
|
|
7131
7148
|
}
|
|
7132
7149
|
|
|
7150
|
+
// src/final-response-budget.ts
|
|
7151
|
+
var FinalResponseBudget = class {
|
|
7152
|
+
counts = /* @__PURE__ */ new Map();
|
|
7153
|
+
canRun(shadow) {
|
|
7154
|
+
const limit = shadow.finalResponseRounds ?? 0;
|
|
7155
|
+
if (limit <= 0) return true;
|
|
7156
|
+
return this.used(shadow.id) < limit;
|
|
7157
|
+
}
|
|
7158
|
+
commit(shadowIds) {
|
|
7159
|
+
for (const id of shadowIds) {
|
|
7160
|
+
this.counts.set(id, this.used(id) + 1);
|
|
7161
|
+
}
|
|
7162
|
+
}
|
|
7163
|
+
used(shadowId) {
|
|
7164
|
+
return this.counts.get(shadowId) ?? 0;
|
|
7165
|
+
}
|
|
7166
|
+
reset() {
|
|
7167
|
+
this.counts.clear();
|
|
7168
|
+
}
|
|
7169
|
+
};
|
|
7170
|
+
|
|
7133
7171
|
// src/management-tools.ts
|
|
7134
7172
|
import { Type } from "typebox";
|
|
7135
7173
|
var ID = Type.String({ pattern: "^[a-z0-9][a-z0-9_-]*$" });
|
|
@@ -7157,6 +7195,7 @@ var SHADOW_FIELDS = {
|
|
|
7157
7195
|
run_with_model: Type.Optional(Type.String()),
|
|
7158
7196
|
thinking_level: Type.Optional(THINKING),
|
|
7159
7197
|
timeout_seconds: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
|
|
7198
|
+
final_response_rounds: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
7160
7199
|
activation_tools: Type.Optional(Type.Array(Type.String())),
|
|
7161
7200
|
tools: Type.Optional(Type.Array(Type.String())),
|
|
7162
7201
|
prompt: Type.Optional(Type.String())
|
|
@@ -7347,6 +7386,7 @@ function toPatch(raw) {
|
|
|
7347
7386
|
runWithModel: raw.run_with_model,
|
|
7348
7387
|
thinkingLevel: raw.thinking_level,
|
|
7349
7388
|
timeoutSeconds: raw.timeout_seconds,
|
|
7389
|
+
finalResponseRounds: raw.final_response_rounds,
|
|
7350
7390
|
activationTools: raw.activation_tools,
|
|
7351
7391
|
tools: raw.tools,
|
|
7352
7392
|
prompt: raw.prompt
|
|
@@ -7711,6 +7751,7 @@ function decideHeartbeat(options) {
|
|
|
7711
7751
|
candidates: [],
|
|
7712
7752
|
modelFiltered: [],
|
|
7713
7753
|
runningExcluded: [],
|
|
7754
|
+
roundFiltered: [],
|
|
7714
7755
|
toolFiltered: []
|
|
7715
7756
|
};
|
|
7716
7757
|
}
|
|
@@ -7752,11 +7793,13 @@ function decideHeartbeat(options) {
|
|
|
7752
7793
|
})),
|
|
7753
7794
|
modelFiltered,
|
|
7754
7795
|
runningExcluded,
|
|
7796
|
+
roundFiltered: [],
|
|
7755
7797
|
toolFiltered
|
|
7756
7798
|
};
|
|
7757
7799
|
}
|
|
7758
7800
|
function decideFinalResponse(options) {
|
|
7759
7801
|
const modelFiltered = [];
|
|
7802
|
+
const roundFiltered = [];
|
|
7760
7803
|
const candidates = options.shadows.filter((shadow) => {
|
|
7761
7804
|
if (!shadow.enabled || !hasTrigger(shadow, "final_response"))
|
|
7762
7805
|
return false;
|
|
@@ -7764,6 +7807,10 @@ function decideFinalResponse(options) {
|
|
|
7764
7807
|
modelFiltered.push(shadow.id);
|
|
7765
7808
|
return false;
|
|
7766
7809
|
}
|
|
7810
|
+
if (options.canRunFinalResponse && !options.canRunFinalResponse(shadow)) {
|
|
7811
|
+
roundFiltered.push(shadow.id);
|
|
7812
|
+
return false;
|
|
7813
|
+
}
|
|
7767
7814
|
return true;
|
|
7768
7815
|
}).map((shadow) => ({ shadow, roll: 0 }));
|
|
7769
7816
|
return {
|
|
@@ -7774,11 +7821,54 @@ function decideFinalResponse(options) {
|
|
|
7774
7821
|
selected: true
|
|
7775
7822
|
})),
|
|
7776
7823
|
modelFiltered,
|
|
7777
|
-
runningExcluded: []
|
|
7824
|
+
runningExcluded: [],
|
|
7825
|
+
roundFiltered
|
|
7778
7826
|
};
|
|
7779
7827
|
}
|
|
7780
7828
|
function matchesModel(shadow, fullModelId) {
|
|
7781
|
-
|
|
7829
|
+
if (!shadow.activeForModels || shadow.activeForModels.length === 0) {
|
|
7830
|
+
return false;
|
|
7831
|
+
}
|
|
7832
|
+
const negatives = [];
|
|
7833
|
+
const positives = [];
|
|
7834
|
+
for (const item of shadow.activeForModels) {
|
|
7835
|
+
const trimmed = item.trim();
|
|
7836
|
+
if (!trimmed) continue;
|
|
7837
|
+
if (trimmed.startsWith("!")) {
|
|
7838
|
+
const pattern = trimmed.slice(1).trim();
|
|
7839
|
+
if (pattern) negatives.push(pattern);
|
|
7840
|
+
} else {
|
|
7841
|
+
positives.push(trimmed);
|
|
7842
|
+
}
|
|
7843
|
+
}
|
|
7844
|
+
for (const neg of negatives) {
|
|
7845
|
+
if (modelPatternMatches(neg, fullModelId)) {
|
|
7846
|
+
return false;
|
|
7847
|
+
}
|
|
7848
|
+
}
|
|
7849
|
+
if (positives.length === 0) {
|
|
7850
|
+
return negatives.length > 0;
|
|
7851
|
+
}
|
|
7852
|
+
return positives.some((pos) => modelPatternMatches(pos, fullModelId));
|
|
7853
|
+
}
|
|
7854
|
+
function modelPatternMatches(pattern, fullModelId) {
|
|
7855
|
+
if (pattern === "*") return true;
|
|
7856
|
+
if (pattern === fullModelId) return true;
|
|
7857
|
+
const slashIndex = fullModelId.indexOf("/");
|
|
7858
|
+
const modelId = slashIndex !== -1 ? fullModelId.slice(slashIndex + 1) : fullModelId;
|
|
7859
|
+
if (!pattern.includes("/") && pattern === modelId) {
|
|
7860
|
+
return true;
|
|
7861
|
+
}
|
|
7862
|
+
if (pattern.includes("*")) {
|
|
7863
|
+
const regex = wildcardToRegex(pattern);
|
|
7864
|
+
if (regex.test(fullModelId)) return true;
|
|
7865
|
+
if (!pattern.includes("/") && regex.test(modelId)) return true;
|
|
7866
|
+
}
|
|
7867
|
+
return false;
|
|
7868
|
+
}
|
|
7869
|
+
function wildcardToRegex(pattern) {
|
|
7870
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
7871
|
+
return new RegExp(`^${escaped.replace(/\*/g, ".*")}$`);
|
|
7782
7872
|
}
|
|
7783
7873
|
function matchesActivationTools(shadow, executedTools) {
|
|
7784
7874
|
if (shadow.activationTools.length === 0) {
|
|
@@ -8682,13 +8772,16 @@ var ShadowMindRuntime = class {
|
|
|
8682
8772
|
runner = new ShadowRunner();
|
|
8683
8773
|
usageStore = new UsageStore(this.agentDir);
|
|
8684
8774
|
active = /* @__PURE__ */ new Map();
|
|
8775
|
+
finalResponseBudget = new FinalResponseBudget();
|
|
8685
8776
|
completionReview = new CompletionReview({
|
|
8686
8777
|
currentEpoch: () => this.epoch,
|
|
8687
8778
|
maxParallel: () => this.configStore.current.maxParallelShadows,
|
|
8688
8779
|
activeCount: () => this.active.size,
|
|
8689
8780
|
activeShadowIds: () => new Set([...this.active.values()].map(({ shadow }) => shadow.id)),
|
|
8781
|
+
canLaunch: () => !this.batcher.hasPending,
|
|
8690
8782
|
launch: (pending, review) => this.launchShadow({ ...pending, completionReview: review }),
|
|
8691
|
-
deliver: (reports) => void this.deliverReports(reports)
|
|
8783
|
+
deliver: (reports) => void this.deliverReports(reports),
|
|
8784
|
+
onReviewCompleted: (shadowIds) => this.finalResponseBudget.commit(shadowIds)
|
|
8692
8785
|
});
|
|
8693
8786
|
recentEvents = [];
|
|
8694
8787
|
recentRuns = [];
|
|
@@ -8720,6 +8813,7 @@ var ShadowMindRuntime = class {
|
|
|
8720
8813
|
this.latestContext = ctx;
|
|
8721
8814
|
this.modelCalls = 0;
|
|
8722
8815
|
this.completedWithFinalText = false;
|
|
8816
|
+
this.finalResponseBudget.reset();
|
|
8723
8817
|
this.sessionUsage = zeroUsage();
|
|
8724
8818
|
this.recentRuns.length = 0;
|
|
8725
8819
|
this.reports.reset();
|
|
@@ -8737,6 +8831,7 @@ var ShadowMindRuntime = class {
|
|
|
8737
8831
|
this.latestContext = ctx;
|
|
8738
8832
|
if (event.source === "extension") return;
|
|
8739
8833
|
this.completedWithFinalText = false;
|
|
8834
|
+
this.finalResponseBudget.reset();
|
|
8740
8835
|
this.epoch += 1;
|
|
8741
8836
|
this.abortAll("new-user-input");
|
|
8742
8837
|
});
|
|
@@ -8866,10 +8961,7 @@ var ShadowMindRuntime = class {
|
|
|
8866
8961
|
const fullModelId = `${ctx.model.provider}/${ctx.model.id}`;
|
|
8867
8962
|
const decision = decideHeartbeat({
|
|
8868
8963
|
heartbeatProbability: config.heartbeatProbability,
|
|
8869
|
-
availableSlots: Math.max(
|
|
8870
|
-
0,
|
|
8871
|
-
config.maxParallelShadows - this.active.size
|
|
8872
|
-
),
|
|
8964
|
+
availableSlots: Math.max(0, config.maxParallelShadows - this.active.size),
|
|
8873
8965
|
shadows: snapshot.shadows,
|
|
8874
8966
|
activeShadowIds: new Set(
|
|
8875
8967
|
[...this.active.values()].map(({ shadow }) => shadow.id)
|
|
@@ -8911,6 +9003,10 @@ var ShadowMindRuntime = class {
|
|
|
8911
9003
|
}
|
|
8912
9004
|
}
|
|
8913
9005
|
async onFinalResponse(ctx) {
|
|
9006
|
+
if (this.batcher.hasPending) {
|
|
9007
|
+
await this.batcher.flush();
|
|
9008
|
+
return;
|
|
9009
|
+
}
|
|
8914
9010
|
const request = this.completionReview.begin(this.epoch);
|
|
8915
9011
|
const snapshot = await this.refresh(ctx);
|
|
8916
9012
|
if (!this.completionReview.isCurrent(request)) {
|
|
@@ -8928,13 +9024,15 @@ var ShadowMindRuntime = class {
|
|
|
8928
9024
|
const fullModelId = `${mainModel.provider}/${mainModel.id}`;
|
|
8929
9025
|
const decision = decideFinalResponse({
|
|
8930
9026
|
shadows: snapshot.shadows,
|
|
8931
|
-
mainModelId: fullModelId
|
|
9027
|
+
mainModelId: fullModelId,
|
|
9028
|
+
canRunFinalResponse: (shadow) => this.finalResponseBudget.canRun(shadow)
|
|
8932
9029
|
});
|
|
8933
9030
|
this.record("final-response", {
|
|
8934
9031
|
candidates: decision.candidates,
|
|
8935
9032
|
activated: decision.activated.map(({ shadow }) => shadow.id),
|
|
8936
9033
|
...decision.modelFiltered.length ? { modelFiltered: decision.modelFiltered } : {},
|
|
8937
|
-
...decision.runningExcluded.length ? { runningExcluded: decision.runningExcluded } : {}
|
|
9034
|
+
...decision.runningExcluded.length ? { runningExcluded: decision.runningExcluded } : {},
|
|
9035
|
+
...decision.roundFiltered.length ? { roundFiltered: decision.roundFiltered } : {}
|
|
8938
9036
|
});
|
|
8939
9037
|
if (!decision.activated.length) {
|
|
8940
9038
|
this.completionReview.schedule(request, []);
|