@friendlyrobot/discord-pi-agent 0.21.0 → 0.21.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 +14 -3
- package/dist/agent-model-service.js +132 -0
- package/dist/agent-resource-service.js +70 -0
- package/dist/agent-service.js +189 -0
- package/dist/agent-turn-runner.js +148 -0
- package/dist/config.js +103 -0
- package/dist/debug-print.js +22 -0
- package/dist/discord-attachments.js +148 -0
- package/dist/discord-auth.js +37 -0
- package/dist/discord-gateway-client.js +49 -0
- package/dist/discord-media-resolution.js +107 -0
- package/dist/discord-message-handler.js +189 -0
- package/dist/discord-replies.js +112 -0
- package/dist/discord-typing.js +75 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +57 -1923
- package/dist/logger.js +26 -0
- package/dist/markdown-table-transformer.js +138 -0
- package/dist/media-description.js +87 -0
- package/dist/message-chunker.js +38 -0
- package/dist/prompt-context.d.ts +2 -13
- package/dist/prompt-context.js +19 -0
- package/dist/prompt-queue.js +37 -0
- package/dist/session-commands.js +281 -0
- package/dist/session-registry.js +73 -0
- package/dist/types.d.ts +7 -6
- package/dist/types.js +1 -0
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ DM prompts omit thread-only fields. `sent_at_local` uses `promptTimeZone` and `p
|
|
|
53
53
|
## Install
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
|
|
56
|
+
npm install @friendlyrobot/discord-pi-agent
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
## Usage
|
|
@@ -163,10 +163,21 @@ Not all models support thinking/reasoning. The configured `thinkingLevel` is app
|
|
|
163
163
|
## Build
|
|
164
164
|
|
|
165
165
|
```bash
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
npm run build
|
|
167
|
+
npm run typecheck
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
+
## Dependency updates
|
|
171
|
+
|
|
172
|
+
To check for newer package versions and update `package.json`, run:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npx npm-check-updates -u
|
|
176
|
+
npm install
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
This is the npm-side replacement for the old `bun update` workflow.
|
|
180
|
+
|
|
170
181
|
## Notes
|
|
171
182
|
|
|
172
183
|
- DM and forum threads supported via `startDiscordGateway`
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createModuleLogger } from "./logger";
|
|
2
|
+
const logger = createModuleLogger("agent-model-service");
|
|
3
|
+
export class AgentModelService {
|
|
4
|
+
config;
|
|
5
|
+
modelRegistry;
|
|
6
|
+
constructor(config, modelRegistry) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
this.modelRegistry = modelRegistry;
|
|
9
|
+
}
|
|
10
|
+
findModel(provider, modelId) {
|
|
11
|
+
return this.modelRegistry.find(provider, modelId);
|
|
12
|
+
}
|
|
13
|
+
async ensureSessionHasConfiguredModel(session) {
|
|
14
|
+
if (session.model) {
|
|
15
|
+
logger.debug({
|
|
16
|
+
model: `${session.model.provider}/${session.model.id}`,
|
|
17
|
+
}, "retaining existing session model");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const desiredModel = this.modelRegistry.find(this.config.modelProvider, this.config.modelId);
|
|
21
|
+
const availableModels = await this.modelRegistry.getAvailable();
|
|
22
|
+
logger.debug({
|
|
23
|
+
count: availableModels.length,
|
|
24
|
+
matches: availableModels
|
|
25
|
+
.filter((model) => {
|
|
26
|
+
return model.provider === this.config.modelProvider;
|
|
27
|
+
})
|
|
28
|
+
.map((model) => `${model.provider}/${model.id}`),
|
|
29
|
+
}, "available models");
|
|
30
|
+
if (!desiredModel) {
|
|
31
|
+
throw new Error(`Configured model not found: ${this.config.modelProvider}/${this.config.modelId}. Check your pi agent config and installed extensions.`);
|
|
32
|
+
}
|
|
33
|
+
logger.info({
|
|
34
|
+
to: `${desiredModel.provider}/${desiredModel.id}`,
|
|
35
|
+
}, "setting initial session model");
|
|
36
|
+
await session.setModel(desiredModel);
|
|
37
|
+
await this.applyConfiguredThinkingLevelForSession(session);
|
|
38
|
+
}
|
|
39
|
+
async listModels(session) {
|
|
40
|
+
const availableModels = await this.modelRegistry.getAvailable();
|
|
41
|
+
const currentDisplay = session?.model
|
|
42
|
+
? `${session.model.provider}/${session.model.id}`
|
|
43
|
+
: null;
|
|
44
|
+
const lines = availableModels.map((model) => {
|
|
45
|
+
const display = `${model.provider}/${model.id}`;
|
|
46
|
+
const marker = currentDisplay === display ? " (current)" : "";
|
|
47
|
+
return ` ${display}${marker}`;
|
|
48
|
+
});
|
|
49
|
+
return [
|
|
50
|
+
`Available models (${availableModels.length}):`,
|
|
51
|
+
...lines,
|
|
52
|
+
`\nUsage: !model <provider/modelId> to switch.`,
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
55
|
+
async switchModel(provider, modelId, session) {
|
|
56
|
+
const model = this.modelRegistry.find(provider, modelId);
|
|
57
|
+
if (!model) {
|
|
58
|
+
const availableModels = await this.modelRegistry.getAvailable();
|
|
59
|
+
const matches = availableModels
|
|
60
|
+
.filter((availableModel) => {
|
|
61
|
+
return availableModel.provider === provider;
|
|
62
|
+
})
|
|
63
|
+
.map((availableModel) => {
|
|
64
|
+
return `${availableModel.provider}/${availableModel.id}`;
|
|
65
|
+
});
|
|
66
|
+
const hint = matches.length > 0
|
|
67
|
+
? `\nModels from "${provider}": ${matches.join(", ")}`
|
|
68
|
+
: `\nUse !model to see all available models.`;
|
|
69
|
+
return `Model not found: ${provider}/${modelId}.${hint}`;
|
|
70
|
+
}
|
|
71
|
+
if (isSameModel(session.model, model)) {
|
|
72
|
+
return `Already using ${provider}/${modelId}.`;
|
|
73
|
+
}
|
|
74
|
+
await session.setModel(model);
|
|
75
|
+
await this.applyConfiguredThinkingLevelForSession(session);
|
|
76
|
+
const thinkingInfo = session.supportsThinking()
|
|
77
|
+
? ` (thinking: ${session.thinkingLevel})`
|
|
78
|
+
: "";
|
|
79
|
+
return `Switched to ${provider}/${modelId}${thinkingInfo}.`;
|
|
80
|
+
}
|
|
81
|
+
getCurrentModelDisplay(session) {
|
|
82
|
+
if (!session?.model) {
|
|
83
|
+
return "(no model selected)";
|
|
84
|
+
}
|
|
85
|
+
return `${session.model.provider}/${session.model.id}`;
|
|
86
|
+
}
|
|
87
|
+
getThinkingLevel(session) {
|
|
88
|
+
if (!session.supportsThinking()) {
|
|
89
|
+
return { current: "off", available: [], supported: false };
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
current: session.thinkingLevel,
|
|
93
|
+
available: session.getAvailableThinkingLevels(),
|
|
94
|
+
supported: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
setThinkingLevel(session, level) {
|
|
98
|
+
if (!session.supportsThinking()) {
|
|
99
|
+
return "Current model does not support reasoning/thinking.";
|
|
100
|
+
}
|
|
101
|
+
const available = session.getAvailableThinkingLevels();
|
|
102
|
+
if (!available.includes(level)) {
|
|
103
|
+
return `Invalid thinking level "${level}" for current model. Available: ${available.join(", ")}`;
|
|
104
|
+
}
|
|
105
|
+
session.setThinkingLevel(level);
|
|
106
|
+
return `Thinking level set to "${level}".`;
|
|
107
|
+
}
|
|
108
|
+
async applyConfiguredThinkingLevelForSession(session) {
|
|
109
|
+
if (session.supportsThinking()) {
|
|
110
|
+
const available = session.getAvailableThinkingLevels();
|
|
111
|
+
if (available.includes(this.config.thinkingLevel)) {
|
|
112
|
+
session.setThinkingLevel(this.config.thinkingLevel);
|
|
113
|
+
logger.debug({
|
|
114
|
+
level: this.config.thinkingLevel,
|
|
115
|
+
}, "thinking level applied");
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
logger.debug({
|
|
119
|
+
requested: this.config.thinkingLevel,
|
|
120
|
+
available,
|
|
121
|
+
}, "thinking level not available for model");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function isSameModel(currentModel, desiredModel) {
|
|
127
|
+
if (!currentModel) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return (currentModel.provider === desiredModel.provider &&
|
|
131
|
+
currentModel.id === desiredModel.id);
|
|
132
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export class AgentResourceService {
|
|
2
|
+
resourceLoader;
|
|
3
|
+
constructor(resourceLoader) {
|
|
4
|
+
this.resourceLoader = resourceLoader;
|
|
5
|
+
}
|
|
6
|
+
getSkillsSummary() {
|
|
7
|
+
const result = this.resourceLoader.getSkills();
|
|
8
|
+
const { skills } = result;
|
|
9
|
+
if (skills.length === 0) {
|
|
10
|
+
return "Skills: (none loaded)";
|
|
11
|
+
}
|
|
12
|
+
const names = skills.map((skill) => {
|
|
13
|
+
return skill.name;
|
|
14
|
+
});
|
|
15
|
+
return `Skills (${skills.length}): ${names.join(", ") || "(none)"}`;
|
|
16
|
+
}
|
|
17
|
+
getExtensionsSummary() {
|
|
18
|
+
const result = this.resourceLoader.getExtensions();
|
|
19
|
+
const { extensions, errors } = result;
|
|
20
|
+
if (extensions.length === 0) {
|
|
21
|
+
return "Extensions: (none loaded)";
|
|
22
|
+
}
|
|
23
|
+
const lines = extensions.map((extension) => {
|
|
24
|
+
const toolCount = extension.tools.size;
|
|
25
|
+
const commandCount = extension.commands.size;
|
|
26
|
+
const parts = [];
|
|
27
|
+
if (toolCount > 0) {
|
|
28
|
+
parts.push(`${toolCount} tool${toolCount !== 1 ? "s" : ""}`);
|
|
29
|
+
}
|
|
30
|
+
if (commandCount > 0) {
|
|
31
|
+
parts.push(`${commandCount} command${commandCount !== 1 ? "s" : ""}`);
|
|
32
|
+
}
|
|
33
|
+
const summary = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
34
|
+
return ` ${extension.path}${summary}`;
|
|
35
|
+
});
|
|
36
|
+
const header = `Extensions (${extensions.length}):`;
|
|
37
|
+
const errorLines = errors.length > 0
|
|
38
|
+
? [
|
|
39
|
+
`Errors (${errors.length}):`,
|
|
40
|
+
...errors.map((error) => {
|
|
41
|
+
return ` ${error.path}: ${error.error}`;
|
|
42
|
+
}),
|
|
43
|
+
]
|
|
44
|
+
: [];
|
|
45
|
+
return [header, ...lines, ...errorLines].join("\n");
|
|
46
|
+
}
|
|
47
|
+
async reloadResources() {
|
|
48
|
+
await this.resourceLoader.reload();
|
|
49
|
+
const extensions = this.resourceLoader
|
|
50
|
+
.getExtensions()
|
|
51
|
+
.extensions.map((extension) => {
|
|
52
|
+
return extension.path;
|
|
53
|
+
});
|
|
54
|
+
const skills = this.resourceLoader.getSkills();
|
|
55
|
+
const skillNames = skills.skills.map((skill) => {
|
|
56
|
+
return skill.name;
|
|
57
|
+
});
|
|
58
|
+
const agentsFiles = this.resourceLoader
|
|
59
|
+
.getAgentsFiles()
|
|
60
|
+
.agentsFiles.map((file) => {
|
|
61
|
+
return file.path;
|
|
62
|
+
});
|
|
63
|
+
return [
|
|
64
|
+
"Resources reloaded.",
|
|
65
|
+
`Extensions (${extensions.length}): ${extensions.join(", ") || "(none)"}`,
|
|
66
|
+
`Skills (${skills.skills.length}): ${skillNames.join(", ") || "(none)"}`,
|
|
67
|
+
`AGENTS.md files (${agentsFiles.length}): ${agentsFiles.join(", ") || "(none)"}`,
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AuthStorage, createAgentSession, DefaultResourceLoader, ModelRegistry, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { AgentModelService } from "./agent-model-service";
|
|
5
|
+
import { AgentResourceService } from "./agent-resource-service";
|
|
6
|
+
import { createModuleLogger } from "./logger";
|
|
7
|
+
import { runAgentTurn } from "./agent-turn-runner";
|
|
8
|
+
const logger = createModuleLogger("agent-service");
|
|
9
|
+
export class AgentService {
|
|
10
|
+
config;
|
|
11
|
+
authStorage;
|
|
12
|
+
modelRegistry;
|
|
13
|
+
settingsManager;
|
|
14
|
+
resourceLoader;
|
|
15
|
+
session = null;
|
|
16
|
+
models;
|
|
17
|
+
resources;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.authStorage = AuthStorage.create(path.join(config.agentDir, "auth.json"));
|
|
21
|
+
this.modelRegistry = ModelRegistry.create(this.authStorage, path.join(config.agentDir, "models.json"));
|
|
22
|
+
this.settingsManager = SettingsManager.create(config.cwd, config.agentDir);
|
|
23
|
+
this.resourceLoader = new DefaultResourceLoader({
|
|
24
|
+
cwd: config.cwd,
|
|
25
|
+
agentDir: config.agentDir,
|
|
26
|
+
settingsManager: this.settingsManager,
|
|
27
|
+
});
|
|
28
|
+
this.models = new AgentModelService(config, this.modelRegistry);
|
|
29
|
+
this.resources = new AgentResourceService(this.resourceLoader);
|
|
30
|
+
}
|
|
31
|
+
async initialize() {
|
|
32
|
+
await fs.mkdir(this.config.agentDir, { recursive: true });
|
|
33
|
+
await fs.mkdir(this.getSessionDir(), { recursive: true });
|
|
34
|
+
logger.info({
|
|
35
|
+
cwd: this.config.cwd,
|
|
36
|
+
agentDir: this.config.agentDir,
|
|
37
|
+
sessionDir: this.getSessionDir(),
|
|
38
|
+
modelProvider: this.config.modelProvider,
|
|
39
|
+
modelId: this.config.modelId,
|
|
40
|
+
thinkingLevel: this.config.thinkingLevel,
|
|
41
|
+
}, "config");
|
|
42
|
+
await this.resourceLoader.reload();
|
|
43
|
+
logger.info({
|
|
44
|
+
extensions: this.resourceLoader
|
|
45
|
+
.getExtensions()
|
|
46
|
+
.extensions.map((extension) => extension.path),
|
|
47
|
+
agentsFiles: this.resourceLoader
|
|
48
|
+
.getAgentsFiles()
|
|
49
|
+
.agentsFiles.map((file) => file.path),
|
|
50
|
+
}, "resources loaded");
|
|
51
|
+
await this.createOrResumeSession();
|
|
52
|
+
await this.ensureConfiguredModel();
|
|
53
|
+
}
|
|
54
|
+
getSession() {
|
|
55
|
+
return this.session;
|
|
56
|
+
}
|
|
57
|
+
getAgentDir() {
|
|
58
|
+
return this.config.agentDir;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Create a temporary in-memory session. For one-shot tasks like image
|
|
62
|
+
* description — no file persistence, no cleanup needed. The caller must
|
|
63
|
+
* setModel() before prompting and dispose() when done.
|
|
64
|
+
*/
|
|
65
|
+
async createTemporarySession() {
|
|
66
|
+
const { session } = await createAgentSession({
|
|
67
|
+
cwd: this.config.cwd,
|
|
68
|
+
agentDir: this.config.agentDir,
|
|
69
|
+
authStorage: this.authStorage,
|
|
70
|
+
modelRegistry: this.modelRegistry,
|
|
71
|
+
resourceLoader: this.resourceLoader,
|
|
72
|
+
settingsManager: this.settingsManager,
|
|
73
|
+
sessionManager: SessionManager.inMemory(),
|
|
74
|
+
thinkingLevel: "off",
|
|
75
|
+
});
|
|
76
|
+
logger.debug({ sessionId: session.sessionId }, "temporary session created");
|
|
77
|
+
return session;
|
|
78
|
+
}
|
|
79
|
+
async createSession(sessionDir) {
|
|
80
|
+
await fs.mkdir(sessionDir, { recursive: true });
|
|
81
|
+
const { session } = await createAgentSession({
|
|
82
|
+
cwd: this.config.cwd,
|
|
83
|
+
agentDir: this.config.agentDir,
|
|
84
|
+
authStorage: this.authStorage,
|
|
85
|
+
modelRegistry: this.modelRegistry,
|
|
86
|
+
resourceLoader: this.resourceLoader,
|
|
87
|
+
settingsManager: this.settingsManager,
|
|
88
|
+
sessionManager: SessionManager.continueRecent(this.config.cwd, sessionDir),
|
|
89
|
+
thinkingLevel: this.config.thinkingLevel,
|
|
90
|
+
});
|
|
91
|
+
logger.debug({
|
|
92
|
+
sessionDir,
|
|
93
|
+
sessionId: session.sessionId,
|
|
94
|
+
sessionFile: session.sessionFile,
|
|
95
|
+
}, "scoped session created");
|
|
96
|
+
await this.models.ensureSessionHasConfiguredModel(session);
|
|
97
|
+
return session;
|
|
98
|
+
}
|
|
99
|
+
async prompt(text) {
|
|
100
|
+
const session = this.requireSession();
|
|
101
|
+
const transformedPrompt = await this.config.promptTransform({
|
|
102
|
+
rawContent: text,
|
|
103
|
+
discordMetadata: "",
|
|
104
|
+
now: () => "",
|
|
105
|
+
userMessage: () => text,
|
|
106
|
+
});
|
|
107
|
+
return runAgentTurn(session, transformedPrompt);
|
|
108
|
+
}
|
|
109
|
+
async compact() {
|
|
110
|
+
const session = this.requireSession();
|
|
111
|
+
await session.compact();
|
|
112
|
+
return `Compaction finished for session ${session.sessionId}.`;
|
|
113
|
+
}
|
|
114
|
+
async resetSession() {
|
|
115
|
+
const previousSession = this.requireSession();
|
|
116
|
+
await previousSession.abort();
|
|
117
|
+
previousSession.dispose();
|
|
118
|
+
this.session = null;
|
|
119
|
+
const { session } = await createAgentSession({
|
|
120
|
+
cwd: this.config.cwd,
|
|
121
|
+
agentDir: this.config.agentDir,
|
|
122
|
+
authStorage: this.authStorage,
|
|
123
|
+
modelRegistry: this.modelRegistry,
|
|
124
|
+
resourceLoader: this.resourceLoader,
|
|
125
|
+
settingsManager: this.settingsManager,
|
|
126
|
+
sessionManager: SessionManager.create(this.config.cwd, this.getSessionDir()),
|
|
127
|
+
thinkingLevel: this.config.thinkingLevel,
|
|
128
|
+
});
|
|
129
|
+
this.session = session;
|
|
130
|
+
await this.ensureConfiguredModel();
|
|
131
|
+
return `Started a fresh session. Old session kept at ${previousSession.sessionFile ?? "(unknown path)"}.`;
|
|
132
|
+
}
|
|
133
|
+
getStatus() {
|
|
134
|
+
const session = this.requireSession();
|
|
135
|
+
const model = this.models.getCurrentModelDisplay(session);
|
|
136
|
+
const contextUsage = session.getContextUsage();
|
|
137
|
+
const thinkingInfo = session.supportsThinking()
|
|
138
|
+
? `thinking: ${session.thinkingLevel} (available: ${session.getAvailableThinkingLevels().join(", ")})`
|
|
139
|
+
: "thinking: not supported";
|
|
140
|
+
return {
|
|
141
|
+
sessionId: session.sessionId,
|
|
142
|
+
sessionFile: session.sessionFile,
|
|
143
|
+
model,
|
|
144
|
+
streaming: session.isStreaming,
|
|
145
|
+
contextUsage,
|
|
146
|
+
thinkingInfo,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async shutdown() {
|
|
150
|
+
const session = this.session;
|
|
151
|
+
if (session) {
|
|
152
|
+
await session.abort();
|
|
153
|
+
session.dispose();
|
|
154
|
+
}
|
|
155
|
+
await this.settingsManager.flush();
|
|
156
|
+
}
|
|
157
|
+
async createOrResumeSession() {
|
|
158
|
+
const { session } = await createAgentSession({
|
|
159
|
+
cwd: this.config.cwd,
|
|
160
|
+
agentDir: this.config.agentDir,
|
|
161
|
+
authStorage: this.authStorage,
|
|
162
|
+
modelRegistry: this.modelRegistry,
|
|
163
|
+
resourceLoader: this.resourceLoader,
|
|
164
|
+
settingsManager: this.settingsManager,
|
|
165
|
+
sessionManager: SessionManager.continueRecent(this.config.cwd, this.getSessionDir()),
|
|
166
|
+
thinkingLevel: this.config.thinkingLevel,
|
|
167
|
+
});
|
|
168
|
+
this.session = session;
|
|
169
|
+
logger.info({
|
|
170
|
+
sessionId: session.sessionId,
|
|
171
|
+
sessionFile: session.sessionFile,
|
|
172
|
+
restoredModel: session.model
|
|
173
|
+
? `${session.model.provider}/${session.model.id}`
|
|
174
|
+
: null,
|
|
175
|
+
}, "session ready");
|
|
176
|
+
}
|
|
177
|
+
async ensureConfiguredModel() {
|
|
178
|
+
await this.models.ensureSessionHasConfiguredModel(this.requireSession());
|
|
179
|
+
}
|
|
180
|
+
requireSession() {
|
|
181
|
+
if (!this.session) {
|
|
182
|
+
throw new Error("Agent session has not been initialized.");
|
|
183
|
+
}
|
|
184
|
+
return this.session;
|
|
185
|
+
}
|
|
186
|
+
getSessionDir() {
|
|
187
|
+
return path.join(this.config.agentDir, "sessions");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { debugPrint } from "./debug-print";
|
|
2
|
+
import { createModuleLogger } from "./logger";
|
|
3
|
+
import { transformMarkdownTablesToCodeBlocks } from "./markdown-table-transformer";
|
|
4
|
+
const logger = createModuleLogger("agent-turn-runner");
|
|
5
|
+
export async function runAgentTurn(session, prompt, options = {}) {
|
|
6
|
+
let streamedText = "";
|
|
7
|
+
let eventCount = 0;
|
|
8
|
+
let toolCount = 0;
|
|
9
|
+
const toolInputsByCallId = new Map();
|
|
10
|
+
const model = session.model
|
|
11
|
+
? `${session.model.provider}/${session.model.id}`
|
|
12
|
+
: "none";
|
|
13
|
+
debugPrint(prompt, "Full Prompt");
|
|
14
|
+
// logger.debug(
|
|
15
|
+
// {
|
|
16
|
+
// promptLength: prompt.length,
|
|
17
|
+
// model,
|
|
18
|
+
// prompt,
|
|
19
|
+
// },
|
|
20
|
+
// "prompt start",
|
|
21
|
+
// );
|
|
22
|
+
const unsubscribe = session.subscribe((event) => {
|
|
23
|
+
eventCount += 1;
|
|
24
|
+
if (event.type === "message_update") {
|
|
25
|
+
if (event.assistantMessageEvent.type === "text_delta") {
|
|
26
|
+
streamedText += event.assistantMessageEvent.delta;
|
|
27
|
+
}
|
|
28
|
+
if (event.assistantMessageEvent.type === "thinking_delta") {
|
|
29
|
+
// Intentionally ignored. Thinking deltas are too noisy for routine logs.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (event.type === "tool_execution_start") {
|
|
33
|
+
toolCount += 1;
|
|
34
|
+
const input = event.toolName === "bash" ? event.args.command : event.args;
|
|
35
|
+
toolInputsByCallId.set(event.toolCallId, input);
|
|
36
|
+
if (event.toolName === "bash") {
|
|
37
|
+
debugPrint(input, "CMD");
|
|
38
|
+
// logger.debug(
|
|
39
|
+
// {
|
|
40
|
+
// toolName: event.toolName,
|
|
41
|
+
// },
|
|
42
|
+
// `agent tool start: [${event.toolName}]`,
|
|
43
|
+
// );
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
logger.debug({
|
|
47
|
+
toolName: event.toolName,
|
|
48
|
+
// input,
|
|
49
|
+
}, `agent tool start: [${event.toolName}]`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (event.type === "tool_execution_end") {
|
|
53
|
+
const input = toolInputsByCallId.get(event.toolCallId);
|
|
54
|
+
toolInputsByCallId.delete(event.toolCallId);
|
|
55
|
+
if (event.toolName === "bash") {
|
|
56
|
+
debugPrint(extractToolOutput(event.result), event.isError ? "BASH TOOL ERROR OUTPUT" : "BASH TOOL OUTPUT");
|
|
57
|
+
// logger.debug(
|
|
58
|
+
// {
|
|
59
|
+
// toolName: event.toolName,
|
|
60
|
+
// isError: event.isError,
|
|
61
|
+
// },
|
|
62
|
+
// `agent tool end: [${event.toolName}] ${truncateForLog(
|
|
63
|
+
// typeof input === "string" ? input : "",
|
|
64
|
+
// )}`,
|
|
65
|
+
// );
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
logger.debug({
|
|
69
|
+
toolName: event.toolName,
|
|
70
|
+
// input: truncateForLog(extractToolOutput(input)),
|
|
71
|
+
isError: event.isError,
|
|
72
|
+
// output: event.result,
|
|
73
|
+
// output: truncateForLog(extractToolOutput(event.result)),
|
|
74
|
+
}, `agent tool end: [${event.toolName}]`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// if (event.type === "agent_end") {
|
|
78
|
+
// logger.debug(
|
|
79
|
+
// {
|
|
80
|
+
// messageCount: event.messages.length,
|
|
81
|
+
// model,
|
|
82
|
+
// toolCount,
|
|
83
|
+
// eventCount,
|
|
84
|
+
// },
|
|
85
|
+
// "agent end",
|
|
86
|
+
// );
|
|
87
|
+
// }
|
|
88
|
+
});
|
|
89
|
+
try {
|
|
90
|
+
await session.prompt(prompt, { images: options.images });
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
unsubscribe();
|
|
94
|
+
}
|
|
95
|
+
const errorMessage = session.agent.state.errorMessage?.trim();
|
|
96
|
+
const fallbackText = getLatestAssistantText(session.messages);
|
|
97
|
+
const finalText = streamedText.trim() || fallbackText.trim();
|
|
98
|
+
if (errorMessage) {
|
|
99
|
+
return errorMessage;
|
|
100
|
+
}
|
|
101
|
+
if (finalText) {
|
|
102
|
+
const transformed = await transformMarkdownTablesToCodeBlocks(finalText);
|
|
103
|
+
debugPrint(finalText, "BEFORE TRANSFORM");
|
|
104
|
+
debugPrint(transformed, "TRANSFORMED");
|
|
105
|
+
return transformed;
|
|
106
|
+
}
|
|
107
|
+
return "No response generated.";
|
|
108
|
+
}
|
|
109
|
+
function truncateForLog(value, maxLength = 400) {
|
|
110
|
+
if (value.length <= maxLength) {
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
return `${value.slice(0, maxLength)}...`;
|
|
114
|
+
}
|
|
115
|
+
function extractToolOutput(output) {
|
|
116
|
+
if (typeof output === "object" && output !== null) {
|
|
117
|
+
const obj = output;
|
|
118
|
+
if (Array.isArray(obj.content)) {
|
|
119
|
+
return obj.content
|
|
120
|
+
.map((item) => {
|
|
121
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
122
|
+
return item.text;
|
|
123
|
+
}
|
|
124
|
+
return JSON.stringify(item);
|
|
125
|
+
})
|
|
126
|
+
.join("\n");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return String(output);
|
|
130
|
+
}
|
|
131
|
+
function getLatestAssistantText(messages) {
|
|
132
|
+
const latestAssistantMessage = [...messages].reverse().find((message) => {
|
|
133
|
+
return message.role === "assistant";
|
|
134
|
+
});
|
|
135
|
+
if (!latestAssistantMessage ||
|
|
136
|
+
!Array.isArray(latestAssistantMessage.content)) {
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
return latestAssistantMessage.content
|
|
140
|
+
.filter((item) => {
|
|
141
|
+
return item.type === "text";
|
|
142
|
+
})
|
|
143
|
+
.map((item) => {
|
|
144
|
+
return item.text ?? "";
|
|
145
|
+
})
|
|
146
|
+
.join("\n")
|
|
147
|
+
.trim();
|
|
148
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
export function resolveConfig(config) {
|
|
4
|
+
const discordAllowedUserId = requireNonEmptyConfigValue("discordAllowedUserId", config.discordAllowedUserId);
|
|
5
|
+
const cwd = requireNonEmptyConfigValue("cwd", config.cwd);
|
|
6
|
+
return {
|
|
7
|
+
discordBotToken: requireNonEmptyConfigValue("discordBotToken", config.discordBotToken),
|
|
8
|
+
discordAllowedUserId,
|
|
9
|
+
cwd,
|
|
10
|
+
agentDir: config.agentDir?.trim() || path.join(cwd, ".pi-agent"),
|
|
11
|
+
modelProvider: config.modelProvider?.trim() || "openrouter",
|
|
12
|
+
modelId: config.modelId?.trim() || "anthropic/claude-3.5-haiku",
|
|
13
|
+
thinkingLevel: parseThinkingLevel(config.thinkingLevel) || "medium",
|
|
14
|
+
promptTimeZone: config.promptTimeZone?.trim() || "UTC",
|
|
15
|
+
promptLocale: config.promptLocale?.trim() || "en-AU",
|
|
16
|
+
promptTransform: config.promptTransform || defaultPromptTransform,
|
|
17
|
+
startupMessage: config.startupMessage === undefined
|
|
18
|
+
? "Bot is online and ready."
|
|
19
|
+
: config.startupMessage,
|
|
20
|
+
shutdownOnSignals: config.shutdownOnSignals ?? true,
|
|
21
|
+
visionModelId: config.visionModelId?.trim() || null,
|
|
22
|
+
discordAllowedForumChannelIds: config.discordAllowedForumChannelIds ?? [],
|
|
23
|
+
discordAllowedUserIds: config.discordAllowedUserIds ?? [
|
|
24
|
+
discordAllowedUserId,
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function loadDiscordGatewayConfigFromEnv(overrides = {}) {
|
|
29
|
+
dotenv.config();
|
|
30
|
+
return resolveConfig({
|
|
31
|
+
discordBotToken: overrides.discordBotToken || process.env.DISCORD_BOT_TOKEN || "",
|
|
32
|
+
discordAllowedUserId: overrides.discordAllowedUserId ||
|
|
33
|
+
process.env.DISCORD_ALLOWED_USER_ID ||
|
|
34
|
+
"",
|
|
35
|
+
cwd: overrides.cwd || process.env.PI_AGENT_CWD || process.cwd(),
|
|
36
|
+
agentDir: overrides.agentDir || process.env.PI_AGENT_DIR,
|
|
37
|
+
modelProvider: overrides.modelProvider || process.env.PI_MODEL_PROVIDER,
|
|
38
|
+
modelId: overrides.modelId || process.env.PI_MODEL_ID,
|
|
39
|
+
thinkingLevel: parseThinkingLevel(overrides.thinkingLevel || process.env.PI_THINKING_LEVEL),
|
|
40
|
+
promptTimeZone: overrides.promptTimeZone || process.env.PI_PROMPT_TIME_ZONE,
|
|
41
|
+
promptLocale: overrides.promptLocale || process.env.PI_PROMPT_LOCALE,
|
|
42
|
+
promptTransform: overrides.promptTransform,
|
|
43
|
+
startupMessage: overrides.startupMessage ?? readStartupMessageFromEnv(),
|
|
44
|
+
shutdownOnSignals: overrides.shutdownOnSignals,
|
|
45
|
+
visionModelId: overrides.visionModelId ?? process.env.PI_VISION_MODEL_ID,
|
|
46
|
+
discordAllowedForumChannelIds: overrides.discordAllowedForumChannelIds ??
|
|
47
|
+
parseStringArrayFromEnv("DISCORD_FORUM_CHANNEL_IDS") ??
|
|
48
|
+
[],
|
|
49
|
+
discordAllowedUserIds: overrides.discordAllowedUserIds ??
|
|
50
|
+
parseStringArrayFromEnv("DISCORD_ALLOWED_USER_IDS"),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function requireNonEmptyConfigValue(name, value) {
|
|
54
|
+
const trimmedValue = value.trim();
|
|
55
|
+
if (!trimmedValue) {
|
|
56
|
+
throw new Error(`Missing required config value: ${name}`);
|
|
57
|
+
}
|
|
58
|
+
return trimmedValue;
|
|
59
|
+
}
|
|
60
|
+
function readStartupMessageFromEnv() {
|
|
61
|
+
const value = process.env.DISCORD_STARTUP_MESSAGE;
|
|
62
|
+
if (value === undefined) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const trimmedValue = value.trim();
|
|
66
|
+
if (!trimmedValue || trimmedValue.toLowerCase() === "false") {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return trimmedValue;
|
|
70
|
+
}
|
|
71
|
+
function parseThinkingLevel(value) {
|
|
72
|
+
if (!value) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const trimmed = value.trim().toLowerCase();
|
|
76
|
+
const validLevels = [
|
|
77
|
+
"off",
|
|
78
|
+
"minimal",
|
|
79
|
+
"low",
|
|
80
|
+
"medium",
|
|
81
|
+
"high",
|
|
82
|
+
"xhigh",
|
|
83
|
+
];
|
|
84
|
+
if (validLevels.includes(trimmed)) {
|
|
85
|
+
return trimmed;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
function defaultPromptTransform(ctx) {
|
|
90
|
+
return [ctx.now(), ctx.discordMetadata, "", ctx.userMessage()]
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.join("\n");
|
|
93
|
+
}
|
|
94
|
+
function parseStringArrayFromEnv(key) {
|
|
95
|
+
const value = process.env[key];
|
|
96
|
+
if (!value) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
return value
|
|
100
|
+
.split(",")
|
|
101
|
+
.map((id) => id.trim())
|
|
102
|
+
.filter(Boolean);
|
|
103
|
+
}
|