@zhin.js/service-activity-feedback 1.0.2 → 1.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/service-activity-feedback",
3
- "version": "1.0.2",
3
+ "version": "1.1.3",
4
4
  "description": "Activity Feedback 服务插件(Plugin Runtime)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -14,7 +14,7 @@
14
14
  "./package.json": "./package.json"
15
15
  },
16
16
  "files": [
17
- "plugin.ts",
17
+ "plugin.js",
18
18
  "schema.json",
19
19
  "src",
20
20
  "lib",
@@ -36,19 +36,18 @@
36
36
  "directory": "plugins/services/activity-feedback"
37
37
  },
38
38
  "dependencies": {
39
- "@zhin.js/agent": "1.0.4",
40
- "@zhin.js/logger": "1.0.75",
41
- "@zhin.js/plugin-runtime": "1.0.1"
39
+ "@zhin.js/agent": "1.1.26",
40
+ "@zhin.js/logger": "1.1.1"
42
41
  },
43
42
  "devDependencies": {
44
- "@types/node": "^26.1.0",
43
+ "@types/node": "^26.1.2",
45
44
  "typescript": "^6.0.3",
46
45
  "vitest": "^4.1.10",
47
- "zhin.js": "4.1.3"
46
+ "zhin.js": "1.1.2"
48
47
  },
49
48
  "peerDependencies": {
50
- "@zhin.js/agent": "1.0.4",
51
- "zhin.js": "4.1.3"
49
+ "@zhin.js/agent": "^1.1.26",
50
+ "zhin.js": "^1.1.2"
52
51
  },
53
52
  "peerDependenciesMeta": {
54
53
  "zhin.js": {
@@ -61,7 +60,7 @@
61
60
  "zhin": {
62
61
  "protocol": 1,
63
62
  "type": "plugin",
64
- "entry": "./plugin.ts",
63
+ "entry": "./plugin.js",
65
64
  "engine": "^1.0.0",
66
65
  "runtime": "trusted",
67
66
  "features": [],
package/plugin.js ADDED
@@ -0,0 +1,48 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { createGenerationAdmissionGate, createToken, definePlugin, outboundHostToken, } from 'zhin.js';
3
+ import { agentEventBusToken } from '@zhin.js/agent/runtime';
4
+ import { getLogger } from '@zhin.js/logger';
5
+ import { loadActivityFeedbackServiceConfig, } from "./lib/config.js";
6
+ import { bindActivityFeedbackToAIEventBus, createActivityFeedbackOrchestratorForRuntime, } from "./lib/ai-event-binder.js";
7
+ import { createOutboundEndpointAccess, } from "./lib/executor.js";
8
+ const logger = getLogger('activity-feedback');
9
+ const activityFeedbackAdmissionToken = createToken('zhin.activity-feedback.generation-admission');
10
+ /**
11
+ * Activity Feedback service — Plugin Runtime entry.
12
+ *
13
+ * Subscribes AI lifecycle events via the generation-owned Agent event bus.
14
+ * When Root provides `outboundHostToken`, typing/status text uses
15
+ * ImRuntime.sendEndpointMessage; otherwise phases no-op.
16
+ */
17
+ export default definePlugin({
18
+ name: 'activity-feedback',
19
+ metadata: {
20
+ displayName: 'Activity Feedback',
21
+ },
22
+ setup(context) {
23
+ const serviceConfig = loadActivityFeedbackServiceConfig(context.config.get());
24
+ if (serviceConfig.enabled === false) {
25
+ return;
26
+ }
27
+ const outbound = context.resources.has(outboundHostToken)
28
+ ? context.resources.use(outboundHostToken)
29
+ : undefined;
30
+ if (!outbound || typeof outbound.runWithView !== 'function') {
31
+ logger.debug('[ActivityFeedback] disabled: OutboundHost with generation-bound runWithView is required');
32
+ return;
33
+ }
34
+ if (!context.resources.has(agentEventBusToken)) {
35
+ logger.debug('[ActivityFeedback] disabled: generation Agent event bus is required');
36
+ return;
37
+ }
38
+ const access = createOutboundEndpointAccess(outbound, logger);
39
+ const orchestrator = createActivityFeedbackOrchestratorForRuntime(serviceConfig, logger, access);
40
+ const admission = createGenerationAdmissionGate();
41
+ context.resources.provide(activityFeedbackAdmissionToken, admission);
42
+ const dispose = bindActivityFeedbackToAIEventBus(context.resources.use(agentEventBusToken), orchestrator, admission, outbound.runWithView.bind(outbound));
43
+ context.lifecycle.add(async () => {
44
+ logger.debug('[ActivityFeedback] Disposing Runtime AI event binder');
45
+ await dispose();
46
+ });
47
+ },
48
+ });
package/schema.json CHANGED
@@ -3,10 +3,126 @@
3
3
  "type": "object",
4
4
  "additionalProperties": false,
5
5
  "properties": {
6
- "enabled": { "type": "boolean", "default": true },
7
- "defaults": { "type": "object", "additionalProperties": true },
8
- "platforms": { "type": "object", "additionalProperties": true },
9
- "endpoints": { "type": "object", "additionalProperties": true },
10
- "schedule": { "type": "object", "additionalProperties": true }
6
+ "enabled": {
7
+ "type": "boolean",
8
+ "default": true,
9
+ "description": "Enable activity feedback for this plugin instance.",
10
+ "x-descriptionZh": "是否启用当前活动反馈插件实例。"
11
+ },
12
+ "defaults": {
13
+ "$ref": "#/$defs/activityFeedback",
14
+ "description": "Default policy for all platforms and Endpoints.",
15
+ "x-descriptionZh": "所有平台与 Endpoint 的默认策略。"
16
+ },
17
+ "platforms": {
18
+ "type": "object",
19
+ "x-keyPlaceholder": "<platform>",
20
+ "additionalProperties": { "$ref": "#/$defs/activityFeedback" },
21
+ "description": "Platform overrides keyed by platform name; values use the same shape as defaults.",
22
+ "x-descriptionZh": "按平台名覆盖;值的结构与 defaults 相同。"
23
+ },
24
+ "endpoints": {
25
+ "type": "object",
26
+ "x-keyPlaceholder": "<platform:endpointKey>",
27
+ "additionalProperties": { "$ref": "#/$defs/activityFeedback" },
28
+ "description": "Endpoint overrides keyed by platform:endpointKey; values use the same shape as defaults.",
29
+ "x-descriptionZh": "按 platform:endpointKey 覆盖;值的结构与 defaults 相同。"
30
+ },
31
+ "schedule": {
32
+ "$ref": "#/$defs/scheduleFeedback",
33
+ "description": "Schedule-only start, finish, and error feedback.",
34
+ "x-descriptionZh": "仅用于 Schedule 的开始、完成与失败反馈。"
35
+ }
36
+ },
37
+ "$defs": {
38
+ "activityFeedback": {
39
+ "type": "object",
40
+ "additionalProperties": false,
41
+ "properties": {
42
+ "enabled": {
43
+ "type": "boolean",
44
+ "default": true,
45
+ "description": "Enable this policy layer.",
46
+ "x-descriptionZh": "是否启用当前策略层。"
47
+ },
48
+ "phases": {
49
+ "type": "object",
50
+ "additionalProperties": false,
51
+ "description": "Feedback policy keyed by lifecycle phase.",
52
+ "x-descriptionZh": "按生命周期 phase 配置反馈策略。",
53
+ "properties": {
54
+ "queued": { "$ref": "#/$defs/scenePhases" },
55
+ "active": { "$ref": "#/$defs/scenePhases" },
56
+ "thinking": { "$ref": "#/$defs/scenePhases" },
57
+ "schedule_start": { "$ref": "#/$defs/scenePhases" },
58
+ "schedule_finish": { "$ref": "#/$defs/scenePhases" },
59
+ "schedule_error": { "$ref": "#/$defs/scenePhases" }
60
+ }
61
+ }
62
+ }
63
+ },
64
+ "scenePhases": {
65
+ "type": "object",
66
+ "additionalProperties": false,
67
+ "properties": {
68
+ "private": { "$ref": "#/$defs/phaseConfig" },
69
+ "group": { "$ref": "#/$defs/phaseConfig" },
70
+ "channel": { "$ref": "#/$defs/phaseConfig" }
71
+ }
72
+ },
73
+ "phaseConfig": {
74
+ "type": "object",
75
+ "additionalProperties": false,
76
+ "properties": {
77
+ "type": {
78
+ "type": "string",
79
+ "enum": ["reaction", "message", "typing", "none"],
80
+ "description": "How the phase is presented.",
81
+ "x-descriptionZh": "该 phase 的呈现方式。"
82
+ },
83
+ "emoji": {
84
+ "type": "string",
85
+ "description": "Reaction value for type=reaction.",
86
+ "x-descriptionZh": "type=reaction 时使用的回应值。"
87
+ },
88
+ "message": {
89
+ "type": "string",
90
+ "description": "Status text for type=message.",
91
+ "x-descriptionZh": "type=message 时使用的状态文本。"
92
+ },
93
+ "autoRemove": {
94
+ "type": "boolean",
95
+ "default": true,
96
+ "description": "Remove the feedback after the phase stops.",
97
+ "x-descriptionZh": "phase 停止后是否自动移除反馈。"
98
+ },
99
+ "removeDelay": {
100
+ "type": "number",
101
+ "description": "Delay before removal in milliseconds; negative values are normalized to zero at runtime.",
102
+ "x-descriptionZh": "自动移除前等待的毫秒数;负值会在运行时归一化为 0。"
103
+ },
104
+ "platformConfig": {
105
+ "type": "object",
106
+ "additionalProperties": true,
107
+ "description": "Platform-specific options.",
108
+ "x-descriptionZh": "平台特定选项。"
109
+ }
110
+ }
111
+ },
112
+ "scheduleFeedback": {
113
+ "type": "object",
114
+ "additionalProperties": false,
115
+ "properties": {
116
+ "phases": {
117
+ "type": "object",
118
+ "additionalProperties": false,
119
+ "properties": {
120
+ "start": { "$ref": "#/$defs/scenePhases" },
121
+ "finish": { "$ref": "#/$defs/scenePhases" },
122
+ "error": { "$ref": "#/$defs/scenePhases" }
123
+ }
124
+ }
125
+ }
126
+ }
11
127
  }
12
128
  }
@@ -1,16 +1,15 @@
1
- import type { Plugin } from 'zhin.js';
2
1
  import {
3
- subscribeAIEvents,
4
2
  subscribeAIEventsOnTarget,
5
- activityFeedbackAiBus,
6
3
  isActivityFeedbackEnabled,
7
4
  type AIEventHandlers,
5
+ type AIEventPayload,
6
+ type AIEventTarget,
8
7
  } from '@zhin.js/agent';
9
8
  import { loadActivityFeedbackServiceConfig, type ActivityFeedbackServiceConfig } from './config.js';
9
+ import type { GenerationAdmissionGate } from 'zhin.js';
10
10
  import {
11
11
  ActivityFeedbackExecutor,
12
12
  createNoopEndpointAccess,
13
- createRootEndpointAccess,
14
13
  type ActivityFeedbackEndpointAccess,
15
14
  } from './executor.js';
16
15
  import { ActivityFeedbackOrchestrator } from './orchestrator.js';
@@ -32,7 +31,13 @@ export function createActivityFeedbackAIEventHandlers(
32
31
  onProcessingStart: async (payload) => {
33
32
  if (!isActivityFeedbackEnabled(payload, 'active')) return;
34
33
  await orchestrator.stopPhase(payload, 'queued', 'processing.start');
34
+ if (typeof payload.iterations === 'number' && payload.iterations > 1) {
35
+ await orchestrator.stopPhase(payload, 'thinking', 'processing.iteration');
36
+ }
35
37
  await orchestrator.startPhase(payload, 'active', 'processing.start');
38
+ if (typeof payload.iterations === 'number' && payload.iterations > 1 && payload.content?.trim()) {
39
+ await orchestrator.updatePhaseText(payload, 'active', payload.content.trim());
40
+ }
36
41
  },
37
42
 
38
43
  onTypingStart: async () => {},
@@ -50,9 +55,10 @@ export function createActivityFeedbackAIEventHandlers(
50
55
  await orchestrator.stopPhase(payload, 'active', 'processing.error');
51
56
  },
52
57
 
53
- onTypingStop: (payload) => {
58
+ onTypingStop: async (payload) => {
54
59
  if (!isActivityFeedbackEnabled(payload, 'active')) return;
55
- return orchestrator.stopPhase(payload, 'active', 'typing.stop');
60
+ await orchestrator.stopPhase(payload, 'thinking', 'typing.stop');
61
+ await orchestrator.stopPhase(payload, 'active', 'typing.stop');
56
62
  },
57
63
 
58
64
  onThinking: async (payload) => {
@@ -63,61 +69,166 @@ export function createActivityFeedbackAIEventHandlers(
63
69
  await orchestrator.updateThinkingText(payload, payload.thinking);
64
70
  },
65
71
 
72
+ onToolCall: async (payload) => {
73
+ if (!isActivityFeedbackEnabled(payload, 'active') || !payload.toolName?.trim()) return;
74
+ const text = `调用工具:${payload.toolName.trim()}…`;
75
+ await orchestrator.updatePhaseText(payload, 'thinking', text);
76
+ await orchestrator.updatePhaseText(payload, 'active', text);
77
+ },
78
+
79
+ onToolResult: async (payload) => {
80
+ if (!isActivityFeedbackEnabled(payload, 'active') || !payload.toolName?.trim()) return;
81
+ const outcome = payload.status === 'error' || payload.error ? '未完成' : '已完成';
82
+ const text = `工具 ${payload.toolName.trim()} ${outcome},继续处理…`;
83
+ await orchestrator.updatePhaseText(payload, 'thinking', text);
84
+ await orchestrator.updatePhaseText(payload, 'active', text);
85
+ },
86
+
66
87
  onSubagentStart: async (payload) => {
67
88
  if (!isActivityFeedbackEnabled(payload, 'thinking')) return;
89
+ // The subagent has an isolated session key; remove its own active placeholder
90
+ // before switching to the richer thinking state.
68
91
  await orchestrator.stopPhase(payload, 'active', 'subagent.start');
69
92
  await orchestrator.startPhase(payload, 'thinking', 'subagent.start');
70
- const label = payload.label
71
- ? `🔍 子任务执行中: ${payload.label}...`
72
- : '🔍 子 agent 处理中...';
73
- await orchestrator.updateThinkingText(payload, label);
93
+ const tag = payload.agentId?.trim() || 'subagent';
94
+ const label = payload.label?.trim()
95
+ ? `子任务执行中: ${payload.label.trim()}...`
96
+ : '思考中...';
97
+ await orchestrator.updateThinkingText(payload, `[${tag}] ${label}`);
74
98
  },
75
99
 
76
100
  onSubagentFinish: async (payload) => {
77
- if (!isActivityFeedbackEnabled(payload, 'active')) return;
78
- await orchestrator.stopPhase(payload, 'thinking', 'subagent.finish');
79
- await orchestrator.startPhase(payload, 'active', 'subagent.finish');
101
+ if (isActivityFeedbackEnabled(payload, 'thinking')) {
102
+ await orchestrator.stopPhase(payload, 'thinking', 'subagent.finish');
103
+ }
104
+ if (isActivityFeedbackEnabled(payload, 'active')) {
105
+ await orchestrator.stopPhase(payload, 'active', 'subagent.finish');
106
+ }
80
107
  },
81
108
 
82
109
  onScheduleStart: (payload) => orchestrator.startPhase(payload, 'schedule_start', 'schedule.start'),
83
- onScheduleFinish: (payload) => orchestrator.stopPhase(payload, 'schedule_start', 'schedule.finish'),
110
+ onScheduleFinish: async (payload) => {
111
+ await orchestrator.stopPhase(payload, 'schedule_start', 'schedule.finish');
112
+ await orchestrator.showTransientPhase(payload, 'schedule_finish', 'schedule.finish');
113
+ },
84
114
  onScheduleError: async (payload) => {
85
115
  await orchestrator.stopPhase(payload, 'schedule_start', 'schedule.error');
86
- await orchestrator.startPhase(payload, 'schedule_error', 'schedule.error');
116
+ await orchestrator.showTransientPhase(payload, 'schedule_error', 'schedule.error');
87
117
  },
88
118
  };
89
119
  }
90
120
 
91
- /** Legacy host Plugin path (ALS-aware subscribeAIEvents). */
92
- export function bindActivityFeedbackToAIEvents(
93
- root: Plugin['root'],
94
- orchestrator: ActivityFeedbackOrchestrator,
95
- ): () => void {
96
- return subscribeAIEvents(root, createActivityFeedbackAIEventHandlers(orchestrator));
97
- }
98
-
99
121
  /**
100
- * Plugin Runtime path: subscribe on module-level `activityFeedbackAiBus`
101
- * (fed by ZhinAgentEventEmitter.emit). No usePlugin / Adapter inject.
122
+ * Plugin Runtime path: subscribe to the generation-owned Agent event bus.
102
123
  */
103
124
  export function bindActivityFeedbackToAIEventBus(
125
+ source: AIEventTarget,
104
126
  orchestrator: ActivityFeedbackOrchestrator,
105
- ): () => void {
106
- return subscribeAIEventsOnTarget(
107
- activityFeedbackAiBus,
127
+ admission: GenerationAdmissionGate,
128
+ runWithView: <T>(operation: () => Promise<T>) => Promise<T>,
129
+ ): () => Promise<void> {
130
+ let stopWatchingRetirement: (() => void) | undefined;
131
+ let cleanupNeedsGenerationView = false;
132
+ const watchRetirement = () => {
133
+ if (stopWatchingRetirement) return;
134
+ cleanupNeedsGenerationView = true;
135
+ stopWatchingRetirement = admission.onDeactivate(() => {
136
+ void close();
137
+ });
138
+ };
139
+ const serialized = createGenerationSerializedTarget(
140
+ source,
141
+ admission,
142
+ runWithView,
143
+ watchRetirement,
144
+ );
145
+ const unsubscribe = subscribeAIEventsOnTarget(
146
+ serialized.target,
108
147
  createActivityFeedbackAIEventHandlers(orchestrator),
109
148
  );
149
+ let shutdown: Promise<void> | undefined;
150
+ function close(): Promise<void> {
151
+ if (shutdown) return shutdown;
152
+ const cleanup = async () => {
153
+ unsubscribe();
154
+ await serialized.close();
155
+ await orchestrator.dispose();
156
+ };
157
+ // Retirement is published before SnapshotStore changes its current pointer.
158
+ // Enter the IM view synchronously here so timers, native-typing keepalives,
159
+ // and final reaction/message cleanup all remain on this generation's Endpoint.
160
+ // A candidate rolled back before admitting any event has no generation-local
161
+ // feedback state, and its view is unavailable while Root is still idle.
162
+ shutdown = cleanupNeedsGenerationView ? runWithView(cleanup) : cleanup();
163
+ return shutdown;
164
+ }
165
+ return async () => {
166
+ stopWatchingRetirement?.();
167
+ await close();
168
+ };
110
169
  }
111
170
 
112
- export function mountActivityFeedbackService(
113
- plugin: Plugin,
114
- orchestrator: ActivityFeedbackOrchestrator,
115
- ): void {
116
- const dispose = bindActivityFeedbackToAIEvents(plugin.root, orchestrator);
117
- plugin.onDispose(() => {
118
- plugin.logger.debug('[ActivityFeedback] Disposing binder');
119
- dispose();
120
- });
171
+ /** Generation-local ordering: one queue per IM session, no module-level runtime state. */
172
+ function createGenerationSerializedTarget(
173
+ source: AIEventTarget,
174
+ admission: GenerationAdmissionGate,
175
+ runWithView: <T>(operation: () => Promise<T>) => Promise<T>,
176
+ onAdmitted: () => void,
177
+ ): {
178
+ target: AIEventTarget;
179
+ close(): Promise<void>;
180
+ } {
181
+ const tails = new Map<string, Promise<void>>();
182
+ const wrappers = new Map<(payload: AIEventPayload) => void | Promise<void>,
183
+ (payload: AIEventPayload) => Promise<void>>();
184
+ let closed = false;
185
+ const target: AIEventTarget = {
186
+ on(event, listener) {
187
+ const wrapped = async (payload: AIEventPayload) => {
188
+ const release = admission.acquire();
189
+ if (!release) return;
190
+ onAdmitted();
191
+ try {
192
+ await runWithView(async () => {
193
+ const key = payload.sessionId || '__global__';
194
+ const previous = tails.get(key);
195
+ const run = async () => {
196
+ if (!closed) await listener(payload);
197
+ };
198
+ const current = previous
199
+ ? previous.catch(() => undefined).then(run)
200
+ : run();
201
+ tails.set(key, current);
202
+ try {
203
+ await current;
204
+ } finally {
205
+ if (tails.get(key) === current) tails.delete(key);
206
+ }
207
+ });
208
+ } finally {
209
+ release();
210
+ }
211
+ };
212
+ wrappers.set(listener, wrapped);
213
+ return source.on(event, wrapped);
214
+ },
215
+ off(event, listener) {
216
+ const wrapped = wrappers.get(listener);
217
+ if (!wrapped) return source.off(event, listener);
218
+ wrappers.delete(listener);
219
+ return source.off(event, wrapped);
220
+ },
221
+ };
222
+ return {
223
+ target,
224
+ async close() {
225
+ closed = true;
226
+ const pending = [...tails.values()];
227
+ await Promise.allSettled(pending);
228
+ tails.clear();
229
+ wrappers.clear();
230
+ },
231
+ };
121
232
  }
122
233
 
123
234
  export type ActivityFeedbackLogger = {
@@ -139,17 +250,6 @@ export function createActivityFeedbackOrchestrator(
139
250
  return new ActivityFeedbackOrchestrator(policy, executor, options.logger);
140
251
  }
141
252
 
142
- export function createActivityFeedbackOrchestratorFromPlugin(
143
- plugin: Plugin,
144
- serviceConfig: ActivityFeedbackServiceConfig,
145
- ): ActivityFeedbackOrchestrator {
146
- return createActivityFeedbackOrchestrator({
147
- serviceConfig,
148
- access: createRootEndpointAccess(plugin.root),
149
- logger: plugin.logger,
150
- });
151
- }
152
-
153
253
  /** Runtime: prefer OutboundHost-backed access; else noop until Host wires outbound. */
154
254
  export function createActivityFeedbackOrchestratorForRuntime(
155
255
  serviceConfig: ActivityFeedbackServiceConfig,
package/src/config.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ActivityFeedbackConfig } from '@zhin.js/agent';
2
2
 
3
- /** 顶层 `activityFeedback` 配置(zhin.config.yml,与 endpoints 解耦) */
3
+ /** Plugin Runtime 实例配置(`plugins.<instanceKey>`,与 Adapter endpoint 配置解耦)。 */
4
4
  export interface ActivityFeedbackServiceConfig {
5
5
  enabled?: boolean;
6
6
  defaults?: ActivityFeedbackConfig;
@@ -61,19 +61,3 @@ export function loadActivityFeedbackServiceConfig(
61
61
  ...raw,
62
62
  };
63
63
  }
64
-
65
- /** @deprecated 使用 ActivityFeedbackPolicy */
66
- export function resolveActivityFeedbackForTarget(
67
- service: ActivityFeedbackServiceConfig,
68
- platform: string,
69
- endpointId: string,
70
- ): ActivityFeedbackConfig | undefined {
71
- if (service.enabled === false) {
72
- return { enabled: false };
73
- }
74
- return mergeActivityFeedbackLayers(
75
- service.defaults,
76
- service.platforms?.[platform],
77
- service.endpoints?.[`${platform}:${endpointId}`],
78
- );
79
- }