@kiturone/kapibala 0.0.8 → 0.0.10
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/index.cjs +79 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -12
- package/dist/index.d.ts +39 -12
- package/dist/index.js +78 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -103,6 +103,7 @@ __export(index_exports, {
|
|
|
103
103
|
createLogId: () => createLogId,
|
|
104
104
|
createMessageId: () => createMessageId,
|
|
105
105
|
createRunCommandTool: () => createRunCommandTool,
|
|
106
|
+
createSessionError: () => createSessionError,
|
|
106
107
|
createSpawnAgentTool: () => createSpawnAgentTool,
|
|
107
108
|
createWaitAgentTool: () => createWaitAgentTool,
|
|
108
109
|
defineTool: () => defineTool,
|
|
@@ -158,7 +159,6 @@ __export(index_exports, {
|
|
|
158
159
|
sanitizeModelErrorMessage: () => sanitizeModelErrorMessage,
|
|
159
160
|
sanitizeSessionTitle: () => sanitizeSessionTitle,
|
|
160
161
|
scanSkillRoot: () => scanSkillRoot,
|
|
161
|
-
sharedWorkspaceLeaseCoordinator: () => sharedWorkspaceLeaseCoordinator,
|
|
162
162
|
skillDigest: () => skillDigest,
|
|
163
163
|
skillFailure: () => skillFailure,
|
|
164
164
|
skillPathKey: () => skillPathKey,
|
|
@@ -308,6 +308,21 @@ var KapibalaError = class extends Error {
|
|
|
308
308
|
this.dispatchOutcome = options?.dispatchOutcome;
|
|
309
309
|
}
|
|
310
310
|
};
|
|
311
|
+
var SESSION_SAFE_MESSAGES = {
|
|
312
|
+
SESSION_LOCKED: "\u4F1A\u8BDD\u5DF2\u88AB\u5176\u4ED6\u6301\u6709\u8005\u5360\u7528",
|
|
313
|
+
SESSION_RECOVERY_IN_PROGRESS: "\u4F1A\u8BDD\u6B63\u5728\u6062\u590D",
|
|
314
|
+
SESSION_LOCK_OWNER_CHANGED: "\u4F1A\u8BDD\u9501\u6301\u6709\u8005\u5DF2\u53D8\u5316",
|
|
315
|
+
SESSION_NOT_FOUND: "\u672A\u627E\u5230\u6307\u5B9A\u4F1A\u8BDD",
|
|
316
|
+
SESSION_ID_AMBIGUOUS: "\u4F1A\u8BDD\u524D\u7F00\u4E0D\u552F\u4E00",
|
|
317
|
+
HISTORY_PAGE_INVALID: "\u5386\u53F2\u9875\u53C2\u6570\u65E0\u6548"
|
|
318
|
+
};
|
|
319
|
+
function createSessionError(code, message) {
|
|
320
|
+
return new KapibalaError(message, {
|
|
321
|
+
code,
|
|
322
|
+
safeMessage: SESSION_SAFE_MESSAGES[code],
|
|
323
|
+
retryPolicy: "after_user_action"
|
|
324
|
+
});
|
|
325
|
+
}
|
|
311
326
|
var PreDispatchError = class extends KapibalaError {
|
|
312
327
|
constructor(message, options) {
|
|
313
328
|
super(message, {
|
|
@@ -1453,19 +1468,23 @@ var SessionLock = class _SessionLock {
|
|
|
1453
1468
|
if (error.code !== "EEXIST") throw error;
|
|
1454
1469
|
const stale = _SessionLock.readOwner(lockPath);
|
|
1455
1470
|
if (!stale || !_SessionLock.isDead(stale.pid))
|
|
1456
|
-
throw
|
|
1471
|
+
throw createSessionError(
|
|
1472
|
+
"SESSION_LOCKED",
|
|
1457
1473
|
`Session locked by a live or unknown owner${stale ? ` (pid=${stale.pid})` : ""}`
|
|
1458
1474
|
);
|
|
1459
1475
|
try {
|
|
1460
1476
|
import_node_fs3.default.mkdirSync(recovery);
|
|
1461
1477
|
} catch {
|
|
1462
|
-
throw
|
|
1478
|
+
throw createSessionError(
|
|
1479
|
+
"SESSION_RECOVERY_IN_PROGRESS",
|
|
1480
|
+
"Session locked: recovery in progress"
|
|
1481
|
+
);
|
|
1463
1482
|
}
|
|
1464
1483
|
try {
|
|
1465
1484
|
import_node_fs3.default.writeFileSync(import_node_path2.default.join(recovery, "owner.json"), JSON.stringify(owner), { flag: "wx" });
|
|
1466
1485
|
const current = _SessionLock.readOwner(lockPath);
|
|
1467
1486
|
if (current?.nonce !== stale.nonce || current.pid !== stale.pid || !_SessionLock.isDead(current.pid)) {
|
|
1468
|
-
throw
|
|
1487
|
+
throw createSessionError("SESSION_LOCK_OWNER_CHANGED", "Session locked: owner changed");
|
|
1469
1488
|
}
|
|
1470
1489
|
const quarantine = `${lockPath}.stale-${(0, import_node_crypto4.randomUUID)()}`;
|
|
1471
1490
|
import_node_fs3.default.renameSync(lockPath, quarantine);
|
|
@@ -1485,7 +1504,8 @@ var SessionLock = class _SessionLock {
|
|
|
1485
1504
|
}
|
|
1486
1505
|
if (installError.code === "EEXIST") {
|
|
1487
1506
|
const winner = _SessionLock.readOwner(lockPath);
|
|
1488
|
-
throw
|
|
1507
|
+
throw createSessionError(
|
|
1508
|
+
"SESSION_LOCKED",
|
|
1489
1509
|
`Session locked by a live or unknown owner${winner ? ` (pid=${winner.pid})` : ""}`
|
|
1490
1510
|
);
|
|
1491
1511
|
}
|
|
@@ -1519,7 +1539,7 @@ var SessionLock = class _SessionLock {
|
|
|
1519
1539
|
if (this.released) return;
|
|
1520
1540
|
const current = _SessionLock.readOwner(this.lockPath);
|
|
1521
1541
|
if (current?.nonce !== this.owner.nonce || current.pid !== this.owner.pid)
|
|
1522
|
-
throw
|
|
1542
|
+
throw createSessionError("SESSION_LOCK_OWNER_CHANGED", "Session lock owner changed");
|
|
1523
1543
|
import_node_fs3.default.unlinkSync(import_node_path2.default.join(this.lockPath, "owner.json"));
|
|
1524
1544
|
import_node_fs3.default.rmdirSync(this.lockPath);
|
|
1525
1545
|
this.released = true;
|
|
@@ -1550,7 +1570,11 @@ var SessionLock = class _SessionLock {
|
|
|
1550
1570
|
/** 无残留或残留进程已死亡时放行;确认存活的回收进行中则明确失败。 */
|
|
1551
1571
|
static ensureNoLiveRecovery(recovery) {
|
|
1552
1572
|
_SessionLock.clearAbandonedRecovery(recovery);
|
|
1553
|
-
if (import_node_fs3.default.existsSync(recovery))
|
|
1573
|
+
if (import_node_fs3.default.existsSync(recovery))
|
|
1574
|
+
throw createSessionError(
|
|
1575
|
+
"SESSION_RECOVERY_IN_PROGRESS",
|
|
1576
|
+
"Session locked: recovery in progress"
|
|
1577
|
+
);
|
|
1554
1578
|
}
|
|
1555
1579
|
/** 仅 ESRCH 表示确定死亡;PID 复用或 EPERM 均保持原锁。 */
|
|
1556
1580
|
static isDead(pid) {
|
|
@@ -1777,30 +1801,36 @@ var SessionManager = class {
|
|
|
1777
1801
|
throw error;
|
|
1778
1802
|
}
|
|
1779
1803
|
}
|
|
1780
|
-
/**
|
|
1804
|
+
/**
|
|
1805
|
+
* 按正文活动排序分页;有效 sidecar 只读元数据,不读取完整 JSONL。默认排除已归档会话。
|
|
1806
|
+
* pageSize 由调用方显式给出时必须在 1–100 之间;非法页参数直接失败,不静默修正。
|
|
1807
|
+
*/
|
|
1781
1808
|
async list(page = 1, options) {
|
|
1782
|
-
|
|
1783
|
-
|
|
1809
|
+
const pageSize = options?.pageSize ?? 20;
|
|
1810
|
+
if (!Number.isSafeInteger(page) || page < 1 || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100)
|
|
1811
|
+
throw createSessionError("HISTORY_PAGE_INVALID", "History page must be a positive integer");
|
|
1784
1812
|
const all = await this.allMetadata();
|
|
1785
1813
|
const filtered = options?.includeArchived ? all : all.filter((m) => !m.archived);
|
|
1786
1814
|
return {
|
|
1787
|
-
items: filtered.slice((page - 1) *
|
|
1815
|
+
items: filtered.slice((page - 1) * pageSize, page * pageSize),
|
|
1788
1816
|
total: filtered.length,
|
|
1789
1817
|
page,
|
|
1790
|
-
pages: Math.max(1, Math.ceil(filtered.length /
|
|
1818
|
+
pages: Math.max(1, Math.ceil(filtered.length / pageSize))
|
|
1791
1819
|
};
|
|
1792
1820
|
}
|
|
1793
1821
|
/** 解析完整 ID 或唯一前缀对应的会话文件与 ID。 */
|
|
1794
1822
|
resolveSession(id) {
|
|
1795
|
-
if (!id || !/^[a-zA-Z0-9-]+$/.test(id))
|
|
1823
|
+
if (!id || !/^[a-zA-Z0-9-]+$/.test(id))
|
|
1824
|
+
throw createSessionError("SESSION_NOT_FOUND", "Session not found");
|
|
1796
1825
|
const all = import_node_fs5.default.readdirSync(this.directory).filter((name) => /^\d+_[a-zA-Z0-9-]+\.jsonl$/.test(name)).map((fileName) => ({
|
|
1797
1826
|
fileName,
|
|
1798
1827
|
conversationId: fileName.slice(fileName.indexOf("_") + 1, -6)
|
|
1799
1828
|
}));
|
|
1800
1829
|
const exact = all.find((m) => m.conversationId === id || m.fileName === id);
|
|
1801
1830
|
const matches = exact ? [exact] : all.filter((m) => m.conversationId.startsWith(id));
|
|
1802
|
-
if (!matches.length) throw
|
|
1803
|
-
if (matches.length !== 1)
|
|
1831
|
+
if (!matches.length) throw createSessionError("SESSION_NOT_FOUND", `Session not found: ${id}`);
|
|
1832
|
+
if (matches.length !== 1)
|
|
1833
|
+
throw createSessionError("SESSION_ID_AMBIGUOUS", `Ambiguous session prefix: ${id}`);
|
|
1804
1834
|
return matches[0];
|
|
1805
1835
|
}
|
|
1806
1836
|
/**
|
|
@@ -2059,13 +2089,18 @@ var SessionManager = class {
|
|
|
2059
2089
|
}));
|
|
2060
2090
|
const exact = all.find((m) => m.conversationId === idOrFileName);
|
|
2061
2091
|
const matches = exact ? [exact] : all.filter((m) => m.conversationId.startsWith(idOrFileName));
|
|
2062
|
-
if (!matches.length)
|
|
2063
|
-
|
|
2092
|
+
if (!matches.length)
|
|
2093
|
+
throw createSessionError("SESSION_NOT_FOUND", `Session not found: ${idOrFileName}`);
|
|
2094
|
+
if (matches.length !== 1)
|
|
2095
|
+
throw createSessionError(
|
|
2096
|
+
"SESSION_ID_AMBIGUOUS",
|
|
2097
|
+
`Ambiguous session prefix: ${idOrFileName}`
|
|
2098
|
+
);
|
|
2064
2099
|
sourceFileName = matches[0].fileName;
|
|
2065
2100
|
}
|
|
2066
2101
|
const sourcePath = import_node_path4.default.join(this.directory, sourceFileName);
|
|
2067
2102
|
if (!import_node_fs5.default.existsSync(sourcePath)) {
|
|
2068
|
-
throw
|
|
2103
|
+
throw createSessionError("SESSION_NOT_FOUND", `Session not found: ${idOrFileName}`);
|
|
2069
2104
|
}
|
|
2070
2105
|
const nativeSource = toNativePhysicalPath(sourcePath);
|
|
2071
2106
|
const beforeStat = import_node_fs5.default.statSync(nativeSource);
|
|
@@ -3434,7 +3469,6 @@ var WorkspaceLeaseCoordinator = class {
|
|
|
3434
3469
|
}
|
|
3435
3470
|
}
|
|
3436
3471
|
};
|
|
3437
|
-
var sharedWorkspaceLeaseCoordinator = new WorkspaceLeaseCoordinator();
|
|
3438
3472
|
|
|
3439
3473
|
// src/context/manager.ts
|
|
3440
3474
|
var import_node_crypto10 = require("crypto");
|
|
@@ -8113,6 +8147,7 @@ var ToolExecutor = class {
|
|
|
8113
8147
|
this.signal?.addEventListener("abort", abortFromSession, { once: true });
|
|
8114
8148
|
}
|
|
8115
8149
|
const toolCtx = {
|
|
8150
|
+
leaseCoordinator: this.leaseCoordinator,
|
|
8116
8151
|
workspaceRoot: this.workspaceRoot,
|
|
8117
8152
|
executionCwd: this.executionCwd,
|
|
8118
8153
|
sandbox: this.sandbox,
|
|
@@ -10835,6 +10870,7 @@ var SessionInbox = class _SessionInbox {
|
|
|
10835
10870
|
|
|
10836
10871
|
// src/runtime/agent/runtime.ts
|
|
10837
10872
|
var AgentRuntime = class {
|
|
10873
|
+
runnerContexts = /* @__PURE__ */ new Map();
|
|
10838
10874
|
tasks = /* @__PURE__ */ new Map();
|
|
10839
10875
|
controllers = /* @__PURE__ */ new Map();
|
|
10840
10876
|
timers = /* @__PURE__ */ new Map();
|
|
@@ -10918,6 +10954,13 @@ var AgentRuntime = class {
|
|
|
10918
10954
|
* - 登记为 preparing 状态并返回 accepted 回执,使父工具事务能立刻闭合。
|
|
10919
10955
|
*/
|
|
10920
10956
|
async prepareSpawn(context, params) {
|
|
10957
|
+
const coordinator = context.leaseCoordinator;
|
|
10958
|
+
if (!coordinator || typeof coordinator !== "object" || typeof coordinator.isLocked !== "function" || typeof coordinator.getActiveLease !== "function" || typeof coordinator.tryAcquire !== "function" || typeof coordinator.acquire !== "function") {
|
|
10959
|
+
throw new KapibalaError("Spawn requires the parent workspace lease coordinator", {
|
|
10960
|
+
code: "CONFIG_INVALID",
|
|
10961
|
+
safeMessage: "\u5FC5\u987B\u663E\u5F0F\u4F20\u5165\u7236\u4F1A\u8BDD\u5199\u79DF\u7EA6\u534F\u8C03\u5668"
|
|
10962
|
+
});
|
|
10963
|
+
}
|
|
10921
10964
|
if (this.isClosed) {
|
|
10922
10965
|
throw new KapibalaError("AgentRuntime is closed", {
|
|
10923
10966
|
code: "CONFIG_INVALID",
|
|
@@ -11060,6 +11103,7 @@ var AgentRuntime = class {
|
|
|
11060
11103
|
timeoutMs
|
|
11061
11104
|
})
|
|
11062
11105
|
};
|
|
11106
|
+
this.runnerContexts.set(agentId, Object.freeze({ leaseCoordinator: context.leaseCoordinator }));
|
|
11063
11107
|
this.tasks.set(agentId, taskRecord);
|
|
11064
11108
|
this.totalActiveCount++;
|
|
11065
11109
|
this.treeActiveCounts.set(rootTaskId, treeActive + 1);
|
|
@@ -11112,6 +11156,7 @@ var AgentRuntime = class {
|
|
|
11112
11156
|
const task = this.tasks.get(agentId);
|
|
11113
11157
|
if (!task) return;
|
|
11114
11158
|
if (task.status === "preparing") {
|
|
11159
|
+
this.runnerContexts.delete(agentId);
|
|
11115
11160
|
task.status = "cancelled";
|
|
11116
11161
|
task.error = "Parent transaction aborted before commit";
|
|
11117
11162
|
task.finishedAt = Date.now();
|
|
@@ -11137,7 +11182,10 @@ var AgentRuntime = class {
|
|
|
11137
11182
|
this.timers.set(task.agentId, timer);
|
|
11138
11183
|
let runError;
|
|
11139
11184
|
try {
|
|
11140
|
-
const
|
|
11185
|
+
const context = this.runnerContexts.get(task.agentId);
|
|
11186
|
+
if (!context)
|
|
11187
|
+
throw new KapibalaError("Runner context is unavailable", { code: "CONFIG_INVALID" });
|
|
11188
|
+
const result = await this.runner(task, controller.signal, context);
|
|
11141
11189
|
if (task.status === "cancelling") {
|
|
11142
11190
|
task.status = "cancelled";
|
|
11143
11191
|
return;
|
|
@@ -11188,6 +11236,7 @@ var AgentRuntime = class {
|
|
|
11188
11236
|
task.error = runError.message;
|
|
11189
11237
|
await this.cascadeCancelChildren(task.agentId, "Parent task failed");
|
|
11190
11238
|
} finally {
|
|
11239
|
+
this.runnerContexts.delete(task.agentId);
|
|
11191
11240
|
this.clearTaskTimer(task.agentId);
|
|
11192
11241
|
this.controllers.delete(task.agentId);
|
|
11193
11242
|
if (!isTerminalTaskStatus(task.status)) {
|
|
@@ -11263,6 +11312,7 @@ var AgentRuntime = class {
|
|
|
11263
11312
|
controller?.abort(new Error(reason));
|
|
11264
11313
|
await this.cascadeCancelChildren(agentId, reason);
|
|
11265
11314
|
const inFlight = this.runningPromises.get(agentId);
|
|
11315
|
+
if (!inFlight) this.runnerContexts.delete(agentId);
|
|
11266
11316
|
if (inFlight) {
|
|
11267
11317
|
const GRACE_PERIOD_MS = 5e3;
|
|
11268
11318
|
let timer;
|
|
@@ -11371,6 +11421,7 @@ var AgentRuntime = class {
|
|
|
11371
11421
|
}
|
|
11372
11422
|
this.timers.clear();
|
|
11373
11423
|
this.controllers.clear();
|
|
11424
|
+
this.runnerContexts.clear();
|
|
11374
11425
|
this.stateListeners.clear();
|
|
11375
11426
|
}
|
|
11376
11427
|
/**
|
|
@@ -11511,9 +11562,15 @@ function createSpawnAgentTool(runtime) {
|
|
|
11511
11562
|
required: ["description", "prompt", "role"]
|
|
11512
11563
|
},
|
|
11513
11564
|
async execute(input, ctx) {
|
|
11565
|
+
if (!ctx.leaseCoordinator)
|
|
11566
|
+
throw new KapibalaError("Missing parent lease coordinator", {
|
|
11567
|
+
code: "CONFIG_INVALID",
|
|
11568
|
+
safeMessage: "\u7F3A\u5C11\u7236\u4F1A\u8BDD\u5199\u79DF\u7EA6\u534F\u8C03\u5668"
|
|
11569
|
+
});
|
|
11514
11570
|
const workspaceRoot = ctx.workspaceRoot ?? ctx.rootDir;
|
|
11515
11571
|
const executionCwd = ctx.executionCwd ?? workspaceRoot;
|
|
11516
11572
|
const spawnContext = {
|
|
11573
|
+
leaseCoordinator: ctx.leaseCoordinator,
|
|
11517
11574
|
rootTaskId: ctx.rootTaskId,
|
|
11518
11575
|
sessionId: ctx.sessionId ?? "session-default",
|
|
11519
11576
|
branchId: ctx.branchId ?? "main",
|
|
@@ -11876,7 +11933,7 @@ var AgentSession = class {
|
|
|
11876
11933
|
this.cwd = resolvedWorkspace.executionCwd;
|
|
11877
11934
|
this.projectKey = resolvedWorkspace.projectKey;
|
|
11878
11935
|
this.sandbox = config.sandbox ?? new PathSandbox({ workspaceRoot: this.workspaceRoot });
|
|
11879
|
-
this.leaseCoordinator = config.leaseCoordinator ??
|
|
11936
|
+
this.leaseCoordinator = config.leaseCoordinator ?? new WorkspaceLeaseCoordinator();
|
|
11880
11937
|
this.tools = new ToolRegistry();
|
|
11881
11938
|
if (config.skills) {
|
|
11882
11939
|
this.skillRegistry = new SkillRegistry(config.skills);
|
|
@@ -13205,6 +13262,7 @@ var AgentSession = class {
|
|
|
13205
13262
|
createLogId,
|
|
13206
13263
|
createMessageId,
|
|
13207
13264
|
createRunCommandTool,
|
|
13265
|
+
createSessionError,
|
|
13208
13266
|
createSpawnAgentTool,
|
|
13209
13267
|
createWaitAgentTool,
|
|
13210
13268
|
defineTool,
|
|
@@ -13260,7 +13318,6 @@ var AgentSession = class {
|
|
|
13260
13318
|
sanitizeModelErrorMessage,
|
|
13261
13319
|
sanitizeSessionTitle,
|
|
13262
13320
|
scanSkillRoot,
|
|
13263
|
-
sharedWorkspaceLeaseCoordinator,
|
|
13264
13321
|
skillDigest,
|
|
13265
13322
|
skillFailure,
|
|
13266
13323
|
skillPathKey,
|