@genee/omp-opsx-addon 0.3.0 → 0.4.0
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/README.md +82 -0
- package/index.ts +154 -64
- package/lib/agent-defs.ts +4 -4
- package/lib/edit-variant-pin.ts +4 -4
- package/lib/logger.ts +33 -0
- package/lib/model-roles.ts +270 -0
- package/lib/pipe-core.ts +784 -0
- package/lib/pipe-push.ts +467 -0
- package/lib/reachability-cache.ts +2 -1
- package/lib/reachability-probe.ts +3 -2
- package/lib/status-probe.ts +4 -3
- package/lib/unified-config.ts +42 -1
- package/lib/usage-poller.ts +208 -7
- package/lib/usage-redis-multiproc.worker.ts +5 -1
- package/lib/usage-render.ts +146 -95
- package/lib/usage-resolver.ts +10 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,6 +63,15 @@ tiers:
|
|
|
63
63
|
# 可选:role → tier 期望
|
|
64
64
|
role_tiers:
|
|
65
65
|
planner: top
|
|
66
|
+
|
|
67
|
+
# 可选:OMP 内置/自定义 model role → tier 期望(或 skip 排除)
|
|
68
|
+
# 仅影响 /pick-model 后 session 覆盖写入哪些模型;合法值 tiny/low/mid/high/top/skip
|
|
69
|
+
model_role_tiers:
|
|
70
|
+
advisor: skip
|
|
71
|
+
plan: skip
|
|
72
|
+
tiny: skip
|
|
73
|
+
vision: skip # vision 固定 high 档候选,仅 skip 有效(见下文)
|
|
74
|
+
task: low
|
|
66
75
|
```
|
|
67
76
|
|
|
68
77
|
---
|
|
@@ -73,6 +82,79 @@ role_tiers:
|
|
|
73
82
|
|
|
74
83
|
决策日志打印到 stderr:`[omp-opsx-addon] planner → anthropic/claude-opus-4-6 (tier=top, gap=0, remaining=0.80)`
|
|
75
84
|
|
|
85
|
+
|
|
86
|
+
### 子 agent 模型落点:session-scoped modelRoles
|
|
87
|
+
|
|
88
|
+
`/pick-model <selector>` / `refresh` / `reset` 应用选择结果时,插件**不**再按 agent 名写
|
|
89
|
+
`task.agentModelOverrides`,而是写 OMP session 运行时覆盖
|
|
90
|
+
`settings.overrideModelRoles({ …全 role… })`——覆盖**宿主当前已知的全部 model role**
|
|
91
|
+
(10 个内置 role + 自定义 role:`getKnownRoleIds` 从 cycleOrder/modelRoles/modelTags 汇总):
|
|
92
|
+
|
|
93
|
+
- 每个 role 跟随一个 tier 的选中模型;同一 tier 的多个 role 共用同一个模型(同候选集
|
|
94
|
+
排序结果唯一)。mid/high 复用 opsx 4 role 的选择(coder→mid、reviewer→high),
|
|
95
|
+
tiny/low/top 在同一过滤候选集上补选。
|
|
96
|
+
- **内置 role 默认 tier 映射**:
|
|
97
|
+
|
|
98
|
+
| role | tier | 触发场景 |
|
|
99
|
+
| --- | --- | --- |
|
|
100
|
+
| `tiny` | tiny | 极小任务 |
|
|
101
|
+
| `smol` | mid | scout 等快速任务 |
|
|
102
|
+
| `task` | mid | task 工具委派的全能力子 agent(与 scout 同档,防能力倒挂) |
|
|
103
|
+
| `commit` | low | AI commit |
|
|
104
|
+
| `default` | high | 主会话 / reviewer |
|
|
105
|
+
| `designer` | high | designer agent |
|
|
106
|
+
| `vision` | high | `/vision`、图像输入(**能力门控**,见下) |
|
|
107
|
+
| `slow` | top | 深度思考 |
|
|
108
|
+
| `plan` | top | plan mode |
|
|
109
|
+
| `advisor` | top | advisor 会话 |
|
|
110
|
+
|
|
111
|
+
**激活语义**:advisor/plan/tiny 等默认未配置的 role 被写入覆盖后,**只在对应功能真正
|
|
112
|
+
触发时**(plan mode、advisor 会话、auto-thinking、AI commit、`/vision`)才会解析到
|
|
113
|
+
selector 模型;功能不触发就不产生消耗。覆盖 session 结束即还原。
|
|
114
|
+
- **自定义 role**(cycleOrder/modelTags 里出现、不在内置表中的 role)默认跟随 `high`,
|
|
115
|
+
并一次性 warn 提示可用 `model_role_tiers` 调档或排除。
|
|
116
|
+
- **`model_role_tiers`(opsx.yml)**:OMP role 名 → `tiny/low/mid/high/top` 或 `skip`,
|
|
117
|
+
覆盖上表默认值;自定义 role 也可配。`skip` 是唯一逃生舱——该 role **不写** session
|
|
118
|
+
覆盖、保留你的 config 值(默认策略是全量覆盖,包含 config 里的 `modelRoles.*` 钉值,
|
|
119
|
+
不做「自动跳过钉值」)。**例外:`vision` role 固定使用 high 档候选**(仅在具备 image
|
|
120
|
+
能力的模型中挑选,见下条),`model_role_tiers.vision` **仅 `skip` 生效**——配
|
|
121
|
+
`top/low/…` 等其他 tier 值会被静默接受但**不改变其选档**(仍按 high 档选图像模型)。
|
|
122
|
+
保守配置示例:
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
model_role_tiers:
|
|
126
|
+
advisor: skip # 不想让 advisor 会话走 selector 模型
|
|
127
|
+
plan: skip # plan mode 保留 config 钉值
|
|
128
|
+
tiny: skip # 无 tiny 档模型时避免回落
|
|
129
|
+
vision: skip # 不覆盖 vision(vision 固定 high 档选图像模型,仅 skip 有效)
|
|
130
|
+
task: low # 显式降档
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- **vision 能力门控**:vision 候选**固定按 high 档**、只从声明 `input` 含 `image` 的模型
|
|
134
|
+
中挑选(不按 `model_role_tiers.vision` 的 tier 值选档,该键仅 `skip` 有效);当前选择里
|
|
135
|
+
没有图像模型时,vision role **不写覆盖**(warn 汇总为 `vision(capability)`,绝不写入
|
|
136
|
+
纯文本模型),vision 功能回落 config/default。恢复手段:换一个含图像模型的 selector
|
|
137
|
+
或用 `model_allowlist` 纳入图像模型;**`model_role_tiers.vision` 改 tier 绕不过门控、
|
|
138
|
+
也不改选档**。
|
|
139
|
+
- **auto role 的 agent 定义**在 `.omp/agents/*.md` frontmatter 注入带引号的 tier alias
|
|
140
|
+
(coder `model: "@smol"`,其余三者 `model: "@default"`);task 工具经 modelRoles 解析。
|
|
141
|
+
- 因此**所有经 role 解析的 agent 都跟随切换**——包括插件未枚举的 OMP 内置/bundled/
|
|
142
|
+
project agent(例如内置 scout 走 `smol`,`/pick-model` selector/refresh/reset 后
|
|
143
|
+
scout 自动换到选中模型)。
|
|
144
|
+
- auto role 与 omp-alias(如 `slow`,值无 `/`)的 dispatch 提示词**不传 `model=`**,
|
|
145
|
+
由 frontmatter alias 声明式接管;pinned 具体 provider/model 仍显式传 model,行为不变。
|
|
146
|
+
- 某 tier 补选没有可用模型(候选耗尽/allowlist 全过滤)时,该 tier 的各 role 不写覆盖、
|
|
147
|
+
warn 汇总(`no-pick`),命令不中断;无任何模型时计划为空,与既有「模型注册表不可用」
|
|
148
|
+
分支一致。
|
|
149
|
+
- 覆盖为 **session 作用域、不写盘**:`overrideModelRoles` 只写 runtime overlay,
|
|
150
|
+
session 结束即还原;不会修改你的 `~/.omp/agent/config.yml`。每次应用前先
|
|
151
|
+
`clearOverride('modelRoles')`,避免上一 selector 的旧 role 值残留;`reset` 重算无约束
|
|
152
|
+
默认并覆盖(**不清空**,否则回落 config 硬钉)。
|
|
153
|
+
|
|
154
|
+
> **手动清理建议**:若全局 `~/.omp/agent/config.yml` 里有 `modelRoles.smol` 钉在会耗尽的
|
|
155
|
+
> 套餐模型(scout 会一直打它直到被 session 覆盖压过),或 `task.agentModelOverrides` 残留
|
|
156
|
+
> 已删除的 `tester` 等 key,可手动删除这些钉值;插件运行期间会以 session 空串中和 per-agent
|
|
157
|
+
> 钉值,但不改动你的配置文件。
|
|
76
158
|
---
|
|
77
159
|
|
|
78
160
|
## 开发
|
package/index.ts
CHANGED
|
@@ -104,23 +104,36 @@ function handleSubagentEvent(payload: unknown, primarySessionId: string): void {
|
|
|
104
104
|
sessionUsageRecorder.record(canonicalizeProvider(provider), usage, subagentStreamKey);
|
|
105
105
|
} catch (err) {
|
|
106
106
|
// Silently fail to avoid breaking the plugin
|
|
107
|
-
|
|
107
|
+
getLogger().error('Failed to handle subagent event', { err });
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
-
import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from '@oh-my-pi/pi-coding-agent';
|
|
110
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext, Settings } from '@oh-my-pi/pi-coding-agent';
|
|
111
|
+
import { getKnownRoleIds, MODEL_ROLE_IDS } from '@oh-my-pi/pi-coding-agent/config/model-roles';
|
|
111
112
|
import type { Model, Api } from '@oh-my-pi/pi-catalog/types';
|
|
112
113
|
import type { UsageReport } from '@oh-my-pi/pi-ai';
|
|
113
114
|
import { createCachedScanner } from './lib/change-scanner.js';
|
|
114
115
|
import { installAgents } from './lib/agent-defs.js';
|
|
116
|
+
import {
|
|
117
|
+
applyRoleModelOverrides,
|
|
118
|
+
buildRoleOverridePlan,
|
|
119
|
+
dispatchModelHint,
|
|
120
|
+
frontmatterModel,
|
|
121
|
+
ROLE_TO_OMP_ALIAS,
|
|
122
|
+
OMP_ROLE_TO_TIER,
|
|
123
|
+
ROLE_CAPABILITY,
|
|
124
|
+
type SkippedRole,
|
|
125
|
+
} from './lib/model-roles.js';
|
|
115
126
|
import { buildStaticPrompt, buildDynamicPrompt } from './lib/system-prompt.js';
|
|
116
127
|
import { type ProviderHealth, canonicalizeProvider, listDetectableProviders, healthCacheKey } from './lib/usage-resolver.js';
|
|
117
128
|
import { readOpsxSettings, type ResolvedOpsxConfig, type RoleValue, OPSX_ROLES } from './lib/unified-config.js';
|
|
118
|
-
import { selectAllRoles, mergePlanProviders, type SelectorResult } from './lib/model-selector.js';
|
|
119
|
-
import { scoreModelWithProvider } from './lib/model-tiers.js';
|
|
129
|
+
import { selectAllRoles, selectModel, mergePlanProviders, type SelectorResult } from './lib/model-selector.js';
|
|
130
|
+
import { scoreModelWithProvider, type TierName } from './lib/model-tiers.js';
|
|
120
131
|
import { DIRECT_FETCHERS } from './lib/direct-fetchers.js';
|
|
121
132
|
import { createUsageWidget } from './lib/usage-widget.js';
|
|
122
133
|
import { renderUsageReports, type ConsumptionTrack } from './lib/usage-render.js';
|
|
123
134
|
import { markExhausted, markAvailable, getExhaustedProviders } from './lib/provider-status.js';
|
|
135
|
+
import { registerPipeCore } from './lib/pipe-core.js';
|
|
136
|
+
import { registerPipePush } from './lib/pipe-push.js';
|
|
124
137
|
import { scheduleStatusProbes } from './lib/status-probe.js';
|
|
125
138
|
import {
|
|
126
139
|
startSharedPoller,
|
|
@@ -158,6 +171,7 @@ import { filterSelectableModels, reachabilityFingerprint } from './lib/selection
|
|
|
158
171
|
import { parseAutoModelArgs, CHINA_EXCLUDE_FAMILIES, listFamilies, listProviders } from './lib/family-filter.js';
|
|
159
172
|
import { hasRateLimitError, hasRegionError, buildErrorDiagnosis, exclusionReasonStats } from './lib/error-scan.js';
|
|
160
173
|
import { applyEditVariantPin } from './lib/edit-variant-pin.js';
|
|
174
|
+
import { setOpsxLogger, getLogger } from './lib/logger.js';
|
|
161
175
|
type Role = 'coder' | 'reviewer' | 'planner' | 'proposal-reviewer';
|
|
162
176
|
|
|
163
177
|
/** Session-scoped selector constraint for `/pick-model <selector> [--china]`. */
|
|
@@ -266,24 +280,17 @@ export interface SelectionContext {
|
|
|
266
280
|
roleModels: Record<Role, string | undefined>;
|
|
267
281
|
/** Display label for each role from config, e.g. "auto", "smol", "deepseek-v4-pro". */
|
|
268
282
|
roleConfigs: Record<Role, string | undefined>;
|
|
283
|
+
/** Parsed role value kind for each role (auto | omp-alias | model | provider-model). */
|
|
284
|
+
roleKind: Record<Role, RoleValue['kind']>;
|
|
285
|
+
/** Full-role session overlay payload written to `overrideModelRoles` (all known OMP roles). */
|
|
286
|
+
roleOverrides: Record<string, string>;
|
|
287
|
+
/** Known roles left out of the overlay payload (config skip / capability gate / no pick). */
|
|
288
|
+
skippedRoles: SkippedRole[];
|
|
269
289
|
}
|
|
270
290
|
|
|
271
291
|
const DEFAULT_PLAN_PROVIDERS = new Set(['minimax-code-cn', 'opencode-go', 'zhipu-coding-plan', 'kimi', 'openai-codex']);
|
|
272
292
|
|
|
273
|
-
/** OPSX role → OMP role alias used in agent frontmatter `model: @<alias>`. */
|
|
274
|
-
const ROLE_TO_OMP_ALIAS: Record<Role, string> = {
|
|
275
|
-
coder: 'smol',
|
|
276
|
-
reviewer: 'default',
|
|
277
|
-
planner: 'default',
|
|
278
|
-
'proposal-reviewer': 'default',
|
|
279
|
-
};
|
|
280
293
|
|
|
281
|
-
/** OMP role alias → expected tier (without the `@`). */
|
|
282
|
-
const OMP_ROLE_TO_TIER: Record<string, string> = {
|
|
283
|
-
smol: 'mid',
|
|
284
|
-
default: 'high',
|
|
285
|
-
slow: 'top',
|
|
286
|
-
};
|
|
287
294
|
|
|
288
295
|
function roleValueToModelString(value: RoleValue): string | undefined {
|
|
289
296
|
if (value.kind === 'auto') return undefined;
|
|
@@ -314,19 +321,39 @@ function synthPinnedResult(role: string, modelString: string | undefined): Selec
|
|
|
314
321
|
export function buildDispatchPrompt(role: Role, task: string, selection: SelectionContext): string {
|
|
315
322
|
const agent = AGENT_MAP[role];
|
|
316
323
|
const sel = selection.selectorResults[role];
|
|
317
|
-
|
|
318
|
-
|
|
324
|
+
// auto roles (and omp-alias roles such as @slow) pass NO model: requestModel
|
|
325
|
+
// is the top-priority resolution layer and would shadow the frontmatter
|
|
326
|
+
// tier-role alias. Only concrete provider/model pins stay explicit.
|
|
327
|
+
const modelHint = dispatchModelHint(role, selection);
|
|
328
|
+
const parts = [`你的 opsx.${role} 模型由 role 自动解析。`];
|
|
319
329
|
if (modelHint) parts.push(`请通过 task 工具调用 ${agent} 并指定 model="${modelHint}"。`);
|
|
320
|
-
else parts.push(
|
|
330
|
+
else parts.push(`请直接通过 task 工具调用 ${agent}(无需指定 model,模型按 role 自动解析)。`);
|
|
321
331
|
if (sel?.decision) parts.push(`原因: ${sel.decision}`);
|
|
322
332
|
parts.push('直接用内置工具实现。');
|
|
323
333
|
return `请使用 task 工具委派给 ${agent}:\ntask(agent:"${agent}"${modelHint ? `, model:"${modelHint}"` : ''}, task:"${parts.join(' ')}\\n\\n${task}")`;
|
|
324
334
|
}
|
|
325
335
|
|
|
336
|
+
/**
|
|
337
|
+
* All model roles the host currently knows (10 built-in + custom roles from
|
|
338
|
+
* cycleOrder/modelRoles/modelTags). Host settings can throw during early
|
|
339
|
+
* startup or partial init; fall back to the built-in set rather than
|
|
340
|
+
* aborting selection (later recomputes pick up custom roles once ready).
|
|
341
|
+
*/
|
|
342
|
+
function safeGetKnownRoleIds(settings: Settings | null | undefined): string[] {
|
|
343
|
+
if (!settings) return [...MODEL_ROLE_IDS];
|
|
344
|
+
try {
|
|
345
|
+
return getKnownRoleIds(settings);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
getLogger().warn(`[omp-opsx-addon] getKnownRoleIds failed; using built-in model roles only: ${err instanceof Error ? err.message : String(err)}`);
|
|
348
|
+
return [...MODEL_ROLE_IDS];
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
326
352
|
function computeSelectionSync(
|
|
327
353
|
config: ResolvedOpsxConfig,
|
|
328
354
|
models: Model<Api>[] | null,
|
|
329
355
|
health: Map<string, ProviderHealth> | null,
|
|
356
|
+
knownRoleIds: string[],
|
|
330
357
|
regionBlockedModels?: Set<string>,
|
|
331
358
|
unreachableProviders?: Set<string>,
|
|
332
359
|
availableByProvider?: Map<string, string[] | null>,
|
|
@@ -348,16 +375,23 @@ function computeSelectionSync(
|
|
|
348
375
|
const selectorResults = {} as Record<Role, SelectorResult>;
|
|
349
376
|
const roleModels = {} as Record<Role, string | undefined>;
|
|
350
377
|
const roleConfigs = {} as Record<Role, string | undefined>;
|
|
378
|
+
const roleKind = {} as Record<Role, RoleValue['kind']>;
|
|
351
379
|
const planSet = mergePlanProviders(config.planProviders);
|
|
352
380
|
for (const role of OPSX_ROLES) {
|
|
353
381
|
const value = config.roles[role];
|
|
354
|
-
|
|
382
|
+
roleKind[role] = value.kind;
|
|
383
|
+
// Config label shown in /pick-model choices markdown (`角色 (label)`):
|
|
384
|
+
// auto roles display the OMP tier-role alias; pinned roles the pin.
|
|
385
|
+
roleConfigs[role] = value.kind === 'auto'
|
|
386
|
+
? ROLE_TO_OMP_ALIAS[role]
|
|
387
|
+
: (roleValueToModelString(value) ?? '?');
|
|
355
388
|
const alias = `@${ROLE_TO_OMP_ALIAS[role]}`;
|
|
356
389
|
if (value.kind === 'auto') {
|
|
357
390
|
if (filteredModels && filteredModels.length > 0) {
|
|
358
391
|
const ompRole = ROLE_TO_OMP_ALIAS[role];
|
|
359
|
-
const tier = (config.roleTiers[role]
|
|
360
|
-
|
|
392
|
+
const tier = (config.roleTiers[role]
|
|
393
|
+
?? (OMP_ROLE_TO_TIER as Record<string, TierName>)[ompRole]
|
|
394
|
+
?? 'mid') as TierName;
|
|
361
395
|
const roleTiers = { ...config.roleTiers, [role]: tier };
|
|
362
396
|
const result = selectAllRoles(
|
|
363
397
|
[role],
|
|
@@ -388,9 +422,56 @@ function computeSelectionSync(
|
|
|
388
422
|
roleModels[role] = modelString?.includes('/') ? modelString : undefined;
|
|
389
423
|
}
|
|
390
424
|
}
|
|
391
|
-
|
|
425
|
+
// change: pick-model-batch-all-roles — build the full-role session overlay:
|
|
426
|
+
// every host-known role (10 built-in + custom) follows one tier's picked
|
|
427
|
+
// model. mid/high reuse the opsx picks (coder/reviewer); tiny/low/top are
|
|
428
|
+
// supplemental picks over the same filtered candidate set; vision picks
|
|
429
|
+
// from image-capable candidates only. See design D2/D3.
|
|
430
|
+
let roleOverrides: Record<string, string> = {};
|
|
431
|
+
let skippedRoles: SkippedRole[] = [];
|
|
432
|
+
if (filteredModels && filteredModels.length > 0) {
|
|
433
|
+
const pickString = (r: SelectorResult): string | undefined =>
|
|
434
|
+
r.picked ? `${r.picked.provider}/${r.picked.id}` : undefined;
|
|
435
|
+
const tierPicks: Partial<Record<TierName, string | undefined>> = {
|
|
436
|
+
mid: pickString(selectorResults.coder),
|
|
437
|
+
high: pickString(selectorResults.reviewer),
|
|
438
|
+
};
|
|
439
|
+
for (const supplementalTier of ['tiny', 'low', 'top'] as const) {
|
|
440
|
+
const synthRole = `__tier_${supplementalTier}`;
|
|
441
|
+
const result = selectModel({
|
|
442
|
+
role: synthRole,
|
|
443
|
+
models: filteredModels,
|
|
444
|
+
roleTiers: { [synthRole]: supplementalTier },
|
|
445
|
+
tierOverrides: config.tierOverrides,
|
|
446
|
+
health: health ?? undefined,
|
|
447
|
+
allowlist: config.modelAllowlist,
|
|
448
|
+
planProviders: planSet,
|
|
449
|
+
});
|
|
450
|
+
tierPicks[supplementalTier] = pickString(result);
|
|
451
|
+
}
|
|
452
|
+
const capabilityPicks: Record<string, string | undefined> = {};
|
|
453
|
+
const visionModels = filteredModels.filter((m) => ROLE_CAPABILITY.vision(m));
|
|
454
|
+
if (visionModels.length > 0) {
|
|
455
|
+
const visionResult = selectModel({
|
|
456
|
+
role: 'vision',
|
|
457
|
+
models: visionModels,
|
|
458
|
+
roleTiers: { vision: 'high' },
|
|
459
|
+
tierOverrides: config.tierOverrides,
|
|
460
|
+
health: health ?? undefined,
|
|
461
|
+
allowlist: config.modelAllowlist,
|
|
462
|
+
planProviders: planSet,
|
|
463
|
+
});
|
|
464
|
+
capabilityPicks.vision = pickString(visionResult);
|
|
465
|
+
}
|
|
466
|
+
const plan = buildRoleOverridePlan(
|
|
467
|
+
{ knownRoleIds, ompRoleTiers: config.ompRoleTiers, tierPicks, capabilityPicks },
|
|
468
|
+
(m) => getLogger().warn(m),
|
|
469
|
+
);
|
|
470
|
+
roleOverrides = plan.overrides;
|
|
471
|
+
skippedRoles = plan.skipped;
|
|
472
|
+
}
|
|
473
|
+
return { selectorResults, roleModels, roleConfigs, roleKind, roleOverrides, skippedRoles };
|
|
392
474
|
}
|
|
393
|
-
|
|
394
475
|
function formatModelChoicesMarkdown(sel: SelectionContext, suffix: string = '', primaryModel?: Model<Api>): string {
|
|
395
476
|
const rows: string[] = [];
|
|
396
477
|
let totalModelCount = 0;
|
|
@@ -481,6 +562,9 @@ const sessionUsageRecorder: SessionUsageRecorder = createSessionUsageRecorder({
|
|
|
481
562
|
// ── plugin entry ────────────────────────────────────────────────────
|
|
482
563
|
|
|
483
564
|
export default (pi: ExtensionAPI) => {
|
|
565
|
+
// Route plugin logging through the host logger (rolling file) instead of
|
|
566
|
+
// console.*, which would corrupt the TUI. Injected before any log path runs.
|
|
567
|
+
setOpsxLogger(pi.logger);
|
|
484
568
|
// Runtime edit-variant pin for deepseek-v4-flash. The host hard-maps the
|
|
485
569
|
// model to the sloppy §/» marker grammar (it misreads hashline ranges),
|
|
486
570
|
// but the model repeatedly breaks sloppy control lines too — retry loops
|
|
@@ -492,7 +576,11 @@ export default (pi: ExtensionAPI) => {
|
|
|
492
576
|
// host reads the merged view directly, making the file equivalent.
|
|
493
577
|
applyEditVariantPin(pi, { 'deepseek-v4-flash': 'replace' });
|
|
494
578
|
const directory = process.cwd();
|
|
495
|
-
const warn = (msg: string) =>
|
|
579
|
+
const warn = (msg: string) => pi.logger.warn(msg);
|
|
580
|
+
// Roles known to the host at the moment of a (re)selection: computed per
|
|
581
|
+
// call so custom roles appearing after startup are covered on
|
|
582
|
+
// refresh/reset/selector/tick.
|
|
583
|
+
const currentKnownRoleIds = (): string[] => safeGetKnownRoleIds(pi.pi?.settings);
|
|
496
584
|
const opsxConfig = readOpsxSettings(directory, warn);
|
|
497
585
|
// Static orchestration rules — built once, reused every turn for cache stability.
|
|
498
586
|
const STATIC_PROMPT = buildStaticPrompt();
|
|
@@ -583,6 +671,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
583
671
|
effectiveConfig,
|
|
584
672
|
models,
|
|
585
673
|
health,
|
|
674
|
+
currentKnownRoleIds(),
|
|
586
675
|
regionBlockedModels,
|
|
587
676
|
reach.unreachable,
|
|
588
677
|
reach.availableByProvider,
|
|
@@ -596,30 +685,16 @@ export default (pi: ExtensionAPI) => {
|
|
|
596
685
|
});
|
|
597
686
|
return selectionPromise;
|
|
598
687
|
};
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
for (const role of OPSX_ROLES) {
|
|
610
|
-
const picked = sel.selectorResults[role]?.picked;
|
|
611
|
-
if (picked) {
|
|
612
|
-
overrides[AGENT_MAP[role]] = `${picked.provider}/${picked.id}`;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
if (Object.keys(overrides).length > 0 || force) {
|
|
616
|
-
settings.override('task.agentModelOverrides', overrides);
|
|
617
|
-
warn(
|
|
618
|
-
`[omp-opsx-addon] task.agentModelOverrides: ${Object.entries(overrides)
|
|
619
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
620
|
-
.join(', ')}`,
|
|
621
|
-
);
|
|
622
|
-
}
|
|
688
|
+
// Apply selector picks by TIER to session-scoped OMP modelRoles (smol =
|
|
689
|
+
// coder/mid, default = reviewer/high), so every role-resolving agent —
|
|
690
|
+
// including built-in scout (smol) — follows selector/refresh/reset.
|
|
691
|
+
// Persisted per-agent pins are neutralized at runtime with empty strings.
|
|
692
|
+
// pi.pi.settings is the host-process Settings singleton (the require()'d
|
|
693
|
+
// copy in node_modules is a separate, uninitialized instance — writing to
|
|
694
|
+
// it was a no-op). This thin wrapper is the single call site the
|
|
695
|
+
// constrained-period tick-reselect path (auto-model-family) also uses.
|
|
696
|
+
const applyRoleModel = (sel: SelectionContext): void => {
|
|
697
|
+
applyRoleModelOverrides(pi.pi?.settings, sel, warn);
|
|
623
698
|
};
|
|
624
699
|
|
|
625
700
|
// ── shared usage poller + widget ──────────────────────────────
|
|
@@ -980,7 +1055,12 @@ export default (pi: ExtensionAPI) => {
|
|
|
980
1055
|
// This fallback call is intentionally NOT threaded with familyConstraint:
|
|
981
1056
|
// it prefers the cached `selection`, and the fallback passes models=null so
|
|
982
1057
|
// filterSelectableModels' family branch never runs — it is not a reselect path.
|
|
983
|
-
const sel = selection ?? computeSelectionSync(
|
|
1058
|
+
const sel = selection ?? computeSelectionSync(
|
|
1059
|
+
opsxConfig,
|
|
1060
|
+
null,
|
|
1061
|
+
null,
|
|
1062
|
+
safeGetKnownRoleIds(pi.pi?.settings),
|
|
1063
|
+
);
|
|
984
1064
|
const changes = await changeScanner.scanChanges(directory, 'doing');
|
|
985
1065
|
// Layered injection: STATIC_PROMPT is byte-identical every turn → prefix cache
|
|
986
1066
|
// hit for [omp-base + skills + static]. Only the dynamic tail re-prefills.
|
|
@@ -994,17 +1074,14 @@ export default (pi: ExtensionAPI) => {
|
|
|
994
1074
|
|
|
995
1075
|
pi.on('session_start', async (_event, ctx) => {
|
|
996
1076
|
try {
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
if (v.kind === 'auto') return undefined;
|
|
1000
|
-
return roleValueToModelString(v);
|
|
1001
|
-
};
|
|
1077
|
+
const frontmatter = (role: Role): string | undefined =>
|
|
1078
|
+
frontmatterModel(role, opsxConfig.roles[role]);
|
|
1002
1079
|
await installAgents(
|
|
1003
1080
|
directory,
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1081
|
+
frontmatter('coder'),
|
|
1082
|
+
frontmatter('reviewer'),
|
|
1083
|
+
frontmatter('planner'),
|
|
1084
|
+
frontmatter('proposal-reviewer'),
|
|
1008
1085
|
);
|
|
1009
1086
|
warn('[omp-opsx-addon] agents ready (pick-model idle until /pick-model refresh)');
|
|
1010
1087
|
} catch (e) {
|
|
@@ -1020,7 +1097,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
1020
1097
|
const getApiKey = ctx.modelRegistry?.getApiKeyForProvider
|
|
1021
1098
|
? (provider: string) => ctx.modelRegistry.getApiKeyForProvider(provider)
|
|
1022
1099
|
: undefined;
|
|
1023
|
-
//
|
|
1100
|
+
// Provider exhaustion seen on the response/tool path; mark it locally
|
|
1024
1101
|
// without a usage fetcher — record it in the process-local binary status
|
|
1025
1102
|
// so the placeholder column reads 耗尽 (see lib/provider-status.ts).
|
|
1026
1103
|
markExhausted(canonical);
|
|
@@ -1119,6 +1196,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
1119
1196
|
effectiveConfig,
|
|
1120
1197
|
models,
|
|
1121
1198
|
health,
|
|
1199
|
+
currentKnownRoleIds(),
|
|
1122
1200
|
regionBlockedModels,
|
|
1123
1201
|
reach.unreachable,
|
|
1124
1202
|
reach.availableByProvider,
|
|
@@ -1154,7 +1232,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
1154
1232
|
}
|
|
1155
1233
|
}
|
|
1156
1234
|
selection = freshSel;
|
|
1157
|
-
|
|
1235
|
+
applyRoleModel(freshSel);
|
|
1158
1236
|
if (changes.length > 0) {
|
|
1159
1237
|
pi.sendMessage({ customType: 'pick-model', content: `✅ 模型已刷新:\n${changes.join('\n')}`, display: true }, { triggerTurn: false });
|
|
1160
1238
|
} else {
|
|
@@ -1181,6 +1259,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
1181
1259
|
effectiveConfig,
|
|
1182
1260
|
models,
|
|
1183
1261
|
health,
|
|
1262
|
+
currentKnownRoleIds(),
|
|
1184
1263
|
regionBlockedModels,
|
|
1185
1264
|
reach.unreachable,
|
|
1186
1265
|
reach.availableByProvider,
|
|
@@ -1205,7 +1284,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
1205
1284
|
if (newPrimary) await pi.setModel(newPrimary);
|
|
1206
1285
|
selection = sel;
|
|
1207
1286
|
selectionHealthKey = buildSelectionHealthKey(health, reach, familyConstraint);
|
|
1208
|
-
|
|
1287
|
+
applyRoleModel(sel);
|
|
1209
1288
|
pi.sendMessage({
|
|
1210
1289
|
customType: 'pick-model',
|
|
1211
1290
|
content: changes.length > 0
|
|
@@ -1265,6 +1344,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
1265
1344
|
effectiveConfig,
|
|
1266
1345
|
models,
|
|
1267
1346
|
health,
|
|
1347
|
+
currentKnownRoleIds(),
|
|
1268
1348
|
regionBlockedModels,
|
|
1269
1349
|
reach.unreachable,
|
|
1270
1350
|
reach.availableByProvider,
|
|
@@ -1324,7 +1404,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
1324
1404
|
if (newPrimary) await pi.setModel(newPrimary);
|
|
1325
1405
|
selection = sel;
|
|
1326
1406
|
selectionHealthKey = buildSelectionHealthKey(health, reach, familyConstraint);
|
|
1327
|
-
|
|
1407
|
+
applyRoleModel(sel);
|
|
1328
1408
|
const content = `✅ 已切换 selector 约束 (${formatFamilyConstraint(familyConstraint)}):\n${changes.join('\n')}${hint ? `\n${hint}` : ''}`;
|
|
1329
1409
|
pi.sendMessage({
|
|
1330
1410
|
customType: 'pick-model',
|
|
@@ -1379,4 +1459,14 @@ export default (pi: ExtensionAPI) => {
|
|
|
1379
1459
|
if (selection) return selection;
|
|
1380
1460
|
return ensureSelection(ctx);
|
|
1381
1461
|
}
|
|
1462
|
+
|
|
1463
|
+
// Cross-broker project-scoped pipe core: registers opsx_pipe_list /
|
|
1464
|
+
// _send / _recv / _wait and starts presence heartbeat + GC on
|
|
1465
|
+
// session_start (managed timers inside lib/pipe-core).
|
|
1466
|
+
registerPipeCore(pi);
|
|
1467
|
+
|
|
1468
|
+
// Cross-broker push/injector: watches the local mbox slot and injects
|
|
1469
|
+
// inbound pipe messages as custom (opsx-pipe) turns; /opsx-pipe mute
|
|
1470
|
+
// toggles the stop-injection flag (managed timers inside lib/pipe-push).
|
|
1471
|
+
registerPipePush(pi);
|
|
1382
1472
|
};
|
package/lib/agent-defs.ts
CHANGED
|
@@ -5,7 +5,7 @@ const MARKER = '<!-- @genee/omp-opsx-addon -->';
|
|
|
5
5
|
const CODE_REVIEWER_MD = (model?: string) => `---
|
|
6
6
|
name: code-reviewer
|
|
7
7
|
description: OpenSpec 代码审查 agent,审查代码实现并执行全局验证
|
|
8
|
-
tools: read, bash, glob, grep${model ? `\nmodel: ${model}` : ''}
|
|
8
|
+
tools: read, bash, glob, grep${model ? `\nmodel: "${model}"` : ''}
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
你是一位资深代码审查员。你根据 OpenSpec 提案和设计文档审查代码实现,并在审阅的同时执行最后一次全局验证。
|
|
@@ -71,7 +71,7 @@ ${MARKER}
|
|
|
71
71
|
const PROPOSAL_REVIEWER_MD = (model?: string) => `---
|
|
72
72
|
name: proposal-reviewer
|
|
73
73
|
description: OpenSpec 提案审查 agent,审查提案质量
|
|
74
|
-
tools: read, bash, glob, grep${model ? `\nmodel: ${model}` : ''}
|
|
74
|
+
tools: read, bash, glob, grep${model ? `\nmodel: "${model}"` : ''}
|
|
75
75
|
---
|
|
76
76
|
|
|
77
77
|
你是一位资深需求审查员。你审查 OpenSpec 提案的完整性和质量。
|
|
@@ -123,7 +123,7 @@ ${MARKER}
|
|
|
123
123
|
const CODER_MD = (model?: string) => `---
|
|
124
124
|
name: coder
|
|
125
125
|
description: OpenSpec 变更实现 agent,同步 tasks.md 进度
|
|
126
|
-
tools: read, write, edit, bash, glob, grep, todo${model ? `\nmodel: ${model}` : ''}
|
|
126
|
+
tools: read, write, edit, bash, glob, grep, todo${model ? `\nmodel: "${model}"` : ''}
|
|
127
127
|
skill: openspec-apply-change
|
|
128
128
|
---
|
|
129
129
|
|
|
@@ -184,7 +184,7 @@ ${MARKER}
|
|
|
184
184
|
const PLANNER_MD = (model?: string) => `---
|
|
185
185
|
name: planner
|
|
186
186
|
description: OpenSpec 提案规划 agent
|
|
187
|
-
tools: read, write, edit, bash, glob, grep, todo${model ? `\nmodel: ${model}` : ''}\nskill: openspec-propose
|
|
187
|
+
tools: read, write, edit, bash, glob, grep, todo${model ? `\nmodel: "${model}"` : ''}\nskill: openspec-propose
|
|
188
188
|
---
|
|
189
189
|
|
|
190
190
|
你是一个 OpenSpec 提案规划 agent。负责创建和更新 OpenSpec 变更提案文档。
|
package/lib/edit-variant-pin.ts
CHANGED
|
@@ -117,7 +117,7 @@ export function applyEditVariantPin(pi: ExtensionAPI, pin: Record<string, string
|
|
|
117
117
|
| { override(path: string, value: unknown): void; getAgentDir?: () => string }
|
|
118
118
|
| undefined;
|
|
119
119
|
if (!settings) {
|
|
120
|
-
|
|
120
|
+
pi.logger?.warn('[omp-opsx-addon] pi.pi.settings unavailable; edit variant pin skipped');
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -139,18 +139,18 @@ export function applyEditVariantPin(pi: ExtensionAPI, pin: Record<string, string
|
|
|
139
139
|
|
|
140
140
|
const configPath = resolveGlobalConfigPath(agentDir);
|
|
141
141
|
if (!configPath) {
|
|
142
|
-
|
|
142
|
+
pi.logger?.warn(
|
|
143
143
|
`[omp-opsx-addon] edit variant pin failed: no global config.yml found under ${agentDir}`,
|
|
144
144
|
);
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
const outcome = persistEditVariantPin(configPath, pin);
|
|
148
148
|
if (outcome === 'written') {
|
|
149
|
-
|
|
149
|
+
pi.logger?.warn(
|
|
150
150
|
`[omp-opsx-addon] edit variant pin persisted to ${configPath} (effective from the next session)`,
|
|
151
151
|
);
|
|
152
152
|
}
|
|
153
153
|
} catch (err) {
|
|
154
|
-
|
|
154
|
+
pi.logger?.warn('[omp-opsx-addon] edit variant pin failed', { err });
|
|
155
155
|
}
|
|
156
156
|
}
|
package/lib/logger.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件日志通道(模块级注入)。
|
|
3
|
+
*
|
|
4
|
+
* 主进程日志不再走 console.*(会在 omp TUI 里打爆界面),改走宿主
|
|
5
|
+
* pi.logger(滚动文件日志)。lib 模块拿不到 pi 对象,故提供模块级 holder:
|
|
6
|
+
* 入口 setOpsxLogger(pi.logger) 注入,lib 侧 getLogger() 读取。
|
|
7
|
+
* 未注入(测试场景直接调用叶子函数)时默认 console,输出与迁移前一致。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** 窄化日志接口:宿主 pi.logger 与 console 的结构交集,仅本插件使用的四级方法。 */
|
|
11
|
+
export interface OpsxLogger {
|
|
12
|
+
error(message: string, context?: Record<string, unknown>): void;
|
|
13
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
14
|
+
info(message: string, context?: Record<string, unknown>): void;
|
|
15
|
+
debug(message: string, context?: Record<string, unknown>): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 模块级当前 logger,默认 console(未注入/测试场景保持旧行为)。 */
|
|
19
|
+
let current: OpsxLogger = console;
|
|
20
|
+
|
|
21
|
+
/** 插件入口注入宿主 pi.logger(进程级单例,多实例重复注入幂等)。
|
|
22
|
+
* 空值注入忽略:保持 getLogger() 恒可用(console 兜底)——测试 mock 的
|
|
23
|
+
* pi 对象可能不含 logger,此时视同未注入,输出与迁移前一致。 */
|
|
24
|
+
export function setOpsxLogger(logger: OpsxLogger): void {
|
|
25
|
+
if (logger) {
|
|
26
|
+
current = logger;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 读取当前 logger。 */
|
|
31
|
+
export function getLogger(): OpsxLogger {
|
|
32
|
+
return current;
|
|
33
|
+
}
|