@steerable/agent-shell 0.6.61 → 0.6.63
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/goal-store.d.ts +26 -0
- package/dist/goal-store.js +67 -3
- package/dist/host/runtime.js +4 -0
- package/dist/image-attachment.js +40 -9
- package/dist/local-backend/goal-continuation-helper.d.ts +16 -0
- package/dist/local-backend/goal-continuation-helper.js +36 -0
- package/dist/local-backend/loop-pty-monitor.d.ts +51 -0
- package/dist/local-backend/loop-pty-monitor.js +136 -0
- package/dist/local-backend/router.d.ts +22 -0
- package/dist/local-backend/router.js +203 -7
- package/dist/local-backend/skills/10-goal/SKILL.md +20 -40
- package/dist/local-backend/skills/11-loop/SKILL.md +18 -55
- package/dist/local-backend/skills/85-local-exec/SKILL.md +1 -1
- package/dist/local-backend/skills/86-proactive-coding/SKILL.md +1 -1
- package/dist/local-backend/turn-file-selection.d.ts +21 -0
- package/dist/local-backend/turn-file-selection.js +186 -0
- package/dist/local-backend/turn-files.d.ts +17 -3
- package/dist/local-backend/turn-files.js +239 -28
- package/dist/present-files.d.ts +7 -0
- package/dist/present-files.js +18 -3
- package/dist/raster-image.d.ts +8 -0
- package/dist/raster-image.js +152 -0
- package/dist/tool-router.d.ts +10 -1
- package/dist/tool-router.js +111 -9
- package/package.json +6 -4
|
@@ -9,6 +9,7 @@ import { llmService, getSidecarSupervisor, whenSidecarSupervisor } from '../llm/
|
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
import { buildWorldState, getActiveCoreLoopStreamId, streamCoreLoopTurn, } from './coreloop-stream.js';
|
|
11
11
|
import { driveWithAutoContinue, resolveAutoContinueMax, } from './auto-continue-helper.js';
|
|
12
|
+
import { buildActiveGoalContext, buildGoalContinuationPrompt, shouldContinueGoal, } from './goal-continuation-helper.js';
|
|
12
13
|
import { buildAmbientDelegateRoster, buildDelegateDispatchInstruction, buildDelegateRosterHint, buildMentionDelegateRoster, BUILTIN_SUBAGENT_PROFILES, mergeTurnSubagentParam, } from './subagent-profiles.js';
|
|
13
14
|
import { resolveMentionedAgentIds } from './mention-targets.js';
|
|
14
15
|
import { presentToolCall } from '../tool-presentation.js';
|
|
@@ -214,6 +215,10 @@ export class LocalBackendRouter {
|
|
|
214
215
|
this.resolveStore = options.resolveStore ?? (() => options.store);
|
|
215
216
|
this.broadcast = options.broadcast ?? null;
|
|
216
217
|
this.taskService = options.taskService ?? null;
|
|
218
|
+
const goals = this.toolRouter.goals?.();
|
|
219
|
+
goals?.onChange((chatId, goal) => {
|
|
220
|
+
this.broadcast?.('goal-changed', { chatId, goal });
|
|
221
|
+
});
|
|
217
222
|
}
|
|
218
223
|
get store() {
|
|
219
224
|
return this.storeContext.getStore() ?? this.defaultStore;
|
|
@@ -229,6 +234,78 @@ export class LocalBackendRouter {
|
|
|
229
234
|
const { method } = request;
|
|
230
235
|
const url = new URL(request.path, 'http://local.backend');
|
|
231
236
|
const pathname = url.pathname;
|
|
237
|
+
const goalMatch = pathname.match(/^\/api\/v2\/chats\/([^/]+)\/goal$/);
|
|
238
|
+
if (goalMatch) {
|
|
239
|
+
const chatId = goalMatch[1];
|
|
240
|
+
const chat = await this.store.getChat(chatId);
|
|
241
|
+
if (!chat)
|
|
242
|
+
return { status: 404, data: { detail: 'chat not found' } };
|
|
243
|
+
const goals = this.toolRouter.goals();
|
|
244
|
+
if (method === 'GET') {
|
|
245
|
+
const result = await goals.get(chatId);
|
|
246
|
+
return { status: 200, data: { goal: result.goal ?? null } };
|
|
247
|
+
}
|
|
248
|
+
if (method === 'POST') {
|
|
249
|
+
const payload = this.toRecord(request.body);
|
|
250
|
+
const action = String(payload.action || '');
|
|
251
|
+
if (action === 'set') {
|
|
252
|
+
const result = await goals.create(chatId, String(payload.objective || ''));
|
|
253
|
+
if (result.success && result.goal) {
|
|
254
|
+
this.startGoalWake(chatId, result.goal);
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
status: result.success ? 200 : 409,
|
|
258
|
+
data: result.success ? { goal: result.goal } : { detail: result.error, goal: result.goal },
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (action === 'clear') {
|
|
262
|
+
const result = await goals.clear(chatId);
|
|
263
|
+
return { status: 200, data: { goal: result.goal ?? null } };
|
|
264
|
+
}
|
|
265
|
+
const current = (await goals.get(chatId)).goal;
|
|
266
|
+
if (!current)
|
|
267
|
+
return { status: 404, data: { detail: 'goal not found' } };
|
|
268
|
+
const result = await goals.update({
|
|
269
|
+
chatId,
|
|
270
|
+
id: current.id,
|
|
271
|
+
revision: current.revision,
|
|
272
|
+
action,
|
|
273
|
+
actor: 'user',
|
|
274
|
+
objective: typeof payload.objective === 'string' ? payload.objective : undefined,
|
|
275
|
+
});
|
|
276
|
+
if (result.success && result.goal && action === 'resume') {
|
|
277
|
+
this.startGoalWake(chatId, result.goal);
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
status: result.success ? 200 : 400,
|
|
281
|
+
data: result.success ? { goal: result.goal } : { detail: result.error, goal: result.goal },
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return this.notFound('Not found');
|
|
285
|
+
}
|
|
286
|
+
const loopsMatch = pathname.match(/^\/api\/v2\/chats\/([^/]+)\/loops$/);
|
|
287
|
+
if (loopsMatch && method === 'GET') {
|
|
288
|
+
const chatId = loopsMatch[1];
|
|
289
|
+
if (!await this.store.getChat(chatId)) {
|
|
290
|
+
return { status: 404, data: { detail: 'chat not found' } };
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
status: 200,
|
|
294
|
+
data: { loops: this.toolRouter.listMonitoredLoops(chatId) },
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const loopMatch = pathname.match(/^\/api\/v2\/chats\/([^/]+)\/loops\/([^/]+)$/);
|
|
298
|
+
if (loopMatch && method === 'DELETE') {
|
|
299
|
+
const chatId = loopMatch[1];
|
|
300
|
+
if (!await this.store.getChat(chatId)) {
|
|
301
|
+
return { status: 404, data: { detail: 'chat not found' } };
|
|
302
|
+
}
|
|
303
|
+
const success = this.toolRouter.stopMonitoredLoop(chatId, loopMatch[2]);
|
|
304
|
+
return {
|
|
305
|
+
status: success ? 200 : 404,
|
|
306
|
+
data: success ? { success: true } : { detail: 'loop not found' },
|
|
307
|
+
};
|
|
308
|
+
}
|
|
232
309
|
if (method === 'GET' && pathname === '/api/v2/auth/me') {
|
|
233
310
|
const authProvider = getAuthProvider();
|
|
234
311
|
if (authProvider) {
|
|
@@ -1949,6 +2026,18 @@ export class LocalBackendRouter {
|
|
|
1949
2026
|
return packHttp;
|
|
1950
2027
|
return this.fallbackResponse(method, pathname);
|
|
1951
2028
|
}
|
|
2029
|
+
startGoalWake(chatId, goal) {
|
|
2030
|
+
void this.wakeChat(chatId, {
|
|
2031
|
+
trigger: 'goal',
|
|
2032
|
+
message: buildGoalContinuationPrompt(goal),
|
|
2033
|
+
sourceId: goal.id,
|
|
2034
|
+
}).catch((error) => {
|
|
2035
|
+
console.warn('[goal] chat wake failed', {
|
|
2036
|
+
chatId,
|
|
2037
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2038
|
+
});
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
1952
2041
|
/**
|
|
1953
2042
|
* 对未显式实现的路由做"温柔降级":GET 返回空集合/空对象,不让 UI 因 404 抛错跳到
|
|
1954
2043
|
* not-found;其他方法返回 success:true 让前端流程继续。所有兜底都在主进程终端
|
|
@@ -1995,7 +2084,78 @@ export class LocalBackendRouter {
|
|
|
1995
2084
|
* - 结束:标准 SSE `[DONE]` 串。
|
|
1996
2085
|
*/
|
|
1997
2086
|
handleStream(request, emit, options = {}) {
|
|
1998
|
-
return this.storeContext.run(this.resolveStore(request.principal), () =>
|
|
2087
|
+
return this.storeContext.run(this.resolveStore(request.principal), async () => {
|
|
2088
|
+
const result = await this.handleStreamScoped(request, emit, options);
|
|
2089
|
+
await this.continueActiveGoal(request, result, options);
|
|
2090
|
+
return result;
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Start one host-initiated turn in an idle chat, then continue an active
|
|
2095
|
+
* goal if that turn still leaves work to do.
|
|
2096
|
+
*/
|
|
2097
|
+
async wakeChat(chatId, input) {
|
|
2098
|
+
return this.storeContext.run(this.store, async () => {
|
|
2099
|
+
this.broadcast?.('chat-turn-started', {
|
|
2100
|
+
chatId,
|
|
2101
|
+
trigger: input.trigger,
|
|
2102
|
+
sourceId: input.sourceId,
|
|
2103
|
+
});
|
|
2104
|
+
const result = await this.handleStreamScoped({
|
|
2105
|
+
method: 'POST',
|
|
2106
|
+
path: `/api/v2/chats/${encodeURIComponent(chatId)}/send`,
|
|
2107
|
+
body: {
|
|
2108
|
+
message: input.message,
|
|
2109
|
+
internal: true,
|
|
2110
|
+
trigger: input.trigger,
|
|
2111
|
+
sourceId: input.sourceId,
|
|
2112
|
+
},
|
|
2113
|
+
}, () => { });
|
|
2114
|
+
this.broadcast?.('chat-turn-finished', {
|
|
2115
|
+
chatId,
|
|
2116
|
+
trigger: input.trigger,
|
|
2117
|
+
sourceId: input.sourceId,
|
|
2118
|
+
status: result.turn?.completionStatus ?? result.status,
|
|
2119
|
+
});
|
|
2120
|
+
if (result.status === 409)
|
|
2121
|
+
return { started: false, reason: 'busy', status: result.status };
|
|
2122
|
+
if (result.status !== 200)
|
|
2123
|
+
return { started: false, reason: 'failed', status: result.status };
|
|
2124
|
+
await this.continueActiveGoal({
|
|
2125
|
+
method: 'POST',
|
|
2126
|
+
path: `/api/v2/chats/${encodeURIComponent(chatId)}/send`,
|
|
2127
|
+
body: { internal: true, trigger: input.trigger, sourceId: input.sourceId },
|
|
2128
|
+
}, result, {});
|
|
2129
|
+
return { started: true, status: result.status };
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
async continueActiveGoal(request, result, options) {
|
|
2133
|
+
if (!result.turn)
|
|
2134
|
+
return;
|
|
2135
|
+
const pathname = new URL(request.path, 'http://local.backend').pathname;
|
|
2136
|
+
const match = pathname.match(/^\/api\/v2\/chats\/([^/]+)\/(?:send|run|agent)$/);
|
|
2137
|
+
if (!match)
|
|
2138
|
+
return;
|
|
2139
|
+
const chatId = decodeURIComponent(match[1]);
|
|
2140
|
+
const goals = this.toolRouter.goals();
|
|
2141
|
+
const current = (await goals.get(chatId)).goal;
|
|
2142
|
+
if (!current)
|
|
2143
|
+
return;
|
|
2144
|
+
const recorded = await goals.recordTurn(chatId, current.id);
|
|
2145
|
+
if (!recorded)
|
|
2146
|
+
return;
|
|
2147
|
+
if (!shouldContinueGoal({
|
|
2148
|
+
phase: recorded.phase,
|
|
2149
|
+
completionStatus: result.turn.completionStatus,
|
|
2150
|
+
madeProgress: result.turn.madeProgress,
|
|
2151
|
+
aborted: options.signal?.aborted === true,
|
|
2152
|
+
}))
|
|
2153
|
+
return;
|
|
2154
|
+
await this.wakeChat(chatId, {
|
|
2155
|
+
trigger: 'goal',
|
|
2156
|
+
message: buildGoalContinuationPrompt(recorded),
|
|
2157
|
+
sourceId: recorded.id,
|
|
2158
|
+
});
|
|
1999
2159
|
}
|
|
2000
2160
|
async handleStreamScoped(request, emit, options = {}) {
|
|
2001
2161
|
const signal = options.signal;
|
|
@@ -2042,7 +2202,7 @@ export class LocalBackendRouter {
|
|
|
2042
2202
|
emit(this.sse('error', { message: 'chat not found' }));
|
|
2043
2203
|
return { status: 404 };
|
|
2044
2204
|
}
|
|
2045
|
-
return this.withChatWriteLock(chatId, emit, async () => {
|
|
2205
|
+
return this.withChatWriteLock(chatId, emit, async (emit) => {
|
|
2046
2206
|
// W7-1: resume=true(仅 send 路由)续跑 durable record 里被中断的 turn,
|
|
2047
2207
|
// 不追加新用户消息;与 regenerate 互斥(regenerate 有自己的路径参数语义)。
|
|
2048
2208
|
const isResume = !regenerateMatch && payload.resume === true;
|
|
@@ -2183,8 +2343,21 @@ export class LocalBackendRouter {
|
|
|
2183
2343
|
const chatAgents = await this.store.listChatAgents();
|
|
2184
2344
|
const mentionedAgentIds = resolveMentionedAgentIds(payload, userMessageText, chatAgents);
|
|
2185
2345
|
if (!regenerateMatch && !isResume) {
|
|
2186
|
-
const
|
|
2187
|
-
|
|
2346
|
+
const internal = payload.internal === true &&
|
|
2347
|
+
(payload.trigger === 'goal' || payload.trigger === 'loop') &&
|
|
2348
|
+
typeof payload.sourceId === 'string';
|
|
2349
|
+
const userMetadata = {
|
|
2350
|
+
...(mentionedAgentIds.length > 0 ? { mentionedAgentIds } : {}),
|
|
2351
|
+
...(internal
|
|
2352
|
+
? {
|
|
2353
|
+
internal: true,
|
|
2354
|
+
trigger: payload.trigger,
|
|
2355
|
+
sourceId: payload.sourceId,
|
|
2356
|
+
}
|
|
2357
|
+
: {}),
|
|
2358
|
+
};
|
|
2359
|
+
const userMeta = Object.keys(userMetadata).length > 0
|
|
2360
|
+
? JSON.stringify(userMetadata)
|
|
2188
2361
|
: null;
|
|
2189
2362
|
const userMessage = await this.store.addMessage(chatId, 'user', userMessageText, userMeta);
|
|
2190
2363
|
currentUserMessageId = userMessage.id;
|
|
@@ -2497,9 +2670,14 @@ export class LocalBackendRouter {
|
|
|
2497
2670
|
const runtimeEnvironment = this.buildRuntimeEnvironmentContext(chatMode);
|
|
2498
2671
|
// 名录进 realityCheck:两条系统提示拼装路径(技能拼装 / 用户整段覆盖)
|
|
2499
2672
|
// 都会带上它,且位置在末尾——不动技能正文那段 prompt cache 前缀。
|
|
2673
|
+
const activeGoal = (await this.toolRouter.goals().get(chatId)).goal;
|
|
2674
|
+
const goalContext = activeGoal?.phase === 'active'
|
|
2675
|
+
? buildActiveGoalContext(activeGoal.objective)
|
|
2676
|
+
: '';
|
|
2500
2677
|
const realityCheck = runtimeEnvironment +
|
|
2501
2678
|
this.buildToolRealityCheck(turnTools, polluted, chatMode) +
|
|
2502
|
-
buildDelegateRosterHint([...delegates.mention, ...delegates.ambient])
|
|
2679
|
+
buildDelegateRosterHint([...delegates.mention, ...delegates.ambient]) +
|
|
2680
|
+
goalContext;
|
|
2503
2681
|
// 用户显式覆盖(payload.systemPrompt 优先 / settings.systemPrompt 自定义了且非默认值次之)走
|
|
2504
2682
|
// "整段替换"路径,保持旧行为可被外部完全控制;否则交给 skill-based
|
|
2505
2683
|
// SystemPromptBuilder 根据本轮可用工具动态拼装。
|
|
@@ -3069,12 +3247,23 @@ export class LocalBackendRouter {
|
|
|
3069
3247
|
emit(this.sse('error', { code: 'chat_busy', message: '该会话正在另一个进程中运行' }));
|
|
3070
3248
|
return { status: 409 };
|
|
3071
3249
|
}
|
|
3250
|
+
let doneChunk;
|
|
3251
|
+
let result;
|
|
3072
3252
|
try {
|
|
3073
|
-
|
|
3253
|
+
result = await body((chunk) => {
|
|
3254
|
+
if (chunk === 'data: [DONE]\n\n') {
|
|
3255
|
+
doneChunk = chunk;
|
|
3256
|
+
return;
|
|
3257
|
+
}
|
|
3258
|
+
emit(chunk);
|
|
3259
|
+
});
|
|
3074
3260
|
}
|
|
3075
3261
|
finally {
|
|
3076
3262
|
lease.release();
|
|
3077
3263
|
}
|
|
3264
|
+
if (doneChunk)
|
|
3265
|
+
emit(doneChunk);
|
|
3266
|
+
return result;
|
|
3078
3267
|
}
|
|
3079
3268
|
sse(event, data) {
|
|
3080
3269
|
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
@@ -3447,6 +3636,7 @@ export class LocalBackendRouter {
|
|
|
3447
3636
|
sinceMs: turnStartedAtMs,
|
|
3448
3637
|
actions: executedActions,
|
|
3449
3638
|
projectRoot: workspaceRoot,
|
|
3639
|
+
finalText: assistantText,
|
|
3450
3640
|
});
|
|
3451
3641
|
}
|
|
3452
3642
|
catch (turnFilesErr) {
|
|
@@ -3553,7 +3743,13 @@ export class LocalBackendRouter {
|
|
|
3553
3743
|
assistantText,
|
|
3554
3744
|
completionStatus,
|
|
3555
3745
|
});
|
|
3556
|
-
return {
|
|
3746
|
+
return {
|
|
3747
|
+
status: 200,
|
|
3748
|
+
turn: {
|
|
3749
|
+
completionStatus,
|
|
3750
|
+
madeProgress: executedActions.length > 0 || assistantText.length > 0,
|
|
3751
|
+
},
|
|
3752
|
+
};
|
|
3557
3753
|
}
|
|
3558
3754
|
/**
|
|
3559
3755
|
* 回合结束后生成下一轮用户输入建议。只走一次 LLM 判断
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: goal
|
|
3
3
|
displayName: Goal tracking
|
|
4
|
-
description:
|
|
4
|
+
description: 把明确要求长期跟踪的目标交给原生 goal 状态与自动续跑;目标会跨回合持续,直到完成、暂停或真实阻塞。
|
|
5
5
|
priority: 500
|
|
6
6
|
tags: [workflow, planning, goal]
|
|
7
7
|
disable-model-invocation: true
|
|
@@ -9,51 +9,31 @@ disable-model-invocation: true
|
|
|
9
9
|
|
|
10
10
|
# 目标跟踪(Goal)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
用法:`/goal <目标>`。
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
- 目标为空时显示 `Usage: /goal <目标>`。
|
|
15
|
+
- Goal 没有时间、token 或回合上限。以 `30m`、`2h` 等时限开头时,明确说明暂不支持时限,然后去掉时限创建目标。
|
|
16
|
+
- “每隔 / 每天 / every”是重复工作,应使用 `/loop`。
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
## 启动
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- **不做什么**:明确本次不处理的范围,避免越做越大。
|
|
20
|
+
1. 用一句话重述完整目标,并保留用户要求的每个交付物与验收证据。
|
|
21
|
+
2. 恰好调用一次 `create_goal`。不要自行创建 goal 文件;不要用 `todo_write` 代替 goal。
|
|
22
|
+
3. 创建失败时明确说明目标没有启动,不要重试创建。
|
|
23
|
+
4. 创建成功后,本回合立即完成第一个具体工作单元;不能只规划或只创建目标就结束。
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
## 持续执行
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
- 目标跨回合存在。保持原目标完整,不得为了本回合能收尾而缩小范围或降低验收标准。
|
|
28
|
+
- 以当前工作区、命令输出、测试结果与外部状态为准;旧对话只用于定位。
|
|
29
|
+
- 多步骤工作可以用 `todo_write` 显示进度,但 todo 只是可见计划,不是 goal 状态,也不能替代实际工作。
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
## 完成核对
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
- 步骤顺序即执行顺序;有依赖关系的写在后面。
|
|
32
|
-
- 验收标准本身也要有对应条目(例如「跑一次测试确认全绿」),否则收尾时没有证据。
|
|
33
|
+
决定完成前先假定“尚未完成”:
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
- 从目标和引用的文件、方案、issue 中逐条导出要求。
|
|
36
|
+
- 为每项要求找到范围匹配的当前证据;缺失、间接或不确定的证据都视为未完成。
|
|
37
|
+
- 意图、部分进展、旧输出和“没发现问题”不能证明完成。
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
- 某条做完 → 立刻标 `completed`,不要攒到最后一起改。
|
|
38
|
-
- 范围变化(发现某步不必做、或必须新增一步)→ **重写清单**并用一句话说明为什么变,不要偷偷跳过。
|
|
39
|
-
- 清单还有未完成项时,**不允许**用纯文本汇报进度然后停手。
|
|
40
|
-
|
|
41
|
-
## 4. 收尾验收
|
|
42
|
-
|
|
43
|
-
所有条目完成后,按验收标准逐条给结论,每条都要带**真实证据**(工具返回的输出、文件路径、退出码、页面变化),不要只写「已完成」。
|
|
44
|
-
|
|
45
|
-
格式:
|
|
46
|
-
|
|
47
|
-
```
|
|
48
|
-
目标:<一句话>
|
|
49
|
-
- [x] 验收1:<结论> —— 证据:<真实输出摘要 / 文件路径>
|
|
50
|
-
- [x] 验收2:<结论> —— 证据:<...>
|
|
51
|
-
- [ ] 验收3:未达成 —— 原因:<真实原因>,建议:<下一步>
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
有未达成项时**如实列出**并给出下一步,不要为了收尾而宣称成功。
|
|
55
|
-
|
|
56
|
-
## 与其他机制的关系
|
|
57
|
-
|
|
58
|
-
- 目标很大、可以并行或上下文很重 → 子任务用 `delegate_subagent` 委派,跨回合的长任务用 `task_run`(有先后关系用 `dependsOn` 串起来),主线仍然靠本清单跟踪。
|
|
59
|
-
- 用户只想先看方案、暂时不执行 → 让他切到输入框底部的 **Plan 模式**,那里会产出可确认的计划;本技能负责的是「认领目标并做完」。
|
|
39
|
+
只有全部要求都有直接证据且没有剩余工作时,才调用 `update_goal`,action 为 `complete`。用户明确要求暂停时才用 `pause`。同一真实阻塞连续出现至少三个目标回合、且没有任何安全的下一步时,才用 `blocked` 并给出具体原因。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loop
|
|
3
3
|
displayName: Repeat on a schedule
|
|
4
|
-
description:
|
|
4
|
+
description: 按固定间隔重复任务,或监控到终态后自动停止;用原生 loop 工具启动受监控后台 shell,并在每次标记输出时唤醒当前会话。
|
|
5
5
|
priority: 500
|
|
6
6
|
tags: [workflow, automation, loop]
|
|
7
7
|
disable-model-invocation: true
|
|
@@ -9,63 +9,26 @@ disable-model-invocation: true
|
|
|
9
9
|
|
|
10
10
|
# 循环执行(Loop)
|
|
11
11
|
|
|
12
|
-
用法:`/loop [间隔]
|
|
12
|
+
用法:`/loop [间隔] <要重复做的事>`。
|
|
13
13
|
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
14
|
+
- 支持前置或后置间隔,如 `5m 查构建`、`查构建 every 5 minutes`。
|
|
15
|
+
- 单位统一为秒传给工具:`30s` → 30,`5m` → 300,`2h` → 7200。
|
|
16
|
+
- 没写间隔时,根据结果值得再次检查的时间选择一个合理间隔,并明确告知用户。
|
|
17
|
+
- prompt 为空时显示 `Usage: /loop [间隔] <prompt>`。
|
|
18
|
+
- 区分有限监控与长期周期任务。`监控构建直到结束` 有终态;`每小时汇总日志` 没有终态,除非用户另行指定。
|
|
17
19
|
|
|
18
|
-
##
|
|
20
|
+
## 启动
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
1. 用真实工具立即执行 prompt 一次;不要先等待。
|
|
23
|
+
2. 有限监控在首次执行后检查终态。若任务已经成功、失败、取消或到达用户指定的其他终态,报告最终结果,不创建 loop。
|
|
24
|
+
3. 仍需继续时调用 `loop_create`,传入自包含的 `prompt` 与正整数 `intervalSeconds`。有限监控的 prompt 必须写明要检查的对象、当前状态的取得方式和终止条件;长期周期任务不得虚构终止条件。
|
|
25
|
+
4. 告知用户:首轮已经执行、循环间隔、loop id,以及有限监控会在终态自动停止,长期周期任务会持续到调用 `loop_stop` 或宿主退出。
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
| --- | --- |
|
|
24
|
-
| **间隔** | 用户给了就按用户的;没给就自己选一个合理值(秒级任务 10-30s,分钟级 1-5m)并说明理由 |
|
|
25
|
-
| **终止条件** | 「什么情况下算成功、可以停」。用户没说就自己拟一个并告知,例如「状态变成 success 就停」 |
|
|
26
|
-
| **最大次数** | 硬上限,默认不超过 10 轮。到达上限必须停下汇报,不允许自动续命 |
|
|
27
|
+
`loop_create` 会启动独立的受监控后台 shell。每次到点时,shell 输出标记,运行时据此唤醒当前会话并再次执行 prompt。唤醒消息包含 loop id;有限监控确认终态后必须调用 `loop_stop`,再报告最终结果。证据不足时保留 loop,等待下一次唤醒。不要自己在当前回合里 `sleep`,不要用 `task_run` 模拟循环,也不要创建重复 loop。
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
## 查看与停止
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
| 总时长在几分钟内、用户想实时看到每轮结果 | **本轮内循环**(见下方 A) |
|
|
35
|
-
| 单轮间隔长、总时长跨越本次回答、或用户要继续聊别的 | **后台任务循环**(见下方 B) |
|
|
36
|
-
|
|
37
|
-
判断不了时问用户一句:「要我现在盯着(几分钟内出结果),还是放后台跑?」
|
|
38
|
-
|
|
39
|
-
### A. 本轮内循环
|
|
40
|
-
|
|
41
|
-
一轮 = 等待 → 执行 → 汇报变化。等待用 `local_exec_shell`,按真实 shell 选方言:
|
|
42
|
-
|
|
43
|
-
| shell | 等待命令 |
|
|
44
|
-
| --- | --- |
|
|
45
|
-
| PowerShell(Windows 默认) | `Start-Sleep -Seconds 30` |
|
|
46
|
-
| bash / zsh | `sleep 30` |
|
|
47
|
-
|
|
48
|
-
- 等待命令的 `timeout` 参数要大于间隔本身,否则会被判超时掐断。
|
|
49
|
-
- 每轮只汇报**变化**(「第 3 轮:状态仍为 running,日志新增 2 行报错」),不要把同样的内容重复贴一遍。
|
|
50
|
-
- 达成终止条件、或到达最大次数 → 立刻停止并给结论。
|
|
51
|
-
|
|
52
|
-
### B. 后台任务循环(task_run)
|
|
53
|
-
|
|
54
|
-
把**整个循环**写进一次 `task_run` 的 `task` 里——后台任务是独立的推理循环,看不到本对话,指令必须自包含,至少包含:
|
|
55
|
-
|
|
56
|
-
1. 要重复执行的具体动作(含命令、路径、判断依据)
|
|
57
|
-
2. 每轮间隔与等待方式
|
|
58
|
-
3. 终止条件与最大轮数
|
|
59
|
-
4. 结束时要带回什么结论
|
|
60
|
-
|
|
61
|
-
发起后告诉用户 `taskId`,以及三个跟进方式:`task_status` 查进度、`task_result` 取最终结论、`task_send` 中途改范围或加信息;任务面板里可以随时停止。
|
|
62
|
-
|
|
63
|
-
**不要**用 `task_run` 起一个「只睡一觉然后再起一个任务」的链条来模拟定时器——一次任务里跑完所有轮次。
|
|
64
|
-
|
|
65
|
-
## 3. 纪律
|
|
66
|
-
|
|
67
|
-
- **先跑一轮再进入等待**:用户要的是结果,不是先等 5 分钟。
|
|
68
|
-
- **不重复起同一个循环**:新建之前先用 `task_status` 看有没有同目的的任务还在跑;有就复用或先停掉。
|
|
69
|
-
- **连续 2 轮同样失败就停**:把真实报错告诉用户并给建议,不要一直重试刷屏。
|
|
70
|
-
- **每轮都必须真调工具**:没有真实 tool_call 就等于这一轮没跑,不允许凭空写「第 2 轮仍在进行」。
|
|
71
|
-
- 用户说停 → 立刻停止本轮内循环;后台任务则说明用 `task_send` 还是任务面板停止,不要再起新的循环。
|
|
31
|
+
- 用户要求查看时调用 `loop_list`。
|
|
32
|
+
- 用户要求停止时,先用 `loop_list` 找到当前会话中匹配的 loop,再调用 `loop_stop`。
|
|
33
|
+
- 多个 loop 都可能匹配且无法确定时,列出候选并询问用户。
|
|
34
|
+
- 停止后不要再创建替代 loop。
|
|
@@ -27,7 +27,7 @@ match: any
|
|
|
27
27
|
- 命令可能改文件、装包、删数据时(destructive 类操作),**先**简单告诉用户你要执行什么,**再**发 tool_call;不要中间停手等用户确认(用户已经请求了)。
|
|
28
28
|
- `local_exec_shell` 返回包含 `stdout` / `stderr` / `exitCode`。非 0 退出码视为失败:把 stderr 摘要给用户,并建议下一步。
|
|
29
29
|
- 路径不存在 / 权限不足时,把 fs 错误原文告诉用户,**不要**反复重试同一个路径。
|
|
30
|
-
-
|
|
30
|
+
- **交付文件要声明**:用户要的产出(表格、幻灯片、文档、报告、图片、导出包)写好后,在最终回复前调用 `present_files`。每份独立交付文件都写 `purpose: "output"`;需要记录预览或中间文件时分别写 `purpose: "preview"` / `"intermediate"`,它们不会生成交付卡片。脚本、命令生成的文件也一样。最终回复用行内代码写出每份独立交付文件的完整路径。辅助脚本、临时文件、只为自查渲染效果导出的预览图和中间导出副本不要标为 output。
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
- **超时 ≠ 失败,严禁原样重跑启动命令**:返回里出现 `timedOut: true` 时,只是 shell 等待被掐断——如果命令是启动 GUI / 长时间运行的程序(如 Qt 界面软件),该程序**很可能仍在运行**,用户可能正在里面操作。此时**不要**再次执行同一条启动命令(会把程序重复启动多次)。正确做法:先确认进程是否在运行(如 `Get-Process`);确实需要长时间等待时传更大的 `timeout` 参数;启动 GUI 程序时优先用不阻塞的方式(PowerShell `Start-Process "app.exe"`),启动成功即视为完成,不要等它退出。
|
|
@@ -47,5 +47,5 @@ match: any
|
|
|
47
47
|
|
|
48
48
|
- **没跑过的代码不要说"运行结果如下"。** 只有看到 `local_run_snippet` 的真实返回(`exitCode: 0` + `stdout`)才能汇报结果;非 0 退出码就读 `stderr` 改代码重跑(最多迭代 2-3 次,还不行就如实说明卡点)。
|
|
49
49
|
- 脚本里读写用户文件时,路径用用户提供的真实路径或项目内相对路径;不要编造路径。
|
|
50
|
-
-
|
|
50
|
+
- 脚本产生的**交付物**(报表、转换后的文件)要落到用户能找到的地方(项目目录或用户指定路径),最终回复前用 `present_files` 以 `purpose: "output"` 声明,并在回复里用行内代码给出完整路径——scratch 目录是临时区,不是交付位置。
|
|
51
51
|
- 代码保持简单直接:能 20 行解决就不要写 100 行;不要引入与任务无关的"工程化"。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本轮哪些文件升成交付卡片。
|
|
3
|
+
*
|
|
4
|
+
* 证据优先级:生成流程结构化结果 > 最终回复引用 > present_files > 文件扫描。
|
|
5
|
+
* 启发式只纠正没有明确 purpose 的 present_files 和扫描结果;明确标为 output
|
|
6
|
+
* 的同名 PDF 与幻灯片会同时保留。
|
|
7
|
+
*/
|
|
8
|
+
export type ArtifactPurpose = 'output' | 'preview' | 'intermediate';
|
|
9
|
+
export type DeliveryEvidenceSource = 'generation' | 'final-reference' | 'present-files';
|
|
10
|
+
export interface DeliveryEvidence {
|
|
11
|
+
path: string;
|
|
12
|
+
source: DeliveryEvidenceSource;
|
|
13
|
+
/** present_files 的旧调用没有 purpose,此时才允许启发式纠错。 */
|
|
14
|
+
purpose?: ArtifactPurpose;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 选出本轮交付卡片的路径。结构化证据不会被文件名启发式覆盖。
|
|
18
|
+
*/
|
|
19
|
+
export declare function selectDeliverablePaths(files: readonly {
|
|
20
|
+
path: string;
|
|
21
|
+
}[], evidence: readonly DeliveryEvidence[]): Set<string>;
|