@steerable/agent-shell 0.6.50 → 0.6.52
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/brand.d.ts +5 -0
- package/dist/goal-store.d.ts +43 -0
- package/dist/goal-store.js +155 -0
- package/dist/host/runtime.js +13 -3
- package/dist/host-tools-runtime.d.ts +6 -0
- package/dist/host-tools-runtime.js +36 -0
- package/dist/host-tools.d.ts +1 -1
- package/dist/host-tools.js +6 -0
- package/dist/local-backend/router.js +22 -17
- package/dist/local-backend/subagent-profiles.js +4 -0
- package/dist/local-backend/task-service.d.ts +6 -0
- package/dist/local-backend/task-service.js +47 -2
- package/dist/local-executor.d.ts +14 -2
- package/dist/local-executor.js +53 -18
- package/dist/product-config.d.ts +10 -0
- package/dist/project-home.d.ts +1 -1
- package/dist/project-home.js +1 -1
- package/dist/project-registry.d.ts +4 -4
- package/dist/project-registry.js +2 -2
- package/dist/sidecar/boot.d.ts +5 -2
- package/dist/sidecar/boot.js +1 -0
- package/dist/sidecar/reverse-approval.d.ts +2 -0
- package/dist/sidecar/reverse-approval.js +2 -0
- package/dist/sidecar/reverse-tools.d.ts +6 -2
- package/dist/sidecar/reverse-tools.js +9 -1
- package/dist/sidecar/types.d.ts +10 -1
- package/dist/storage/write-lease-error.js +4 -1
- package/dist/tool-presentation.d.ts +23 -0
- package/dist/tool-presentation.js +266 -0
- package/dist/tool-router.d.ts +26 -8
- package/dist/tool-router.js +254 -18
- package/dist/workspace-search.d.ts +35 -0
- package/dist/workspace-search.js +186 -0
- package/package.json +4 -4
package/dist/brand.d.ts
CHANGED
|
@@ -18,6 +18,11 @@ export interface Brand {
|
|
|
18
18
|
agentName: string;
|
|
19
19
|
/** 一句话定位(fallback prompt 用) */
|
|
20
20
|
tagline: string;
|
|
21
|
+
/**
|
|
22
|
+
* 新对话落地页 / 空会话首屏副文案。省略用 shell 默认句;
|
|
23
|
+
* 空字符串表示不显示。只进渲染层,不进系统提示词。
|
|
24
|
+
*/
|
|
25
|
+
homeHint?: string;
|
|
21
26
|
/** 新对话 / 首页未选手动专家时绑定的内置智能体 id */
|
|
22
27
|
defaultAgentId: string;
|
|
23
28
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话目标。每个 chat 一份,写在用户数据目录的 goals.json。
|
|
3
|
+
*
|
|
4
|
+
* 修订号做乐观并发:update_goal 必须带上一次读到的 revision。
|
|
5
|
+
*/
|
|
6
|
+
export type GoalPhase = 'active' | 'paused' | 'blocked' | 'complete';
|
|
7
|
+
export type GoalAction = 'edit' | 'pause' | 'resume' | 'complete' | 'blocked';
|
|
8
|
+
export declare const GOAL_ACTIONS: readonly GoalAction[];
|
|
9
|
+
export interface StoredGoal {
|
|
10
|
+
id: string;
|
|
11
|
+
chatId: string;
|
|
12
|
+
revision: number;
|
|
13
|
+
objective: string;
|
|
14
|
+
phase: GoalPhase;
|
|
15
|
+
blockedReason?: string;
|
|
16
|
+
updatedAt: number;
|
|
17
|
+
}
|
|
18
|
+
export interface GoalToolResult {
|
|
19
|
+
success: boolean;
|
|
20
|
+
goal?: StoredGoal | null;
|
|
21
|
+
error?: string;
|
|
22
|
+
needsFollowup?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare class GoalStore {
|
|
25
|
+
private readonly filePath;
|
|
26
|
+
private pending;
|
|
27
|
+
constructor(filePath: string);
|
|
28
|
+
static default(): GoalStore;
|
|
29
|
+
get(chatId: string): Promise<GoalToolResult>;
|
|
30
|
+
create(chatId: string, objective: string): Promise<GoalToolResult>;
|
|
31
|
+
update(input: {
|
|
32
|
+
chatId: string;
|
|
33
|
+
id: string;
|
|
34
|
+
revision: number;
|
|
35
|
+
action: string;
|
|
36
|
+
objective?: string;
|
|
37
|
+
reason?: string;
|
|
38
|
+
}): Promise<GoalToolResult>;
|
|
39
|
+
private queue;
|
|
40
|
+
private load;
|
|
41
|
+
private read;
|
|
42
|
+
private write;
|
|
43
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话目标。每个 chat 一份,写在用户数据目录的 goals.json。
|
|
3
|
+
*
|
|
4
|
+
* 修订号做乐观并发:update_goal 必须带上一次读到的 revision。
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { getUserDataDir } from './runtime.js';
|
|
10
|
+
export const GOAL_ACTIONS = [
|
|
11
|
+
'edit',
|
|
12
|
+
'pause',
|
|
13
|
+
'resume',
|
|
14
|
+
'complete',
|
|
15
|
+
'blocked',
|
|
16
|
+
];
|
|
17
|
+
export class GoalStore {
|
|
18
|
+
filePath;
|
|
19
|
+
pending = Promise.resolve();
|
|
20
|
+
constructor(filePath) {
|
|
21
|
+
this.filePath = filePath;
|
|
22
|
+
}
|
|
23
|
+
static default() {
|
|
24
|
+
return new GoalStore(path.join(getUserDataDir(), 'goals.json'));
|
|
25
|
+
}
|
|
26
|
+
async get(chatId) {
|
|
27
|
+
const data = await this.load();
|
|
28
|
+
return { success: true, goal: data.goals[chatId] ?? null };
|
|
29
|
+
}
|
|
30
|
+
async create(chatId, objective) {
|
|
31
|
+
const text = objective.trim();
|
|
32
|
+
if (!text)
|
|
33
|
+
return { success: false, error: 'objective 不能为空', needsFollowup: true };
|
|
34
|
+
return this.queue(async () => {
|
|
35
|
+
const data = await this.read();
|
|
36
|
+
const existing = data.goals[chatId];
|
|
37
|
+
if (existing && existing.phase !== 'complete') {
|
|
38
|
+
return {
|
|
39
|
+
success: false,
|
|
40
|
+
error: `本会话已有未完成目标(${existing.id},revision ${existing.revision})。先 update_goal 完成或改写它。`,
|
|
41
|
+
goal: existing,
|
|
42
|
+
needsFollowup: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const goal = {
|
|
46
|
+
id: randomUUID(),
|
|
47
|
+
chatId,
|
|
48
|
+
revision: 1,
|
|
49
|
+
objective: text,
|
|
50
|
+
phase: 'active',
|
|
51
|
+
updatedAt: Date.now(),
|
|
52
|
+
};
|
|
53
|
+
data.goals[chatId] = goal;
|
|
54
|
+
await this.write(data);
|
|
55
|
+
return { success: true, goal };
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async update(input) {
|
|
59
|
+
if (!GOAL_ACTIONS.includes(input.action)) {
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
error: `action 必须是 ${GOAL_ACTIONS.join(' | ')}`,
|
|
63
|
+
needsFollowup: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const action = input.action;
|
|
67
|
+
return this.queue(async () => {
|
|
68
|
+
const data = await this.read();
|
|
69
|
+
const current = data.goals[input.chatId];
|
|
70
|
+
if (!current || current.id !== input.id) {
|
|
71
|
+
return { success: false, error: '目标不存在或 id 不匹配。先 get_goal。', needsFollowup: true };
|
|
72
|
+
}
|
|
73
|
+
if (current.revision !== input.revision) {
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
error: `revision 已变(当前 ${current.revision})。重新 get_goal 后再更新。`,
|
|
77
|
+
goal: current,
|
|
78
|
+
needsFollowup: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const next = applyAction(current, action, input.objective, input.reason);
|
|
82
|
+
if ('error' in next)
|
|
83
|
+
return { success: false, error: next.error, goal: current, needsFollowup: true };
|
|
84
|
+
data.goals[input.chatId] = next;
|
|
85
|
+
await this.write(data);
|
|
86
|
+
return { success: true, goal: next };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
queue(fn) {
|
|
90
|
+
const run = this.pending.then(fn, fn);
|
|
91
|
+
this.pending = run.then(() => undefined, () => undefined);
|
|
92
|
+
return run;
|
|
93
|
+
}
|
|
94
|
+
async load() {
|
|
95
|
+
return this.queue(() => this.read());
|
|
96
|
+
}
|
|
97
|
+
async read() {
|
|
98
|
+
try {
|
|
99
|
+
const raw = await readFile(this.filePath, 'utf8');
|
|
100
|
+
const parsed = JSON.parse(raw);
|
|
101
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.goals)
|
|
102
|
+
return { goals: {} };
|
|
103
|
+
return parsed;
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const code = err.code;
|
|
107
|
+
if (code === 'ENOENT')
|
|
108
|
+
return { goals: {} };
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async write(data) {
|
|
113
|
+
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
114
|
+
await writeFile(this.filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function applyAction(current, action, objective, reason) {
|
|
118
|
+
const next = { ...current, revision: current.revision + 1, updatedAt: Date.now() };
|
|
119
|
+
if (action === 'edit') {
|
|
120
|
+
const text = objective?.trim() ?? '';
|
|
121
|
+
if (!text)
|
|
122
|
+
return { error: 'edit 需要 objective' };
|
|
123
|
+
next.objective = text;
|
|
124
|
+
return next;
|
|
125
|
+
}
|
|
126
|
+
if (action === 'pause') {
|
|
127
|
+
if (current.phase !== 'active')
|
|
128
|
+
return { error: `只有 active 目标可以暂停(当前 ${current.phase})` };
|
|
129
|
+
next.phase = 'paused';
|
|
130
|
+
return next;
|
|
131
|
+
}
|
|
132
|
+
if (action === 'resume') {
|
|
133
|
+
if (current.phase !== 'paused' && current.phase !== 'blocked') {
|
|
134
|
+
return { error: `只有 paused 或 blocked 目标可以恢复(当前 ${current.phase})` };
|
|
135
|
+
}
|
|
136
|
+
next.phase = 'active';
|
|
137
|
+
delete next.blockedReason;
|
|
138
|
+
return next;
|
|
139
|
+
}
|
|
140
|
+
if (action === 'complete') {
|
|
141
|
+
if (current.phase === 'complete')
|
|
142
|
+
return { error: '目标已经完成' };
|
|
143
|
+
next.phase = 'complete';
|
|
144
|
+
return next;
|
|
145
|
+
}
|
|
146
|
+
const text = reason?.trim() ?? '';
|
|
147
|
+
if (!text)
|
|
148
|
+
return { error: 'blocked 需要 reason' };
|
|
149
|
+
if (current.phase !== 'active' && current.phase !== 'paused') {
|
|
150
|
+
return { error: `当前阶段 ${current.phase} 不能标为 blocked` };
|
|
151
|
+
}
|
|
152
|
+
next.phase = 'blocked';
|
|
153
|
+
next.blockedReason = text;
|
|
154
|
+
return next;
|
|
155
|
+
}
|
package/dist/host/runtime.js
CHANGED
|
@@ -120,7 +120,13 @@ export async function createHostRuntime(options) {
|
|
|
120
120
|
if (!projectId)
|
|
121
121
|
return null;
|
|
122
122
|
const project = projectRegistry.get(projectId);
|
|
123
|
-
return project
|
|
123
|
+
return project
|
|
124
|
+
? {
|
|
125
|
+
name: project.name,
|
|
126
|
+
folderPath: project.folderPath,
|
|
127
|
+
sourceFolders: project.sourceFolders ?? [],
|
|
128
|
+
}
|
|
129
|
+
: null;
|
|
124
130
|
};
|
|
125
131
|
// 场景包装配(2.3):产品组装根(products/<id>/active.ts)在 import 期
|
|
126
132
|
// 把包装配函数注册进 pack-assembly 注册表;这里统一装配。宿主不 import
|
|
@@ -214,8 +220,8 @@ export async function createHostRuntime(options) {
|
|
|
214
220
|
store: defaultStore,
|
|
215
221
|
toolRouter,
|
|
216
222
|
resolveProjectRoot: async (chatId) => localBackendRouter.resolveChatWorkspaceRoot(chatId),
|
|
217
|
-
//
|
|
218
|
-
//
|
|
223
|
+
// 读:项目家目录之外,附件目录和源文件夹(含各自子目录)也可读。
|
|
224
|
+
// 写:源文件夹及其子目录与家目录同一档;附件目录保持只读。
|
|
219
225
|
resolveAdditionalReadRoots: async (chatId) => {
|
|
220
226
|
const project = await localBackendRouter.resolveChatProject(chatId);
|
|
221
227
|
return [
|
|
@@ -223,6 +229,10 @@ export async function createHostRuntime(options) {
|
|
|
223
229
|
...(project?.sourceFolders ?? []),
|
|
224
230
|
];
|
|
225
231
|
},
|
|
232
|
+
resolveAdditionalWriteRoots: async (chatId) => {
|
|
233
|
+
const project = await localBackendRouter.resolveChatProject(chatId);
|
|
234
|
+
return project?.sourceFolders ?? [];
|
|
235
|
+
},
|
|
226
236
|
approvalHandler: approvalBridge.handler,
|
|
227
237
|
askUserHandler: askUserBridge.handler,
|
|
228
238
|
// P2b: resume 时 sidecar 把记录里的读证据推给 LocalExecutor 的
|
|
@@ -5,6 +5,12 @@ export declare function isProductApprovalEnabled(): boolean;
|
|
|
5
5
|
export declare function resolveTurnApproval(storePath?: string): ReturnType<typeof buildHostApproval>;
|
|
6
6
|
export declare function assertHostIpcAllowed(channel: string): void;
|
|
7
7
|
export declare function resolveTurnChatMode(requested: unknown): ChatModeId;
|
|
8
|
+
/** 解析当前产品是否启用多智能体编排六件套(STEERABLE_ORCHESTRATION 环境变量与回合 payload 优先)。 */
|
|
9
|
+
export declare function resolveTurnOrchestration(payload?: Record<string, unknown>): {
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
maxDepth: number;
|
|
12
|
+
maxParallel: number;
|
|
13
|
+
} | undefined;
|
|
8
14
|
export declare function isProductLlmLocked(): boolean;
|
|
9
15
|
/** 设置页开着走已存/出厂默认;关掉则钉死 product.json 的 llm。 */
|
|
10
16
|
export declare function resolveRuntimeLlmSettings(persisted: LlmSettings | null): LlmSettings;
|
|
@@ -32,6 +32,42 @@ export function assertHostIpcAllowed(channel) {
|
|
|
32
32
|
export function resolveTurnChatMode(requested) {
|
|
33
33
|
return clampChatMode(requested, resolveChatModes(getProductConfig().chatModes));
|
|
34
34
|
}
|
|
35
|
+
/** 解析当前产品是否启用多智能体编排六件套(STEERABLE_ORCHESTRATION 环境变量与回合 payload 优先)。 */
|
|
36
|
+
export function resolveTurnOrchestration(payload) {
|
|
37
|
+
if (process.env.STEERABLE_ORCHESTRATION === '0')
|
|
38
|
+
return undefined;
|
|
39
|
+
if (process.env.STEERABLE_ORCHESTRATION === '1') {
|
|
40
|
+
return { enabled: true, maxDepth: 1, maxParallel: 4 };
|
|
41
|
+
}
|
|
42
|
+
// 回合请求体优先(用户在前端设置中打开/关闭,随 metadata/payload 透传)
|
|
43
|
+
if (payload) {
|
|
44
|
+
if (payload.orchestration === false)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (payload.orchestration === true) {
|
|
47
|
+
return { enabled: true, maxDepth: 1, maxParallel: 4 };
|
|
48
|
+
}
|
|
49
|
+
const meta = payload.metadata;
|
|
50
|
+
if (meta && typeof meta === 'object') {
|
|
51
|
+
if (meta.orchestration === false)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (meta.orchestration === true) {
|
|
54
|
+
return { enabled: true, maxDepth: 1, maxParallel: 4 };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const config = getProductConfig().orchestration;
|
|
59
|
+
if (config === true) {
|
|
60
|
+
return { enabled: true, maxDepth: 1, maxParallel: 4 };
|
|
61
|
+
}
|
|
62
|
+
if (typeof config === 'object' && config !== null && config.enabled !== false) {
|
|
63
|
+
return {
|
|
64
|
+
enabled: true,
|
|
65
|
+
maxDepth: typeof config.maxDepth === 'number' ? config.maxDepth : 1,
|
|
66
|
+
maxParallel: typeof config.maxParallel === 'number' ? config.maxParallel : 4,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
35
71
|
export function isProductLlmLocked() {
|
|
36
72
|
return !resolveSettingsChrome(getProductConfig().settings).llm;
|
|
37
73
|
}
|
package/dist/host-tools.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export type ResolvedSettingsChrome = Record<SettingsItemId, boolean>;
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function resolveSettingsChrome(value?: unknown, tools?: ResolvedHostTools): ResolvedSettingsChrome;
|
|
36
36
|
export declare function hasGeneralSettingsChrome(chrome?: ResolvedSettingsChrome): boolean;
|
|
37
|
-
export declare const LOCAL_FS_TOOL_NAMES: readonly ["local_exec_shell", "local_read_file", "view_image", "local_write_file", "local_edit_file", "present_files", "local_open_path", "local_run_snippet", "local_list_scripts", "local_run_script"];
|
|
37
|
+
export declare const LOCAL_FS_TOOL_NAMES: readonly ["local_exec_shell", "local_read_file", "view_image", "local_write_file", "local_edit_file", "present_files", "local_open_path", "local_run_snippet", "local_list_scripts", "local_run_script", "grep", "glob", "pwsh"];
|
|
38
38
|
export declare function resolveHostToolSurface(value: boolean | {
|
|
39
39
|
capability?: boolean;
|
|
40
40
|
chrome?: boolean;
|
package/dist/host-tools.js
CHANGED
|
@@ -84,6 +84,9 @@ export const LOCAL_FS_TOOL_NAMES = [
|
|
|
84
84
|
'local_run_snippet',
|
|
85
85
|
'local_list_scripts',
|
|
86
86
|
'local_run_script',
|
|
87
|
+
'grep',
|
|
88
|
+
'glob',
|
|
89
|
+
'pwsh',
|
|
87
90
|
];
|
|
88
91
|
const FAMILY_TOOL_PREFIX = {
|
|
89
92
|
terminal: [],
|
|
@@ -97,6 +100,9 @@ const FAMILY_TOOL_PREFIX = {
|
|
|
97
100
|
'worktree_create',
|
|
98
101
|
'worktree_list',
|
|
99
102
|
'worktree_remove',
|
|
103
|
+
'job_list',
|
|
104
|
+
'job_output',
|
|
105
|
+
'job_kill',
|
|
100
106
|
],
|
|
101
107
|
mcp: ['mcp_list_tools', 'mcp_tool_exec'],
|
|
102
108
|
web: ['web_search', 'web_fetch'],
|
|
@@ -10,6 +10,7 @@ import { buildWorldState, getActiveCoreLoopStreamId, streamCoreLoopTurn, } from
|
|
|
10
10
|
import { driveWithAutoContinue, resolveAutoContinueMax, } from './auto-continue-helper.js';
|
|
11
11
|
import { buildAmbientDelegateRoster, buildDelegateDispatchInstruction, buildDelegateRosterHint, buildMentionDelegateRoster, BUILTIN_SUBAGENT_PROFILES, mergeTurnSubagentParam, } from './subagent-profiles.js';
|
|
12
12
|
import { resolveMentionedAgentIds } from './mention-targets.js';
|
|
13
|
+
import { presentToolCall } from '../tool-presentation.js';
|
|
13
14
|
import { readSidecarHistoryEntries, timelineFromHistoryEntries, } from './task-process.js';
|
|
14
15
|
import { appendTimelineDelta, freezeTimelineReasoning, sealLastTimelineBlock, syncTimelineTools, } from './turn-timeline.js';
|
|
15
16
|
import { turnDurationMs } from './turn-duration.js';
|
|
@@ -23,7 +24,7 @@ import { setDefaultExecTimeoutMs } from '../local-executor.js';
|
|
|
23
24
|
import { allowEgressForBaseUrl, getActiveEgressBroker, getEgressPosture } from '../sidecar/egress-proxy.js';
|
|
24
25
|
import { buildExecSandbox, parseExecPolicy } from '../sidecar/exec-sandbox.js';
|
|
25
26
|
import { clampExecPolicy } from '../host-tools.js';
|
|
26
|
-
import { getResolvedHostTools, isProductLlmLocked, resolveRuntimeLlmSettings, resolveTurnApproval, resolveTurnChatMode, } from '../host-tools-runtime.js';
|
|
27
|
+
import { getResolvedHostTools, isProductLlmLocked, resolveRuntimeLlmSettings, resolveTurnApproval, resolveTurnChatMode, resolveTurnOrchestration, } from '../host-tools-runtime.js';
|
|
27
28
|
import { SidecarSupervisor } from '../sidecar/index.js';
|
|
28
29
|
import { diagnoseLlmConnection } from './llm-diagnose.js';
|
|
29
30
|
import { brandSkillVars, buildSystemPrompt, buildForcedSkillMessage, conditionsFromTools, } from './prompt-builder.js';
|
|
@@ -2448,17 +2449,17 @@ export class LocalBackendRouter {
|
|
|
2448
2449
|
? chatProject.folderPath
|
|
2449
2450
|
: await this.resolveChatWorkspaceRoot(chatId);
|
|
2450
2451
|
if (chatProject) {
|
|
2452
|
+
const sourceFolderLine = chatProject.sourceFolders.length > 0
|
|
2453
|
+
? `源文件夹及其子目录也可读写:${chatProject.sourceFolders.join('、')}。`
|
|
2454
|
+
: '如确需读写家目录外的目录,可请用户把该目录附加为源文件夹。';
|
|
2451
2455
|
systemPrompt +=
|
|
2452
2456
|
`\n\n【项目模式】当前对话绑定项目「${chatProject.name}」,家目录:${chatProject.folderPath}\n` +
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
`如确需读取项目外的文件,可请用户在项目里附加为源文件夹。\n` +
|
|
2460
|
-
`例外:本会话的用户上传附件目录 ${chatAttachmentsDirPath(chatId)} 也在项目家目录外,` +
|
|
2461
|
-
`但它已作为**只读放行根**开放给 local_read_file(用绝对路径可直接读取,不会被项目围栏拒绝)。` +
|
|
2457
|
+
`文件写入(local_write_file / local_edit_file)和命令写入可以落在家目录及其全部子目录内。` +
|
|
2458
|
+
`${sourceFolderLine}` +
|
|
2459
|
+
`这些范围之外的写入会被拒绝。命令未指定 cwd 时在家目录运行;显式 cwd 必须落在上述可写目录(含子目录)内。` +
|
|
2460
|
+
`相对路径按家目录解析。\n` +
|
|
2461
|
+
`例外:本会话的用户上传附件目录 ${chatAttachmentsDirPath(chatId)} 只读,不在可写范围内,` +
|
|
2462
|
+
`但 local_read_file 可以用绝对路径读取。` +
|
|
2462
2463
|
/* shell-neutral:allow generic document extensions, not product identity */ `用户附到本条消息的文件都放在那里;需要转换(docx / pdf / xlsx / pptx 等)时,` +
|
|
2463
2464
|
`把该绝对路径交给 local_exec_shell 的转换脚本处理即可,**不要**因为它在项目家目录外就拒绝读取或要求用户重新拷贝。`;
|
|
2464
2465
|
// W6-5 + W6-7a:项目级规则文件(AGENTS.md / CLAUDE.md)是不可信输入,
|
|
@@ -2481,7 +2482,7 @@ export class LocalBackendRouter {
|
|
|
2481
2482
|
else {
|
|
2482
2483
|
systemPrompt +=
|
|
2483
2484
|
`\n\n【对话工作区】当前对话没有绑定项目。工作区:${workspaceRoot}\n` +
|
|
2484
|
-
`你的文件写入(local_write_file)和命令执行(local_exec_shell
|
|
2485
|
+
`你的文件写入(local_write_file)和命令执行(local_exec_shell)都被限制在该目录及其子目录内:` +
|
|
2485
2486
|
`写入路径越界会被拒绝;命令默认在工作区下运行,显式指定的 cwd 越界也会被拒绝。` +
|
|
2486
2487
|
`请一律使用工作区内的路径(相对路径按工作区解析)。` +
|
|
2487
2488
|
`用户若要把对话绑到已有项目,可在输入框上方选择项目。`;
|
|
@@ -2940,7 +2941,7 @@ export class LocalBackendRouter {
|
|
|
2940
2941
|
// spec (default.harness.yaml) is the single source of truth for
|
|
2941
2942
|
// maxRounds / maxToolErrors, and an explicit request param overrides it.
|
|
2942
2943
|
// W4-2: per-exec sandbox for shell tool calls, default-on. Writable
|
|
2943
|
-
// roots =
|
|
2944
|
+
// roots = 项目家目录(或无项目时的对话工作区)+ 源文件夹。每个根含其子目录。
|
|
2944
2945
|
// `execPolicy: 'full'`(输入框「完整权限」)关闭这一层,让 mkdir
|
|
2945
2946
|
// Downloads 这类工作区外写入不再被 Seatbelt 拦成 Operation not permitted。
|
|
2946
2947
|
const chatProject = await this.resolveChatProject(chatId);
|
|
@@ -2952,6 +2953,7 @@ export class LocalBackendRouter {
|
|
|
2952
2953
|
// 工作区)。
|
|
2953
2954
|
...collectPackExecWritableRoots(chatId),
|
|
2954
2955
|
workspaceRoot,
|
|
2956
|
+
...(chatProject?.sourceFolders ?? []),
|
|
2955
2957
|
], { policy: clampExecPolicy(parseExecPolicy(payload.execPolicy), getResolvedHostTools()) });
|
|
2956
2958
|
// W4-1: approval algebra, host mode — every tool call is gated by the
|
|
2957
2959
|
// Electron approval UI over the reverse channel. The durable scope
|
|
@@ -2989,6 +2991,7 @@ export class LocalBackendRouter {
|
|
|
2989
2991
|
const turnFileRoots = [
|
|
2990
2992
|
...collectPackExecWritableRoots(chatId),
|
|
2991
2993
|
workspaceRoot,
|
|
2994
|
+
...(chatProject?.sourceFolders ?? []),
|
|
2992
2995
|
];
|
|
2993
2996
|
try {
|
|
2994
2997
|
const coreLoopOptions = {
|
|
@@ -3077,7 +3080,7 @@ export class LocalBackendRouter {
|
|
|
3077
3080
|
...(toolPolicy.mode === 'all' ? {} : { toolPolicy }),
|
|
3078
3081
|
},
|
|
3079
3082
|
execSandbox,
|
|
3080
|
-
approval,
|
|
3083
|
+
approval: approval ? { ...approval, writableRoots: turnFileRoots } : undefined,
|
|
3081
3084
|
// delegate-on-pool 统一:模型的多代理面收敛为单工具
|
|
3082
3085
|
// delegate_subagent(默认开)。内置画像集(explore/research/coder)
|
|
3083
3086
|
// 带工具域/轮次/并发/系统提示,模型按画像 description 选委派
|
|
@@ -3085,10 +3088,10 @@ export class LocalBackendRouter {
|
|
|
3085
3088
|
// 子运行执行,生命周期经 onChildEvent → SSE 进 UI 编排卡片。
|
|
3086
3089
|
subagent,
|
|
3087
3090
|
// 六件套(agent_spawn/send/wait/close/list/interrupt)收缩为
|
|
3088
|
-
// opt-in 高级模式:STEERABLE_ORCHESTRATION
|
|
3089
|
-
// delegate 共享同一 AgentPool(同一 maxParallel 预算)。
|
|
3090
|
-
...(
|
|
3091
|
-
? { orchestration:
|
|
3091
|
+
// opt-in 高级模式:STEERABLE_ORCHESTRATION 环境变量、回合 payload 或 product.json.orchestration 开启;
|
|
3092
|
+
// 开启后与 delegate 共享同一 AgentPool(同一 maxParallel 预算)。
|
|
3093
|
+
...(resolveTurnOrchestration(payload)
|
|
3094
|
+
? { orchestration: resolveTurnOrchestration(payload) }
|
|
3092
3095
|
: {}),
|
|
3093
3096
|
signal,
|
|
3094
3097
|
onText: (delta) => {
|
|
@@ -3110,6 +3113,7 @@ export class LocalBackendRouter {
|
|
|
3110
3113
|
tool: call.tool,
|
|
3111
3114
|
mode: chatMode,
|
|
3112
3115
|
arguments: call.arguments,
|
|
3116
|
+
view: presentToolCall(call.tool, call.arguments),
|
|
3113
3117
|
threw: false,
|
|
3114
3118
|
});
|
|
3115
3119
|
syncTimelineTools(timeline, executedActions);
|
|
@@ -3124,6 +3128,7 @@ export class LocalBackendRouter {
|
|
|
3124
3128
|
tool: action.tool,
|
|
3125
3129
|
mode: chatMode,
|
|
3126
3130
|
arguments: action.arguments,
|
|
3131
|
+
view: presentToolCall(action.tool, action.arguments, action.result),
|
|
3127
3132
|
result: action.result,
|
|
3128
3133
|
success: action.success,
|
|
3129
3134
|
error: action.error,
|
|
@@ -20,8 +20,12 @@ import { loadSkills } from './skill-loader.js';
|
|
|
20
20
|
/** 只读工具域:探索与调研画像共享的「摸不到写操作」基线。 */
|
|
21
21
|
const READ_ONLY_TOOLS = [
|
|
22
22
|
'local_read_file',
|
|
23
|
+
'view_image',
|
|
23
24
|
'local_list_scripts',
|
|
24
25
|
'local_open_path',
|
|
26
|
+
'grep',
|
|
27
|
+
'glob',
|
|
28
|
+
'get_goal',
|
|
25
29
|
'web_search',
|
|
26
30
|
'web_fetch',
|
|
27
31
|
'mcp_list_tools',
|
|
@@ -54,6 +54,7 @@ export interface TaskServiceDeps {
|
|
|
54
54
|
resolveChatProject: (chatId: string) => Promise<{
|
|
55
55
|
name: string;
|
|
56
56
|
folderPath: string;
|
|
57
|
+
sourceFolders?: string[];
|
|
57
58
|
} | null>;
|
|
58
59
|
/**
|
|
59
60
|
* 无项目对话的可写根(Documents/<应用>/conversations/<chatId>)。
|
|
@@ -120,6 +121,11 @@ export declare class TaskService {
|
|
|
120
121
|
private executeTask;
|
|
121
122
|
/** task_status 工具:单任务或本 chat 全部任务的快照。 */
|
|
122
123
|
status(chatId: string, taskId?: string): Promise<Record<string, unknown>>;
|
|
124
|
+
/**
|
|
125
|
+
* job_kill:结束 running 或 blocked 任务。running 先打标记再取消流,
|
|
126
|
+
* 终态回写看到标记后保持 failed,避免流的正常完成把杀死盖掉。
|
|
127
|
+
*/
|
|
128
|
+
kill(chatId: string, taskId: string, reason?: string): Promise<Record<string, unknown>>;
|
|
123
129
|
/** task_result 工具:取终态任务的完整结果。 */
|
|
124
130
|
result(chatId: string, taskId: string): Promise<Record<string, unknown>>;
|
|
125
131
|
/** 喂给模型的任务视图(完整字段,answer 截断防爆上下文)。 */
|
|
@@ -32,6 +32,7 @@ import { buildExecSandbox } from '../sidecar/exec-sandbox.js';
|
|
|
32
32
|
import { resolveTurnApproval } from '../host-tools-runtime.js';
|
|
33
33
|
import { readSidecarHistoryEntries, timelineFromHistoryEntries, } from './task-process.js';
|
|
34
34
|
import { appendTimelineDelta, syncTimelineTools, } from './turn-timeline.js';
|
|
35
|
+
import { presentToolCall } from '../tool-presentation.js';
|
|
35
36
|
/** 任务回合的系统提示词:自包含、汇报结果、不假装执行。 */
|
|
36
37
|
function buildTaskSystemPrompt(input) {
|
|
37
38
|
const lines = [
|
|
@@ -278,7 +279,11 @@ export class TaskService {
|
|
|
278
279
|
(this.deps.resolveChatWorkspaceRoot
|
|
279
280
|
? await this.deps.resolveChatWorkspaceRoot(chatId)
|
|
280
281
|
: null);
|
|
281
|
-
const
|
|
282
|
+
const writableRoots = [
|
|
283
|
+
...(fenceRoot ? [fenceRoot] : []),
|
|
284
|
+
...(project?.sourceFolders ?? []),
|
|
285
|
+
];
|
|
286
|
+
const execSandbox = buildExecSandbox(writableRoots);
|
|
282
287
|
const approval = resolveTurnApproval();
|
|
283
288
|
// depth-1:任务回合没有 task_run(不能再派生任务),其余工具面与
|
|
284
289
|
// 普通回合一致;task_status / task_result 保留(任务可以自查/互查)。
|
|
@@ -332,7 +337,7 @@ export class TaskService {
|
|
|
332
337
|
},
|
|
333
338
|
}),
|
|
334
339
|
execSandbox,
|
|
335
|
-
approval,
|
|
340
|
+
approval: approval ? { ...approval, writableRoots } : undefined,
|
|
336
341
|
// 与主对话同一套内置画像(explore/research/coder)——任务回合的
|
|
337
342
|
// 子代理编排语义不分叉。
|
|
338
343
|
subagent: builtinSubagentParam(),
|
|
@@ -341,6 +346,8 @@ export class TaskService {
|
|
|
341
346
|
askUser: false,
|
|
342
347
|
onStreamId: (streamId) => {
|
|
343
348
|
entry.streamId = streamId;
|
|
349
|
+
if (entry.killed)
|
|
350
|
+
void supervisor.cancelChat(streamId);
|
|
344
351
|
},
|
|
345
352
|
onText: (delta) => {
|
|
346
353
|
answer += delta;
|
|
@@ -356,6 +363,7 @@ export class TaskService {
|
|
|
356
363
|
id: call.id,
|
|
357
364
|
tool: call.tool,
|
|
358
365
|
arguments: call.arguments,
|
|
366
|
+
view: presentToolCall(call.tool, call.arguments),
|
|
359
367
|
threw: false,
|
|
360
368
|
});
|
|
361
369
|
syncTimelineTools(entry.timeline, entry.actions);
|
|
@@ -369,6 +377,7 @@ export class TaskService {
|
|
|
369
377
|
id: action.id,
|
|
370
378
|
tool: action.tool,
|
|
371
379
|
arguments: action.arguments,
|
|
380
|
+
view: presentToolCall(action.tool, action.arguments, action.result),
|
|
372
381
|
result: action.result,
|
|
373
382
|
success: action.success,
|
|
374
383
|
error: action.error,
|
|
@@ -398,6 +407,10 @@ export class TaskService {
|
|
|
398
407
|
traceId = failureTraceId;
|
|
399
408
|
}
|
|
400
409
|
}
|
|
410
|
+
if (entry.killed) {
|
|
411
|
+
status = 'failed';
|
|
412
|
+
error = entry.killReason || 'killed by job_kill';
|
|
413
|
+
}
|
|
401
414
|
await this.store.updateTask(taskId, {
|
|
402
415
|
status,
|
|
403
416
|
answer: answer.trim() || null,
|
|
@@ -430,6 +443,38 @@ export class TaskService {
|
|
|
430
443
|
tasks: tasks.map((t) => this.toModelView(t)),
|
|
431
444
|
};
|
|
432
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* job_kill:结束 running 或 blocked 任务。running 先打标记再取消流,
|
|
448
|
+
* 终态回写看到标记后保持 failed,避免流的正常完成把杀死盖掉。
|
|
449
|
+
*/
|
|
450
|
+
async kill(chatId, taskId, reason) {
|
|
451
|
+
const task = await this.store.getTask(taskId);
|
|
452
|
+
if (!task || task.chatId !== chatId) {
|
|
453
|
+
return { success: false, error: `任务不存在:${taskId}` };
|
|
454
|
+
}
|
|
455
|
+
if (task.status === 'completed' || task.status === 'failed') {
|
|
456
|
+
return { success: false, error: `任务已终态(${task.status}),不能再结束。` };
|
|
457
|
+
}
|
|
458
|
+
const note = reason?.trim() || 'killed by job_kill';
|
|
459
|
+
const entry = this.running.get(taskId);
|
|
460
|
+
if (entry) {
|
|
461
|
+
entry.killed = true;
|
|
462
|
+
entry.killReason = note;
|
|
463
|
+
}
|
|
464
|
+
if (!entry || task.status === 'blocked') {
|
|
465
|
+
await this.store.updateTask(taskId, { status: 'failed', error: note });
|
|
466
|
+
this.deps.broadcast?.('task-updated', { chatId, taskId });
|
|
467
|
+
if (task.status === 'blocked')
|
|
468
|
+
await this.maybeUnblock(chatId);
|
|
469
|
+
return { success: true, job_id: taskId, status: 'failed', killed: true };
|
|
470
|
+
}
|
|
471
|
+
const getSupervisor = this.deps.getSupervisor ?? getSidecarSupervisor;
|
|
472
|
+
const supervisor = getSupervisor();
|
|
473
|
+
if (entry.streamId && supervisor) {
|
|
474
|
+
await supervisor.cancelChat(entry.streamId);
|
|
475
|
+
}
|
|
476
|
+
return { success: true, job_id: taskId, status: 'failed', killed: true };
|
|
477
|
+
}
|
|
433
478
|
/** task_result 工具:取终态任务的完整结果。 */
|
|
434
479
|
async result(chatId, taskId) {
|
|
435
480
|
const task = await this.store.getTask(taskId);
|
package/dist/local-executor.d.ts
CHANGED
|
@@ -159,8 +159,20 @@ export declare function isGuiLaunchCommand(command: string): boolean;
|
|
|
159
159
|
* 字符串/相对路径判断,不碰 fs,方便 ToolRouter 与 LocalExecutor 共用。
|
|
160
160
|
*/
|
|
161
161
|
export declare function isPathWithinRoot(resolvedPath: string, resolvedRoot: string): boolean;
|
|
162
|
+
/** `~` 展开后 resolve。围栏比较前路径与根都要过这一步。 */
|
|
163
|
+
export declare function expandFencePath(inputPath: string): string;
|
|
164
|
+
/**
|
|
165
|
+
* 可写根:项目家目录(或对话工作区)加上额外可写根(源文件夹)。
|
|
166
|
+
* 每个根都包含它自己和全部子目录。空列表表示不设围栏。
|
|
167
|
+
*/
|
|
168
|
+
export declare function writableFenceRoots(projectRoot?: string | null, additionalWriteRoots?: string[] | null): string[];
|
|
162
169
|
/** 越界错误消息(给模型看,引导它把操作收回项目目录内)。 */
|
|
163
170
|
export declare function buildProjectRootViolation(resolvedPath: string, resolvedRoot: string): string;
|
|
171
|
+
/**
|
|
172
|
+
* 路径不在任一可写根(含其子目录)内时返回给模型的错误;在围栏内或未设围栏时返回 null。
|
|
173
|
+
* `resolvedPath` 与 `roots` 都应已 resolve。
|
|
174
|
+
*/
|
|
175
|
+
export declare function writableFenceViolation(resolvedPath: string, roots: string[]): string | null;
|
|
164
176
|
export interface CommandSafetyConfigPayload {
|
|
165
177
|
disabledPatternIds: string[];
|
|
166
178
|
customPatterns: Array<{
|
|
@@ -216,13 +228,13 @@ export declare class LocalExecutor {
|
|
|
216
228
|
};
|
|
217
229
|
executeShell(request: LocalExecRequest): Promise<LocalExecResult>;
|
|
218
230
|
readLocalFile(request: LocalFileReadRequest, projectRoot?: string | null, additionalReadRoots?: string[] | null): Promise<LocalFileReadResult>;
|
|
219
|
-
writeLocalFile(request: LocalFileWriteRequest, projectRoot?: string | null): Promise<LocalFileWriteResult>;
|
|
231
|
+
writeLocalFile(request: LocalFileWriteRequest, projectRoot?: string | null, additionalWriteRoots?: string[] | null): Promise<LocalFileWriteResult>;
|
|
220
232
|
/**
|
|
221
233
|
* local_edit_file 的实现(W6-1):结构化批量编辑。先按 expectedVersion 做
|
|
222
234
|
* read-before-write 冲突检测,再在原文上做三级模糊定位 + 逆序替换,最后
|
|
223
235
|
* 原子落盘。返回新版本令牌与统一 diff。
|
|
224
236
|
*/
|
|
225
|
-
editLocalFile(request: LocalFileEditRequest, projectRoot?: string | null): Promise<LocalFileEditResult>;
|
|
237
|
+
editLocalFile(request: LocalFileEditRequest, projectRoot?: string | null, additionalWriteRoots?: string[] | null): Promise<LocalFileEditResult>;
|
|
226
238
|
/**
|
|
227
239
|
* read-before-write 冲突检测:当前内容版本与调用方期望不一致时返回错误
|
|
228
240
|
* 消息(引导重新读取),一致返回 null。文件不存在且期望存在也算冲突,
|