@ai-zen/agents-host 0.0.0-stage → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.md +759 -2
- package/dist/app.d.ts +384 -0
- package/dist/app.js +1148 -0
- package/dist/cli.d.ts +21 -0
- package/dist/cli.js +353 -0
- package/dist/config.d.ts +97 -0
- package/dist/config.js +171 -0
- package/dist/handlers/agentHandlers.d.ts +18 -0
- package/dist/handlers/agentHandlers.js +20 -0
- package/dist/handlers/assetHandlers.d.ts +29 -0
- package/dist/handlers/assetHandlers.js +35 -0
- package/dist/handlers/capabilities.d.ts +26 -0
- package/dist/handlers/capabilities.js +138 -0
- package/dist/handlers/completionHandlers.d.ts +18 -0
- package/dist/handlers/completionHandlers.js +13 -0
- package/dist/handlers/configHandlers.d.ts +31 -0
- package/dist/handlers/configHandlers.js +59 -0
- package/dist/handlers/conversationHandlers.d.ts +118 -0
- package/dist/handlers/conversationHandlers.js +314 -0
- package/dist/handlers/handoffHandlers.d.ts +17 -0
- package/dist/handlers/handoffHandlers.js +14 -0
- package/dist/handlers/hostHandlers.d.ts +39 -0
- package/dist/handlers/hostHandlers.js +56 -0
- package/dist/handlers/index.d.ts +128 -0
- package/dist/handlers/index.js +92 -0
- package/dist/handlers/messageHandlers.d.ts +11 -0
- package/dist/handlers/messageHandlers.js +10 -0
- package/dist/handlers/modelHandlers.d.ts +17 -0
- package/dist/handlers/modelHandlers.js +19 -0
- package/dist/handlers/noticeHandlers.d.ts +24 -0
- package/dist/handlers/noticeHandlers.js +24 -0
- package/dist/handlers/questionHandlers.d.ts +16 -0
- package/dist/handlers/questionHandlers.js +16 -0
- package/dist/handlers/searchHandlers.d.ts +12 -0
- package/dist/handlers/searchHandlers.js +12 -0
- package/dist/handlers/workspaceHandlers.d.ts +29 -0
- package/dist/handlers/workspaceHandlers.js +47 -0
- package/dist/i18n/en.d.ts +243 -0
- package/dist/i18n/en.js +270 -0
- package/dist/i18n/index.d.ts +43 -0
- package/dist/i18n/index.js +72 -0
- package/dist/i18n/zh.d.ts +240 -0
- package/dist/i18n/zh.js +257 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +15 -0
- package/dist/net/UploadReceiver.d.ts +186 -0
- package/dist/net/UploadReceiver.js +494 -0
- package/dist/net/WsHub.d.ts +231 -0
- package/dist/net/WsHub.js +689 -0
- package/dist/net/auth.d.ts +73 -0
- package/dist/net/auth.js +74 -0
- package/dist/net/discovery.d.ts +72 -0
- package/dist/net/discovery.js +171 -0
- package/dist/net/index.d.ts +14 -0
- package/dist/net/index.js +10 -0
- package/dist/runtime/ChatRunner.d.ts +374 -0
- package/dist/runtime/ChatRunner.js +1204 -0
- package/dist/runtime/ConversationDrafts.d.ts +43 -0
- package/dist/runtime/ConversationDrafts.js +47 -0
- package/dist/runtime/RunRegistry.d.ts +79 -0
- package/dist/runtime/RunRegistry.js +108 -0
- package/dist/runtime/ScopeRegistry.d.ts +153 -0
- package/dist/runtime/ScopeRegistry.js +259 -0
- package/dist/runtime/TransientRunner.d.ts +90 -0
- package/dist/runtime/TransientRunner.js +179 -0
- package/dist/runtime/index.d.ts +14 -0
- package/dist/runtime/index.js +10 -0
- package/dist/services/AgentService.d.ts +150 -0
- package/dist/services/AgentService.js +718 -0
- package/dist/services/AssetService.d.ts +338 -0
- package/dist/services/AssetService.js +739 -0
- package/dist/services/AutoNaming.d.ts +92 -0
- package/dist/services/AutoNaming.js +235 -0
- package/dist/services/CompletionService.d.ts +122 -0
- package/dist/services/CompletionService.js +442 -0
- package/dist/services/ConfigService.d.ts +171 -0
- package/dist/services/ConfigService.js +695 -0
- package/dist/services/ConversationService.d.ts +274 -0
- package/dist/services/ConversationService.js +902 -0
- package/dist/services/CredentialService.d.ts +33 -0
- package/dist/services/CredentialService.js +86 -0
- package/dist/services/DiscoveryService.d.ts +182 -0
- package/dist/services/DiscoveryService.js +998 -0
- package/dist/services/HandoffService.d.ts +189 -0
- package/dist/services/HandoffService.js +414 -0
- package/dist/services/Logger.d.ts +130 -0
- package/dist/services/Logger.js +256 -0
- package/dist/services/MessageService.d.ts +88 -0
- package/dist/services/MessageService.js +280 -0
- package/dist/services/ModelService.d.ts +89 -0
- package/dist/services/ModelService.js +489 -0
- package/dist/services/NoticeService.d.ts +93 -0
- package/dist/services/NoticeService.js +217 -0
- package/dist/services/QuestionService.d.ts +184 -0
- package/dist/services/QuestionService.js +457 -0
- package/dist/services/SearchService.d.ts +54 -0
- package/dist/services/SearchService.js +117 -0
- package/dist/services/WorkspaceService.d.ts +96 -0
- package/dist/services/WorkspaceService.js +213 -0
- package/dist/services/imageSize.d.ts +41 -0
- package/dist/services/imageSize.js +146 -0
- package/dist/services/index.d.ts +20 -0
- package/dist/services/index.js +15 -0
- package/dist/services/secretMask.d.ts +24 -0
- package/dist/services/secretMask.js +34 -0
- package/dist/storage/AssetRepository.d.ts +87 -0
- package/dist/storage/AssetRepository.js +180 -0
- package/dist/storage/ConversationRepository.d.ts +55 -0
- package/dist/storage/ConversationRepository.js +128 -0
- package/dist/storage/CredentialRepository.d.ts +28 -0
- package/dist/storage/CredentialRepository.js +47 -0
- package/dist/storage/Database.d.ts +95 -0
- package/dist/storage/Database.js +180 -0
- package/dist/storage/MessageRepository.d.ts +104 -0
- package/dist/storage/MessageRepository.js +323 -0
- package/dist/storage/MetaRepository.d.ts +21 -0
- package/dist/storage/MetaRepository.js +33 -0
- package/dist/storage/NoticeRepository.d.ts +31 -0
- package/dist/storage/NoticeRepository.js +89 -0
- package/dist/storage/QuestionRepository.d.ts +34 -0
- package/dist/storage/QuestionRepository.js +96 -0
- package/dist/storage/WorkspaceRepository.d.ts +25 -0
- package/dist/storage/WorkspaceRepository.js +44 -0
- package/dist/storage/dbWorker.d.ts +10 -0
- package/dist/storage/dbWorker.js +132 -0
- package/dist/storage/index.d.ts +20 -0
- package/dist/storage/index.js +16 -0
- package/dist/storage/migrations.d.ts +26 -0
- package/dist/storage/migrations.js +207 -0
- package/dist/storage/types.d.ts +298 -0
- package/dist/storage/types.js +14 -0
- package/dist/wire/errors.d.ts +105 -0
- package/dist/wire/errors.js +143 -0
- package/dist/wire/events.d.ts +102 -0
- package/dist/wire/events.js +13 -0
- package/dist/wire/frames.d.ts +106 -0
- package/dist/wire/frames.js +156 -0
- package/dist/wire/index.d.ts +31 -0
- package/dist/wire/index.js +31 -0
- package/dist/wire/methods.d.ts +1896 -0
- package/dist/wire/methods.js +99 -0
- package/dist/wire/states.d.ts +487 -0
- package/dist/wire/states.js +61 -0
- package/package.json +63 -3
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ScopeRegistry` —— workspaceId → `Scope` 的持有者、依赖计数与空闲回收(architecture §4 / PRD D28)。
|
|
3
|
+
*
|
|
4
|
+
* 一个 Workspace 一个 Scope(PRD D9):同一 Workspace 下的多段对话复用同一个 Scope,
|
|
5
|
+
* 所以这里记的是**依赖计数**而不是「谁在打开它」。计数归零之后不立刻拆,而是起一个
|
|
6
|
+
* 10 分钟的空闲计时;计时到点才真正销毁 —— 销毁就是 `Scope.dispose()`,它会逆序调用各插件的
|
|
7
|
+
* `dispose()`,MCP 插件在那里断开它自持的全部连接,于是「Scope 与 MCP 连接同生共死」
|
|
8
|
+
* 不用第二套代码来表达。
|
|
9
|
+
*
|
|
10
|
+
* 计时器只有这一处(architecture §4 A10):L4 说的「10 分钟无活动」与 D28 说的
|
|
11
|
+
* 「Scope 空闲 10 分钟」是同一个计时器的两个说法,别处不再另起一份。
|
|
12
|
+
*
|
|
13
|
+
* Scope 会被销毁的三种场合(architecture §4):空闲到点、Workspace 被删、配置变更要重连 MCP。
|
|
14
|
+
* 三者都收敛到本文件的 `dispose`,区别只在「谁来叫」。
|
|
15
|
+
*
|
|
16
|
+
* 时间与 Scope 的创建方式都从构造参数进来:测试用假时钟推进十分钟,接线层提供真实的
|
|
17
|
+
* SDK 装配(见本文件末尾的 `createWorkspaceScopeFactory`)。
|
|
18
|
+
*/
|
|
19
|
+
import { allInOne, Scope } from "@ai-zen/agents-sdk";
|
|
20
|
+
/** 空闲回收的缺省时长:10 分钟(PRD D28) */
|
|
21
|
+
export const DEFAULT_SCOPE_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
22
|
+
/**
|
|
23
|
+
* 真实时钟。无状态,可以放心共享;需要可控时间的地方(测试、或将来把时钟挂到根对象上)
|
|
24
|
+
* 通过构造参数换成别的实现。
|
|
25
|
+
*/
|
|
26
|
+
export const systemClock = {
|
|
27
|
+
now: () => Date.now(),
|
|
28
|
+
setTimeout: (handler, ms) => {
|
|
29
|
+
const timer = setTimeout(handler, ms);
|
|
30
|
+
// 空闲回收计时不该单独把进程钉住:真要退出时由根对象按「什么都不欠了」判断
|
|
31
|
+
timer.unref?.();
|
|
32
|
+
return () => clearTimeout(timer);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
/** 一个 Workspace 对应的一条记录 */
|
|
36
|
+
class ScopeEntry {
|
|
37
|
+
workspaceId;
|
|
38
|
+
/**
|
|
39
|
+
* 创建中的 Promise;与 `resolved` 指向同一个 Scope。
|
|
40
|
+
*
|
|
41
|
+
* 在构造函数里就把它起来:`acquire` 登记完条目之后才能有人 await 到它,
|
|
42
|
+
* 而构造函数是同步跑完的,所以不存在「拿到条目但 Promise 还没赋值」的窗口。
|
|
43
|
+
*/
|
|
44
|
+
created;
|
|
45
|
+
/** 创建完成后才有值 */
|
|
46
|
+
resolved;
|
|
47
|
+
/** 依赖计数:当前有多少个调用方正拿着它 */
|
|
48
|
+
refs = 1;
|
|
49
|
+
/** 空闲计时器的取消函数 */
|
|
50
|
+
cancelIdle;
|
|
51
|
+
/** 配置变更时它正被持有:等最后一次 release 再销毁(不在别人用着的时候拆能力) */
|
|
52
|
+
recycleWhenFree = false;
|
|
53
|
+
/** 正在销毁(幂等) */
|
|
54
|
+
disposing;
|
|
55
|
+
constructor(workspaceId, start) {
|
|
56
|
+
this.workspaceId = workspaceId;
|
|
57
|
+
this.created = start(this);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export class ScopeRegistry {
|
|
61
|
+
entries = new Map();
|
|
62
|
+
createScope;
|
|
63
|
+
idleTimeoutMs;
|
|
64
|
+
clock;
|
|
65
|
+
onError;
|
|
66
|
+
allRecycledListeners = new Set();
|
|
67
|
+
constructor(options) {
|
|
68
|
+
this.createScope = options.createScope;
|
|
69
|
+
this.idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_SCOPE_IDLE_TIMEOUT_MS;
|
|
70
|
+
this.clock = options.clock ?? systemClock;
|
|
71
|
+
this.onError = options.onError;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 借一个 Scope:没有就懒创建,依赖计数 +1,并取消空闲计时。
|
|
75
|
+
*
|
|
76
|
+
* 并发的两次 `acquire` 只会创建一次(两个调用方等同一个 Promise)。
|
|
77
|
+
*/
|
|
78
|
+
acquire(workspaceId) {
|
|
79
|
+
const existing = this.entries.get(workspaceId);
|
|
80
|
+
if (existing) {
|
|
81
|
+
existing.refs += 1;
|
|
82
|
+
this.cancelIdle(existing);
|
|
83
|
+
return existing.created;
|
|
84
|
+
}
|
|
85
|
+
// 先登记再创建:创建过程中再来的 acquire 会命中这条记录、等同一个 Promise
|
|
86
|
+
const entry = new ScopeEntry(workspaceId, (created) => this.create(created));
|
|
87
|
+
this.entries.set(workspaceId, entry);
|
|
88
|
+
return entry.created;
|
|
89
|
+
}
|
|
90
|
+
/** 创建失败时不留下坏条目:下一次 acquire 重新尝试 */
|
|
91
|
+
async create(entry) {
|
|
92
|
+
try {
|
|
93
|
+
const scope = await this.createScope(entry.workspaceId);
|
|
94
|
+
entry.resolved = scope;
|
|
95
|
+
return scope;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (this.entries.get(entry.workspaceId) === entry) {
|
|
99
|
+
this.entries.delete(entry.workspaceId);
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 归还:依赖计数 −1。归零后按两种场合分叉 ——
|
|
106
|
+
* 平时起 10 分钟空闲计时;若期间发生过配置变更(`recycleAll`)要求它重建,就立刻销毁。
|
|
107
|
+
*/
|
|
108
|
+
release(workspaceId) {
|
|
109
|
+
const entry = this.entries.get(workspaceId);
|
|
110
|
+
if (!entry)
|
|
111
|
+
return;
|
|
112
|
+
entry.refs = Math.max(0, entry.refs - 1);
|
|
113
|
+
if (entry.refs > 0)
|
|
114
|
+
return;
|
|
115
|
+
if (entry.recycleWhenFree) {
|
|
116
|
+
void this.dispose(entry);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this.scheduleIdle(entry);
|
|
120
|
+
}
|
|
121
|
+
/** 看而不借:已经建好就返回它,没有或还在建就返回 `undefined` */
|
|
122
|
+
peek(workspaceId) {
|
|
123
|
+
return this.entries.get(workspaceId)?.resolved;
|
|
124
|
+
}
|
|
125
|
+
/** 当前持有几个 Scope(含正在创建的) */
|
|
126
|
+
size() {
|
|
127
|
+
return this.entries.size;
|
|
128
|
+
}
|
|
129
|
+
/** 当前持有哪几个(配置改动后要报告「回收了哪些项目的 Scope」) */
|
|
130
|
+
workspaceIds() {
|
|
131
|
+
return [...this.entries.keys()];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 全部回收 —— 配置改完要重连 MCP 时用(PRD F-E2:改完即生效,靠重连而不是重启)。
|
|
135
|
+
*
|
|
136
|
+
* 没人用的立刻销毁;正被持有的打上标记,等最后一次 `release` 再销毁。这样正在跑的那一轮
|
|
137
|
+
* 不会在写到一半时被抽掉能力,而它结束后下一段对话拿到的已经是按新配置建的 Scope。
|
|
138
|
+
*/
|
|
139
|
+
async recycleAll() {
|
|
140
|
+
const entries = [...this.entries.values()];
|
|
141
|
+
await Promise.all(entries.map((entry) => {
|
|
142
|
+
if (entry.refs > 0) {
|
|
143
|
+
entry.recycleWhenFree = true;
|
|
144
|
+
return Promise.resolve();
|
|
145
|
+
}
|
|
146
|
+
return this.dispose(entry);
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 销毁某一个 Workspace 的 Scope —— Workspace 被删时用。
|
|
151
|
+
*
|
|
152
|
+
* 与 `recycleAll` 的区别:这里不等引用归零,因为 Workspace 已经不在了,
|
|
153
|
+
* 再留着这个 Scope 也没有下一个调用方。
|
|
154
|
+
*/
|
|
155
|
+
disposeWorkspace(workspaceId) {
|
|
156
|
+
const entry = this.entries.get(workspaceId);
|
|
157
|
+
if (!entry)
|
|
158
|
+
return Promise.resolve();
|
|
159
|
+
return this.dispose(entry);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* 全部回收之后通知根对象(architecture §4:根对象据此判断进程能不能退出)。
|
|
163
|
+
*
|
|
164
|
+
* 返回取消订阅的函数。
|
|
165
|
+
*/
|
|
166
|
+
onAllRecycled(listener) {
|
|
167
|
+
this.allRecycledListeners.add(listener);
|
|
168
|
+
return () => {
|
|
169
|
+
this.allRecycledListeners.delete(listener);
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
scheduleIdle(entry) {
|
|
173
|
+
this.cancelIdle(entry);
|
|
174
|
+
entry.cancelIdle = this.clock.setTimeout(() => {
|
|
175
|
+
entry.cancelIdle = undefined;
|
|
176
|
+
void this.dispose(entry);
|
|
177
|
+
}, this.idleTimeoutMs);
|
|
178
|
+
}
|
|
179
|
+
cancelIdle(entry) {
|
|
180
|
+
entry.cancelIdle?.();
|
|
181
|
+
entry.cancelIdle = undefined;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* 销毁:先把条目从表里摘掉(之后 acquire 会重建),再等创建完成并 `dispose()`。
|
|
185
|
+
*
|
|
186
|
+
* 创建还没完成就得销毁时也要等它 —— 否则刚建出来的 Scope 没人拆,MCP 连接会漏。
|
|
187
|
+
*/
|
|
188
|
+
dispose(entry) {
|
|
189
|
+
entry.disposing ??= this.doDispose(entry);
|
|
190
|
+
return entry.disposing;
|
|
191
|
+
}
|
|
192
|
+
async doDispose(entry) {
|
|
193
|
+
this.cancelIdle(entry);
|
|
194
|
+
if (this.entries.get(entry.workspaceId) === entry) {
|
|
195
|
+
this.entries.delete(entry.workspaceId);
|
|
196
|
+
}
|
|
197
|
+
let scope;
|
|
198
|
+
try {
|
|
199
|
+
scope = await entry.created;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// 创建本来就失败了:没有东西要拆
|
|
203
|
+
scope = undefined;
|
|
204
|
+
}
|
|
205
|
+
if (scope) {
|
|
206
|
+
try {
|
|
207
|
+
// Scope.dispose() 逆序调用各插件的 dispose():MCP 连接在这里断开(D28 同生共死)
|
|
208
|
+
await scope.dispose();
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
this.onError?.(error, entry.workspaceId);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
this.notifyAllRecycled();
|
|
215
|
+
}
|
|
216
|
+
notifyAllRecycled() {
|
|
217
|
+
if (this.entries.size > 0)
|
|
218
|
+
return;
|
|
219
|
+
for (const listener of [...this.allRecycledListeners]) {
|
|
220
|
+
try {
|
|
221
|
+
listener();
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// 通知是「可以判退出了」这种事的广播,一个监听器出问题不该影响别的
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** 取一组目录(固定值与函数的两种写法收在一个地方) */
|
|
230
|
+
function pathsFor(option, cwd) {
|
|
231
|
+
if (option === undefined)
|
|
232
|
+
return undefined;
|
|
233
|
+
return typeof option === "function" ? option(cwd) : option;
|
|
234
|
+
}
|
|
235
|
+
/** 造一个「按 SDK 装能力」的工厂;返回值直接喂给 `ScopeRegistry` 的 `createScope` */
|
|
236
|
+
export function createWorkspaceScopeFactory(options) {
|
|
237
|
+
return async (workspaceId) => {
|
|
238
|
+
const workspace = await options.resolveWorkspace(workspaceId);
|
|
239
|
+
if (!workspace) {
|
|
240
|
+
throw new Error(`Workspace "${workspaceId}" not found`);
|
|
241
|
+
}
|
|
242
|
+
const config = await options.getConfig();
|
|
243
|
+
const abilityPaths = {
|
|
244
|
+
skillsPaths: pathsFor(options.skillsPaths, workspace.cwd),
|
|
245
|
+
toolsPaths: pathsFor(options.toolsPaths, workspace.cwd),
|
|
246
|
+
subAgentsPaths: pathsFor(options.subAgentsPaths, workspace.cwd),
|
|
247
|
+
mcpPaths: options.mcpPaths?.(workspace.cwd),
|
|
248
|
+
};
|
|
249
|
+
const scope = new Scope({
|
|
250
|
+
config,
|
|
251
|
+
cwd: workspace.cwd,
|
|
252
|
+
agentsDir: options.agentsDir,
|
|
253
|
+
deps: options.deps,
|
|
254
|
+
});
|
|
255
|
+
scope.use(...allInOne(abilityPaths));
|
|
256
|
+
await scope.init();
|
|
257
|
+
return scope;
|
|
258
|
+
};
|
|
259
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 临时对话(PRD D3 / F-A2 / R1 / R49):一次请求跑一轮,零痕迹。
|
|
3
|
+
*
|
|
4
|
+
* 五件「没有」就是它的定义:**没有 id、不落库、不进对话列表、不可 attach、不推流式事件**。
|
|
5
|
+
* 所以本文件的结构是刻意做窄的 —— 它没有库、没有事件出口、没有轮次注册表,只有「借一个
|
|
6
|
+
* Scope、跑一轮、把结果一次交回去」。做不到的事情(落库、推流)在这里不是被判断掉,而是
|
|
7
|
+
* 根本没有可用的通道。
|
|
8
|
+
*
|
|
9
|
+
* ## 为什么单独一个文件而不是给 `ChatRunner` 加开关
|
|
10
|
+
*
|
|
11
|
+
* `ChatRunner` 的骨架是「落库 + 推流 + 边流边写」,这三件事对临时对话全是多余的;
|
|
12
|
+
* 加开关会让每一条写库路径后面都跟一个 `if`,而临时对话在语义上又确实是一次「跑一轮」。
|
|
13
|
+
* 于是执行逻辑复用 core 的同一套(`agent.send()` 跑完整组内循环),落库与推送这一层换掉。
|
|
14
|
+
*
|
|
15
|
+
* ## Scope 的借用口径
|
|
16
|
+
*
|
|
17
|
+
* D3 的原话是「只借 Scope,与 Workspace 无关」:临时对话没有 Workspace 身份,它只认一个
|
|
18
|
+
* `cwd`(相对路径的解析基准)。所以这里不用 `workspaceId` 去借,而是用
|
|
19
|
+
* `transientScopeKey(cwd)` 这个**按目录**的键(见 {@link transientScopeKey}):
|
|
20
|
+
*
|
|
21
|
+
* - 同一个目录连着跑两次临时对话,会命中同一个 Scope(借还计数加一减一);
|
|
22
|
+
* - 键里带着 cwd,于是 Scope 的创建方(接线层)能直接从键里解析出目录来装配能力;
|
|
23
|
+
* - 回收仍然交给持有那份空闲计时的 `ScopeRegistry`(A10:计时器只有一处),
|
|
24
|
+
* 本文件不自建缓存、也不自建计时器。
|
|
25
|
+
*/
|
|
26
|
+
import type { AgentNS } from "@ai-zen/agents-core";
|
|
27
|
+
import type { ConfigManager, Scope } from "@ai-zen/agents-sdk";
|
|
28
|
+
import type { Language } from "../i18n/index.js";
|
|
29
|
+
import type { AgentFactory, ChatAgent } from "./ChatRunner.js";
|
|
30
|
+
import type { ConversationTransientParams, ConversationTransientResult } from "../wire/index.js";
|
|
31
|
+
/**
|
|
32
|
+
* 临时对话借 Scope 用的键:按目录,不按 Workspace。
|
|
33
|
+
*
|
|
34
|
+
* 前缀不可省:接线层拿到的键要能区分「这是某个 Workspace」还是「这是某个目录的临时借用」,
|
|
35
|
+
* 否则一个名叫 `transient:/x` 的 Workspace id 就能和临时借用撞在一起。
|
|
36
|
+
*/
|
|
37
|
+
export declare function transientScopeKey(cwd: string): string;
|
|
38
|
+
/** 从借用键里取回目录;不是临时键时返回 `null`(接线层的 Scope 工厂用它分流) */
|
|
39
|
+
export declare function cwdOfTransientScopeKey(key: string): string | null;
|
|
40
|
+
/** 借 / 还 Scope 的窄接口(结构上 `ScopeRegistry` 满足) */
|
|
41
|
+
export interface TransientScopeBorrower {
|
|
42
|
+
acquire(key: string): Promise<Scope>;
|
|
43
|
+
release(key: string): void;
|
|
44
|
+
}
|
|
45
|
+
/** 装配临时对话用的 agent:比 `AgentFactory` 少两个身份字段(它没有对话、没有 Workspace) */
|
|
46
|
+
export type TransientAgentFactory = (context: {
|
|
47
|
+
scope: Scope;
|
|
48
|
+
agentId: string;
|
|
49
|
+
modelId: string | null;
|
|
50
|
+
/** 这一轮开始前的历史;临时对话永远是空数组 */
|
|
51
|
+
history: AgentNS.Message[];
|
|
52
|
+
}) => Promise<ChatAgent>;
|
|
53
|
+
/**
|
|
54
|
+
* 把通用的 `AgentFactory` 适配成临时对话用的那个形状。
|
|
55
|
+
*
|
|
56
|
+
* `conversationId` / `workspaceId` 填一个显式占位:工厂里可能把它们写进钩子载荷(日志、
|
|
57
|
+
* 交接服务的决策),填 `""` 会让日志里出现一个空身份,填占位符至少看得出来这是临时对话。
|
|
58
|
+
* `thinkingLevel` 恒为 `null`:临时对话没有会话行,也就没有会话级档位,跟助手定义上的那份。
|
|
59
|
+
*/
|
|
60
|
+
export declare function transientAgentFactoryFrom(createAgent: AgentFactory): TransientAgentFactory;
|
|
61
|
+
export interface TransientRunnerDeps {
|
|
62
|
+
/** 借 Scope 的入口(接的是持有空闲计时的那一个,A10) */
|
|
63
|
+
scopes: TransientScopeBorrower;
|
|
64
|
+
createAgent: TransientAgentFactory;
|
|
65
|
+
/** 配置唯一入口:没点名助手时用它读默认助手(`AppConfig.defaultAgent`) */
|
|
66
|
+
configManager: ConfigManager;
|
|
67
|
+
now?: () => number;
|
|
68
|
+
}
|
|
69
|
+
/** 一次临时对话的结果里,`problem` 是「跑完了但这轮不算成功」时的那份说明 */
|
|
70
|
+
export declare class TransientRunner {
|
|
71
|
+
private readonly scopes;
|
|
72
|
+
private readonly createAgent;
|
|
73
|
+
private readonly configManager;
|
|
74
|
+
private readonly now;
|
|
75
|
+
constructor(deps: TransientRunnerDeps);
|
|
76
|
+
/**
|
|
77
|
+
* 跑一轮,把结果一次交回去。
|
|
78
|
+
*
|
|
79
|
+
* `cwd` 必填(契约要求):临时对话的目录基准不能靠进程工作目录猜 —— 宿主是常驻进程,
|
|
80
|
+
* 它的 cwd 由启动方式决定,与调用方想的那个目录没有关系。
|
|
81
|
+
*
|
|
82
|
+
* 失败走 `HostError`(没有会话可回看,说不出「哪一轮失败了」);跑完但结局不好
|
|
83
|
+
* (消息停在 error / aborted)时用 `problem` 说明,`message` 照样带回去。
|
|
84
|
+
*/
|
|
85
|
+
run(params: ConversationTransientParams, language?: Language): Promise<ConversationTransientResult>;
|
|
86
|
+
/** 这一轮用哪个助手:入参点名优先,否则配置里的默认助手(没有就说不清用哪个) */
|
|
87
|
+
private resolveAgentId;
|
|
88
|
+
/** 执行 / 装配抛出的东西收敛成宿主的错误:临时对话没有会话可回看,只能在这一句里说清 */
|
|
89
|
+
private toHostError;
|
|
90
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 临时对话(PRD D3 / F-A2 / R1 / R49):一次请求跑一轮,零痕迹。
|
|
3
|
+
*
|
|
4
|
+
* 五件「没有」就是它的定义:**没有 id、不落库、不进对话列表、不可 attach、不推流式事件**。
|
|
5
|
+
* 所以本文件的结构是刻意做窄的 —— 它没有库、没有事件出口、没有轮次注册表,只有「借一个
|
|
6
|
+
* Scope、跑一轮、把结果一次交回去」。做不到的事情(落库、推流)在这里不是被判断掉,而是
|
|
7
|
+
* 根本没有可用的通道。
|
|
8
|
+
*
|
|
9
|
+
* ## 为什么单独一个文件而不是给 `ChatRunner` 加开关
|
|
10
|
+
*
|
|
11
|
+
* `ChatRunner` 的骨架是「落库 + 推流 + 边流边写」,这三件事对临时对话全是多余的;
|
|
12
|
+
* 加开关会让每一条写库路径后面都跟一个 `if`,而临时对话在语义上又确实是一次「跑一轮」。
|
|
13
|
+
* 于是执行逻辑复用 core 的同一套(`agent.send()` 跑完整组内循环),落库与推送这一层换掉。
|
|
14
|
+
*
|
|
15
|
+
* ## Scope 的借用口径
|
|
16
|
+
*
|
|
17
|
+
* D3 的原话是「只借 Scope,与 Workspace 无关」:临时对话没有 Workspace 身份,它只认一个
|
|
18
|
+
* `cwd`(相对路径的解析基准)。所以这里不用 `workspaceId` 去借,而是用
|
|
19
|
+
* `transientScopeKey(cwd)` 这个**按目录**的键(见 {@link transientScopeKey}):
|
|
20
|
+
*
|
|
21
|
+
* - 同一个目录连着跑两次临时对话,会命中同一个 Scope(借还计数加一减一);
|
|
22
|
+
* - 键里带着 cwd,于是 Scope 的创建方(接线层)能直接从键里解析出目录来装配能力;
|
|
23
|
+
* - 回收仍然交给持有那份空闲计时的 `ScopeRegistry`(A10:计时器只有一处),
|
|
24
|
+
* 本文件不自建缓存、也不自建计时器。
|
|
25
|
+
*/
|
|
26
|
+
import { AgentNS as AgentNSValue, Message } from "@ai-zen/agents-core";
|
|
27
|
+
import { DEFAULT_LANGUAGE, render } from "../i18n/index.js";
|
|
28
|
+
import { injectedContextOf } from "./ChatRunner.js";
|
|
29
|
+
import { HostError } from "../wire/errors.js";
|
|
30
|
+
/**
|
|
31
|
+
* 临时对话借 Scope 用的键:按目录,不按 Workspace。
|
|
32
|
+
*
|
|
33
|
+
* 前缀不可省:接线层拿到的键要能区分「这是某个 Workspace」还是「这是某个目录的临时借用」,
|
|
34
|
+
* 否则一个名叫 `transient:/x` 的 Workspace id 就能和临时借用撞在一起。
|
|
35
|
+
*/
|
|
36
|
+
export function transientScopeKey(cwd) {
|
|
37
|
+
return `transient:${cwd}`;
|
|
38
|
+
}
|
|
39
|
+
/** 从借用键里取回目录;不是临时键时返回 `null`(接线层的 Scope 工厂用它分流) */
|
|
40
|
+
export function cwdOfTransientScopeKey(key) {
|
|
41
|
+
return key.startsWith("transient:") ? key.slice("transient:".length) : null;
|
|
42
|
+
}
|
|
43
|
+
/** 临时对话在装配时用的身份占位(`AgentFactory` 的入参里有这两项,临时对话没有真值) */
|
|
44
|
+
const TRANSIENT_IDENTITY = "conversation.transient";
|
|
45
|
+
/**
|
|
46
|
+
* 把通用的 `AgentFactory` 适配成临时对话用的那个形状。
|
|
47
|
+
*
|
|
48
|
+
* `conversationId` / `workspaceId` 填一个显式占位:工厂里可能把它们写进钩子载荷(日志、
|
|
49
|
+
* 交接服务的决策),填 `""` 会让日志里出现一个空身份,填占位符至少看得出来这是临时对话。
|
|
50
|
+
* `thinkingLevel` 恒为 `null`:临时对话没有会话行,也就没有会话级档位,跟助手定义上的那份。
|
|
51
|
+
*/
|
|
52
|
+
export function transientAgentFactoryFrom(createAgent) {
|
|
53
|
+
return async (context) => await createAgent({
|
|
54
|
+
conversationId: TRANSIENT_IDENTITY,
|
|
55
|
+
workspaceId: TRANSIENT_IDENTITY,
|
|
56
|
+
agentId: context.agentId,
|
|
57
|
+
modelId: context.modelId,
|
|
58
|
+
thinkingLevel: null,
|
|
59
|
+
scope: context.scope,
|
|
60
|
+
history: context.history,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** 一次临时对话的结果里,`problem` 是「跑完了但这轮不算成功」时的那份说明 */
|
|
64
|
+
export class TransientRunner {
|
|
65
|
+
scopes;
|
|
66
|
+
createAgent;
|
|
67
|
+
configManager;
|
|
68
|
+
now;
|
|
69
|
+
constructor(deps) {
|
|
70
|
+
this.scopes = deps.scopes;
|
|
71
|
+
this.createAgent = deps.createAgent;
|
|
72
|
+
this.configManager = deps.configManager;
|
|
73
|
+
this.now = deps.now ?? Date.now;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 跑一轮,把结果一次交回去。
|
|
77
|
+
*
|
|
78
|
+
* `cwd` 必填(契约要求):临时对话的目录基准不能靠进程工作目录猜 —— 宿主是常驻进程,
|
|
79
|
+
* 它的 cwd 由启动方式决定,与调用方想的那个目录没有关系。
|
|
80
|
+
*
|
|
81
|
+
* 失败走 `HostError`(没有会话可回看,说不出「哪一轮失败了」);跑完但结局不好
|
|
82
|
+
* (消息停在 error / aborted)时用 `problem` 说明,`message` 照样带回去。
|
|
83
|
+
*/
|
|
84
|
+
async run(params, language = DEFAULT_LANGUAGE) {
|
|
85
|
+
const cwd = typeof params.cwd === "string" ? params.cwd.trim() : "";
|
|
86
|
+
if (cwd.length === 0) {
|
|
87
|
+
throw new HostError("invalid_params", {
|
|
88
|
+
params: { field: "cwd" },
|
|
89
|
+
detail: { field: "cwd", reason: "empty" },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const agentId = await this.resolveAgentId(params.agentId);
|
|
93
|
+
const scopeKey = transientScopeKey(cwd);
|
|
94
|
+
const scope = await this.scopes.acquire(scopeKey);
|
|
95
|
+
try {
|
|
96
|
+
const agent = await this.createAgent({
|
|
97
|
+
scope,
|
|
98
|
+
agentId,
|
|
99
|
+
modelId: params.modelId ?? null,
|
|
100
|
+
history: [],
|
|
101
|
+
});
|
|
102
|
+
// F-D9:临时对话也一样告诉模型「当前目录是哪儿」。它不落库,所以这条 `hidden` 消息
|
|
103
|
+
// 只活在内存里(与那段落库的 cwd 消息同一个形状);目录就是入参里的那个 cwd
|
|
104
|
+
agent.append(new Message({
|
|
105
|
+
role: AgentNSValue.Role.User,
|
|
106
|
+
content: injectedContextOf(cwd, language),
|
|
107
|
+
status: AgentNSValue.MessageStatus.Completed,
|
|
108
|
+
hidden: true,
|
|
109
|
+
}));
|
|
110
|
+
await agent.send(params.input.content);
|
|
111
|
+
const message = lastAssistant(agent.messages);
|
|
112
|
+
if (!message) {
|
|
113
|
+
throw new HostError("internal", {
|
|
114
|
+
messageKey: "error.transientFailed",
|
|
115
|
+
params: { reason: "no assistant message came back" },
|
|
116
|
+
detail: { cwd, agentId },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return { message, subagents: [], problem: problemOf(message, language) };
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
throw this.toHostError(error, cwd, language);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
// 与一轮普通对话一致:跑完就还(agent 本身跑完即弃,D28)
|
|
126
|
+
this.scopes.release(scopeKey);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** 这一轮用哪个助手:入参点名优先,否则配置里的默认助手(没有就说不清用哪个) */
|
|
130
|
+
async resolveAgentId(agentId) {
|
|
131
|
+
if (typeof agentId === "string" && agentId.trim().length > 0)
|
|
132
|
+
return agentId.trim();
|
|
133
|
+
const config = await this.configManager.read();
|
|
134
|
+
const fallback = config.defaultAgent;
|
|
135
|
+
if (!fallback) {
|
|
136
|
+
throw new HostError("config_invalid", {
|
|
137
|
+
detail: { reason: "no_agent", field: "agentId" },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return fallback;
|
|
141
|
+
}
|
|
142
|
+
/** 执行 / 装配抛出的东西收敛成宿主的错误:临时对话没有会话可回看,只能在这一句里说清 */
|
|
143
|
+
toHostError(error, cwd, language) {
|
|
144
|
+
if (HostError.is(error))
|
|
145
|
+
return error;
|
|
146
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
147
|
+
return new HostError("internal", {
|
|
148
|
+
messageKey: "error.transientFailed",
|
|
149
|
+
params: { reason },
|
|
150
|
+
detail: { cwd, reason, message: render(language, "error.transientFailed", { reason }) },
|
|
151
|
+
cause: error,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** 这一轮产出的那条助手消息:取最后一条(工具轮次会有多条,最后一条才是最终答复) */
|
|
156
|
+
function lastAssistant(messages) {
|
|
157
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
158
|
+
const message = messages[index];
|
|
159
|
+
if (message && message.role === AgentNSValue.Role.Assistant)
|
|
160
|
+
return message;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
/** 跑完了但结局不好:消息自己的状态就是事实(core 的口径),这里把它翻成一句说明 */
|
|
165
|
+
function problemOf(message, language) {
|
|
166
|
+
const status = message.status;
|
|
167
|
+
if (status !== AgentNSValue.MessageStatus.Error && status !== AgentNSValue.MessageStatus.Aborted) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const reason = status === AgentNSValue.MessageStatus.Aborted ? "aborted" : "error";
|
|
171
|
+
const params = { reason };
|
|
172
|
+
return {
|
|
173
|
+
code: `transient.${reason}`,
|
|
174
|
+
messageKey: "error.transientFailed",
|
|
175
|
+
params,
|
|
176
|
+
message: render(language, "error.transientFailed", params),
|
|
177
|
+
detail: { status, finishReason: message.finish_reason ?? null },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 运行时的出口:Scope 的持有与回收、轮次占位、一轮的执行者。
|
|
3
|
+
*
|
|
4
|
+
* 这三块是「比 service 更长寿」的那一层(architecture §3):ScopeRegistry 与 RunRegistry
|
|
5
|
+
* 随进程存活,ChatRunner 无状态(每轮现造),三者都由单根注入依赖,不导出任何模块级单例。
|
|
6
|
+
*/
|
|
7
|
+
export { createWorkspaceScopeFactory, DEFAULT_SCOPE_IDLE_TIMEOUT_MS, ScopeRegistry, systemClock, } from "./ScopeRegistry.js";
|
|
8
|
+
export type { Clock, ScopeFactory, ScopeRegistryOptions, WorkspaceScopeFactoryOptions, } from "./ScopeRegistry.js";
|
|
9
|
+
export { RunRegistry } from "./RunRegistry.js";
|
|
10
|
+
export type { RunHandle, RunRegistryOptions, RunSummary, RunsView } from "./RunRegistry.js";
|
|
11
|
+
export { ChatRunner, createSdkAgentFactory, DEFAULT_STREAM_WRITE_INTERVAL_MS, } from "./ChatRunner.js";
|
|
12
|
+
export type { AgentBuildContext, AgentFactory, AttachmentPreparation, AttachmentPreparer, ChatAgent, ChatEventEmitter, ChatRunnerOptions, ChatRunParams, ChatTurn, ConversationStore, HandoffProvider, MessageStore, ReconcileResult, SdkAgentFactoryOptions, TurnSettlement, TurnSettledFacts, } from "./ChatRunner.js";
|
|
13
|
+
export { ConversationDrafts } from "./ConversationDrafts.js";
|
|
14
|
+
export type { ConversationDraftStore } from "./ConversationDrafts.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 运行时的出口:Scope 的持有与回收、轮次占位、一轮的执行者。
|
|
3
|
+
*
|
|
4
|
+
* 这三块是「比 service 更长寿」的那一层(architecture §3):ScopeRegistry 与 RunRegistry
|
|
5
|
+
* 随进程存活,ChatRunner 无状态(每轮现造),三者都由单根注入依赖,不导出任何模块级单例。
|
|
6
|
+
*/
|
|
7
|
+
export { createWorkspaceScopeFactory, DEFAULT_SCOPE_IDLE_TIMEOUT_MS, ScopeRegistry, systemClock, } from "./ScopeRegistry.js";
|
|
8
|
+
export { RunRegistry } from "./RunRegistry.js";
|
|
9
|
+
export { ChatRunner, createSdkAgentFactory, DEFAULT_STREAM_WRITE_INTERVAL_MS, } from "./ChatRunner.js";
|
|
10
|
+
export { ConversationDrafts } from "./ConversationDrafts.js";
|