wave-agent-sdk 1.1.4 → 1.1.5
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/dist/agent.d.ts +2 -0
- package/dist/agent.js +13 -1
- package/dist/builtin/plugins.js +26 -2
- package/dist/managers/aiManager.js +9 -0
- package/dist/utils/container.d.ts +6 -0
- package/dist/utils/container.js +9 -0
- package/dist/utils/containerSetup.d.ts +11 -1
- package/dist/utils/containerSetup.js +21 -5
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export declare class Agent {
|
|
|
31
31
|
private taskManager;
|
|
32
32
|
private foregroundTaskManager;
|
|
33
33
|
private container;
|
|
34
|
+
/** Unregister module-level listeners registered during container setup; invoked at the end of destroy(). */
|
|
35
|
+
private teardown;
|
|
34
36
|
private configurationService;
|
|
35
37
|
private workdir;
|
|
36
38
|
private systemPrompt?;
|
package/dist/agent.js
CHANGED
|
@@ -56,6 +56,8 @@ export class Agent {
|
|
|
56
56
|
this.isAborting = false; // Transient guard: prevents tryDispatch from firing during abortMessage (reset when the abort completes)
|
|
57
57
|
this.isDestroyed = false; // Terminal guard: set in destroy(), never reset — no dispatch may ever start after destroy
|
|
58
58
|
this.dispatchAborted = false; // Set on abort while a dispatch is running: suppress the .finally re-check so preserved notifications don't get dispatched after abort
|
|
59
|
+
/** Unregister module-level listeners registered during container setup; invoked at the end of destroy(). */
|
|
60
|
+
this.teardown = () => { };
|
|
59
61
|
this.sessionStartTime = Date.now();
|
|
60
62
|
const { logger, workdir, systemPrompt, stream = true } = options;
|
|
61
63
|
// Set working directory early as we need it for loading configuration
|
|
@@ -68,7 +70,7 @@ export class Agent {
|
|
|
68
70
|
this.stream = stream; // Save streaming mode flag
|
|
69
71
|
// Store options for dynamic configuration resolution
|
|
70
72
|
this.options = options;
|
|
71
|
-
|
|
73
|
+
const { container, teardown } = setupAgentContainer({
|
|
72
74
|
options,
|
|
73
75
|
workdir: this.workdir,
|
|
74
76
|
configurationService: this.configurationService,
|
|
@@ -92,6 +94,8 @@ export class Agent {
|
|
|
92
94
|
addPermissionRule: (rule) => this.addPermissionRule(rule),
|
|
93
95
|
addUsage: (usage) => this.messageManager.addUsage(usage),
|
|
94
96
|
});
|
|
97
|
+
this.container = container;
|
|
98
|
+
this.teardown = teardown;
|
|
95
99
|
// Retrieve managers from container
|
|
96
100
|
this.foregroundTaskManager = this.container.get("ForegroundTaskManager");
|
|
97
101
|
this.memoryRuleManager = this.container.get("MemoryRuleManager");
|
|
@@ -717,6 +721,14 @@ export class Agent {
|
|
|
717
721
|
if (!drained) {
|
|
718
722
|
this.logger?.error(`Async work did not drain: ${this.asyncWorkRegistry.size} live work item(s) remain after destroy`);
|
|
719
723
|
}
|
|
724
|
+
// Unregister module-level listeners (remote settings hot-update, auth
|
|
725
|
+
// change) so the agent's object graph becomes collectable. Without this,
|
|
726
|
+
// the module-level callback arrays pin every created agent via the
|
|
727
|
+
// per-agent closure contexts, even after the host drops its references.
|
|
728
|
+
this.teardown();
|
|
729
|
+
// Break the DI container's internal references (services/factories) so no
|
|
730
|
+
// per-agent manager is retained through it after destroy.
|
|
731
|
+
this.container.clear();
|
|
720
732
|
}
|
|
721
733
|
/**
|
|
722
734
|
* Trigger the rewind UI callback
|
package/dist/builtin/plugins.js
CHANGED
|
@@ -41,6 +41,9 @@ const guidance = [
|
|
|
41
41
|
"- 边界模糊时也先写 spec 草稿请用户确认,不要直接改代码。",
|
|
42
42
|
"- 规格编写技能(specify)由 AI 自动触发:对话中涉及新需求或需求变更时主动创建或更新规格文件,不需要用户手动调用(不出现在斜杠命令列表中)。",
|
|
43
43
|
\`- 新增或修改 spec 后运行校验:\${specCount}(自动检测 docs/specs/,否则 specs/,否则退出)。\`,
|
|
44
|
+
"- 阶段衔接一律用 AskUserQuestion 单选让用户点击决策,不要求自然语言:spec 确认(确认通过/需要修改)、是否制作原型、是否制定技术方案(plan)均通过单选推进。",
|
|
45
|
+
"- 可选原型阶段(在规格之后、plan 之前):仅实现前端界面,数据全部使用 mock;可选 plan 阶段:进入 plan 模式制定技术方案,批准后再编码。二者均可跳过——最短流程=规格+编码,最长=规格+原型+plan+编码。仅当需求涉及前端界面时才询问是否制作原型,无前端界面的需求(后端服务、CLI、算法库等)直接跳过原型询问。",
|
|
46
|
+
"- 用 task 工具追踪进度:规格、原型、plan、编码各阶段开始前用 TaskCreate 创建任务并标记进行中(TaskUpdate),完成/批准/确认后标记完成,让用户在任务列表中看到当前所处阶段。",
|
|
44
47
|
].join("\\n");
|
|
45
48
|
|
|
46
49
|
// JSON form → parsed as hookSpecificOutput.additionalContext by the hook manager.
|
|
@@ -128,7 +131,7 @@ if (warnings.length) {
|
|
|
128
131
|
`,
|
|
129
132
|
"plugins/sdd/skills/specify/SKILL.md": `---
|
|
130
133
|
name: specify
|
|
131
|
-
description:
|
|
134
|
+
description: 根据自然语言描述创建或更新功能规格说明,并通过单选衔接可选原型、技术方案(plan)与编码阶段。
|
|
132
135
|
user-invocable: false
|
|
133
136
|
---
|
|
134
137
|
|
|
@@ -144,6 +147,8 @@ $ARGUMENTS
|
|
|
144
147
|
|
|
145
148
|
根据对话中的功能描述,执行以下步骤:
|
|
146
149
|
|
|
150
|
+
0. **创建进度任务**:用 TaskCreate 创建「编写功能规格」任务,并用 TaskUpdate 标记进行中。后续每个阶段(原型、plan、编码)同样在开始前创建任务、结束后更新状态,让用户在任务列表中看到当前进度。
|
|
151
|
+
|
|
147
152
|
1. **确定规格文件路径**:
|
|
148
153
|
- **确定规格根目录**:优先复用项目中已有的规格目录——若 \`docs/specs/\` 存在则用之,否则若 \`specs/\` 存在则用之,否则默认 \`specs/\`(并在完成报告中说明所选目录,便于用户纠正)。
|
|
149
154
|
- **选择分组**:若规格目录下已有分组子目录,沿用其既有分组约定;否则默认扁平结构(直接放在规格根目录下)。
|
|
@@ -165,7 +170,26 @@ $ARGUMENTS
|
|
|
165
170
|
- 将每个标记作为问题展示,附带建议答案
|
|
166
171
|
- 等待用户回复后更新规格文件
|
|
167
172
|
|
|
168
|
-
5.
|
|
173
|
+
5. **校验并确认规格**:
|
|
174
|
+
- 运行会话引导中给出的 spec-count 校验命令(自动检测 docs/specs/,否则 specs/,否则跳过)
|
|
175
|
+
- 输出规格文件路径,并通过 AskUserQuestion 单选请求确认(选项:确认通过 / 需要修改)
|
|
176
|
+
- 选「需要修改」→ 按用户反馈更新规格后重新校验,并再次单选确认;选「确认通过」→ 将「编写功能规格」任务标记完成
|
|
177
|
+
|
|
178
|
+
6. **询问是否制作原型(可选阶段,位于规格之后、plan 之前)**:
|
|
179
|
+
- 仅当需求涉及前端界面时弹出询问;需求不涉及前端界面(如后端服务、CLI 工具、算法库)时,跳过本阶段直接进入下一步,不弹出原型选择
|
|
180
|
+
- 通过 AskUserQuestion 单选询问(选项:制作原型 / 跳过)
|
|
181
|
+
- 选「制作原型」→ 用 TaskCreate 创建「制作原型」任务并标记进行中;仅实现前端界面,数据全部使用 mock(不接后端、不接真实数据);完成后展示可交互原型供用户查看,并将任务标记完成
|
|
182
|
+
- 选「跳过」→ 直接进入下一步
|
|
183
|
+
|
|
184
|
+
7. **询问是否制定技术方案(可选 plan 阶段)**:
|
|
185
|
+
- 通过 AskUserQuestion 单选询问(选项:进入 plan 模式 / 跳过)
|
|
186
|
+
- 选「进入 plan 模式」→ 用 TaskCreate 创建「制定技术方案」任务并标记进行中;调用 EnterPlanMode 进入 plan 模式,制定技术方案(技术选型、架构设计、实现步骤)并写入计划文件;用 ExitPlanMode 请求批准——被拒绝则按反馈更新方案后重新请求,批准后标记任务完成
|
|
187
|
+
- 选「跳过」→ 直接进入下一步
|
|
188
|
+
|
|
189
|
+
8. **编码阶段**:
|
|
190
|
+
- 用 TaskCreate 创建「实现功能」任务并标记进行中
|
|
191
|
+
- 按已确认的规格实现;若制作了原型则遵循其交互设计,若批准了技术方案则遵循其架构
|
|
192
|
+
- 实现完成后将任务标记完成
|
|
169
193
|
|
|
170
194
|
## 指南
|
|
171
195
|
|
|
@@ -1353,6 +1353,9 @@ ${question}`;
|
|
|
1353
1353
|
// Add streaming callbacks only if streaming is enabled
|
|
1354
1354
|
if (this.stream) {
|
|
1355
1355
|
callAgentOptions.onContentUpdate = (content) => {
|
|
1356
|
+
// Agent may have been destroyed mid-stream; ignore in-flight updates.
|
|
1357
|
+
if (!this.messageManager)
|
|
1358
|
+
return;
|
|
1356
1359
|
// Create assistant message on first chunk if not already created
|
|
1357
1360
|
if (!assistantMessageCreated) {
|
|
1358
1361
|
this.messageManager.addAssistantMessage();
|
|
@@ -1361,6 +1364,9 @@ ${question}`;
|
|
|
1361
1364
|
this.messageManager.updateCurrentMessageContent(content);
|
|
1362
1365
|
};
|
|
1363
1366
|
callAgentOptions.onToolUpdate = (toolCall) => {
|
|
1367
|
+
// Agent may have been destroyed mid-stream; ignore in-flight updates.
|
|
1368
|
+
if (!this.messageManager)
|
|
1369
|
+
return;
|
|
1364
1370
|
// Create assistant message on first tool update if not already created
|
|
1365
1371
|
if (!assistantMessageCreated) {
|
|
1366
1372
|
this.messageManager.addAssistantMessage();
|
|
@@ -1383,6 +1389,9 @@ ${question}`;
|
|
|
1383
1389
|
});
|
|
1384
1390
|
};
|
|
1385
1391
|
callAgentOptions.onReasoningUpdate = (reasoning) => {
|
|
1392
|
+
// Agent may have been destroyed mid-stream; ignore in-flight updates.
|
|
1393
|
+
if (!this.messageManager)
|
|
1394
|
+
return;
|
|
1386
1395
|
// Create assistant message on first reasoning update if not already created
|
|
1387
1396
|
if (!assistantMessageCreated) {
|
|
1388
1397
|
this.messageManager.addAssistantMessage();
|
|
@@ -27,4 +27,10 @@ export declare class Container {
|
|
|
27
27
|
* Check if a service exists
|
|
28
28
|
*/
|
|
29
29
|
has(token: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Remove all registered services and factories. Call during teardown so the
|
|
32
|
+
* container does not retain references to per-agent services after the
|
|
33
|
+
* agent is destroyed (breaking the agent's object graph).
|
|
34
|
+
*/
|
|
35
|
+
clear(): void;
|
|
30
36
|
}
|
package/dist/utils/container.js
CHANGED
|
@@ -52,6 +52,15 @@ export class Container {
|
|
|
52
52
|
this.factories.has(token) ||
|
|
53
53
|
(this.parent ? this.parent.has(token) : false));
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Remove all registered services and factories. Call during teardown so the
|
|
57
|
+
* container does not retain references to per-agent services after the
|
|
58
|
+
* agent is destroyed (breaking the agent's object graph).
|
|
59
|
+
*/
|
|
60
|
+
clear() {
|
|
61
|
+
this.services.clear();
|
|
62
|
+
this.factories.clear();
|
|
63
|
+
}
|
|
55
64
|
}
|
|
56
65
|
// Example usage for ToolManager:
|
|
57
66
|
/*
|
|
@@ -16,4 +16,14 @@ export interface AgentContainerSetupOptions {
|
|
|
16
16
|
addPermissionRule: (rule: string) => Promise<void>;
|
|
17
17
|
addUsage: (usage: Usage) => void;
|
|
18
18
|
}
|
|
19
|
-
export
|
|
19
|
+
export interface AgentContainerSetupResult {
|
|
20
|
+
container: Container;
|
|
21
|
+
/**
|
|
22
|
+
* Unregister module-level listeners registered during setup (remote settings
|
|
23
|
+
* hot-update, auth change). Must be called when the agent is destroyed;
|
|
24
|
+
* without it the module-level callback arrays pin the agent's object graph
|
|
25
|
+
* (the callback closures capture per-agent managers).
|
|
26
|
+
*/
|
|
27
|
+
teardown: () => void;
|
|
28
|
+
}
|
|
29
|
+
export declare function setupAgentContainer(setupOptions: AgentContainerSetupOptions): AgentContainerSetupResult;
|
|
@@ -38,6 +38,12 @@ export function setupAgentContainer(setupOptions) {
|
|
|
38
38
|
const container = new Container();
|
|
39
39
|
container.register("AgentOptions", options);
|
|
40
40
|
container.register("Workdir", workdir);
|
|
41
|
+
// Module-level listener teardowns collected during setup and returned to the
|
|
42
|
+
// agent, which invokes them in destroy(). The remote-settings callback
|
|
43
|
+
// strongly captures the per-agent LiveConfigManager: without unsubscribing
|
|
44
|
+
// it in destroy(), the module-level callback array keeps every created
|
|
45
|
+
// agent's object graph alive even after the host drops its references.
|
|
46
|
+
const teardowns = [];
|
|
41
47
|
if (options.worktreeName) {
|
|
42
48
|
container.register("WorktreeName", options.worktreeName);
|
|
43
49
|
container.register("MainRepoRoot", getGitMainRepoRoot(workdir));
|
|
@@ -107,14 +113,14 @@ export function setupAgentContainer(setupOptions) {
|
|
|
107
113
|
});
|
|
108
114
|
container.register("McpManager", mcpManager);
|
|
109
115
|
// Wire up auth change callback to refresh/clear remote settings
|
|
110
|
-
authService.onAuthChange(async (event) => {
|
|
116
|
+
teardowns.push(authService.onAuthChange(async (event) => {
|
|
111
117
|
if (event === "login") {
|
|
112
118
|
await remoteSettingsService.refresh();
|
|
113
119
|
}
|
|
114
120
|
else if (event === "logout") {
|
|
115
121
|
remoteSettingsService.clear();
|
|
116
122
|
}
|
|
117
|
-
});
|
|
123
|
+
}));
|
|
118
124
|
const lspManager = options.lspManager || new LspManager(container);
|
|
119
125
|
container.register("LspManager", lspManager);
|
|
120
126
|
const permissionManager = new PermissionManager(container, {
|
|
@@ -245,9 +251,12 @@ export function setupAgentContainer(setupOptions) {
|
|
|
245
251
|
container.register("LiveConfigManager", liveConfigManager);
|
|
246
252
|
// Wire up remote settings hot-update: when polling detects changed settings,
|
|
247
253
|
// reload configuration so admin changes propagate to the running agent.
|
|
248
|
-
|
|
254
|
+
// The callback strongly captures the per-agent LiveConfigManager, so it MUST
|
|
255
|
+
// be unsubscribed in destroy() via teardown — otherwise the module-level
|
|
256
|
+
// callback array pins the whole agent object graph.
|
|
257
|
+
teardowns.push(remoteSettingsService.onSettingsUpdate(async () => {
|
|
249
258
|
await liveConfigManager.reload();
|
|
250
|
-
});
|
|
259
|
+
}));
|
|
251
260
|
const subagentManager = new SubagentManager(container, {
|
|
252
261
|
workdir,
|
|
253
262
|
callbacks: {
|
|
@@ -287,5 +296,12 @@ export function setupAgentContainer(setupOptions) {
|
|
|
287
296
|
cronManager.start();
|
|
288
297
|
const workflowManager = new WorkflowManager(container);
|
|
289
298
|
container.register("WorkflowManager", workflowManager);
|
|
290
|
-
return
|
|
299
|
+
return {
|
|
300
|
+
container,
|
|
301
|
+
teardown: () => {
|
|
302
|
+
for (const unsubscribe of teardowns) {
|
|
303
|
+
unsubscribe();
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
};
|
|
291
307
|
}
|