@dsh-fly/dsh-plugin-fly-mode 0.1.6 → 0.1.7

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/cordis.patch.yml CHANGED
@@ -159,10 +159,7 @@
159
159
  group: true
160
160
  isolate:
161
161
  planMode: true
162
- # 评审姿态自己的服务(ReviewModeController 用 super(ctx,'reviewMode') **提供**服务 ⇒
163
162
  # 必须坐在带 isolate realm 的 group 里;否则它往 root realm 发布,挂载即被拒 —— 实测报错原文:
164
- # "Preset services require isolate realms: reviewMode")。
165
- reviewMode: true
166
163
  config:
167
164
  - id: plan-mode
168
165
  name: '@deepseek-ai/dsh-plan-mode'
@@ -187,8 +184,6 @@
187
184
  # 行保持**裸名**(内联形状的契约):生成器认这个裸名,再把它改写成模式包内的
188
185
  # `dsh-plugin-fly-mode/tools/dsh-plugin-fly-review/index.js`(同那 8 个内联工具行);
189
186
  # 包内的源码布局则是 `tools/index.js`(内联器照这个路径拷贝)。
190
- - id: tool-fly-review
191
- name: '@dsh-fly/dsh-plugin-fly-mode/tools/dsh-plugin-fly-review/index.js'
192
187
  # ── compaction ──────────────────────────────────────────────────────────────
193
188
 
194
189
  # `compaction-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-fly/dsh-plugin-fly-mode",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Fly 模式作为一个可安装的包:**两个 preset** —— Fly 安全模式(preset-fly,7 个安全工具内联 + smart)与 Fly 标准模式(preset-fly-standard,上游 standard 的行表 + smart,无安全工具);依赖单独发布的 dsh-plugin-fly-smart。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -514,16 +514,30 @@ export function apply(ctx) {
514
514
  render: (_args, value) => [{ type: 'text', text: value }],
515
515
  },
516
516
  async execute(args, exec) {
517
+ // 证据形状(规划 §5.1 的第一个试点):返回值是**信封 JSON**,不再是裸表格文本。
518
+ // 真值判据不变(observed.dump 里是 objdump 原文),新增 confidence 与 limitations ——
519
+ // 逼这个工具回答"这个结论看不见什么"(本仓最贵的那条教训)。
517
520
  const run = await runCmd(shell, ['objdump', '-h', args.path], exec)
518
- if (run.exitCode === 0 && run.stdout.trim() !== '') return run.stdout
521
+ const ok = run.exitCode === 0 && run.stdout.trim() !== ''
519
522
  // GNU binutils is not part of macOS. Instead of an opaque "command not found",
520
523
  // say which method failed and what to use instead (same shape as bin_readelf).
521
- const kind = await runCmd(shell, ['file', '-b', args.path], exec)
522
- return [
523
- `objdump -h is unavailable or failed on this host (exit ${run.exitCode}), so section headers could not be read with it`,
524
- `file(1) says: ${kind.stdout.trim() || '(nothing)'}`,
525
- 'GNU binutils does not ship with macOS; for Mach-O use rev_imports (nm -u) or rev_disasm (objdump -d)',
526
- ].join('\n')
524
+ const kind = ok ? null : await runCmd(shell, ['file', '-b', args.path], exec)
525
+ return JSON.stringify({
526
+ tool: 'bin_sections',
527
+ target: args.path,
528
+ operation: 'read ELF section headers (table only: no flags, no relocations, no disassembly)',
529
+ observed: {
530
+ dump: ok ? run.stdout : '',
531
+ method: ok ? 'objdump -h' : null,
532
+ format: ok ? null : ((kind.stdout || '').trim() || null),
533
+ exitCode: run.exitCode,
534
+ },
535
+ confidence: 'observed',
536
+ limitations: ok
537
+ ? ['读的是 objdump -h 的表格原文,未解析节标志 / 重定位 / 节内容']
538
+ : [`objdump -h 不在本机或读不了这个文件(exit ${run.exitCode}),observed.dump 因此为空 —— 节表没有被读到`,
539
+ 'macOS 不带 GNU binutils;Mach-O 用 rev_imports (nm -u) 或 rev_disasm (objdump -d)'],
540
+ }, null, 2)
527
541
  },
528
542
  }))
529
543
  }
@@ -454,8 +454,21 @@ export function apply(ctx) {
454
454
  parameters: {},
455
455
  output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
456
456
  async execute(_args, exec) {
457
+ // 证据形状(规划 §5.1):信封 JSON —— 原有读数整体进 observed,另加 confidence / limitations。
458
+ // 这个工具量的是**当前链路快照**(活的),所以 limitations 必须说清"这是一次采样、不是历史"。
457
459
  exec.signal.throwIfAborted()
458
- return JSON.stringify(await collectLink(shell, exec.signal), null, 2)
460
+ const snapshot = await collectLink(shell, exec.signal)
461
+ return JSON.stringify({
462
+ tool: 'diag_wifi_link',
463
+ target: 'the Wi-Fi interface of this host',
464
+ operation: 'read one passive snapshot of the local Wi-Fi link (no probe requests, no packets)',
465
+ observed: snapshot,
466
+ confidence: 'observed',
467
+ limitations: [
468
+ '一次采样:只说"这一刻"的取值,不说明它稳不稳(要历史用 diag_net_watch 记)',
469
+ '被动读系统报告:不主动发探测、不连接任何东西,因此看不到对端或邻居的行为',
470
+ ],
471
+ }, null, 2)
459
472
  },
460
473
  }))
461
474
 
@@ -1,47 +0,0 @@
1
- # dsh-plugin-fly-review —— Fly 评审姿态
2
-
3
- `/review` 开关一个**每会话的评审姿态**:只读地看、**先讲阻塞问题**、每条结论都带证据
4
- (`path:line` / 命令输出 / 复现步骤)、不夸奖也不复述;看完用 `exit_review_mode` 把判词**交用户裁决**。
5
-
6
- 它是 **agent 平面插件** —— 由 preset 的**行表**挂载(像 `@deepseek-ai/dsh-plan-mode` 那样),
7
- 不是 profile 层(没有 `dsh.bundle`)。形状照上游 plan 模式实现,名字与文案全归本包:
8
-
9
- | 机制 | 说明 |
10
- | --- | --- |
11
- | 姿态的效果 | **一段引导**(`review:policy` 段)—— 进/出姿态只改提示词段,**不改工具目录**(收口工具常驻注册) |
12
- | 状态 | 记在**会话日志**(`review/mode`),由会话投影折叠 ⇒ **resume / fork 自动恢复** |
13
- | 切态时机 | **下一个被接受的 turn 边界**(`agent/pre-step`)⇒ 同一步里前后一致 |
14
- | 命令 | `/review` 开 · `/review off` 关 · `/review <文字>` 附带一句焦点(文字会作为用户消息交给模型) |
15
- | 收口 | `exit_review_mode(review)` 弹裁决卡(`接受评审并退出` / `继续评审`),判词正文进卡片 detail |
16
-
17
- ## ⚠️ 只读是**引导**,不是强制
18
-
19
- 姿态**不限制权限** —— 这正是上游 plan 模式的口径(它的注释:sandbox mode 与 approval policy
20
- *独立*执行,不读也不写模式状态)。要**硬**只读,请配合权限/沙箱策略,或者用一个**不含写工具行**的 preset。
21
-
22
- 本包不在文案里把引导说成强制;命令回执里也明确写着这一点。
23
-
24
- ## 配置(行配置)
25
-
26
- ```yaml
27
- - id: fly-review
28
- name: 'dsh-plugin-fly-review'
29
- config:
30
- section: |
31
- …你的纪律文本(整段覆写内置那份)…
32
- ```
33
-
34
- · `section` 可省(用内置纪律文本)· **未知键在加载期直接抛**(配置写错要大声失败,不能被忽略)
35
-
36
- ## 纪律文本的来源
37
-
38
- 内置文本取自 `dsh-plugin-fly-smart` 的 `consultPrompt`
39
- (`tools/index.js:352-365`,2026-10-13 版),改写成"模式"口吻。用户裁决"不动 smart 自己"
40
- ⇒ 两份文本**共存**:本包一份、smart 一份。要收敛时走"抽库 + 两个消费者构建期内联"
41
- (同 `@dsh-fly/fly-panel-style` 那次的姿势),不做跨仓钉死。
42
-
43
- ## 测试
44
-
45
- ```bash
46
- node --test tests/ # 桩按真接缝形状喂:投影读写、命令注册、段注入、收口工具
47
- ```
@@ -1,37 +0,0 @@
1
- {
2
- "name": "dsh-plugin-fly-review",
3
- "version": "0.1.0",
4
- "description": "Fly 评审姿态(agent 平面插件):/review 开关一个只读评审姿态,引导模型先讲阻塞问题、给 path:line 证据,判词由用户裁决。照 @deepseek-ai/dsh-plan-mode 的形状实现。",
5
- "license": "MIT",
6
- "type": "module",
7
- "main": "./tools/index.js",
8
- "exports": {
9
- ".": "./tools/index.js",
10
- "./tools/index.js": "./tools/index.js",
11
- "./package.json": "./package.json"
12
- },
13
- "files": [
14
- "tools",
15
- "README.md"
16
- ],
17
- "scripts": {
18
- "test": "node --test tests/*.spec.mjs"
19
- },
20
- "devDependencies": {
21
- "@deepseek-ai/cordis": "^4.0.4",
22
- "@deepseek-ai/dsh-brand": "^0.2.0-rc.2",
23
- "@deepseek-ai/dsh-llm": "^0.2.0-rc.2",
24
- "@deepseek-ai/dsh-tools": "^0.2.0-rc.2",
25
- "@deepseek-ai/dsh-user-questions": "^0.2.0-rc.2",
26
- "zod": "^3.24.0"
27
- },
28
- "repository": {
29
- "type": "git",
30
- "url": "git+https://github.com/ethanchen669/dsh-fly.git",
31
- "directory": "packages/dsh-plugin-fly-review"
32
- },
33
- "publishConfig": {
34
- "access": "public",
35
- "registry": "https://registry.npmjs.org/"
36
- }
37
- }
@@ -1,328 +0,0 @@
1
- /**
2
- * `dsh-plugin-fly-review` —— **评审姿态**(agent 平面插件,照 `@deepseek-ai/dsh-plan-mode` 的形状 ✓)。
3
- *
4
- * 它做的事只有一件:当会话处在评审姿态时,给**每次模型请求**插一段引导(`review:policy` 段),
5
- * 告诉模型"只读地看、先讲阻塞问题、给 path:line 证据、不夸不复述";收口工具 `exit_review_mode`
6
- * 把判词交用户裁决,用户接受即退出姿态。
7
- *
8
- * 三条来自上游的实测事实(决定了本文件的写法):
9
- * · 姿态的**效果全部**来自那段引导 —— 进/出姿态只改提示词段,**不改工具目录**(收口工具常驻注册);
10
- * · 状态记在**会话日志**里(`session.append('review/mode', {active})`),由会话投影折叠
11
- * ⇒ resume / fork 自动恢复;切态落在**下一个被接受的 turn 边界**(`agent/pre-step`);
12
- * · **权限不归它管**:沙箱与审批策略独立执行 ⇒ 这里的"只读"是**引导**,不是强制(README 写明 ✓,
13
- * 不许在文案里说成强制 ✗)。
14
- *
15
- * 纪律文本取自 smart 的 `consultPrompt`(`dsh-plugin-fly-smart/tools/index.js:352-365`,2026-10-13 版)。
16
- * 用户裁决"**不动 smart 自己**" ⇒ 两份文本**共存**(本文件一份、smart 一份);要收敛时走
17
- * "抽库 + 两个消费者构建期内联"那条老路(同 `@dsh-fly/fly-panel-style` 的姿势),不在这里做跨仓钉死。
18
- */
19
- import { Service } from '@deepseek-ai/cordis'
20
- import { brandString } from '@deepseek-ai/dsh-brand'
21
- import { z } from 'zod'
22
- import { createUserMessage } from '@deepseek-ai/dsh-llm'
23
- import { defineTool } from '@deepseek-ai/dsh-tools'
24
- import { UserQuestionError } from '@deepseek-ai/dsh-user-questions'
25
-
26
- /** 本包的 npm 名(命令/段的定义 id 用它,插件实例的 data 归属也用它)。 */
27
- const PACKAGE = 'dsh-plugin-fly-review'
28
- /** 收口工具名。**常驻注册**(模式关闭时也在),所以进/出姿态不改工具目录。 */
29
- export const EXIT_REVIEW_MODE = 'exit_review_mode'
30
- /** 会话日志里的事件名;投影键同源。 */
31
- const MODE_EVENT = 'review/mode'
32
- const PROJECTION_KEY = 'review'
33
- /** 提示词段的段名与槽位(槽位沿用上游那一族的 policy 槽:`getSectionOrder` 是按名取值的具名槽)。 */
34
- const SECTION_NAME = 'review:policy'
35
- const SECTION_ORDER = 'PLAN_POLICY'
36
- /** 命令名。斜杠菜单里就是这个。 */
37
- const COMMAND_NAME = 'review'
38
- /** 裁决卡的选项标签:**逐字相等**分派(前缀只能当手打兜底;改了文案必须同改分支)。 */
39
- const ACCEPT_LABEL = '接受评审并退出'
40
- const KEEP_LABEL = '继续评审'
41
- const REVIEW_QUESTION_ID = 'review-mode'
42
- /** 默认纪律文本(行配置 `section` 可整段覆写)。 */
43
- export const DEFAULT_SECTION = [
44
- 'You are reviewing, not authoring: this session is in review mode.',
45
- '',
46
- 'Read-only posture: do not edit files, do not run commands with side effects, and do not "fix it while you are there".',
47
- 'If something must be changed, say what and why — the change itself happens after the review.',
48
- '',
49
- 'Order your findings:',
50
- '1. Blocking problems first — what cannot ship, what is provably wrong, what is missing.',
51
- '2. Then concrete additions, each one actionable.',
52
- '',
53
- 'Every claim carries evidence: a `path:line`, a command with its output, or steps that reproduce it.',
54
- 'No praise. Do not restate the thing under review. If you found nothing blocking, say that plainly.',
55
- '',
56
- 'Finish by handing the verdict to the user with `exit_review_mode`; their decision ends the posture.',
57
- ].join('\n')
58
-
59
- const REVIEW_DESCRIPTION = `Use only in review mode. Present your review verdict to the user with ${EXIT_REVIEW_MODE}: blocking problems first, then additions, every claim with evidence. On acceptance the review posture ends (the verdict does not authorise any further change by itself).`
60
-
61
- /**
62
- * 校验行配置。`section` 可省(用内置纪律文本 ✓);**未知键直接抛** —— 配置写错了要在
63
- * 加载期就大声失败,而不是被忽略(照上游 `resolveConfig` 的口径)。
64
- */
65
- export function resolveConfig(config = {}) {
66
- const section = config.section ?? DEFAULT_SECTION
67
- if (typeof section !== 'string' || section.trim() === '') {
68
- throw new Error(`${PACKAGE}: config.section must be a non-empty string (omit it to use the built-in discipline text)`)
69
- }
70
- const unknown = Object.keys(config).filter(key => key !== 'section')
71
- if (unknown.length > 0) throw new Error(`${PACKAGE}: unknown config key(s) ${unknown.join(', ')} — config is { section }`)
72
- return { section }
73
- }
74
-
75
- /** 会话日志投影:折叠命令与事件,重建"现在是不是评审姿态"。 */
76
- export const reviewProjectionDefinition = {
77
- key: PROJECTION_KEY,
78
- stateVersion: 1,
79
- stateSchema: z.object({
80
- active: z.boolean(),
81
- wanted: z.boolean().nullable(),
82
- running: z.object({ commandId: z.string(), wanted: z.boolean() }).strict().nullable(),
83
- activeAtLastHeader: z.boolean().nullable(),
84
- }).strict(),
85
- init: () => ({ active: false, wanted: null, running: null, activeAtLastHeader: null }),
86
- apply: (state, event) => {
87
- if (event.type === 'command/run' && event.data.name === COMMAND_NAME) {
88
- if (event.data.args === undefined) return state
89
- return { ...state, running: { commandId: event.data.commandId, wanted: event.data.args.trim() !== 'off' } }
90
- }
91
- if (event.type === 'command/done' && event.data.commandId === state.running?.commandId) {
92
- const wanted = event.data.kind === 'success' && state.running.wanted !== state.active ? state.running.wanted : null
93
- return { ...state, wanted, running: null }
94
- }
95
- if (event.type === MODE_EVENT) return { ...state, active: event.data.active, wanted: null }
96
- if (event.type === 'request/header') return { ...state, activeAtLastHeader: state.active }
97
- return state
98
- },
99
- wire: {
100
- viewSchema: z.object({ active: z.boolean(), pending: z.boolean() }),
101
- view: state => ({ active: state.active, pending: state.wanted !== null }),
102
- },
103
- }
104
-
105
- /** 评审姿态的服务:拥有日志态、段、命令与收口工具。 */
106
- export class ReviewModeController extends Service {
107
- static inject = ['tools', 'systemPrompt', 'sessionProjections']
108
-
109
- /** 校验过的纪律文本(默认内置,行配置可覆写)。 */
110
- section
111
-
112
- /** 等待"下一个被接受的 turn 边界"落地的选择(`narrate` = 是否顺带注入一句通知)。 */
113
- pendingIntents = new WeakMap()
114
-
115
- constructor(ctx, config = {}) {
116
- super(ctx, 'reviewMode')
117
- this.section = resolveConfig(config).section
118
- let disposed = false
119
-
120
- // ① 边界:下一个被接受的 turn 之前把待定选择写进日志(同一步里前后一致)。
121
- ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
122
- const decision = await next()
123
- const pending = this.pendingIntents.get(agent.session)
124
- if (decision.kind === 'reject' || signal.aborted || pending === undefined) return decision
125
- let narration
126
- try {
127
- narration = this.onBoundary(agent.session)
128
- } catch (error) {
129
- ctx.logger.warn(`${PACKAGE}: failed to append the selected review mode at step start: %o`, error)
130
- return decision
131
- }
132
- return !pending.narrate || narration === undefined ? decision : { ...decision, messages: [...decision.messages, narration] }
133
- })
134
-
135
- ctx.effect(() => () => { disposed = true }, `${PACKAGE}: close service lifetime`)
136
-
137
- // ② 引导段:只在姿态激活时非空(进/出姿态的全部效果就是这个)。
138
- ctx.systemPrompt.section({
139
- name: SECTION_NAME,
140
- order: ctx.systemPrompt.getSectionOrder(SECTION_ORDER),
141
- text: (context) => {
142
- if (context.agent === undefined) return ''
143
- const active = this.pendingIntents.get(context.agent.session)?.active ?? this.loggedActive(context.agent.session)
144
- return active ? this.section : ''
145
- },
146
- })
147
-
148
- // ③ 状态进会话日志 ⇒ resume/fork 自动恢复。
149
- ctx.sessionProjections.register(reviewProjectionDefinition)
150
-
151
- // ④ 命令:`/review` 开,`/review off` 关,`/review <文字>` 附带一句焦点。
152
- ctx.inject(['commands'], (commandCtx) => {
153
- commandCtx.commands.register({
154
- definitionId: brandString(PACKAGE),
155
- name: COMMAND_NAME,
156
- description: 'Enter or leave review mode',
157
- input: { hint: '[off|focus]', attachments: true },
158
- handler: ({ agent, rawInput, attachments }) => {
159
- const message = rawInput.trim()
160
- if (message === 'off' && attachments.length > 0) {
161
- return { kind: 'error', text: 'Attachments cannot accompany /review off.' }
162
- }
163
- if (message === 'off') {
164
- switch (this.set(agent, false)) {
165
- case 'committed': return { kind: 'success', text: 'Review mode off.' }
166
- case 'queued': return { kind: 'success', text: 'Leaving review mode (applies from the next step).' }
167
- case 'cancelled': return { kind: 'success', text: 'Review mode entry cancelled.' }
168
- default: return this.loggedActive(agent.session)
169
- ? { kind: 'success', text: 'Leaving review mode (applies from the next step).' }
170
- : { kind: 'success', text: 'Review mode is already inactive.' }
171
- }
172
- }
173
- const outcome = this.set(agent, true)
174
- if (message !== '' || attachments.length > 0) {
175
- agent.steer(createUserMessage({
176
- content: [...attachments, ...(message === '' ? [] : [{ type: 'text', text: message }])],
177
- source: { kind: PACKAGE },
178
- }))
179
- }
180
- return {
181
- kind: 'success',
182
- text: outcome === 'committed'
183
- ? 'Review mode on. Use /review off to leave. (The posture is guidance: the sandbox and approval policy still decide what runs.)'
184
- : 'Entering review mode (applies from the next step). Use /review off to leave.',
185
- }
186
- },
187
- })
188
- })
189
-
190
- // ⑤ 收口工具:常驻注册,把判词交用户裁决。
191
- ctx.tools.register(defineTool({
192
- name: EXIT_REVIEW_MODE,
193
- description: REVIEW_DESCRIPTION,
194
- parameters: {
195
- review: { type: 'string', required: true, description: 'The complete review verdict, as markdown, starting with a # heading that names it.' },
196
- },
197
- output: {
198
- schema: {
199
- type: 'object',
200
- additionalProperties: false,
201
- properties: { accepted: { type: 'boolean', const: true, required: true } },
202
- },
203
- // 收口旁白:这句是**给模型的指令**,所以口径必须是我们自己的 —— 接受评审**不授权**任何改动
204
- //(上游 plan 模式这里的原文是 "carry out the plan starting with your next step",评审不能这么写 ✗)。
205
- render: () => [{
206
- type: 'text',
207
- text: 'Review accepted — the review posture is over. The verdict is the user\'s now; acceptance by itself authorises no further change.',
208
- }],
209
- },
210
- // **箭头函数**:`defineTool` 调 execute 时不绑 this(上游 plan-mode 同样用箭头 ✓)。
211
- // 写成方法简写 ⇒ 运行期 `this` 是 undefined ⇒ 收口工具一调用就炸 ✗(本轮单测抓到的真 bug)。
212
- execute: async (args, exec) => {
213
- const agent = exec.agent
214
- if (agent === undefined) throw new Error(`${EXIT_REVIEW_MODE} requires a calling agent (no session to switch)`)
215
- if (!this.loggedActive(agent.session)) throw new Error(`${EXIT_REVIEW_MODE} is only available in review mode`)
216
- if (!/^#\s+\S/.test(args.review.trim())) throw new Error(`${EXIT_REVIEW_MODE} requires a non-empty markdown review starting with a # heading`)
217
- const interaction = ctx.get('userQuestions')
218
- if (interaction === undefined) throw new Error('no user-questions channel is available to decide the review; ask the user to switch the session mode instead')
219
- const answer = await interaction.ask({
220
- questions: [{
221
- id: REVIEW_QUESTION_ID,
222
- header: '评审裁决',
223
- question: '接受这份评审并退出评审姿态?',
224
- detail: args.review,
225
- options: [
226
- { label: ACCEPT_LABEL, description: '退出评审姿态。判词归你;这本身不授权任何后续改动。' },
227
- { label: KEEP_LABEL, description: '留在评审姿态,你的反馈回到模型,继续看。' },
228
- ],
229
- intent: { kind: 'review-mode', accept: ACCEPT_LABEL, callId: exec.callId },
230
- }],
231
- agent,
232
- signal: exec.signal,
233
- }).catch((cause) => {
234
- if (cause instanceof UserQuestionError && cause.code === 'ASK_CANCELLED') throw new Error('The user dismissed the review card to speak instead; stay in review mode and keep going.')
235
- throw cause
236
- })
237
- if (disposed) throw new Error('the review-mode service was reloaded while the review was under decision; present the review again')
238
- const items = answer.answers.filter(entry => entry.id === REVIEW_QUESTION_ID)
239
- const item = items.length === 1 ? items[0] : undefined
240
- if (item?.selected.length !== 1 || item.selected[0] !== ACCEPT_LABEL || item.custom !== undefined) {
241
- const feedback = item?.custom ?? ''
242
- throw new Error(feedback === ''
243
- ? 'The user chose to keep reviewing; revise the review and present it again.'
244
- : `The user chose to keep reviewing; their feedback: ${feedback}`)
245
- }
246
- this.pendingIntents.set(agent.session, { active: false, narrate: false })
247
- return { accepted: true }
248
- },
249
- }))
250
- }
251
-
252
- /** 日志态 + 待定选择。 */
253
- get(agent) {
254
- const active = this.loggedActive(agent.session)
255
- const pending = this.pendingIntents.get(agent.session)
256
- return pending === undefined ? { active } : { active, pending: pending.active }
257
- }
258
-
259
- /**
260
- * 选择评审姿态是否激活。
261
- * 回合之间直接落日志(不会再有 in-turn pre-step);回合开着就排队等下一个被接受的 pre-step。
262
- * 重复选择当前/已待定的状态是 no-op。
263
- */
264
- set(agent, active) {
265
- const session = agent.session
266
- if (active === (this.pendingIntents.get(session)?.active ?? this.loggedActive(session))) return 'noop'
267
- if (this.hasOpenTurn(session)) {
268
- this.pendingIntents.set(session, { active, narrate: true })
269
- return this.loggedActive(session) === active ? 'cancelled' : 'queued'
270
- }
271
- if (active === this.loggedActive(session)) {
272
- this.pendingIntents.delete(session)
273
- return 'cancelled'
274
- }
275
- session.append(MODE_EVENT, { active })
276
- this.pendingIntents.delete(session)
277
- const narration = this.narration(session, active)
278
- if (narration !== undefined) agent.inject(narration)
279
- return 'committed'
280
- }
281
-
282
- /** 把待定选择写进日志(下一个请求组装之前)。返回要追加的通知消息(若有)。 */
283
- onBoundary(session) {
284
- const pending = this.pendingIntents.get(session)
285
- if (pending === undefined) return undefined
286
- const target = pending.active
287
- this.pendingIntents.delete(session)
288
- if (target === this.loggedActive(session)) return undefined
289
- session.append(MODE_EVENT, { active: target })
290
- return this.narration(session, target)
291
- }
292
-
293
- /** 用户切远近态时的通知(只在"上一次请求头描述的是另一种姿态"时才说)。 */
294
- narration(session, target) {
295
- const told = this.loggedActiveAtLastHeader(session)
296
- if (told === undefined || told === target) return undefined
297
- const text = target
298
- ? 'The user switched this session into review mode: read-only posture, blocking problems first, evidence for every claim.'
299
- : 'The user switched this session back out of review mode.'
300
- return createUserMessage({
301
- content: [{ type: 'text', text }],
302
- source: { kind: PACKAGE, form: 'notice', summary: text },
303
- })
304
- }
305
-
306
- loggedActive(session) {
307
- return this.reviewState(session).active
308
- }
309
-
310
- loggedActiveAtLastHeader(session) {
311
- return this.reviewState(session).activeAtLastHeader ?? undefined
312
- }
313
-
314
- hasOpenTurn(session) {
315
- const state = this.ctx.sessionProjections.stateOf(session, 'turnBoundary')
316
- if (state === undefined) throw new Error(`${PACKAGE} requires the turnBoundary session projection`)
317
- return state.openTurnStartSeq !== null
318
- }
319
-
320
- /** 读本包的投影态;读不到就**大声抛**(静默返回 false 会让姿态看起来"没生效"却查不出原因)。 */
321
- reviewState(session) {
322
- const state = this.ctx.sessionProjections.stateOf(session, PROJECTION_KEY)
323
- if (state === undefined) throw new Error(`${PACKAGE} requires the ${PROJECTION_KEY} session projection`)
324
- return state
325
- }
326
- }
327
-
328
- export default ReviewModeController