@offerpilot/axiomruntime 0.0.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/README.md +185 -0
- package/dist/cli/commands/add.js +29 -0
- package/dist/cli/commands/context.js +29 -0
- package/dist/cli/commands/doctor.js +62 -0
- package/dist/cli/commands/edit.js +85 -0
- package/dist/cli/commands/help.js +16 -0
- package/dist/cli/commands/list.js +25 -0
- package/dist/cli/commands/log.js +63 -0
- package/dist/cli/commands/memory.js +241 -0
- package/dist/cli/commands/report.js +35 -0
- package/dist/cli/commands/session.js +74 -0
- package/dist/cli/commands/setup.js +86 -0
- package/dist/cli/commands/status.js +50 -0
- package/dist/cli/commands/telegram.js +701 -0
- package/dist/cli/commands/use.js +108 -0
- package/dist/cli/commands/version.js +12 -0
- package/dist/cli/index.js +276 -0
- package/dist/cli/output/table.js +22 -0
- package/dist/cli/prompts/prompt.js +37 -0
- package/dist/cli/registry.js +16 -0
- package/dist/core/config/cache-store.js +193 -0
- package/dist/core/config/json-store.js +114 -0
- package/dist/core/config/paths.js +85 -0
- package/dist/core/config/providers-store.js +89 -0
- package/dist/core/config/schema.js +60 -0
- package/dist/core/config/session-store.js +30 -0
- package/dist/core/config/usage-store.js +18 -0
- package/dist/core/context/context-service.js +186 -0
- package/dist/core/integrations/integration-state.js +105 -0
- package/dist/core/logs/log-service.js +56 -0
- package/dist/core/memory/embedding-check.js +121 -0
- package/dist/core/memory/memory-config.js +122 -0
- package/dist/core/models/model-discovery.js +430 -0
- package/dist/core/models/model-filter.js +13 -0
- package/dist/core/providers/provider-service.js +212 -0
- package/dist/core/reports/report-service.js +166 -0
- package/dist/core/runner/command-resolver.js +60 -0
- package/dist/core/runner/engine-registry.js +93 -0
- package/dist/core/runner/fallback.js +114 -0
- package/dist/core/runner/openai-usage-http.js +82 -0
- package/dist/core/runner/openai-usage-proxy.js +1 -0
- package/dist/core/runner/openai-usage-recording.js +172 -0
- package/dist/core/runner/openai-usage-responses.js +469 -0
- package/dist/core/runner/openai-usage-server.js +319 -0
- package/dist/core/runner/openai-usage-types.js +1 -0
- package/dist/core/runner/tool-runner.js +138 -0
- package/dist/core/sessions/session-service.js +47 -0
- package/dist/core/status/doctor-service.js +391 -0
- package/dist/core/status/status-service.js +60 -0
- package/dist/core/types.js +1 -0
- package/dist/core/usage/pricing.js +113 -0
- package/dist/core/usage/usage-service.js +30 -0
- package/dist/core/utils/is-record.js +3 -0
- package/dist/server/index.js +28 -0
- package/dist/server/runtime-server.js +430 -0
- package/dist/telegram/bot-registry.js +80 -0
- package/dist/telegram/bot.js +128 -0
- package/dist/telegram/config.js +235 -0
- package/dist/telegram/engine/claude-engine.js +240 -0
- package/dist/telegram/engine/codex-engine.js +437 -0
- package/dist/telegram/engine/engine-utils.js +67 -0
- package/dist/telegram/engine/process-utils.js +132 -0
- package/dist/telegram/engine/registry.js +31 -0
- package/dist/telegram/engine/types.js +1 -0
- package/dist/telegram/handler-registry.js +28 -0
- package/dist/telegram/handlers/callback.js +311 -0
- package/dist/telegram/handlers/command.js +272 -0
- package/dist/telegram/handlers/document.js +108 -0
- package/dist/telegram/handlers/memory.js +305 -0
- package/dist/telegram/handlers/message.js +701 -0
- package/dist/telegram/handlers/provider.js +332 -0
- package/dist/telegram/handlers/setup.js +527 -0
- package/dist/telegram/handlers/usage.js +124 -0
- package/dist/telegram/index.js +93 -0
- package/dist/telegram/interaction/approval.js +108 -0
- package/dist/telegram/interaction/command-menu.js +253 -0
- package/dist/telegram/interaction/formatter.js +487 -0
- package/dist/telegram/interaction/keyboards.js +145 -0
- package/dist/telegram/interaction/progress-reporter.js +160 -0
- package/dist/telegram/interaction/prompt-middleware.js +168 -0
- package/dist/telegram/interaction/result-store.js +41 -0
- package/dist/telegram/interaction/token-budget.js +21 -0
- package/dist/telegram/interaction/tool-name.js +41 -0
- package/dist/telegram/lifecycle-registry.js +47 -0
- package/dist/telegram/log.js +46 -0
- package/dist/telegram/memory/memory-inject.js +52 -0
- package/dist/telegram/memory/memory-service.js +413 -0
- package/dist/telegram/memory/memory-store.js +216 -0
- package/dist/telegram/memory/types.js +1 -0
- package/dist/telegram/network-retry.js +22 -0
- package/dist/telegram/network.js +53 -0
- package/dist/telegram/session/manager.js +229 -0
- package/dist/telegram/session/store.js +363 -0
- package/dist/telegram/session/types.js +1 -0
- package/dist/telegram/supervisor.js +57 -0
- package/dist/telegram/templates/messages.js +1 -0
- package/docs/README.md +98 -0
- package/docs/USAGE.html +853 -0
- package/package.json +57 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { withTelegramNetworkRetry } from "../network-retry.js";
|
|
2
|
+
import { escapeHtml, Formatter, truncateText } from "./formatter.js";
|
|
3
|
+
const MAX_TIMELINE_ENTRIES = 8;
|
|
4
|
+
export class ProgressReporter {
|
|
5
|
+
api;
|
|
6
|
+
chatId;
|
|
7
|
+
messageId;
|
|
8
|
+
pending;
|
|
9
|
+
timer;
|
|
10
|
+
finalized = false;
|
|
11
|
+
lastSignature = "";
|
|
12
|
+
toolCount = 0;
|
|
13
|
+
throttleMs;
|
|
14
|
+
silent;
|
|
15
|
+
timeline = [];
|
|
16
|
+
lastTimelinePhase;
|
|
17
|
+
responsePreview = "";
|
|
18
|
+
startTime = Date.now();
|
|
19
|
+
constructor(api, chatId, options = {}) {
|
|
20
|
+
this.api = api;
|
|
21
|
+
this.chatId = chatId;
|
|
22
|
+
this.silent = options.silent ?? false;
|
|
23
|
+
this.throttleMs = options.throttleMs ?? 1500;
|
|
24
|
+
}
|
|
25
|
+
async update(update) {
|
|
26
|
+
if (this.silent || this.finalized || update.phase === "done")
|
|
27
|
+
return;
|
|
28
|
+
if (update.phase === "tool_calling")
|
|
29
|
+
this.toolCount += 1;
|
|
30
|
+
this.pending = update;
|
|
31
|
+
if (this.timer)
|
|
32
|
+
return;
|
|
33
|
+
this.timer = setTimeout(() => {
|
|
34
|
+
this.timer = undefined;
|
|
35
|
+
void this.flush();
|
|
36
|
+
}, this.throttleMs);
|
|
37
|
+
this.timer.unref?.();
|
|
38
|
+
}
|
|
39
|
+
async finalize() {
|
|
40
|
+
if (this.finalized)
|
|
41
|
+
return;
|
|
42
|
+
this.finalized = true;
|
|
43
|
+
this.clearPending();
|
|
44
|
+
this.pending = undefined;
|
|
45
|
+
if (!this.messageId || this.silent)
|
|
46
|
+
return;
|
|
47
|
+
try {
|
|
48
|
+
await this.api.deleteMessage(this.chatId, this.messageId);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Progress messages are best-effort and must not affect task completion.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async replaceWithHtml(html, fallbackText, extra) {
|
|
55
|
+
if (this.silent || this.finalized)
|
|
56
|
+
return false;
|
|
57
|
+
this.clearPending();
|
|
58
|
+
this.pending = undefined;
|
|
59
|
+
if (!this.messageId)
|
|
60
|
+
return false;
|
|
61
|
+
try {
|
|
62
|
+
await withTelegramNetworkRetry(() => this.api.editMessageText(this.chatId, this.messageId, html, { parse_mode: "HTML", ...extra }));
|
|
63
|
+
this.finalized = true;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
this.handleTelegramError(error);
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
await withTelegramNetworkRetry(() => this.api.editMessageText(this.chatId, this.messageId, fallbackText || "(empty)", extra ? { ...extra } : undefined));
|
|
71
|
+
this.finalized = true;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
this.handleTelegramError(error);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async flush() {
|
|
80
|
+
if (this.silent || this.finalized || !this.pending)
|
|
81
|
+
return;
|
|
82
|
+
const update = this.pending;
|
|
83
|
+
this.pending = undefined;
|
|
84
|
+
const signature = buildProgressSignature(update, this.toolCount);
|
|
85
|
+
if (signature === this.lastSignature)
|
|
86
|
+
return;
|
|
87
|
+
if (update.responseText?.trim()) {
|
|
88
|
+
this.responsePreview = update.responseText.trim();
|
|
89
|
+
}
|
|
90
|
+
if (this.lastTimelinePhase !== update.phase || update.phase !== "writing") {
|
|
91
|
+
const entry = Formatter.progress(update, { toolCount: this.toolCount });
|
|
92
|
+
this.appendTimeline(entry);
|
|
93
|
+
this.lastTimelinePhase = update.phase;
|
|
94
|
+
}
|
|
95
|
+
const preview = this.responsePreview
|
|
96
|
+
? `<blockquote>${escapeHtml(truncateText(this.responsePreview, 1200))}</blockquote>`
|
|
97
|
+
: "";
|
|
98
|
+
const text = [Formatter.progressTimeline(this.getDisplayEntries()), preview].filter(Boolean).join("\n\n");
|
|
99
|
+
try {
|
|
100
|
+
if (!this.messageId) {
|
|
101
|
+
const message = await withTelegramNetworkRetry(() => this.api.sendMessage(this.chatId, text, { parse_mode: "HTML" }));
|
|
102
|
+
this.messageId = readMessageId(message);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
await withTelegramNetworkRetry(() => this.api.editMessageText(this.chatId, this.messageId, text, { parse_mode: "HTML" }));
|
|
106
|
+
}
|
|
107
|
+
this.lastSignature = signature;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
this.handleTelegramError(error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
appendTimeline(entry) {
|
|
114
|
+
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
|
|
115
|
+
const last = this.timeline[this.timeline.length - 1];
|
|
116
|
+
if (last && stripElapsed(last) === stripElapsed(entry)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this.timeline.push(`${entry} <i>${elapsed}s</i>`);
|
|
120
|
+
}
|
|
121
|
+
getDisplayEntries() {
|
|
122
|
+
if (this.timeline.length <= MAX_TIMELINE_ENTRIES)
|
|
123
|
+
return this.timeline;
|
|
124
|
+
const hidden = this.timeline.length - MAX_TIMELINE_ENTRIES;
|
|
125
|
+
return [`<i>… +${hidden} more</i>`, ...this.timeline.slice(-MAX_TIMELINE_ENTRIES)];
|
|
126
|
+
}
|
|
127
|
+
handleTelegramError(error) {
|
|
128
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
129
|
+
if (/429|Too Many Requests/i.test(message)) {
|
|
130
|
+
this.throttleMs = Math.min(this.throttleMs * 2, 5000);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
clearPending() {
|
|
134
|
+
if (!this.timer)
|
|
135
|
+
return;
|
|
136
|
+
clearTimeout(this.timer);
|
|
137
|
+
this.timer = undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function buildProgressSignature(update, toolCount) {
|
|
141
|
+
return [
|
|
142
|
+
update.phase,
|
|
143
|
+
update.message ?? "",
|
|
144
|
+
update.tool ?? "",
|
|
145
|
+
update.toolKind ?? "",
|
|
146
|
+
update.mcpServer ?? "",
|
|
147
|
+
update.responseText ?? "",
|
|
148
|
+
update.phase === "tool_calling" ? toolCount : ""
|
|
149
|
+
].join("\0");
|
|
150
|
+
}
|
|
151
|
+
function readMessageId(value) {
|
|
152
|
+
if (value && typeof value === "object" && "message_id" in value) {
|
|
153
|
+
const id = Number(value.message_id);
|
|
154
|
+
return Number.isFinite(id) ? id : undefined;
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
function stripElapsed(entry) {
|
|
159
|
+
return entry.replace(/ <i>[\d.]+s<\/i>$/, "");
|
|
160
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { buildPromptWithMemory } from "../memory/memory-inject.js";
|
|
2
|
+
import { countPromptTokens, truncateToTokenBudget } from "./token-budget.js";
|
|
3
|
+
const RECENT_CONVERSATION_TOKEN_BUDGET = 1400;
|
|
4
|
+
const MAX_TURN_TOKEN_BUDGET = 600;
|
|
5
|
+
const MIN_RECENT_TURN_TOKEN_BUDGET = 300;
|
|
6
|
+
const ATTACHMENT_TOKEN_BUDGET = 3000;
|
|
7
|
+
const MAX_ATTACHMENT_TOKEN_BUDGET = 1200;
|
|
8
|
+
const promptMiddlewares = [];
|
|
9
|
+
export function registerPromptMiddleware(middleware) {
|
|
10
|
+
const existingIndex = promptMiddlewares.findIndex((item) => item.name === middleware.name);
|
|
11
|
+
if (existingIndex >= 0) {
|
|
12
|
+
promptMiddlewares.splice(existingIndex, 1, middleware);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
promptMiddlewares.push(middleware);
|
|
16
|
+
}
|
|
17
|
+
promptMiddlewares.sort((a, b) => a.priority - b.priority || a.name.localeCompare(b.name));
|
|
18
|
+
}
|
|
19
|
+
export function listPromptMiddlewares() {
|
|
20
|
+
return [...promptMiddlewares];
|
|
21
|
+
}
|
|
22
|
+
export async function applyPromptMiddlewares(ctx) {
|
|
23
|
+
let prompt = ctx.prompt;
|
|
24
|
+
for (const middleware of [...promptMiddlewares].reverse()) {
|
|
25
|
+
prompt = await middleware.transform({ ...ctx, prompt });
|
|
26
|
+
}
|
|
27
|
+
return prompt;
|
|
28
|
+
}
|
|
29
|
+
function buildPromptWithAttachments(prompt, session) {
|
|
30
|
+
if (!session.attachments.length)
|
|
31
|
+
return prompt;
|
|
32
|
+
let remaining = ATTACHMENT_TOKEN_BUDGET;
|
|
33
|
+
const blocks = [];
|
|
34
|
+
for (const attachment of session.attachments) {
|
|
35
|
+
const name = escapeXmlAttribute(attachment.name);
|
|
36
|
+
const filePath = escapeXmlAttribute(attachment.path);
|
|
37
|
+
if (attachment.kind === "image") {
|
|
38
|
+
blocks.push(`<attachment name="${name}" path="${filePath}" kind="image" />`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (remaining <= 0)
|
|
42
|
+
break;
|
|
43
|
+
const budget = Math.min(remaining, MAX_ATTACHMENT_TOKEN_BUDGET);
|
|
44
|
+
const text = escapeXmlLikeText(attachment.text, budget);
|
|
45
|
+
const block = [
|
|
46
|
+
`<attachment name="${name}" path="${filePath}" kind="text">`,
|
|
47
|
+
text,
|
|
48
|
+
"</attachment>"
|
|
49
|
+
].join("\n");
|
|
50
|
+
remaining -= countPromptTokens(block);
|
|
51
|
+
blocks.push(block);
|
|
52
|
+
}
|
|
53
|
+
if (!blocks.length)
|
|
54
|
+
return prompt;
|
|
55
|
+
return [prompt, "", "Attached files:", ...blocks].join("\n");
|
|
56
|
+
}
|
|
57
|
+
const REPLY_CONTEXT_TOKEN_BUDGET = 800;
|
|
58
|
+
function buildPromptWithReplyContext(prompt, replyToText, includeAgentContext = true) {
|
|
59
|
+
if (!includeAgentContext)
|
|
60
|
+
return prompt;
|
|
61
|
+
if (!replyToText?.trim())
|
|
62
|
+
return prompt;
|
|
63
|
+
const truncated = truncateToTokenBudget(replyToText.trim(), REPLY_CONTEXT_TOKEN_BUDGET);
|
|
64
|
+
return [
|
|
65
|
+
`<referenced_message hint="The user is replying to this earlier message for context">`,
|
|
66
|
+
escapeXmlLikeText(truncated, REPLY_CONTEXT_TOKEN_BUDGET),
|
|
67
|
+
"</referenced_message>",
|
|
68
|
+
"",
|
|
69
|
+
prompt
|
|
70
|
+
].join("\n");
|
|
71
|
+
}
|
|
72
|
+
function buildPromptWithRecentConversation(prompt, session, resume, includeAgentContext = true) {
|
|
73
|
+
if (!includeAgentContext)
|
|
74
|
+
return prompt;
|
|
75
|
+
if (resume)
|
|
76
|
+
return prompt;
|
|
77
|
+
if (prompt.trimStart().startsWith("/"))
|
|
78
|
+
return prompt;
|
|
79
|
+
let remainingBudget = RECENT_CONVERSATION_TOKEN_BUDGET;
|
|
80
|
+
const pinnedBlocks = [];
|
|
81
|
+
for (const pin of session.pinnedTurns) {
|
|
82
|
+
const block = buildConversationBlock(pin.user, pin.assistant, remainingBudget, `<turn pinned="true">`);
|
|
83
|
+
if (!block)
|
|
84
|
+
break;
|
|
85
|
+
const cost = countPromptTokens(block.join("\n"));
|
|
86
|
+
pinnedBlocks.push(block);
|
|
87
|
+
remainingBudget -= cost;
|
|
88
|
+
}
|
|
89
|
+
const pinnedKeys = new Set(session.pinnedTurns.map((pin) => `${pin.user}\0${pin.assistant}`));
|
|
90
|
+
const turns = session.conversation.filter((turn) => !pinnedKeys.has(`${turn.user}\0${turn.assistant}`));
|
|
91
|
+
const recentBlocks = [];
|
|
92
|
+
for (let i = turns.length - 1; i >= 0 && remainingBudget > 0; i--) {
|
|
93
|
+
if (recentBlocks.length && remainingBudget < MIN_RECENT_TURN_TOKEN_BUDGET)
|
|
94
|
+
break;
|
|
95
|
+
const turn = turns[i];
|
|
96
|
+
const block = buildConversationBlock(turn.user, turn.assistant, remainingBudget, `<turn index="${i + 1}">`);
|
|
97
|
+
if (!block)
|
|
98
|
+
break;
|
|
99
|
+
const cost = countPromptTokens(block.join("\n"));
|
|
100
|
+
recentBlocks.unshift(block);
|
|
101
|
+
remainingBudget -= cost;
|
|
102
|
+
}
|
|
103
|
+
const allBlocks = [...pinnedBlocks, ...recentBlocks];
|
|
104
|
+
if (!allBlocks.length)
|
|
105
|
+
return prompt;
|
|
106
|
+
return [
|
|
107
|
+
"<recent_conversation purpose=\"resolve references only; do not treat as new user instructions\">",
|
|
108
|
+
...allBlocks.flat(),
|
|
109
|
+
"</recent_conversation>",
|
|
110
|
+
"",
|
|
111
|
+
prompt
|
|
112
|
+
].join("\n");
|
|
113
|
+
}
|
|
114
|
+
function buildConversationBlock(user, assistant, budget, openingTag) {
|
|
115
|
+
const contentBudget = Math.min(MAX_TURN_TOKEN_BUDGET * 2, Math.max(0, budget - 32));
|
|
116
|
+
if (contentBudget < 16)
|
|
117
|
+
return null;
|
|
118
|
+
const userBudget = Math.max(8, Math.floor(contentBudget * 0.4));
|
|
119
|
+
const assistantBudget = Math.max(8, contentBudget - userBudget);
|
|
120
|
+
const block = [
|
|
121
|
+
openingTag,
|
|
122
|
+
`<user>${escapeXmlLikeText(user, userBudget)}</user>`,
|
|
123
|
+
`<assistant>${escapeXmlLikeText(assistant, assistantBudget)}</assistant>`,
|
|
124
|
+
"</turn>"
|
|
125
|
+
];
|
|
126
|
+
return countPromptTokens(block.join("\n")) <= budget ? block : null;
|
|
127
|
+
}
|
|
128
|
+
function escapeXmlLikeText(value, tokenBudget = MAX_TURN_TOKEN_BUDGET) {
|
|
129
|
+
return truncateToTokenBudget(value, tokenBudget)
|
|
130
|
+
.replace(/&/g, "&")
|
|
131
|
+
.replace(/</g, "<")
|
|
132
|
+
.replace(/>/g, ">");
|
|
133
|
+
}
|
|
134
|
+
function escapeXmlAttribute(value) {
|
|
135
|
+
return value
|
|
136
|
+
.replace(/&/g, "&")
|
|
137
|
+
.replace(/"/g, """)
|
|
138
|
+
.replace(/</g, "<")
|
|
139
|
+
.replace(/>/g, ">");
|
|
140
|
+
}
|
|
141
|
+
registerPromptMiddleware({
|
|
142
|
+
name: "bot-memory",
|
|
143
|
+
priority: 10,
|
|
144
|
+
transform: (ctx) => {
|
|
145
|
+
if (!ctx.memoryService)
|
|
146
|
+
return ctx.prompt;
|
|
147
|
+
if (!ctx.includeAgentContext)
|
|
148
|
+
return ctx.prompt;
|
|
149
|
+
if (ctx.prompt.trimStart().startsWith("/"))
|
|
150
|
+
return ctx.prompt;
|
|
151
|
+
return buildPromptWithMemory(ctx.memoryService, ctx.chatId, ctx.prompt).prompt;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
registerPromptMiddleware({
|
|
155
|
+
name: "reply-context",
|
|
156
|
+
priority: 15,
|
|
157
|
+
transform: (ctx) => buildPromptWithReplyContext(ctx.prompt, ctx.replyToText, ctx.includeAgentContext)
|
|
158
|
+
});
|
|
159
|
+
registerPromptMiddleware({
|
|
160
|
+
name: "recent-conversation",
|
|
161
|
+
priority: 20,
|
|
162
|
+
transform: (ctx) => buildPromptWithRecentConversation(ctx.prompt, ctx.session, ctx.resume, ctx.includeAgentContext)
|
|
163
|
+
});
|
|
164
|
+
registerPromptMiddleware({
|
|
165
|
+
name: "attachments",
|
|
166
|
+
priority: 30,
|
|
167
|
+
transform: (ctx) => buildPromptWithAttachments(ctx.prompt, ctx.session)
|
|
168
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
2
|
+
function resolveTtlMs() {
|
|
3
|
+
const raw = process.env.TELEGRAM_RESULT_TTL_MS;
|
|
4
|
+
if (!raw)
|
|
5
|
+
return DEFAULT_TTL_MS;
|
|
6
|
+
const parsed = Number.parseInt(raw, 10);
|
|
7
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
8
|
+
return DEFAULT_TTL_MS;
|
|
9
|
+
return parsed;
|
|
10
|
+
}
|
|
11
|
+
export class ResultStore {
|
|
12
|
+
store = new Map();
|
|
13
|
+
ttlMs;
|
|
14
|
+
constructor(ttlMs) {
|
|
15
|
+
this.ttlMs = ttlMs ?? resolveTtlMs();
|
|
16
|
+
}
|
|
17
|
+
set(chatId, result) {
|
|
18
|
+
this.cleanup();
|
|
19
|
+
this.store.set(chatId, { result, expires: Date.now() + this.ttlMs });
|
|
20
|
+
}
|
|
21
|
+
get(chatId) {
|
|
22
|
+
const entry = this.store.get(chatId);
|
|
23
|
+
if (!entry)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (Date.now() > entry.expires) {
|
|
26
|
+
this.store.delete(chatId);
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return entry.result;
|
|
30
|
+
}
|
|
31
|
+
delete(chatId) {
|
|
32
|
+
this.store.delete(chatId);
|
|
33
|
+
}
|
|
34
|
+
cleanup() {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
for (const [id, entry] of this.store) {
|
|
37
|
+
if (now > entry.expires)
|
|
38
|
+
this.store.delete(id);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { decode, encode } from "gpt-tokenizer";
|
|
2
|
+
export function countPromptTokens(value) {
|
|
3
|
+
return encode(value).length;
|
|
4
|
+
}
|
|
5
|
+
export function isWithinTokenBudget(value, maxTokens) {
|
|
6
|
+
return countPromptTokens(value) <= maxTokens;
|
|
7
|
+
}
|
|
8
|
+
export function truncateToTokenBudget(value, maxTokens) {
|
|
9
|
+
if (maxTokens <= 0)
|
|
10
|
+
return "";
|
|
11
|
+
const tokens = encode(value);
|
|
12
|
+
if (tokens.length <= maxTokens)
|
|
13
|
+
return value;
|
|
14
|
+
const suffix = "...";
|
|
15
|
+
const suffixTokens = encode(suffix);
|
|
16
|
+
if (suffixTokens.length >= maxTokens) {
|
|
17
|
+
return decode(suffixTokens.slice(0, maxTokens));
|
|
18
|
+
}
|
|
19
|
+
const bodyTokens = tokens.slice(0, maxTokens - suffixTokens.length);
|
|
20
|
+
return `${decode(bodyTokens).trimEnd()}${suffix}`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const MAX_TOOL_PART_LENGTH = 24;
|
|
2
|
+
export function parseToolName(value, options = {}) {
|
|
3
|
+
const raw = typeof value === "string" && value.trim() ? value.trim() : "tool";
|
|
4
|
+
if (options.kind === "mcp") {
|
|
5
|
+
return {
|
|
6
|
+
raw,
|
|
7
|
+
name: truncateToolPart(raw),
|
|
8
|
+
kind: "mcp",
|
|
9
|
+
mcpServer: truncateToolPart(options.mcpServer || "mcp")
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (options.kind === "command") {
|
|
13
|
+
return {
|
|
14
|
+
raw,
|
|
15
|
+
name: truncateToolPart(raw),
|
|
16
|
+
kind: "command"
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const mcp = raw.match(/^mcp__(.+?)__(.+)$/);
|
|
20
|
+
if (mcp) {
|
|
21
|
+
return {
|
|
22
|
+
raw,
|
|
23
|
+
name: truncateToolPart(mcp[2] || "tool"),
|
|
24
|
+
kind: "mcp",
|
|
25
|
+
mcpServer: truncateToolPart(mcp[1] || "mcp")
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
raw,
|
|
30
|
+
name: truncateToolPart(raw),
|
|
31
|
+
kind: "builtin"
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function truncateToolPart(value, maxLength = MAX_TOOL_PART_LENGTH) {
|
|
35
|
+
const text = value.trim() || "tool";
|
|
36
|
+
if (text.length <= maxLength)
|
|
37
|
+
return text;
|
|
38
|
+
if (maxLength <= 3)
|
|
39
|
+
return text.slice(0, maxLength);
|
|
40
|
+
return `${text.slice(0, maxLength - 3)}...`;
|
|
41
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { writeTelegramLog } from "./log.js";
|
|
2
|
+
export class TelegramTaskLifecycleRegistry {
|
|
3
|
+
hooks = new Set();
|
|
4
|
+
register(hook) {
|
|
5
|
+
this.hooks.add(hook);
|
|
6
|
+
return () => {
|
|
7
|
+
this.hooks.delete(hook);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
async emitTaskCompleted(event) {
|
|
11
|
+
for (const hook of this.hooks) {
|
|
12
|
+
if (!hook.onTaskCompleted)
|
|
13
|
+
continue;
|
|
14
|
+
await this.callHook("task_completed", () => hook.onTaskCompleted(event));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async emitTaskFailed(event) {
|
|
18
|
+
for (const hook of this.hooks) {
|
|
19
|
+
if (!hook.onTaskFailed)
|
|
20
|
+
continue;
|
|
21
|
+
await this.callHook("task_failed", () => hook.onTaskFailed(event));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async callHook(action, fn) {
|
|
25
|
+
try {
|
|
26
|
+
await fn();
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
await writeTelegramLog({
|
|
30
|
+
level: "error",
|
|
31
|
+
action: "lifecycle_hook_failed",
|
|
32
|
+
message: error instanceof Error ? error.message : String(error),
|
|
33
|
+
metadata: { lifecycleAction: action }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const defaultTelegramLifecycleRegistry = new TelegramTaskLifecycleRegistry();
|
|
39
|
+
export function registerTelegramTaskLifecycleHook(hook) {
|
|
40
|
+
return defaultTelegramLifecycleRegistry.register(hook);
|
|
41
|
+
}
|
|
42
|
+
export function emitTelegramTaskCompleted(event) {
|
|
43
|
+
return defaultTelegramLifecycleRegistry.emitTaskCompleted(event);
|
|
44
|
+
}
|
|
45
|
+
export function emitTelegramTaskFailed(event) {
|
|
46
|
+
return defaultTelegramLifecycleRegistry.emitTaskFailed(event);
|
|
47
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getTelegramLogPath } from "../core/config/paths.js";
|
|
4
|
+
import { writeLog } from "../core/logs/log-service.js";
|
|
5
|
+
export async function writeTelegramLog(input) {
|
|
6
|
+
const line = {
|
|
7
|
+
timestamp: new Date().toISOString(),
|
|
8
|
+
level: input.level ?? "info",
|
|
9
|
+
action: input.action,
|
|
10
|
+
message: input.message,
|
|
11
|
+
metadata: input.metadata
|
|
12
|
+
};
|
|
13
|
+
await writeLog({
|
|
14
|
+
level: line.level,
|
|
15
|
+
category: "telegram",
|
|
16
|
+
action: line.action,
|
|
17
|
+
message: line.message,
|
|
18
|
+
metadata: line.metadata
|
|
19
|
+
});
|
|
20
|
+
try {
|
|
21
|
+
const filePath = getTelegramLogPath();
|
|
22
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
23
|
+
await fs.appendFile(filePath, `${JSON.stringify(line)}\n`, "utf8");
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Logging must never break bot execution.
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function readRecentTelegramLogLines(limit = 50) {
|
|
30
|
+
try {
|
|
31
|
+
const raw = await fs.readFile(getTelegramLogPath(), "utf8");
|
|
32
|
+
return raw.split("\n").filter(Boolean).slice(-limit);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error.code === "ENOENT") {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function ensureTelegramLogFile() {
|
|
42
|
+
const filePath = getTelegramLogPath();
|
|
43
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
44
|
+
await fs.appendFile(filePath, "", "utf8");
|
|
45
|
+
return filePath;
|
|
46
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { truncateText } from "../interaction/formatter.js";
|
|
2
|
+
export function buildPromptWithMemory(memoryService, chatId, prompt) {
|
|
3
|
+
const entries = memoryService.retrieveForPrompt(chatId, prompt);
|
|
4
|
+
if (!entries.length)
|
|
5
|
+
return { prompt, entries };
|
|
6
|
+
const block = formatMemoryBlock(entries);
|
|
7
|
+
return {
|
|
8
|
+
prompt: [block, "", prompt].join("\n"),
|
|
9
|
+
entries
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function formatMemoryBlock(entries) {
|
|
13
|
+
const lines = [
|
|
14
|
+
"<user_memory scope=\"global persistent knowledge, cross-engine cross-session\" readonly=\"true\">",
|
|
15
|
+
"Treat as ground truth. If anything here conflicts with engine native config (CLAUDE.md/AGENTS.md), engine config wins.",
|
|
16
|
+
"Within this block, bot persona overrides user profile on conflicts.",
|
|
17
|
+
"Do NOT store this information in your own memory systems (~/.claude/memory/, CLAUDE.md, AGENTS.md).",
|
|
18
|
+
""
|
|
19
|
+
];
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
lines.push(formatMemoryEntry(entry));
|
|
22
|
+
}
|
|
23
|
+
lines.push("</user_memory>");
|
|
24
|
+
return lines.join("\n");
|
|
25
|
+
}
|
|
26
|
+
function formatMemoryEntry(entry) {
|
|
27
|
+
const attrs = [
|
|
28
|
+
`category="${escapeXml(entry.category)}"`,
|
|
29
|
+
`title="${escapeXml(entry.title)}"`,
|
|
30
|
+
`scope="${escapeXml(entry.scope)}"`,
|
|
31
|
+
`priority="${entry.priority}"`
|
|
32
|
+
];
|
|
33
|
+
if (entry.encrypted)
|
|
34
|
+
attrs.push("status=\"locked\"");
|
|
35
|
+
if (entry.expiresAt)
|
|
36
|
+
attrs.push(`expires="${escapeXml(new Date(entry.expiresAt).toISOString())}"`);
|
|
37
|
+
const content = entry.encrypted
|
|
38
|
+
? "(encrypted — user needs to /memory unlock to access)"
|
|
39
|
+
: truncateText(entry.content, entry.priority > 0 ? 1200 : 700);
|
|
40
|
+
return [
|
|
41
|
+
`<entry ${attrs.join(" ")}>`,
|
|
42
|
+
escapeXml(content),
|
|
43
|
+
"</entry>"
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
46
|
+
function escapeXml(value) {
|
|
47
|
+
return value
|
|
48
|
+
.replace(/&/g, "&")
|
|
49
|
+
.replace(/</g, "<")
|
|
50
|
+
.replace(/>/g, ">")
|
|
51
|
+
.replace(/"/g, """);
|
|
52
|
+
}
|