@oyasmi/pipiclaw 0.5.1 → 0.5.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 +308 -209
- package/dist/agent/channel-runner.d.ts +47 -0
- package/dist/agent/channel-runner.js +441 -0
- package/dist/agent/index.d.ts +3 -0
- package/dist/agent/index.js +2 -0
- package/dist/agent/progress-formatter.d.ts +4 -0
- package/dist/agent/progress-formatter.js +52 -0
- package/dist/agent/run-queue.d.ts +7 -0
- package/dist/agent/run-queue.js +26 -0
- package/dist/agent/runner-factory.d.ts +3 -0
- package/dist/agent/runner-factory.js +10 -0
- package/dist/agent/session-events.d.ts +14 -0
- package/dist/agent/session-events.js +215 -0
- package/dist/agent/session-resource-gate.d.ts +10 -0
- package/dist/agent/session-resource-gate.js +44 -0
- package/dist/agent/type-guards.d.ts +22 -0
- package/dist/agent/type-guards.js +106 -0
- package/dist/agent/types.d.ts +160 -0
- package/dist/agent/types.js +22 -0
- package/dist/agent.d.ts +2 -16
- package/dist/agent.js +1 -782
- package/dist/command-extension.d.ts +0 -1
- package/dist/command-extension.js +0 -1
- package/dist/commands.d.ts +0 -1
- package/dist/commands.js +0 -1
- package/dist/config-loader.d.ts +0 -1
- package/dist/config-loader.js +1 -2
- package/dist/context.d.ts +58 -15
- package/dist/context.js +50 -8
- package/dist/index.d.ts +12 -13
- package/dist/index.js +12 -13
- package/dist/log.d.ts +0 -1
- package/dist/log.js +0 -1
- package/dist/main.d.ts +0 -1
- package/dist/main.js +5 -405
- package/dist/memory/bootstrap.d.ts +6 -0
- package/dist/memory/bootstrap.js +46 -0
- package/dist/{memory-candidates.d.ts → memory/candidates.d.ts} +1 -1
- package/dist/{memory-candidates.js → memory/candidates.js} +33 -21
- package/dist/memory/chinese-words.d.ts +1 -0
- package/dist/memory/chinese-words.js +273 -0
- package/dist/{memory-consolidation.d.ts → memory/consolidation.d.ts} +0 -1
- package/dist/{memory-consolidation.js → memory/consolidation.js} +26 -35
- package/dist/{memory-files.d.ts → memory/files.d.ts} +0 -6
- package/dist/{memory-files.js → memory/files.js} +11 -36
- package/dist/{memory-lifecycle.d.ts → memory/lifecycle.d.ts} +23 -6
- package/dist/memory/lifecycle.js +246 -0
- package/dist/{memory-recall.d.ts → memory/recall.d.ts} +2 -2
- package/dist/memory/recall.js +501 -0
- package/dist/{session-memory.d.ts → memory/session.d.ts} +1 -1
- package/dist/{session-memory.js → memory/session.js} +31 -62
- package/dist/model-utils.d.ts +0 -1
- package/dist/model-utils.js +0 -1
- package/dist/paths.d.ts +0 -1
- package/dist/paths.js +0 -1
- package/dist/prompt-builder.d.ts +0 -1
- package/dist/prompt-builder.js +0 -1
- package/dist/runtime/bootstrap.d.ts +47 -0
- package/dist/runtime/bootstrap.js +450 -0
- package/dist/{delivery.d.ts → runtime/delivery.d.ts} +0 -1
- package/dist/{delivery.js → runtime/delivery.js} +1 -2
- package/dist/{dingtalk.d.ts → runtime/dingtalk.d.ts} +10 -1
- package/dist/{dingtalk.js → runtime/dingtalk.js} +87 -28
- package/dist/{events.d.ts → runtime/events.d.ts} +0 -1
- package/dist/{events.js → runtime/events.js} +1 -2
- package/dist/{store.d.ts → runtime/store.d.ts} +5 -1
- package/dist/{store.js → runtime/store.js} +60 -20
- package/dist/sandbox.d.ts +0 -1
- package/dist/sandbox.js +1 -2
- package/dist/{llm-json.d.ts → shared/llm-json.d.ts} +0 -1
- package/dist/{llm-json.js → shared/llm-json.js} +0 -1
- package/dist/shared/markdown-sections.d.ts +6 -0
- package/dist/{markdown-sections.js → shared/markdown-sections.js} +10 -4
- package/dist/{shell-escape.d.ts → shared/shell-escape.d.ts} +0 -1
- package/dist/{shell-escape.js → shared/shell-escape.js} +0 -1
- package/dist/shared/text-utils.d.ts +9 -0
- package/dist/shared/text-utils.js +36 -0
- package/dist/shared/type-guards.d.ts +5 -0
- package/dist/shared/type-guards.js +12 -0
- package/dist/shared/types.d.ts +14 -0
- package/dist/shared/types.js +1 -0
- package/dist/sidecar-worker.d.ts +0 -1
- package/dist/sidecar-worker.js +1 -8
- package/dist/{sub-agents.d.ts → subagents/discovery.d.ts} +0 -1
- package/dist/{sub-agents.js → subagents/discovery.js} +2 -3
- package/dist/{tools/subagent.d.ts → subagents/tool.d.ts} +2 -16
- package/dist/{tools/subagent.js → subagents/tool.js} +16 -38
- package/dist/tools/attach.d.ts +0 -1
- package/dist/tools/attach.js +0 -1
- package/dist/tools/bash.d.ts +0 -1
- package/dist/tools/bash.js +0 -1
- package/dist/tools/edit.d.ts +0 -1
- package/dist/tools/edit.js +1 -2
- package/dist/tools/index.d.ts +1 -2
- package/dist/tools/index.js +1 -2
- package/dist/tools/read.d.ts +0 -1
- package/dist/tools/read.js +1 -2
- package/dist/tools/truncate.d.ts +0 -1
- package/dist/tools/truncate.js +0 -1
- package/dist/tools/write-content.d.ts +0 -1
- package/dist/tools/write-content.js +1 -2
- package/dist/tools/write.d.ts +0 -1
- package/dist/tools/write.js +0 -1
- package/package.json +9 -3
- package/CHANGELOG.md +0 -47
- package/dist/agent.d.ts.map +0 -1
- package/dist/agent.js.map +0 -1
- package/dist/command-extension.d.ts.map +0 -1
- package/dist/command-extension.js.map +0 -1
- package/dist/commands.d.ts.map +0 -1
- package/dist/commands.js.map +0 -1
- package/dist/config-loader.d.ts.map +0 -1
- package/dist/config-loader.js.map +0 -1
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js.map +0 -1
- package/dist/delivery.d.ts.map +0 -1
- package/dist/delivery.js.map +0 -1
- package/dist/dingtalk.d.ts.map +0 -1
- package/dist/dingtalk.js.map +0 -1
- package/dist/events.d.ts.map +0 -1
- package/dist/events.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/llm-json.d.ts.map +0 -1
- package/dist/llm-json.js.map +0 -1
- package/dist/log.d.ts.map +0 -1
- package/dist/log.js.map +0 -1
- package/dist/main.d.ts.map +0 -1
- package/dist/main.js.map +0 -1
- package/dist/markdown-sections.d.ts +0 -6
- package/dist/markdown-sections.d.ts.map +0 -1
- package/dist/markdown-sections.js.map +0 -1
- package/dist/memory-candidates.d.ts.map +0 -1
- package/dist/memory-candidates.js.map +0 -1
- package/dist/memory-consolidation.d.ts.map +0 -1
- package/dist/memory-consolidation.js.map +0 -1
- package/dist/memory-files.d.ts.map +0 -1
- package/dist/memory-files.js.map +0 -1
- package/dist/memory-lifecycle.d.ts.map +0 -1
- package/dist/memory-lifecycle.js +0 -150
- package/dist/memory-lifecycle.js.map +0 -1
- package/dist/memory-recall.d.ts.map +0 -1
- package/dist/memory-recall.js +0 -218
- package/dist/memory-recall.js.map +0 -1
- package/dist/model-utils.d.ts.map +0 -1
- package/dist/model-utils.js.map +0 -1
- package/dist/paths.d.ts.map +0 -1
- package/dist/paths.js.map +0 -1
- package/dist/prompt-builder.d.ts.map +0 -1
- package/dist/prompt-builder.js.map +0 -1
- package/dist/sandbox.d.ts.map +0 -1
- package/dist/sandbox.js.map +0 -1
- package/dist/session-memory-files.d.ts +0 -2
- package/dist/session-memory-files.d.ts.map +0 -1
- package/dist/session-memory-files.js +0 -2
- package/dist/session-memory-files.js.map +0 -1
- package/dist/session-memory.d.ts.map +0 -1
- package/dist/session-memory.js.map +0 -1
- package/dist/shell-escape.d.ts.map +0 -1
- package/dist/shell-escape.js.map +0 -1
- package/dist/sidecar-worker.d.ts.map +0 -1
- package/dist/sidecar-worker.js.map +0 -1
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js.map +0 -1
- package/dist/sub-agents.d.ts.map +0 -1
- package/dist/sub-agents.js.map +0 -1
- package/dist/tools/attach.d.ts.map +0 -1
- package/dist/tools/attach.js.map +0 -1
- package/dist/tools/bash.d.ts.map +0 -1
- package/dist/tools/bash.js.map +0 -1
- package/dist/tools/edit.d.ts.map +0 -1
- package/dist/tools/edit.js.map +0 -1
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/read.d.ts.map +0 -1
- package/dist/tools/read.js.map +0 -1
- package/dist/tools/subagent.d.ts.map +0 -1
- package/dist/tools/subagent.js.map +0 -1
- package/dist/tools/truncate.d.ts.map +0 -1
- package/dist/tools/truncate.js.map +0 -1
- package/dist/tools/write-content.d.ts.map +0 -1
- package/dist/tools/write-content.js.map +0 -1
- package/dist/tools/write.d.ts.map +0 -1
- package/dist/tools/write.js.map +0 -1
- package/docs/improve-memory/design.md +0 -537
- package/docs/improve-memory/interfaces-and-tests.md +0 -473
- package/docs/improve-memory/spec.md +0 -357
- package/docs/memory-rfc.md +0 -297
- package/docs/proj-review.md +0 -188
- package/docs/subagent/pi-subagent-analyse.txt +0 -190
- package/docs/subagent/pi-subagent-design.txt +0 -266
- package/docs/subagent/pi-subagent-phase1-plan.txt +0 -529
- package/docs/test-supplementation-plan.md +0 -553
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type BuiltInCommand } from "../commands.js";
|
|
2
|
+
import type { DingTalkContext } from "../runtime/dingtalk.js";
|
|
3
|
+
import type { ChannelStore } from "../runtime/store.js";
|
|
4
|
+
import { type SandboxConfig } from "../sandbox.js";
|
|
5
|
+
import { type AgentRunner } from "./types.js";
|
|
6
|
+
export declare class ChannelRunner implements AgentRunner {
|
|
7
|
+
private readonly sandboxConfig;
|
|
8
|
+
private readonly channelId;
|
|
9
|
+
private readonly channelDir;
|
|
10
|
+
private readonly workspacePath;
|
|
11
|
+
private readonly workspaceDir;
|
|
12
|
+
private readonly session;
|
|
13
|
+
private readonly agent;
|
|
14
|
+
private readonly sessionManager;
|
|
15
|
+
private readonly settingsManager;
|
|
16
|
+
private readonly modelRegistry;
|
|
17
|
+
private readonly memoryLifecycle;
|
|
18
|
+
private readonly sessionResourceGate;
|
|
19
|
+
private readonly sessionReady;
|
|
20
|
+
private subAgentDiscovery;
|
|
21
|
+
private activeModel;
|
|
22
|
+
private currentSkills;
|
|
23
|
+
private firstTurnMemoryBootstrapPending;
|
|
24
|
+
private runState;
|
|
25
|
+
constructor(sandboxConfig: SandboxConfig, channelId: string, channelDir: string);
|
|
26
|
+
run(ctx: DingTalkContext, store: ChannelStore): Promise<{
|
|
27
|
+
stopReason: string;
|
|
28
|
+
errorMessage?: string;
|
|
29
|
+
}>;
|
|
30
|
+
handleBuiltinCommand(ctx: DingTalkContext, command: BuiltInCommand): Promise<void>;
|
|
31
|
+
queueSteer(text: string, userName?: string): Promise<void>;
|
|
32
|
+
queueFollowUp(text: string, userName?: string): Promise<void>;
|
|
33
|
+
abort(): Promise<void>;
|
|
34
|
+
private sendCommandReply;
|
|
35
|
+
private requireQueuedMessage;
|
|
36
|
+
private shouldPreserveRawInput;
|
|
37
|
+
private formatUserMessage;
|
|
38
|
+
private queueBusyMessage;
|
|
39
|
+
private resetRunState;
|
|
40
|
+
private refreshSessionResources;
|
|
41
|
+
private initializeSession;
|
|
42
|
+
private reloadSessionResources;
|
|
43
|
+
private ensureSessionReady;
|
|
44
|
+
private refreshSubAgentDiscovery;
|
|
45
|
+
private subscribeToSessionEvents;
|
|
46
|
+
private buildFirstTurnMemoryBootstrap;
|
|
47
|
+
}
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { Agent } from "@mariozechner/pi-agent-core";
|
|
2
|
+
import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, ModelRegistry, SessionManager, } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
4
|
+
import { dirname, join, resolve } from "path";
|
|
5
|
+
import { createCommandExtension } from "../command-extension.js";
|
|
6
|
+
import { renderBuiltInHelp } from "../commands.js";
|
|
7
|
+
import { getAgentConfig, getApiKeyForModel, getSoul, loadPipiclawSkills } from "../config-loader.js";
|
|
8
|
+
import { PipiclawSettingsManager } from "../context.js";
|
|
9
|
+
import * as log from "../log.js";
|
|
10
|
+
import { buildFirstTurnMemoryBootstrap as renderFirstTurnMemoryBootstrap } from "../memory/bootstrap.js";
|
|
11
|
+
import { createMemoryCandidateCache } from "../memory/candidates.js";
|
|
12
|
+
import { getChannelMemoryPath } from "../memory/files.js";
|
|
13
|
+
import { MemoryLifecycle } from "../memory/lifecycle.js";
|
|
14
|
+
import { recallRelevantMemory } from "../memory/recall.js";
|
|
15
|
+
import { resolveInitialModel } from "../model-utils.js";
|
|
16
|
+
import { APP_HOME_DIR, AUTH_CONFIG_PATH, MODELS_CONFIG_PATH } from "../paths.js";
|
|
17
|
+
import { buildAppendSystemPrompt } from "../prompt-builder.js";
|
|
18
|
+
import { createExecutor } from "../sandbox.js";
|
|
19
|
+
import { HAN_REGEX } from "../shared/text-utils.js";
|
|
20
|
+
import { isRecord } from "../shared/type-guards.js";
|
|
21
|
+
import { discoverSubAgents, formatSubAgentList } from "../subagents/discovery.js";
|
|
22
|
+
import { createPipiclawTools } from "../tools/index.js";
|
|
23
|
+
import { clipUserInput } from "./progress-formatter.js";
|
|
24
|
+
import { createRunQueue } from "./run-queue.js";
|
|
25
|
+
import { handleSessionEvent } from "./session-events.js";
|
|
26
|
+
import { SessionResourceGate } from "./session-resource-gate.js";
|
|
27
|
+
import { getLastAssistantUsage } from "./type-guards.js";
|
|
28
|
+
import { createEmptyRunState, MAX_USER_MESSAGE_CHARS, } from "./types.js";
|
|
29
|
+
function isSilentOutcome(outcome) {
|
|
30
|
+
return outcome.kind === "silent";
|
|
31
|
+
}
|
|
32
|
+
function isFinalOutcome(outcome) {
|
|
33
|
+
return outcome.kind === "final";
|
|
34
|
+
}
|
|
35
|
+
function getFinalOutcomeText(outcome) {
|
|
36
|
+
return isFinalOutcome(outcome) ? outcome.text : null;
|
|
37
|
+
}
|
|
38
|
+
function createModelRegistry(authStorage, modelsJsonPath) {
|
|
39
|
+
const registryClass = ModelRegistry;
|
|
40
|
+
return typeof registryClass.create === "function"
|
|
41
|
+
? registryClass.create(authStorage, modelsJsonPath)
|
|
42
|
+
: new registryClass(authStorage, modelsJsonPath);
|
|
43
|
+
}
|
|
44
|
+
function asSdkSettingsManager(manager) {
|
|
45
|
+
return manager;
|
|
46
|
+
}
|
|
47
|
+
export class ChannelRunner {
|
|
48
|
+
constructor(sandboxConfig, channelId, channelDir) {
|
|
49
|
+
this.firstTurnMemoryBootstrapPending = true;
|
|
50
|
+
// --- Per run ---
|
|
51
|
+
this.runState = createEmptyRunState();
|
|
52
|
+
this.sandboxConfig = sandboxConfig;
|
|
53
|
+
this.channelId = channelId;
|
|
54
|
+
this.channelDir = channelDir;
|
|
55
|
+
const executor = createExecutor(sandboxConfig);
|
|
56
|
+
this.workspaceDir = resolve(dirname(channelDir));
|
|
57
|
+
this.workspacePath = executor.getWorkspacePath(this.workspaceDir);
|
|
58
|
+
// Initial skill summaries
|
|
59
|
+
const initialSkills = loadPipiclawSkills(channelDir, this.workspacePath);
|
|
60
|
+
this.currentSkills = initialSkills;
|
|
61
|
+
// Create session manager
|
|
62
|
+
const contextFile = join(channelDir, "context.jsonl");
|
|
63
|
+
this.sessionManager = SessionManager.open(contextFile, channelDir);
|
|
64
|
+
this.settingsManager = new PipiclawSettingsManager(APP_HOME_DIR);
|
|
65
|
+
// Create AuthStorage and ModelRegistry
|
|
66
|
+
const authStorage = AuthStorage.create(AUTH_CONFIG_PATH);
|
|
67
|
+
this.modelRegistry = createModelRegistry(authStorage, MODELS_CONFIG_PATH);
|
|
68
|
+
// Resolve model: prefer saved global default, fall back to first available model
|
|
69
|
+
this.activeModel = resolveInitialModel(this.modelRegistry, this.settingsManager);
|
|
70
|
+
log.logInfo(`Using model: ${this.activeModel.provider}/${this.activeModel.id} (${this.activeModel.name})`);
|
|
71
|
+
this.subAgentDiscovery = this.refreshSubAgentDiscovery();
|
|
72
|
+
// Create tools
|
|
73
|
+
const tools = createPipiclawTools({
|
|
74
|
+
executor,
|
|
75
|
+
getCurrentModel: () => this.activeModel,
|
|
76
|
+
getAvailableModels: () => this.modelRegistry.getAvailable(),
|
|
77
|
+
resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
|
|
78
|
+
workspaceDir: this.workspaceDir,
|
|
79
|
+
channelDir: this.channelDir,
|
|
80
|
+
workspacePath: this.workspacePath,
|
|
81
|
+
channelId: this.channelId,
|
|
82
|
+
sandboxConfig: this.sandboxConfig,
|
|
83
|
+
getSubAgentDiscovery: () => this.subAgentDiscovery,
|
|
84
|
+
getMemoryRecallSettings: () => this.settingsManager.getMemoryRecallSettings(),
|
|
85
|
+
});
|
|
86
|
+
// Create agent
|
|
87
|
+
this.agent = new Agent({
|
|
88
|
+
initialState: {
|
|
89
|
+
systemPrompt: "",
|
|
90
|
+
model: this.activeModel,
|
|
91
|
+
thinkingLevel: "off",
|
|
92
|
+
tools,
|
|
93
|
+
},
|
|
94
|
+
convertToLlm,
|
|
95
|
+
getApiKey: async () => getApiKeyForModel(this.modelRegistry, this.activeModel),
|
|
96
|
+
});
|
|
97
|
+
this.memoryLifecycle = new MemoryLifecycle({
|
|
98
|
+
channelId: this.channelId,
|
|
99
|
+
channelDir: this.channelDir,
|
|
100
|
+
getMessages: () => this.session.messages,
|
|
101
|
+
getSessionEntries: () => this.sessionManager.getBranch(),
|
|
102
|
+
getModel: () => this.session.model ?? this.activeModel,
|
|
103
|
+
resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
|
|
104
|
+
getSessionMemorySettings: () => this.settingsManager.getSessionMemorySettings(),
|
|
105
|
+
});
|
|
106
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
107
|
+
cwd: process.cwd(),
|
|
108
|
+
agentDir: APP_HOME_DIR,
|
|
109
|
+
settingsManager: asSdkSettingsManager(this.settingsManager),
|
|
110
|
+
extensionFactories: [
|
|
111
|
+
this.memoryLifecycle.createExtensionFactory(),
|
|
112
|
+
createCommandExtension({
|
|
113
|
+
getCurrentModel: () => this.session.model ?? this.activeModel,
|
|
114
|
+
getAvailableModels: async () => {
|
|
115
|
+
this.modelRegistry.refresh();
|
|
116
|
+
return await this.modelRegistry.getAvailable();
|
|
117
|
+
},
|
|
118
|
+
getSessionStats: () => this.session.getSessionStats(),
|
|
119
|
+
getThinkingLevel: () => this.session.thinkingLevel,
|
|
120
|
+
switchModel: async (model) => {
|
|
121
|
+
await this.session.setModel(model);
|
|
122
|
+
this.activeModel = model;
|
|
123
|
+
},
|
|
124
|
+
refreshSessionResources: async () => {
|
|
125
|
+
await this.refreshSessionResources();
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
],
|
|
129
|
+
appendSystemPromptOverride: (base) => {
|
|
130
|
+
const soul = getSoul(this.workspaceDir);
|
|
131
|
+
const sections = [...base];
|
|
132
|
+
if (soul) {
|
|
133
|
+
sections.unshift(soul);
|
|
134
|
+
}
|
|
135
|
+
sections.push(buildAppendSystemPrompt(this.workspacePath, this.channelId, this.sandboxConfig, {
|
|
136
|
+
subAgentList: formatSubAgentList(this.subAgentDiscovery.agents),
|
|
137
|
+
}));
|
|
138
|
+
return sections;
|
|
139
|
+
},
|
|
140
|
+
agentsFilesOverride: () => {
|
|
141
|
+
const agentConfig = getAgentConfig(this.channelDir);
|
|
142
|
+
return {
|
|
143
|
+
agentsFiles: agentConfig ? [{ path: `${this.workspacePath}/AGENTS.md`, content: agentConfig }] : [],
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
skillsOverride: (base) => ({
|
|
147
|
+
skills: [...base.skills, ...this.currentSkills],
|
|
148
|
+
diagnostics: base.diagnostics,
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
const baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
|
|
152
|
+
// Create AgentSession
|
|
153
|
+
this.session = new AgentSession({
|
|
154
|
+
agent: this.agent,
|
|
155
|
+
sessionManager: this.sessionManager,
|
|
156
|
+
settingsManager: asSdkSettingsManager(this.settingsManager),
|
|
157
|
+
cwd: process.cwd(),
|
|
158
|
+
modelRegistry: this.modelRegistry,
|
|
159
|
+
resourceLoader,
|
|
160
|
+
baseToolsOverride,
|
|
161
|
+
});
|
|
162
|
+
this.sessionResourceGate = new SessionResourceGate(async () => {
|
|
163
|
+
await this.reloadSessionResources();
|
|
164
|
+
});
|
|
165
|
+
// Subscribe to session events
|
|
166
|
+
this.subscribeToSessionEvents();
|
|
167
|
+
this.sessionReady = this.initializeSession();
|
|
168
|
+
}
|
|
169
|
+
// === Public API ===
|
|
170
|
+
async run(ctx, store) {
|
|
171
|
+
this.resetRunState(ctx, store);
|
|
172
|
+
const runQueue = createRunQueue(ctx);
|
|
173
|
+
this.runState.queue = runQueue.queue;
|
|
174
|
+
try {
|
|
175
|
+
await this.ensureSessionReady();
|
|
176
|
+
// Ensure channel directory exists
|
|
177
|
+
await mkdir(this.channelDir, { recursive: true });
|
|
178
|
+
const candidateCache = createMemoryCandidateCache();
|
|
179
|
+
const clippedInput = clipUserInput(ctx.message.text, MAX_USER_MESSAGE_CHARS);
|
|
180
|
+
const userMessage = this.formatUserMessage(clippedInput, ctx.message.userName);
|
|
181
|
+
let promptText = this.shouldPreserveRawInput(ctx.message.text) ? clippedInput : userMessage;
|
|
182
|
+
let recalledContextText = "";
|
|
183
|
+
let durableMemoryBootstrapText = "";
|
|
184
|
+
this.memoryLifecycle.noteUserTurnStarted();
|
|
185
|
+
if (!this.shouldPreserveRawInput(ctx.message.text)) {
|
|
186
|
+
const recallSettings = this.settingsManager.getMemoryRecallSettings();
|
|
187
|
+
if (recallSettings.enabled) {
|
|
188
|
+
const recall = await recallRelevantMemory({
|
|
189
|
+
query: clippedInput,
|
|
190
|
+
workspaceDir: this.workspaceDir,
|
|
191
|
+
channelDir: this.channelDir,
|
|
192
|
+
maxCandidates: recallSettings.maxCandidates,
|
|
193
|
+
maxInjected: recallSettings.maxInjected,
|
|
194
|
+
maxChars: recallSettings.maxChars,
|
|
195
|
+
rerankWithModel: recallSettings.rerankWithModel,
|
|
196
|
+
autoRerank: HAN_REGEX.test(clippedInput),
|
|
197
|
+
model: this.session.model ?? this.activeModel,
|
|
198
|
+
resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
|
|
199
|
+
candidateCache,
|
|
200
|
+
});
|
|
201
|
+
if (recall.renderedText) {
|
|
202
|
+
recalledContextText = recall.renderedText;
|
|
203
|
+
promptText = `${recall.renderedText}\n\n<user_message>\n${promptText}\n</user_message>`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (this.firstTurnMemoryBootstrapPending) {
|
|
207
|
+
durableMemoryBootstrapText = await this.buildFirstTurnMemoryBootstrap();
|
|
208
|
+
if (durableMemoryBootstrapText) {
|
|
209
|
+
promptText = `${durableMemoryBootstrapText}\n\n${promptText}`;
|
|
210
|
+
}
|
|
211
|
+
this.firstTurnMemoryBootstrapPending = false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Debug: write context to last_prompt.json (only with PIPICLAW_DEBUG=1)
|
|
215
|
+
if (process.env.PIPICLAW_DEBUG) {
|
|
216
|
+
const debugContext = {
|
|
217
|
+
systemPrompt: this.agent.state.systemPrompt,
|
|
218
|
+
messages: this.session.messages,
|
|
219
|
+
durableMemoryBootstrap: durableMemoryBootstrapText || undefined,
|
|
220
|
+
recalledContext: recalledContextText || undefined,
|
|
221
|
+
newUserMessage: promptText,
|
|
222
|
+
};
|
|
223
|
+
await writeFile(join(this.channelDir, "last_prompt.json"), JSON.stringify(debugContext, null, 2));
|
|
224
|
+
}
|
|
225
|
+
await this.sessionResourceGate.runPrompt(async () => {
|
|
226
|
+
await this.session.prompt(promptText);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
this.runState.stopReason = "error";
|
|
231
|
+
this.runState.errorMessage = err instanceof Error ? err.message : String(err);
|
|
232
|
+
log.logWarning(`[${this.channelId}] Runner failed`, this.runState.errorMessage);
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
await runQueue.drain();
|
|
236
|
+
const finalOutcome = this.runState.finalOutcome;
|
|
237
|
+
const finalOutcomeText = getFinalOutcomeText(finalOutcome);
|
|
238
|
+
try {
|
|
239
|
+
if (this.runState.stopReason === "error" &&
|
|
240
|
+
this.runState.errorMessage &&
|
|
241
|
+
!this.runState.finalResponseDelivered) {
|
|
242
|
+
try {
|
|
243
|
+
await ctx.replaceMessage("_Sorry, something went wrong_");
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
247
|
+
log.logWarning("Failed to post error message", errMsg);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else if (isSilentOutcome(finalOutcome)) {
|
|
251
|
+
try {
|
|
252
|
+
await ctx.deleteMessage();
|
|
253
|
+
log.logInfo("Silent response - deleted message");
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
257
|
+
log.logWarning("Failed to delete message for silent response", errMsg);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else if (finalOutcomeText && !this.runState.finalResponseDelivered) {
|
|
261
|
+
try {
|
|
262
|
+
await ctx.replaceMessage(finalOutcomeText);
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
266
|
+
log.logWarning("Failed to replace message with final text", errMsg);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
await ctx.flush();
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
await ctx.close();
|
|
273
|
+
}
|
|
274
|
+
// Log usage summary
|
|
275
|
+
if (this.runState.totalUsage.cost.total > 0) {
|
|
276
|
+
const lastAssistantMessage = getLastAssistantUsage(this.session.messages);
|
|
277
|
+
const contextTokens = lastAssistantMessage
|
|
278
|
+
? lastAssistantMessage.usage.input +
|
|
279
|
+
lastAssistantMessage.usage.output +
|
|
280
|
+
lastAssistantMessage.usage.cacheRead +
|
|
281
|
+
lastAssistantMessage.usage.cacheWrite
|
|
282
|
+
: 0;
|
|
283
|
+
const currentRunModel = this.session.model ?? this.activeModel;
|
|
284
|
+
const contextWindow = currentRunModel.contextWindow || 200000;
|
|
285
|
+
log.logUsageSummary(this.runState.logCtx, this.runState.totalUsage, contextTokens, contextWindow);
|
|
286
|
+
}
|
|
287
|
+
// Clear run state
|
|
288
|
+
this.runState.ctx = null;
|
|
289
|
+
this.runState.logCtx = null;
|
|
290
|
+
this.runState.queue = null;
|
|
291
|
+
}
|
|
292
|
+
return { stopReason: this.runState.stopReason, errorMessage: this.runState.errorMessage };
|
|
293
|
+
}
|
|
294
|
+
async handleBuiltinCommand(ctx, command) {
|
|
295
|
+
try {
|
|
296
|
+
switch (command.name) {
|
|
297
|
+
case "help":
|
|
298
|
+
await this.sendCommandReply(ctx, renderBuiltInHelp());
|
|
299
|
+
return;
|
|
300
|
+
case "stop":
|
|
301
|
+
await this.sendCommandReply(ctx, "No task is running. Use `/stop` only while a task is running.");
|
|
302
|
+
return;
|
|
303
|
+
case "steer":
|
|
304
|
+
this.requireQueuedMessage(command.args, "steer");
|
|
305
|
+
await this.sendCommandReply(ctx, "No task is running. Send the message directly instead of using `/steer`.");
|
|
306
|
+
return;
|
|
307
|
+
case "followup":
|
|
308
|
+
this.requireQueuedMessage(command.args, "followup");
|
|
309
|
+
await this.sendCommandReply(ctx, "No task is running. Send the message directly now, or use `/followup` while a task is running.");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
315
|
+
log.logWarning(`[${this.channelId}] Built-in command failed`, errMsg);
|
|
316
|
+
await this.sendCommandReply(ctx, `命令执行失败:${errMsg}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async queueSteer(text, userName) {
|
|
320
|
+
await this.queueBusyMessage("steer", this.requireQueuedMessage(text, "steer"), userName);
|
|
321
|
+
}
|
|
322
|
+
async queueFollowUp(text, userName) {
|
|
323
|
+
await this.queueBusyMessage("followUp", this.requireQueuedMessage(text, "followup"), userName);
|
|
324
|
+
}
|
|
325
|
+
async abort() {
|
|
326
|
+
await this.session.abort();
|
|
327
|
+
}
|
|
328
|
+
// === Private helpers ===
|
|
329
|
+
async sendCommandReply(ctx, text) {
|
|
330
|
+
const delivered = await ctx.respondPlain(text);
|
|
331
|
+
if (!delivered) {
|
|
332
|
+
await ctx.replaceMessage(text);
|
|
333
|
+
await ctx.flush();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
requireQueuedMessage(text, commandName) {
|
|
337
|
+
const trimmedText = text.trim();
|
|
338
|
+
if (!trimmedText) {
|
|
339
|
+
throw new Error(`/${commandName} requires a message.`);
|
|
340
|
+
}
|
|
341
|
+
return trimmedText;
|
|
342
|
+
}
|
|
343
|
+
shouldPreserveRawInput(text) {
|
|
344
|
+
return text.trim().startsWith("/");
|
|
345
|
+
}
|
|
346
|
+
formatUserMessage(text, userName, now = new Date()) {
|
|
347
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
348
|
+
const offset = -now.getTimezoneOffset();
|
|
349
|
+
const offsetSign = offset >= 0 ? "+" : "-";
|
|
350
|
+
const offsetHours = pad(Math.floor(Math.abs(offset) / 60));
|
|
351
|
+
const offsetMins = pad(Math.abs(offset) % 60);
|
|
352
|
+
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetHours}:${offsetMins}`;
|
|
353
|
+
return `[${timestamp}] [${userName || "unknown"}]: ${text}`;
|
|
354
|
+
}
|
|
355
|
+
async queueBusyMessage(delivery, text, userName) {
|
|
356
|
+
if (!this.session.isStreaming) {
|
|
357
|
+
throw new Error("No task is currently running.");
|
|
358
|
+
}
|
|
359
|
+
const clippedText = clipUserInput(text, MAX_USER_MESSAGE_CHARS);
|
|
360
|
+
if (clippedText !== text.trim()) {
|
|
361
|
+
log.logWarning(`[${this.channelId}] Queued message exceeded ${MAX_USER_MESSAGE_CHARS} chars and was clipped`);
|
|
362
|
+
}
|
|
363
|
+
await this.sessionResourceGate.runPrompt(async () => {
|
|
364
|
+
await this.session.prompt(this.formatUserMessage(clippedText, userName), {
|
|
365
|
+
streamingBehavior: delivery,
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
resetRunState(ctx, store) {
|
|
370
|
+
this.runState = createEmptyRunState();
|
|
371
|
+
this.runState.ctx = ctx;
|
|
372
|
+
this.runState.logCtx = {
|
|
373
|
+
channelId: ctx.message.channel,
|
|
374
|
+
userName: ctx.message.userName,
|
|
375
|
+
channelName: ctx.channelName,
|
|
376
|
+
};
|
|
377
|
+
this.runState.store = store;
|
|
378
|
+
}
|
|
379
|
+
async refreshSessionResources() {
|
|
380
|
+
await this.ensureSessionReady();
|
|
381
|
+
await this.sessionResourceGate.requestRefresh();
|
|
382
|
+
}
|
|
383
|
+
async initializeSession() {
|
|
384
|
+
await this.reloadSessionResources();
|
|
385
|
+
}
|
|
386
|
+
async reloadSessionResources() {
|
|
387
|
+
const skills = loadPipiclawSkills(this.channelDir, this.workspacePath);
|
|
388
|
+
this.currentSkills = skills;
|
|
389
|
+
this.subAgentDiscovery = this.refreshSubAgentDiscovery();
|
|
390
|
+
this.firstTurnMemoryBootstrapPending = true;
|
|
391
|
+
await this.session.reload();
|
|
392
|
+
}
|
|
393
|
+
async ensureSessionReady() {
|
|
394
|
+
await this.sessionReady;
|
|
395
|
+
}
|
|
396
|
+
refreshSubAgentDiscovery() {
|
|
397
|
+
this.modelRegistry.refresh();
|
|
398
|
+
const discovery = discoverSubAgents(this.workspaceDir, this.modelRegistry.getAvailable());
|
|
399
|
+
for (const warning of discovery.warnings) {
|
|
400
|
+
log.logWarning(`Sub-agent config warning (${this.channelId})`, warning);
|
|
401
|
+
}
|
|
402
|
+
return discovery;
|
|
403
|
+
}
|
|
404
|
+
// === Session event subscription ===
|
|
405
|
+
subscribeToSessionEvents() {
|
|
406
|
+
this.session.subscribe(async (event) => {
|
|
407
|
+
if (isRecord(event) && "reason" in event && event.reason === "new") {
|
|
408
|
+
this.firstTurnMemoryBootstrapPending = true;
|
|
409
|
+
}
|
|
410
|
+
if (!this.runState.ctx || !this.runState.logCtx || !this.runState.queue)
|
|
411
|
+
return;
|
|
412
|
+
await handleSessionEvent(event, {
|
|
413
|
+
ctx: this.runState.ctx,
|
|
414
|
+
logCtx: this.runState.logCtx,
|
|
415
|
+
queue: this.runState.queue,
|
|
416
|
+
pendingTools: this.runState.pendingTools,
|
|
417
|
+
store: this.runState.store,
|
|
418
|
+
runState: this.runState,
|
|
419
|
+
memoryLifecycle: this.memoryLifecycle,
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
async buildFirstTurnMemoryBootstrap() {
|
|
424
|
+
const readOptionalFile = async (path) => {
|
|
425
|
+
try {
|
|
426
|
+
return await readFile(path, "utf-8");
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
return "";
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
const [channelMemory, workspaceMemory] = await Promise.all([
|
|
433
|
+
readOptionalFile(getChannelMemoryPath(this.channelDir)),
|
|
434
|
+
readOptionalFile(join(this.workspaceDir, "MEMORY.md")),
|
|
435
|
+
]);
|
|
436
|
+
return renderFirstTurnMemoryBootstrap({
|
|
437
|
+
channelMemory,
|
|
438
|
+
workspaceMemory,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProgressEntryKind } from "./types.js";
|
|
2
|
+
export declare function clipUserInput(text: string, maxChars: number): string;
|
|
3
|
+
export declare function formatProgressEntry(kind: ProgressEntryKind, text: string): string;
|
|
4
|
+
export declare function extractToolResultText(result: unknown): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function sanitizeProgressText(text) {
|
|
2
|
+
return text
|
|
3
|
+
.replace(/\uFFFC/g, "")
|
|
4
|
+
.replace(/\r/g, "")
|
|
5
|
+
.trim();
|
|
6
|
+
}
|
|
7
|
+
export function clipUserInput(text, maxChars) {
|
|
8
|
+
const normalized = text.replace(/\r/g, "").trim();
|
|
9
|
+
if (normalized.length <= maxChars) {
|
|
10
|
+
return normalized;
|
|
11
|
+
}
|
|
12
|
+
const headChars = Math.floor(maxChars * 0.6);
|
|
13
|
+
const tailChars = maxChars - headChars;
|
|
14
|
+
return `${normalized.slice(0, headChars)}\n\n[... omitted ${normalized.length - maxChars} chars ...]\n\n${normalized.slice(-tailChars)}`;
|
|
15
|
+
}
|
|
16
|
+
export function formatProgressEntry(kind, text) {
|
|
17
|
+
const cleaned = sanitizeProgressText(text);
|
|
18
|
+
if (!cleaned)
|
|
19
|
+
return "";
|
|
20
|
+
const normalized = cleaned.replace(/\n+/g, " ").trim();
|
|
21
|
+
switch (kind) {
|
|
22
|
+
case "tool":
|
|
23
|
+
return `Running: ${normalized}`;
|
|
24
|
+
case "thinking":
|
|
25
|
+
return `Thinking: ${normalized}`;
|
|
26
|
+
case "error":
|
|
27
|
+
return `Error: ${normalized}`;
|
|
28
|
+
case "assistant":
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function extractToolResultText(result) {
|
|
33
|
+
if (typeof result === "string") {
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
if (result &&
|
|
37
|
+
typeof result === "object" &&
|
|
38
|
+
"content" in result &&
|
|
39
|
+
Array.isArray(result.content)) {
|
|
40
|
+
const content = result.content;
|
|
41
|
+
const textParts = [];
|
|
42
|
+
for (const part of content) {
|
|
43
|
+
if (part.type === "text" && part.text) {
|
|
44
|
+
textParts.push(part.text);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (textParts.length > 0) {
|
|
48
|
+
return textParts.join("\n");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return JSON.stringify(result);
|
|
52
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DingTalkContext } from "../runtime/dingtalk.js";
|
|
2
|
+
import type { RunQueue } from "./types.js";
|
|
3
|
+
export interface CreatedRunQueue {
|
|
4
|
+
queue: RunQueue;
|
|
5
|
+
drain: () => Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare function createRunQueue(ctx: DingTalkContext): CreatedRunQueue;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as log from "../log.js";
|
|
2
|
+
export function createRunQueue(ctx) {
|
|
3
|
+
let queueChain = Promise.resolve();
|
|
4
|
+
const queue = {
|
|
5
|
+
enqueue: (fn, errorContext) => {
|
|
6
|
+
queueChain = queueChain.then(async () => {
|
|
7
|
+
try {
|
|
8
|
+
await fn();
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
12
|
+
log.logWarning(`DingTalk API error (${errorContext})`, errMsg);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
enqueueMessage: function (text, target, errorContext, doLog = true) {
|
|
17
|
+
this.enqueue(() => (target === "main" ? ctx.respond(text, doLog) : ctx.respondInThread(text)), errorContext);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
queue,
|
|
22
|
+
drain: async () => {
|
|
23
|
+
await queueChain;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ChannelRunner } from "./channel-runner.js";
|
|
2
|
+
const channelRunners = new Map();
|
|
3
|
+
export function getOrCreateRunner(sandboxConfig, channelId, channelDir) {
|
|
4
|
+
const existing = channelRunners.get(channelId);
|
|
5
|
+
if (existing)
|
|
6
|
+
return existing;
|
|
7
|
+
const runner = new ChannelRunner(sandboxConfig, channelId, channelDir);
|
|
8
|
+
channelRunners.set(channelId, runner);
|
|
9
|
+
return runner;
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MemoryLifecycle } from "../memory/lifecycle.js";
|
|
2
|
+
import type { DingTalkContext } from "../runtime/dingtalk.js";
|
|
3
|
+
import type { ChannelStore } from "../runtime/store.js";
|
|
4
|
+
import type { PendingTool, RunLogContext, RunQueue, RunState } from "./types.js";
|
|
5
|
+
export interface SessionEventHandlerContext {
|
|
6
|
+
ctx: DingTalkContext;
|
|
7
|
+
logCtx: RunLogContext;
|
|
8
|
+
queue: RunQueue;
|
|
9
|
+
pendingTools: Map<string, PendingTool>;
|
|
10
|
+
store: ChannelStore | null;
|
|
11
|
+
runState: RunState;
|
|
12
|
+
memoryLifecycle: MemoryLifecycle;
|
|
13
|
+
}
|
|
14
|
+
export declare function handleSessionEvent(event: unknown, context: SessionEventHandlerContext): Promise<void>;
|