@kkelly-offical/kkcode 0.2.3-preview.1 → 0.2.3
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 -4
- package/package.json +3 -3
- package/src/commands/agent.mjs +41 -3
- package/src/commands/background.mjs +29 -0
- package/src/index.mjs +0 -0
- package/src/orchestration/background-manager.mjs +44 -1
- package/src/orchestration/task-scheduler.mjs +15 -6
- package/src/session/compaction.mjs +150 -42
- package/src/session/store.mjs +14 -5
- package/src/session/system-prompt.mjs +3 -3
- package/src/tool/prompt/task_group.txt +10 -0
- package/src/tool/prompt/task_parallel.txt +3 -0
- package/src/tool/registry.mjs +14 -3
- package/src/tool/task-tool.mjs +77 -23
- package/src/ui/repl-background-panel.mjs +6 -1
- package/src/version.mjs +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# kkcode
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@kkelly-offical/kkcode)
|
|
4
4
|
[](https://github.com/kkelly-offical/kkcode/releases)
|
|
5
5
|

|
|
6
6
|

|
|
@@ -219,18 +219,25 @@ For a deeper boundary matrix, see [CLI General Assistant Capability Matrix](docs
|
|
|
219
219
|
|
|
220
220
|
**English**
|
|
221
221
|
- kkcode supports bounded delegation through the `task` surface.
|
|
222
|
+
- Assistant mode may call subagents directly when the user explicitly asks for one or more agents.
|
|
223
|
+
- Use `task_group` to launch multiple parallel background subagents as one observable group.
|
|
224
|
+
- Use `kkcode agent list --json` to inspect built-in, custom, and configured subagent roles.
|
|
222
225
|
- Use `fresh_agent` for isolated implementation work.
|
|
223
226
|
- Use `fork_context` for read-only sidecar work such as research or verification.
|
|
224
227
|
- Do not outsource core understanding when the main thread must synthesize the result.
|
|
225
228
|
|
|
226
229
|
**中文**
|
|
227
230
|
- kkcode 通过 `task` 能力支持有边界的委派。
|
|
231
|
+
- 当用户显式要求一个或多个智能体工作时,Assistant 模式可以直接调用子智能体。
|
|
232
|
+
- 使用 `task_group` 可以把多个后台子智能体作为同一个并行组启动和观察。
|
|
233
|
+
- 使用 `kkcode agent list --json` 查看内置、自定义和配置覆盖后的子智能体角色。
|
|
228
234
|
- `fresh_agent` 适合隔离实现任务。
|
|
229
235
|
- `fork_context` 适合研究、审计、验证这类只读 sidecar 任务。
|
|
230
236
|
- 如果主线程必须综合判断,就不要把理解工作本身外包出去。
|
|
231
237
|
|
|
232
238
|
**Background task contract / 后台任务契约**
|
|
233
239
|
- 通过 `background_output` 查看后台任务输出
|
|
240
|
+
- 通过 `kkcode background parallel` 查看并行子智能体分组和 lane 状态
|
|
234
241
|
- 通过 `background_cancel` 取消后台任务
|
|
235
242
|
- 终态固定为 `completed` / `cancelled` / `error` / `interrupted`
|
|
236
243
|
|
|
@@ -376,16 +383,20 @@ update:
|
|
|
376
383
|
<a id="release-status"></a>
|
|
377
384
|
## Release Status / 发布状态
|
|
378
385
|
|
|
379
|
-
**Current
|
|
386
|
+
**Current stable / 当前稳定版本**: `v0.2.3`
|
|
380
387
|
**Latest releases / 最新发布**: [GitHub Releases](https://github.com/kkelly-offical/kkcode/releases)
|
|
381
388
|
**Package / 包地址**: [npm](https://www.npmjs.com/package/@kkelly-offical/kkcode)
|
|
382
389
|
|
|
383
390
|
**English**
|
|
384
|
-
- `0.2.3
|
|
391
|
+
- `0.2.3` is the stable assistant/subagent/context release: Assistant can explicitly delegate to one or many subagents, parallel lanes are observable, updater support is included, and context compaction keeps prior summaries plus recent evidence.
|
|
392
|
+
- `0.2.3-preview.2` validated the context compaction path.
|
|
393
|
+
- `0.2.3-preview.1` validated updater checks and the `kkcode update` command.
|
|
385
394
|
- `0.2.1` rebuilt kkcode around Assistant as the default general-purpose lane, with dedicated Agent and LongAgent modes for coding work.
|
|
386
395
|
|
|
387
396
|
**中文**
|
|
388
|
-
- `0.2.3
|
|
397
|
+
- `0.2.3` 是稳定版 Assistant / 子智能体 / 上下文版本:Assistant 可以显式委派一个或多个子智能体,并行 lane 可观察,包含更新器能力,上下文压缩会保留旧摘要和近期证据。
|
|
398
|
+
- `0.2.3-preview.2` 验证了上下文压缩路径。
|
|
399
|
+
- `0.2.3-preview.1` 验证了更新检查和 `kkcode update` 命令。
|
|
389
400
|
- `0.2.1` 将 kkcode 重构为以 Assistant 为默认入口的通用个人助手,同时保留专门面向代码工作的 Agent 和 LongAgent 模式。
|
|
390
401
|
|
|
391
402
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kkelly-offical/kkcode",
|
|
3
|
-
"version": "0.2.3
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "CLI-first personal assistant with dedicated coding and LongAgent modes for governed terminal workflows, MCP integrations, and extensible automation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.5.2",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"author": "kkelly-offical",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "https://github.com/kkelly-offical/kkcode.git"
|
|
11
|
+
"url": "git+https://github.com/kkelly-offical/kkcode.git"
|
|
12
12
|
},
|
|
13
13
|
"homepage": "https://github.com/kkelly-offical/kkcode",
|
|
14
14
|
"bugs": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"mcp"
|
|
26
26
|
],
|
|
27
27
|
"bin": {
|
|
28
|
-
"kkcode": "
|
|
28
|
+
"kkcode": "src/index.mjs"
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"src/",
|
package/src/commands/agent.mjs
CHANGED
|
@@ -1,17 +1,55 @@
|
|
|
1
1
|
import { Command } from "commander"
|
|
2
2
|
import { buildContext, printContextWarnings } from "../context.mjs"
|
|
3
3
|
import { LongAgentManager } from "../orchestration/longagent-manager.mjs"
|
|
4
|
+
import { listAgents } from "../agent/agent.mjs"
|
|
5
|
+
import { CustomAgentRegistry } from "../agent/custom-agent-loader.mjs"
|
|
4
6
|
|
|
5
7
|
export function createAgentCommand() {
|
|
6
8
|
const cmd = new Command("agent").description("inspect subagents and longagent runs")
|
|
7
9
|
|
|
8
10
|
cmd
|
|
9
11
|
.command("list")
|
|
10
|
-
.description("list configured
|
|
11
|
-
.
|
|
12
|
+
.description("list active subagent roles and configured overrides")
|
|
13
|
+
.option("--json", "print structured JSON")
|
|
14
|
+
.option("--configured", "print only config-defined agent.subagents overrides")
|
|
15
|
+
.option("--include-hidden", "include hidden internal roles")
|
|
16
|
+
.action(async (options) => {
|
|
12
17
|
const ctx = await buildContext()
|
|
13
18
|
printContextWarnings(ctx)
|
|
14
|
-
|
|
19
|
+
await CustomAgentRegistry.initialize(process.cwd())
|
|
20
|
+
const configured = ctx.configState.config.agent?.subagents || {}
|
|
21
|
+
if (options.configured) {
|
|
22
|
+
console.log(JSON.stringify(configured, null, 2))
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
const roles = listAgents({ includeHidden: options.includeHidden === true })
|
|
26
|
+
.filter((agent) => agent.mode === "subagent")
|
|
27
|
+
.map((agent) => {
|
|
28
|
+
const override = configured[agent.name] || null
|
|
29
|
+
return {
|
|
30
|
+
name: agent.name,
|
|
31
|
+
description: agent.description || "",
|
|
32
|
+
permission: agent.permission || "default",
|
|
33
|
+
model: override?.model || agent.model || null,
|
|
34
|
+
providerType: override?.providerType || override?.provider_type || null,
|
|
35
|
+
tools: agent.tools || null,
|
|
36
|
+
custom: agent._customAgent === true,
|
|
37
|
+
scope: agent._scope || null,
|
|
38
|
+
source: agent._source || null,
|
|
39
|
+
configured: Boolean(override)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
if (options.json) {
|
|
43
|
+
console.log(JSON.stringify(roles, null, 2))
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
for (const role of roles) {
|
|
47
|
+
const flags = [role.permission, role.custom ? "custom" : "builtin", role.configured ? "configured" : ""].filter(Boolean).join(", ")
|
|
48
|
+
const model = role.model ? " model=" + role.model : ""
|
|
49
|
+
const provider = role.providerType ? " provider=" + role.providerType : ""
|
|
50
|
+
console.log(role.name + " [" + flags + "]" + model + provider)
|
|
51
|
+
if (role.description) console.log(" " + role.description)
|
|
52
|
+
}
|
|
15
53
|
})
|
|
16
54
|
|
|
17
55
|
cmd
|
|
@@ -14,6 +14,9 @@ function printTaskSummary(task) {
|
|
|
14
14
|
if (!summary) return
|
|
15
15
|
console.log(`[${summary.status}] ${summary.id} :: ${summary.description}`)
|
|
16
16
|
console.log(` attempt=${summary.attempt} subagent=${summary.subagent || "-"} execution_mode=${summary.execution_mode || "-"} session=${summary.session_id || "-"}`)
|
|
17
|
+
if (summary.group_id) {
|
|
18
|
+
console.log(` group=${summary.group_id} label=${summary.group_label || "-"}`)
|
|
19
|
+
}
|
|
17
20
|
if (summary.interruption_reason) {
|
|
18
21
|
console.log(` interruption=${summary.interruption_reason}`)
|
|
19
22
|
}
|
|
@@ -56,6 +59,32 @@ export function createBackgroundCommand() {
|
|
|
56
59
|
})
|
|
57
60
|
})
|
|
58
61
|
|
|
62
|
+
|
|
63
|
+
cmd
|
|
64
|
+
.command("parallel")
|
|
65
|
+
.description("show delegated background tasks grouped as parallel subagent lanes")
|
|
66
|
+
.option("--json", "print raw JSON")
|
|
67
|
+
.action(async (options) => {
|
|
68
|
+
await withContext(async () => {
|
|
69
|
+
const groups = BackgroundManager.summarizeParallel(await BackgroundManager.list())
|
|
70
|
+
if (options.json) {
|
|
71
|
+
console.log(JSON.stringify(groups, null, 2))
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
if (groups.length === 0) {
|
|
75
|
+
console.log("no parallel subagent groups")
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
for (const group of groups) {
|
|
79
|
+
console.log("[group] " + group.group_id + " :: " + group.group_label + " total=" + group.total + " active=" + group.active)
|
|
80
|
+
for (const lane of group.lanes) {
|
|
81
|
+
console.log(" [" + lane.status + "] " + lane.id + " subagent=" + (lane.subagent || "-") + " task=" + (lane.logical_task_id || "-") + " session=" + (lane.session_id || "-"))
|
|
82
|
+
if (lane.result_preview) console.log(" preview=" + lane.result_preview)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
59
88
|
cmd
|
|
60
89
|
.command("output")
|
|
61
90
|
.description("print the terminal result payload for one background task")
|
package/src/index.mjs
CHANGED
|
File without changes
|
|
@@ -70,6 +70,8 @@ function summarizeTask(task) {
|
|
|
70
70
|
background_mode: task.backgroundMode || null,
|
|
71
71
|
subagent: task.payload?.subagent || task.payload?.subagentType || null,
|
|
72
72
|
execution_mode: task.payload?.executionMode || null,
|
|
73
|
+
group_id: task.payload?.groupId || null,
|
|
74
|
+
group_label: task.payload?.groupLabel || null,
|
|
73
75
|
session_id: task.payload?.subSessionId || null,
|
|
74
76
|
parent_session_id: task.payload?.parentSessionId || null,
|
|
75
77
|
stage_id: task.payload?.stageId || null,
|
|
@@ -104,10 +106,47 @@ function summarizeTaskList(tasks = []) {
|
|
|
104
106
|
recent_terminal: tasks
|
|
105
107
|
.filter((task) => TERMINAL_STATES.has(task.status))
|
|
106
108
|
.slice(0, 3)
|
|
107
|
-
.map((task) => summarizeTask(task))
|
|
109
|
+
.map((task) => summarizeTask(task)),
|
|
110
|
+
parallel_groups: summarizeParallelGroups(tasks)
|
|
108
111
|
}
|
|
109
112
|
}
|
|
110
113
|
|
|
114
|
+
function summarizeParallelGroups(tasks = []) {
|
|
115
|
+
const groups = new Map()
|
|
116
|
+
for (const task of tasks) {
|
|
117
|
+
const payload = task.payload || {}
|
|
118
|
+
const groupId = payload.groupId || payload.parentSessionId || "ungrouped"
|
|
119
|
+
const groupLabel = payload.groupLabel || payload.parentSessionId || "ungrouped"
|
|
120
|
+
if (!groups.has(groupId)) {
|
|
121
|
+
groups.set(groupId, {
|
|
122
|
+
group_id: groupId,
|
|
123
|
+
group_label: groupLabel,
|
|
124
|
+
parent_session_id: payload.parentSessionId || null,
|
|
125
|
+
total: 0,
|
|
126
|
+
active: 0,
|
|
127
|
+
counts: { pending: 0, running: 0, completed: 0, cancelled: 0, error: 0, interrupted: 0 },
|
|
128
|
+
lanes: []
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
const group = groups.get(groupId)
|
|
132
|
+
const status = task.status || "unknown"
|
|
133
|
+
group.total += 1
|
|
134
|
+
if (group.counts[status] !== undefined) group.counts[status] += 1
|
|
135
|
+
if (status === "pending" || status === "running") group.active += 1
|
|
136
|
+
group.lanes.push({
|
|
137
|
+
id: task.id,
|
|
138
|
+
status,
|
|
139
|
+
subagent: payload.subagent || payload.subagentType || null,
|
|
140
|
+
execution_mode: payload.executionMode || null,
|
|
141
|
+
logical_task_id: payload.logicalTaskId || null,
|
|
142
|
+
session_id: payload.subSessionId || null,
|
|
143
|
+
description: task.description || "",
|
|
144
|
+
result_preview: extractTaskResultPreview(task)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
return [...groups.values()].sort((a, b) => b.active - a.active || b.total - a.total)
|
|
148
|
+
}
|
|
149
|
+
|
|
111
150
|
function resolveWorkerTimeoutMs(config = {}, payload = {}) {
|
|
112
151
|
const raw = Number(payload.workerTimeoutMs || config.background?.worker_timeout_ms || 900000)
|
|
113
152
|
return Number.isFinite(raw) ? Math.max(1000, raw) : 900000
|
|
@@ -430,6 +469,10 @@ export const BackgroundManager = {
|
|
|
430
469
|
return summarizeTaskList(tasks)
|
|
431
470
|
},
|
|
432
471
|
|
|
472
|
+
summarizeParallel(tasks) {
|
|
473
|
+
return summarizeParallelGroups(tasks)
|
|
474
|
+
},
|
|
475
|
+
|
|
433
476
|
async list() {
|
|
434
477
|
await ensureBackgroundTaskRuntimeDir()
|
|
435
478
|
return readAllTasks()
|
|
@@ -195,7 +195,8 @@ async function ensureDelegatedSession({ executionMode, parentSessionId, subSessi
|
|
|
195
195
|
|
|
196
196
|
export function createTaskDelegate({ config, parentSessionId, model, providerType, runSubtask }) {
|
|
197
197
|
return async function delegateTask(args = {}) {
|
|
198
|
-
const
|
|
198
|
+
const requestedExecutionMode = args.inherit_context === true && !args.execution_mode ? "fork_context" : args.execution_mode
|
|
199
|
+
const executionModeResult = normalizeExecutionMode(requestedExecutionMode)
|
|
199
200
|
if (executionModeResult.error) return { error: executionModeResult.error }
|
|
200
201
|
const executionMode = executionModeResult.mode
|
|
201
202
|
const isolationResult = normalizeIsolation(args.isolation)
|
|
@@ -211,7 +212,7 @@ export function createTaskDelegate({ config, parentSessionId, model, providerTyp
|
|
|
211
212
|
})
|
|
212
213
|
|
|
213
214
|
const subSessionId = String(args.session_id || `sub_${parentSessionId}_${Date.now()}`)
|
|
214
|
-
const prompt = buildDelegationPrompt(args)
|
|
215
|
+
const prompt = buildDelegationPrompt({ ...args, execution_mode: executionMode })
|
|
215
216
|
|
|
216
217
|
const subModel = subagent.model || model
|
|
217
218
|
const subProvider = subagent.providerType || providerType
|
|
@@ -229,7 +230,9 @@ export function createTaskDelegate({ config, parentSessionId, model, providerTyp
|
|
|
229
230
|
model: subModel,
|
|
230
231
|
providerType: subProvider,
|
|
231
232
|
subagent,
|
|
232
|
-
allowQuestion: args.allow_question === true
|
|
233
|
+
allowQuestion: args.allow_question === true,
|
|
234
|
+
groupId: args.group_id || null,
|
|
235
|
+
groupLabel: args.group_label || null
|
|
233
236
|
})
|
|
234
237
|
await log(out.reply)
|
|
235
238
|
if (isCancelled()) return { cancelled: true }
|
|
@@ -243,7 +246,9 @@ export function createTaskDelegate({ config, parentSessionId, model, providerTyp
|
|
|
243
246
|
reply: out.reply,
|
|
244
247
|
tool_events: out.toolEvents?.length || 0,
|
|
245
248
|
file_changes: fileChanges,
|
|
246
|
-
edit_feedback: editFeedback
|
|
249
|
+
edit_feedback: editFeedback,
|
|
250
|
+
group_id: args.group_id || null,
|
|
251
|
+
group_label: args.group_label || null
|
|
247
252
|
}
|
|
248
253
|
}
|
|
249
254
|
|
|
@@ -265,7 +270,9 @@ export function createTaskDelegate({ config, parentSessionId, model, providerTyp
|
|
|
265
270
|
stageId: args.stage_id || null,
|
|
266
271
|
logicalTaskId: args.task_id || null,
|
|
267
272
|
plannedFiles: Array.isArray(args.planned_files) ? args.planned_files : [],
|
|
268
|
-
allowQuestion: args.allow_question === true
|
|
273
|
+
allowQuestion: args.allow_question === true,
|
|
274
|
+
groupId: args.group_id || null,
|
|
275
|
+
groupLabel: args.group_label || null
|
|
269
276
|
},
|
|
270
277
|
config
|
|
271
278
|
})
|
|
@@ -274,7 +281,9 @@ export function createTaskDelegate({ config, parentSessionId, model, providerTyp
|
|
|
274
281
|
status: task.status,
|
|
275
282
|
session_id: subSessionId,
|
|
276
283
|
execution_mode: executionMode,
|
|
277
|
-
isolation
|
|
284
|
+
isolation,
|
|
285
|
+
group_id: args.group_id || null,
|
|
286
|
+
group_label: args.group_label || null
|
|
278
287
|
}
|
|
279
288
|
}
|
|
280
289
|
|
|
@@ -5,36 +5,36 @@ import { saveCheckpoint } from "./checkpoint.mjs"
|
|
|
5
5
|
import { recordTurn } from "../usage/usage-meter.mjs"
|
|
6
6
|
import { loadPricing, calculateCost } from "../usage/pricing.mjs"
|
|
7
7
|
|
|
8
|
-
const COMPACTION_SYSTEM = `You are a conversation summarizer. Create a structured summary preserving all critical information for continued work.
|
|
8
|
+
const COMPACTION_SYSTEM = `You are a conversation summarizer. Create a structured, merge-safe summary preserving all critical information for continued work.
|
|
9
9
|
|
|
10
10
|
## Output Format
|
|
11
11
|
|
|
12
|
+
Return exactly:
|
|
13
|
+
|
|
14
|
+
<context-state>
|
|
15
|
+
{
|
|
16
|
+
"goal": "The user's current overall goal",
|
|
17
|
+
"completed": ["Completed work with specific file paths, function names, and line numbers"],
|
|
18
|
+
"in_progress": ["Current work being done"],
|
|
19
|
+
"files_modified": [{"path":"path/to/file","changes":["specific change"]}],
|
|
20
|
+
"key_decisions": ["Decision, constraint, or user preference"],
|
|
21
|
+
"errors_resolved": ["Error -> fix applied"],
|
|
22
|
+
"evidence": ["Important command output, test result, provider error, or exact failure detail"],
|
|
23
|
+
"next_steps": ["Specific next action item"]
|
|
24
|
+
}
|
|
25
|
+
</context-state>
|
|
26
|
+
|
|
12
27
|
<summary>
|
|
13
|
-
|
|
14
|
-
<completed>
|
|
15
|
-
- Completed task with specific details (file paths, function names, line numbers)
|
|
16
|
-
</completed>
|
|
17
|
-
<in_progress>Current work being done, if any</in_progress>
|
|
18
|
-
<files_modified>
|
|
19
|
-
- path/to/file: specific change description
|
|
20
|
-
</files_modified>
|
|
21
|
-
<key_decisions>
|
|
22
|
-
- Decision and reasoning
|
|
23
|
-
- User preferences or constraints
|
|
24
|
-
</key_decisions>
|
|
25
|
-
<errors_resolved>
|
|
26
|
-
- Error description → fix applied
|
|
27
|
-
</errors_resolved>
|
|
28
|
-
<next_steps>
|
|
29
|
-
- Specific next action items
|
|
30
|
-
</next_steps>
|
|
28
|
+
Concise human-readable continuation summary.
|
|
31
29
|
</summary>
|
|
32
30
|
|
|
33
31
|
Rules:
|
|
34
32
|
- Use the SAME LANGUAGE as the conversation
|
|
33
|
+
- Merge the prior context state with the new conversation delta; do not summarize the prior state as another chat message
|
|
35
34
|
- Preserve ALL file paths, function names, variable names, and technical identifiers exactly
|
|
36
35
|
- Include specific code changes, not just "modified file X"
|
|
37
36
|
- Omit tool call metadata and message formatting details
|
|
37
|
+
- Preserve exact errors, failing test names, package versions, release labels, and user constraints in evidence
|
|
38
38
|
- Be concise but never drop actionable information`
|
|
39
39
|
|
|
40
40
|
const DEFAULT_THRESHOLD_MESSAGES = 50
|
|
@@ -42,6 +42,118 @@ const DEFAULT_THRESHOLD_RATIO = 0.7
|
|
|
42
42
|
const DEFAULT_KEEP_RECENT = 6
|
|
43
43
|
const DEFAULT_KEEP_RECENT_TURNS = 3
|
|
44
44
|
const TOOL_RESULT_PREVIEW_LIMIT = 200
|
|
45
|
+
const EVIDENCE_PREVIEW_LIMIT = 900
|
|
46
|
+
const PATH_RE = /(?:^|\s)([A-Za-z0-9_.@~/-]+\.(?:mjs|js|ts|tsx|jsx|json|yaml|yml|md|txt|rs|go|py|sh|toml|lock))(?:[:\s]|$)/g
|
|
47
|
+
const IMPORTANT_LINE_RE = /(error|failed|failure|exception|traceback|assert|reject|denied|unauthorized|context|compact|version|publish|npm|test|lint|typecheck|diff|modified)/i
|
|
48
|
+
|
|
49
|
+
export function isCompactionSummaryMessage(msg) {
|
|
50
|
+
const content = msg?.content
|
|
51
|
+
if (typeof content === "string") return content.includes("<compaction-summary")
|
|
52
|
+
if (Array.isArray(content)) {
|
|
53
|
+
return content.some((block) => {
|
|
54
|
+
if (typeof block === "string") return block.includes("<compaction-summary")
|
|
55
|
+
return block?.type === "text" && typeof block.text === "string" && block.text.includes("<compaction-summary")
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function extractCompactionSummary(content) {
|
|
62
|
+
const text = Array.isArray(content)
|
|
63
|
+
? content.map((block) => typeof block === "string" ? block : (block?.text || block?.content || "")).join("\n")
|
|
64
|
+
: String(content || "")
|
|
65
|
+
const match = text.match(/<compaction-summary(?:\s[^>]*)?>\s*([\s\S]*?)\s*<\/compaction-summary>/i)
|
|
66
|
+
return (match ? match[1] : "").trim()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function clip(text, limit = EVIDENCE_PREVIEW_LIMIT) {
|
|
70
|
+
const raw = String(text || "")
|
|
71
|
+
if (raw.length <= limit) return raw
|
|
72
|
+
return raw.slice(0, limit) + "... [truncated " + raw.length + " chars]"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function extractPaths(text) {
|
|
76
|
+
const paths = new Set()
|
|
77
|
+
let match
|
|
78
|
+
PATH_RE.lastIndex = 0
|
|
79
|
+
while ((match = PATH_RE.exec(String(text || ""))) !== null) {
|
|
80
|
+
paths.add(match[1])
|
|
81
|
+
if (paths.size >= 12) break
|
|
82
|
+
}
|
|
83
|
+
return [...paths]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function importantLines(text, limit = 12) {
|
|
87
|
+
return String(text || "")
|
|
88
|
+
.split(/\r?\n/)
|
|
89
|
+
.map((line) => line.trim())
|
|
90
|
+
.filter((line) => line && IMPORTANT_LINE_RE.test(line))
|
|
91
|
+
.slice(0, limit)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function collectEvidenceLedger(messages, previewLimit = EVIDENCE_PREVIEW_LIMIT) {
|
|
95
|
+
const evidence = []
|
|
96
|
+
for (const msg of messages) {
|
|
97
|
+
const content = msg.content
|
|
98
|
+
const blocks = Array.isArray(content) ? content : [{ type: "text", text: content }]
|
|
99
|
+
for (const block of blocks) {
|
|
100
|
+
const raw = String(block?.content || block?.text || "")
|
|
101
|
+
if (!raw) continue
|
|
102
|
+
if (block.type === "tool_result") {
|
|
103
|
+
const lines = importantLines(raw)
|
|
104
|
+
const paths = extractPaths(raw)
|
|
105
|
+
if (block.is_error || lines.length || paths.length) {
|
|
106
|
+
evidence.push([
|
|
107
|
+
"- role=" + msg.role + " tool_result" + (block.is_error ? " ERROR" : ""),
|
|
108
|
+
paths.length ? " paths: " + paths.join(", ") : "",
|
|
109
|
+
lines.length ? " key_lines:\n" + lines.map((line) => " " + clip(line, 220)).join("\n") : " preview: " + clip(raw, previewLimit)
|
|
110
|
+
].filter(Boolean).join("\n"))
|
|
111
|
+
}
|
|
112
|
+
} else if (typeof content === "string" && raw.length > 1000) {
|
|
113
|
+
const lines = importantLines(raw)
|
|
114
|
+
const paths = extractPaths(raw)
|
|
115
|
+
if (lines.length || paths.length) {
|
|
116
|
+
evidence.push([
|
|
117
|
+
"- role=" + msg.role + " long_text",
|
|
118
|
+
paths.length ? " paths: " + paths.join(", ") : "",
|
|
119
|
+
lines.length ? " key_lines:\n" + lines.map((line) => " " + clip(line, 220)).join("\n") : ""
|
|
120
|
+
].filter(Boolean).join("\n"))
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (evidence.length >= 20) return evidence
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return evidence
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function buildCompactionPrompt({ previousSummary = "", messages, evidence = [] }) {
|
|
130
|
+
const transcript = messages.map((m) => {
|
|
131
|
+
const content = m.content
|
|
132
|
+
if (Array.isArray(content)) {
|
|
133
|
+
return "[" + m.role + "]: " + content.map((b) => {
|
|
134
|
+
if (b.type === "text") return b.text || ""
|
|
135
|
+
if (b.type === "tool_use") return "[tool_use:" + b.name + "(" + JSON.stringify(b.input || {}).slice(0, 120) + ")]"
|
|
136
|
+
if (b.type === "tool_result") return "[tool_result:" + (b.is_error ? "ERROR " : "") + (b.content || "") + "]"
|
|
137
|
+
return ""
|
|
138
|
+
}).filter(Boolean).join("\n")
|
|
139
|
+
}
|
|
140
|
+
return "[" + m.role + "]: " + content
|
|
141
|
+
}).join("\n\n")
|
|
142
|
+
|
|
143
|
+
return [
|
|
144
|
+
"<prior-context-state>",
|
|
145
|
+
previousSummary || "No prior compacted context.",
|
|
146
|
+
"</prior-context-state>",
|
|
147
|
+
"",
|
|
148
|
+
"<evidence-ledger>",
|
|
149
|
+
evidence.length ? evidence.join("\n") : "No extracted evidence ledger.",
|
|
150
|
+
"</evidence-ledger>",
|
|
151
|
+
"",
|
|
152
|
+
"<conversation-delta>",
|
|
153
|
+
transcript,
|
|
154
|
+
"</conversation-delta>"
|
|
155
|
+
].join("\n")
|
|
156
|
+
}
|
|
45
157
|
|
|
46
158
|
// Estimate tokens from a string, accounting for CJK characters (~1.5 chars/token vs ~4 for Latin)
|
|
47
159
|
export function estimateStringTokens(str) {
|
|
@@ -193,8 +305,12 @@ export async function compactSession({
|
|
|
193
305
|
baseUrl = null,
|
|
194
306
|
apiKeyEnv = null
|
|
195
307
|
}) {
|
|
196
|
-
const history = await getConversationHistory(sessionId, 9999)
|
|
308
|
+
const history = await getConversationHistory(sessionId, 9999, { includeMetadata: true })
|
|
197
309
|
if (history.length <= keepRecent + 2) return { compacted: false, reason: "too few messages" }
|
|
310
|
+
const previousSummary = isCompactionSummaryMessage(history[0])
|
|
311
|
+
? extractCompactionSummary(history[0].content)
|
|
312
|
+
: ""
|
|
313
|
+
const workingHistory = previousSummary ? history.slice(1) : history
|
|
198
314
|
|
|
199
315
|
// Turn-based split: keep last keepRecentTurns complete turns
|
|
200
316
|
// A "turn" = one user interaction cycle (user msg + model response + all tool calls)
|
|
@@ -202,7 +318,7 @@ export async function compactSession({
|
|
|
202
318
|
let splitIdx
|
|
203
319
|
const turnIds = []
|
|
204
320
|
const seenTurns = new Set()
|
|
205
|
-
for (const msg of
|
|
321
|
+
for (const msg of workingHistory) {
|
|
206
322
|
if (msg.turnId && !seenTurns.has(msg.turnId)) {
|
|
207
323
|
seenTurns.add(msg.turnId)
|
|
208
324
|
turnIds.push(msg.turnId)
|
|
@@ -210,29 +326,19 @@ export async function compactSession({
|
|
|
210
326
|
}
|
|
211
327
|
if (turnIds.length > keepRecentTurns) {
|
|
212
328
|
const keepFromTurnId = turnIds[turnIds.length - keepRecentTurns]
|
|
213
|
-
splitIdx =
|
|
214
|
-
if (splitIdx < 0) splitIdx =
|
|
329
|
+
splitIdx = workingHistory.findIndex(msg => msg.turnId === keepFromTurnId)
|
|
330
|
+
if (splitIdx < 0) splitIdx = workingHistory.length - keepRecent
|
|
215
331
|
} else {
|
|
216
332
|
// Fallback: not enough turns, use message count
|
|
217
|
-
splitIdx =
|
|
333
|
+
splitIdx = workingHistory.length - keepRecent
|
|
218
334
|
}
|
|
219
|
-
const toSummarize =
|
|
220
|
-
const kept =
|
|
335
|
+
const toSummarize = workingHistory.slice(0, splitIdx)
|
|
336
|
+
const kept = workingHistory.slice(splitIdx)
|
|
221
337
|
|
|
222
|
-
// Layer 1: prune large tool outputs before sending to LLM
|
|
338
|
+
// Layer 1: extract exact evidence, then prune large tool outputs before sending to LLM
|
|
339
|
+
const evidence = collectEvidenceLedger(toSummarize)
|
|
223
340
|
const pruned = pruneForSummary(toSummarize)
|
|
224
|
-
const summaryPrompt =
|
|
225
|
-
const content = m.content
|
|
226
|
-
if (Array.isArray(content)) {
|
|
227
|
-
return `[${m.role}]: ${content.map((b) => {
|
|
228
|
-
if (b.type === "text") return b.text || ""
|
|
229
|
-
if (b.type === "tool_use") return `[tool_use:${b.name}(${JSON.stringify(b.input || {}).slice(0, 120)})]`
|
|
230
|
-
if (b.type === "tool_result") return `[tool_result:${b.is_error ? "ERROR " : ""}${b.content || ""}]`
|
|
231
|
-
return ""
|
|
232
|
-
}).filter(Boolean).join("\n")}`
|
|
233
|
-
}
|
|
234
|
-
return `[${m.role}]: ${content}`
|
|
235
|
-
}).join("\n\n")
|
|
341
|
+
const summaryPrompt = buildCompactionPrompt({ previousSummary, messages: pruned, evidence })
|
|
236
342
|
|
|
237
343
|
const hookPayload = await HookBus.sessionCompacting({
|
|
238
344
|
sessionId,
|
|
@@ -266,7 +372,7 @@ export async function compactSession({
|
|
|
266
372
|
// Replace all messages with: [summary] + [kept recent messages]
|
|
267
373
|
const summaryMessage = {
|
|
268
374
|
role: "user",
|
|
269
|
-
content: `<compaction-summary>\n${summaryText}\n</compaction-summary>`
|
|
375
|
+
content: `<compaction-summary version="2">\n${summaryText}\n</compaction-summary>`
|
|
270
376
|
}
|
|
271
377
|
await replaceMessages(sessionId, [summaryMessage, ...kept])
|
|
272
378
|
|
|
@@ -285,8 +391,10 @@ export async function compactSession({
|
|
|
285
391
|
compactedAt: Date.now(),
|
|
286
392
|
summarizeCount: toSummarize.length,
|
|
287
393
|
keepCount: kept.length,
|
|
288
|
-
summaryVersion:
|
|
289
|
-
summaryLength: summaryText.length
|
|
394
|
+
summaryVersion: 2,
|
|
395
|
+
summaryLength: summaryText.length,
|
|
396
|
+
previousSummaryLength: previousSummary.length,
|
|
397
|
+
evidenceCount: evidence.length
|
|
290
398
|
})
|
|
291
399
|
|
|
292
400
|
return {
|
package/src/session/store.mjs
CHANGED
|
@@ -323,7 +323,7 @@ export async function listSessions({ cwd = null, limit = 100, includeChildren =
|
|
|
323
323
|
})
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
export async function getConversationHistory(sessionId, limit = 30) {
|
|
326
|
+
export async function getConversationHistory(sessionId, limit = 30, options = {}) {
|
|
327
327
|
return withLock(async () => {
|
|
328
328
|
await ensureLoadedUnsafe()
|
|
329
329
|
const data = await loadSessionDataUnsafe(sessionId)
|
|
@@ -339,10 +339,19 @@ export async function getConversationHistory(sessionId, limit = 30) {
|
|
|
339
339
|
const sliced = firstIsCompaction
|
|
340
340
|
? [msgs[0], ...msgs.slice(1).slice(-limit)]
|
|
341
341
|
: msgs.slice(-limit)
|
|
342
|
-
return sliced.map((msg) =>
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
342
|
+
return sliced.map((msg) => {
|
|
343
|
+
const base = {
|
|
344
|
+
role: msg.role,
|
|
345
|
+
content: msg.content
|
|
346
|
+
}
|
|
347
|
+
if (!options.includeMetadata) return base
|
|
348
|
+
return {
|
|
349
|
+
...base,
|
|
350
|
+
turnId: msg.turnId,
|
|
351
|
+
step: msg.step,
|
|
352
|
+
synthetic: msg.synthetic
|
|
353
|
+
}
|
|
354
|
+
})
|
|
346
355
|
})
|
|
347
356
|
}
|
|
348
357
|
|
|
@@ -97,7 +97,7 @@ export async function agentPrompt(agent) {
|
|
|
97
97
|
// Layer 4: Mode reminder (stable within mode)
|
|
98
98
|
export async function modeReminder(mode) {
|
|
99
99
|
const contractBlock = renderPublicModeContract()
|
|
100
|
-
if (mode === "assistant") return `${contractBlock}\n\nAssistant mode active. Treat this as the default CLI personal assistant lane for bounded terminal-native work: local files, logs, system checks, web lookup, Git/GitHub assistance, notes, task organization, and lightweight automation. Escalate to agent/code for explicit coding mutations and to longagent for staged multi-file delivery.`
|
|
100
|
+
if (mode === "assistant") return `${contractBlock}\n\nAssistant mode active. Treat this as the default CLI personal assistant lane for bounded terminal-native work: local files, logs, system checks, web lookup, Git/GitHub assistance, notes, task organization, and lightweight automation. Escalate to agent/code for explicit coding mutations and to longagent for staged multi-file delivery. When the user explicitly asks to summon, call, delegate to, or run one or more subagents, you must use task for one subagent or task_group for multiple parallel subagents. Use inherit_context=true or execution_mode=fork_context only for read-only sidecar work that needs the parent transcript; use fresh_agent for implementation work.`
|
|
101
101
|
if (mode === "plan") return `${contractBlock}\n\n${await loadSessionPrompt("plan.txt")}`
|
|
102
102
|
if (mode === "agent") return `${contractBlock}\n\n${await loadSessionPrompt("agent.txt")}\n\nCoding lane active. Focus on inspect/patch/verify coding work, keep diffs small, and validate with the narrowest useful tests.`
|
|
103
103
|
if (mode === "longagent") {
|
|
@@ -244,7 +244,7 @@ export async function buildSystemPromptBlocks({ mode, model, cwd, agent = null,
|
|
|
244
244
|
|
|
245
245
|
// Block 5.5: Available sub-agents (stable — changes only when custom agents change)
|
|
246
246
|
const allAgents = listAgents({ includeHidden: false })
|
|
247
|
-
const customSubagents = allAgents.filter((a) => a.mode === "subagent" && a.
|
|
247
|
+
const customSubagents = allAgents.filter((a) => a.mode === "subagent" && a.hidden !== true)
|
|
248
248
|
if (customSubagents.length) {
|
|
249
249
|
const agentLines = customSubagents.map((a) => {
|
|
250
250
|
const perms = a.permission === "readonly" ? " (read-only)" : a.permission === "full" ? " (full access)" : ""
|
|
@@ -254,7 +254,7 @@ export async function buildSystemPromptBlocks({ mode, model, cwd, agent = null,
|
|
|
254
254
|
"# Available Sub-agents",
|
|
255
255
|
"",
|
|
256
256
|
"Delegate specialized work to these sub-agents using the `task` tool with `subagent_type` parameter.",
|
|
257
|
-
"Use sub-agents when a task is self-contained and would benefit from a specialist,
|
|
257
|
+
"Use sub-agents when a task is self-contained and would benefit from a specialist, to save context window space, or whenever the user explicitly asks for subagents. Use task_group for multiple independent lanes.",
|
|
258
258
|
"",
|
|
259
259
|
...agentLines
|
|
260
260
|
].join("\n")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Use `task_group` when the user explicitly asks for multiple subagents, parallel agents, separate specialist lanes, or a comparison between independent agent findings.
|
|
2
|
+
|
|
3
|
+
Behavior:
|
|
4
|
+
- Launches every lane as a background delegated task.
|
|
5
|
+
- Returns a shared `group_id`, `group_label`, per-lane task ids, and a compact visualization payload.
|
|
6
|
+
- Use `inherit_context=true` or `execution_mode="fork_context"` only for read-only sidecar work that needs the parent transcript.
|
|
7
|
+
- Use `fresh_agent` for implementation or mutation work.
|
|
8
|
+
- Provide `subagent_type`, `objective` or `prompt`, `write_scope`, and `deliverable` for each lane when possible.
|
|
9
|
+
|
|
10
|
+
After launching a group, use `task_parallel`, `task_list`, or `task_output` to observe progress and collect results.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
Use task_parallel to inspect delegated background tasks grouped as parallel subagent lanes.
|
|
2
|
+
|
|
3
|
+
It returns groups with group_id, group_label, aggregate status counts, active lane counts, and per-lane status/result previews. Use it after task_group or when summarizing concurrent subagent progress for the user.
|
package/src/tool/registry.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { pathToFileURL } from "node:url"
|
|
|
8
8
|
import { atomicWriteFile, replaceInFileTransactional, replaceAllInFileTransactional, diffLineCount, buildStructuredPatch } from "./edit-transaction.mjs"
|
|
9
9
|
import { withFileLock } from "./file-lock-manager.mjs"
|
|
10
10
|
import { BackgroundManager } from "../orchestration/background-manager.mjs"
|
|
11
|
-
import { createTaskTool } from "./task-tool.mjs"
|
|
11
|
+
import { createTaskTool, createTaskGroupTool } from "./task-tool.mjs"
|
|
12
12
|
import { McpRegistry } from "../mcp/registry.mjs"
|
|
13
13
|
import { SkillRegistry } from "../skill/registry.mjs"
|
|
14
14
|
import { askQuestionInteractive } from "./question-prompt.mjs"
|
|
@@ -881,6 +881,17 @@ function builtinTools(config) {
|
|
|
881
881
|
}
|
|
882
882
|
}
|
|
883
883
|
|
|
884
|
+
|
|
885
|
+
const taskParallelTool = {
|
|
886
|
+
name: "task_parallel",
|
|
887
|
+
description: "Show delegated background tasks grouped as parallel subagent lanes.",
|
|
888
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
889
|
+
async execute() {
|
|
890
|
+
const tasks = await BackgroundManager.list()
|
|
891
|
+
return BackgroundManager.summarizeParallel(tasks)
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
884
895
|
const taskGetTool = {
|
|
885
896
|
name: "task_get",
|
|
886
897
|
description: "Retrieve one delegated background task summary and result payload by task_id.",
|
|
@@ -1570,7 +1581,7 @@ function builtinTools(config) {
|
|
|
1570
1581
|
const gitTools = config?.git_auto?.enabled !== false ? gitAutoTools : []
|
|
1571
1582
|
const gitFullAutoToolsList = config?.git_auto?.full_auto === true ? gitFullAutoTools : []
|
|
1572
1583
|
|
|
1573
|
-
return [listTool, sysinfoTool, readTool, writeTool, editTool, patchTool, multieditTool, globTool, grepTool, bashTool, createTaskTool(), outputTool, cancelTool, taskListTool, taskGetTool, taskStopTool, taskOutputTool, todowriteTool, questionTool, skillTool, webfetchTool, websearchTool, codesearchTool, notebookeditTool, enterPlanTool, exitPlanTool, ...gitTools, ...gitFullAutoToolsList]
|
|
1584
|
+
return [listTool, sysinfoTool, readTool, writeTool, editTool, patchTool, multieditTool, globTool, grepTool, bashTool, createTaskTool(), createTaskGroupTool(), outputTool, cancelTool, taskListTool, taskParallelTool, taskGetTool, taskStopTool, taskOutputTool, todowriteTool, questionTool, skillTool, webfetchTool, websearchTool, codesearchTool, notebookeditTool, enterPlanTool, exitPlanTool, ...gitTools, ...gitFullAutoToolsList]
|
|
1574
1585
|
}
|
|
1575
1586
|
|
|
1576
1587
|
function mcpTools() {
|
|
@@ -1593,7 +1604,7 @@ function mcpTools() {
|
|
|
1593
1604
|
|
|
1594
1605
|
function toolAllowedByMode(toolName, mode) {
|
|
1595
1606
|
if (mode === "plan") {
|
|
1596
|
-
return !["write", "edit", "patch", "multiedit", "notebookedit", "bash", "task", "git_snapshot", "git_restore", "git_apply_patch", "git_delete_snapshot"].includes(toolName)
|
|
1607
|
+
return !["write", "edit", "patch", "multiedit", "notebookedit", "bash", "task", "task_group", "git_snapshot", "git_restore", "git_apply_patch", "git_delete_snapshot"].includes(toolName)
|
|
1597
1608
|
}
|
|
1598
1609
|
return true
|
|
1599
1610
|
}
|
package/src/tool/task-tool.mjs
CHANGED
|
@@ -1,36 +1,90 @@
|
|
|
1
|
+
function taskProperties() {
|
|
2
|
+
return {
|
|
3
|
+
prompt: { type: "string", description: "self-contained task brief for the delegated subagent" },
|
|
4
|
+
objective: { type: "string", description: "primary outcome to achieve when synthesizing a delegation brief" },
|
|
5
|
+
why: { type: "string", description: "context or decision pressure behind the delegated work" },
|
|
6
|
+
write_scope: { type: "string", description: "explicit write scope such as read-only, specific files, or no mutations" },
|
|
7
|
+
description: { type: "string", description: "short task description for background task tracking" },
|
|
8
|
+
subagent_type: { type: "string", description: "explicit subagent type" },
|
|
9
|
+
category: { type: "string", description: "routing category" },
|
|
10
|
+
session_id: { type: "string", description: "continue from an existing delegated sub-session instead of starting fresh" },
|
|
11
|
+
stage_id: { type: "string", description: "optional stage id for orchestration" },
|
|
12
|
+
task_id: { type: "string", description: "optional logical task id" },
|
|
13
|
+
group_id: { type: "string", description: "optional parallel group id for related delegated tasks" },
|
|
14
|
+
group_label: { type: "string", description: "optional human-readable parallel group label" },
|
|
15
|
+
starting_points: { type: "array", items: { type: "string" }, description: "relevant files, symbols, tests, or commands the subagent should start from" },
|
|
16
|
+
constraints: { type: "array", items: { type: "string" }, description: "architectural boundaries, forbidden edits, or safety constraints for the delegated run" },
|
|
17
|
+
deliverable: { type: "string", description: "expected output from the subagent, such as a patch, findings, or a concise summary" },
|
|
18
|
+
execution_mode: { type: "string", enum: ["fresh_agent", "fork_context"], description: "delegation mode: fresh_agent for isolated work, fork_context for read-only sidecars that inherit the parent transcript" },
|
|
19
|
+
inherit_context: { type: "boolean", description: "shortcut for execution_mode=fork_context; only valid for read-only sidecar work" },
|
|
20
|
+
isolation: { type: "string", enum: ["default", "worktree"], description: "execution isolation for delegated work" },
|
|
21
|
+
run_in_background: { type: "boolean", description: "run async in background for non-blocking sidecar work" },
|
|
22
|
+
planned_files: { type: "array", items: { type: "string" }, description: "planned files for this task" },
|
|
23
|
+
allow_question: { type: "boolean", description: "allow question tool during delegated run; foreground only" }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function newGroupId() {
|
|
28
|
+
return "grp_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 8)
|
|
29
|
+
}
|
|
30
|
+
|
|
1
31
|
export function createTaskTool() {
|
|
2
32
|
return {
|
|
3
33
|
name: "task",
|
|
4
|
-
description: "Delegate complex multi-step work to a subagent that makes its own LLM calls.
|
|
34
|
+
description: "Delegate complex multi-step work to a subagent that makes its own LLM calls. Use inherit_context=true or execution_mode=fork_context for read-only sidecars that need the parent transcript. Background tasks spawn a separate worker process and must be observed via task_list/task_output.",
|
|
35
|
+
inputSchema: { type: "object", properties: taskProperties(), required: [] },
|
|
36
|
+
async execute(args, ctx) {
|
|
37
|
+
if (typeof ctx.delegateTask === "function") return ctx.delegateTask(args || {})
|
|
38
|
+
return { error: "task delegate unavailable" }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createTaskGroupTool() {
|
|
44
|
+
return {
|
|
45
|
+
name: "task_group",
|
|
46
|
+
description: "Launch multiple delegated subagents as one parallel background group. Use this when the user explicitly asks to summon multiple agents, run agents in parallel, split research/review/verification lanes, or compare independent specialist findings.",
|
|
5
47
|
inputSchema: {
|
|
6
48
|
type: "object",
|
|
7
49
|
properties: {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
deliverable: { type: "string", description: "expected output from the subagent, such as a patch, findings, or a concise summary" },
|
|
15
|
-
description: { type: "string", description: "short task description for background task tracking" },
|
|
16
|
-
subagent_type: { type: "string", description: "explicit subagent type" },
|
|
17
|
-
category: { type: "string", description: "routing category" },
|
|
18
|
-
execution_mode: { type: "string", enum: ["fresh_agent", "fork_context"], description: "delegation mode: 'fresh_agent' (default) for implementation or isolated new work, or 'fork_context' for read-only sidecar work that must inherit the parent transcript" },
|
|
19
|
-
isolation: { type: "string", enum: ["default", "worktree"], description: "execution isolation: 'default' runs in the main workspace, 'worktree' creates a local detached git worktree for isolated background sidecar execution" },
|
|
20
|
-
run_in_background: { type: "boolean", description: "run async in background for non-blocking sidecar work; background delegates cannot ask interactive questions" },
|
|
21
|
-
session_id: { type: "string", description: "continue from an existing delegated sub-session instead of starting fresh" },
|
|
22
|
-
stage_id: { type: "string", description: "optional stage id for orchestration" },
|
|
23
|
-
task_id: { type: "string", description: "optional logical task id" },
|
|
24
|
-
planned_files: { type: "array", items: { type: "string" }, description: "planned files for this task" },
|
|
25
|
-
allow_question: { type: "boolean", description: "allow question tool during delegated run; foreground only, not supported for run_in_background=true" }
|
|
50
|
+
group_id: { type: "string", description: "optional stable parallel group id" },
|
|
51
|
+
group_label: { type: "string", description: "human-readable label for the parallel group" },
|
|
52
|
+
inherit_context: { type: "boolean", description: "default context inheritance for tasks that do not specify execution_mode" },
|
|
53
|
+
execution_mode: { type: "string", enum: ["fresh_agent", "fork_context"], description: "default execution mode for tasks" },
|
|
54
|
+
isolation: { type: "string", enum: ["default", "worktree"], description: "default isolation mode for tasks" },
|
|
55
|
+
tasks: { type: "array", description: "subagent tasks to launch in parallel", items: { type: "object", properties: taskProperties(), required: [] } }
|
|
26
56
|
},
|
|
27
|
-
required: []
|
|
57
|
+
required: ["tasks"]
|
|
28
58
|
},
|
|
29
59
|
async execute(args, ctx) {
|
|
30
|
-
if (typeof ctx.delegateTask !== "function") {
|
|
31
|
-
|
|
60
|
+
if (typeof ctx.delegateTask !== "function") return { error: "task delegate unavailable" }
|
|
61
|
+
const tasks = Array.isArray(args?.tasks) ? args.tasks : []
|
|
62
|
+
if (tasks.length === 0) return { error: "task_group.tasks must contain at least one task" }
|
|
63
|
+
const groupId = String(args.group_id || newGroupId())
|
|
64
|
+
const groupLabel = String(args.group_label || "parallel subagents")
|
|
65
|
+
const results = []
|
|
66
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
67
|
+
const task = tasks[i] || {}
|
|
68
|
+
const merged = {
|
|
69
|
+
...task,
|
|
70
|
+
group_id: task.group_id || groupId,
|
|
71
|
+
group_label: task.group_label || groupLabel,
|
|
72
|
+
run_in_background: true,
|
|
73
|
+
inherit_context: task.inherit_context ?? args.inherit_context,
|
|
74
|
+
execution_mode: task.execution_mode || args.execution_mode,
|
|
75
|
+
isolation: task.isolation || args.isolation || "default",
|
|
76
|
+
description: task.description || task.objective || task.prompt || groupLabel,
|
|
77
|
+
task_id: task.task_id || "lane_" + (i + 1)
|
|
78
|
+
}
|
|
79
|
+
results.push(await ctx.delegateTask(merged))
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
group_id: groupId,
|
|
83
|
+
group_label: groupLabel,
|
|
84
|
+
status: results.some((r) => r?.error) ? "partial_error" : "launched",
|
|
85
|
+
tasks: results,
|
|
86
|
+
visualization: results.map((result, index) => ({ lane: index + 1, task_id: result.background_task_id || null, subagent: tasks[index]?.subagent_type || tasks[index]?.category || "default-subagent", status: result.status || (result.error ? "error" : "unknown"), session_id: result.session_id || null, error: result.error || null }))
|
|
32
87
|
}
|
|
33
|
-
return ctx.delegateTask(args || {})
|
|
34
88
|
}
|
|
35
89
|
}
|
|
36
90
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export function renderBackgroundSummaryPanel(backgroundSummary) {
|
|
2
2
|
if (!backgroundSummary) return []
|
|
3
|
-
|
|
3
|
+
const groups = Array.isArray(backgroundSummary.parallel_groups) ? backgroundSummary.parallel_groups : []
|
|
4
|
+
const activeGroups = groups.filter((group) => Number(group.active || 0) > 0).length
|
|
5
|
+
const activeLanes = groups.reduce((sum, group) => sum + Number(group.active || 0), 0)
|
|
6
|
+
const lines = [
|
|
4
7
|
`background=${backgroundSummary.active} active (pending:${backgroundSummary.counts.pending}, running:${backgroundSummary.counts.running})`,
|
|
5
8
|
`background.terminal=completed:${backgroundSummary.counts.completed} interrupted:${backgroundSummary.counts.interrupted} error:${backgroundSummary.counts.error}`
|
|
6
9
|
]
|
|
10
|
+
if (groups.length) lines.push(`parallel=${activeGroups} active group(s), ${activeLanes} active lane(s)`)
|
|
11
|
+
return lines
|
|
7
12
|
}
|
package/src/version.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export const PACKAGE_NAME = "@kkelly-offical/kkcode"
|
|
2
|
-
export const PACKAGE_VERSION = "0.2.3
|
|
3
|
-
export const RELEASE_LABEL = "0.2.3
|
|
2
|
+
export const PACKAGE_VERSION = "0.2.3"
|
|
3
|
+
export const RELEASE_LABEL = "0.2.3"
|