@oyasmi/pipiclaw 0.5.9 → 0.6.1
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/agent/channel-runner.d.ts +8 -0
- package/dist/agent/channel-runner.js +132 -24
- package/dist/agent/context-budget.d.ts +9 -0
- package/dist/agent/context-budget.js +31 -0
- package/dist/agent/session-events.js +11 -4
- package/dist/agent/type-guards.js +4 -2
- package/dist/agent/types.d.ts +10 -3
- package/dist/agent/types.js +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/memory/candidates.d.ts +8 -5
- package/dist/memory/candidates.js +92 -42
- package/dist/memory/consolidation.js +13 -4
- package/dist/memory/recall.d.ts +2 -2
- package/dist/memory/recall.js +2 -3
- package/dist/memory/session.js +2 -2
- package/dist/memory/sidecar-worker.d.ts +1 -0
- package/dist/memory/sidecar-worker.js +56 -1
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +1 -0
- package/dist/runtime/bootstrap.d.ts +1 -0
- package/dist/runtime/bootstrap.js +52 -13
- package/dist/runtime/delivery.js +101 -12
- package/dist/runtime/dingtalk.d.ts +11 -1
- package/dist/runtime/dingtalk.js +69 -24
- package/dist/runtime/events.d.ts +17 -2
- package/dist/runtime/events.js +107 -19
- package/dist/security/command-guard.js +4 -0
- package/dist/security/config.d.ts +6 -0
- package/dist/security/config.js +38 -6
- package/dist/security/path-guard.js +4 -0
- package/dist/security/platform.d.ts +1 -0
- package/dist/security/platform.js +3 -0
- package/dist/settings.d.ts +4 -1
- package/dist/settings.js +31 -6
- package/dist/shared/config-diagnostics.d.ts +7 -0
- package/dist/shared/config-diagnostics.js +3 -0
- package/dist/subagents/tool.d.ts +2 -0
- package/dist/subagents/tool.js +2 -3
- package/dist/tools/config.d.ts +7 -0
- package/dist/tools/config.js +63 -7
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.js +3 -2
- package/dist/web/client.d.ts +1 -0
- package/dist/web/client.js +30 -18
- package/dist/web/config.d.ts +1 -0
- package/dist/web/config.js +1 -0
- package/dist/web/fetch.d.ts +1 -0
- package/dist/web/fetch.js +7 -5
- package/dist/web/search-providers.js +6 -3
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import { type SandboxConfig } from "../sandbox.js";
|
|
|
4
4
|
import { type BuiltInCommand } from "./commands.js";
|
|
5
5
|
import { type AgentRunner } from "./types.js";
|
|
6
6
|
export declare class ChannelRunner implements AgentRunner {
|
|
7
|
+
private readonly executor;
|
|
7
8
|
private readonly sandboxConfig;
|
|
8
9
|
private readonly channelId;
|
|
9
10
|
private readonly channelDir;
|
|
@@ -15,6 +16,7 @@ export declare class ChannelRunner implements AgentRunner {
|
|
|
15
16
|
private readonly settingsManager;
|
|
16
17
|
private readonly modelRegistry;
|
|
17
18
|
private readonly memoryLifecycle;
|
|
19
|
+
private readonly memoryCandidateStore;
|
|
18
20
|
private readonly sessionResourceGate;
|
|
19
21
|
private readonly sessionReady;
|
|
20
22
|
private subAgentDiscovery;
|
|
@@ -41,8 +43,14 @@ export declare class ChannelRunner implements AgentRunner {
|
|
|
41
43
|
private refreshSessionResources;
|
|
42
44
|
private initializeSession;
|
|
43
45
|
private reloadSessionResources;
|
|
46
|
+
private bindSessionExtensions;
|
|
44
47
|
private ensureSessionReady;
|
|
48
|
+
private maybeRunPreventiveCompactionForIncomingText;
|
|
45
49
|
private refreshSubAgentDiscovery;
|
|
50
|
+
private reportSettingsDiagnostics;
|
|
51
|
+
private reportConfigDiagnostics;
|
|
52
|
+
private buildRuntimeTools;
|
|
53
|
+
private rebuildSessionTools;
|
|
46
54
|
private subscribeToSessionEvents;
|
|
47
55
|
private buildFirstTurnMemoryBootstrap;
|
|
48
56
|
}
|
|
@@ -4,7 +4,7 @@ import { mkdir, readFile, writeFile } from "fs/promises";
|
|
|
4
4
|
import { dirname, join, resolve } from "path";
|
|
5
5
|
import * as log from "../log.js";
|
|
6
6
|
import { buildFirstTurnMemoryBootstrap as renderFirstTurnMemoryBootstrap } from "../memory/bootstrap.js";
|
|
7
|
-
import {
|
|
7
|
+
import { createMemoryCandidateStore } from "../memory/candidates.js";
|
|
8
8
|
import { getChannelMemoryPath } from "../memory/files.js";
|
|
9
9
|
import { MemoryLifecycle } from "../memory/lifecycle.js";
|
|
10
10
|
import { recallRelevantMemory } from "../memory/recall.js";
|
|
@@ -12,13 +12,17 @@ import { getApiKeyForModel } from "../models/api-keys.js";
|
|
|
12
12
|
import { resolveInitialModel } from "../models/utils.js";
|
|
13
13
|
import { APP_HOME_DIR, AUTH_CONFIG_PATH, MODELS_CONFIG_PATH } from "../paths.js";
|
|
14
14
|
import { createExecutor } from "../sandbox.js";
|
|
15
|
+
import { loadSecurityConfigWithDiagnostics } from "../security/config.js";
|
|
15
16
|
import { PipiclawSettingsManager } from "../settings.js";
|
|
17
|
+
import { formatConfigDiagnostic } from "../shared/config-diagnostics.js";
|
|
16
18
|
import { HAN_REGEX } from "../shared/text-utils.js";
|
|
17
19
|
import { isRecord } from "../shared/type-guards.js";
|
|
18
20
|
import { discoverSubAgents, formatSubAgentList } from "../subagents/discovery.js";
|
|
21
|
+
import { loadToolsConfigWithDiagnostics } from "../tools/config.js";
|
|
19
22
|
import { createPipiclawTools } from "../tools/index.js";
|
|
20
23
|
import { createCommandExtension } from "./command-extension.js";
|
|
21
24
|
import { renderBuiltInHelp } from "./commands.js";
|
|
25
|
+
import { estimateIncomingMessageTokens, getPreventiveCompactionDecision } from "./context-budget.js";
|
|
22
26
|
import { clipUserInput } from "./progress-formatter.js";
|
|
23
27
|
import { buildAppendSystemPrompt } from "./prompt-builder.js";
|
|
24
28
|
import { createRunQueue } from "./run-queue.js";
|
|
@@ -54,6 +58,7 @@ export class ChannelRunner {
|
|
|
54
58
|
this.channelId = channelId;
|
|
55
59
|
this.channelDir = channelDir;
|
|
56
60
|
const executor = createExecutor(sandboxConfig);
|
|
61
|
+
this.executor = executor;
|
|
57
62
|
this.workspaceDir = resolve(dirname(channelDir));
|
|
58
63
|
this.workspacePath = executor.getWorkspacePath(this.workspaceDir);
|
|
59
64
|
// Initial skill summaries
|
|
@@ -63,6 +68,8 @@ export class ChannelRunner {
|
|
|
63
68
|
const contextFile = join(channelDir, "context.jsonl");
|
|
64
69
|
this.sessionManager = SessionManager.open(contextFile, channelDir);
|
|
65
70
|
this.settingsManager = new PipiclawSettingsManager(APP_HOME_DIR);
|
|
71
|
+
this.reportSettingsDiagnostics();
|
|
72
|
+
this.memoryCandidateStore = createMemoryCandidateStore();
|
|
66
73
|
// Create AuthStorage and ModelRegistry
|
|
67
74
|
const authStorage = AuthStorage.create(AUTH_CONFIG_PATH);
|
|
68
75
|
this.modelRegistry = createModelRegistry(authStorage, MODELS_CONFIG_PATH);
|
|
@@ -71,19 +78,7 @@ export class ChannelRunner {
|
|
|
71
78
|
log.logInfo(`Using model: ${this.activeModel.provider}/${this.activeModel.id} (${this.activeModel.name})`);
|
|
72
79
|
this.subAgentDiscovery = this.refreshSubAgentDiscovery();
|
|
73
80
|
// Create tools
|
|
74
|
-
const tools =
|
|
75
|
-
executor,
|
|
76
|
-
getCurrentModel: () => this.activeModel,
|
|
77
|
-
getAvailableModels: () => this.modelRegistry.getAvailable(),
|
|
78
|
-
resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
|
|
79
|
-
workspaceDir: this.workspaceDir,
|
|
80
|
-
channelDir: this.channelDir,
|
|
81
|
-
workspacePath: this.workspacePath,
|
|
82
|
-
channelId: this.channelId,
|
|
83
|
-
sandboxConfig: this.sandboxConfig,
|
|
84
|
-
getSubAgentDiscovery: () => this.subAgentDiscovery,
|
|
85
|
-
getMemoryRecallSettings: () => this.settingsManager.getMemoryRecallSettings(),
|
|
86
|
-
});
|
|
81
|
+
const tools = this.buildRuntimeTools();
|
|
87
82
|
// Create agent
|
|
88
83
|
this.agent = new Agent({
|
|
89
84
|
initialState: {
|
|
@@ -174,16 +169,17 @@ export class ChannelRunner {
|
|
|
174
169
|
this.runState.queue = runQueue.queue;
|
|
175
170
|
try {
|
|
176
171
|
await this.ensureSessionReady();
|
|
177
|
-
|
|
178
|
-
await mkdir(this.channelDir, { recursive: true });
|
|
179
|
-
const candidateCache = createMemoryCandidateCache();
|
|
172
|
+
this.memoryLifecycle.noteUserTurnStarted();
|
|
180
173
|
const clippedInput = clipUserInput(ctx.message.text, MAX_USER_MESSAGE_CHARS);
|
|
181
174
|
const userMessage = this.formatUserMessage(clippedInput, ctx.message.userName);
|
|
182
|
-
|
|
175
|
+
const preserveRawInput = this.shouldPreserveRawInput(ctx.message.text);
|
|
176
|
+
await this.maybeRunPreventiveCompactionForIncomingText(preserveRawInput ? clippedInput : userMessage);
|
|
177
|
+
// Ensure channel directory exists
|
|
178
|
+
await mkdir(this.channelDir, { recursive: true });
|
|
179
|
+
let promptText = preserveRawInput ? clippedInput : userMessage;
|
|
183
180
|
let recalledContextText = "";
|
|
184
181
|
let durableMemoryBootstrapText = "";
|
|
185
|
-
|
|
186
|
-
if (!this.shouldPreserveRawInput(ctx.message.text)) {
|
|
182
|
+
if (!preserveRawInput) {
|
|
187
183
|
const recallSettings = this.settingsManager.getMemoryRecallSettings();
|
|
188
184
|
if (recallSettings.enabled) {
|
|
189
185
|
const recall = await recallRelevantMemory({
|
|
@@ -197,7 +193,7 @@ export class ChannelRunner {
|
|
|
197
193
|
autoRerank: HAN_REGEX.test(clippedInput),
|
|
198
194
|
model: this.session.model ?? this.activeModel,
|
|
199
195
|
resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
|
|
200
|
-
|
|
196
|
+
candidateStore: this.memoryCandidateStore,
|
|
201
197
|
});
|
|
202
198
|
if (recall.renderedText) {
|
|
203
199
|
recalledContextText = recall.renderedText;
|
|
@@ -241,7 +237,20 @@ export class ChannelRunner {
|
|
|
241
237
|
this.runState.errorMessage &&
|
|
242
238
|
!this.runState.finalResponseDelivered) {
|
|
243
239
|
try {
|
|
244
|
-
|
|
240
|
+
const baseErrorSummary = this.runState.errorMessage.length > 240
|
|
241
|
+
? `${this.runState.errorMessage.slice(0, 237)}...`
|
|
242
|
+
: this.runState.errorMessage;
|
|
243
|
+
const compactionSummary = this.runState.lastCompactionError &&
|
|
244
|
+
this.runState.lastCompactionError !== this.runState.errorMessage
|
|
245
|
+
? this.runState.lastCompactionError.length > 240
|
|
246
|
+
? `${this.runState.lastCompactionError.slice(0, 237)}...`
|
|
247
|
+
: this.runState.lastCompactionError
|
|
248
|
+
: undefined;
|
|
249
|
+
const detailLines = [`\`${baseErrorSummary}\``];
|
|
250
|
+
if (compactionSummary) {
|
|
251
|
+
detailLines.push(`Recovery: \`${compactionSummary}\``);
|
|
252
|
+
}
|
|
253
|
+
await ctx.replaceMessage(`_Sorry, something went wrong._\n\n${detailLines.join("\n\n")}`);
|
|
245
254
|
}
|
|
246
255
|
catch (err) {
|
|
247
256
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -258,6 +267,16 @@ export class ChannelRunner {
|
|
|
258
267
|
log.logWarning("Failed to delete message for silent response", errMsg);
|
|
259
268
|
}
|
|
260
269
|
}
|
|
270
|
+
else if (this.runState.stopReason === "aborted" && !this.runState.finalResponseDelivered) {
|
|
271
|
+
try {
|
|
272
|
+
await ctx.deleteMessage();
|
|
273
|
+
log.logInfo("Aborted response - discarded active delivery state");
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
277
|
+
log.logWarning("Failed to discard active delivery state after abort", errMsg);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
261
280
|
else if (finalOutcomeText && !this.runState.finalResponseDelivered) {
|
|
262
281
|
try {
|
|
263
282
|
await ctx.replaceMessage(finalOutcomeText);
|
|
@@ -364,8 +383,10 @@ export class ChannelRunner {
|
|
|
364
383
|
if (clippedText !== text.trim()) {
|
|
365
384
|
log.logWarning(`[${this.channelId}] Queued message exceeded ${MAX_USER_MESSAGE_CHARS} chars and was clipped`);
|
|
366
385
|
}
|
|
386
|
+
const queuedMessage = this.formatUserMessage(clippedText, userName);
|
|
387
|
+
await this.maybeRunPreventiveCompactionForIncomingText(queuedMessage);
|
|
367
388
|
await this.sessionResourceGate.runPrompt(async () => {
|
|
368
|
-
await this.session.prompt(
|
|
389
|
+
await this.session.prompt(queuedMessage, {
|
|
369
390
|
streamingBehavior: delivery,
|
|
370
391
|
});
|
|
371
392
|
});
|
|
@@ -386,17 +407,67 @@ export class ChannelRunner {
|
|
|
386
407
|
}
|
|
387
408
|
async initializeSession() {
|
|
388
409
|
await this.reloadSessionResources();
|
|
410
|
+
await this.bindSessionExtensions();
|
|
389
411
|
}
|
|
390
412
|
async reloadSessionResources() {
|
|
413
|
+
this.settingsManager.reload();
|
|
414
|
+
this.reportSettingsDiagnostics();
|
|
391
415
|
const skills = loadPipiclawSkills(this.channelDir, this.workspacePath);
|
|
392
416
|
this.currentSkills = skills;
|
|
393
417
|
this.subAgentDiscovery = this.refreshSubAgentDiscovery();
|
|
394
|
-
this.
|
|
418
|
+
this.rebuildSessionTools();
|
|
395
419
|
await this.session.reload();
|
|
396
420
|
}
|
|
421
|
+
async bindSessionExtensions() {
|
|
422
|
+
await this.session.bindExtensions({
|
|
423
|
+
commandContextActions: {
|
|
424
|
+
waitForIdle: () => this.session.agent.waitForIdle(),
|
|
425
|
+
newSession: async (options) => {
|
|
426
|
+
const success = await this.session.newSession(options);
|
|
427
|
+
return { cancelled: !success };
|
|
428
|
+
},
|
|
429
|
+
fork: async (entryId) => {
|
|
430
|
+
const result = await this.session.fork(entryId);
|
|
431
|
+
return { cancelled: result.cancelled };
|
|
432
|
+
},
|
|
433
|
+
navigateTree: async (targetId, options) => {
|
|
434
|
+
const result = await this.session.navigateTree(targetId, options);
|
|
435
|
+
return { cancelled: result.cancelled };
|
|
436
|
+
},
|
|
437
|
+
switchSession: async (sessionPath) => {
|
|
438
|
+
const success = await this.session.switchSession(sessionPath);
|
|
439
|
+
return { cancelled: !success };
|
|
440
|
+
},
|
|
441
|
+
reload: async () => {
|
|
442
|
+
await this.refreshSessionResources();
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
}
|
|
397
447
|
async ensureSessionReady() {
|
|
398
448
|
await this.sessionReady;
|
|
399
449
|
}
|
|
450
|
+
async maybeRunPreventiveCompactionForIncomingText(incomingText) {
|
|
451
|
+
const currentModel = this.session.model ?? this.activeModel;
|
|
452
|
+
const contextUsage = this.session.getContextUsage();
|
|
453
|
+
const contextTokens = contextUsage?.tokens;
|
|
454
|
+
const incomingTokens = estimateIncomingMessageTokens(incomingText);
|
|
455
|
+
const decision = getPreventiveCompactionDecision(contextTokens, incomingTokens, currentModel.contextWindow);
|
|
456
|
+
if (!decision.shouldCompact) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const currentTokens = contextTokens ?? 0;
|
|
460
|
+
const startedAt = Date.now();
|
|
461
|
+
log.logInfo(`[${this.channelId}] Preventive compaction triggered: projected ${decision.projectedTokens}/${currentModel.contextWindow} tokens (current=${currentTokens}, incoming≈${incomingTokens}), threshold=${decision.thresholdTokens}`);
|
|
462
|
+
try {
|
|
463
|
+
await this.session.compact();
|
|
464
|
+
log.logInfo(`[${this.channelId}] Preventive compaction complete in ${Date.now() - startedAt}ms`);
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
468
|
+
log.logWarning(`[${this.channelId}] Preventive compaction failed`, message);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
400
471
|
refreshSubAgentDiscovery() {
|
|
401
472
|
this.modelRegistry.refresh();
|
|
402
473
|
const discovery = discoverSubAgents(this.workspaceDir, this.modelRegistry.getAvailable());
|
|
@@ -405,6 +476,43 @@ export class ChannelRunner {
|
|
|
405
476
|
}
|
|
406
477
|
return discovery;
|
|
407
478
|
}
|
|
479
|
+
reportSettingsDiagnostics() {
|
|
480
|
+
for (const { scope, error } of this.settingsManager.drainErrors()) {
|
|
481
|
+
log.logWarning(`[${this.channelId}] Failed to load ${scope} settings`, `${error.message}\n${join(APP_HOME_DIR, "settings.json")}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
reportConfigDiagnostics(diagnostics) {
|
|
485
|
+
for (const diagnostic of diagnostics) {
|
|
486
|
+
log.logWarning(`[${this.channelId}] ${formatConfigDiagnostic(diagnostic)}`, diagnostic.path);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
buildRuntimeTools() {
|
|
490
|
+
const securityLoad = loadSecurityConfigWithDiagnostics(APP_HOME_DIR);
|
|
491
|
+
const toolsLoad = loadToolsConfigWithDiagnostics(APP_HOME_DIR);
|
|
492
|
+
this.reportConfigDiagnostics([...securityLoad.diagnostics, ...toolsLoad.diagnostics]);
|
|
493
|
+
return createPipiclawTools({
|
|
494
|
+
executor: this.executor,
|
|
495
|
+
getCurrentModel: () => this.activeModel,
|
|
496
|
+
getAvailableModels: () => this.modelRegistry.getAvailable(),
|
|
497
|
+
resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
|
|
498
|
+
workspaceDir: this.workspaceDir,
|
|
499
|
+
channelDir: this.channelDir,
|
|
500
|
+
workspacePath: this.workspacePath,
|
|
501
|
+
channelId: this.channelId,
|
|
502
|
+
sandboxConfig: this.sandboxConfig,
|
|
503
|
+
getSubAgentDiscovery: () => this.subAgentDiscovery,
|
|
504
|
+
getMemoryRecallSettings: () => this.settingsManager.getMemoryRecallSettings(),
|
|
505
|
+
memoryCandidateStore: this.memoryCandidateStore,
|
|
506
|
+
securityConfig: securityLoad.config,
|
|
507
|
+
toolsConfig: toolsLoad.config,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
rebuildSessionTools() {
|
|
511
|
+
const tools = this.buildRuntimeTools();
|
|
512
|
+
this.agent.setTools(tools);
|
|
513
|
+
this.session._baseToolsOverride =
|
|
514
|
+
Object.fromEntries(tools.map((tool) => [tool.name, tool]));
|
|
515
|
+
}
|
|
408
516
|
// === Session event subscription ===
|
|
409
517
|
subscribeToSessionEvents() {
|
|
410
518
|
this.session.subscribe(async (event) => {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const PREVENTIVE_COMPACTION_THRESHOLD_RATIO = 0.75;
|
|
2
|
+
export interface PreventiveCompactionDecision {
|
|
3
|
+
shouldCompact: boolean;
|
|
4
|
+
projectedTokens: number | null;
|
|
5
|
+
thresholdTokens: number;
|
|
6
|
+
ratio: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function estimateIncomingMessageTokens(text: string): number;
|
|
9
|
+
export declare function getPreventiveCompactionDecision(contextTokens: number | null | undefined, incomingTokens: number, contextWindow: number, thresholdRatio?: number): PreventiveCompactionDecision;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const PREVENTIVE_COMPACTION_THRESHOLD_RATIO = 0.75;
|
|
2
|
+
const ESTIMATED_CHARS_PER_TOKEN = 3;
|
|
3
|
+
export function estimateIncomingMessageTokens(text) {
|
|
4
|
+
if (!text) {
|
|
5
|
+
return 0;
|
|
6
|
+
}
|
|
7
|
+
return Math.ceil(text.length / ESTIMATED_CHARS_PER_TOKEN);
|
|
8
|
+
}
|
|
9
|
+
export function getPreventiveCompactionDecision(contextTokens, incomingTokens, contextWindow, thresholdRatio = PREVENTIVE_COMPACTION_THRESHOLD_RATIO) {
|
|
10
|
+
const normalizedContextWindow = Number.isFinite(contextWindow) ? Math.max(0, Math.floor(contextWindow)) : 0;
|
|
11
|
+
const normalizedIncomingTokens = Number.isFinite(incomingTokens) && incomingTokens > 0 ? Math.floor(incomingTokens) : 0;
|
|
12
|
+
const normalizedRatio = Number.isFinite(thresholdRatio) && thresholdRatio > 0
|
|
13
|
+
? Math.min(thresholdRatio, 1)
|
|
14
|
+
: PREVENTIVE_COMPACTION_THRESHOLD_RATIO;
|
|
15
|
+
const thresholdTokens = Math.floor(normalizedContextWindow * normalizedRatio);
|
|
16
|
+
if (contextTokens === null || contextTokens === undefined || !Number.isFinite(contextTokens) || contextTokens < 0) {
|
|
17
|
+
return {
|
|
18
|
+
shouldCompact: false,
|
|
19
|
+
projectedTokens: null,
|
|
20
|
+
thresholdTokens,
|
|
21
|
+
ratio: normalizedRatio,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const projectedTokens = Math.floor(contextTokens) + normalizedIncomingTokens;
|
|
25
|
+
return {
|
|
26
|
+
shouldCompact: normalizedContextWindow > 0 && projectedTokens >= thresholdTokens,
|
|
27
|
+
projectedTokens,
|
|
28
|
+
thresholdTokens,
|
|
29
|
+
ratio: normalizedRatio,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -195,16 +195,23 @@ export async function handleSessionEvent(event, context) {
|
|
|
195
195
|
return;
|
|
196
196
|
}
|
|
197
197
|
if (isAutoCompactionStartEvent(event)) {
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
const label = event.reason === "manual" ? "Compacting context..." : "Compacting context...";
|
|
199
|
+
log.logInfo(`Compaction started (reason: ${event.reason})`);
|
|
200
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("assistant", label), false), "compaction start");
|
|
200
201
|
return;
|
|
201
202
|
}
|
|
202
203
|
if (isAutoCompactionEndEvent(event)) {
|
|
203
204
|
if (event.result) {
|
|
204
|
-
|
|
205
|
+
runState.lastCompactionError = undefined;
|
|
206
|
+
log.logInfo(`Compaction complete: ${event.result.tokensBefore} tokens compacted`);
|
|
205
207
|
}
|
|
206
208
|
else if (event.aborted) {
|
|
207
|
-
log.logInfo("
|
|
209
|
+
log.logInfo("Compaction aborted");
|
|
210
|
+
}
|
|
211
|
+
else if (event.errorMessage) {
|
|
212
|
+
runState.lastCompactionError = event.errorMessage;
|
|
213
|
+
log.logWarning("Compaction failed", event.errorMessage);
|
|
214
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("error", truncate(event.errorMessage ?? "Compaction failed", 200)), false), "compaction error");
|
|
208
215
|
}
|
|
209
216
|
return;
|
|
210
217
|
}
|
|
@@ -65,10 +65,12 @@ export function isTurnEndEvent(value) {
|
|
|
65
65
|
return hasEventType(value, "turn_end") && "message" in value && Array.isArray(value.toolResults);
|
|
66
66
|
}
|
|
67
67
|
export function isAutoCompactionStartEvent(value) {
|
|
68
|
-
return hasEventType(value, "auto_compaction_start") && (value.reason === "threshold" || value.reason === "overflow")
|
|
68
|
+
return ((hasEventType(value, "auto_compaction_start") && (value.reason === "threshold" || value.reason === "overflow")) ||
|
|
69
|
+
(hasEventType(value, "compaction_start") &&
|
|
70
|
+
(value.reason === "threshold" || value.reason === "overflow" || value.reason === "manual")));
|
|
69
71
|
}
|
|
70
72
|
export function isAutoCompactionEndEvent(value) {
|
|
71
|
-
return hasEventType(value, "auto_compaction_end");
|
|
73
|
+
return hasEventType(value, "auto_compaction_end") || hasEventType(value, "compaction_end");
|
|
72
74
|
}
|
|
73
75
|
export function isAutoRetryStartEvent(value) {
|
|
74
76
|
return (hasEventType(value, "auto_retry_start") &&
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export interface RunState {
|
|
|
44
44
|
totalUsage: UsageTotals;
|
|
45
45
|
stopReason: string;
|
|
46
46
|
errorMessage: string | undefined;
|
|
47
|
+
lastCompactionError: string | undefined;
|
|
47
48
|
finalOutcome: FinalOutcome;
|
|
48
49
|
finalResponseDelivered: boolean;
|
|
49
50
|
}
|
|
@@ -118,11 +119,17 @@ export type SessionEvent = {
|
|
|
118
119
|
type: "auto_compaction_start";
|
|
119
120
|
reason: "threshold" | "overflow";
|
|
120
121
|
} | {
|
|
121
|
-
type: "
|
|
122
|
+
type: "compaction_start";
|
|
123
|
+
reason: "manual" | "threshold" | "overflow";
|
|
124
|
+
} | {
|
|
125
|
+
type: "auto_compaction_end" | "compaction_end";
|
|
126
|
+
reason?: "manual" | "threshold" | "overflow";
|
|
122
127
|
result?: {
|
|
123
128
|
tokensBefore: number;
|
|
124
129
|
};
|
|
125
130
|
aborted?: boolean;
|
|
131
|
+
errorMessage?: string;
|
|
132
|
+
willRetry?: boolean;
|
|
126
133
|
} | {
|
|
127
134
|
type: "auto_retry_start";
|
|
128
135
|
attempt: number;
|
|
@@ -149,10 +156,10 @@ export type TurnEndEvent = Extract<SessionEvent, {
|
|
|
149
156
|
type: "turn_end";
|
|
150
157
|
}>;
|
|
151
158
|
export type AutoCompactionStartEvent = Extract<SessionEvent, {
|
|
152
|
-
type: "auto_compaction_start";
|
|
159
|
+
type: "auto_compaction_start" | "compaction_start";
|
|
153
160
|
}>;
|
|
154
161
|
export type AutoCompactionEndEvent = Extract<SessionEvent, {
|
|
155
|
-
type: "auto_compaction_end";
|
|
162
|
+
type: "auto_compaction_end" | "compaction_end";
|
|
156
163
|
}>;
|
|
157
164
|
export type AutoRetryStartEvent = Extract<SessionEvent, {
|
|
158
165
|
type: "auto_retry_start";
|
package/dist/agent/types.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { type BuiltInCommand, type BuiltInCommandName, parseBuiltInCommand, rend
|
|
|
3
3
|
export { type AgentRunner, getOrCreateRunner } from "./agent/index.js";
|
|
4
4
|
export { type AppendSystemPromptOptions, buildAppendSystemPrompt } from "./agent/prompt-builder.js";
|
|
5
5
|
export { getAgentConfig, getSoul, loadPipiclawSkills, } from "./agent/workspace-resources.js";
|
|
6
|
-
export { type BuildMemoryCandidatesOptions, buildMemoryCandidates, type MemoryCandidate, } from "./memory/candidates.js";
|
|
6
|
+
export { type BuildMemoryCandidatesOptions, buildMemoryCandidates, createMemoryCandidateStore, type MemoryCandidate, type MemoryCandidateStore, } from "./memory/candidates.js";
|
|
7
7
|
export { type BackgroundMaintenanceResult, type ConsolidationRunOptions, type InlineConsolidationResult, runBackgroundMaintenance, runInlineConsolidation, } from "./memory/consolidation.js";
|
|
8
8
|
export { ensureChannelMemoryFiles, ensureChannelMemoryFilesSync, getChannelSessionPath, readChannelSession, rewriteChannelSession, } from "./memory/files.js";
|
|
9
9
|
export { type ConsolidationReason, MemoryLifecycle, type MemoryLifecycleOptions } from "./memory/lifecycle.js";
|
|
@@ -12,11 +12,11 @@ export { renderSessionMemory, type SessionMemoryState, type SessionMemoryUpdateO
|
|
|
12
12
|
export { runSidecarTask, type SidecarResult, type SidecarTask, } from "./memory/sidecar-worker.js";
|
|
13
13
|
export { getApiKeyForModel } from "./models/api-keys.js";
|
|
14
14
|
export { findExactModelReferenceMatch, findModelReferenceMatch, formatModelList, formatModelReference, resolveInitialModel, } from "./models/utils.js";
|
|
15
|
-
export { APP_HOME_DIR, APP_NAME, AUTH_CONFIG_PATH, CHANNEL_CONFIG_PATH, MODELS_CONFIG_PATH, SETTINGS_CONFIG_PATH, SUB_AGENTS_DIR, SUB_AGENTS_DIR_NAME, TOOLS_CONFIG_PATH, WORKSPACE_DIR, } from "./paths.js";
|
|
15
|
+
export { APP_HOME_DIR, APP_NAME, AUTH_CONFIG_PATH, CHANNEL_CONFIG_PATH, MODELS_CONFIG_PATH, SECURITY_CONFIG_PATH, SETTINGS_CONFIG_PATH, SUB_AGENTS_DIR, SUB_AGENTS_DIR_NAME, TOOLS_CONFIG_PATH, WORKSPACE_DIR, } from "./paths.js";
|
|
16
16
|
export { ensureChannelDir, getChannelDir, getChannelDirName, } from "./runtime/channel-paths.js";
|
|
17
17
|
export { createDingTalkContext } from "./runtime/delivery.js";
|
|
18
18
|
export { type BusyMessageMode, DingTalkBot, type DingTalkConfig, type DingTalkContext, type DingTalkEvent, type DingTalkHandler, } from "./runtime/dingtalk.js";
|
|
19
|
-
export { createEventsWatcher, EventsWatcher, type ImmediateEvent, type OneShotEvent, type PeriodicEvent, type ScheduledEvent, } from "./runtime/events.js";
|
|
19
|
+
export { createEventsWatcher, type EventAction, EventsWatcher, type ImmediateEvent, type OneShotEvent, type PeriodicEvent, type ScheduledEvent, } from "./runtime/events.js";
|
|
20
20
|
export { ChannelStore, type LoggedMessage, type LoggedSubAgentRun } from "./runtime/store.js";
|
|
21
21
|
export { createExecutor, type ExecOptions, type ExecResult, type Executor, parseSandboxArg, type SandboxConfig, validateSandbox, } from "./sandbox.js";
|
|
22
22
|
export { type PipiclawMemoryRecallSettings, type PipiclawSessionMemorySettings, type PipiclawSettings, PipiclawSettingsManager, } from "./settings.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { parseBuiltInCommand, renderBuiltInHelp, } from "./agent/commands.js";
|
|
|
3
3
|
export { getOrCreateRunner } from "./agent/index.js";
|
|
4
4
|
export { buildAppendSystemPrompt } from "./agent/prompt-builder.js";
|
|
5
5
|
export { getAgentConfig, getSoul, loadPipiclawSkills, } from "./agent/workspace-resources.js";
|
|
6
|
-
export { buildMemoryCandidates, } from "./memory/candidates.js";
|
|
6
|
+
export { buildMemoryCandidates, createMemoryCandidateStore, } from "./memory/candidates.js";
|
|
7
7
|
export { runBackgroundMaintenance, runInlineConsolidation, } from "./memory/consolidation.js";
|
|
8
8
|
export { ensureChannelMemoryFiles, ensureChannelMemoryFilesSync, getChannelSessionPath, readChannelSession, rewriteChannelSession, } from "./memory/files.js";
|
|
9
9
|
export { MemoryLifecycle } from "./memory/lifecycle.js";
|
|
@@ -12,7 +12,7 @@ export { renderSessionMemory, updateChannelSessionMemory, } from "./memory/sessi
|
|
|
12
12
|
export { runSidecarTask, } from "./memory/sidecar-worker.js";
|
|
13
13
|
export { getApiKeyForModel } from "./models/api-keys.js";
|
|
14
14
|
export { findExactModelReferenceMatch, findModelReferenceMatch, formatModelList, formatModelReference, resolveInitialModel, } from "./models/utils.js";
|
|
15
|
-
export { APP_HOME_DIR, APP_NAME, AUTH_CONFIG_PATH, CHANNEL_CONFIG_PATH, MODELS_CONFIG_PATH, SETTINGS_CONFIG_PATH, SUB_AGENTS_DIR, SUB_AGENTS_DIR_NAME, TOOLS_CONFIG_PATH, WORKSPACE_DIR, } from "./paths.js";
|
|
15
|
+
export { APP_HOME_DIR, APP_NAME, AUTH_CONFIG_PATH, CHANNEL_CONFIG_PATH, MODELS_CONFIG_PATH, SECURITY_CONFIG_PATH, SETTINGS_CONFIG_PATH, SUB_AGENTS_DIR, SUB_AGENTS_DIR_NAME, TOOLS_CONFIG_PATH, WORKSPACE_DIR, } from "./paths.js";
|
|
16
16
|
export { ensureChannelDir, getChannelDir, getChannelDirName, } from "./runtime/channel-paths.js";
|
|
17
17
|
export { createDingTalkContext } from "./runtime/delivery.js";
|
|
18
18
|
export { DingTalkBot, } from "./runtime/dingtalk.js";
|
|
@@ -12,10 +12,13 @@ export interface MemoryCandidate {
|
|
|
12
12
|
export interface BuildMemoryCandidatesOptions {
|
|
13
13
|
workspaceDir: string;
|
|
14
14
|
channelDir: string;
|
|
15
|
-
cache?: MemoryCandidateCache;
|
|
16
15
|
}
|
|
17
|
-
export
|
|
18
|
-
|
|
16
|
+
export declare class MemoryCandidateStore {
|
|
17
|
+
private files;
|
|
18
|
+
private inflight;
|
|
19
|
+
invalidate(path?: string): void;
|
|
20
|
+
getCandidates(options: BuildMemoryCandidatesOptions): Promise<MemoryCandidate[]>;
|
|
21
|
+
private loadFileCandidates;
|
|
19
22
|
}
|
|
20
|
-
export declare function
|
|
21
|
-
export declare function buildMemoryCandidates(options: BuildMemoryCandidatesOptions): Promise<MemoryCandidate[]>;
|
|
23
|
+
export declare function createMemoryCandidateStore(): MemoryCandidateStore;
|
|
24
|
+
export declare function buildMemoryCandidates(options: BuildMemoryCandidatesOptions, store?: MemoryCandidateStore): Promise<MemoryCandidate[]>;
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import { readFile } from "fs/promises";
|
|
1
|
+
import { readFile, stat } from "fs/promises";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { splitH1Sections, splitH2Sections } from "../shared/markdown-sections.js";
|
|
4
4
|
import { getChannelHistoryPath, getChannelMemoryPath, getChannelSessionPath } from "./files.js";
|
|
5
|
-
export function createMemoryCandidateCache() {
|
|
6
|
-
return {
|
|
7
|
-
entries: new Map(),
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
5
|
function normalizeContent(content) {
|
|
11
6
|
return content.replace(/\r/g, "").trim();
|
|
12
7
|
}
|
|
@@ -24,6 +19,9 @@ function slugify(value) {
|
|
|
24
19
|
.replace(/[^a-z0-9]+/g, "-")
|
|
25
20
|
.replace(/^-+|-+$/g, "") || "section");
|
|
26
21
|
}
|
|
22
|
+
function sameFingerprint(a, b) {
|
|
23
|
+
return a.exists === b.exists && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs && a.size === b.size;
|
|
24
|
+
}
|
|
27
25
|
function inferPriority(source, title) {
|
|
28
26
|
const normalizedTitle = title.trim().toLowerCase();
|
|
29
27
|
if (source === "channel-session") {
|
|
@@ -76,9 +74,6 @@ function buildCandidate(source, path, title, content, timestamp, searchText) {
|
|
|
76
74
|
priority: inferPriority(source, title),
|
|
77
75
|
};
|
|
78
76
|
}
|
|
79
|
-
function buildCacheKey(options) {
|
|
80
|
-
return `${options.workspaceDir}\u0000${options.channelDir}`;
|
|
81
|
-
}
|
|
82
77
|
function buildWorkspaceOrChannelMemoryCandidates(source, path, content) {
|
|
83
78
|
const sections = splitH2Sections(content);
|
|
84
79
|
if (sections.length === 0 && content) {
|
|
@@ -98,41 +93,96 @@ function buildSessionCandidates(path, content) {
|
|
|
98
93
|
: `${sessionTitle.trim()}\n${section.content}`));
|
|
99
94
|
}
|
|
100
95
|
function buildHistoryCandidates(path, content) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
const sections = splitH2Sections(content).filter((section) => section.content.trim());
|
|
97
|
+
if (sections.length === 0) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const foldedSections = sections.filter((section) => section.heading.startsWith("Folded History Through "));
|
|
101
|
+
const recentSectionLimit = 8;
|
|
102
|
+
const recentSections = sections.slice(-recentSectionLimit);
|
|
103
|
+
const selectedSections = Array.from(new Set([...foldedSections, ...recentSections]));
|
|
104
|
+
return selectedSections.map((section) => buildCandidate("channel-history", path, section.heading, section.content, section.heading));
|
|
104
105
|
}
|
|
105
|
-
async function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
106
|
+
async function readFingerprint(path) {
|
|
107
|
+
try {
|
|
108
|
+
const stats = await stat(path);
|
|
109
|
+
return {
|
|
110
|
+
exists: true,
|
|
111
|
+
mtimeMs: stats.mtimeMs,
|
|
112
|
+
ctimeMs: stats.ctimeMs,
|
|
113
|
+
size: stats.size,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return {
|
|
118
|
+
exists: false,
|
|
119
|
+
mtimeMs: 0,
|
|
120
|
+
ctimeMs: 0,
|
|
121
|
+
size: 0,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
122
124
|
}
|
|
123
|
-
export
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
export class MemoryCandidateStore {
|
|
126
|
+
constructor() {
|
|
127
|
+
this.files = new Map();
|
|
128
|
+
this.inflight = new Map();
|
|
129
|
+
}
|
|
130
|
+
invalidate(path) {
|
|
131
|
+
if (!path) {
|
|
132
|
+
this.files.clear();
|
|
133
|
+
this.inflight.clear();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.files.delete(path);
|
|
137
|
+
this.inflight.delete(path);
|
|
126
138
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
async getCandidates(options) {
|
|
140
|
+
const definitions = [
|
|
141
|
+
{
|
|
142
|
+
path: getChannelSessionPath(options.channelDir),
|
|
143
|
+
build: buildSessionCandidates,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
path: getChannelMemoryPath(options.channelDir),
|
|
147
|
+
build: (path, content) => buildWorkspaceOrChannelMemoryCandidates("channel-memory", path, content),
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
path: join(options.workspaceDir, "MEMORY.md"),
|
|
151
|
+
build: (path, content) => buildWorkspaceOrChannelMemoryCandidates("workspace-memory", path, content),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
path: getChannelHistoryPath(options.channelDir),
|
|
155
|
+
build: buildHistoryCandidates,
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
const candidateGroups = await Promise.all(definitions.map(async (definition) => this.loadFileCandidates(definition.path, definition.build)));
|
|
159
|
+
return candidateGroups.flat();
|
|
160
|
+
}
|
|
161
|
+
async loadFileCandidates(path, build) {
|
|
162
|
+
const pending = this.inflight.get(path);
|
|
163
|
+
if (pending) {
|
|
164
|
+
return pending;
|
|
165
|
+
}
|
|
166
|
+
const work = (async () => {
|
|
167
|
+
const fingerprint = await readFingerprint(path);
|
|
168
|
+
const cached = this.files.get(path);
|
|
169
|
+
if (cached && sameFingerprint(cached.fingerprint, fingerprint)) {
|
|
170
|
+
return cached.candidates;
|
|
171
|
+
}
|
|
172
|
+
const content = fingerprint.exists ? await readOptionalFile(path) : "";
|
|
173
|
+
const candidates = build(path, content);
|
|
174
|
+
this.files.set(path, { fingerprint, candidates });
|
|
175
|
+
return candidates;
|
|
176
|
+
})().finally(() => {
|
|
177
|
+
this.inflight.delete(path);
|
|
178
|
+
});
|
|
179
|
+
this.inflight.set(path, work);
|
|
180
|
+
return work;
|
|
131
181
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return
|
|
182
|
+
}
|
|
183
|
+
export function createMemoryCandidateStore() {
|
|
184
|
+
return new MemoryCandidateStore();
|
|
185
|
+
}
|
|
186
|
+
export async function buildMemoryCandidates(options, store = createMemoryCandidateStore()) {
|
|
187
|
+
return store.getCandidates(options);
|
|
138
188
|
}
|