work-ally 0.2.0-alpha.1

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.
Files changed (172) hide show
  1. package/AGENTS.md +110 -0
  2. package/DASHBOARD.md +160 -0
  3. package/PRODUCT.md +113 -0
  4. package/README.md +403 -0
  5. package/ally.sh +171 -0
  6. package/bridge/src/approval-rules.ts +360 -0
  7. package/bridge/src/channel-delivery.ts +207 -0
  8. package/bridge/src/channel-types.ts +22 -0
  9. package/bridge/src/channels/fake/adapter.ts +31 -0
  10. package/bridge/src/channels/feishu/adapter.ts +411 -0
  11. package/bridge/src/channels/feishu/approvals.ts +6 -0
  12. package/bridge/src/channels/feishu/formatter.ts +276 -0
  13. package/bridge/src/channels/feishu/normalize.ts +368 -0
  14. package/bridge/src/codex-config.ts +52 -0
  15. package/bridge/src/config.ts +240 -0
  16. package/bridge/src/fake-runtime-client.ts +505 -0
  17. package/bridge/src/handoff-service.ts +494 -0
  18. package/bridge/src/logger.ts +194 -0
  19. package/bridge/src/memory-digest.ts +186 -0
  20. package/bridge/src/receiver-approval-autonomy.ts +158 -0
  21. package/bridge/src/receiver-control-core.ts +140 -0
  22. package/bridge/src/receiver-control-work-session.ts +218 -0
  23. package/bridge/src/receiver-control.ts +83 -0
  24. package/bridge/src/receiver-delivery.ts +136 -0
  25. package/bridge/src/receiver-helpers.ts +96 -0
  26. package/bridge/src/receiver-human-gate.ts +333 -0
  27. package/bridge/src/receiver-inbound-preflight.ts +162 -0
  28. package/bridge/src/receiver-recovery.ts +236 -0
  29. package/bridge/src/receiver-runtime-callbacks.ts +367 -0
  30. package/bridge/src/receiver-runtime-policy.ts +132 -0
  31. package/bridge/src/receiver-runtime-state.ts +124 -0
  32. package/bridge/src/receiver-support-actions.ts +189 -0
  33. package/bridge/src/receiver-thread-start.ts +57 -0
  34. package/bridge/src/receiver-turn-coordination.ts +94 -0
  35. package/bridge/src/receiver-turn-execution.ts +257 -0
  36. package/bridge/src/receiver-turn-failure.ts +143 -0
  37. package/bridge/src/receiver-turn-result.ts +185 -0
  38. package/bridge/src/receiver-turn-steer.ts +70 -0
  39. package/bridge/src/receiver-work-session.ts +76 -0
  40. package/bridge/src/receiver.ts +329 -0
  41. package/bridge/src/router.ts +62 -0
  42. package/bridge/src/runtime-client-agent-messages.ts +150 -0
  43. package/bridge/src/runtime-client-message-dispatch.ts +176 -0
  44. package/bridge/src/runtime-client-protocol.ts +411 -0
  45. package/bridge/src/runtime-client-request-ops.ts +56 -0
  46. package/bridge/src/runtime-client-run-turn.ts +158 -0
  47. package/bridge/src/runtime-client-thread-ops.ts +270 -0
  48. package/bridge/src/runtime-client-transport.ts +309 -0
  49. package/bridge/src/runtime-client-turn-poll.ts +224 -0
  50. package/bridge/src/runtime-client-turn-read.ts +185 -0
  51. package/bridge/src/runtime-client-turn-state.ts +105 -0
  52. package/bridge/src/runtime-client.ts +344 -0
  53. package/bridge/src/runtime-user-input.ts +403 -0
  54. package/bridge/src/scheduler.ts +239 -0
  55. package/bridge/src/server-handoff-command.ts +364 -0
  56. package/bridge/src/server-main.ts +80 -0
  57. package/bridge/src/server-routine-command.ts +60 -0
  58. package/bridge/src/server-routine-execution.ts +222 -0
  59. package/bridge/src/server-runtime-app-support.ts +107 -0
  60. package/bridge/src/server-runtime-app.ts +238 -0
  61. package/bridge/src/server-thread-sync-command.ts +63 -0
  62. package/bridge/src/server.ts +17 -0
  63. package/bridge/src/session-store-delivery.ts +220 -0
  64. package/bridge/src/session-store-human-gate.ts +380 -0
  65. package/bridge/src/session-store-inbound-acceptance.ts +66 -0
  66. package/bridge/src/session-store-meta.ts +134 -0
  67. package/bridge/src/session-store-turn-ledger.ts +272 -0
  68. package/bridge/src/session-store.ts +380 -0
  69. package/bridge/src/system-notify.ts +220 -0
  70. package/bridge/src/thread-sync.ts +200 -0
  71. package/bridge/src/translator.ts +494 -0
  72. package/bridge/src/types.ts +289 -0
  73. package/bridge/src/utils.ts +104 -0
  74. package/bridge/src/work-session-store.ts +471 -0
  75. package/docs/.gitkeep +0 -0
  76. package/docs/architecture/codex-feishu-bridge-proposal.md +2742 -0
  77. package/docs/completed/FEATURE-feishu-markdown-and-reply-support.md +327 -0
  78. package/docs/completed/README.md +21 -0
  79. package/docs/completed/SPEC-approval-autonomy-and-safe-defaults.md +205 -0
  80. package/docs/completed/SPEC-approval-batch-and-strict-reply-shortcuts.md +153 -0
  81. package/docs/completed/SPEC-conversation-noise-reduction-and-busy-input-gate.md +538 -0
  82. package/docs/completed/SPEC-engineering-sop-skillization.md +190 -0
  83. package/docs/completed/SPEC-faithful-bridge-core-thinning-v2.md +376 -0
  84. package/docs/completed/SPEC-faithful-bridge-core-thinning.md +1071 -0
  85. package/docs/completed/SPEC-group-chat-sender-identity.md +301 -0
  86. package/docs/completed/SPEC-middleware-exception-visibility.md +227 -0
  87. package/docs/completed/SPEC-nightly-memory-digest-visibility.md +121 -0
  88. package/docs/completed/SPEC-project-group-chat-human-centered-conversation-mapping.md +326 -0
  89. package/docs/completed/SPEC-remove-cli-persona-bootstrap.md +201 -0
  90. package/docs/developer-workflow.md +49 -0
  91. package/docs/implementation/SPEC-codex-same-machine-session-handoff-implementation.md +239 -0
  92. package/docs/implementation/test-coverage-map.md +363 -0
  93. package/docs/implementation/work-ally-implementation-guide.md +790 -0
  94. package/docs/issues/README.md +10 -0
  95. package/docs/issues/pending/ANALYSIS-ally-premature-recovery-notice-and-task-state-semantics-2026-03-18.md +295 -0
  96. package/docs/issues/resolved/ANALYSIS-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +466 -0
  97. package/docs/issues/resolved/ANALYSIS-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +261 -0
  98. package/docs/issues/resolved/ANALYSIS-codex-app-server-transport-disconnect-semantics-2026-03-14.md +606 -0
  99. package/docs/issues/resolved/ANALYSIS-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +348 -0
  100. package/docs/issues/resolved/ANALYSIS-runtime-turn-delivery-and-recovery-2026-03-14.md +603 -0
  101. package/docs/issues/resolved/ANALYSIS-self-test-gap-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +166 -0
  102. package/docs/issues/resolved/ANALYSIS-self-test-gap-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +186 -0
  103. package/docs/issues/resolved/ANALYSIS-self-test-gap-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +166 -0
  104. package/docs/issues/resolved/REPORT-ally-runtime-turn-delivery-3b42fb8-2026-03-15.md +373 -0
  105. package/docs/manual-acceptance.md +127 -0
  106. package/docs/ops-runbook.md +44 -0
  107. package/docs/planning/FEATURE-memory-system.md +748 -0
  108. package/docs/planning/SPEC-active-turn-steer-and-context-compaction-visibility.md +269 -0
  109. package/docs/planning/SPEC-approval-rules-inheritance-and-local-validation-lane.md +450 -0
  110. package/docs/planning/SPEC-assistant-persona-bootstrap.md +199 -0
  111. package/docs/planning/SPEC-assistant-rename.md +610 -0
  112. package/docs/planning/SPEC-bridge-app-server-protocol-alignment.md +667 -0
  113. package/docs/planning/SPEC-claude-runtime-host-for-work-ally.md +434 -0
  114. package/docs/planning/SPEC-cli-feishu-codex-session-unification.md +236 -0
  115. package/docs/planning/SPEC-codex-same-machine-session-handoff.md +873 -0
  116. package/docs/planning/SPEC-feishu-reaction-shortcuts.md +282 -0
  117. package/docs/planning/SPEC-local-stable-release-boundary.md +166 -0
  118. package/docs/planning/SPEC-managed-thread-entry-and-surface-mobility.md +862 -0
  119. package/docs/planning/SPEC-minimal-bridge-semantics-and-user-visible-surface.md +362 -0
  120. package/docs/planning/SPEC-npm-alpha-distribution-and-install-first-release.md +222 -0
  121. package/docs/planning/SPEC-remove-websocket-runtime-transport.md +364 -0
  122. package/docs/planning/SPEC-runtime-abstraction-phase-1.md +424 -0
  123. package/docs/planning/SPEC-runtime-connection-and-turn-recovery-semantics.md +274 -0
  124. package/docs/planning/SPEC-session-presence-and-state-visibility.md +397 -0
  125. package/docs/planning/SPEC-skill-first-capability-packaging.md +338 -0
  126. package/docs/planning/SPEC-stable-archive-contract.md +456 -0
  127. package/docs/planning/SPEC-supervised-start-boundary.md +127 -0
  128. package/docs/planning/SPEC-user-barrier-reduction-and-activation.md +832 -0
  129. package/docs/planning/ally-next.md +1278 -0
  130. package/docs/planning/assistant-workbench-spec.md +725 -0
  131. package/docs/planning/product-workbench.md +283 -0
  132. package/docs/product-onboarding.md +227 -0
  133. package/docs/product-spec-standard.md +528 -0
  134. package/docs/troubleshooting.md +45 -0
  135. package/docs/user-quickstart.md +46 -0
  136. package/internal/dispatch.sh +95 -0
  137. package/internal/lib/common.sh +1450 -0
  138. package/internal/modules/assistant/manage.sh +1312 -0
  139. package/internal/modules/bootstrap/setup.sh +144 -0
  140. package/internal/modules/config/init-env.sh +10 -0
  141. package/internal/modules/global/manage.sh +154 -0
  142. package/internal/modules/handoff/manage.sh +54 -0
  143. package/internal/modules/mcp/manage.sh +83 -0
  144. package/internal/modules/ops/logs.sh +76 -0
  145. package/internal/modules/routines/manage.sh +55 -0
  146. package/internal/modules/runtime/assistant-autosave.sh +26 -0
  147. package/internal/modules/runtime/restart.sh +6 -0
  148. package/internal/modules/runtime/start.sh +283 -0
  149. package/internal/modules/runtime/status.sh +194 -0
  150. package/internal/modules/runtime/stop.sh +55 -0
  151. package/internal/modules/runtime/supervisor.sh +216 -0
  152. package/internal/modules/runtime/update.sh +26 -0
  153. package/package.json +41 -0
  154. package/runtime/config/.gitkeep +0 -0
  155. package/runtime/host/.gitkeep +0 -0
  156. package/runtime/host/healthcheck-codex-app-server.ts +22 -0
  157. package/runtime/host/ping-pong-codex-app-server.ts +66 -0
  158. package/runtime/host/probe-codex-app-server.ts +115 -0
  159. package/skills/archive-reader/SKILL.md +9 -0
  160. package/skills/feishu-production-debug/SKILL.md +37 -0
  161. package/skills/feishu-production-debug/references/feishu-debug-order.md +49 -0
  162. package/skills/feishu-production-debug/references/platform-permission-baseline.md +23 -0
  163. package/skills/issue-to-spec-triage/SKILL.md +44 -0
  164. package/skills/issue-to-spec-triage/references/triage-rules.md +66 -0
  165. package/skills/memory-digest/SKILL.md +9 -0
  166. package/skills/post-implementation-closure/SKILL.md +39 -0
  167. package/skills/post-implementation-closure/references/closure-checklist.md +45 -0
  168. package/skills/post-implementation-closure/references/doc-drift-map.md +49 -0
  169. package/skills/product-spec/SKILL.md +244 -0
  170. package/templates/env.example +5 -0
  171. package/templates/routines/nightly-memory-digest.yaml +10 -0
  172. package/templates/workspace/AGENTS.md +26 -0
@@ -0,0 +1,505 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { randomId, sleep } from './utils.ts';
3
+ import type { ApprovalRequest, ConnectionLifecycleEvent, RuntimeProgressMeta, RuntimeThreadStatus, RuntimeTurnResult, UserInputRequest } from './types.ts';
4
+
5
+ type FakeApprovalDecision = 'accept' | 'acceptForSession' | 'decline' | 'cancel';
6
+
7
+ interface FakeTurnOptions {
8
+ threadId: string;
9
+ prompt: string;
10
+ messageId: string;
11
+ onTurnStarted?: (turnId: string) => Promise<void> | void;
12
+ onProgress?: (text: string, meta?: RuntimeProgressMeta) => Promise<void> | void;
13
+ onThreadStatus?: (status: RuntimeThreadStatus, meta?: { threadId?: string | null; turnId?: string | null }) => Promise<void> | void;
14
+ onApproval?: (approval: ApprovalRequest) => Promise<void> | void;
15
+ onUserInput?: (request: UserInputRequest) => Promise<void> | void;
16
+ }
17
+
18
+ type FakeUserInputResponse = { answers: Record<string, { answers: string[] }> };
19
+ type FakeElicitationResponse = { action: 'accept' | 'decline' | 'cancel'; content: unknown | null };
20
+
21
+
22
+ export class FakeRuntimeClient {
23
+ private readonly approvalResolvers = new Map<string, (decision: FakeApprovalDecision) => void>();
24
+ private readonly emitter = new EventEmitter();
25
+ private readonly approvalThreadIds = new Map<string, string>();
26
+ private readonly interruptedTurns = new Set<string>();
27
+ private readonly threadStatuses = new Map<string, RuntimeThreadStatus>();
28
+ private readonly userInputResolvers = new Map<string, (response: FakeUserInputResponse | FakeElicitationResponse) => void>();
29
+ private readonly userInputThreadIds = new Map<string, string>();
30
+ private readonly pendingUserInputResponses = new Map<string, FakeUserInputResponse | FakeElicitationResponse>();
31
+ private readonly activeThreadTurns = new Map<string, string>();
32
+ private readonly turnPrompts = new Map<string, string[]>();
33
+ private readonly turnThreadIds = new Map<string, string>();
34
+
35
+ private setThreadStatus(threadId: string, status: RuntimeThreadStatus): RuntimeThreadStatus {
36
+ const normalized = {
37
+ type: status.type,
38
+ activeFlags: [...status.activeFlags],
39
+ } satisfies RuntimeThreadStatus;
40
+ this.threadStatuses.set(threadId, normalized);
41
+ return normalized;
42
+ }
43
+
44
+ onConnectionEvent(listener: (event: ConnectionLifecycleEvent) => void): void {
45
+ this.emitter.on('connectionLifecycle', listener);
46
+ }
47
+
48
+ onServerRequestResolved(listener: (event: { requestId: string | number; threadId: string | null; turnId: string | null }) => void): void {
49
+ this.emitter.on('serverRequestResolved', listener);
50
+ }
51
+
52
+ offServerRequestResolved(listener: (event: { requestId: string | number; threadId: string | null; turnId: string | null }) => void): void {
53
+ this.emitter.off('serverRequestResolved', listener);
54
+ }
55
+
56
+ offConnectionEvent(listener: (event: ConnectionLifecycleEvent) => void): void {
57
+ this.emitter.off('connectionLifecycle', listener);
58
+ }
59
+
60
+ emitConnectionEvent(event: ConnectionLifecycleEvent): void {
61
+ this.emitter.emit('connectionLifecycle', event);
62
+ }
63
+
64
+ emitServerRequestResolved(event: { requestId: string | number; threadId: string | null; turnId: string | null }): void {
65
+ this.emitter.emit('serverRequestResolved', event);
66
+ }
67
+
68
+
69
+ async healthcheck(): Promise<boolean> {
70
+ return true;
71
+ }
72
+
73
+ async startThread(_cwd?: string, _options?: unknown): Promise<{ id: string }> {
74
+ const id = randomId('thread');
75
+ this.setThreadStatus(id, { type: 'idle', activeFlags: [] });
76
+ return { id };
77
+ }
78
+
79
+ async resumeThread(threadId: string, _cwd?: string, _options?: unknown): Promise<{ id: string }> {
80
+ if (!this.threadStatuses.has(threadId)) {
81
+ this.setThreadStatus(threadId, { type: 'idle', activeFlags: [] });
82
+ }
83
+ return { id: threadId };
84
+ }
85
+
86
+ async readThreadStatus(threadId: string): Promise<RuntimeThreadStatus | null> {
87
+ return this.threadStatuses.get(threadId) || null;
88
+ }
89
+
90
+ async readThreadWithTurns(threadId: string): Promise<{ id: string; status: RuntimeThreadStatus | null; turns: any[] }> {
91
+ return {
92
+ id: threadId,
93
+ status: this.threadStatuses.get(threadId) || null,
94
+ turns: [],
95
+ };
96
+ }
97
+
98
+ async disconnect(): Promise<void> {
99
+ return;
100
+ }
101
+
102
+ async interruptTurn(threadId: string, turnId: string): Promise<void> {
103
+ this.interruptedTurns.add(turnId);
104
+ if (this.activeThreadTurns.get(threadId) === turnId) {
105
+ this.activeThreadTurns.delete(threadId);
106
+ this.setThreadStatus(threadId, { type: 'idle', activeFlags: [] });
107
+ }
108
+ this.turnPrompts.delete(turnId);
109
+ this.turnThreadIds.delete(turnId);
110
+ }
111
+
112
+ async steerTurn(threadId: string, turnId: string, prompt: string): Promise<void> {
113
+ if (this.activeThreadTurns.get(threadId) !== turnId) {
114
+ throw new Error(`turn not active: ${turnId}`);
115
+ }
116
+ const prompts = this.turnPrompts.get(turnId) || [];
117
+ prompts.push(prompt);
118
+ this.turnPrompts.set(turnId, prompts);
119
+ }
120
+
121
+ async resolveApproval(requestId: string | number, decision: FakeApprovalDecision): Promise<void> {
122
+ const key = String(requestId);
123
+ const resolver = this.approvalResolvers.get(key);
124
+ const threadId = this.approvalThreadIds.get(key);
125
+ const turnId = threadId ? this.activeThreadTurns.get(threadId) || null : null;
126
+ if (threadId) {
127
+ this.setThreadStatus(threadId, { type: 'active', activeFlags: [] });
128
+ this.approvalThreadIds.delete(key);
129
+ }
130
+ this.emitter.emit('serverRequestResolved', { requestId, threadId: threadId || null, turnId });
131
+ if (resolver) {
132
+ this.approvalResolvers.delete(key);
133
+ resolver(decision);
134
+ }
135
+ }
136
+
137
+ async resolveUserInput(requestId: string | number, response: FakeUserInputResponse): Promise<void> {
138
+ const key = String(requestId);
139
+ const resolver = this.userInputResolvers.get(key);
140
+ const threadId = this.userInputThreadIds.get(key);
141
+ const turnId = threadId ? this.activeThreadTurns.get(threadId) || null : null;
142
+ if (threadId) {
143
+ this.setThreadStatus(threadId, { type: 'active', activeFlags: [] });
144
+ this.userInputThreadIds.delete(key);
145
+ }
146
+ this.emitter.emit('serverRequestResolved', { requestId, threadId: threadId || null, turnId });
147
+ if (resolver) {
148
+ this.userInputResolvers.delete(key);
149
+ resolver(response);
150
+ return;
151
+ }
152
+ this.pendingUserInputResponses.set(key, response);
153
+ }
154
+
155
+ async resolveElicitation(requestId: string | number, response: FakeElicitationResponse): Promise<void> {
156
+ const key = String(requestId);
157
+ const resolver = this.userInputResolvers.get(key);
158
+ const threadId = this.userInputThreadIds.get(key);
159
+ const turnId = threadId ? this.activeThreadTurns.get(threadId) || null : null;
160
+ if (threadId) {
161
+ this.setThreadStatus(threadId, { type: 'active', activeFlags: [] });
162
+ this.userInputThreadIds.delete(key);
163
+ }
164
+ this.emitter.emit('serverRequestResolved', { requestId, threadId: threadId || null, turnId });
165
+ if (resolver) {
166
+ this.userInputResolvers.delete(key);
167
+ resolver(response);
168
+ return;
169
+ }
170
+ this.pendingUserInputResponses.set(key, response);
171
+ }
172
+
173
+ private isInterrupted(turnId: string): boolean {
174
+ return this.interruptedTurns.has(turnId);
175
+ }
176
+
177
+ private async waitForApproval(approval: ApprovalRequest): Promise<FakeApprovalDecision> {
178
+ this.approvalThreadIds.set(String(approval.requestId), approval.threadId);
179
+ return new Promise<FakeApprovalDecision>((resolve) => {
180
+ this.approvalResolvers.set(String(approval.requestId), resolve);
181
+ });
182
+ }
183
+
184
+ private async waitForUserInput(request: UserInputRequest): Promise<FakeUserInputResponse | FakeElicitationResponse> {
185
+ const key = String(request.requestId);
186
+ this.userInputThreadIds.set(key, request.threadId);
187
+ const early = this.pendingUserInputResponses.get(key);
188
+ if (early) {
189
+ this.pendingUserInputResponses.delete(key);
190
+ return early;
191
+ }
192
+ return new Promise((resolve) => {
193
+ this.userInputResolvers.set(key, resolve);
194
+ });
195
+ }
196
+
197
+ async runTurn(options: FakeTurnOptions): Promise<RuntimeTurnResult> {
198
+ const turnId = randomId('turn');
199
+ this.activeThreadTurns.set(options.threadId, turnId);
200
+ this.turnPrompts.set(turnId, [options.prompt]);
201
+ this.turnThreadIds.set(turnId, options.threadId);
202
+ this.setThreadStatus(options.threadId, { type: 'active', activeFlags: [] });
203
+ await options.onTurnStarted?.(turnId);
204
+ await options.onProgress?.('正在处理...');
205
+
206
+ if (options.prompt.includes('[context-compaction]')) {
207
+ const itemId = 'item_context_compaction_1';
208
+ await options.onProgress?.('Codex 正在压缩当前会话上下文。', {
209
+ kind: 'context_compaction',
210
+ phase: null,
211
+ turnId,
212
+ itemId,
213
+ });
214
+ await options.onProgress?.('Codex 正在压缩当前会话上下文。', {
215
+ kind: 'context_compaction',
216
+ phase: null,
217
+ turnId,
218
+ itemId,
219
+ });
220
+ }
221
+
222
+ if (options.prompt.includes('[slow]')) {
223
+ await sleep(150);
224
+ } else {
225
+ await sleep(20);
226
+ }
227
+
228
+ if (this.isInterrupted(turnId)) {
229
+ this.activeThreadTurns.delete(options.threadId);
230
+ this.setThreadStatus(options.threadId, { type: 'idle', activeFlags: [] });
231
+ this.turnPrompts.delete(turnId);
232
+ this.turnThreadIds.delete(turnId);
233
+ return {
234
+ threadId: options.threadId,
235
+ turnId,
236
+ status: 'failed',
237
+ reply: '',
238
+ error: 'turn interrupted',
239
+ };
240
+ }
241
+
242
+ if (options.prompt.includes('[file-approval]')) {
243
+ const approval: ApprovalRequest = {
244
+ approvalId: randomId('approval'),
245
+ kind: 'file_change',
246
+ requestId: randomId('request'),
247
+ threadId: options.threadId,
248
+ turnId,
249
+ messageId: options.messageId,
250
+ reason: 'fake file-change approval gate',
251
+ grantRoot: '/workspace/docs',
252
+ cwd: '/workspace',
253
+ };
254
+ const waitingStatus = this.setThreadStatus(options.threadId, { type: 'active', activeFlags: ['waitingOnApproval'] });
255
+ await options.onThreadStatus?.(waitingStatus, { threadId: options.threadId, turnId });
256
+ await options.onApproval?.(approval);
257
+ const decision = await this.waitForApproval(approval);
258
+ if (decision === 'decline' || decision === 'cancel') {
259
+ this.activeThreadTurns.delete(options.threadId);
260
+ this.setThreadStatus(options.threadId, { type: 'idle', activeFlags: [] });
261
+ this.turnPrompts.delete(turnId);
262
+ this.turnThreadIds.delete(turnId);
263
+ return {
264
+ threadId: options.threadId,
265
+ turnId,
266
+ status: 'failed',
267
+ reply: '',
268
+ error: 'approval declined',
269
+ };
270
+ }
271
+ this.setThreadStatus(options.threadId, { type: 'active', activeFlags: [] });
272
+ }
273
+
274
+ if (options.prompt.includes('[approval]')) {
275
+ const approval: ApprovalRequest = {
276
+ approvalId: randomId('approval'),
277
+ kind: 'command',
278
+ requestId: randomId('request'),
279
+ threadId: options.threadId,
280
+ turnId,
281
+ messageId: options.messageId,
282
+ command: 'echo protected-action',
283
+ reason: 'fake approval gate',
284
+ };
285
+ const waitingStatus = this.setThreadStatus(options.threadId, { type: 'active', activeFlags: ['waitingOnApproval'] });
286
+ await options.onThreadStatus?.(waitingStatus, { threadId: options.threadId, turnId });
287
+ await options.onApproval?.(approval);
288
+ const decision = await this.waitForApproval(approval);
289
+ if (decision === 'decline' || decision === 'cancel') {
290
+ this.activeThreadTurns.delete(options.threadId);
291
+ this.setThreadStatus(options.threadId, { type: 'idle', activeFlags: [] });
292
+ this.turnPrompts.delete(turnId);
293
+ this.turnThreadIds.delete(turnId);
294
+ return {
295
+ threadId: options.threadId,
296
+ turnId,
297
+ status: 'failed',
298
+ reply: '',
299
+ error: 'approval declined',
300
+ };
301
+ }
302
+ this.setThreadStatus(options.threadId, { type: 'active', activeFlags: [] });
303
+ }
304
+
305
+ if (options.prompt.includes('[ask-user]')) {
306
+ const request: UserInputRequest = {
307
+ requestId: randomId('request_user_input'),
308
+ threadId: options.threadId,
309
+ turnId,
310
+ itemId: randomId('item'),
311
+ messageId: options.messageId,
312
+ questions: [
313
+ {
314
+ id: 'clarify_goal',
315
+ header: '补充目标',
316
+ question: '你希望我重点关注什么?',
317
+ options: [
318
+ { label: '摘要', description: '提取核心要点' },
319
+ { label: '改写', description: '直接给出改写稿' },
320
+ ],
321
+ },
322
+ ],
323
+ };
324
+ const waitingStatus = this.setThreadStatus(options.threadId, { type: 'active', activeFlags: ['waitingOnUserInput'] });
325
+ await options.onThreadStatus?.(waitingStatus, { threadId: options.threadId, turnId });
326
+ await options.onUserInput?.(request);
327
+ const response = await this.waitForUserInput(request) as FakeUserInputResponse;
328
+ const answer = response.answers.clarify_goal?.answers?.join(' / ') || '未提供答案';
329
+ this.setThreadStatus(options.threadId, { type: 'active', activeFlags: [] });
330
+ const reply = `fake runtime reply after user input: ${answer}`;
331
+ this.activeThreadTurns.delete(options.threadId);
332
+ this.setThreadStatus(options.threadId, { type: 'idle', activeFlags: [] });
333
+ this.turnPrompts.delete(turnId);
334
+ this.turnThreadIds.delete(turnId);
335
+ return {
336
+ threadId: options.threadId,
337
+ turnId,
338
+ status: 'completed',
339
+ reply,
340
+ };
341
+ }
342
+
343
+ if (options.prompt.includes('[mcp-elicitation]') || options.prompt.includes('[mcp-elicitation-github]')) {
344
+ const isGithub = options.prompt.includes('[mcp-elicitation-github]');
345
+ const request: UserInputRequest = {
346
+ requestId: randomId('request_user_input'),
347
+ threadId: options.threadId,
348
+ turnId,
349
+ itemId: null,
350
+ messageId: options.messageId,
351
+ kind: 'mcp_elicitation',
352
+ elicitation: {
353
+ serverName: isGithub ? 'github' : 'feishu',
354
+ mode: 'form',
355
+ message: isGithub
356
+ ? 'The github MCP server wants to run the tool "create-issue", which may modify data and access external systems. Allow this action?'
357
+ : 'The feishu MCP server wants to run the tool "fetch-doc", which may modify data and access external systems. Allow this action?',
358
+ requestedSchema: {
359
+ type: 'object',
360
+ properties: {
361
+ response: {
362
+ oneOf: [
363
+ { const: 'approved', title: 'Approve this Session', description: 'Run the tool and remember this choice for this session.' },
364
+ { const: 'approved_once', title: 'Approve Once', description: 'Run the tool and continue.' },
365
+ { const: 'declined', title: 'Deny', description: 'Decline this tool call and continue.' },
366
+ { const: 'cancelled', title: 'Cancel', description: 'Cancel this tool call.' },
367
+ ],
368
+ },
369
+ },
370
+ },
371
+ },
372
+ questions: [
373
+ {
374
+ id: 'response',
375
+ header: 'MCP 确认',
376
+ question: isGithub
377
+ ? 'The github MCP server wants to run the tool "create-issue", which may modify data and access external systems. Allow this action?'
378
+ : 'The feishu MCP server wants to run the tool "fetch-doc", which may modify data and access external systems. Allow this action?',
379
+ options: [
380
+ { label: 'Approve Once', description: 'Run the tool and continue.' },
381
+ { label: 'Approve this Session', description: 'Run the tool and remember this choice for this session.' },
382
+ { label: 'Deny', description: 'Decline this tool call and continue.' },
383
+ { label: 'Cancel', description: 'Cancel this tool call.' },
384
+ ],
385
+ },
386
+ ],
387
+ };
388
+ const waitingStatus = this.setThreadStatus(options.threadId, { type: 'active', activeFlags: ['waitingOnUserInput'] });
389
+ await options.onThreadStatus?.(waitingStatus, { threadId: options.threadId, turnId });
390
+ await options.onUserInput?.(request);
391
+ const response = await this.waitForUserInput(request) as FakeElicitationResponse;
392
+ this.setThreadStatus(options.threadId, { type: 'active', activeFlags: [] });
393
+ const selected = (response.content && typeof response.content === 'object' && !Array.isArray(response.content))
394
+ ? String((response.content as any).response || '')
395
+ : '';
396
+ if (response.action !== 'accept' || !['approved', 'approved_once', 'Approve this Session', 'Approve Once'].includes(selected)) {
397
+ this.activeThreadTurns.delete(options.threadId);
398
+ this.setThreadStatus(options.threadId, { type: 'idle', activeFlags: [] });
399
+ this.turnPrompts.delete(turnId);
400
+ this.turnThreadIds.delete(turnId);
401
+ return {
402
+ threadId: options.threadId,
403
+ turnId,
404
+ status: 'failed',
405
+ reply: '',
406
+ error: 'mcp elicitation declined',
407
+ };
408
+ }
409
+ const reply = 'fake runtime reply after MCP elicitation approval';
410
+ this.activeThreadTurns.delete(options.threadId);
411
+ this.setThreadStatus(options.threadId, { type: 'idle', activeFlags: [] });
412
+ this.turnPrompts.delete(turnId);
413
+ this.turnThreadIds.delete(turnId);
414
+ return {
415
+ threadId: options.threadId,
416
+ turnId,
417
+ status: 'completed',
418
+ reply,
419
+ };
420
+ }
421
+
422
+ if (options.prompt.includes('[mcp-tool-approval]') || options.prompt.includes('[mcp-tool-approval-github]')) {
423
+ const isGithub = options.prompt.includes('[mcp-tool-approval-github]');
424
+ const request: UserInputRequest = {
425
+ requestId: randomId('request_user_input'),
426
+ threadId: options.threadId,
427
+ turnId,
428
+ itemId: randomId('item'),
429
+ messageId: options.messageId,
430
+ questions: [
431
+ {
432
+ id: 'mcp_tool_call_approval_call_1',
433
+ header: 'Approve app tool call?',
434
+ question: isGithub
435
+ ? 'The github MCP server wants to run the tool "create-issue", which may modify data and access external systems. Allow this action?'
436
+ : 'The feishu MCP server wants to run the tool "fetch-doc", which may modify data and access external systems. Allow this action?',
437
+ options: [
438
+ { label: 'Approve Once', description: 'Run the tool and continue.' },
439
+ { label: 'Approve this Session', description: 'Run the tool and remember this choice for this session.' },
440
+ { label: 'Deny', description: 'Decline this tool call and continue.' },
441
+ { label: 'Cancel', description: 'Cancel this tool call' },
442
+ ],
443
+ },
444
+ ],
445
+ };
446
+ const waitingStatus = this.setThreadStatus(options.threadId, { type: 'active', activeFlags: ['waitingOnUserInput'] });
447
+ await options.onThreadStatus?.(waitingStatus, { threadId: options.threadId, turnId });
448
+ await options.onUserInput?.(request);
449
+ const response = await this.waitForUserInput(request) as FakeUserInputResponse;
450
+ const answer = response.answers.mcp_tool_call_approval_call_1?.answers?.[0] || '';
451
+ this.setThreadStatus(options.threadId, { type: 'active', activeFlags: [] });
452
+ if (answer !== 'Approve this Session' && answer !== 'Approve Once') {
453
+ this.activeThreadTurns.delete(options.threadId);
454
+ this.setThreadStatus(options.threadId, { type: 'idle', activeFlags: [] });
455
+ this.turnPrompts.delete(turnId);
456
+ this.turnThreadIds.delete(turnId);
457
+ return {
458
+ threadId: options.threadId,
459
+ turnId,
460
+ status: 'failed',
461
+ reply: '',
462
+ error: 'mcp tool call declined',
463
+ };
464
+ }
465
+ const reply = 'fake runtime reply after MCP tool approval';
466
+ this.activeThreadTurns.delete(options.threadId);
467
+ this.setThreadStatus(options.threadId, { type: 'idle', activeFlags: [] });
468
+ this.turnPrompts.delete(turnId);
469
+ this.turnThreadIds.delete(turnId);
470
+ return {
471
+ threadId: options.threadId,
472
+ turnId,
473
+ status: 'completed',
474
+ reply,
475
+ };
476
+ }
477
+
478
+ if (this.isInterrupted(turnId)) {
479
+ this.activeThreadTurns.delete(options.threadId);
480
+ this.setThreadStatus(options.threadId, { type: 'idle', activeFlags: [] });
481
+ this.turnPrompts.delete(turnId);
482
+ this.turnThreadIds.delete(turnId);
483
+ return {
484
+ threadId: options.threadId,
485
+ turnId,
486
+ status: 'failed',
487
+ reply: '',
488
+ error: 'turn interrupted',
489
+ };
490
+ }
491
+
492
+ const prompts = this.turnPrompts.get(turnId) || [options.prompt];
493
+ const reply = `fake runtime reply: ${prompts.join(' | ')}`;
494
+ this.activeThreadTurns.delete(options.threadId);
495
+ this.setThreadStatus(options.threadId, { type: 'idle', activeFlags: [] });
496
+ this.turnPrompts.delete(turnId);
497
+ this.turnThreadIds.delete(turnId);
498
+ return {
499
+ threadId: options.threadId,
500
+ turnId,
501
+ status: 'completed',
502
+ reply,
503
+ };
504
+ }
505
+ }