@ai-agent-forge/plugin-steering-guard 0.0.0-stage → 0.88.16

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 CHANGED
@@ -1,3 +1,27 @@
1
- # Temporary Holding Version
1
+ # @agent-forge/plugin-steering-guard
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Agent Forge 中途引导落账提醒插件(`docs/design/中途引导与任务状态联动设计.md` §4.1 路线 A、§7 S5,第二层强化):检测到 run 进行中到达的用户引导时,向下一次模型请求的视图尾部注入一条 `display:false` 的提醒,提示模型按六类分类引导并按跨面落账顺序在本回合内落账。第一层常驻纪律由 goal 插件的 `TASK_TOOLS_GUIDE` 承载,本插件只做决策点提醒,不重复常驻文本。
4
+
5
+ bundled 分发,宿主通过 manifest 加载;禁用面为会话装配的 `disabledPluginIds`(`agent-forge.plugin.steering-guard`)。提醒只在请求视图内生存(不写 `context.messages`、不进 `newMessages`,不持久化),对用户不可见。
6
+
7
+ ## 行为契约
8
+
9
+ - 探测:宿主在 `_runInputHandlers` 内联 await `beforeInput` loop 钩子,先于 prompt 路径的 run 激活、也先于 steer/followUp 入队返回。钩子时刻读 `SessionAPI.isIdle()` 是确定性的:idle ⇒ 该输入开新 run(清零 pending),busy ⇒ 运行中(含压缩中)到达的引导(pending 加一)。
10
+ - 注入:`transformContext` 链路在 pending > 0 且任务面在场时,向请求视图尾部追加一条提醒并消费一个计数(同一引导只提醒一次;run 内连发多条引导各提醒一次)。
11
+ - 生效边界:`SessionAPI.getActiveTools` 与任务面工具(`goal_set`/`plan_set`/`todo_update`/`workflow_set`/`workflow_advance`)无交集时不注入(计数保留,下一请求再判);`getActiveTools` 不可用时视为在场(宁可多提醒);宿主无 `SessionAPI` 时插件保持惰性(一切输入按 run-start 处理,零提醒)。
12
+ - 压缩边界:不维护消息计数基线,不存在压缩致基线失真的问题;压缩期间 `isIdle()` 为 false,压缩窗口内到达的引导照常注入(确定性测试钉住)。
13
+
14
+ ## 探测结论(实施前源码验证)
15
+
16
+ - `input.received@1` 不流经 `CapabilityAPI.subscribe` 可见的总线:该事件经 `dispatchInputReceivedLifecycle` 只派发到 capability runtime 的 lifecycle 目录通道(`dispatchLifecycleImpl` 只读 `lifecycleRegistrations`),而 `subscribe`/`publish` 是独立的插件间总线(宿主从不向其发布宿主事件,`plan.changed` 等均由插件自身 `api.publish`)。
17
+ - lifecycle 通道的派发是 fire-and-forget 异步(宿主 `void dispatch...`),相对 run 激活(`runDefaultLoop` 置 `_isAgentRunActive`)无顺序保证——"新 prompt 开新 run"路径在派发时刻读 `isIdle()` 是竞态,无法确定性满足"首请求/新回合首请求不提醒"。
18
+ - 故探测事件选 `beforeInput` loop 钩子(与 `transformContext` 同属 loop-hook 公共通道),状态机与决策矩阵全为纯函数(`src/detector.ts`)。
19
+
20
+ ## 测试
21
+
22
+ `npm test`(确定性,零真实 AI/网络):
23
+
24
+ - `test/detector.test.ts`:决策矩阵逐格(首请求/新回合/steering/去重/重放/连发/压缩窗口/任务面缺席/默认在场)。
25
+ - `test/entry.test.ts`:公共面接线(钩子注册、信号映射、注入与去重、门控保留计数、dispose)。
26
+ - `test/manifests.test.ts`:三份清单(plugin.json / agent-forge.json / package.json)一致性。
27
+ - `test/registration.test.ts`:manifest 通道真实装载(需先构建 dist:`npm run build`)。
@@ -0,0 +1,19 @@
1
+ {
2
+ "packageFormatVersion": 1,
3
+ "id": "agent-forge.plugin.steering-guard",
4
+ "pluginVersion": "0.88.16",
5
+ "apiVersion": "1",
6
+ "minHostVersion": "0.88.0",
7
+ "platform": {
8
+ "os": [
9
+ "windows",
10
+ "macos",
11
+ "linux"
12
+ ],
13
+ "runtime": [
14
+ {
15
+ "name": "node"
16
+ }
17
+ ]
18
+ }
19
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * 中途引导落账提醒的探测核心(设计 `docs/design/中途引导与任务状态联动设计.md`
3
+ * §4.1 路线 A、§7 S5;第二层强化,第一层常驻纪律在 goal 插件的
4
+ * TASK_TOOLS_GUIDE)。全部逻辑为纯函数:输入是输入到达信号 + 既有状态,输出是
5
+ * 是否注入与新状态;entry.ts 只做公共面接线与闭包状态持有。
6
+ *
7
+ * 探测选型(实施前源码验证,见 README "探测结论"):
8
+ * - `input.received@1` 不流经 `CapabilityAPI.subscribe` 可见的插件间事件总线
9
+ * (宿主从不向该总线发布宿主事件;它只走 lifecycle 目录通道),且其派发为
10
+ * fire-and-forget 异步(`agent-session.ts` 的 `void dispatchInputReceived...`),
11
+ * 相对 run 激活(`runDefaultLoop` 置 `_isAgentRunActive`)无顺序保证——在派发
12
+ * 时刻读 `isIdle()` 对"新 prompt 开新 run"路径是竞态,无法确定性地满足
13
+ * "首请求/新回合首请求不提醒"。
14
+ * - 因此探测事件取 `beforeInput` loop 钩子:宿主在 `_runInputHandlers` 内联
15
+ * await 该链,先于 prompt() 的 isStreaming 分支与 run 激活、也先于 steer/
16
+ * followUp 入队返回。钩子调用时刻读 `isIdle()` 因此是确定性的:
17
+ * idle === 该输入开新 run("run-start"),busy === 该输入是运行中到达的
18
+ * steering/followUp("steer")。这正是本插件需要的"run 内新增 user 消息 vs
19
+ * 新 run 首条"判别器,且与 transformContext 同属 loop-hook 公共通道。
20
+ *
21
+ * 压缩边界:本路线不维护消息计数基线,不存在"压缩后基线失真"问题。压缩期间
22
+ * `isIdle()` 为 false(isIdle = !runActive && !compacting),压缩窗口内到达的
23
+ * 引导照常置 pending,压缩后的下一请求照常注入——"压缩不丢失引导提醒"由构造
24
+ * 保证(测试矩阵钉住)。
25
+ */
26
+ /** 注入消息的 customType(标记来源;display:false,仅存在于该次请求视图)。 */
27
+ export declare const STEERING_GUARD_CUSTOM_TYPE = "steering_guard";
28
+ /**
29
+ * 任务面工具名(生效边界,设计 §3.7):这些工具全部不在场(readonly 会话或
30
+ * 用户禁用任务面插件)时,引导落账无承载面,不注入提醒。
31
+ */
32
+ export declare const TASK_FACE_TOOL_NAMES: readonly string[];
33
+ /** 注入到请求视图尾部的提醒文本(英文,≤60 词,契约固定措辞)。 */
34
+ export declare const STEERING_GUARD_REMINDER_TEXT = "[steering-guard] Mid-run user input arrived. Apply the steering discipline: classify it (background/add/modify/cancel/pivot/fork), settle required state changes this turn via the task tools \u2014 workflow-level consequences first, then plan/goal, todos last; touch only affected records; a cancelled todo needs the user's line as evidence.";
35
+ /**
36
+ * 结构化镜像宿主 `core/messages.ts` 的 CustomMessage(同 loop-guard 先例:内核
37
+ * 类型不跨入插件面,字段漂移由宿主 e2e 拦截)。本插件只生产 display:false 的
38
+ * 模型面提醒,不写 context.messages、不进 newMessages,故不持久化。
39
+ */
40
+ export interface SteeringGuardReminderMessage {
41
+ role: "custom";
42
+ customType: string;
43
+ content: string;
44
+ display: boolean;
45
+ timestamp: number;
46
+ }
47
+ /** display:false 的引导落账提醒消息(transformContext 注入用,同请求视图内生存)。 */
48
+ export declare function createSteeringGuardReminder(): SteeringGuardReminderMessage;
49
+ /**
50
+ * 输入到达信号:"run-start" = 该输入开启新 run(isIdle 为 true 时收到);
51
+ * "steer" = 该输入在 run(或压缩)进行中到达。
52
+ */
53
+ export type SteeringInputSignal = "run-start" | "steer";
54
+ /**
55
+ * 吸收一个输入到达信号,返回新的 pending 计数。
56
+ *
57
+ * - "run-start":新 run 的首条输入就是本轮 prompt 本身,不是中途引导——清零
58
+ * pending(同时丢弃上一 run 中止后残留的未消费计数,防止跨 run 泄漏提醒)。
59
+ * - "steer":运行中到达一条引导,pending 加一。计数而非布尔:同一 run 内连发
60
+ * 两条引导各提醒一次(每次请求消费一条;两条在同一次请求前到达时,分两次
61
+ * 请求各注入一次)。
62
+ */
63
+ export declare function absorbSteeringInputSignal(pendingSteerCount: number, signal: SteeringInputSignal): number;
64
+ /** 一次注入判定的输出:是否注入 + 消费后的新 pending 计数。 */
65
+ export interface SteeringReminderDecision {
66
+ readonly inject: boolean;
67
+ readonly pendingSteerCount: number;
68
+ }
69
+ /**
70
+ * 注入判定:pending > 0 且任务面在场时注入一条提醒并消费一个计数;任务面缺席
71
+ * 时保留计数(本请求不注入,下一请求再判——工具面可能在 run 内动态恢复);
72
+ * pending 为 0 时维持去重(同一引导只提醒一次、重放不重复提醒)。
73
+ */
74
+ export declare function decideSteeringReminder(pendingSteerCount: number, taskFacePresent: boolean): SteeringReminderDecision;
75
+ /**
76
+ * 任务面在场判定(生效边界,设计 §3.7):`getActiveTools` 不可用(SessionAPI
77
+ * 可选面缺席)时视为在场——宁可多提醒;在场列表与任务面工具无交集时不注入。
78
+ */
79
+ export declare function isTaskFacePresent(activeTools: readonly string[] | undefined): boolean;
80
+ //# sourceMappingURL=detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,uGAAuD;AACvD,eAAO,MAAM,0BAA0B,mBAAmB,CAAC;AAE3D;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,SAAS,MAAM,EAMjD,CAAC;AAEF,iGAAuC;AACvC,eAAO,MAAM,4BAA4B,yVACyS,CAAC;AAEnV;;;;GAIG;AACH,MAAM,WAAW,4BAA4B;IAC5C,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,8GAA8D;AAC9D,wBAAgB,2BAA2B,IAAI,4BAA4B,CAQ1E;AAED;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,OAAO,CAAC;AAExD;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,iBAAiB,EAAE,MAAM,EAAE,MAAM,EAAE,mBAAmB,GAAG,MAAM,CAExG;AAED,qFAAyC;AACzC,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACnC;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,iBAAiB,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,GAAG,wBAAwB,CAIpH;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAGrF","sourcesContent":["/**\n * 中途引导落账提醒的探测核心(设计 `docs/design/中途引导与任务状态联动设计.md`\n * §4.1 路线 A、§7 S5;第二层强化,第一层常驻纪律在 goal 插件的\n * TASK_TOOLS_GUIDE)。全部逻辑为纯函数:输入是输入到达信号 + 既有状态,输出是\n * 是否注入与新状态;entry.ts 只做公共面接线与闭包状态持有。\n *\n * 探测选型(实施前源码验证,见 README \"探测结论\"):\n * - `input.received@1` 不流经 `CapabilityAPI.subscribe` 可见的插件间事件总线\n * (宿主从不向该总线发布宿主事件;它只走 lifecycle 目录通道),且其派发为\n * fire-and-forget 异步(`agent-session.ts` 的 `void dispatchInputReceived...`),\n * 相对 run 激活(`runDefaultLoop` 置 `_isAgentRunActive`)无顺序保证——在派发\n * 时刻读 `isIdle()` 对\"新 prompt 开新 run\"路径是竞态,无法确定性地满足\n * \"首请求/新回合首请求不提醒\"。\n * - 因此探测事件取 `beforeInput` loop 钩子:宿主在 `_runInputHandlers` 内联\n * await 该链,先于 prompt() 的 isStreaming 分支与 run 激活、也先于 steer/\n * followUp 入队返回。钩子调用时刻读 `isIdle()` 因此是确定性的:\n * idle === 该输入开新 run(\"run-start\"),busy === 该输入是运行中到达的\n * steering/followUp(\"steer\")。这正是本插件需要的\"run 内新增 user 消息 vs\n * 新 run 首条\"判别器,且与 transformContext 同属 loop-hook 公共通道。\n *\n * 压缩边界:本路线不维护消息计数基线,不存在\"压缩后基线失真\"问题。压缩期间\n * `isIdle()` 为 false(isIdle = !runActive && !compacting),压缩窗口内到达的\n * 引导照常置 pending,压缩后的下一请求照常注入——\"压缩不丢失引导提醒\"由构造\n * 保证(测试矩阵钉住)。\n */\n\n/** 注入消息的 customType(标记来源;display:false,仅存在于该次请求视图)。 */\nexport const STEERING_GUARD_CUSTOM_TYPE = \"steering_guard\";\n\n/**\n * 任务面工具名(生效边界,设计 §3.7):这些工具全部不在场(readonly 会话或\n * 用户禁用任务面插件)时,引导落账无承载面,不注入提醒。\n */\nexport const TASK_FACE_TOOL_NAMES: readonly string[] = [\n\t\"goal_set\",\n\t\"plan_set\",\n\t\"todo_update\",\n\t\"workflow_set\",\n\t\"workflow_advance\",\n];\n\n/** 注入到请求视图尾部的提醒文本(英文,≤60 词,契约固定措辞)。 */\nexport const STEERING_GUARD_REMINDER_TEXT =\n\t\"[steering-guard] Mid-run user input arrived. Apply the steering discipline: classify it (background/add/modify/cancel/pivot/fork), settle required state changes this turn via the task tools — workflow-level consequences first, then plan/goal, todos last; touch only affected records; a cancelled todo needs the user's line as evidence.\";\n\n/**\n * 结构化镜像宿主 `core/messages.ts` 的 CustomMessage(同 loop-guard 先例:内核\n * 类型不跨入插件面,字段漂移由宿主 e2e 拦截)。本插件只生产 display:false 的\n * 模型面提醒,不写 context.messages、不进 newMessages,故不持久化。\n */\nexport interface SteeringGuardReminderMessage {\n\trole: \"custom\";\n\tcustomType: string;\n\tcontent: string;\n\tdisplay: boolean;\n\ttimestamp: number;\n}\n\n/** display:false 的引导落账提醒消息(transformContext 注入用,同请求视图内生存)。 */\nexport function createSteeringGuardReminder(): SteeringGuardReminderMessage {\n\treturn {\n\t\trole: \"custom\",\n\t\tcustomType: STEERING_GUARD_CUSTOM_TYPE,\n\t\tcontent: STEERING_GUARD_REMINDER_TEXT,\n\t\tdisplay: false,\n\t\ttimestamp: Date.now(),\n\t};\n}\n\n/**\n * 输入到达信号:\"run-start\" = 该输入开启新 run(isIdle 为 true 时收到);\n * \"steer\" = 该输入在 run(或压缩)进行中到达。\n */\nexport type SteeringInputSignal = \"run-start\" | \"steer\";\n\n/**\n * 吸收一个输入到达信号,返回新的 pending 计数。\n *\n * - \"run-start\":新 run 的首条输入就是本轮 prompt 本身,不是中途引导——清零\n * pending(同时丢弃上一 run 中止后残留的未消费计数,防止跨 run 泄漏提醒)。\n * - \"steer\":运行中到达一条引导,pending 加一。计数而非布尔:同一 run 内连发\n * 两条引导各提醒一次(每次请求消费一条;两条在同一次请求前到达时,分两次\n * 请求各注入一次)。\n */\nexport function absorbSteeringInputSignal(pendingSteerCount: number, signal: SteeringInputSignal): number {\n\treturn signal === \"run-start\" ? 0 : pendingSteerCount + 1;\n}\n\n/** 一次注入判定的输出:是否注入 + 消费后的新 pending 计数。 */\nexport interface SteeringReminderDecision {\n\treadonly inject: boolean;\n\treadonly pendingSteerCount: number;\n}\n\n/**\n * 注入判定:pending > 0 且任务面在场时注入一条提醒并消费一个计数;任务面缺席\n * 时保留计数(本请求不注入,下一请求再判——工具面可能在 run 内动态恢复);\n * pending 为 0 时维持去重(同一引导只提醒一次、重放不重复提醒)。\n */\nexport function decideSteeringReminder(pendingSteerCount: number, taskFacePresent: boolean): SteeringReminderDecision {\n\tif (pendingSteerCount <= 0) return { inject: false, pendingSteerCount: 0 };\n\tif (!taskFacePresent) return { inject: false, pendingSteerCount };\n\treturn { inject: true, pendingSteerCount: pendingSteerCount - 1 };\n}\n\n/**\n * 任务面在场判定(生效边界,设计 §3.7):`getActiveTools` 不可用(SessionAPI\n * 可选面缺席)时视为在场——宁可多提醒;在场列表与任务面工具无交集时不注入。\n */\nexport function isTaskFacePresent(activeTools: readonly string[] | undefined): boolean {\n\tif (!activeTools) return true;\n\treturn TASK_FACE_TOOL_NAMES.some((name) => activeTools.includes(name));\n}\n"]}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * 中途引导落账提醒的探测核心(设计 `docs/design/中途引导与任务状态联动设计.md`
3
+ * §4.1 路线 A、§7 S5;第二层强化,第一层常驻纪律在 goal 插件的
4
+ * TASK_TOOLS_GUIDE)。全部逻辑为纯函数:输入是输入到达信号 + 既有状态,输出是
5
+ * 是否注入与新状态;entry.ts 只做公共面接线与闭包状态持有。
6
+ *
7
+ * 探测选型(实施前源码验证,见 README "探测结论"):
8
+ * - `input.received@1` 不流经 `CapabilityAPI.subscribe` 可见的插件间事件总线
9
+ * (宿主从不向该总线发布宿主事件;它只走 lifecycle 目录通道),且其派发为
10
+ * fire-and-forget 异步(`agent-session.ts` 的 `void dispatchInputReceived...`),
11
+ * 相对 run 激活(`runDefaultLoop` 置 `_isAgentRunActive`)无顺序保证——在派发
12
+ * 时刻读 `isIdle()` 对"新 prompt 开新 run"路径是竞态,无法确定性地满足
13
+ * "首请求/新回合首请求不提醒"。
14
+ * - 因此探测事件取 `beforeInput` loop 钩子:宿主在 `_runInputHandlers` 内联
15
+ * await 该链,先于 prompt() 的 isStreaming 分支与 run 激活、也先于 steer/
16
+ * followUp 入队返回。钩子调用时刻读 `isIdle()` 因此是确定性的:
17
+ * idle === 该输入开新 run("run-start"),busy === 该输入是运行中到达的
18
+ * steering/followUp("steer")。这正是本插件需要的"run 内新增 user 消息 vs
19
+ * 新 run 首条"判别器,且与 transformContext 同属 loop-hook 公共通道。
20
+ *
21
+ * 压缩边界:本路线不维护消息计数基线,不存在"压缩后基线失真"问题。压缩期间
22
+ * `isIdle()` 为 false(isIdle = !runActive && !compacting),压缩窗口内到达的
23
+ * 引导照常置 pending,压缩后的下一请求照常注入——"压缩不丢失引导提醒"由构造
24
+ * 保证(测试矩阵钉住)。
25
+ */
26
+ /** 注入消息的 customType(标记来源;display:false,仅存在于该次请求视图)。 */
27
+ export const STEERING_GUARD_CUSTOM_TYPE = "steering_guard";
28
+ /**
29
+ * 任务面工具名(生效边界,设计 §3.7):这些工具全部不在场(readonly 会话或
30
+ * 用户禁用任务面插件)时,引导落账无承载面,不注入提醒。
31
+ */
32
+ export const TASK_FACE_TOOL_NAMES = [
33
+ "goal_set",
34
+ "plan_set",
35
+ "todo_update",
36
+ "workflow_set",
37
+ "workflow_advance",
38
+ ];
39
+ /** 注入到请求视图尾部的提醒文本(英文,≤60 词,契约固定措辞)。 */
40
+ export const STEERING_GUARD_REMINDER_TEXT = "[steering-guard] Mid-run user input arrived. Apply the steering discipline: classify it (background/add/modify/cancel/pivot/fork), settle required state changes this turn via the task tools — workflow-level consequences first, then plan/goal, todos last; touch only affected records; a cancelled todo needs the user's line as evidence.";
41
+ /** display:false 的引导落账提醒消息(transformContext 注入用,同请求视图内生存)。 */
42
+ export function createSteeringGuardReminder() {
43
+ return {
44
+ role: "custom",
45
+ customType: STEERING_GUARD_CUSTOM_TYPE,
46
+ content: STEERING_GUARD_REMINDER_TEXT,
47
+ display: false,
48
+ timestamp: Date.now(),
49
+ };
50
+ }
51
+ /**
52
+ * 吸收一个输入到达信号,返回新的 pending 计数。
53
+ *
54
+ * - "run-start":新 run 的首条输入就是本轮 prompt 本身,不是中途引导——清零
55
+ * pending(同时丢弃上一 run 中止后残留的未消费计数,防止跨 run 泄漏提醒)。
56
+ * - "steer":运行中到达一条引导,pending 加一。计数而非布尔:同一 run 内连发
57
+ * 两条引导各提醒一次(每次请求消费一条;两条在同一次请求前到达时,分两次
58
+ * 请求各注入一次)。
59
+ */
60
+ export function absorbSteeringInputSignal(pendingSteerCount, signal) {
61
+ return signal === "run-start" ? 0 : pendingSteerCount + 1;
62
+ }
63
+ /**
64
+ * 注入判定:pending > 0 且任务面在场时注入一条提醒并消费一个计数;任务面缺席
65
+ * 时保留计数(本请求不注入,下一请求再判——工具面可能在 run 内动态恢复);
66
+ * pending 为 0 时维持去重(同一引导只提醒一次、重放不重复提醒)。
67
+ */
68
+ export function decideSteeringReminder(pendingSteerCount, taskFacePresent) {
69
+ if (pendingSteerCount <= 0)
70
+ return { inject: false, pendingSteerCount: 0 };
71
+ if (!taskFacePresent)
72
+ return { inject: false, pendingSteerCount };
73
+ return { inject: true, pendingSteerCount: pendingSteerCount - 1 };
74
+ }
75
+ /**
76
+ * 任务面在场判定(生效边界,设计 §3.7):`getActiveTools` 不可用(SessionAPI
77
+ * 可选面缺席)时视为在场——宁可多提醒;在场列表与任务面工具无交集时不注入。
78
+ */
79
+ export function isTaskFacePresent(activeTools) {
80
+ if (!activeTools)
81
+ return true;
82
+ return TASK_FACE_TOOL_NAMES.some((name) => activeTools.includes(name));
83
+ }
84
+ //# sourceMappingURL=detector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detector.js","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,uGAAuD;AACvD,MAAM,CAAC,MAAM,0BAA0B,GAAG,gBAAgB,CAAC;AAE3D;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAsB;IACtD,UAAU;IACV,UAAU;IACV,aAAa;IACb,cAAc;IACd,kBAAkB;CAClB,CAAC;AAEF,iGAAuC;AACvC,MAAM,CAAC,MAAM,4BAA4B,GACxC,mVAAiV,CAAC;AAenV,8GAA8D;AAC9D,MAAM,UAAU,2BAA2B,GAAiC;IAC3E,OAAO;QACN,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE,0BAA0B;QACtC,OAAO,EAAE,4BAA4B;QACrC,OAAO,EAAE,KAAK;QACd,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;KACrB,CAAC;AAAA,CACF;AAQD;;;;;;;;GAQG;AACH,MAAM,UAAU,yBAAyB,CAAC,iBAAyB,EAAE,MAA2B,EAAU;IACzG,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB,GAAG,CAAC,CAAC;AAAA,CAC1D;AAQD;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CAAC,iBAAyB,EAAE,eAAwB,EAA4B;IACrH,IAAI,iBAAiB,IAAI,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;IAC3E,IAAI,CAAC,eAAe;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAClE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,EAAE,iBAAiB,GAAG,CAAC,EAAE,CAAC;AAAA,CAClE;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,WAA0C,EAAW;IACtF,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAC9B,OAAO,oBAAoB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;AAAA,CACvE","sourcesContent":["/**\n * 中途引导落账提醒的探测核心(设计 `docs/design/中途引导与任务状态联动设计.md`\n * §4.1 路线 A、§7 S5;第二层强化,第一层常驻纪律在 goal 插件的\n * TASK_TOOLS_GUIDE)。全部逻辑为纯函数:输入是输入到达信号 + 既有状态,输出是\n * 是否注入与新状态;entry.ts 只做公共面接线与闭包状态持有。\n *\n * 探测选型(实施前源码验证,见 README \"探测结论\"):\n * - `input.received@1` 不流经 `CapabilityAPI.subscribe` 可见的插件间事件总线\n * (宿主从不向该总线发布宿主事件;它只走 lifecycle 目录通道),且其派发为\n * fire-and-forget 异步(`agent-session.ts` 的 `void dispatchInputReceived...`),\n * 相对 run 激活(`runDefaultLoop` 置 `_isAgentRunActive`)无顺序保证——在派发\n * 时刻读 `isIdle()` 对\"新 prompt 开新 run\"路径是竞态,无法确定性地满足\n * \"首请求/新回合首请求不提醒\"。\n * - 因此探测事件取 `beforeInput` loop 钩子:宿主在 `_runInputHandlers` 内联\n * await 该链,先于 prompt() 的 isStreaming 分支与 run 激活、也先于 steer/\n * followUp 入队返回。钩子调用时刻读 `isIdle()` 因此是确定性的:\n * idle === 该输入开新 run(\"run-start\"),busy === 该输入是运行中到达的\n * steering/followUp(\"steer\")。这正是本插件需要的\"run 内新增 user 消息 vs\n * 新 run 首条\"判别器,且与 transformContext 同属 loop-hook 公共通道。\n *\n * 压缩边界:本路线不维护消息计数基线,不存在\"压缩后基线失真\"问题。压缩期间\n * `isIdle()` 为 false(isIdle = !runActive && !compacting),压缩窗口内到达的\n * 引导照常置 pending,压缩后的下一请求照常注入——\"压缩不丢失引导提醒\"由构造\n * 保证(测试矩阵钉住)。\n */\n\n/** 注入消息的 customType(标记来源;display:false,仅存在于该次请求视图)。 */\nexport const STEERING_GUARD_CUSTOM_TYPE = \"steering_guard\";\n\n/**\n * 任务面工具名(生效边界,设计 §3.7):这些工具全部不在场(readonly 会话或\n * 用户禁用任务面插件)时,引导落账无承载面,不注入提醒。\n */\nexport const TASK_FACE_TOOL_NAMES: readonly string[] = [\n\t\"goal_set\",\n\t\"plan_set\",\n\t\"todo_update\",\n\t\"workflow_set\",\n\t\"workflow_advance\",\n];\n\n/** 注入到请求视图尾部的提醒文本(英文,≤60 词,契约固定措辞)。 */\nexport const STEERING_GUARD_REMINDER_TEXT =\n\t\"[steering-guard] Mid-run user input arrived. Apply the steering discipline: classify it (background/add/modify/cancel/pivot/fork), settle required state changes this turn via the task tools — workflow-level consequences first, then plan/goal, todos last; touch only affected records; a cancelled todo needs the user's line as evidence.\";\n\n/**\n * 结构化镜像宿主 `core/messages.ts` 的 CustomMessage(同 loop-guard 先例:内核\n * 类型不跨入插件面,字段漂移由宿主 e2e 拦截)。本插件只生产 display:false 的\n * 模型面提醒,不写 context.messages、不进 newMessages,故不持久化。\n */\nexport interface SteeringGuardReminderMessage {\n\trole: \"custom\";\n\tcustomType: string;\n\tcontent: string;\n\tdisplay: boolean;\n\ttimestamp: number;\n}\n\n/** display:false 的引导落账提醒消息(transformContext 注入用,同请求视图内生存)。 */\nexport function createSteeringGuardReminder(): SteeringGuardReminderMessage {\n\treturn {\n\t\trole: \"custom\",\n\t\tcustomType: STEERING_GUARD_CUSTOM_TYPE,\n\t\tcontent: STEERING_GUARD_REMINDER_TEXT,\n\t\tdisplay: false,\n\t\ttimestamp: Date.now(),\n\t};\n}\n\n/**\n * 输入到达信号:\"run-start\" = 该输入开启新 run(isIdle 为 true 时收到);\n * \"steer\" = 该输入在 run(或压缩)进行中到达。\n */\nexport type SteeringInputSignal = \"run-start\" | \"steer\";\n\n/**\n * 吸收一个输入到达信号,返回新的 pending 计数。\n *\n * - \"run-start\":新 run 的首条输入就是本轮 prompt 本身,不是中途引导——清零\n * pending(同时丢弃上一 run 中止后残留的未消费计数,防止跨 run 泄漏提醒)。\n * - \"steer\":运行中到达一条引导,pending 加一。计数而非布尔:同一 run 内连发\n * 两条引导各提醒一次(每次请求消费一条;两条在同一次请求前到达时,分两次\n * 请求各注入一次)。\n */\nexport function absorbSteeringInputSignal(pendingSteerCount: number, signal: SteeringInputSignal): number {\n\treturn signal === \"run-start\" ? 0 : pendingSteerCount + 1;\n}\n\n/** 一次注入判定的输出:是否注入 + 消费后的新 pending 计数。 */\nexport interface SteeringReminderDecision {\n\treadonly inject: boolean;\n\treadonly pendingSteerCount: number;\n}\n\n/**\n * 注入判定:pending > 0 且任务面在场时注入一条提醒并消费一个计数;任务面缺席\n * 时保留计数(本请求不注入,下一请求再判——工具面可能在 run 内动态恢复);\n * pending 为 0 时维持去重(同一引导只提醒一次、重放不重复提醒)。\n */\nexport function decideSteeringReminder(pendingSteerCount: number, taskFacePresent: boolean): SteeringReminderDecision {\n\tif (pendingSteerCount <= 0) return { inject: false, pendingSteerCount: 0 };\n\tif (!taskFacePresent) return { inject: false, pendingSteerCount };\n\treturn { inject: true, pendingSteerCount: pendingSteerCount - 1 };\n}\n\n/**\n * 任务面在场判定(生效边界,设计 §3.7):`getActiveTools` 不可用(SessionAPI\n * 可选面缺席)时视为在场——宁可多提醒;在场列表与任务面工具无交集时不注入。\n */\nexport function isTaskFacePresent(activeTools: readonly string[] | undefined): boolean {\n\tif (!activeTools) return true;\n\treturn TASK_FACE_TOOL_NAMES.some((name) => activeTools.includes(name));\n}\n"]}
@@ -0,0 +1,27 @@
1
+ import type { CapabilityAPI } from "@agent-forge/plugin-sdk";
2
+ /**
3
+ * Default-export factory consumed by the host's manifest plugin loader
4
+ * (design `docs/design/中途引导与任务状态联动设计.md` §4.1 路线 A、§7 S5:
5
+ * 第二层强化——检测到运行中到达的用户引导时,向下一次模型请求的视图尾部
6
+ * 注入一条 display:false 的引导落账提醒;第一层常驻纪律由 goal 插件的
7
+ * TASK_TOOLS_GUIDE 承载,本插件不重复渲染常驻文本).
8
+ *
9
+ * Detection rides the public `beforeInput` loop hook (see detector.ts for the
10
+ * probe record: `input.received@1` neither flows through the
11
+ * `CapabilityAPI.subscribe` plugin-to-plugin bus nor gives a deterministic
12
+ * idle/busy read at fire-and-forget delivery time, while `beforeInput` is
13
+ * awaited inline by the host BEFORE run activation on the prompt path and
14
+ * during the active run on the steer path). The reminder rides the public
15
+ * `transformContext` chain, appended to the final request view tail exactly
16
+ * like the agent-loop-policy warn reminder (display:false, not persisted —
17
+ * no context.messages write, no newMessages entry).
18
+ *
19
+ * Disabling this plugin (`disabledPluginIds`) removes both links. A host
20
+ * without `SessionAPI` leaves the guard inert (every input reads as
21
+ * "run-start"); a host without `getActiveTools` counts the task face as
22
+ * present (better one reminder too many than a silent miss).
23
+ */
24
+ export default function createSteeringGuardPluginEntry(api: CapabilityAPI): {
25
+ dispose: () => void;
26
+ };
27
+ //# sourceMappingURL=entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../src/entry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGX,aAAa,EACb,MAAM,yBAAyB,CAAC;AASjC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,OAAO,UAAU,8BAA8B,CAAC,GAAG,EAAE,aAAa,GAAG;IAAE,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAmClG","sourcesContent":["import type {\n\tAgentLoopBeforeInputHookV1,\n\tAgentLoopTransformContextHookV1,\n\tCapabilityAPI,\n} from \"@agent-forge/plugin-sdk\";\nimport {\n\tabsorbSteeringInputSignal,\n\tcreateSteeringGuardReminder,\n\tdecideSteeringReminder,\n\tisTaskFacePresent,\n\ttype SteeringInputSignal,\n} from \"./detector.ts\";\n\n/**\n * Default-export factory consumed by the host's manifest plugin loader\n * (design `docs/design/中途引导与任务状态联动设计.md` §4.1 路线 A、§7 S5:\n * 第二层强化——检测到运行中到达的用户引导时,向下一次模型请求的视图尾部\n * 注入一条 display:false 的引导落账提醒;第一层常驻纪律由 goal 插件的\n * TASK_TOOLS_GUIDE 承载,本插件不重复渲染常驻文本).\n *\n * Detection rides the public `beforeInput` loop hook (see detector.ts for the\n * probe record: `input.received@1` neither flows through the\n * `CapabilityAPI.subscribe` plugin-to-plugin bus nor gives a deterministic\n * idle/busy read at fire-and-forget delivery time, while `beforeInput` is\n * awaited inline by the host BEFORE run activation on the prompt path and\n * during the active run on the steer path). The reminder rides the public\n * `transformContext` chain, appended to the final request view tail exactly\n * like the agent-loop-policy warn reminder (display:false, not persisted —\n * no context.messages write, no newMessages entry).\n *\n * Disabling this plugin (`disabledPluginIds`) removes both links. A host\n * without `SessionAPI` leaves the guard inert (every input reads as\n * \"run-start\"); a host without `getActiveTools` counts the task face as\n * present (better one reminder too many than a silent miss).\n */\nexport default function createSteeringGuardPluginEntry(api: CapabilityAPI): { dispose: () => void } {\n\t/** 跨请求状态:未消费的引导提醒计数(闭包持有,决策逻辑全在 detector.ts)。 */\n\tlet pendingSteerCount = 0;\n\n\tconst beforeInput: AgentLoopBeforeInputHookV1 = async (_input) => {\n\t\t// 钩子时刻的 isIdle 是确定性的(见 detector.ts 探测记录):idle === 该输入\n\t\t// 开新 run(清零残留计数),busy === 运行中/压缩中到达的引导。\n\t\tconst idle = api.session ? api.session.isIdle() : true;\n\t\tconst signal: SteeringInputSignal = idle ? \"run-start\" : \"steer\";\n\t\tpendingSteerCount = absorbSteeringInputSignal(pendingSteerCount, signal);\n\t\t// 纯观察位:不拦截、不改写输入文本。\n\t\treturn undefined;\n\t};\n\n\tconst transformContext: AgentLoopTransformContextHookV1 = async (messages, _signal) => {\n\t\tconst decision = decideSteeringReminder(pendingSteerCount, isTaskFacePresent(api.session?.getActiveTools?.()));\n\t\tpendingSteerCount = decision.pendingSteerCount;\n\t\tif (!decision.inject) return undefined;\n\t\t// 非变异地追加到请求视图尾部(不写 context.messages,故不持久化);\n\t\t// 消费即计数减一:同一引导只提醒一次。\n\t\treturn [...messages, createSteeringGuardReminder()];\n\t};\n\n\tconst inputLink = api.registerLoopHook(\"beforeInput\", beforeInput);\n\t// 与 agent-loop-policy 同序理由:注册在默认上下文策略链之后,提醒落到最终\n\t// 请求视图尾部(幸存于任何策略裁剪)。\n\tconst reminderLink = api.registerLoopHook(\"transformContext\", transformContext);\n\treturn {\n\t\tdispose: () => {\n\t\t\t// Registration disposal in the capability runtime is synchronous;\n\t\t\t// void consumes the declared Promise possibility of the contract.\n\t\t\tvoid reminderLink.dispose();\n\t\t\tvoid inputLink.dispose();\n\t\t},\n\t};\n}\n"]}
package/dist/entry.js ADDED
@@ -0,0 +1,58 @@
1
+ import { absorbSteeringInputSignal, createSteeringGuardReminder, decideSteeringReminder, isTaskFacePresent, } from "./detector.js";
2
+ /**
3
+ * Default-export factory consumed by the host's manifest plugin loader
4
+ * (design `docs/design/中途引导与任务状态联动设计.md` §4.1 路线 A、§7 S5:
5
+ * 第二层强化——检测到运行中到达的用户引导时,向下一次模型请求的视图尾部
6
+ * 注入一条 display:false 的引导落账提醒;第一层常驻纪律由 goal 插件的
7
+ * TASK_TOOLS_GUIDE 承载,本插件不重复渲染常驻文本).
8
+ *
9
+ * Detection rides the public `beforeInput` loop hook (see detector.ts for the
10
+ * probe record: `input.received@1` neither flows through the
11
+ * `CapabilityAPI.subscribe` plugin-to-plugin bus nor gives a deterministic
12
+ * idle/busy read at fire-and-forget delivery time, while `beforeInput` is
13
+ * awaited inline by the host BEFORE run activation on the prompt path and
14
+ * during the active run on the steer path). The reminder rides the public
15
+ * `transformContext` chain, appended to the final request view tail exactly
16
+ * like the agent-loop-policy warn reminder (display:false, not persisted —
17
+ * no context.messages write, no newMessages entry).
18
+ *
19
+ * Disabling this plugin (`disabledPluginIds`) removes both links. A host
20
+ * without `SessionAPI` leaves the guard inert (every input reads as
21
+ * "run-start"); a host without `getActiveTools` counts the task face as
22
+ * present (better one reminder too many than a silent miss).
23
+ */
24
+ export default function createSteeringGuardPluginEntry(api) {
25
+ /** 跨请求状态:未消费的引导提醒计数(闭包持有,决策逻辑全在 detector.ts)。 */
26
+ let pendingSteerCount = 0;
27
+ const beforeInput = async (_input) => {
28
+ // 钩子时刻的 isIdle 是确定性的(见 detector.ts 探测记录):idle === 该输入
29
+ // 开新 run(清零残留计数),busy === 运行中/压缩中到达的引导。
30
+ const idle = api.session ? api.session.isIdle() : true;
31
+ const signal = idle ? "run-start" : "steer";
32
+ pendingSteerCount = absorbSteeringInputSignal(pendingSteerCount, signal);
33
+ // 纯观察位:不拦截、不改写输入文本。
34
+ return undefined;
35
+ };
36
+ const transformContext = async (messages, _signal) => {
37
+ const decision = decideSteeringReminder(pendingSteerCount, isTaskFacePresent(api.session?.getActiveTools?.()));
38
+ pendingSteerCount = decision.pendingSteerCount;
39
+ if (!decision.inject)
40
+ return undefined;
41
+ // 非变异地追加到请求视图尾部(不写 context.messages,故不持久化);
42
+ // 消费即计数减一:同一引导只提醒一次。
43
+ return [...messages, createSteeringGuardReminder()];
44
+ };
45
+ const inputLink = api.registerLoopHook("beforeInput", beforeInput);
46
+ // 与 agent-loop-policy 同序理由:注册在默认上下文策略链之后,提醒落到最终
47
+ // 请求视图尾部(幸存于任何策略裁剪)。
48
+ const reminderLink = api.registerLoopHook("transformContext", transformContext);
49
+ return {
50
+ dispose: () => {
51
+ // Registration disposal in the capability runtime is synchronous;
52
+ // void consumes the declared Promise possibility of the contract.
53
+ void reminderLink.dispose();
54
+ void inputLink.dispose();
55
+ },
56
+ };
57
+ }
58
+ //# sourceMappingURL=entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sourceRoot":"","sources":["../src/entry.ts"],"names":[],"mappings":"AAKA,OAAO,EACN,yBAAyB,EACzB,2BAA2B,EAC3B,sBAAsB,EACtB,iBAAiB,GAEjB,MAAM,eAAe,CAAC;AAEvB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,OAAO,UAAU,8BAA8B,CAAC,GAAkB,EAA2B;IACnG,6GAAiD;IACjD,IAAI,iBAAiB,GAAG,CAAC,CAAC;IAE1B,MAAM,WAAW,GAA+B,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC;QACjE,gGAAsD;QACtD,sFAAwC;QACxC,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACvD,MAAM,MAAM,GAAwB,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC;QACjE,iBAAiB,GAAG,yBAAyB,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;QACzE,sDAAoB;QACpB,OAAO,SAAS,CAAC;IAAA,CACjB,CAAC;IAEF,MAAM,gBAAgB,GAAoC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,CAAC;QACtF,MAAM,QAAQ,GAAG,sBAAsB,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE,CAAC,CAAC,CAAC;QAC/G,iBAAiB,GAAG,QAAQ,CAAC,iBAAiB,CAAC;QAC/C,IAAI,CAAC,QAAQ,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QACvC,4FAA4C;QAC5C,yDAAqB;QACrB,OAAO,CAAC,GAAG,QAAQ,EAAE,2BAA2B,EAAE,CAAC,CAAC;IAAA,CACpD,CAAC;IAEF,MAAM,SAAS,GAAG,GAAG,CAAC,gBAAgB,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;IACnE,oGAAgD;IAChD,yDAAqB;IACrB,MAAM,YAAY,GAAG,GAAG,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,CAAC;IAChF,OAAO;QACN,OAAO,EAAE,GAAG,EAAE,CAAC;YACd,kEAAkE;YAClE,kEAAkE;YAClE,KAAK,YAAY,CAAC,OAAO,EAAE,CAAC;YAC5B,KAAK,SAAS,CAAC,OAAO,EAAE,CAAC;QAAA,CACzB;KACD,CAAC;AAAA,CACF","sourcesContent":["import type {\n\tAgentLoopBeforeInputHookV1,\n\tAgentLoopTransformContextHookV1,\n\tCapabilityAPI,\n} from \"@agent-forge/plugin-sdk\";\nimport {\n\tabsorbSteeringInputSignal,\n\tcreateSteeringGuardReminder,\n\tdecideSteeringReminder,\n\tisTaskFacePresent,\n\ttype SteeringInputSignal,\n} from \"./detector.ts\";\n\n/**\n * Default-export factory consumed by the host's manifest plugin loader\n * (design `docs/design/中途引导与任务状态联动设计.md` §4.1 路线 A、§7 S5:\n * 第二层强化——检测到运行中到达的用户引导时,向下一次模型请求的视图尾部\n * 注入一条 display:false 的引导落账提醒;第一层常驻纪律由 goal 插件的\n * TASK_TOOLS_GUIDE 承载,本插件不重复渲染常驻文本).\n *\n * Detection rides the public `beforeInput` loop hook (see detector.ts for the\n * probe record: `input.received@1` neither flows through the\n * `CapabilityAPI.subscribe` plugin-to-plugin bus nor gives a deterministic\n * idle/busy read at fire-and-forget delivery time, while `beforeInput` is\n * awaited inline by the host BEFORE run activation on the prompt path and\n * during the active run on the steer path). The reminder rides the public\n * `transformContext` chain, appended to the final request view tail exactly\n * like the agent-loop-policy warn reminder (display:false, not persisted —\n * no context.messages write, no newMessages entry).\n *\n * Disabling this plugin (`disabledPluginIds`) removes both links. A host\n * without `SessionAPI` leaves the guard inert (every input reads as\n * \"run-start\"); a host without `getActiveTools` counts the task face as\n * present (better one reminder too many than a silent miss).\n */\nexport default function createSteeringGuardPluginEntry(api: CapabilityAPI): { dispose: () => void } {\n\t/** 跨请求状态:未消费的引导提醒计数(闭包持有,决策逻辑全在 detector.ts)。 */\n\tlet pendingSteerCount = 0;\n\n\tconst beforeInput: AgentLoopBeforeInputHookV1 = async (_input) => {\n\t\t// 钩子时刻的 isIdle 是确定性的(见 detector.ts 探测记录):idle === 该输入\n\t\t// 开新 run(清零残留计数),busy === 运行中/压缩中到达的引导。\n\t\tconst idle = api.session ? api.session.isIdle() : true;\n\t\tconst signal: SteeringInputSignal = idle ? \"run-start\" : \"steer\";\n\t\tpendingSteerCount = absorbSteeringInputSignal(pendingSteerCount, signal);\n\t\t// 纯观察位:不拦截、不改写输入文本。\n\t\treturn undefined;\n\t};\n\n\tconst transformContext: AgentLoopTransformContextHookV1 = async (messages, _signal) => {\n\t\tconst decision = decideSteeringReminder(pendingSteerCount, isTaskFacePresent(api.session?.getActiveTools?.()));\n\t\tpendingSteerCount = decision.pendingSteerCount;\n\t\tif (!decision.inject) return undefined;\n\t\t// 非变异地追加到请求视图尾部(不写 context.messages,故不持久化);\n\t\t// 消费即计数减一:同一引导只提醒一次。\n\t\treturn [...messages, createSteeringGuardReminder()];\n\t};\n\n\tconst inputLink = api.registerLoopHook(\"beforeInput\", beforeInput);\n\t// 与 agent-loop-policy 同序理由:注册在默认上下文策略链之后,提醒落到最终\n\t// 请求视图尾部(幸存于任何策略裁剪)。\n\tconst reminderLink = api.registerLoopHook(\"transformContext\", transformContext);\n\treturn {\n\t\tdispose: () => {\n\t\t\t// Registration disposal in the capability runtime is synchronous;\n\t\t\t// void consumes the declared Promise possibility of the contract.\n\t\t\tvoid reminderLink.dispose();\n\t\t\tvoid inputLink.dispose();\n\t\t},\n\t};\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
- "name": "@ai-agent-forge/plugin-steering-guard",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@ai-agent-forge/plugin-steering-guard",
3
+ "version": "0.88.16",
4
+ "description": "Steering-guard capability plugin for Agent Forge (mid-run steering reminder injected into the request view)",
5
+ "type": "module",
6
+ "main": "./dist/entry.js",
7
+ "types": "./dist/entry.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/entry.d.ts",
11
+ "import": "./dist/entry.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "agent-forge.json",
17
+ "plugin.json",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "clean": "shx rm -rf dist",
22
+ "build": "tsgo -p tsconfig.build.json",
23
+ "test": "vitest --run",
24
+ "prepublishOnly": "npm run clean && npm run build",
25
+ "test:coverage": "vitest --run --coverage"
26
+ },
27
+ "keywords": [
28
+ "agent-forge",
29
+ "plugin",
30
+ "steering-guard"
31
+ ],
32
+ "author": "Agent Forge contributors",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/agent-forge/agent-forge.git",
37
+ "directory": "plugins/steering-guard"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/agent-forge/agent-forge/issues"
41
+ },
42
+ "homepage": "https://github.com/agent-forge/agent-forge#readme",
43
+ "engines": {
44
+ "node": ">=22.19.0"
45
+ },
46
+ "dependencies": {},
47
+ "devDependencies": {
48
+ "shx": "0.3.4",
49
+ "vitest": "4.1.11"
50
+ }
51
+ }
package/plugin.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "agent-forge.plugin.steering-guard",
3
+ "version": "0.88.16",
4
+ "apiVersion": "1-draft",
5
+ "entry": "./dist/entry.js",
6
+ "hostVersion": ">=0.88.0"
7
+ }