@llblab/pi-telegram 0.35.2 → 0.36.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/AGENTS.md +2 -1
- package/BACKLOG.md +11 -0
- package/CHANGELOG.md +23 -0
- package/README.md +36 -7
- package/docs/README.md +3 -2
- package/docs/architecture.md +30 -3
- package/docs/compact-matrix-literal.md +105 -118
- package/docs/generative-apps.md +310 -0
- package/docs/multi-instance-bus.md +1 -1
- package/docs/outbound.md +2 -2
- package/docs/public-api.md +3 -2
- package/docs/ui-style.md +15 -7
- package/index.ts +14 -0
- package/lib/bindings.ts +118 -8
- package/lib/generative-app-worker.mjs +104 -0
- package/lib/generative-apps.ts +958 -0
- package/lib/menu-queue.ts +105 -112
- package/lib/outbound-buttons.ts +58 -10
- package/lib/outbound-markup.ts +81 -22
- package/lib/outbound.ts +5 -1
- package/lib/prompts.ts +1 -0
- package/lib/queue.ts +98 -42
- package/lib/routing.ts +15 -0
- package/lib/runtime.ts +0 -23
- package/lib/updates.ts +79 -26
- package/package.json +1 -1
- package/skills/generated-control-surface/SKILL.md +12 -4
- package/skills/generative-apps/SKILL.md +114 -0
- package/skills/telegram-bridge/SKILL.md +8 -2
package/lib/queue.ts
CHANGED
|
@@ -34,7 +34,17 @@ export type TelegramQueueLane = "control" | "priority" | "default";
|
|
|
34
34
|
export type TelegramQueueReactionDisposition =
|
|
35
35
|
| { kind: "default" }
|
|
36
36
|
| { kind: "priority"; emoji: string }
|
|
37
|
-
| { kind: "suppressed"; emoji: string }
|
|
37
|
+
| { kind: "suppressed"; emoji: string }
|
|
38
|
+
| {
|
|
39
|
+
kind: "priority-suppressed";
|
|
40
|
+
priorityEmoji: string;
|
|
41
|
+
suppressionEmoji: string;
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
kind: "reaction-transition";
|
|
45
|
+
priorityEmoji?: string | null;
|
|
46
|
+
suppressionEmoji?: string | null;
|
|
47
|
+
};
|
|
38
48
|
|
|
39
49
|
export interface TelegramQueueAdmissionReceipt {
|
|
40
50
|
queueKind: TelegramQueueItemKind;
|
|
@@ -899,7 +909,7 @@ export function applyTelegramQueuePromptReactionDisposition<
|
|
|
899
909
|
items: TelegramQueueItem<TContext>[],
|
|
900
910
|
messageId: number,
|
|
901
911
|
disposition: TelegramQueueReactionDisposition,
|
|
902
|
-
|
|
912
|
+
destinationLaneOrder?: number,
|
|
903
913
|
scope?: TelegramQueueMessageScope,
|
|
904
914
|
): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
|
|
905
915
|
let nextItems = items;
|
|
@@ -911,17 +921,38 @@ export function applyTelegramQueuePromptReactionDisposition<
|
|
|
911
921
|
) {
|
|
912
922
|
continue;
|
|
913
923
|
}
|
|
914
|
-
const
|
|
915
|
-
disposition.
|
|
916
|
-
|
|
917
|
-
|
|
924
|
+
const isPriority = disposition.kind === "reaction-transition"
|
|
925
|
+
? disposition.priorityEmoji === undefined
|
|
926
|
+
? item.queueLane === "priority"
|
|
927
|
+
: disposition.priorityEmoji !== null
|
|
928
|
+
: disposition.kind === "priority" ||
|
|
929
|
+
disposition.kind === "priority-suppressed";
|
|
930
|
+
const queueLane: TelegramQueueLane = isPriority ? "priority" : "default";
|
|
931
|
+
const laneOrder = item.queueLane === queueLane
|
|
932
|
+
? item.laneOrder
|
|
933
|
+
: destinationLaneOrder;
|
|
918
934
|
if (laneOrder === undefined) {
|
|
919
|
-
throw new Error("Telegram
|
|
935
|
+
throw new Error("Telegram destination lane order is unavailable.");
|
|
920
936
|
}
|
|
921
|
-
const priorityEmoji =
|
|
922
|
-
|
|
937
|
+
const priorityEmoji = disposition.kind === "reaction-transition"
|
|
938
|
+
? disposition.priorityEmoji === undefined
|
|
939
|
+
? item.priorityEmoji
|
|
940
|
+
: disposition.priorityEmoji ?? undefined
|
|
941
|
+
: disposition.kind === "priority"
|
|
942
|
+
? disposition.emoji
|
|
943
|
+
: disposition.kind === "priority-suppressed"
|
|
944
|
+
? disposition.priorityEmoji
|
|
945
|
+
: undefined;
|
|
923
946
|
const reactionSuppressionEmoji =
|
|
924
|
-
disposition.kind === "
|
|
947
|
+
disposition.kind === "reaction-transition"
|
|
948
|
+
? disposition.suppressionEmoji === undefined
|
|
949
|
+
? item.reactionSuppressionEmoji
|
|
950
|
+
: disposition.suppressionEmoji ?? undefined
|
|
951
|
+
: disposition.kind === "suppressed"
|
|
952
|
+
? disposition.emoji
|
|
953
|
+
: disposition.kind === "priority-suppressed"
|
|
954
|
+
? disposition.suppressionEmoji
|
|
955
|
+
: undefined;
|
|
925
956
|
if (
|
|
926
957
|
item.queueLane === queueLane &&
|
|
927
958
|
item.laneOrder === laneOrder &&
|
|
@@ -1984,7 +2015,6 @@ export interface TelegramSessionShutdownState<TQueueItem> {
|
|
|
1984
2015
|
queuedTelegramItems: TQueueItem[];
|
|
1985
2016
|
nextQueuedTelegramItemOrder: number;
|
|
1986
2017
|
nextQueuedTelegramControlOrder: number;
|
|
1987
|
-
nextPriorityReactionOrder: number;
|
|
1988
2018
|
currentTelegramModel: undefined;
|
|
1989
2019
|
activeTelegramToolExecutions: number;
|
|
1990
2020
|
pendingTelegramModelSwitch: undefined;
|
|
@@ -1996,7 +2026,6 @@ export interface TelegramSessionShutdownState<TQueueItem> {
|
|
|
1996
2026
|
export interface TelegramSessionRuntimeCounterState {
|
|
1997
2027
|
nextQueuedTelegramItemOrder?: number;
|
|
1998
2028
|
nextQueuedTelegramControlOrder?: number;
|
|
1999
|
-
nextPriorityReactionOrder?: number;
|
|
2000
2029
|
}
|
|
2001
2030
|
|
|
2002
2031
|
export interface TelegramSessionRuntimeFlagState {
|
|
@@ -2104,8 +2133,7 @@ export interface TelegramQueueMutationRuntimeDeps<
|
|
|
2104
2133
|
TContext,
|
|
2105
2134
|
> extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
|
|
2106
2135
|
ctx: TContext;
|
|
2107
|
-
|
|
2108
|
-
incrementNextPriorityReactionOrder?: () => void;
|
|
2136
|
+
allocateLaneOrder?: () => number;
|
|
2109
2137
|
onItemsDiscarded?: (
|
|
2110
2138
|
items: readonly TelegramQueueItem<TContext>[],
|
|
2111
2139
|
ctx: TContext,
|
|
@@ -2116,8 +2144,7 @@ export interface TelegramQueueMutationRuntimeDeps<
|
|
|
2116
2144
|
export interface TelegramQueueMutationControllerDeps<
|
|
2117
2145
|
TContext,
|
|
2118
2146
|
> extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
|
|
2119
|
-
|
|
2120
|
-
incrementNextPriorityReactionOrder?: () => void;
|
|
2147
|
+
allocateLaneOrder?: () => number;
|
|
2121
2148
|
onItemsDiscarded?: (
|
|
2122
2149
|
items: readonly TelegramQueueItem<TContext>[],
|
|
2123
2150
|
ctx: TContext,
|
|
@@ -2227,7 +2254,6 @@ export function buildTelegramSessionShutdownState<
|
|
|
2227
2254
|
queuedTelegramItems: [],
|
|
2228
2255
|
nextQueuedTelegramItemOrder: 0,
|
|
2229
2256
|
nextQueuedTelegramControlOrder: 0,
|
|
2230
|
-
nextPriorityReactionOrder: 0,
|
|
2231
2257
|
currentTelegramModel: undefined,
|
|
2232
2258
|
activeTelegramToolExecutions: 0,
|
|
2233
2259
|
pendingTelegramModelSwitch: undefined,
|
|
@@ -2492,24 +2518,40 @@ export function applyTelegramQueuePromptReactionDispositionRuntime<TContext>(
|
|
|
2492
2518
|
deps: TelegramQueueMutationRuntimeDeps<TContext>,
|
|
2493
2519
|
scope?: TelegramQueueMessageScope,
|
|
2494
2520
|
): boolean {
|
|
2495
|
-
const
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2521
|
+
const queuedItems = deps.getQueuedItems();
|
|
2522
|
+
const changesLane = queuedItems.some((item) => {
|
|
2523
|
+
if (
|
|
2524
|
+
!isPendingTelegramTurn(item) ||
|
|
2525
|
+
!isTelegramQueueItemInMessageScope(item, scope) ||
|
|
2526
|
+
!item.sourceMessageIds.includes(messageId)
|
|
2527
|
+
) {
|
|
2528
|
+
return false;
|
|
2529
|
+
}
|
|
2530
|
+
const queueLane: TelegramQueueLane =
|
|
2531
|
+
disposition.kind === "reaction-transition"
|
|
2532
|
+
? disposition.priorityEmoji === undefined
|
|
2533
|
+
? item.queueLane
|
|
2534
|
+
: disposition.priorityEmoji === null
|
|
2535
|
+
? "default"
|
|
2536
|
+
: "priority"
|
|
2537
|
+
: disposition.kind === "priority" ||
|
|
2538
|
+
disposition.kind === "priority-suppressed"
|
|
2539
|
+
? "priority"
|
|
2540
|
+
: "default";
|
|
2541
|
+
return item.queueLane !== queueLane;
|
|
2542
|
+
});
|
|
2543
|
+
const destinationLaneOrder = changesLane
|
|
2544
|
+
? deps.allocateLaneOrder?.()
|
|
2545
|
+
: undefined;
|
|
2546
|
+
if (changesLane && destinationLaneOrder === undefined) return false;
|
|
2502
2547
|
const { changed, items } = applyTelegramQueuePromptReactionDisposition(
|
|
2503
|
-
|
|
2548
|
+
queuedItems,
|
|
2504
2549
|
messageId,
|
|
2505
2550
|
disposition,
|
|
2506
|
-
|
|
2551
|
+
destinationLaneOrder,
|
|
2507
2552
|
scope,
|
|
2508
2553
|
);
|
|
2509
2554
|
if (!changed) return false;
|
|
2510
|
-
if (disposition.kind === "priority") {
|
|
2511
|
-
deps.incrementNextPriorityReactionOrder?.();
|
|
2512
|
-
}
|
|
2513
2555
|
commitReorderedTelegramQueueItemsRuntime(items, deps);
|
|
2514
2556
|
return true;
|
|
2515
2557
|
}
|
|
@@ -2931,18 +2973,33 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
2931
2973
|
{ phase: "transport-generation" },
|
|
2932
2974
|
);
|
|
2933
2975
|
}
|
|
2934
|
-
const
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2976
|
+
const canDispatch = deps.canDispatch(ctx);
|
|
2977
|
+
let nextActiveIndex = 0;
|
|
2978
|
+
if (canDispatch) {
|
|
2979
|
+
while (nextActiveIndex < activeItems.length) {
|
|
2980
|
+
const candidate = activeItems[nextActiveIndex];
|
|
2981
|
+
if (
|
|
2982
|
+
!candidate ||
|
|
2983
|
+
candidate.kind !== "prompt" ||
|
|
2984
|
+
candidate.reactionSuppressionEmoji === undefined
|
|
2985
|
+
) {
|
|
2986
|
+
break;
|
|
2987
|
+
}
|
|
2988
|
+
if (deps.hasPendingInboundQueueMutationForItem?.(candidate)) {
|
|
2989
|
+
deps.updateStatus(ctx);
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
if (
|
|
2993
|
+
deps.isQueueItemAdmissionReady &&
|
|
2994
|
+
!deps.isQueueItemAdmissionReady(candidate)
|
|
2995
|
+
) {
|
|
2996
|
+
deps.updateStatus(ctx);
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
nextActiveIndex += 1;
|
|
2944
3000
|
}
|
|
2945
3001
|
}
|
|
3002
|
+
const dispatchableItems = activeItems.slice(nextActiveIndex);
|
|
2946
3003
|
const nextItem = dispatchableItems[0];
|
|
2947
3004
|
if (
|
|
2948
3005
|
nextItem &&
|
|
@@ -2961,12 +3018,11 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
2961
3018
|
}
|
|
2962
3019
|
const dispatchPlan = planNextTelegramQueueAction(
|
|
2963
3020
|
dispatchableItems,
|
|
2964
|
-
|
|
3021
|
+
canDispatch,
|
|
2965
3022
|
);
|
|
2966
|
-
if (dispatchPlan.kind !== "none") {
|
|
3023
|
+
if (nextActiveIndex > 0 || dispatchPlan.kind !== "none") {
|
|
2967
3024
|
deps.setQueuedItems([
|
|
2968
3025
|
...dispatchPlan.remainingItems,
|
|
2969
|
-
...suppressedActiveItems,
|
|
2970
3026
|
...protectedInactiveItems,
|
|
2971
3027
|
]);
|
|
2972
3028
|
}
|
package/lib/routing.ts
CHANGED
|
@@ -590,6 +590,11 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
590
590
|
ctx: TContext,
|
|
591
591
|
) => Promise<boolean>;
|
|
592
592
|
buttonActionStore?: OutboundHandlers.TelegramButtonActionStore;
|
|
593
|
+
invokeBoundButtonAction?: (
|
|
594
|
+
action: OutboundHandlers.TelegramOutboundButtonAction,
|
|
595
|
+
query: TCallbackQuery,
|
|
596
|
+
ctx: TContext,
|
|
597
|
+
) => Promise<false | "new" | "edit">;
|
|
593
598
|
inboundHandlerRuntime: TelegramInboundHandlerRuntime<TContext>;
|
|
594
599
|
threadStore?: Threads.TelegramTopicTargetStore;
|
|
595
600
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
@@ -1566,6 +1571,16 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1566
1571
|
{
|
|
1567
1572
|
resolveAction: deps.buttonActionStore.resolve,
|
|
1568
1573
|
answerCallbackQuery: deps.answerCallbackQuery,
|
|
1574
|
+
...(deps.invokeBoundButtonAction
|
|
1575
|
+
? {
|
|
1576
|
+
invokeBoundAction: (buttonQuery, action, context) =>
|
|
1577
|
+
deps.invokeBoundButtonAction!(
|
|
1578
|
+
action,
|
|
1579
|
+
buttonQuery as TCallbackQuery,
|
|
1580
|
+
context,
|
|
1581
|
+
),
|
|
1582
|
+
}
|
|
1583
|
+
: {}),
|
|
1569
1584
|
editMessageReplyMarkup: deps.editMessageReplyMarkup
|
|
1570
1585
|
? async (chatId, messageId, replyMarkup) => {
|
|
1571
1586
|
try {
|
package/lib/runtime.ts
CHANGED
|
@@ -10,7 +10,6 @@ const TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS = 250;
|
|
|
10
10
|
export interface TelegramRuntimeQueueCounters {
|
|
11
11
|
nextQueuedTelegramItemOrder: number;
|
|
12
12
|
nextQueuedTelegramControlOrder: number;
|
|
13
|
-
nextPriorityReactionOrder: number;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
export interface TelegramRuntimeLifecycleFlags {
|
|
@@ -34,8 +33,6 @@ export interface TelegramRuntimeQueuePort {
|
|
|
34
33
|
syncCounters: (counters: Partial<TelegramRuntimeQueueCounters>) => void;
|
|
35
34
|
allocateItemOrder: () => number;
|
|
36
35
|
allocateControlOrder: () => number;
|
|
37
|
-
getNextPriorityReactionOrder: () => number;
|
|
38
|
-
incrementNextPriorityReactionOrder: () => void;
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
export interface TelegramRuntimeLifecyclePort {
|
|
@@ -85,7 +82,6 @@ export function createTelegramBridgeRuntimeState(): TelegramBridgeRuntimeState {
|
|
|
85
82
|
return {
|
|
86
83
|
nextQueuedTelegramItemOrder: 0,
|
|
87
84
|
nextQueuedTelegramControlOrder: 0,
|
|
88
|
-
nextPriorityReactionOrder: 0,
|
|
89
85
|
activeTelegramToolExecutions: 0,
|
|
90
86
|
telegramTurnDispatchPending: false,
|
|
91
87
|
compactionInProgress: false,
|
|
@@ -104,10 +100,6 @@ export function createTelegramBridgeRuntime(
|
|
|
104
100
|
syncTelegramQueueRuntimeCounters(state, counters),
|
|
105
101
|
allocateItemOrder: () => allocateTelegramQueueItemOrder(state),
|
|
106
102
|
allocateControlOrder: () => allocateTelegramQueueControlOrder(state),
|
|
107
|
-
getNextPriorityReactionOrder: () =>
|
|
108
|
-
getNextTelegramPriorityReactionOrder(state),
|
|
109
|
-
incrementNextPriorityReactionOrder: () =>
|
|
110
|
-
incrementNextTelegramPriorityReactionOrder(state),
|
|
111
103
|
},
|
|
112
104
|
lifecycle: {
|
|
113
105
|
syncFlags: (flags) => syncTelegramLifecycleRuntimeFlags(state, flags),
|
|
@@ -159,9 +151,6 @@ export function syncTelegramQueueRuntimeCounters(
|
|
|
159
151
|
state.nextQueuedTelegramControlOrder =
|
|
160
152
|
counters.nextQueuedTelegramControlOrder;
|
|
161
153
|
}
|
|
162
|
-
if (counters.nextPriorityReactionOrder !== undefined) {
|
|
163
|
-
state.nextPriorityReactionOrder = counters.nextPriorityReactionOrder;
|
|
164
|
-
}
|
|
165
154
|
}
|
|
166
155
|
|
|
167
156
|
export function allocateTelegramQueueItemOrder(
|
|
@@ -176,18 +165,6 @@ export function allocateTelegramQueueControlOrder(
|
|
|
176
165
|
return state.nextQueuedTelegramControlOrder++;
|
|
177
166
|
}
|
|
178
167
|
|
|
179
|
-
export function getNextTelegramPriorityReactionOrder(
|
|
180
|
-
state: TelegramBridgeRuntimeState,
|
|
181
|
-
): number {
|
|
182
|
-
return state.nextPriorityReactionOrder;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export function incrementNextTelegramPriorityReactionOrder(
|
|
186
|
-
state: TelegramBridgeRuntimeState,
|
|
187
|
-
): void {
|
|
188
|
-
state.nextPriorityReactionOrder += 1;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
168
|
export function syncTelegramLifecycleRuntimeFlags(
|
|
192
169
|
state: TelegramBridgeRuntimeState,
|
|
193
170
|
flags: Partial<TelegramRuntimeLifecycleFlags>,
|
package/lib/updates.ts
CHANGED
|
@@ -133,24 +133,61 @@ export function getTelegramQueueReactionDisposition(
|
|
|
133
133
|
emojis,
|
|
134
134
|
TELEGRAM_REMOVAL_REACTION_EMOJIS,
|
|
135
135
|
);
|
|
136
|
-
if (suppressionEmoji) return { kind: "suppressed", emoji: suppressionEmoji };
|
|
137
136
|
const priorityEmoji = getTelegramReactionEmoji(
|
|
138
137
|
emojis,
|
|
139
138
|
TELEGRAM_PRIORITY_REACTION_EMOJIS,
|
|
140
139
|
);
|
|
140
|
+
if (suppressionEmoji && priorityEmoji) {
|
|
141
|
+
return {
|
|
142
|
+
kind: "priority-suppressed",
|
|
143
|
+
priorityEmoji,
|
|
144
|
+
suppressionEmoji,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (suppressionEmoji) return { kind: "suppressed", emoji: suppressionEmoji };
|
|
141
148
|
if (priorityEmoji) return { kind: "priority", emoji: priorityEmoji };
|
|
142
149
|
return { kind: "default" };
|
|
143
150
|
}
|
|
144
151
|
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
):
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
function getTelegramQueueReactionTransition(
|
|
153
|
+
oldReactions: TelegramReactionType[],
|
|
154
|
+
newReactions: TelegramReactionType[],
|
|
155
|
+
): TelegramQueueReactionDisposition | undefined {
|
|
156
|
+
const oldEmojis = collectTelegramReactionEmojis(oldReactions);
|
|
157
|
+
const newEmojis = collectTelegramReactionEmojis(newReactions);
|
|
158
|
+
const oldPriorityEmoji = getTelegramReactionEmoji(
|
|
159
|
+
oldEmojis,
|
|
160
|
+
TELEGRAM_PRIORITY_REACTION_EMOJIS,
|
|
153
161
|
);
|
|
162
|
+
const newPriorityEmoji = getTelegramReactionEmoji(
|
|
163
|
+
newEmojis,
|
|
164
|
+
TELEGRAM_PRIORITY_REACTION_EMOJIS,
|
|
165
|
+
);
|
|
166
|
+
const oldSuppressionEmoji = getTelegramReactionEmoji(
|
|
167
|
+
oldEmojis,
|
|
168
|
+
TELEGRAM_REMOVAL_REACTION_EMOJIS,
|
|
169
|
+
);
|
|
170
|
+
const newSuppressionEmoji = getTelegramReactionEmoji(
|
|
171
|
+
newEmojis,
|
|
172
|
+
TELEGRAM_REMOVAL_REACTION_EMOJIS,
|
|
173
|
+
);
|
|
174
|
+
if (
|
|
175
|
+
oldPriorityEmoji === newPriorityEmoji &&
|
|
176
|
+
oldSuppressionEmoji === newSuppressionEmoji
|
|
177
|
+
) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
const transition: Extract<
|
|
181
|
+
TelegramQueueReactionDisposition,
|
|
182
|
+
{ kind: "reaction-transition" }
|
|
183
|
+
> = { kind: "reaction-transition" };
|
|
184
|
+
if (oldPriorityEmoji !== newPriorityEmoji) {
|
|
185
|
+
transition.priorityEmoji = newPriorityEmoji ?? null;
|
|
186
|
+
}
|
|
187
|
+
if (oldSuppressionEmoji !== newSuppressionEmoji) {
|
|
188
|
+
transition.suppressionEmoji = newSuppressionEmoji ?? null;
|
|
189
|
+
}
|
|
190
|
+
return transition;
|
|
154
191
|
}
|
|
155
192
|
|
|
156
193
|
export function extractDeletedTelegramMessageIds(
|
|
@@ -849,6 +886,19 @@ export type TelegramMessageOwnershipRecorder = (
|
|
|
849
886
|
input: TelegramMessageOwnershipRecorderInput,
|
|
850
887
|
) => void;
|
|
851
888
|
|
|
889
|
+
interface TelegramUnauthorizedReplyOptions {
|
|
890
|
+
parseMode?: "HTML";
|
|
891
|
+
target?: { chatId: number; threadId?: number };
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const TELEGRAM_UNAUTHORIZED_DENIAL_COPY = "Access denied.";
|
|
895
|
+
|
|
896
|
+
function formatTelegramUnauthorizedDenial(format: "plain" | "html"): string {
|
|
897
|
+
return format === "html"
|
|
898
|
+
? `🚫 <b>${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}</b>`
|
|
899
|
+
: `🚫 ${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}`;
|
|
900
|
+
}
|
|
901
|
+
|
|
852
902
|
export interface TelegramUpdateRuntimeDeps<
|
|
853
903
|
TContext = unknown,
|
|
854
904
|
TReactionUpdate extends TelegramMessageReactionUpdated =
|
|
@@ -890,7 +940,11 @@ export interface TelegramUpdateRuntimeDeps<
|
|
|
890
940
|
callbackQueryId: string,
|
|
891
941
|
text?: string,
|
|
892
942
|
) => Promise<void>;
|
|
893
|
-
answerGuestQuery: (
|
|
943
|
+
answerGuestQuery: (
|
|
944
|
+
guestQueryId: string,
|
|
945
|
+
text?: string,
|
|
946
|
+
options?: Pick<TelegramUnauthorizedReplyOptions, "parseMode">,
|
|
947
|
+
) => Promise<void>;
|
|
894
948
|
handleAuthorizedTelegramCallbackQuery: (
|
|
895
949
|
query: TCallbackQuery,
|
|
896
950
|
ctx: TContext,
|
|
@@ -899,7 +953,7 @@ export interface TelegramUpdateRuntimeDeps<
|
|
|
899
953
|
chatId: number,
|
|
900
954
|
replyToMessageId: number,
|
|
901
955
|
text: string,
|
|
902
|
-
options?:
|
|
956
|
+
options?: TelegramUnauthorizedReplyOptions,
|
|
903
957
|
) => Promise<number | undefined>;
|
|
904
958
|
handleAuthorizedTelegramMessage: (
|
|
905
959
|
message: TMessage,
|
|
@@ -959,7 +1013,11 @@ export interface TelegramUpdateRuntimeControllerDeps<
|
|
|
959
1013
|
callbackQueryId: string,
|
|
960
1014
|
text?: string,
|
|
961
1015
|
) => Promise<void>;
|
|
962
|
-
answerGuestQuery: (
|
|
1016
|
+
answerGuestQuery: (
|
|
1017
|
+
guestQueryId: string,
|
|
1018
|
+
text?: string,
|
|
1019
|
+
options?: Pick<TelegramUnauthorizedReplyOptions, "parseMode">,
|
|
1020
|
+
) => Promise<void>;
|
|
963
1021
|
handleAuthorizedTelegramCallbackQuery: (
|
|
964
1022
|
query: TCallbackQuery,
|
|
965
1023
|
ctx: TContext,
|
|
@@ -968,7 +1026,7 @@ export interface TelegramUpdateRuntimeControllerDeps<
|
|
|
968
1026
|
chatId: number,
|
|
969
1027
|
replyToMessageId: number,
|
|
970
1028
|
text: string,
|
|
971
|
-
options?:
|
|
1029
|
+
options?: TelegramUnauthorizedReplyOptions,
|
|
972
1030
|
) => Promise<number | undefined>;
|
|
973
1031
|
handleAuthorizedTelegramMessage: (
|
|
974
1032
|
message: TMessage,
|
|
@@ -1299,17 +1357,11 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
|
|
|
1299
1357
|
typeof reactionUpdate.chat.id === "number"
|
|
1300
1358
|
? { chatId: reactionUpdate.chat.id }
|
|
1301
1359
|
: undefined;
|
|
1302
|
-
const
|
|
1360
|
+
const reactionTransition = getTelegramQueueReactionTransition(
|
|
1303
1361
|
reactionUpdate.old_reaction,
|
|
1304
|
-
);
|
|
1305
|
-
const newDisposition = getTelegramQueueReactionDisposition(
|
|
1306
1362
|
reactionUpdate.new_reaction,
|
|
1307
1363
|
);
|
|
1308
|
-
if (
|
|
1309
|
-
areTelegramQueueReactionDispositionsEqual(oldDisposition, newDisposition)
|
|
1310
|
-
) {
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1364
|
+
if (!reactionTransition) return;
|
|
1313
1365
|
deps.assertExecutionCurrent?.();
|
|
1314
1366
|
await deps.flushPendingMediaGroupMessage?.(reactionUpdate.message_id);
|
|
1315
1367
|
deps.assertExecutionCurrent?.();
|
|
@@ -1317,7 +1369,7 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
|
|
|
1317
1369
|
deps.assertExecutionCurrent?.();
|
|
1318
1370
|
deps.applyQueuedTelegramTurnReactionByMessageId(
|
|
1319
1371
|
reactionUpdate.message_id,
|
|
1320
|
-
|
|
1372
|
+
reactionTransition,
|
|
1321
1373
|
deps.ctx,
|
|
1322
1374
|
reactionScope,
|
|
1323
1375
|
);
|
|
@@ -1418,7 +1470,7 @@ export async function executeTelegramUpdatePlan<
|
|
|
1418
1470
|
assertExecutionCurrent();
|
|
1419
1471
|
await deps.answerCallbackQuery(
|
|
1420
1472
|
callbackQueryId,
|
|
1421
|
-
"
|
|
1473
|
+
formatTelegramUnauthorizedDenial("plain"),
|
|
1422
1474
|
);
|
|
1423
1475
|
}
|
|
1424
1476
|
return;
|
|
@@ -1433,7 +1485,8 @@ export async function executeTelegramUpdatePlan<
|
|
|
1433
1485
|
assertExecutionCurrent();
|
|
1434
1486
|
await deps.answerGuestQuery(
|
|
1435
1487
|
plan.guestMessage.guest_query_id,
|
|
1436
|
-
"
|
|
1488
|
+
formatTelegramUnauthorizedDenial("html"),
|
|
1489
|
+
{ parseMode: "HTML" },
|
|
1437
1490
|
);
|
|
1438
1491
|
return;
|
|
1439
1492
|
}
|
|
@@ -1553,8 +1606,8 @@ export async function executeTelegramUpdatePlan<
|
|
|
1553
1606
|
await deps.sendTextReply(
|
|
1554
1607
|
replyTarget.chatId,
|
|
1555
1608
|
replyTarget.messageId,
|
|
1556
|
-
"
|
|
1557
|
-
{ target: replyTarget },
|
|
1609
|
+
formatTelegramUnauthorizedDenial("html"),
|
|
1610
|
+
{ parseMode: "HTML", target: replyTarget },
|
|
1558
1611
|
);
|
|
1559
1612
|
}
|
|
1560
1613
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: generated-control-surface
|
|
3
|
-
description: Proactively compiles current state, available capabilities, and user intent into contextual, evidence-backed, ephemeral prompt-button interfaces. Use on Telegram turns and other prompt-button transports whenever controls materially shorten likely feedback, without requiring an explicit user request,
|
|
3
|
+
description: Proactively compiles current state, available capabilities, and user intent into contextual, evidence-backed, ephemeral prompt-button interfaces. Use on Telegram turns and other prompt-button transports whenever controls materially shorten likely feedback, without requiring an explicit user request; route reusable deterministic loops toward Generative Apps, omit decorative UI, and preserve domain ownership while fixed transport menus and callbacks remain with their runtime owners.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Generated Control Surface
|
|
@@ -23,7 +23,11 @@ The primitive belongs to the Surface plane: it projects State, exposes Agency ca
|
|
|
23
23
|
|
|
24
24
|
## Scope
|
|
25
25
|
|
|
26
|
-
Use this Skill only to synthesize a state-derived prompt-button surface. Operating or modifying an existing Telegram bridge menu, callback interface, fixed frontend,
|
|
26
|
+
Use this Skill only to synthesize a state-derived prompt-button surface. Operating or modifying an existing Telegram bridge menu, callback interface, fixed frontend, runtime-owned control, or installed Generative App stays with that subsystem and does not route here merely because it contains buttons.
|
|
27
|
+
|
|
28
|
+
This Skill and `generative-apps` share one logical button-matrix and `label + prompt` interaction model while retaining different execution owners. On Telegram, the bridge runtime owns the full JSON/CML action notation: this Skill emits an ephemeral surface from current context, while a Generative App returns reusable views through its script ABI. Shared rendering does not justify a third button Skill or move transport grammar into either interaction Skill.
|
|
29
|
+
|
|
30
|
+
When a generated surface reveals repeated stable interaction with bounded state and deterministic transitions, load the complementary `generative-apps` Skill and consider compiling that loop. A Generative App may retain ordinary model-mediated prompt buttons beside deterministic bound methods, so only the stable actions need bypass inference. Conversely, keep one-off, interpretive, changing, and context-heavy interaction here when a reusable script would add no concrete latency, token, cost, reliability, or UX value.
|
|
27
31
|
|
|
28
32
|
On Telegram turns, evaluate this Skill proactively rather than waiting for the user to ask for buttons. Load and apply it when a likely next decision, approval, navigation step, inspection, or bounded action can be made materially easier through controls; its correct output may still contain zero buttons when the admission test fails.
|
|
29
33
|
|
|
@@ -80,7 +84,9 @@ A surface normally contains:
|
|
|
80
84
|
|
|
81
85
|
Prefer 2–6 controls for feedback and decisions; navigation collections may use up to 12 when the additional entries remain scannable. Split larger sets by category or page instead of building a button wall. Do not add navigation controls when the surface is a one-step decision.
|
|
82
86
|
|
|
83
|
-
|
|
87
|
+
When a logical grid has no semantic column headings but Markdown table syntax requires a header row, use the grid's first data row as the syntactic header and render each remaining row once beneath it. Do not insert blank, dash-only, duplicate, or invented placeholder headings: they add a false row to the projected topology. Use ordinary semantic headings when the data actually has named columns.
|
|
88
|
+
|
|
89
|
+
Present compact metadata as stacked key-value rows that reuse status-surface grammar: a short bold label, a colon, and an inline-code value when the value is path-like, numeric, an identifier, or machine state. In Telegram Rich Markdown, use an actual Markdown list or blank paragraph boundaries so soft line breaks cannot collapse several fields into one visual line. Prefer ``- **Path:** `/home/llb` `` and ``- **Entries:** `1–10 of 52` `` over prose fragments joined by a middle dot or other decorative section separator.
|
|
84
90
|
|
|
85
91
|
## Truth Modes
|
|
86
92
|
|
|
@@ -118,7 +124,7 @@ Re-check mutable targets immediately before execution. Access denial never autho
|
|
|
118
124
|
|
|
119
125
|
## Prompt Buttons
|
|
120
126
|
|
|
121
|
-
Use the transport's canonical prompt-button syntax. For pi-telegram, one top-level `telegram_button` comment accepts one JSON object, double-quoted attributes,
|
|
127
|
+
Use the transport's canonical prompt-button syntax. For pi-telegram, one top-level `telegram_button` comment accepts one JSON object, double-quoted attributes, an adaptive JSON/CML matrix, or positional Compact Matrix Literal (CML). One matrix or row may mix named JSON objects with positional cells, and commas are optional only between completed elements while JSON object internals remain strict. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires an explicit prompt and accepts only `primary`, `success`, or `danger`. It trims atom boundaries, preserves other printable text literally, and decodes only `\|`, `\}`, and `\\`. Prefer CML whenever the model authors the control and it can express the required surface; fall back to expanded JSON only for multiline prompts, non-positional metadata, or a concrete CML parse/render failure, never merely from implementation habit. Deterministic Generative App scripts may return ordinary JSON because their source payload does not consume model-output tokens; author and operate those adapters through the `generative-apps` Skill rather than growing a parallel app workflow here. A top-level cell becomes one full-width row, while a nested row groups one or more controls horizontally without a parser-level width cap. Prefer one layout comment for multiple controls instead of repeating the marker; `telegram_buttons` is a plural alias, not a different format.
|
|
122
128
|
|
|
123
129
|
### Semantic Row Composition
|
|
124
130
|
|
|
@@ -158,6 +164,8 @@ Preserve the ordinary admission test: proactively offer an interactive surface e
|
|
|
158
164
|
- Keep trivial interaction state in the visible conversation. When state becomes too large, long-lived, or error-prone for reliable conversational reconstruction, persist a small human-auditable Markdown state artifact at a deterministic task-owned path and render from it. The artifact belongs to the underlying task or domain, not to this Skill as shadow application state.
|
|
159
165
|
- When transition rules are non-trivial or correctness-sensitive, use a small deterministic state-transition owner—script, module, tool, or existing domain API—that validates `current state + admitted action → next state`; let the model compile the surface from its result instead of informally simulating every transition. Do not create code or files for a trivial one-step interaction.
|
|
160
166
|
- Treat repeated clicks against current state, not stale button appearance. If an action is already consumed or unavailable, keep state unchanged and say so briefly. Preserve an occupied or selected button when spatial layout matters, using its label or selected style as the visual state; omit unavailable controls when layout does not matter. Transport-level disabled buttons are optional, not assumed.
|
|
167
|
+
- Preserve tap-ahead on transports where existing controls remain actionable and rapid clicks queue separate turns. In a source-then-destination interaction, persist the source selection but do not regenerate the board, enumerate destinations, or duplicate controls between the two prompts; emit at most a minimal acknowledgement and let the already visible surface carry the destination click. Regenerate after the completed transition, invalid input, or evidence that the transport cannot preserve the intermediate surface.
|
|
168
|
+
- Resolve coordinate selection by current state rather than rigid click parity. Clicking any currently selectable source selects or replaces the source and then waits; acknowledge a replacement tersely without regenerating the surface. Only a click that is not a selectable source becomes a destination attempt when a source is already selected, at which point the domain owner validates the transition. Without a selected source, a non-source coordinate is a no-op.
|
|
161
169
|
|
|
162
170
|
```html
|
|
163
171
|
<!-- telegram_button {"label":"🔍 Inspect run","prompt":"Inspect Run run:example read-only, summarize its current status and latest material evidence, then regenerate relevant supervision controls."} -->
|