@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/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 both atoms, preserves non-structural printable text, and decodes only `\|`, `\}`, and `\\`. Shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are rejected.
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 second unescaped CML separator, 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.
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.
@@ -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 matrix cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}` or `{label|prompt}`, 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
+ - `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 / suppress | Queue delete actions, destructive confirmations, suppression reaction | The explicit queue button deletes; the reaction reversibly suppresses a waiting turn. |
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
- | `👎` | Suppress waiting turn | Queue reaction shortcut and suppressed queue badge | Suppression is reversible and is not negative feedback to the agent. |
81
- | `👻` | Suppress waiting turn | Queue reaction shortcut and suppressed queue badge | Disappear/suppress metaphor. |
82
- | `💔` | Suppress waiting turn | Queue reaction shortcut and suppressed queue badge | Reversible cancel metaphor. |
83
- | `💩` | Suppress waiting turn | Queue reaction shortcut and suppressed queue badge | Reversible reject metaphor. |
84
- | `🗑` | Suppress or explicitly delete | Queue reaction shortcut, suppressed queue badge, and queue delete UI | The reaction is reversible suppression; only the explicit queue button is destructive. |
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
- getNextPriorityReactionOrder: deps.queue.getNextPriorityReactionOrder,
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?: { replyMarkup?: unknown },
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
+ }