@llblab/pi-telegram 0.27.4 → 0.27.6
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/CHANGELOG.md +11 -0
- package/README.md +1 -1
- package/docs/activity.md +1 -1
- package/docs/multi-instance-bus.md +2 -0
- package/docs/outbound.md +2 -2
- package/index.ts +48 -0
- package/lib/activity.ts +3 -1
- package/lib/agent-messages.ts +116 -0
- package/lib/bindings.ts +6 -0
- package/lib/bus-follower.ts +81 -0
- package/lib/bus-leader.ts +94 -0
- package/lib/bus.ts +114 -0
- package/lib/outbound-attachments.ts +63 -5
- package/lib/prompts.ts +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.27.6: Autonomous Follow-Up Delivery Hotfix
|
|
4
|
+
|
|
5
|
+
- `Follow-Up Finals`: Completed public assistant blocks from unclassified Pi turns now follow the default-enabled Proactive Push path, alongside explicitly local and autonomous turns. Impact: actor terminal follow-ups and comparable extension-triggered continuations no longer show reasoning/activity in Telegram while silently dropping the final answer.
|
|
6
|
+
- `Safety and Coverage`: Telegram-originated finals remain exclusively owned by active-turn settlement, while unclassified output still requires Proactive Push policy plus fresh direct/follower transport authority. Regression coverage proves unclassified finals deliver once when enabled and remain suppressed when disabled.
|
|
7
|
+
|
|
8
|
+
## 0.27.5: Cross-Instance Agent Turns
|
|
9
|
+
|
|
10
|
+
- `Live Thread Addressing`: `telegram_message` now accepts `thread` as a case-insensitive live thread name or numeric id. Resolution uses leader-owned live registrations and rejects unknown, ambiguous, same-target, offline, and cross-chat destinations before visible Telegram mutation.
|
|
11
|
+
- `Agent Turn Routing`: A successful targeted send routes the same text through the authenticated, generation-fenced bus into the owning Pi instance as `[agent|from-thread:<name>]`, preserving one visible Telegram message without impersonating direct user input. Leader and follower targets share the ordinary inbound queue path, request deduplication, rendering, buttons, and authority fencing.
|
|
12
|
+
- `Compatibility and Coverage`: Existing default and `chat_id` / `thread_id` behavior remains compatible; active-turn cross-thread sends gain agent routing when available. Regressions cover protocol parsing, stale generations, name resolution, ambiguity, attribution, preflight ordering, and tool delivery.
|
|
13
|
+
|
|
3
14
|
## 0.27.4: Disconnect Leadership Release Hotfix
|
|
4
15
|
|
|
5
16
|
- `Leadership Release`: Manual disconnect now treats thread deletion as a durable cleanup side effect rather than a prerequisite for relinquishing transport leadership. A leader or promoted follower persists the exact cleanup intent, attempts close/delete under its epoch, and then stops polling and releases the owner lock even when Telegram rejects cleanup or the epoch changes. Impact: `Telegram Error` after `/telegram-disconnect` can no longer leave a live process pinning leadership and force newly connected instances to become followers.
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
It is a **runtime adapter**, not a remote terminal. Start or supervise work in the Pi TUI, then continue from Telegram while away from the keyboard. Each Telegram destination follows a running Pi instance and sends prompts into that instance's currently active session; it is not permanently bound to one session file or session identity. The bridge preserves Pi session semantics instead of pretending Telegram is a PTY, shell, process launcher, or session browser. That boundary is the product: Telegram gets safe runtime handles, not raw terminal power.
|
|
10
10
|
|
|
11
|
-
Every completed intermediate commentary block from a Telegram-originated turn is delivered once as its own message before the existing final reply. Proactive push is enabled by default for local and
|
|
11
|
+
Every completed intermediate commentary block from a Telegram-originated turn is delivered once as its own message before the existing final reply. Proactive push is enabled by default for local, autonomous, and unclassified extension follow-up work: `assistant.proactivePush` projects visible checkpoints and the final answer to the authorized Telegram target once and in order; set it explicitly to `false` to disable only that local/autonomous projection. Neither path mirrors local prompts, thinking, tool traffic, token deltas, or stale-generation work. The separate `Activity` setting defaults to `verbose` so new installations discover collapsed provider-exposed thinking and tool evidence immediately; operators can narrow it to one class or choose `quiet`. See [Outbound](docs/outbound.md#public-assistant-output) and the [configuration reference](docs/public-api.md#configuration-api).
|
|
12
12
|
|
|
13
13
|
This repository is an actively maintained fork of [`badlogic/pi-telegram`](https://github.com/badlogic/pi-telegram). It started from upstream commit [`cb34008`](https://github.com/badlogic/pi-telegram/commit/cb34008460b6c1ca036d92322f69d87f626be0fc) and has since diverged substantially.
|
|
14
14
|
|
package/docs/activity.md
CHANGED
|
@@ -236,7 +236,7 @@ The Delivery API independently serializes concrete Telegram operations per targe
|
|
|
236
236
|
|
|
237
237
|
### Core assistant-output projection
|
|
238
238
|
|
|
239
|
-
Activity's built-in assistant-output projection uses the same normalized `assistant-segment` boundary exposed to public handlers. For `telegram` activity it always projects complete `intermediate` commentary to the immutable originating target, while final and terminal-partial segments remain with active-turn settlement. For `local` or
|
|
239
|
+
Activity's built-in assistant-output projection uses the same normalized `assistant-segment` boundary exposed to public handlers. For `telegram` activity it always projects complete `intermediate` commentary to the immutable originating target, while final and terminal-partial segments remain with active-turn settlement. For `local`, `autonomous`, or unclassified extension follow-up activity, `assistant.proactivePush` enables projection of every completed public block, including intermediate commentary/checkpoints and the final block. This closes actor-follow-up delivery without reclassifying it as direct user input. It never projects text token deltas, reasoning events, tool events or payloads, or empty text.
|
|
240
240
|
|
|
241
241
|
The projection does not delay Activity dispatch or Pi lifecycle. Its ordered admission tail deduplicates normalized event identity, while existing routing and outbound owners revalidate the immutable admission-time target, profile/token transport generation, direct leader epoch or follower registration generation, and session generation immediately before each send. Active-turn final delivery waits for admitted commentary inside its existing background task. A replacement or stale owner drops queued work rather than rerouting it, and an already-started non-idempotent Bot API mutation follows the normal `commit-unknown` no-replay contract.
|
|
242
242
|
|
|
@@ -281,6 +281,8 @@ Follower instances receive normalized events, not raw Telegram transport interna
|
|
|
281
281
|
|
|
282
282
|
Followers do not call Telegram Bot API directly for routed Telegram work. Instead, they call a leader-owned transport port:
|
|
283
283
|
|
|
284
|
+
Explicit `telegram_message(..., thread)` delivery also uses the bus as an agent-message plane. The leader resolves the case-insensitive name or numeric id against live leader/follower registrations, rejects ambiguous, stale, same-instance, cross-chat, and replayed requests, then coordinates visible Bot API delivery with one source-attributed synthetic turn routed through the destination instance's ordinary queue. The destination sees `[agent|from-thread:<name>]`, not an impersonated user message. Generation fencing and request-ledger deduplication apply to both resolution and routing.
|
|
285
|
+
|
|
284
286
|
```text
|
|
285
287
|
follower reply/preview/upload/chat-action/download/callback-answer -> leader IPC -> Telegram API
|
|
286
288
|
```
|
package/docs/outbound.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`pi-telegram` maps hidden assistant-authored HTML comments to Telegram-native outbound actions.
|
|
4
4
|
|
|
5
|
-
Normal Telegram-turn replies are intentionally prompt-driven: the agent writes Markdown plus small hidden top-level blocks, and the bridge performs transport after `agent_end`. `telegram_voice` and `telegram_button` are not Pi tools. For local/TUI-initiated work where the user explicitly asks to send something to Telegram, the bridge also exposes direct tools: `telegram_message` for Markdown text and `telegram_attach` for file delivery when no Telegram turn is active. In classic mode, direct local/TUI delivery requires this Pi instance to own `/telegram-connect`; in Threaded Mode, a registered follower may route direct-tool sends through the leader-owned bus transport. During an active Telegram turn, `telegram_message` rejects an implicit or same-turn target so ordinary final delivery remains the sole current-target response
|
|
5
|
+
Normal Telegram-turn replies are intentionally prompt-driven: the agent writes Markdown plus small hidden top-level blocks, and the bridge performs transport after `agent_end`. `telegram_voice` and `telegram_button` are not Pi tools. For local/TUI-initiated work where the user explicitly asks to send something to Telegram, the bridge also exposes direct tools: `telegram_message` for Markdown text and `telegram_attach` for file delivery when no Telegram turn is active. In classic mode, direct local/TUI delivery requires this Pi instance to own `/telegram-connect`; in Threaded Mode, a registered follower may route direct-tool sends through the leader-owned bus transport. During an active Telegram turn, `telegram_message` rejects an implicit or same-turn target so ordinary final delivery remains the sole current-target response. Its `thread` argument accepts a case-insensitive live thread name or numeric id: the bridge preflights one live owner, sends visibly, then admits the text as a source-attributed turn in that instance. Unknown, ambiguous, same-target, offline, and cross-chat destinations fail before sending. Existing `chat_id` plus `thread_id` targeting stays compatible; registered followers use authenticated, generation-fenced bus routing. Outbound behavior combines assistant prompt markup, text command-template handlers, registered voice synthesis providers, generated artifacts, direct Telegram tools, and reply delivery. Direct `telegram_message` text is planned through the same reply markup path, so embedded top-level `telegram_button` comments become buttons attached to that text message.
|
|
6
6
|
|
|
7
7
|
Text handlers use the portable [Command Template Standard](./command-templates.md). Programmatic outbound handlers use `registerTelegramOutboundHandler(kind, handler)`. Voice replies can use configured command-template handlers or the provider API described in [Voice Integration](./voice.md).
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ Text handlers use the portable [Command Template Standard](./command-templates.m
|
|
|
10
10
|
|
|
11
11
|
Every completed `assistant-segment` with `placement: "intermediate"` from a Telegram-originated turn is delivered as its own message to the immutable originating target before the ordinary active-turn final reply. This commentary path always applies; `assistant.proactivePush` does not disable it. Final and terminal-partial Telegram segments remain owned by active-turn settlement so the final answer, voice, buttons, previews, and artifacts are not duplicated.
|
|
12
12
|
|
|
13
|
-
Proactive projection defaults on for local and
|
|
13
|
+
Proactive projection defaults on for local, autonomous, and unclassified extension follow-up Pi work. With `assistant.proactivePush` omitted or set to `true`, every completed public block—including visible commentary/checkpoints and the final answer—is projected to the instance's authorized target in source order; set it explicitly to `false` to opt out of only this local/autonomous projection. Both paths consume normalized complete Activity segments rather than raw token deltas, reasoning, or tool traffic.
|
|
14
14
|
|
|
15
15
|
Projected blocks use `assistant.rendering` independently of voice policy. Rich mode sends native Rich Markdown and HTML mode keeps the established HTML renderer; projection does not synthesize voice or attach queued files merely because Rich rendering is active. Ordered admission revalidates the exact target, profile/token transport generation, leader epoch or follower registration generation, and session generation before each send. Active-turn final delivery waits for admitted commentary inside its existing background delivery task, preserving commentary-before-final order without blocking Pi lifecycle completion. A `commit-unknown` outcome never permits replay.
|
|
16
16
|
|
package/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as Activity from "./lib/activity.ts";
|
|
8
|
+
import * as AgentMessages from "./lib/agent-messages.ts";
|
|
8
9
|
import * as ActivityVerbosity from "./lib/activity-verbosity.ts";
|
|
9
10
|
import * as Bindings from "./lib/bindings.ts";
|
|
10
11
|
import * as BusApi from "./lib/bus-api.ts";
|
|
@@ -811,6 +812,17 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
811
812
|
handleAuthorizedReactionUpdate:
|
|
812
813
|
inboundRouteRuntime.handleAuthorizedReactionUpdate,
|
|
813
814
|
});
|
|
815
|
+
const agentMessageRuntime = AgentMessages.createTelegramAgentMessageRuntime({
|
|
816
|
+
instanceId: telegramInstanceId,
|
|
817
|
+
getAllowedChatId: configStore.getAllowedUserId,
|
|
818
|
+
getLeaderTarget: telegramBusLeaderState.getTarget,
|
|
819
|
+
getLeaderThreadName() {
|
|
820
|
+
return findCurrentThreadRecord()?.threadName;
|
|
821
|
+
},
|
|
822
|
+
followerRegistry: telegramBusFollowerRegistry,
|
|
823
|
+
getContext: telegramSessionContextStore.get,
|
|
824
|
+
handleUpdate: inboundRouteRuntime.handleUpdate,
|
|
825
|
+
});
|
|
814
826
|
const telegramBusFollowerAssemblyDeps =
|
|
815
827
|
BusFollower.createTelegramBusFollowerRuntimeAssemblyDeps<
|
|
816
828
|
Pi.ExtensionContext,
|
|
@@ -898,6 +910,16 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
898
910
|
startPolling: pollingRuntime.start,
|
|
899
911
|
stopPolling: pollingRuntime.stop,
|
|
900
912
|
authorizeFollowerApiCall,
|
|
913
|
+
resolveAgentTarget(follower, selector) {
|
|
914
|
+
return agentMessageRuntime.resolveTarget(selector, follower.target);
|
|
915
|
+
},
|
|
916
|
+
routeAgentMessage(follower, message) {
|
|
917
|
+
return agentMessageRuntime.route({
|
|
918
|
+
sourceTarget: follower.target,
|
|
919
|
+
sourceThreadName: follower.threadName,
|
|
920
|
+
message,
|
|
921
|
+
});
|
|
922
|
+
},
|
|
901
923
|
isFollowerProcessAlive: Locks.isProcessAlive,
|
|
902
924
|
shouldCleanupConfirmedDeadFollower:
|
|
903
925
|
configControls.resolveAutomaticThreadCleanupEnabled,
|
|
@@ -1094,6 +1116,32 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1094
1116
|
callMultipart,
|
|
1095
1117
|
getDefaultChatId: proactivePushChatIdGetter,
|
|
1096
1118
|
getDefaultTarget: proactivePushTargetGetter,
|
|
1119
|
+
async resolveAgentTarget(selector) {
|
|
1120
|
+
if (lockRuntime.owns()) {
|
|
1121
|
+
const target = agentMessageRuntime.resolveTarget(
|
|
1122
|
+
selector,
|
|
1123
|
+
proactivePushTargetGetter(),
|
|
1124
|
+
);
|
|
1125
|
+
if (!target) {
|
|
1126
|
+
throw new Error(
|
|
1127
|
+
"Telegram agent target is unavailable, ambiguous, or not live.",
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
return target;
|
|
1131
|
+
}
|
|
1132
|
+
return telegramBusFollowerClients.agentMessages.resolveTarget(selector);
|
|
1133
|
+
},
|
|
1134
|
+
async routeAgentMessage(message) {
|
|
1135
|
+
if (lockRuntime.owns()) {
|
|
1136
|
+
await agentMessageRuntime.route({
|
|
1137
|
+
sourceTarget: proactivePushTargetGetter(),
|
|
1138
|
+
sourceThreadName: findCurrentThreadRecord()?.threadName,
|
|
1139
|
+
message,
|
|
1140
|
+
});
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
await telegramBusFollowerClients.agentMessages.routeMessage(message);
|
|
1144
|
+
},
|
|
1097
1145
|
canSendDirect() {
|
|
1098
1146
|
return (
|
|
1099
1147
|
ownsTelegramDirectDelivery() ||
|
package/lib/activity.ts
CHANGED
|
@@ -732,7 +732,9 @@ export function createTelegramAssistantOutputRuntime<TAuthority = undefined>(dep
|
|
|
732
732
|
const admitted = new Set<string>();
|
|
733
733
|
const isEligibleEvent = (event: TelegramAssistantSegmentEvent): boolean =>
|
|
734
734
|
(event.source === "telegram" && event.placement === "intermediate") ||
|
|
735
|
-
((event.source === "local" ||
|
|
735
|
+
((event.source === "local" ||
|
|
736
|
+
event.source === "autonomous" ||
|
|
737
|
+
event.source === "unknown") &&
|
|
736
738
|
deps.isEnabled());
|
|
737
739
|
|
|
738
740
|
return {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-instance Telegram agent message resolution and turn injection
|
|
3
|
+
* Zones: multi-instance bus, inbound routing, operational delivery
|
|
4
|
+
* Owns live target resolution, source attribution, same-target fencing, and synthetic turn construction; excludes Bot API delivery and bus transport.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
TelegramBusAgentMessage,
|
|
9
|
+
TelegramBusAgentTargetSelector,
|
|
10
|
+
TelegramBusFollowerRegistry,
|
|
11
|
+
} from "./bus.ts";
|
|
12
|
+
import type { TelegramRoutedMessage } from "./routing.ts";
|
|
13
|
+
import type { TelegramTarget } from "./target.ts";
|
|
14
|
+
|
|
15
|
+
export interface TelegramAgentMessageRuntimeDeps<TContext, TUpdate> {
|
|
16
|
+
instanceId: string;
|
|
17
|
+
getAllowedChatId: () => number | undefined;
|
|
18
|
+
getLeaderTarget: () => TelegramTarget | undefined;
|
|
19
|
+
getLeaderThreadName: () => string | undefined;
|
|
20
|
+
followerRegistry: TelegramBusFollowerRegistry;
|
|
21
|
+
getContext: () => TContext | undefined;
|
|
22
|
+
handleUpdate: (update: TUpdate, ctx: TContext) => Promise<void>;
|
|
23
|
+
getNowMs?: () => number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createTelegramAgentMessageRuntime<TContext, TUpdate>(
|
|
27
|
+
deps: TelegramAgentMessageRuntimeDeps<TContext, TUpdate>,
|
|
28
|
+
) {
|
|
29
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
30
|
+
const sameTarget = (left?: TelegramTarget, right?: TelegramTarget) =>
|
|
31
|
+
left?.chatId === right?.chatId && left?.threadId === right?.threadId;
|
|
32
|
+
const listLiveTargets = () => {
|
|
33
|
+
const targets: Array<{
|
|
34
|
+
target: TelegramTarget & { threadId: number };
|
|
35
|
+
threadName?: string;
|
|
36
|
+
}> = [];
|
|
37
|
+
const leaderTarget = deps.getLeaderTarget();
|
|
38
|
+
if (leaderTarget?.threadId) {
|
|
39
|
+
targets.push({
|
|
40
|
+
target: { chatId: leaderTarget.chatId, threadId: leaderTarget.threadId },
|
|
41
|
+
threadName: deps.getLeaderThreadName(),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
for (const follower of deps.followerRegistry.list()) {
|
|
45
|
+
if (!follower.target?.threadId) continue;
|
|
46
|
+
targets.push({
|
|
47
|
+
target: {
|
|
48
|
+
chatId: follower.target.chatId,
|
|
49
|
+
threadId: follower.target.threadId,
|
|
50
|
+
},
|
|
51
|
+
threadName: follower.threadName,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return targets;
|
|
55
|
+
};
|
|
56
|
+
const resolveTarget = (
|
|
57
|
+
selector: TelegramBusAgentTargetSelector,
|
|
58
|
+
sourceTarget?: TelegramTarget,
|
|
59
|
+
): (TelegramTarget & { threadId: number }) | undefined => {
|
|
60
|
+
const allowedChatId = deps.getAllowedChatId();
|
|
61
|
+
const chatId = selector.chatId ?? allowedChatId;
|
|
62
|
+
if (chatId === undefined || chatId !== allowedChatId) return undefined;
|
|
63
|
+
const matches = listLiveTargets().filter((candidate) => {
|
|
64
|
+
if (candidate.target.chatId !== chatId) return false;
|
|
65
|
+
if (selector.threadId !== undefined) {
|
|
66
|
+
return candidate.target.threadId === selector.threadId;
|
|
67
|
+
}
|
|
68
|
+
const requestedName = selector.threadName?.trim().toLocaleLowerCase();
|
|
69
|
+
return (
|
|
70
|
+
requestedName !== undefined &&
|
|
71
|
+
requestedName.length > 0 &&
|
|
72
|
+
candidate.threadName?.trim().toLocaleLowerCase() === requestedName
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
if (matches.length !== 1) return undefined;
|
|
76
|
+
return sameTarget(matches[0]!.target, sourceTarget)
|
|
77
|
+
? undefined
|
|
78
|
+
: matches[0]!.target;
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
resolveTarget,
|
|
82
|
+
async route(input: {
|
|
83
|
+
sourceTarget?: TelegramTarget;
|
|
84
|
+
sourceThreadName?: string;
|
|
85
|
+
message: TelegramBusAgentMessage;
|
|
86
|
+
}): Promise<void> {
|
|
87
|
+
const target = resolveTarget(
|
|
88
|
+
{
|
|
89
|
+
chatId: input.message.target.chatId,
|
|
90
|
+
threadId: input.message.target.threadId,
|
|
91
|
+
},
|
|
92
|
+
input.sourceTarget,
|
|
93
|
+
);
|
|
94
|
+
if (!target) throw new Error("Telegram agent target is no longer live.");
|
|
95
|
+
const allowedChatId = deps.getAllowedChatId();
|
|
96
|
+
const ctx = deps.getContext();
|
|
97
|
+
if (allowedChatId === undefined || !ctx) {
|
|
98
|
+
throw new Error("Telegram agent turn routing is unavailable.");
|
|
99
|
+
}
|
|
100
|
+
const sourceLabel =
|
|
101
|
+
input.sourceThreadName
|
|
102
|
+
?.replace(/[\r\n\[\]]+/g, " ")
|
|
103
|
+
.trim()
|
|
104
|
+
.slice(0, 64) || "Pi";
|
|
105
|
+
const message = {
|
|
106
|
+
message_id: input.message.messageId,
|
|
107
|
+
date: Math.floor(getNowMs() / 1000),
|
|
108
|
+
chat: { id: target.chatId, type: "private" as const },
|
|
109
|
+
from: { id: allowedChatId, is_bot: false, first_name: "Pi Agent" },
|
|
110
|
+
message_thread_id: target.threadId,
|
|
111
|
+
text: `[agent|from-thread:${sourceLabel}]\n\n${input.message.text}`,
|
|
112
|
+
} as TelegramRoutedMessage;
|
|
113
|
+
await deps.handleUpdate({ message } as TUpdate, ctx);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
package/lib/bindings.ts
CHANGED
|
@@ -121,6 +121,8 @@ interface TelegramCommandsAndToolsBindingDeps {
|
|
|
121
121
|
callMultipart: OutboundHandlers.TelegramVoiceReplySenderDeps["sendMultipart"];
|
|
122
122
|
getDefaultChatId: () => number | undefined;
|
|
123
123
|
getDefaultTarget?: () => OutboundAttachments.TelegramQueuedOutboundAttachmentTurnView["target"];
|
|
124
|
+
resolveAgentTarget?: OutboundAttachments.TelegramOutboundMessageToolRegistrationDeps["resolveAgentTarget"];
|
|
125
|
+
routeAgentMessage?: OutboundAttachments.TelegramOutboundMessageToolRegistrationDeps["routeAgentMessage"];
|
|
124
126
|
canSendDirect: () => boolean;
|
|
125
127
|
updateStatus: TelegramBridgeStatusUpdater;
|
|
126
128
|
recordRuntimeEvent: TelegramRuntimeEventRecorder;
|
|
@@ -143,6 +145,8 @@ export function registerTelegramCommandsAndTools({
|
|
|
143
145
|
callMultipart,
|
|
144
146
|
getDefaultChatId,
|
|
145
147
|
getDefaultTarget,
|
|
148
|
+
resolveAgentTarget,
|
|
149
|
+
routeAgentMessage,
|
|
146
150
|
canSendDirect,
|
|
147
151
|
recordRuntimeEvent,
|
|
148
152
|
updateStatus,
|
|
@@ -159,6 +163,8 @@ export function registerTelegramCommandsAndTools({
|
|
|
159
163
|
getDefaultChatId,
|
|
160
164
|
getDefaultTarget,
|
|
161
165
|
getActiveTurn: activeTurnRuntime.get,
|
|
166
|
+
resolveAgentTarget,
|
|
167
|
+
routeAgentMessage,
|
|
162
168
|
canSendDirect,
|
|
163
169
|
planMessage:
|
|
164
170
|
OutboundHandlers.createTelegramOutboundReplyPlanner(buttonActionStore),
|
package/lib/bus-follower.ts
CHANGED
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
getTelegramBusSocketPath,
|
|
26
26
|
resolveTelegramBusSocketPath,
|
|
27
27
|
sendTelegramBusLocalEnvelope,
|
|
28
|
+
type TelegramBusAgentMessage,
|
|
29
|
+
type TelegramBusAgentTargetSelector,
|
|
28
30
|
type TelegramBusEnvelope,
|
|
29
31
|
type TelegramBusSocketPathSource,
|
|
30
32
|
} from "./bus.ts";
|
|
@@ -736,6 +738,12 @@ export function createTelegramBusFollowerClientRuntime<
|
|
|
736
738
|
getAuthSecret: deps.getApiAuthSecret,
|
|
737
739
|
getRegistrationGeneration: deps.getRegistrationGeneration,
|
|
738
740
|
}),
|
|
741
|
+
agentMessages: createTelegramBusAgentMessageClient({
|
|
742
|
+
...sharedClientDeps,
|
|
743
|
+
instanceId: deps.instanceId,
|
|
744
|
+
getAuthSecret: deps.getApiAuthSecret,
|
|
745
|
+
getRegistrationGeneration: deps.getRegistrationGeneration,
|
|
746
|
+
}),
|
|
739
747
|
foreignOwnedUpdateForwarder: createTelegramBusForeignOwnedUpdateForwarder<
|
|
740
748
|
TContext,
|
|
741
749
|
TReactionUpdate,
|
|
@@ -755,6 +763,79 @@ export function createTelegramBusFollowerClientRuntime<
|
|
|
755
763
|
};
|
|
756
764
|
}
|
|
757
765
|
|
|
766
|
+
export function createTelegramBusAgentMessageClient(
|
|
767
|
+
deps: TelegramBusFollowerApiCallerDeps,
|
|
768
|
+
): {
|
|
769
|
+
resolveTarget: (
|
|
770
|
+
selector: TelegramBusAgentTargetSelector,
|
|
771
|
+
) => Promise<TelegramTarget & { threadId: number }>;
|
|
772
|
+
routeMessage: (message: TelegramBusAgentMessage) => Promise<void>;
|
|
773
|
+
} {
|
|
774
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
775
|
+
const timeoutMs =
|
|
776
|
+
deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
|
|
777
|
+
const request = async (
|
|
778
|
+
envelope:
|
|
779
|
+
| Extract<TelegramBusEnvelope, { kind: "follower.resolveAgentTarget" }>
|
|
780
|
+
| Extract<TelegramBusEnvelope, { kind: "follower.routeAgentMessage" }>,
|
|
781
|
+
): Promise<unknown> => {
|
|
782
|
+
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
783
|
+
const response = await sendTelegramBusLocalEnvelope({
|
|
784
|
+
socketPath,
|
|
785
|
+
timeoutMs,
|
|
786
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
787
|
+
endpoint: socketPath,
|
|
788
|
+
operation: "operation",
|
|
789
|
+
}),
|
|
790
|
+
envelope,
|
|
791
|
+
});
|
|
792
|
+
if (response?.kind === "bus.ack" && response.ok) return response.result;
|
|
793
|
+
throw new Error(
|
|
794
|
+
response?.kind === "bus.ack"
|
|
795
|
+
? response.message ?? "Telegram bus agent message failed."
|
|
796
|
+
: "Telegram bus agent message did not return an acknowledgement.",
|
|
797
|
+
);
|
|
798
|
+
};
|
|
799
|
+
const registrationFields = () => ({
|
|
800
|
+
auth: deps.getAuthSecret?.(),
|
|
801
|
+
instanceId: deps.instanceId,
|
|
802
|
+
...(deps.getRegistrationGeneration?.()
|
|
803
|
+
? { registrationGeneration: deps.getRegistrationGeneration?.() }
|
|
804
|
+
: {}),
|
|
805
|
+
});
|
|
806
|
+
return {
|
|
807
|
+
async resolveTarget(selector) {
|
|
808
|
+
const result = await request({
|
|
809
|
+
kind: "follower.resolveAgentTarget",
|
|
810
|
+
requestId: deps.createRequestId(),
|
|
811
|
+
...registrationFields(),
|
|
812
|
+
selector,
|
|
813
|
+
sentAtMs: getNowMs(),
|
|
814
|
+
});
|
|
815
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
816
|
+
throw new Error("Telegram bus returned an invalid agent target.");
|
|
817
|
+
}
|
|
818
|
+
const target = result as Record<string, unknown>;
|
|
819
|
+
if (
|
|
820
|
+
typeof target.chatId !== "number" ||
|
|
821
|
+
typeof target.threadId !== "number"
|
|
822
|
+
) {
|
|
823
|
+
throw new Error("Telegram bus returned an invalid agent target.");
|
|
824
|
+
}
|
|
825
|
+
return { chatId: target.chatId, threadId: target.threadId };
|
|
826
|
+
},
|
|
827
|
+
async routeMessage(message) {
|
|
828
|
+
await request({
|
|
829
|
+
kind: "follower.routeAgentMessage",
|
|
830
|
+
requestId: deps.createRequestId(),
|
|
831
|
+
...registrationFields(),
|
|
832
|
+
message,
|
|
833
|
+
sentAtMs: getNowMs(),
|
|
834
|
+
});
|
|
835
|
+
},
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
758
839
|
export function createTelegramBusFollowerApiCaller(
|
|
759
840
|
deps: TelegramBusFollowerApiCallerDeps,
|
|
760
841
|
): (method: string, args: unknown[]) => Promise<unknown> {
|
package/lib/bus-leader.ts
CHANGED
|
@@ -258,6 +258,20 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
258
258
|
args: unknown[];
|
|
259
259
|
}) => boolean;
|
|
260
260
|
recordFollowerMessageOwnership?: TelegramBusFollowerMessageOwnershipRecorder;
|
|
261
|
+
resolveAgentTarget?: (
|
|
262
|
+
follower: TelegramBusFollowerView,
|
|
263
|
+
selector: Extract<
|
|
264
|
+
TelegramBusEnvelope,
|
|
265
|
+
{ kind: "follower.resolveAgentTarget" }
|
|
266
|
+
>["selector"],
|
|
267
|
+
) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
|
|
268
|
+
routeAgentMessage?: (
|
|
269
|
+
follower: TelegramBusFollowerView,
|
|
270
|
+
message: Extract<
|
|
271
|
+
TelegramBusEnvelope,
|
|
272
|
+
{ kind: "follower.routeAgentMessage" }
|
|
273
|
+
>["message"],
|
|
274
|
+
) => Promise<void> | void;
|
|
261
275
|
provisionFollowerTarget?: (
|
|
262
276
|
registration: TelegramBusInstanceRegistration,
|
|
263
277
|
) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
|
|
@@ -1024,6 +1038,20 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
1024
1038
|
args: unknown[];
|
|
1025
1039
|
}) => boolean;
|
|
1026
1040
|
recordFollowerMessageOwnership?: TelegramBusFollowerMessageOwnershipRecorder;
|
|
1041
|
+
resolveAgentTarget?: (
|
|
1042
|
+
follower: TelegramBusFollowerView,
|
|
1043
|
+
selector: Extract<
|
|
1044
|
+
TelegramBusEnvelope,
|
|
1045
|
+
{ kind: "follower.resolveAgentTarget" }
|
|
1046
|
+
>["selector"],
|
|
1047
|
+
) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
|
|
1048
|
+
routeAgentMessage?: (
|
|
1049
|
+
follower: TelegramBusFollowerView,
|
|
1050
|
+
message: Extract<
|
|
1051
|
+
TelegramBusEnvelope,
|
|
1052
|
+
{ kind: "follower.routeAgentMessage" }
|
|
1053
|
+
>["message"],
|
|
1054
|
+
) => Promise<void> | void;
|
|
1027
1055
|
provisionFollowerTarget?: (
|
|
1028
1056
|
registration: TelegramBusInstanceRegistration,
|
|
1029
1057
|
) =>
|
|
@@ -1044,6 +1072,67 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
1044
1072
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1045
1073
|
const runFollowerMutation =
|
|
1046
1074
|
deps.runFollowerMutation ?? createTelegramBusFollowerMutationRunner();
|
|
1075
|
+
const handleAgentRequest = async (
|
|
1076
|
+
envelope: Extract<
|
|
1077
|
+
TelegramBusEnvelope,
|
|
1078
|
+
{
|
|
1079
|
+
kind:
|
|
1080
|
+
| "follower.resolveAgentTarget"
|
|
1081
|
+
| "follower.routeAgentMessage";
|
|
1082
|
+
}
|
|
1083
|
+
>,
|
|
1084
|
+
): Promise<TelegramBusEnvelope> => {
|
|
1085
|
+
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
1086
|
+
if (!follower) {
|
|
1087
|
+
return {
|
|
1088
|
+
kind: "bus.ack",
|
|
1089
|
+
requestId: envelope.requestId,
|
|
1090
|
+
ok: false,
|
|
1091
|
+
message: "Unknown Telegram bus follower instance.",
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
if (
|
|
1095
|
+
follower.registrationGeneration &&
|
|
1096
|
+
envelope.registrationGeneration !== follower.registrationGeneration
|
|
1097
|
+
) {
|
|
1098
|
+
return {
|
|
1099
|
+
kind: "bus.ack",
|
|
1100
|
+
requestId: envelope.requestId,
|
|
1101
|
+
ok: false,
|
|
1102
|
+
message: "Stale Telegram bus follower registration generation.",
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
deps.followerRegistry.heartbeat(envelope.instanceId, getNowMs());
|
|
1106
|
+
if (envelope.kind === "follower.resolveAgentTarget") {
|
|
1107
|
+
const target = await deps.resolveAgentTarget?.(
|
|
1108
|
+
follower,
|
|
1109
|
+
envelope.selector,
|
|
1110
|
+
);
|
|
1111
|
+
return target
|
|
1112
|
+
? {
|
|
1113
|
+
kind: "bus.ack",
|
|
1114
|
+
requestId: envelope.requestId,
|
|
1115
|
+
ok: true,
|
|
1116
|
+
result: target,
|
|
1117
|
+
}
|
|
1118
|
+
: {
|
|
1119
|
+
kind: "bus.ack",
|
|
1120
|
+
requestId: envelope.requestId,
|
|
1121
|
+
ok: false,
|
|
1122
|
+
message: "Telegram agent target is unavailable or ambiguous.",
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
if (!deps.routeAgentMessage) {
|
|
1126
|
+
return {
|
|
1127
|
+
kind: "bus.ack",
|
|
1128
|
+
requestId: envelope.requestId,
|
|
1129
|
+
ok: false,
|
|
1130
|
+
message: "Telegram agent message routing is unavailable.",
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
await deps.routeAgentMessage(follower, envelope.message);
|
|
1134
|
+
return { kind: "bus.ack", requestId: envelope.requestId, ok: true };
|
|
1135
|
+
};
|
|
1047
1136
|
const forwardToFollower = async (
|
|
1048
1137
|
envelope: Extract<
|
|
1049
1138
|
TelegramBusEnvelope,
|
|
@@ -1251,6 +1340,9 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
1251
1340
|
message: "Unknown Telegram bus follower instance.",
|
|
1252
1341
|
};
|
|
1253
1342
|
}
|
|
1343
|
+
case "follower.resolveAgentTarget":
|
|
1344
|
+
case "follower.routeAgentMessage":
|
|
1345
|
+
return handleAgentRequest(envelope);
|
|
1254
1346
|
case "leader.forwardCallback":
|
|
1255
1347
|
case "leader.forwardReaction":
|
|
1256
1348
|
case "leader.forwardMessage":
|
|
@@ -1619,6 +1711,8 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
1619
1711
|
callApi: deps.callApi,
|
|
1620
1712
|
authorizeFollowerApiCall: deps.authorizeFollowerApiCall,
|
|
1621
1713
|
recordFollowerMessageOwnership: deps.recordFollowerMessageOwnership,
|
|
1714
|
+
resolveAgentTarget: deps.resolveAgentTarget,
|
|
1715
|
+
routeAgentMessage: deps.routeAgentMessage,
|
|
1622
1716
|
provisionFollowerTarget: deps.provisionFollowerTarget,
|
|
1623
1717
|
onFollowerDisconnected: deps.onFollowerDisconnected,
|
|
1624
1718
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
package/lib/bus.ts
CHANGED
|
@@ -448,6 +448,18 @@ export function createTelegramFollowerApiCallAuthorizer(deps: {
|
|
|
448
448
|
});
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
+
export interface TelegramBusAgentTargetSelector {
|
|
452
|
+
chatId?: number;
|
|
453
|
+
threadId?: number;
|
|
454
|
+
threadName?: string;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export interface TelegramBusAgentMessage {
|
|
458
|
+
target: TelegramTarget & { threadId: number };
|
|
459
|
+
messageId: number;
|
|
460
|
+
text: string;
|
|
461
|
+
}
|
|
462
|
+
|
|
451
463
|
export type TelegramBusEnvelope = (
|
|
452
464
|
| {
|
|
453
465
|
kind: "follower.register";
|
|
@@ -511,6 +523,22 @@ export type TelegramBusEnvelope = (
|
|
|
511
523
|
reason: "thread-restore";
|
|
512
524
|
sentAtMs: number;
|
|
513
525
|
}
|
|
526
|
+
| {
|
|
527
|
+
kind: "follower.resolveAgentTarget";
|
|
528
|
+
requestId: string;
|
|
529
|
+
instanceId: string;
|
|
530
|
+
registrationGeneration?: string;
|
|
531
|
+
selector: TelegramBusAgentTargetSelector;
|
|
532
|
+
sentAtMs: number;
|
|
533
|
+
}
|
|
534
|
+
| {
|
|
535
|
+
kind: "follower.routeAgentMessage";
|
|
536
|
+
requestId: string;
|
|
537
|
+
instanceId: string;
|
|
538
|
+
registrationGeneration?: string;
|
|
539
|
+
message: TelegramBusAgentMessage;
|
|
540
|
+
sentAtMs: number;
|
|
541
|
+
}
|
|
514
542
|
| {
|
|
515
543
|
kind: "follower.callApi";
|
|
516
544
|
requestId: string;
|
|
@@ -605,6 +633,12 @@ export function parseTelegramBusEnvelope(
|
|
|
605
633
|
case "leader.replaceFollowerTarget":
|
|
606
634
|
envelope = parseReplaceFollowerTargetEnvelope(value, requestId);
|
|
607
635
|
break;
|
|
636
|
+
case "follower.resolveAgentTarget":
|
|
637
|
+
envelope = parseResolveAgentTargetEnvelope(value, requestId);
|
|
638
|
+
break;
|
|
639
|
+
case "follower.routeAgentMessage":
|
|
640
|
+
envelope = parseRouteAgentMessageEnvelope(value, requestId);
|
|
641
|
+
break;
|
|
608
642
|
case "follower.callApi":
|
|
609
643
|
envelope = parseCallApiEnvelope(value, requestId);
|
|
610
644
|
break;
|
|
@@ -1582,6 +1616,86 @@ function parseReplaceFollowerTargetEnvelope(
|
|
|
1582
1616
|
};
|
|
1583
1617
|
}
|
|
1584
1618
|
|
|
1619
|
+
function parseResolveAgentTargetEnvelope(
|
|
1620
|
+
value: Record<string, unknown>,
|
|
1621
|
+
requestId: string,
|
|
1622
|
+
): TelegramBusEnvelope | undefined {
|
|
1623
|
+
const selectorValue = isRecord(value.selector) ? value.selector : undefined;
|
|
1624
|
+
if (
|
|
1625
|
+
typeof value.instanceId !== "string" ||
|
|
1626
|
+
!selectorValue ||
|
|
1627
|
+
typeof value.sentAtMs !== "number"
|
|
1628
|
+
) {
|
|
1629
|
+
return undefined;
|
|
1630
|
+
}
|
|
1631
|
+
const chatId =
|
|
1632
|
+
typeof selectorValue.chatId === "number" &&
|
|
1633
|
+
Number.isInteger(selectorValue.chatId)
|
|
1634
|
+
? selectorValue.chatId
|
|
1635
|
+
: undefined;
|
|
1636
|
+
const threadId =
|
|
1637
|
+
typeof selectorValue.threadId === "number" &&
|
|
1638
|
+
Number.isInteger(selectorValue.threadId) &&
|
|
1639
|
+
selectorValue.threadId > 0
|
|
1640
|
+
? selectorValue.threadId
|
|
1641
|
+
: undefined;
|
|
1642
|
+
const threadName =
|
|
1643
|
+
typeof selectorValue.threadName === "string" &&
|
|
1644
|
+
selectorValue.threadName.trim()
|
|
1645
|
+
? selectorValue.threadName.trim()
|
|
1646
|
+
: undefined;
|
|
1647
|
+
if ((threadId === undefined) === (threadName === undefined)) return undefined;
|
|
1648
|
+
return {
|
|
1649
|
+
kind: "follower.resolveAgentTarget",
|
|
1650
|
+
requestId,
|
|
1651
|
+
instanceId: value.instanceId,
|
|
1652
|
+
...(typeof value.registrationGeneration === "string"
|
|
1653
|
+
? { registrationGeneration: value.registrationGeneration }
|
|
1654
|
+
: {}),
|
|
1655
|
+
selector: {
|
|
1656
|
+
...(chatId !== undefined ? { chatId } : {}),
|
|
1657
|
+
...(threadId !== undefined ? { threadId } : {}),
|
|
1658
|
+
...(threadName !== undefined ? { threadName } : {}),
|
|
1659
|
+
},
|
|
1660
|
+
sentAtMs: value.sentAtMs,
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function parseRouteAgentMessageEnvelope(
|
|
1665
|
+
value: Record<string, unknown>,
|
|
1666
|
+
requestId: string,
|
|
1667
|
+
): TelegramBusEnvelope | undefined {
|
|
1668
|
+
const messageValue = isRecord(value.message) ? value.message : undefined;
|
|
1669
|
+
const target = parseThreadTarget(messageValue?.target);
|
|
1670
|
+
if (
|
|
1671
|
+
typeof value.instanceId !== "string" ||
|
|
1672
|
+
!messageValue ||
|
|
1673
|
+
!target ||
|
|
1674
|
+
typeof messageValue.messageId !== "number" ||
|
|
1675
|
+
!Number.isInteger(messageValue.messageId) ||
|
|
1676
|
+
messageValue.messageId <= 0 ||
|
|
1677
|
+
typeof messageValue.text !== "string" ||
|
|
1678
|
+
!messageValue.text.trim() ||
|
|
1679
|
+
typeof value.sentAtMs !== "number"
|
|
1680
|
+
) {
|
|
1681
|
+
return undefined;
|
|
1682
|
+
}
|
|
1683
|
+
return {
|
|
1684
|
+
kind: "follower.routeAgentMessage",
|
|
1685
|
+
requestId,
|
|
1686
|
+
instanceId: value.instanceId,
|
|
1687
|
+
...(typeof value.registrationGeneration === "string"
|
|
1688
|
+
? { registrationGeneration: value.registrationGeneration }
|
|
1689
|
+
: {}),
|
|
1690
|
+
message: {
|
|
1691
|
+
target,
|
|
1692
|
+
messageId: messageValue.messageId,
|
|
1693
|
+
text: messageValue.text,
|
|
1694
|
+
},
|
|
1695
|
+
sentAtMs: value.sentAtMs,
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1585
1699
|
function parseCallApiEnvelope(
|
|
1586
1700
|
value: Record<string, unknown>,
|
|
1587
1701
|
requestId: string,
|
|
@@ -9,6 +9,10 @@ import { basename } from "node:path";
|
|
|
9
9
|
|
|
10
10
|
import { Type } from "@sinclair/typebox";
|
|
11
11
|
|
|
12
|
+
import type {
|
|
13
|
+
TelegramBusAgentMessage,
|
|
14
|
+
TelegramBusAgentTargetSelector,
|
|
15
|
+
} from "./bus.ts";
|
|
12
16
|
import type { ExtensionAPI } from "./pi.ts";
|
|
13
17
|
import {
|
|
14
18
|
TELEGRAM_ATTACH_PROMPT_GUIDELINES,
|
|
@@ -84,6 +88,10 @@ export interface TelegramOutboundMessageToolRegistrationDeps extends TelegramOut
|
|
|
84
88
|
getActiveTurn?: () =>
|
|
85
89
|
| { chatId: number; target?: TelegramTarget }
|
|
86
90
|
| undefined;
|
|
91
|
+
resolveAgentTarget?: (
|
|
92
|
+
selector: TelegramBusAgentTargetSelector,
|
|
93
|
+
) => Promise<TelegramTarget & { threadId: number }>;
|
|
94
|
+
routeAgentMessage?: (message: TelegramBusAgentMessage) => Promise<void>;
|
|
87
95
|
canSendDirect: () => boolean;
|
|
88
96
|
planMessage: (markdown: string) => TelegramOutboundMessagePlan;
|
|
89
97
|
sendMarkdownMessage: (
|
|
@@ -515,6 +523,12 @@ export function registerTelegramOutboundMessageTool(
|
|
|
515
523
|
description: "Optional Telegram topic thread id with chat_id",
|
|
516
524
|
}),
|
|
517
525
|
),
|
|
526
|
+
thread: Type.Optional(
|
|
527
|
+
Type.Union([Type.String(), Type.Number()], {
|
|
528
|
+
description:
|
|
529
|
+
"Optional live Pi thread name or numeric id for visible delivery plus a target-agent turn",
|
|
530
|
+
}),
|
|
531
|
+
),
|
|
518
532
|
}),
|
|
519
533
|
async execute(_toolCallId, params) {
|
|
520
534
|
try {
|
|
@@ -522,9 +536,12 @@ export function registerTelegramOutboundMessageTool(
|
|
|
522
536
|
text: params.text,
|
|
523
537
|
chatId: params.chat_id,
|
|
524
538
|
threadId: params.thread_id,
|
|
539
|
+
agentThread: params.thread,
|
|
525
540
|
getDefaultChatId: deps.getDefaultChatId,
|
|
526
541
|
getDefaultTarget: deps.getDefaultTarget,
|
|
527
542
|
getActiveTurn: deps.getActiveTurn,
|
|
543
|
+
resolveAgentTarget: deps.resolveAgentTarget,
|
|
544
|
+
routeAgentMessage: deps.routeAgentMessage,
|
|
528
545
|
canSendDirect: deps.canSendDirect,
|
|
529
546
|
planMessage: deps.planMessage,
|
|
530
547
|
sendMarkdownMessage: deps.sendMarkdownMessage,
|
|
@@ -736,12 +753,17 @@ export async function sendTelegramOutboundMessage(options: {
|
|
|
736
753
|
text: string;
|
|
737
754
|
chatId?: number;
|
|
738
755
|
threadId?: number;
|
|
756
|
+
agentThread?: string | number;
|
|
739
757
|
target?: TelegramTarget;
|
|
740
758
|
getDefaultChatId?: () => number | undefined;
|
|
741
759
|
getDefaultTarget?: () => TelegramTarget | undefined;
|
|
742
760
|
getActiveTurn?: () =>
|
|
743
761
|
| { chatId: number; target?: TelegramTarget }
|
|
744
762
|
| undefined;
|
|
763
|
+
resolveAgentTarget?: (
|
|
764
|
+
selector: TelegramBusAgentTargetSelector,
|
|
765
|
+
) => Promise<TelegramTarget & { threadId: number }>;
|
|
766
|
+
routeAgentMessage?: (message: TelegramBusAgentMessage) => Promise<void>;
|
|
745
767
|
canSendDirect: () => boolean;
|
|
746
768
|
planMessage: (markdown: string) => TelegramOutboundMessagePlan;
|
|
747
769
|
sendMarkdownMessage: (
|
|
@@ -754,17 +776,41 @@ export async function sendTelegramOutboundMessage(options: {
|
|
|
754
776
|
details: { chatId: number; messageId?: number };
|
|
755
777
|
}> {
|
|
756
778
|
assertTelegramDirectDeliveryAllowed(options.canSendDirect);
|
|
779
|
+
const activeTurn = options.getActiveTurn?.();
|
|
780
|
+
const requestedAgentSelector: TelegramBusAgentTargetSelector | undefined =
|
|
781
|
+
options.agentThread !== undefined
|
|
782
|
+
? typeof options.agentThread === "number"
|
|
783
|
+
? { chatId: options.chatId, threadId: options.agentThread }
|
|
784
|
+
: { chatId: options.chatId, threadName: options.agentThread }
|
|
785
|
+
: activeTurn &&
|
|
786
|
+
options.resolveAgentTarget &&
|
|
787
|
+
options.routeAgentMessage &&
|
|
788
|
+
((options.target?.threadId !== undefined) ||
|
|
789
|
+
(options.chatId !== undefined && options.threadId !== undefined))
|
|
790
|
+
? {
|
|
791
|
+
chatId: options.target?.chatId ?? options.chatId,
|
|
792
|
+
threadId: options.target?.threadId ?? options.threadId,
|
|
793
|
+
}
|
|
794
|
+
: undefined;
|
|
795
|
+
let agentTarget: (TelegramTarget & { threadId: number }) | undefined;
|
|
796
|
+
if (requestedAgentSelector) {
|
|
797
|
+
if (!options.resolveAgentTarget || !options.routeAgentMessage) {
|
|
798
|
+
throw new Error("Telegram agent turn routing is unavailable.");
|
|
799
|
+
}
|
|
800
|
+
agentTarget = await options.resolveAgentTarget(requestedAgentSelector);
|
|
801
|
+
}
|
|
757
802
|
const { chatId, target } = resolveTelegramOutboundTarget({
|
|
758
|
-
chatId: options.chatId,
|
|
759
|
-
threadId: options.threadId,
|
|
760
|
-
target: options.target,
|
|
803
|
+
chatId: agentTarget?.chatId ?? options.chatId,
|
|
804
|
+
threadId: agentTarget?.threadId ?? options.threadId,
|
|
805
|
+
target: agentTarget ?? options.target,
|
|
761
806
|
getDefaultChatId: options.getDefaultChatId,
|
|
762
807
|
getDefaultTarget: options.getDefaultTarget,
|
|
763
808
|
});
|
|
764
|
-
const activeTurn = options.getActiveTurn?.();
|
|
765
809
|
if (activeTurn) {
|
|
766
810
|
const hasExplicitTarget =
|
|
767
|
-
options.chatId !== undefined ||
|
|
811
|
+
options.chatId !== undefined ||
|
|
812
|
+
options.target !== undefined ||
|
|
813
|
+
options.agentThread !== undefined;
|
|
768
814
|
const activeTarget = activeTurn.target ?? { chatId: activeTurn.chatId };
|
|
769
815
|
const requestedTarget = target ?? { chatId };
|
|
770
816
|
const targetsMatch =
|
|
@@ -781,6 +827,18 @@ export async function sendTelegramOutboundMessage(options: {
|
|
|
781
827
|
replyMarkup: plan.replyMarkup,
|
|
782
828
|
target,
|
|
783
829
|
});
|
|
830
|
+
if (agentTarget) {
|
|
831
|
+
if (messageId === undefined) {
|
|
832
|
+
throw new Error(
|
|
833
|
+
"Telegram message was sent but no message id was returned for agent routing.",
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
await options.routeAgentMessage!({
|
|
837
|
+
target: agentTarget,
|
|
838
|
+
messageId,
|
|
839
|
+
text: options.text,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
784
842
|
return {
|
|
785
843
|
content: [
|
|
786
844
|
{
|
package/lib/prompts.ts
CHANGED
|
@@ -29,9 +29,9 @@ export const TELEGRAM_MESSAGE_PROMPT_SNIPPET =
|
|
|
29
29
|
"Send direct Telegram Markdown text when the user explicitly asks for Telegram delivery outside the normal reply flow.";
|
|
30
30
|
export const TELEGRAM_MESSAGE_PROMPT_GUIDELINES = [
|
|
31
31
|
"Use telegram_message only when the user explicitly asks to send a message to Telegram from the local/TUI side, or names a concrete Telegram delivery target.",
|
|
32
|
-
"For
|
|
32
|
+
"For a live Pi thread target, provide thread as its case-insensitive name or numeric id; the bridge sends visibly and admits one attributed turn to that live instance. Unknown, ambiguous, same, or offline targets fail before sending.",
|
|
33
33
|
"Add buttons by embedding the same top-level telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.",
|
|
34
|
-
"During an active Telegram turn, omit telegram_message for the current target and answer normally; use
|
|
34
|
+
"During an active Telegram turn, omit telegram_message for the current target and answer normally; use thread only when the user requests delivery to a different live Pi thread.",
|
|
35
35
|
] as const;
|
|
36
36
|
|
|
37
37
|
const TELEGRAM_MODEL_CONTEXT_TOOL_NAMES = new Set([
|