@steerable/agent-shell 0.6.17 → 0.6.18
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/local-backend/router.js +21 -1
- package/dist/local-executor.d.ts +29 -0
- package/dist/local-executor.js +81 -1
- package/dist/runtime.js +5 -1
- package/dist/tool-router.js +15 -2
- package/package.json +4 -4
|
@@ -898,7 +898,19 @@ export class LocalBackendRouter {
|
|
|
898
898
|
}
|
|
899
899
|
}
|
|
900
900
|
if (method === 'DELETE' && pathname.startsWith('/api/v2/chat-agents/skills/delete/')) {
|
|
901
|
-
|
|
901
|
+
// URL.pathname 保留百分号编码——目录名/frontmatter 名含空格等字符时
|
|
902
|
+
// 客户端发来的是 fancy%20skill,不 decode 永远匹配不上 fancy skill
|
|
903
|
+
// (projects/mcp 路由段都 decode,这里对齐)。非法编码按 400 处理。
|
|
904
|
+
let skillName;
|
|
905
|
+
try {
|
|
906
|
+
skillName = decodeURIComponent(pathname.slice('/api/v2/chat-agents/skills/delete/'.length)).trim();
|
|
907
|
+
}
|
|
908
|
+
catch {
|
|
909
|
+
return {
|
|
910
|
+
status: 400,
|
|
911
|
+
data: { error: 'skillName is not valid percent-encoding' },
|
|
912
|
+
};
|
|
913
|
+
}
|
|
902
914
|
if (!skillName) {
|
|
903
915
|
return {
|
|
904
916
|
status: 400,
|
|
@@ -1743,6 +1755,14 @@ export class LocalBackendRouter {
|
|
|
1743
1755
|
// user+assistant pair onto history while the original (possibly bad)
|
|
1744
1756
|
// assistant reply stayed in place — the model saw both and had no real
|
|
1745
1757
|
// reason to answer differently.
|
|
1758
|
+
//
|
|
1759
|
+
// sidecar 门必须先于任何写操作:rerun 回合只能跑在 sidecar 上
|
|
1760
|
+
// (handleCoreLoopTurn 无 sidecar 直接 503)。若先截断再 503,旧回复
|
|
1761
|
+
// 已删、新回复不会产生、record 也不存在——非破坏性承诺破窗。
|
|
1762
|
+
if (!getSidecarSupervisor()) {
|
|
1763
|
+
emit(this.sse('error', { message: 'coreloop enabled but sidecar is not running' }));
|
|
1764
|
+
return { status: 503 };
|
|
1765
|
+
}
|
|
1746
1766
|
const targetMessageId = regenerateMatch[2];
|
|
1747
1767
|
// The user turn that prompted the target reply is whatever immediately
|
|
1748
1768
|
// precedes it — re-derive its text so buildConversationMessages gets
|
package/dist/local-executor.d.ts
CHANGED
|
@@ -30,12 +30,22 @@ export interface LocalFileReadRequest {
|
|
|
30
30
|
path: string;
|
|
31
31
|
encoding?: BufferEncoding;
|
|
32
32
|
maxSize?: number;
|
|
33
|
+
/** 1-based 起始行(CC Read 风格分页)。与 limit 配合做部分视图读取。 */
|
|
34
|
+
offset?: number;
|
|
35
|
+
/** 最多返回的行数(配合 offset 分页);缺省返回到文件尾。 */
|
|
36
|
+
limit?: number;
|
|
33
37
|
}
|
|
34
38
|
export interface LocalFileReadResult {
|
|
35
39
|
success: boolean;
|
|
36
40
|
content?: string;
|
|
37
41
|
/** 内容版本令牌(SHA-256)。读后写场景把它回传给 edit/write 做冲突检测。 */
|
|
38
42
|
version?: string;
|
|
43
|
+
/**
|
|
44
|
+
* 本次返回的只是部分视图(offset/limit 分页或超长裁剪)——模型没看到完整
|
|
45
|
+
* 内容,本会话对该路径的整体覆写将被拒绝(框架 partial_reads / CC
|
|
46
|
+
* isPartialView 对齐);定点修改走 local_edit_file 不受此限。
|
|
47
|
+
*/
|
|
48
|
+
partial?: boolean;
|
|
39
49
|
error?: string;
|
|
40
50
|
}
|
|
41
51
|
export interface LocalFileWriteRequest {
|
|
@@ -105,6 +115,18 @@ export interface LocalRunCodeResult extends LocalExecResult {
|
|
|
105
115
|
/** 本次调用实际安装成功的依赖。 */
|
|
106
116
|
installedPackages?: string[];
|
|
107
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* 部分视图裁剪(框架 `_clip` / CC Read 对齐)。version 永远基于完整内容计算,
|
|
120
|
+
* 部分视图的 CAS 令牌保持有效;返回的 partial 标记驱动整体覆写门。
|
|
121
|
+
*
|
|
122
|
+
* - 给了 offset/limit:按 1-based 行号切片,并在省略处打行数标记(含继续
|
|
123
|
+
* 分页的下一个 offset);
|
|
124
|
+
* - 没给但内容超长:头尾裁剪,中间打省略字符数标记。
|
|
125
|
+
*/
|
|
126
|
+
export declare function slicePartialView(content: string, offset?: number, limit?: number): {
|
|
127
|
+
content: string;
|
|
128
|
+
partial: boolean;
|
|
129
|
+
};
|
|
108
130
|
/** 内容版本令牌:UTF-8 内容的 SHA-256。用于 read-before-write 冲突检测。 */
|
|
109
131
|
/**
|
|
110
132
|
* read-before-write 冲突令牌:内容的 SHA-256(UTF-8)十六进制。与框架
|
|
@@ -167,6 +189,13 @@ export declare class LocalExecutor {
|
|
|
167
189
|
* 由 sidecar 的 `read_state.seed` 反向调用重灌(seedReadState)。
|
|
168
190
|
*/
|
|
169
191
|
private readonly readFileState;
|
|
192
|
+
/**
|
|
193
|
+
* 部分视图门(框架 partial_reads / CC isPartialView 对齐):本会话只读到
|
|
194
|
+
* 部分内容的分页/裁剪读取把路径登记在这里;writeLocalFile 拒绝整体覆写
|
|
195
|
+
* 未见全文的文件。完整读取或本会话自己的写/编辑成功后销记。会话恢复不
|
|
196
|
+
* 重灌此集合(与框架一致:resume 只重建 version 证据,门退化为 CAS)。
|
|
197
|
+
*/
|
|
198
|
+
private readonly partialReadPaths;
|
|
170
199
|
constructor(maxOutputBytes?: number, applyEditsFn?: ApplyEditsFn);
|
|
171
200
|
updateSafetyConfig(config: CommandSafetyConfigPayload): void;
|
|
172
201
|
/**
|
package/dist/local-executor.js
CHANGED
|
@@ -61,6 +61,55 @@ const NPM_PACKAGE_RE = /^(@[A-Za-z0-9._-]+\/)?[A-Za-z0-9._-]+(@[A-Za-z0-9._~^<>=
|
|
|
61
61
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
62
62
|
const DEFAULT_MAX_OUTPUT_BYTES = 100 * 1024;
|
|
63
63
|
const DEFAULT_MAX_READ_SIZE = 1024 * 1024;
|
|
64
|
+
/**
|
|
65
|
+
* 单次读取返回给模型的字符上限(框架 workspace_tools `_MAX_OUTPUT` 对齐)。
|
|
66
|
+
* 超出部分头尾裁剪并打标记——保留尾部,日志/编译错误的关键行通常在末尾。
|
|
67
|
+
*/
|
|
68
|
+
const MAX_READ_OUTPUT_CHARS = 100_000;
|
|
69
|
+
/**
|
|
70
|
+
* 部分视图裁剪(框架 `_clip` / CC Read 对齐)。version 永远基于完整内容计算,
|
|
71
|
+
* 部分视图的 CAS 令牌保持有效;返回的 partial 标记驱动整体覆写门。
|
|
72
|
+
*
|
|
73
|
+
* - 给了 offset/limit:按 1-based 行号切片,并在省略处打行数标记(含继续
|
|
74
|
+
* 分页的下一个 offset);
|
|
75
|
+
* - 没给但内容超长:头尾裁剪,中间打省略字符数标记。
|
|
76
|
+
*/
|
|
77
|
+
export function slicePartialView(content, offset, limit) {
|
|
78
|
+
if (offset !== undefined || limit !== undefined) {
|
|
79
|
+
const start = offset ?? 1;
|
|
80
|
+
if (!Number.isInteger(start) || start < 1) {
|
|
81
|
+
throw new Error(`offset 必须是 >= 1 的整数(1-based 行号),收到 ${offset}`);
|
|
82
|
+
}
|
|
83
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {
|
|
84
|
+
throw new Error(`limit 必须是 >= 1 的整数,收到 ${limit}`);
|
|
85
|
+
}
|
|
86
|
+
const lines = content.split('\n');
|
|
87
|
+
if (start > lines.length) {
|
|
88
|
+
throw new Error(`offset ${start} 超出文件行数(共 ${lines.length} 行)。`);
|
|
89
|
+
}
|
|
90
|
+
const end = limit === undefined ? lines.length : Math.min(lines.length, start - 1 + limit);
|
|
91
|
+
let view = lines.slice(start - 1, end).join('\n');
|
|
92
|
+
const omittedAbove = start - 1;
|
|
93
|
+
const omittedBelow = lines.length - end;
|
|
94
|
+
if (omittedAbove > 0) {
|
|
95
|
+
view = `...[{上文省略 ${omittedAbove} 行}]...\n${view}`;
|
|
96
|
+
}
|
|
97
|
+
if (omittedBelow > 0) {
|
|
98
|
+
view = `${view}\n...[{下文省略 ${omittedBelow} 行;用 offset=${end + 1} 继续}]...`;
|
|
99
|
+
}
|
|
100
|
+
return { content: view, partial: omittedAbove > 0 || omittedBelow > 0 };
|
|
101
|
+
}
|
|
102
|
+
if (content.length <= MAX_READ_OUTPUT_CHARS) {
|
|
103
|
+
return { content, partial: false };
|
|
104
|
+
}
|
|
105
|
+
const head = Math.floor(MAX_READ_OUTPUT_CHARS / 5);
|
|
106
|
+
const tail = MAX_READ_OUTPUT_CHARS - head;
|
|
107
|
+
const omitted = content.length - head - tail;
|
|
108
|
+
return {
|
|
109
|
+
content: `${content.slice(0, head)}\n...[{省略 ${omitted} 字符;用 offset/limit 分段读取}]...\n${content.slice(-tail)}`,
|
|
110
|
+
partial: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
64
113
|
/** 内容版本令牌:UTF-8 内容的 SHA-256。用于 read-before-write 冲突检测。 */
|
|
65
114
|
/**
|
|
66
115
|
* read-before-write 冲突令牌:内容的 SHA-256(UTF-8)十六进制。与框架
|
|
@@ -175,6 +224,13 @@ export class LocalExecutor {
|
|
|
175
224
|
* 由 sidecar 的 `read_state.seed` 反向调用重灌(seedReadState)。
|
|
176
225
|
*/
|
|
177
226
|
readFileState = new Map();
|
|
227
|
+
/**
|
|
228
|
+
* 部分视图门(框架 partial_reads / CC isPartialView 对齐):本会话只读到
|
|
229
|
+
* 部分内容的分页/裁剪读取把路径登记在这里;writeLocalFile 拒绝整体覆写
|
|
230
|
+
* 未见全文的文件。完整读取或本会话自己的写/编辑成功后销记。会话恢复不
|
|
231
|
+
* 重灌此集合(与框架一致:resume 只重建 version 证据,门退化为 CAS)。
|
|
232
|
+
*/
|
|
233
|
+
partialReadPaths = new Set();
|
|
178
234
|
constructor(maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES, applyEditsFn = applyEdits) {
|
|
179
235
|
this.maxOutputBytes = maxOutputBytes;
|
|
180
236
|
this.applyEditsFn = applyEditsFn;
|
|
@@ -405,10 +461,19 @@ export class LocalExecutor {
|
|
|
405
461
|
return { success: false, error: `File too large (${fileStat.size} bytes), max=${maxSize}` };
|
|
406
462
|
}
|
|
407
463
|
const content = await readFile(filePath, { encoding });
|
|
464
|
+
// version 基于完整内容:部分视图的读证据 CAS 仍然有效(框架对齐)。
|
|
408
465
|
const version = hashContent(content);
|
|
409
466
|
// P2b: 记录读证据,后续 write/edit 缺省 expectedVersion 时自动 CAS。
|
|
410
467
|
this.readFileState.set(filePath, version);
|
|
411
|
-
|
|
468
|
+
const view = slicePartialView(content, request.offset, request.limit);
|
|
469
|
+
// 部分视图登记/销记:整体覆写门(writeLocalFile)据此拒绝未见全文的覆写。
|
|
470
|
+
if (view.partial) {
|
|
471
|
+
this.partialReadPaths.add(filePath);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
this.partialReadPaths.delete(filePath);
|
|
475
|
+
}
|
|
476
|
+
return { success: true, content: view.content, version, partial: view.partial };
|
|
412
477
|
}
|
|
413
478
|
catch (error) {
|
|
414
479
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -427,6 +492,17 @@ export class LocalExecutor {
|
|
|
427
492
|
return this.serializeFileOp(filePath, async () => {
|
|
428
493
|
try {
|
|
429
494
|
const encoding = request.encoding ?? 'utf-8';
|
|
495
|
+
// 部分视图门:本会话对该路径只读到分页/裁剪后的内容,整体覆写会销毁
|
|
496
|
+
// 未见内容(框架 partial_reads / CC isPartialView 对齐)。定点修改走
|
|
497
|
+
// local_edit_file;确需整体重写时先用 offset/limit 分段读完。
|
|
498
|
+
if (this.partialReadPaths.has(filePath)) {
|
|
499
|
+
return {
|
|
500
|
+
success: false,
|
|
501
|
+
error: `拒绝整体覆写:${filePath} 本会话只读到部分内容(分页/裁剪),` +
|
|
502
|
+
'覆写会销毁未见内容。请改用 local_edit_file 做定点修改;' +
|
|
503
|
+
'确需整体重写时先用 offset/limit 分段读完。',
|
|
504
|
+
};
|
|
505
|
+
}
|
|
430
506
|
// P2b 自动 CAS:调用方没给 expectedVersion 时回落到本会话的读证据
|
|
431
507
|
// (含 read_state.seed 重灌的)。隐式期望只对仍存在的文件生效——
|
|
432
508
|
// 读后被删除的文件重新写入是新建,没有可覆盖的他人改动。
|
|
@@ -453,6 +529,8 @@ export class LocalExecutor {
|
|
|
453
529
|
const version = hashContent(request.content);
|
|
454
530
|
// 写成功后回写读证据:本会话自己的连续写不互相冲突。
|
|
455
531
|
this.readFileState.set(filePath, version);
|
|
532
|
+
// 模型亲自写了全文,部分视图门不再适用(框架对齐)。
|
|
533
|
+
this.partialReadPaths.delete(filePath);
|
|
456
534
|
return { success: true, version };
|
|
457
535
|
}
|
|
458
536
|
catch (error) {
|
|
@@ -498,6 +576,8 @@ export class LocalExecutor {
|
|
|
498
576
|
await this.atomicWrite(filePath, result.content, encoding);
|
|
499
577
|
const version = hashContent(result.content);
|
|
500
578
|
this.readFileState.set(filePath, version);
|
|
579
|
+
// 注意:编辑不销记 partialReadPaths——定点编辑不代表模型见过全文,
|
|
580
|
+
// 后续盲写仍会销毁未见内容(框架「Cleared by a full read or a write」对齐)。
|
|
501
581
|
return {
|
|
502
582
|
success: true,
|
|
503
583
|
version,
|
package/dist/runtime.js
CHANGED
|
@@ -124,8 +124,12 @@ export function resolveProductWebDist(entryUrl) {
|
|
|
124
124
|
const here = path.dirname(fileURLToPath(entryUrl));
|
|
125
125
|
const productId = path.basename(here);
|
|
126
126
|
for (const candidate of [
|
|
127
|
+
// 源码平面:入口在 products/<id>/(web dist 在同级 web/dist)。
|
|
127
128
|
path.join(here, 'web', 'dist'),
|
|
128
|
-
|
|
129
|
+
// 编译平面 A:入口在仓根 dist/products/<id>/(上溯 3 级到仓根)。
|
|
130
|
+
path.resolve(here, '..', '..', '..', 'products', productId, 'web', 'dist'),
|
|
131
|
+
// 编译平面 B:入口在产品内 products/<id>/dist/products/<id>/(上溯 5 级到仓根)。
|
|
132
|
+
path.resolve(here, '..', '..', '..', '..', '..', 'products', productId, 'web', 'dist'),
|
|
129
133
|
]) {
|
|
130
134
|
if (fs.existsSync(path.join(candidate, 'index.html')))
|
|
131
135
|
return candidate;
|
package/dist/tool-router.js
CHANGED
|
@@ -125,11 +125,24 @@ export class ToolRouter {
|
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
127
|
name: 'local_read_file',
|
|
128
|
-
description: 'Read a local text file'
|
|
128
|
+
description: 'Read a local text file. Large files can be paged with offset/limit (1-based line numbers); ' +
|
|
129
|
+
'a paged or clipped result is marked `partial: true`, and a full-file overwrite of a path ' +
|
|
130
|
+
'only partially read in this session is rejected — use local_edit_file for targeted changes, ' +
|
|
131
|
+
'or finish reading all segments before rewriting.',
|
|
129
132
|
mode: 'read',
|
|
130
133
|
inputSchema: {
|
|
131
134
|
type: 'object',
|
|
132
|
-
properties: {
|
|
135
|
+
properties: {
|
|
136
|
+
path: { type: 'string' },
|
|
137
|
+
offset: {
|
|
138
|
+
type: 'number',
|
|
139
|
+
description: 'Optional 1-based line number to start reading from (for paging large files).',
|
|
140
|
+
},
|
|
141
|
+
limit: {
|
|
142
|
+
type: 'number',
|
|
143
|
+
description: 'Optional maximum number of lines to return (for paging large files).',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
133
146
|
required: ['path'],
|
|
134
147
|
},
|
|
135
148
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-shell",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.18",
|
|
4
4
|
"description": "Steerable framework — product-neutral desktop/headless agent host (Tier 5). Electron main + preload + headless HTTP server (BS) sharing one storage/sidecar/tooling core; scenario packs extend it through the @steerable/pack-sdk contract and are composed at build time by the consuming product.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://steerableframework.com/",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"electron-store": "^8.1.0",
|
|
42
42
|
"he": "^1.2.0",
|
|
43
43
|
"node-pty": "^1.1.0",
|
|
44
|
-
"@steerable/
|
|
45
|
-
"@steerable/
|
|
46
|
-
"@steerable/agent-protocol": "0.6.
|
|
44
|
+
"@steerable/pack-sdk": "0.6.18",
|
|
45
|
+
"@steerable/agent-harness": "0.6.18",
|
|
46
|
+
"@steerable/agent-protocol": "0.6.18"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/better-sqlite3": "^7.6.13",
|