@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/docs/outbound.md
CHANGED
|
@@ -164,11 +164,11 @@ I can continue.
|
|
|
164
164
|
|
|
165
165
|
Rules:
|
|
166
166
|
|
|
167
|
-
- `telegram_button` accepts a JSON object, JSON matrix, [Compact Matrix Literal](./compact-matrix-literal.md), or double-quoted HTML-like attributes; `telegram_buttons` is an exact plural alias. CML uses `{value}` or `{label|prompt}`, trims
|
|
167
|
+
- `telegram_button` accepts a JSON object, adaptive JSON/CML matrix, positional [Compact Matrix Literal](./compact-matrix-literal.md), or double-quoted HTML-like attributes; `telegram_buttons` is an exact plural alias. Named JSON objects and positional cells may coexist in one matrix or row, 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 a prompt and accepts only `primary`, `success`, or `danger`. It trims every atom, preserves non-structural printable text, and decodes only `\|`, `\}`, and `\\`. Shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are rejected.
|
|
168
168
|
- A colon after either button marker is rejected so every payload form shares one unambiguous action marker.
|
|
169
169
|
- Use `label` plus `prompt`, or the compact `value` key when both strings are identical. Explicit `label` or `prompt` takes precedence over its `value` fallback. Use JSON with `\n` escapes for multiline prompts.
|
|
170
170
|
- The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it remains literal Markdown.
|
|
171
|
-
- Prefer one matrix comment for multiple buttons. Each top-level JSON object or CML cell becomes one full-width inline-keyboard row in source order; a nested row groups one or more buttons horizontally. The parser imposes no artificial per-row width cap; empty rows, malformed cells, unknown/trailing CML escapes, a
|
|
171
|
+
- Prefer one matrix comment for multiple buttons. Each top-level JSON object or CML cell becomes one full-width inline-keyboard row in source order; a nested row groups one or more buttons horizontally. The parser imposes no artificial per-row width cap; empty rows, malformed cells, unknown/trailing CML escapes, a third unescaped CML separator, empty atoms, unknown selected styles, and deeper nesting are rejected atomically. Generated surfaces default to five columns and expand to six through eight only for short position-bearing labels. Repeated singular comments remain valid.
|
|
172
172
|
- Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
|
|
173
173
|
- After Telegram accepts a generated button callback as a queued prompt, the bridge changes that exact button to its configured selection style without changing agent-authored text or emoji. Set `selected_style` to `primary` (blue), `success` (green), or `danger` (red); omitted or invalid values fall back to `primary`. The style never suppresses queue admission. Other choices stay visually unchanged and remain available; the callback acknowledgement remains the fallback on clients that do not render button styles.
|
|
174
174
|
- When generated button markup is the entire assistant reply, the bridge supplies the standard `☑️ **Choose an option:**` heading as visible message text so Telegram has a message to which it can attach the inline keyboard.
|
package/docs/public-api.md
CHANGED
|
@@ -61,11 +61,12 @@ This command surface is a mobile companion subset, not a raw terminal-command br
|
|
|
61
61
|
|
|
62
62
|
### Tools and assistant-authored actions
|
|
63
63
|
|
|
64
|
+
- `telegram_bind({ app, script, argument? } | { app, method, argument? })` installs and initializes one canonical managed Generative App module under `<agent-dir>/genapps/<app>/<app>.mjs`, or invokes one named method on an installed app. Installation rejects silent replacement and noncanonical/symlink sources. Methods receive immutable JSON state, one optional JSON argument, cancellation, revision, and a bounded non-shell process port; successful state changes commit to `state.json` plus `states.jsonl`, while output-only methods leave history unchanged. After one-shot `tgbtn` resolution, a complete `app::method` or `app::method(<strict JSON>)` prompt invokes the installed app before Pi queue admission and sends its planned Markdown/buttons directly; malformed or failed bound actions never fall back to a model prompt. Direct app-output buttons retain hidden source revisions and stale actions fail before method execution; sibling processes serialize transitions and recover dead lock owners. Bound actions send a fresh message by default and retain the clicked button's selected state on its prior surface. A result may opt into `viewMode: "edit"` to replace the callback message and keyboard in place, with one fresh-send fallback only for that explicit action. Agent-mediated initial-surface revisions, process-birth lock proof, automatic refresh, and voice output remain open.
|
|
64
65
|
- `telegram_attach(paths, chat_id?, thread_id?, caption?)` is the stable artifact delivery tool for generated files. During Telegram turns it queues files for the active reply; with `assistant.rendering: "rich"`, exactly one PNG/JPEG, MP4, or MP3 artifact plus non-empty final Markdown can become one reply-anchored Rich Message. HTML mode, multiple/unsupported files, Guest Mode, and voice outputs retain their established paths. Outside Telegram turns the tool sends files directly to the paired/default chat, the registered follower's assigned thread, or an explicit `chat_id` plus optional `thread_id` when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus.
|
|
65
66
|
- `telegram_message(text, chat_id?, thread_id?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. During an active Telegram turn, omitted targeting and an explicit target equal to that turn are rejected so the ordinary final-reply path remains the sole current-target response; an explicit different chat/thread target remains allowed for requested cross-target delivery. Outside active turns, paired/default local/TUI delivery remains unchanged. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
|
|
66
|
-
- The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, and profile-specific debugging guidance. The regular prompt routes applicable turns to that Skill. `telegram_attach` and `telegram_message` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
|
|
67
|
+
- The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, Generative App operation, and profile-specific debugging guidance. The regular prompt routes applicable turns to that Skill. `telegram_attach`, `telegram_bind`, and `telegram_message` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
|
|
67
68
|
- `telegram_voice` hidden comments request Telegram-native voice delivery through either a JSON object or double-quoted attributes. Equivalent `text` or `value` supplies the spoken payload; explicit `text` takes precedence.
|
|
68
|
-
- `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. One marker accepts a JSON object, JSON matrix, [Compact Matrix Literal](./compact-matrix-literal.md), or double-quoted attributes; `telegram_buttons` is an exact plural alias. Top-level
|
|
69
|
+
- `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. One marker accepts a JSON object, adaptive JSON/CML matrix, positional [Compact Matrix Literal](./compact-matrix-literal.md), or double-quoted attributes; `telegram_buttons` is an exact plural alias. Named JSON objects and positional cells may coexist in one matrix or row, and commas are optional only between completed elements. Top-level cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}`, `{label|prompt}`, or `{label|prompt|selected_style}`; the optional third atom requires a prompt and accepts only `primary`, `success`, or `danger`. It trims atom boundaries and supports only the minimal escapes `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Use JSON `label` plus `prompt`, or `value` when both strings are identical. Action markers are colon-free; colon-prefixed payloads are rejected. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit standalone button actions.
|
|
69
70
|
|
|
70
71
|
Prompt guidance is context-aware: local/TUI prompts see only explicit direct-delivery guidance, while Telegram-originated turns receive the full action-comment syntax and phone-width output contract.
|
|
71
72
|
|
package/docs/ui-style.md
CHANGED
|
@@ -54,7 +54,7 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
|
|
|
54
54
|
| `➡️` | Choose replacement target | Thread replace/restore target buttons that select which Pi instance should move to the current thread | Use inside the second replace/restore chooser, not for ordinary reroutes. |
|
|
55
55
|
| `☑️` | Activate / choose this item | Model detail activation action, generated button-only choice heading | Positive selection cue; use `🟢 Active` for already-current state. |
|
|
56
56
|
| `❌` | No / cancel | Confirmation cancel buttons | Use for safe cancellation, not destructive removal. |
|
|
57
|
-
| `🗑` | Delete /
|
|
57
|
+
| `🗑` | Delete / defer removal | Destructive confirmations and removal reaction | In the queue menu, reversible Keep/Skip selectors replace immediate deletion. |
|
|
58
58
|
|
|
59
59
|
### State Indicators And Button Grammars
|
|
60
60
|
|
|
@@ -62,13 +62,21 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
|
|
|
62
62
|
| --- | --- | --- | --- |
|
|
63
63
|
| `🟢` | Current/active/enabled `On` | Current option in vertical lists, active state rows, active `On` toggle | One strong current marker per option list. |
|
|
64
64
|
| `🟡` | Active `Off` or elevated/filter state | Active `Off` toggle, Priority/Scoped active tab | Yellow means intentionally not-normal or off/default-caution, not error. |
|
|
65
|
+
| `🔴` | Active destructive/deferred disposition | Active queue `Skip` selector | Red distinguishes a prompt that will be discarded at dispatch from reversible neutral or elevated state. |
|
|
65
66
|
| `🟣` | Normal/default active tab | Normal priority tab, All/default scope tab, active page picker | Use for neutral active tabs. |
|
|
66
67
|
| `⚫️` | Inactive placeholder | Inactive toggle values and inactive tabs | Keeps row width stable. |
|
|
67
68
|
| `⬆️` | Navigate upward | `⬆️ Main menu`, `⬆️ Back` | Always first row in submenus. |
|
|
68
69
|
|
|
69
70
|
### Queue Reaction Shortcuts
|
|
70
71
|
|
|
71
|
-
Queue reactions are shortcut controls for waiting turns. Preserve their semantics across Telegram reactions, queue-menu rows, status previews, and tests.
|
|
72
|
+
Queue reactions are shortcut controls for waiting turns. Preserve their semantics across Telegram reactions, queue-menu rows, status previews, and tests. Positive emoji control the Priority/Normal lane; negative emoji control Keep/Skip. These categories are independent, may coexist, and mutate only their own dimension. Crossing lanes appends the prompt at the destination FIFO tail; changing Keep/Skip or changing emoji within one category preserves lane position. Skip wins only when dispatch reaches the prompt.
|
|
73
|
+
|
|
74
|
+
Queue item detail renders two independent selector rows:
|
|
75
|
+
|
|
76
|
+
- `🟡 Priority` / `⚫️ Normal` or `⚫️ Priority` / `🟣 Normal` selects the lane.
|
|
77
|
+
- `🟢 Keep` / `⚫️ Skip` or `⚫️ Keep` / `🔴 Skip` selects dispatch disposition.
|
|
78
|
+
|
|
79
|
+
The menu may clear internal Skip but cannot remove a reaction created by the user through Telegram's Bot API.
|
|
72
80
|
|
|
73
81
|
| Emoji | Meaning | Canonical surfaces | Notes |
|
|
74
82
|
| --- | --- | --- | --- |
|
|
@@ -77,11 +85,11 @@ Queue reactions are shortcut controls for waiting turns. Preserve their semantic
|
|
|
77
85
|
| `❤` / `❤️` | Promote to priority | Queue reaction shortcut | Normalize display consistently where code normalizes reactions. |
|
|
78
86
|
| `🕊` / `🕊️` | Promote to priority | Queue reaction shortcut | Soft/peaceful promotion gesture. |
|
|
79
87
|
| `🔥` | Promote to priority | Queue reaction shortcut | Urgent/hot promotion gesture. |
|
|
80
|
-
| `👎` |
|
|
81
|
-
| `👻` |
|
|
82
|
-
| `💔` |
|
|
83
|
-
| `💩` |
|
|
84
|
-
| `🗑` |
|
|
88
|
+
| `👎` | Defer removal of waiting turn | Queue reaction shortcut and queue emoji marker | Reversible until the marked turn reaches dispatch; not negative feedback to the agent. |
|
|
89
|
+
| `👻` | Defer removal of waiting turn | Queue reaction shortcut and queue emoji marker | Disappear/remove metaphor. |
|
|
90
|
+
| `💔` | Defer removal of waiting turn | Queue reaction shortcut and queue emoji marker | Reversible cancel metaphor. |
|
|
91
|
+
| `💩` | Defer removal of waiting turn | Queue reaction shortcut and queue emoji marker | Reversible reject metaphor. |
|
|
92
|
+
| `🗑` | Defer removal | Queue reaction shortcut and queue emoji marker | Like Skip, the reaction remains reversible until dispatch reaches the marked turn. |
|
|
85
93
|
|
|
86
94
|
### Decorative Or Local-Example Emoji
|
|
87
95
|
|
package/index.ts
CHANGED
|
@@ -228,6 +228,8 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
228
228
|
const proactivePushChatIdGetter =
|
|
229
229
|
Config.createTelegramProactivePushChatIdGetter(proactivePushTargetGetter);
|
|
230
230
|
const buttonActionStore = Outbound.createTelegramButtonActionStore();
|
|
231
|
+
const planGenerativeAppOutput =
|
|
232
|
+
Outbound.createTelegramOutboundReplyPlanner(buttonActionStore);
|
|
231
233
|
const pendingModelSwitchStore =
|
|
232
234
|
Model.createPendingModelSwitchStore<
|
|
233
235
|
Model.ScopedTelegramModel<ActivePiModel>
|
|
@@ -502,6 +504,16 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
502
504
|
getHandlers: configStore.getOutboundHandlers,
|
|
503
505
|
recordRuntimeEvent,
|
|
504
506
|
});
|
|
507
|
+
const invokeGenerativeAppBoundButtonAction =
|
|
508
|
+
Bindings.createTelegramGenerativeAppBoundButtonActionInvoker({
|
|
509
|
+
agentDir: Paths.resolveAgentDir(),
|
|
510
|
+
assertExecutionCurrent: Updates.assertTelegramUpdateExecutionCurrent,
|
|
511
|
+
getExecutionFence: Updates.getTelegramUpdateExecutionFence,
|
|
512
|
+
planOutput: planGenerativeAppOutput,
|
|
513
|
+
sendMarkdownReply,
|
|
514
|
+
editInteractiveMessage,
|
|
515
|
+
recordRuntimeEvent,
|
|
516
|
+
});
|
|
505
517
|
const {
|
|
506
518
|
activityRuntime,
|
|
507
519
|
activityVerbosityRuntime,
|
|
@@ -760,6 +772,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
760
772
|
settingsMenuCallbackHandler: settingsMenuRuntime.handleCallbackQuery,
|
|
761
773
|
sectionRegistry,
|
|
762
774
|
buttonActionStore,
|
|
775
|
+
invokeBoundButtonAction: invokeGenerativeAppBoundButtonAction,
|
|
763
776
|
inboundHandlerRuntime,
|
|
764
777
|
threadStore,
|
|
765
778
|
updateStatus,
|
|
@@ -1219,6 +1232,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1219
1232
|
|
|
1220
1233
|
Bindings.registerTelegramCommandsAndTools({
|
|
1221
1234
|
pi,
|
|
1235
|
+
agentDir: Paths.resolveAgentDir(),
|
|
1222
1236
|
configStore,
|
|
1223
1237
|
persistConfig: persistTelegramConfigWithSync,
|
|
1224
1238
|
setup,
|
package/lib/bindings.ts
CHANGED
|
@@ -26,6 +26,7 @@ import * as Setup from "./setup.ts";
|
|
|
26
26
|
import * as Status from "./status.ts";
|
|
27
27
|
import * as TelegramApi from "./telegram-api.ts";
|
|
28
28
|
import type { TelegramTarget } from "./target.ts";
|
|
29
|
+
import * as GenerativeApps from "./generative-apps.ts";
|
|
29
30
|
|
|
30
31
|
type ActivePiModel = NonNullable<Pi.ExtensionContext["model"]>;
|
|
31
32
|
|
|
@@ -53,10 +54,7 @@ export interface TelegramQueueBindingRuntime<TContext> {
|
|
|
53
54
|
|
|
54
55
|
export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
55
56
|
store: Queue.TelegramQueueStateStore<TContext>;
|
|
56
|
-
queue: Pick<
|
|
57
|
-
Runtime.TelegramBridgeRuntime["queue"],
|
|
58
|
-
"getNextPriorityReactionOrder" | "incrementNextPriorityReactionOrder"
|
|
59
|
-
>;
|
|
57
|
+
queue: Pick<Runtime.TelegramBridgeRuntime["queue"], "allocateItemOrder">;
|
|
60
58
|
lifecycle: Pick<
|
|
61
59
|
Runtime.TelegramBridgeRuntime["lifecycle"],
|
|
62
60
|
"isCompactionInProgress" | "hasDispatchPending"
|
|
@@ -95,9 +93,7 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
|
95
93
|
}): TelegramQueueBindingRuntime<TContext> {
|
|
96
94
|
const mutation = Queue.createTelegramQueueMutationController({
|
|
97
95
|
...deps.store,
|
|
98
|
-
|
|
99
|
-
incrementNextPriorityReactionOrder:
|
|
100
|
-
deps.queue.incrementNextPriorityReactionOrder,
|
|
96
|
+
allocateLaneOrder: deps.queue.allocateItemOrder,
|
|
101
97
|
onItemsDiscarded(items, ctx) {
|
|
102
98
|
deps.admission.getSettlement()?.onItemsDiscarded(items, ctx);
|
|
103
99
|
},
|
|
@@ -146,6 +142,108 @@ export function createTelegramQueueBindingRuntime<TContext>(deps: {
|
|
|
146
142
|
};
|
|
147
143
|
}
|
|
148
144
|
|
|
145
|
+
export function createTelegramGenerativeAppBoundButtonActionInvoker<
|
|
146
|
+
TQuery extends {
|
|
147
|
+
message?: { chat?: { id?: number }; message_id?: number };
|
|
148
|
+
},
|
|
149
|
+
>(deps: {
|
|
150
|
+
agentDir: string;
|
|
151
|
+
assertExecutionCurrent: (query: TQuery) => void;
|
|
152
|
+
getExecutionFence: (query: TQuery) => GenerativeApps.GenerativeAppExecutionFence | undefined;
|
|
153
|
+
planOutput: ReturnType<typeof OutboundHandlers.createTelegramOutboundReplyPlanner>;
|
|
154
|
+
sendMarkdownReply: (
|
|
155
|
+
chatId: number,
|
|
156
|
+
replyToMessageId: number,
|
|
157
|
+
markdown: string,
|
|
158
|
+
options?: { replyMarkup?: OutboundHandlers.TelegramOutboundButtonMarkup },
|
|
159
|
+
) => Promise<unknown>;
|
|
160
|
+
editInteractiveMessage?: (
|
|
161
|
+
chatId: number,
|
|
162
|
+
messageId: number,
|
|
163
|
+
markdown: string,
|
|
164
|
+
mode: "markdown",
|
|
165
|
+
replyMarkup: OutboundHandlers.TelegramOutboundButtonMarkup,
|
|
166
|
+
) => Promise<void>;
|
|
167
|
+
recordRuntimeEvent: TelegramRuntimeEventRecorder;
|
|
168
|
+
}): (
|
|
169
|
+
action: OutboundHandlers.TelegramOutboundButtonAction,
|
|
170
|
+
query: TQuery,
|
|
171
|
+
) => Promise<false | "new" | "edit"> {
|
|
172
|
+
return async (action, query) => {
|
|
173
|
+
let boundAction: GenerativeApps.GenerativeAppBoundAction | undefined;
|
|
174
|
+
try {
|
|
175
|
+
boundAction = GenerativeApps.parseGenerativeAppBoundAction(action.prompt);
|
|
176
|
+
if (!boundAction) return false;
|
|
177
|
+
deps.assertExecutionCurrent(query);
|
|
178
|
+
const result = await GenerativeApps.invokeGenerativeApp({
|
|
179
|
+
agentDir: deps.agentDir,
|
|
180
|
+
...(deps.getExecutionFence(query)
|
|
181
|
+
? { execution: deps.getExecutionFence(query) }
|
|
182
|
+
: {}),
|
|
183
|
+
...(boundAction.argument !== undefined
|
|
184
|
+
? { argument: boundAction.argument }
|
|
185
|
+
: {}),
|
|
186
|
+
...(action.binding?.app === boundAction.app
|
|
187
|
+
? {
|
|
188
|
+
expectedGeneration: action.binding.generation,
|
|
189
|
+
expectedRevision: action.binding.revision,
|
|
190
|
+
}
|
|
191
|
+
: {}),
|
|
192
|
+
method: boundAction.method,
|
|
193
|
+
app: boundAction.app,
|
|
194
|
+
});
|
|
195
|
+
deps.assertExecutionCurrent(query);
|
|
196
|
+
const chatId = query.message?.chat?.id;
|
|
197
|
+
const messageId = query.message?.message_id;
|
|
198
|
+
if (typeof chatId !== "number" || typeof messageId !== "number") {
|
|
199
|
+
throw new Error("Generative App callback target is unavailable.");
|
|
200
|
+
}
|
|
201
|
+
const reply = deps.planOutput(result.output, {
|
|
202
|
+
binding: {
|
|
203
|
+
generation: result.generation,
|
|
204
|
+
app: result.app,
|
|
205
|
+
revision: result.revision,
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
if (result.viewMode === "edit" && deps.editInteractiveMessage) {
|
|
209
|
+
let editFailed = false;
|
|
210
|
+
try {
|
|
211
|
+
await deps.editInteractiveMessage(
|
|
212
|
+
chatId,
|
|
213
|
+
messageId,
|
|
214
|
+
reply.markdown,
|
|
215
|
+
"markdown",
|
|
216
|
+
reply.replyMarkup ?? { inline_keyboard: [] },
|
|
217
|
+
);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
editFailed = true;
|
|
220
|
+
deps.recordRuntimeEvent("generative-app", error, {
|
|
221
|
+
phase: "bound-action-edit-fallback",
|
|
222
|
+
app: boundAction.app,
|
|
223
|
+
method: boundAction.method,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
deps.assertExecutionCurrent(query);
|
|
227
|
+
if (!editFailed) return "edit";
|
|
228
|
+
}
|
|
229
|
+
deps.assertExecutionCurrent(query);
|
|
230
|
+
await deps.sendMarkdownReply(chatId, messageId, reply.markdown, {
|
|
231
|
+
replyMarkup: reply.replyMarkup,
|
|
232
|
+
});
|
|
233
|
+
deps.assertExecutionCurrent(query);
|
|
234
|
+
return "new";
|
|
235
|
+
} catch (error) {
|
|
236
|
+
deps.recordRuntimeEvent("generative-app", error, {
|
|
237
|
+
phase: "bound-action",
|
|
238
|
+
...(boundAction
|
|
239
|
+
? { app: boundAction.app, method: boundAction.method }
|
|
240
|
+
: {}),
|
|
241
|
+
});
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
149
247
|
export interface TelegramAgentMessageToolRoutingRuntime {
|
|
150
248
|
resolveAgentTarget: TelegramAgentTargetResolver;
|
|
151
249
|
routeAgentMessage: TelegramAgentMessageRouter;
|
|
@@ -331,6 +429,7 @@ export function createTelegramActivityBindingRuntime<TTransportStamp>(deps: {
|
|
|
331
429
|
|
|
332
430
|
interface TelegramCommandsAndToolsBindingDeps {
|
|
333
431
|
pi: Pi.ExtensionAPI;
|
|
432
|
+
agentDir: string;
|
|
334
433
|
configStore: Config.TelegramConfigStore;
|
|
335
434
|
persistConfig: (config?: Config.TelegramConfig) => Promise<void>;
|
|
336
435
|
setup: Setup.TelegramSetupGuard;
|
|
@@ -348,7 +447,10 @@ interface TelegramCommandsAndToolsBindingDeps {
|
|
|
348
447
|
chatId: number,
|
|
349
448
|
replyToMessageId: number | undefined,
|
|
350
449
|
markdown: string,
|
|
351
|
-
options?: {
|
|
450
|
+
options?: {
|
|
451
|
+
replyMarkup?: unknown;
|
|
452
|
+
target?: { chatId: number; threadId?: number };
|
|
453
|
+
},
|
|
352
454
|
) => Promise<number | undefined>;
|
|
353
455
|
callMultipart: OutboundHandlers.TelegramVoiceReplySenderDeps["sendMultipart"];
|
|
354
456
|
getDefaultChatId: () => number | undefined;
|
|
@@ -362,6 +464,7 @@ interface TelegramCommandsAndToolsBindingDeps {
|
|
|
362
464
|
|
|
363
465
|
export function registerTelegramCommandsAndTools({
|
|
364
466
|
pi,
|
|
467
|
+
agentDir,
|
|
365
468
|
configStore,
|
|
366
469
|
persistConfig,
|
|
367
470
|
setup,
|
|
@@ -383,6 +486,13 @@ export function registerTelegramCommandsAndTools({
|
|
|
383
486
|
recordRuntimeEvent,
|
|
384
487
|
updateStatus,
|
|
385
488
|
}: TelegramCommandsAndToolsBindingDeps): void {
|
|
489
|
+
GenerativeApps.registerTelegramBindTool(pi, {
|
|
490
|
+
agentDir,
|
|
491
|
+
getActiveTurn: activeTurnRuntime.get,
|
|
492
|
+
planOutput: OutboundHandlers.createTelegramOutboundReplyPlanner(buttonActionStore),
|
|
493
|
+
sendMarkdownReply,
|
|
494
|
+
recordRuntimeEvent,
|
|
495
|
+
});
|
|
386
496
|
OutboundAttachments.registerTelegramOutboundAttachmentTool(pi, {
|
|
387
497
|
getActiveTurn: activeTurnRuntime.get,
|
|
388
498
|
getDefaultChatId,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generative App isolated method worker
|
|
3
|
+
* Zones: managed Generative App execution, bounded local processes
|
|
4
|
+
* Owns one terminable app-method invocation and its child-process cleanup
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
|
|
11
|
+
if (!parentPort) throw new Error("Generative App worker requires a parent port.");
|
|
12
|
+
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const runningChildren = new Set();
|
|
15
|
+
parentPort.on("message", (message) => {
|
|
16
|
+
if (message?.type !== "abort") return;
|
|
17
|
+
controller.abort();
|
|
18
|
+
for (const child of runningChildren) child.kill();
|
|
19
|
+
parentPort.postMessage({ type: "abort-ack" });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function runBoundedProcess(input) {
|
|
23
|
+
if (!input || typeof input !== "object") {
|
|
24
|
+
throw new Error("Generative App run input must be an object.");
|
|
25
|
+
}
|
|
26
|
+
const command = input.command?.trim();
|
|
27
|
+
if (!command) throw new Error("Generative App run.command is required.");
|
|
28
|
+
if (!Array.isArray(input.args ?? []) || (input.args?.length ?? 0) > workerData.runMaxArgs) {
|
|
29
|
+
throw new Error(`Generative App run.args accepts at most ${workerData.runMaxArgs} strings.`);
|
|
30
|
+
}
|
|
31
|
+
const args = (input.args ?? []).map((argument) => {
|
|
32
|
+
if (typeof argument !== "string") throw new Error("Generative App run.args must contain strings.");
|
|
33
|
+
return argument;
|
|
34
|
+
});
|
|
35
|
+
if (!input.cwd || typeof input.cwd !== "string") {
|
|
36
|
+
throw new Error("Generative App run.cwd is required.");
|
|
37
|
+
}
|
|
38
|
+
const timeoutMs = Math.min(
|
|
39
|
+
workerData.runMaxTimeoutMs,
|
|
40
|
+
Math.max(1, Math.round(input.timeoutMs ?? workerData.methodTimeoutMs)),
|
|
41
|
+
);
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const child = execFile(
|
|
44
|
+
command,
|
|
45
|
+
args,
|
|
46
|
+
{
|
|
47
|
+
cwd: input.cwd,
|
|
48
|
+
encoding: "utf8",
|
|
49
|
+
maxBuffer: workerData.runMaxStreamBytes,
|
|
50
|
+
shell: false,
|
|
51
|
+
signal: controller.signal,
|
|
52
|
+
timeout: timeoutMs,
|
|
53
|
+
windowsHide: true,
|
|
54
|
+
},
|
|
55
|
+
(error, stdout, stderr) => {
|
|
56
|
+
runningChildren.delete(child);
|
|
57
|
+
if (error && error.code !== "ABORT_ERR") {
|
|
58
|
+
resolve({
|
|
59
|
+
code: typeof error.code === "number" ? error.code : 1,
|
|
60
|
+
killed: error.killed === true,
|
|
61
|
+
stderr: String(stderr).slice(-workerData.runMaxStreamBytes),
|
|
62
|
+
stdout: String(stdout).slice(-workerData.runMaxStreamBytes),
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (error) {
|
|
67
|
+
reject(error);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
resolve({
|
|
71
|
+
code: 0,
|
|
72
|
+
killed: false,
|
|
73
|
+
stderr: String(stderr),
|
|
74
|
+
stdout: String(stdout),
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
runningChildren.add(child);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const module = await import(pathToFileURL(workerData.modulePath).href);
|
|
84
|
+
if (typeof module.init !== "function") {
|
|
85
|
+
throw new Error(`Generative App ${workerData.app} must export named function init.`);
|
|
86
|
+
}
|
|
87
|
+
const method = module[workerData.method];
|
|
88
|
+
if (typeof method !== "function") {
|
|
89
|
+
throw new Error(`Generative App ${workerData.app} does not export method ${workerData.method}.`);
|
|
90
|
+
}
|
|
91
|
+
const result = await method({
|
|
92
|
+
...(workerData.argumentPresent ? { argument: workerData.argument } : {}),
|
|
93
|
+
revision: workerData.revision,
|
|
94
|
+
run: runBoundedProcess,
|
|
95
|
+
signal: controller.signal,
|
|
96
|
+
...(workerData.statePresent ? { state: workerData.state } : {}),
|
|
97
|
+
});
|
|
98
|
+
parentPort.postMessage({ ok: true, result });
|
|
99
|
+
} catch (error) {
|
|
100
|
+
parentPort.postMessage({
|
|
101
|
+
ok: false,
|
|
102
|
+
error: error instanceof Error ? error.message : String(error),
|
|
103
|
+
});
|
|
104
|
+
}
|