skyloom 1.16.1 → 1.17.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/README.md +15 -3
- package/dist/cli/loom_chat.d.ts.map +1 -1
- package/dist/cli/loom_chat.js +17 -0
- package/dist/cli/loom_chat.js.map +1 -1
- package/dist/cli/main.js +37 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/core/agent.d.ts +2 -0
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +25 -4
- package/dist/core/agent.js.map +1 -1
- package/dist/core/bgproc.d.ts +59 -0
- package/dist/core/bgproc.d.ts.map +1 -0
- package/dist/core/bgproc.js +135 -0
- package/dist/core/bgproc.js.map +1 -0
- package/dist/core/commands.d.ts.map +1 -1
- package/dist/core/commands.js +20 -0
- package/dist/core/commands.js.map +1 -1
- package/dist/core/diagnostics.d.ts +39 -0
- package/dist/core/diagnostics.d.ts.map +1 -0
- package/dist/core/diagnostics.js +206 -0
- package/dist/core/diagnostics.js.map +1 -0
- package/dist/core/diff.d.ts +31 -0
- package/dist/core/diff.d.ts.map +1 -0
- package/dist/core/diff.js +82 -0
- package/dist/core/diff.js.map +1 -0
- package/dist/core/envcontext.d.ts +25 -0
- package/dist/core/envcontext.d.ts.map +1 -0
- package/dist/core/envcontext.js +112 -0
- package/dist/core/envcontext.js.map +1 -0
- package/dist/core/factory.d.ts +2 -0
- package/dist/core/factory.d.ts.map +1 -1
- package/dist/core/factory.js +35 -2
- package/dist/core/factory.js.map +1 -1
- package/dist/core/sandbox.d.ts +1 -0
- package/dist/core/sandbox.d.ts.map +1 -1
- package/dist/core/sandbox.js +1 -0
- package/dist/core/sandbox.js.map +1 -1
- package/dist/core/security.d.ts +22 -2
- package/dist/core/security.d.ts.map +1 -1
- package/dist/core/security.js +54 -24
- package/dist/core/security.js.map +1 -1
- package/dist/core/skill.d.ts +4 -0
- package/dist/core/skill.d.ts.map +1 -1
- package/dist/core/skill.js +1 -0
- package/dist/core/skill.js.map +1 -1
- package/dist/core/subagent.d.ts +75 -0
- package/dist/core/subagent.d.ts.map +1 -0
- package/dist/core/subagent.js +287 -0
- package/dist/core/subagent.js.map +1 -0
- package/dist/core/tool.d.ts +25 -1
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +113 -36
- package/dist/core/tool.js.map +1 -1
- package/dist/plugins/loader.d.ts +49 -8
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +129 -16
- package/dist/plugins/loader.js.map +1 -1
- package/dist/tools/builtin.d.ts.map +1 -1
- package/dist/tools/builtin.js +126 -13
- package/dist/tools/builtin.js.map +1 -1
- package/dist/tools/spawn.d.ts +23 -0
- package/dist/tools/spawn.d.ts.map +1 -0
- package/dist/tools/spawn.js +77 -0
- package/dist/tools/spawn.js.map +1 -0
- package/docs/OPTIMIZATION_PLAN.md +21 -4
- package/package.json +1 -1
- package/src/cli/loom_chat.ts +11 -0
- package/src/cli/main.ts +31 -1
- package/src/core/agent.ts +25 -4
- package/src/core/bgproc.ts +153 -0
- package/src/core/commands.ts +20 -0
- package/src/core/diagnostics.ts +178 -0
- package/src/core/diff.ts +98 -0
- package/src/core/envcontext.ts +79 -0
- package/src/core/factory.ts +31 -2
- package/src/core/sandbox.ts +1 -1
- package/src/core/security.ts +62 -21
- package/src/core/skill.ts +1 -1
- package/src/core/subagent.ts +272 -0
- package/src/core/tool.ts +119 -40
- package/src/plugins/loader.ts +145 -18
- package/src/tools/builtin.ts +115 -13
- package/src/tools/spawn.ts +92 -0
- package/tests/agent.test.ts +35 -2
- package/tests/bgproc.test.ts +65 -0
- package/tests/diagnostics.test.ts +86 -0
- package/tests/edit_diff.test.ts +102 -0
- package/tests/envcontext.test.ts +67 -0
- package/tests/plugins.test.ts +84 -0
- package/tests/security.test.ts +87 -0
- package/tests/subagent.test.ts +211 -0
- package/tests/tool.test.ts +116 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spawn_agent tool — launch a general-purpose, isolated-context subagent to
|
|
3
|
+
* handle one focused, self-contained task and return only its final report.
|
|
4
|
+
*
|
|
5
|
+
* This is sky's analogue of Claude Code's Task tool. Subagent types are
|
|
6
|
+
* discovered from built-ins plus `.claude/agents` / `.sky/agents` definition
|
|
7
|
+
* files (see core/subagent). Subagents never receive spawn_agent themselves,
|
|
8
|
+
* so there is no recursive fan-out.
|
|
9
|
+
*/
|
|
10
|
+
import type { ToolDefinition } from '../core/tool';
|
|
11
|
+
import type { ToolRegistry } from '../core/tool';
|
|
12
|
+
import type { SkillRegistry } from '../core/skill';
|
|
13
|
+
import type { LLMClient } from '../core/llm';
|
|
14
|
+
import type { MessageBus } from '../core/bus';
|
|
15
|
+
export declare function createSpawnAgentTool(opts: {
|
|
16
|
+
config: any;
|
|
17
|
+
llm: LLMClient;
|
|
18
|
+
bus: MessageBus;
|
|
19
|
+
baseToolRegistry: ToolRegistry;
|
|
20
|
+
baseSkillRegistry: SkillRegistry;
|
|
21
|
+
cwd?: string;
|
|
22
|
+
}): ToolDefinition;
|
|
23
|
+
//# sourceMappingURL=spawn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spawn.d.ts","sourceRoot":"","sources":["../../src/tools/spawn.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,MAAM,EAAE,GAAG,CAAC;IACZ,GAAG,EAAE,SAAS,CAAC;IACf,GAAG,EAAE,UAAU,CAAC;IAChB,gBAAgB,EAAE,YAAY,CAAC;IAC/B,iBAAiB,EAAE,aAAa,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GAAG,cAAc,CAmEjB"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* spawn_agent tool — launch a general-purpose, isolated-context subagent to
|
|
4
|
+
* handle one focused, self-contained task and return only its final report.
|
|
5
|
+
*
|
|
6
|
+
* This is sky's analogue of Claude Code's Task tool. Subagent types are
|
|
7
|
+
* discovered from built-ins plus `.claude/agents` / `.sky/agents` definition
|
|
8
|
+
* files (see core/subagent). Subagents never receive spawn_agent themselves,
|
|
9
|
+
* so there is no recursive fan-out.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.createSpawnAgentTool = createSpawnAgentTool;
|
|
13
|
+
const subagent_1 = require("../core/subagent");
|
|
14
|
+
function createSpawnAgentTool(opts) {
|
|
15
|
+
const cwd = opts.cwd || process.cwd();
|
|
16
|
+
const buildDescription = () => {
|
|
17
|
+
const defs = (0, subagent_1.loadSubagentDefinitions)(cwd);
|
|
18
|
+
const lines = [...defs.values()].map((d) => ` - ${d.name}: ${d.description}`);
|
|
19
|
+
return ('派生一个隔离上下文的子智能体来独立完成一个聚焦、自洽的任务,只返回它的最终报告。' +
|
|
20
|
+
'当任务需要大量搜索/调研、或你想把一段独立工作从主上下文里隔离出去时使用。' +
|
|
21
|
+
'子智能体看不到你的对话历史,所以 task 必须自带全部所需上下文(目标、相关文件、约束)。' +
|
|
22
|
+
'它无法反问你,会一次性完成并汇报。可并行派生多个互不依赖的子智能体。\n\n可用子智能体类型:\n' +
|
|
23
|
+
lines.join('\n'));
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
name: 'spawn_agent',
|
|
27
|
+
description: buildDescription(),
|
|
28
|
+
parameters: [
|
|
29
|
+
{
|
|
30
|
+
name: 'agent_type',
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: '子智能体类型(见工具描述中的可用类型,如 general-purpose / explore 或自定义)',
|
|
33
|
+
required: true,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'task',
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: '完整、自洽的任务描述。子智能体看不到对话历史,必须在此写清目标、相关文件路径与约束。',
|
|
39
|
+
required: true,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'description',
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: '可选:对该任务的简短(3-5 词)标签,用于展示。',
|
|
45
|
+
required: false,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
// Long-running by nature (full nested agent loop); give it generous headroom.
|
|
49
|
+
timeout: 600000,
|
|
50
|
+
handler: async (params) => {
|
|
51
|
+
const agentType = String(params.agent_type || '').trim();
|
|
52
|
+
const task = String(params.task || '').trim();
|
|
53
|
+
if (!agentType)
|
|
54
|
+
return '[spawn_agent error] agent_type is required.';
|
|
55
|
+
if (!task)
|
|
56
|
+
return '[spawn_agent error] task is required.';
|
|
57
|
+
const defs = (0, subagent_1.loadSubagentDefinitions)(cwd);
|
|
58
|
+
const def = defs.get(agentType);
|
|
59
|
+
if (!def) {
|
|
60
|
+
const available = [...defs.keys()].join(', ');
|
|
61
|
+
return `[spawn_agent error] unknown agent_type '${agentType}'. Available: ${available}`;
|
|
62
|
+
}
|
|
63
|
+
const report = await (0, subagent_1.runSubagent)({
|
|
64
|
+
def,
|
|
65
|
+
task,
|
|
66
|
+
config: opts.config,
|
|
67
|
+
llm: opts.llm,
|
|
68
|
+
bus: opts.bus,
|
|
69
|
+
baseToolRegistry: opts.baseToolRegistry,
|
|
70
|
+
baseSkillRegistry: opts.baseSkillRegistry,
|
|
71
|
+
});
|
|
72
|
+
const header = `[subagent ${def.name} 完成]`;
|
|
73
|
+
return `${header}\n${report}`;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=spawn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spawn.js","sourceRoot":"","sources":["../../src/tools/spawn.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AASH,oDA0EC;AA5ED,+CAAwE;AAExE,SAAgB,oBAAoB,CAAC,IAOpC;IACC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAEtC,MAAM,gBAAgB,GAAG,GAAW,EAAE;QACpC,MAAM,IAAI,GAAG,IAAA,kCAAuB,EAAC,GAAG,CAAC,CAAC;QAC1C,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;QAC/E,OAAO,CACL,0CAA0C;YAC1C,uCAAuC;YACvC,gDAAgD;YAChD,mDAAmD;YACnD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CACjB,CAAC;IACJ,CAAC,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,aAAa;QACnB,WAAW,EAAE,gBAAgB,EAAE;QAC/B,UAAU,EAAE;YACV;gBACE,IAAI,EAAE,YAAY;gBAClB,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,sDAAsD;gBACnE,QAAQ,EAAE,IAAI;aACf;YACD;gBACE,IAAI,EAAE,MAAM;gBACZ,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,4CAA4C;gBACzD,QAAQ,EAAE,IAAI;aACf;YACD;gBACE,IAAI,EAAE,aAAa;gBACnB,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,2BAA2B;gBACxC,QAAQ,EAAE,KAAK;aAChB;SACF;QACD,8EAA8E;QAC9E,OAAO,EAAE,MAAM;QACf,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;YACxB,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YACzD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YAC9C,IAAI,CAAC,SAAS;gBAAE,OAAO,6CAA6C,CAAC;YACrE,IAAI,CAAC,IAAI;gBAAE,OAAO,uCAAuC,CAAC;YAE1D,MAAM,IAAI,GAAG,IAAA,kCAAuB,EAAC,GAAG,CAAC,CAAC;YAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAChC,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,MAAM,SAAS,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC9C,OAAO,2CAA2C,SAAS,iBAAiB,SAAS,EAAE,CAAC;YAC1F,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,IAAA,sBAAW,EAAC;gBAC/B,GAAG;gBACH,IAAI;gBACJ,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,GAAG,EAAE,IAAI,CAAC,GAAG;gBACb,GAAG,EAAE,IAAI,CAAC,GAAG;gBACb,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;gBACvC,iBAAiB,EAAE,IAAI,CAAC,iBAAiB;aAC1C,CAAC,CAAC;YAEH,MAAM,MAAM,GAAG,aAAa,GAAG,CAAC,IAAI,MAAM,CAAC;YAC3C,OAAO,GAAG,MAAM,KAAK,MAAM,EAAE,CAAC;QAChC,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -120,13 +120,13 @@
|
|
|
120
120
|
|
|
121
121
|
- [x] **P4.1 Session 恢复 + 持久化修复**:新增 CLI `/sessions`(编号列表、标记当前)、`/resume <序号|id>`、`/new`。**并修复了一个严重 bug**:`persistDb()`(唯一写盘的代码)**从未被调用** → sql.js 纯内存 → 会话/长期记忆/工作记忆**重启全丢**("启动自动恢复最新会话"形同虚设)。改为所有写经 `dbRun` 触发**防抖落盘** + `close()` 同步保存。新增跨实例持久化回归测试。
|
|
122
122
|
- [x] **P4.2 自动压缩(catalog 感知触发)**:`shouldAutoCompact()`/`contextUsage()` 不再硬编码 128K —— 改为按当前模型在 catalog 里的真实 `context` 窗口判断(留 20% 余量给回复)。修复了小窗口模型(deepseek-reasoner 64K、mixtral 32K)长聊时**先于压缩就溢出**的隐患;状态栏 % 与模型名也正确了。压缩本身(摘要 + 保留近 N 条 + 指令保真)已存在并已接线。后续:结构化 checkpoint + 溢出后重试(对标 opencode)。
|
|
123
|
-
- [
|
|
123
|
+
- [x] **P4.3 环境上下文快照**(对标 Claude Code `<env>` 块):[`envcontext.ts`](../src/core/envcontext.ts) 的 `buildEnvBlock` 合成「运行环境」块(工作目录、平台、Node、git 仓库/分支、日期),`gitInfo` 用纯文件读取识别仓库与分支(兼容 worktree 的 `.git` 文件)。`BaseAgent.injectEnvironment` 在 init/reinitLanguage 把它注入系统提示,与对话历史分离。新增 [`tests/envcontext.test.ts`](../tests/envcontext.test.ts) 6 用例。
|
|
124
124
|
|
|
125
125
|
### Phase 5 — 工具与插件健壮性|~1 天
|
|
126
126
|
|
|
127
|
-
- [
|
|
128
|
-
- [
|
|
129
|
-
- [
|
|
127
|
+
- [x] **P5.1 工具输入校验 + 强制 coercion**(对标 opencode `tools.md`):`ToolRegistry.validateAndCoerce` 在 `execute` 里于缓存/执行**之前**校验并归一化入参 —— 必填项缺失即拒、按声明类型 coerce(`"5"`→5、`"true"`→true、JSON 串→array/object)、enum 成员校验,handler 收到的是干净的类型化值,非法输入返回**可操作的**错误供模型重试。修了 `parseInt` 截断浮点(`"3.5"`→3)的隐患。见 [`tests/tool.test.ts`](../tests/tool.test.ts)(+7 用例)。输出校验暂未做。
|
|
128
|
+
- [x] **P5.2 权限模式内聚**(对标 Claude Code 权限模式):把审批逻辑收敛为一个纯函数 `decideApproval(level, mode, tool) → allow/ask/deny`([security.ts](../src/core/security.ts)),`checkApproval` 只做红线门禁 + 调用它 + 回调。新增模式 `acceptEdits`(自动放行文件编辑类工具,其余照 default 询问)与 `bypass`(除红线外全放行);保留 `auto/interactive/strict` 行为不变。`/perm <default|auto|accept|strict|bypass>` 运行时切换(linear + loom),`config.cli.approvalMode` 启动时生效。新增 [`tests/security.test.ts`](../tests/security.test.ts) 11 用例覆盖决策矩阵。后续:让工具自身发起 `permission.assert`(细粒度 scope)。
|
|
129
|
+
- [x] **P5.3 插件 hook 生命周期**(对标 opencode 插件):[`plugins/loader.ts`](../src/plugins/loader.ts) 从扁平加载器升级为有序 hook 系统。插件导出 `activate(ctx)`,通过 `ctx.registerTool` / `ctx.on(hook, fn)` 在**自己的 scope** 注册;`unload(name)` 精确移除其工具与 hook handler(并调 `deactivate`)。核心 hook:`init`(加载后、agent 起来前由 `SystemContext.initAll` 触发)、`tool.register`、`provider.update`。handler 按注册顺序触发、单个抛错隔离。**保留 legacy `register(registry)`**(diff 注册表追踪其工具以支持卸载)。新增 [`tests/plugins.test.ts`](../tests/plugins.test.ts) 7 用例。后续:`provider.update` 在 model_config 变更处触发。
|
|
130
130
|
|
|
131
131
|
### Phase 6 — 测试与质量门禁|~1.5 天
|
|
132
132
|
|
|
@@ -176,3 +176,20 @@ M5 打磨成品 Phase 7 → 美学工程化 + 品牌资产 + 发布
|
|
|
176
176
|
| 流式与工具执行交错复杂 | 复用已存在的 `chatStreamImpl` 事件模型,只接线不重写 |
|
|
177
177
|
| Catalog 迁移漏模型 | 以 `config/models.yaml` 为真值源迁移,向导/README 派生 |
|
|
178
178
|
| 美学改动破坏现有观感 | 设计 token 抽取为"提取"非"重画",先快照对比 |
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 6. Phase 8 — Claude Code / opencode 能力对齐 ✅ 已完成
|
|
183
|
+
|
|
184
|
+
四项核心能力补齐,全部 `tsc --noEmit` 绿 + 单测覆盖(351 → 387 用例,+36):
|
|
185
|
+
|
|
186
|
+
- [x] **P8.1 通用可定义子智能体**(对标 Claude Code `Task` 工具)
|
|
187
|
+
[`src/core/subagent.ts`](../src/core/subagent.ts) + [`src/tools/spawn.ts`](../src/tools/spawn.ts):`spawn_agent` 工具派生**隔离上下文**的子智能体(独立临时记忆,用完即删),只回传最终报告。内置 `general-purpose` / `explore`;支持 `.sky/agents/*.md` 与 `.claude/agents/*.md` 自定义(frontmatter 兼容 Claude Code,工具名自动映射)。子智能体永不持有 `spawn_agent`(无递归)。`/agents` 列举。新增 [`tests/subagent.test.ts`](../tests/subagent.test.ts) 13 用例。
|
|
188
|
+
- [x] **P8.2 精确编辑 + diff**(对标 Claude Code `Edit`)
|
|
189
|
+
[`src/core/diff.ts`](../src/core/diff.ts) + 升级 `edit_file`:强制**唯一匹配**(歧义即拒)、`replace_all`、no-op 检测、返回统一 diff(+/- 统计)。并修复旧实现 `String.replace` 把 `$&`/`$1` 当替换模式的隐患。新增 [`tests/edit_diff.test.ts`](../tests/edit_diff.test.ts) 10 用例。
|
|
190
|
+
- [x] **P8.3 后台任务 / 长进程**(对标 Claude Code Bash `run_in_background`)
|
|
191
|
+
[`src/core/bgproc.ts`](../src/core/bgproc.ts):`run_bash` 加 `background=true` 派后台子进程,`bash_output`(增量读)/`list_bash`/`kill_bash` 控制。复用沙箱红线预检;滚动输出上限;会话退出 `killAll`(无孤儿)。新增 [`tests/bgproc.test.ts`](../tests/bgproc.test.ts) 5 用例。
|
|
192
|
+
- [x] **P8.4 诊断(LSP 关键能力)**(对标 opencode LSP)
|
|
193
|
+
[`src/core/diagnostics.ts`](../src/core/diagnostics.ts) + `get_diagnostics` 工具:TS/JS 经**工作区** TypeScript 编译器 API 取真实语义诊断(行:列 + TS 码),零额外安装;其他语言走 `config.diagnostics` 配置的外部 checker(解析 `file:line:col` 输出)。新增 [`tests/diagnostics.test.ts`](../tests/diagnostics.test.ts) 8 用例。
|
|
194
|
+
|
|
195
|
+
> 这是把"完整的多 Agent 终端"推进到**与 Claude Code / opencode 同代能力面**的一步:可派生子智能体、精确可审计的编辑、后台进程、按文件诊断闭环。
|
package/package.json
CHANGED
package/src/cli/loom_chat.ts
CHANGED
|
@@ -424,6 +424,17 @@ export async function loomChat(ctx: any, startAgent: any, deps: LoomChatDeps): P
|
|
|
424
424
|
dim(`模式 → ${mode.current}${mode.current === "default" ? "" : " · Shift+Tab 或 /default 切回"}`);
|
|
425
425
|
continue;
|
|
426
426
|
}
|
|
427
|
+
if (cmdL === "/perm" || cmdL.startsWith("/perm ")) {
|
|
428
|
+
const { getSecurity, PERMISSION_MODE_ALIASES } = require("../core/security");
|
|
429
|
+
const sec = getSecurity();
|
|
430
|
+
const arg = inp.split(/\s+/)[1]?.toLowerCase();
|
|
431
|
+
if (!arg) { dim(`权限模式: ${sec.approvalMode} · 可选 default | auto | accept | strict | bypass`); continue; }
|
|
432
|
+
const m = PERMISSION_MODE_ALIASES[arg];
|
|
433
|
+
if (!m) { dim(`未知权限模式 '${arg}' · 可选 default | auto | accept | strict | bypass`); continue; }
|
|
434
|
+
sec.setMode(m);
|
|
435
|
+
dim(`✓ 权限模式 → ${m}`);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
427
438
|
if (cmdL === "/context") {
|
|
428
439
|
try {
|
|
429
440
|
const d = agent.contextDetail();
|
package/src/cli/main.ts
CHANGED
|
@@ -303,8 +303,14 @@ async function chat(agentName: string, modelOverride?: string, classic?: boolean
|
|
|
303
303
|
|
|
304
304
|
// Wire up security approval — prompt user for HIGH/CRITICAL operations
|
|
305
305
|
try {
|
|
306
|
-
const { getSecurity, DangerLevel } = require("../core/security");
|
|
306
|
+
const { getSecurity, DangerLevel, PERMISSION_MODE_ALIASES } = require("../core/security");
|
|
307
307
|
const sec = getSecurity();
|
|
308
|
+
// Honor a configured permission mode (config.yaml cli.approvalMode), mapped
|
|
309
|
+
// through the same aliases as /perm.
|
|
310
|
+
const cfgMode = (ctx as any).config?.cli?.approvalMode || (ctx as any).config?.cli?.approval_mode;
|
|
311
|
+
if (cfgMode && PERMISSION_MODE_ALIASES[String(cfgMode).toLowerCase()]) {
|
|
312
|
+
sec.setMode(PERMISSION_MODE_ALIASES[String(cfgMode).toLowerCase()]);
|
|
313
|
+
}
|
|
308
314
|
sec.setApprovalCallback(async (tool: string, args: Record<string, any>, level: number) => {
|
|
309
315
|
process.stdout.write(chalk.yellow(`\n ⚠ ${tool} ( danger level ${level} )\n`));
|
|
310
316
|
process.stdout.write(chalk.dim(` args: ${JSON.stringify(args).slice(0, 80)}\n`));
|
|
@@ -415,6 +421,17 @@ async function chat(agentName: string, modelOverride?: string, classic?: boolean
|
|
|
415
421
|
process.stdout.write(chalk.dim(` 模式 → ${MODE.current} · ${MODE.describe()}\n`));
|
|
416
422
|
continue;
|
|
417
423
|
}
|
|
424
|
+
if (cmdL === "/perm" || cmdL.startsWith("/perm ")) {
|
|
425
|
+
const { getSecurity, PERMISSION_MODE_ALIASES } = require("../core/security");
|
|
426
|
+
const sec = getSecurity();
|
|
427
|
+
const arg = inp.split(/\s+/)[1]?.toLowerCase();
|
|
428
|
+
if (!arg) { process.stdout.write(chalk.dim(` 权限模式: ${sec.approvalMode} · 可选 default | auto | accept | strict | bypass\n`)); continue; }
|
|
429
|
+
const m = PERMISSION_MODE_ALIASES[arg];
|
|
430
|
+
if (!m) { process.stdout.write(chalk.yellow(` 未知权限模式 '${arg}' · 可选 default | auto | accept | strict | bypass\n`)); continue; }
|
|
431
|
+
sec.setMode(m);
|
|
432
|
+
process.stdout.write(chalk.green(` ✓ 权限模式 → ${m}\n`));
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
418
435
|
if (cmdL === "/context") {
|
|
419
436
|
try {
|
|
420
437
|
const d = currentAgent.contextDetail();
|
|
@@ -438,6 +455,19 @@ async function chat(agentName: string, modelOverride?: string, classic?: boolean
|
|
|
438
455
|
process.stdout.write("\n");
|
|
439
456
|
continue;
|
|
440
457
|
}
|
|
458
|
+
if (cmdL === "/agents") {
|
|
459
|
+
const { loadSubagentDefinitions } = require("../core/subagent");
|
|
460
|
+
const defs = loadSubagentDefinitions();
|
|
461
|
+
process.stdout.write(chalk.bold(`\n 可派生子智能体 · spawn_agent\n`));
|
|
462
|
+
for (const d of defs.values()) {
|
|
463
|
+
const scope = d.source === "builtin" ? "内置" : "自定义";
|
|
464
|
+
const tools = d.tools === null ? "全部工具" : `${d.tools.length} 个工具`;
|
|
465
|
+
process.stdout.write(chalk.dim(` ◇ ${String(d.name).padEnd(18)} ${d.description}\n`));
|
|
466
|
+
process.stdout.write(chalk.dim(` └ ${scope} · ${tools}${d.model ? ` · ${d.model}` : ""}\n`));
|
|
467
|
+
}
|
|
468
|
+
process.stdout.write(chalk.dim(`\n 自定义: 在 .sky/agents/ 或 .claude/agents/ 放 <name>.md (frontmatter: description/tools/model)\n\n`));
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
441
471
|
if (cmdL === "/trace") {
|
|
442
472
|
const trace = (currentAgent as any).getLastTrace?.();
|
|
443
473
|
if (!trace || !trace.spans?.length) { process.stdout.write(chalk.dim(" 本会话还没有可追踪的运行\n")); continue; }
|
package/src/core/agent.ts
CHANGED
|
@@ -166,6 +166,17 @@ export class BaseAgent {
|
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/** Consolidated environment snapshot (cwd/platform/git/date) — see envcontext. */
|
|
170
|
+
protected injectEnvironment(prompt: string): string {
|
|
171
|
+
try {
|
|
172
|
+
const { buildEnvBlock } = require('./envcontext');
|
|
173
|
+
const lang = (this.config as any).llm?.language || 'zh';
|
|
174
|
+
return prompt + '\n\n' + buildEnvBlock({ lang });
|
|
175
|
+
} catch {
|
|
176
|
+
return prompt;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
169
180
|
/** Always return the live current time — never stale. */
|
|
170
181
|
protected currentTimeTag(): string {
|
|
171
182
|
const date = new Date();
|
|
@@ -229,6 +240,7 @@ export class BaseAgent {
|
|
|
229
240
|
this._baseSystemPrompt = '';
|
|
230
241
|
this._baseSystemPrompt = this.resolveSystemPrompt();
|
|
231
242
|
this._baseSystemPrompt = this.injectWorkspaceInfo(this._baseSystemPrompt);
|
|
243
|
+
this._baseSystemPrompt = this.injectEnvironment(this._baseSystemPrompt);
|
|
232
244
|
this._baseSystemPrompt = this.injectBehaviorRules(this._baseSystemPrompt);
|
|
233
245
|
this._baseSystemPrompt = this.injectProgrammingWisdom(this._baseSystemPrompt);
|
|
234
246
|
this._baseSystemPrompt = this.injectProjectMemory(this._baseSystemPrompt);
|
|
@@ -254,6 +266,7 @@ export class BaseAgent {
|
|
|
254
266
|
|
|
255
267
|
this._baseSystemPrompt = this.resolveSystemPrompt();
|
|
256
268
|
this._baseSystemPrompt = this.injectWorkspaceInfo(this._baseSystemPrompt);
|
|
269
|
+
this._baseSystemPrompt = this.injectEnvironment(this._baseSystemPrompt);
|
|
257
270
|
this._baseSystemPrompt = this.injectBehaviorRules(this._baseSystemPrompt);
|
|
258
271
|
this._baseSystemPrompt = this.injectProgrammingWisdom(this._baseSystemPrompt);
|
|
259
272
|
this._baseSystemPrompt = this.injectProjectMemory(this._baseSystemPrompt);
|
|
@@ -609,8 +622,12 @@ export class BaseAgent {
|
|
|
609
622
|
|
|
610
623
|
for (let i = 0; i < parsed.length; i++) {
|
|
611
624
|
const p = parsed[i];
|
|
612
|
-
// Dedup
|
|
613
|
-
|
|
625
|
+
// Dedup identical calls within this round for read-only tools (idempotent
|
|
626
|
+
// or cacheable, never dangerous): the model often emits the same
|
|
627
|
+
// read_file / web_search twice in one parallel batch — run it once and
|
|
628
|
+
// share the result. Safe because one round observes one world state.
|
|
629
|
+
const td = p.tool as ToolDefinition | undefined;
|
|
630
|
+
if (options?.dedupCacheable && p.toolArgs && td && (td.idempotent || td.cacheable) && !td.dangerous) {
|
|
614
631
|
const key = `${p.toolName}:${JSON.stringify(p.toolArgs, Object.keys(p.toolArgs).sort())}`;
|
|
615
632
|
if (seenDedupKeys.has(key)) {
|
|
616
633
|
execPlan.push({ idx: i, prep: p, isDuplicate: true });
|
|
@@ -629,6 +646,11 @@ export class BaseAgent {
|
|
|
629
646
|
const results = new Array<{ tc: ToolCall; result: string; success: boolean; toolName: string } | null>(parsed.length).fill(null);
|
|
630
647
|
const uniquePlan = execPlan.filter(e => !e.isDuplicate);
|
|
631
648
|
const concurrency = resolveConcurrency((this.config as any)?.llm?.tool_concurrency);
|
|
649
|
+
// Enter ACTING once for the whole batch rather than per-tool (each parallel
|
|
650
|
+
// worker re-setting the same state was a redundant async no-op).
|
|
651
|
+
if (uniquePlan.some(e => e.prep.tool && !e.prep.parseError && !e.prep.denied)) {
|
|
652
|
+
await this.setState(AgentState.ACTING);
|
|
653
|
+
}
|
|
632
654
|
const completed = await mapBounded(
|
|
633
655
|
uniquePlan,
|
|
634
656
|
async ({ idx, prep }, _i, aborted) => {
|
|
@@ -651,7 +673,6 @@ export class BaseAgent {
|
|
|
651
673
|
}
|
|
652
674
|
|
|
653
675
|
if (onStatus) onStatus(p.label);
|
|
654
|
-
await this.setState(AgentState.ACTING);
|
|
655
676
|
|
|
656
677
|
// File checkpoint: snapshot the target before any mutating file tool
|
|
657
678
|
// runs, so /rewind can restore the pre-turn state.
|
|
@@ -1393,7 +1414,7 @@ export class BaseAgent {
|
|
|
1393
1414
|
});
|
|
1394
1415
|
|
|
1395
1416
|
// ── Execute all tools via shared pipeline ──
|
|
1396
|
-
await this.executeToolCalls(response.toolCalls, { onStatus: onStatus ?? undefined, ephemeral });
|
|
1417
|
+
await this.executeToolCalls(response.toolCalls, { dedupCacheable: true, onStatus: onStatus ?? undefined, ephemeral });
|
|
1397
1418
|
await this.setState(AgentState.THINKING);
|
|
1398
1419
|
}
|
|
1399
1420
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background process manager — long-running shells that don't block the agent.
|
|
3
|
+
*
|
|
4
|
+
* `run_bash` with background=true spawns a child here and returns immediately
|
|
5
|
+
* with a job id. The agent later pulls incremental output (bash_output), lists
|
|
6
|
+
* jobs (list_bash), or terminates one (kill_bash). Children are NOT detached,
|
|
7
|
+
* so they die with the sky process — no orphan management needed across runs.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn, type ChildProcess, execSync } from 'child_process';
|
|
11
|
+
import { getLogger } from './logger';
|
|
12
|
+
import { preflightCheck } from './sandbox';
|
|
13
|
+
|
|
14
|
+
const log = getLogger('bgproc');
|
|
15
|
+
|
|
16
|
+
/** Per-job rolling output cap — keep the tail, drop the oldest. */
|
|
17
|
+
const MAX_LOG_BYTES = 512 * 1024;
|
|
18
|
+
|
|
19
|
+
export type BgStatus = 'running' | 'exited' | 'killed' | 'error';
|
|
20
|
+
|
|
21
|
+
export interface BgJobView {
|
|
22
|
+
id: string;
|
|
23
|
+
command: string;
|
|
24
|
+
pid: number | null;
|
|
25
|
+
status: BgStatus;
|
|
26
|
+
exitCode: number | null;
|
|
27
|
+
startedAt: number;
|
|
28
|
+
endedAt: number | null;
|
|
29
|
+
/** Total bytes ever produced (before any rolling trim). */
|
|
30
|
+
totalBytes: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface BgJob extends BgJobView {
|
|
34
|
+
child: ChildProcess | null;
|
|
35
|
+
log: string; // combined stdout+stderr in arrival order
|
|
36
|
+
readOffset: number; // cursor for incremental reads
|
|
37
|
+
trimmed: number; // bytes dropped from the front by the rolling cap
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class BackgroundManager {
|
|
41
|
+
private jobs = new Map<string, BgJob>();
|
|
42
|
+
private seq = 0;
|
|
43
|
+
|
|
44
|
+
private append(job: BgJob, text: string): void {
|
|
45
|
+
job.log += text;
|
|
46
|
+
job.totalBytes += Buffer.byteLength(text, 'utf8');
|
|
47
|
+
if (job.log.length > MAX_LOG_BYTES) {
|
|
48
|
+
const drop = job.log.length - MAX_LOG_BYTES;
|
|
49
|
+
job.log = job.log.slice(drop);
|
|
50
|
+
job.trimmed += drop;
|
|
51
|
+
job.readOffset = Math.max(0, job.readOffset - drop);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Start a background command. Returns the job id, or an error string. */
|
|
56
|
+
start(command: string, opts?: { cwd?: string; env?: Record<string, string> }): { id?: string; error?: string } {
|
|
57
|
+
const check = preflightCheck(command);
|
|
58
|
+
if (check) return { error: `[BLOCKED] ${check}` };
|
|
59
|
+
|
|
60
|
+
const id = `bg_${(++this.seq).toString(36)}_${Date.now().toString(36)}`;
|
|
61
|
+
let child: ChildProcess;
|
|
62
|
+
try {
|
|
63
|
+
child = spawn(command, {
|
|
64
|
+
shell: true,
|
|
65
|
+
cwd: opts?.cwd || process.cwd(),
|
|
66
|
+
env: { ...process.env, ...(opts?.env || {}) },
|
|
67
|
+
windowsHide: true,
|
|
68
|
+
});
|
|
69
|
+
} catch (e: any) {
|
|
70
|
+
return { error: `Failed to start background command: ${e.message || e}` };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const job: BgJob = {
|
|
74
|
+
id, command, pid: child.pid ?? null, status: 'running', exitCode: null,
|
|
75
|
+
startedAt: Date.now(), endedAt: null, totalBytes: 0,
|
|
76
|
+
child, log: '', readOffset: 0, trimmed: 0,
|
|
77
|
+
};
|
|
78
|
+
this.jobs.set(id, job);
|
|
79
|
+
|
|
80
|
+
child.stdout?.on('data', (d) => this.append(job, d.toString()));
|
|
81
|
+
child.stderr?.on('data', (d) => this.append(job, d.toString()));
|
|
82
|
+
child.on('error', (e) => {
|
|
83
|
+
job.status = 'error';
|
|
84
|
+
job.endedAt = Date.now();
|
|
85
|
+
this.append(job, `\n[spawn error] ${e.message}\n`);
|
|
86
|
+
log.warn('bg_error', { id, error: e.message });
|
|
87
|
+
});
|
|
88
|
+
child.on('exit', (code, signal) => {
|
|
89
|
+
if (job.status === 'running') job.status = signal ? 'killed' : 'exited';
|
|
90
|
+
job.exitCode = code;
|
|
91
|
+
job.endedAt = Date.now();
|
|
92
|
+
job.child = null;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return { id };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get(id: string): BgJob | undefined { return this.jobs.get(id); }
|
|
99
|
+
|
|
100
|
+
list(): BgJobView[] {
|
|
101
|
+
return [...this.jobs.values()].map(toView).sort((a, b) => b.startedAt - a.startedAt);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Read output produced since the last read; advances the cursor. */
|
|
105
|
+
read(id: string): { ok: boolean; text?: string; status?: BgStatus; exitCode?: number | null; error?: string } {
|
|
106
|
+
const job = this.jobs.get(id);
|
|
107
|
+
if (!job) return { ok: false, error: `No background job '${id}'.` };
|
|
108
|
+
const chunk = job.log.slice(job.readOffset);
|
|
109
|
+
job.readOffset = job.log.length;
|
|
110
|
+
return { ok: true, text: chunk, status: job.status, exitCode: job.exitCode };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
kill(id: string): { ok: boolean; error?: string } {
|
|
114
|
+
const job = this.jobs.get(id);
|
|
115
|
+
if (!job) return { ok: false, error: `No background job '${id}'.` };
|
|
116
|
+
if (job.status !== 'running' || !job.child) return { ok: false, error: `Job '${id}' is not running (${job.status}).` };
|
|
117
|
+
const pid = job.child.pid;
|
|
118
|
+
try {
|
|
119
|
+
if (process.platform === 'win32' && pid) {
|
|
120
|
+
// child.kill() doesn't reap the cmd.exe process tree on Windows.
|
|
121
|
+
try { execSync(`taskkill /pid ${pid} /T /F`, { stdio: 'ignore' }); }
|
|
122
|
+
catch { job.child.kill(); }
|
|
123
|
+
} else {
|
|
124
|
+
job.child.kill('SIGTERM');
|
|
125
|
+
}
|
|
126
|
+
job.status = 'killed';
|
|
127
|
+
job.endedAt = Date.now();
|
|
128
|
+
return { ok: true };
|
|
129
|
+
} catch (e: any) {
|
|
130
|
+
return { ok: false, error: `Failed to kill '${id}': ${e.message || e}` };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Terminate all running jobs (called on session shutdown). */
|
|
135
|
+
killAll(): void {
|
|
136
|
+
for (const job of this.jobs.values()) {
|
|
137
|
+
if (job.status === 'running') this.kill(job.id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function toView(j: BgJob): BgJobView {
|
|
143
|
+
return {
|
|
144
|
+
id: j.id, command: j.command, pid: j.pid, status: j.status,
|
|
145
|
+
exitCode: j.exitCode, startedAt: j.startedAt, endedAt: j.endedAt, totalBytes: j.totalBytes,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let _mgr: BackgroundManager | null = null;
|
|
150
|
+
export function getBackgroundManager(): BackgroundManager {
|
|
151
|
+
if (!_mgr) _mgr = new BackgroundManager();
|
|
152
|
+
return _mgr;
|
|
153
|
+
}
|
package/src/core/commands.ts
CHANGED
|
@@ -313,6 +313,16 @@ export const BUILTIN_COMMANDS: CommandInfo[] = [
|
|
|
313
313
|
takesArgs: false,
|
|
314
314
|
source: 'builtin',
|
|
315
315
|
},
|
|
316
|
+
{
|
|
317
|
+
name: 'agents',
|
|
318
|
+
aliases: [],
|
|
319
|
+
description: 'List spawnable subagents',
|
|
320
|
+
label: '可派生子智能体',
|
|
321
|
+
category: 'context',
|
|
322
|
+
hints: [],
|
|
323
|
+
takesArgs: false,
|
|
324
|
+
source: 'builtin',
|
|
325
|
+
},
|
|
316
326
|
{
|
|
317
327
|
name: 'workspace',
|
|
318
328
|
aliases: [],
|
|
@@ -428,6 +438,16 @@ export const BUILTIN_COMMANDS: CommandInfo[] = [
|
|
|
428
438
|
takesArgs: false,
|
|
429
439
|
source: 'builtin',
|
|
430
440
|
},
|
|
441
|
+
{
|
|
442
|
+
name: 'perm',
|
|
443
|
+
aliases: [],
|
|
444
|
+
description: 'Set permission mode (default|auto|accept|strict|bypass)',
|
|
445
|
+
label: '权限模式(default/auto/accept/strict/bypass)',
|
|
446
|
+
category: 'workflow',
|
|
447
|
+
hints: ['default', 'auto', 'accept', 'strict', 'bypass'],
|
|
448
|
+
takesArgs: true,
|
|
449
|
+
source: 'builtin',
|
|
450
|
+
},
|
|
431
451
|
|
|
432
452
|
// ── File & Checkpoint ──
|
|
433
453
|
{
|