-
Notifications
You must be signed in to change notification settings - Fork 0
feat(delivery): add @agentworkforce/delivery — unified multi-target messaging #250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
d9055ef
84a4c92
686eaf5
b353732
4f609e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| { | ||
| "name": "@agentworkforce/delivery", | ||
| "version": "0.1.0", | ||
| "private": false, | ||
| "type": "module", | ||
| "main": "dist/index.js", | ||
| "types": "dist/index.d.ts", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./dist/index.d.ts", | ||
| "default": "./dist/index.js" | ||
| }, | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "files": [ | ||
| "dist", | ||
| "package.json" | ||
| ], | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/AgentWorkforce/workforce", | ||
| "directory": "packages/delivery" | ||
| }, | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsc -p tsconfig.json", | ||
| "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", | ||
| "typecheck": "tsc -p tsconfig.json --noEmit", | ||
| "test": "tsc -p tsconfig.json && node --test dist/**/*.test.js dist/*.test.js", | ||
| "lint": "tsc -p tsconfig.json --noEmit" | ||
| }, | ||
| "dependencies": { | ||
| "@agentworkforce/runtime": "^4.0.0", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The manifest declares Useful? React with 👍 / 👎.
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| "@relayfile/relay-helpers": "^0.4.2" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,231 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { slackClient, telegramClient } from '@relayfile/relay-helpers'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { SlackClient, TelegramClient } from '@relayfile/relay-helpers'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { WorkforceCtx } from '@agentworkforce/runtime'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| resolveDeliveryTargets, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| slackChannel, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| telegramChat, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type DeliveryClient, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type DeliveryOptions, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type DeliveryResult, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type DeliveryTransports, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type SlackRef, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type TelegramRef | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } from './types.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const WRITEBACK_TIMEOUT_MS = 15_000; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Create a delivery client that auto-discovers configured transports from | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * the persona context and sends to all of them. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Blocking mode (the default): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * const heads = await delivery.send(header); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * await delivery.send(body, { replyTo: heads }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Non-blocking parentRef mode (zero receipt round-trips): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * const heads = await delivery.publish(header); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * await delivery.send(body, { replyTo: heads, nonBlocking: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export function createDelivery( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ctx: WorkforceCtx, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| transports?: DeliveryTransports | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ): DeliveryClient { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const targets = resolveDeliveryTargets(ctx); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Two sets of clients: blocking (waits for receipt) and non-blocking | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // (0ms timeout, returns draft refs immediately for parentRef threading). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const slackBlocking = transports?.slack ?? (targets.includes('slack') | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? slackClient({ writebackTimeoutMs: WRITEBACK_TIMEOUT_MS }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| : undefined); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const slackNonBlocking = targets.includes('slack') | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? slackClient({ writebackTimeoutMs: 0 }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const telegramBlocking = transports?.telegram ?? (targets.includes('telegram') | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? telegramClient({ writebackTimeoutMs: WRITEBACK_TIMEOUT_MS }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| : undefined); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const telegramNonBlocking = targets.includes('telegram') | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? telegramClient({ writebackTimeoutMs: 0 }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| : undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When custom We should fall back to
Suggested change
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return new DeliveryClientImpl(ctx, targets, { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| slackBlocking, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| slackNonBlocking, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| telegramBlocking, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| telegramNonBlocking | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| interface DeliveryTransportsInternal { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| slackBlocking?: SlackClient; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| slackNonBlocking?: SlackClient; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| telegramBlocking?: TelegramClient; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| telegramNonBlocking?: TelegramClient; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| class DeliveryClientImpl implements DeliveryClient { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly targets: ReadonlyArray<'slack' | 'telegram'>; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private ctx: WorkforceCtx; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| private t: DeliveryTransportsInternal; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ctx: WorkforceCtx, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| targets: Array<'slack' | 'telegram'>, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| transports: DeliveryTransportsInternal | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.ctx = ctx; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.targets = targets; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.t = transports; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| async send(text: string, opts?: DeliveryOptions): Promise<DeliveryResult> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const nonBlocking = opts?.nonBlocking === true; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+94
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
We should check if
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const refs: Array<SlackRef | TelegramRef> = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const errors: string[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const tasks: Promise<void>[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const target of this.targets) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const parentRef = opts?.replyTo?.refs.find((r) => r.provider === target); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (target === 'slack') { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| tasks.push( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.sendSlack(text, parentRef as SlackRef | undefined, nonBlocking) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .then((ref) => { if (ref) refs.push(ref); }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .catch((err) => { errors.push(`slack: ${String(err)}`); }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (target === 'telegram') { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| tasks.push( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.sendTelegram(text, parentRef as TelegramRef | undefined, nonBlocking) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .then((ref) => { if (ref) refs.push(ref); }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .catch((err) => { errors.push(`telegram: ${String(err)}`); }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| await Promise.all(tasks); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // In non-blocking mode, draft refs always succeed (no receipt wait to fail). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Treat any ref as success. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const ok = nonBlocking | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? refs.length > 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| : errors.length === 0 && refs.length === this.targets.length; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!ok && errors.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.ctx.log?.('warn', 'delivery.partial-failure', { errors, nonBlocking }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!ok && refs.length === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Delivery failed to all targets: ${errors.join('; ')}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return { ok, refs }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| async publish(text: string): Promise<DeliveryResult> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.send(text, { nonBlocking: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // ── Slack ────────────────────────────────────────────────────────────── | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private async sendSlack( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| text: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| parentRef: SlackRef | undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| nonBlocking: boolean | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<SlackRef | null> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const channel = slackChannel(this.ctx); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!channel) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (nonBlocking) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.sendSlackNonBlocking(channel, text, parentRef); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.sendSlackBlocking(channel, text, parentRef); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private async sendSlackBlocking( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| channel: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| text: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| parentRef?: SlackRef | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<SlackRef | null> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const client = this.t.slackBlocking; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!client) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = parentRef?.draftRef | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? await client.post(channel, text, { replyTo: parentRef.draftRef }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| : await client.post(channel, text); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!result.ts) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.ctx.log?.('warn', 'delivery.slack.no-receipt', { channel }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| provider: 'slack', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| channel: result.channel, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ts: result.ts, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| draftRef: result.ref | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Non-blocking Slack: uses messages.write() directly with writebackTimeoutMs:0. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * The parentRef is embedded in the message body so the cloud orders the message | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * under the parent server-side — zero receipt round-trips. The returned draftRef | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * is the relay path, usable as a parent for subsequent threaded sends. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Mirrors the x-reply-radar parentRef threading pattern (internal-agents). | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| private async sendSlackNonBlocking( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| channel: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| text: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| parentRef?: SlackRef | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<SlackRef | null> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const client = this.t.slackNonBlocking; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!client) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const body: Record<string, unknown> = { text }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (parentRef?.draftRef) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Embed parentRef in the body — the cloud lifts it from the streamed head | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // and orders this message under the parent once the parent delivers. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| body.parentRef = parentRef.draftRef; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await client.messages.write({ channelId: channel }, body); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| provider: 'slack', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| channel, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ts: '', // Not available yet (non-blocking) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| draftRef: result.path | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // ── Telegram ─────────────────────────────────────────────────────────── | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private async sendTelegram( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| text: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| parentRef: TelegramRef | undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| nonBlocking: boolean | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<TelegramRef | null> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const chatId = telegramChat(this.ctx); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!chatId) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const client = nonBlocking ? this.t.telegramNonBlocking : this.t.telegramBlocking; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!client) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = parentRef?.messageId | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? await client.sendMessage(chatId, text, { replyToMessageId: Number(parentRef.messageId) || undefined }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| : await client.sendMessage(chatId, text); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!nonBlocking && !result.ok) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.ctx.log?.('warn', 'delivery.telegram.no-receipt', { chatId }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| provider: 'telegram', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| chatId: String(result.chatId), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| messageId: result.ok ? result.messageId : '' | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In the documented Useful? React with 👍 / 👎.
Comment on lines
+229
to
+241
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -nP -C4 '\bsendMessage\b|replyToMessageId|messageId|chatId' $(fd -p 'relay-helpers' -t d node_modules 2>/dev/null) 2>/dev/nullRepository: AgentWorkforce/workforce Length of output: 162 🏁 Script executed: # First, locate the delivery.ts file and examine the context around lines 216-228
fd -type f -name "delivery.ts" packages/delivery/src/Repository: AgentWorkforce/workforce Length of output: 333 🏁 Script executed: # Locate the delivery.ts file
fd -t f -name "delivery.ts" packages/delivery/src/Repository: AgentWorkforce/workforce Length of output: 393 🏁 Script executed: # Find the delivery.ts file using find command
find . -name "delivery.ts" -path "*/packages/delivery/src/*"Repository: AgentWorkforce/workforce Length of output: 198 🏁 Script executed: # Read the delivery.ts file to see the context around lines 216-228
cat -n ./packages/delivery/src/delivery.ts | sed -n '200,240p'Repository: AgentWorkforce/workforce Length of output: 1296 🏁 Script executed: # Now let's find where the telegram client is defined/imported
grep -n "telegramBlocking\|telegramNonBlocking\|telegram.*Client" ./packages/delivery/src/delivery.ts | head -20Repository: AgentWorkforce/workforce Length of output: 729 🏁 Script executed: # Find where TelegramClient is defined - likely in relay-helpers
find . -name "*.ts" -o -name "*.d.ts" | xargs grep -l "TelegramClient" | head -5Repository: AgentWorkforce/workforce Length of output: 231 🏁 Script executed: # Check the types.ts file for TelegramClient definition
cat -n ./packages/delivery/src/types.tsRepository: AgentWorkforce/workforce Length of output: 4884 🏁 Script executed: # Search for relay-helpers to understand what sendMessage returns
find . -name "relay-helpers" -o -path "*/node_modules/@relayfile/relay-helpers*" 2>/dev/null | head -5Repository: AgentWorkforce/workforce Length of output: 162 🏁 Script executed: # Search for test files or usage of sendMessage in the codebase
find . -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "sendMessage\|sendTelegram" 2>/dev/null | head -5Repository: AgentWorkforce/workforce Length of output: 162 🏁 Script executed: # Let's look at the Slack implementation for comparison
cat -n ./packages/delivery/src/delivery.ts | sed -n '140,200p'Repository: AgentWorkforce/workforce Length of output: 2470 🏁 Script executed: # Let's see the full send method to understand the flow
cat -n ./packages/delivery/src/delivery.ts | sed -n '100,145p'Repository: AgentWorkforce/workforce Length of output: 1871 🏁 Script executed: # Check package.json to see relay-helpers version/repo
find . -name "package.json" -path "*/packages/delivery/*" -exec cat {} \;Repository: AgentWorkforce/workforce Length of output: 1112 🏁 Script executed: # Search for relay-helpers documentation or type definitions in node_modules or package info
grep -r "interface.*TelegramClient\|sendMessage.*return\|type.*sendMessage" . --include="*.ts" --include="*.d.ts" 2>/dev/null | head -20Repository: AgentWorkforce/workforce Length of output: 162 🏁 Script executed: # Let's search for comments or documentation about sendMessage behavior in non-blocking mode
grep -n "writebackTimeoutMs\|sendMessage\|non-blocking\|nonBlocking" ./packages/delivery/src/delivery.tsRepository: AgentWorkforce/workforce Length of output: 1558 🌐 Web query:
💡 Result: There is no standardized Telegram client API, library, or documentation that defines parameters named "relay-helpers," "sendMessage," or "writebackTimeoutMs" as a unified, interconnected feature set [1][2][3][4][5][6][7]. Based on current technical documentation and repository patterns: 1. Relay Helpers: The term "relay" often refers to custom, project-specific middleware or wrappers designed to forward messages between systems (e.g., Claude Code, local notification services, or HTTP-to-Telegram bridges) [1][3][4][8]. These are typically bespoke implementations rather than a shared, standardized library. 2. writebackTimeoutMs: This parameter does not exist in standard Telegram Bot API documentation [4][9][10]. It is likely a custom configuration option within a specific, private, or project-specific relay implementation intended to manage asynchronous write operations or response timeouts for bridged messages [1][3]. 3. Non-blocking sendMessage: In the context of Telegram clients (such as Telethon or python-telegram-bot), "non-blocking" behavior is typically achieved by using native asynchronous programming patterns (e.g., Citations:
🏁 Script executed: # Let's look for any TypeScript type definitions or interfaces related to sendMessage result
grep -r "result\.ok\|result\.messageId\|result\.chatId" ./packages/delivery/src/ --include="*.ts"Repository: AgentWorkforce/workforce Length of output: 396 Telegram non-blocking threading is broken; parentRef threading relies on the previous messageId which is empty. In non-blocking mode, Fix by either:
Additionally, 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import type { WorkforceCtx } from '@agentworkforce/runtime'; | ||
|
|
||
| /** | ||
| * Resolve a persona input value from the runtime ctx. | ||
| * | ||
| * Resolution order (mirrors persona-kit): agent-provided value → | ||
| * process env (`spec.env` or the input key) → spec default. | ||
| */ | ||
| export function input(ctx: WorkforceCtx, name: string): string | undefined { | ||
| const spec = ctx.persona.inputSpecs?.[name]; | ||
| const v = process.env[spec?.env ?? name] ?? ctx.persona.inputs?.[name] ?? spec?.default; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a deployment supplies an input value and the runner environment also contains the same input/env name, Useful? React with 👍 / 👎. |
||
| return v && String(v).trim() ? String(v).trim() : undefined; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The We should explicitly check for const v = process.env[spec?.env ?? name] ?? ctx.persona.inputs?.[name] ?? spec?.default;
if (v === undefined || v === null) return undefined;
const trimmed = String(v).trim();
return trimmed !== '' ? trimmed : undefined;
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| /** | ||
| * Split a comma-separated string into trimmed, non-empty entries. | ||
| */ | ||
| export function list(raw: string | undefined): string[] { | ||
| return (raw ?? '').split(',').map((s) => s.trim()).filter(Boolean); | ||
| } | ||
|
|
||
| /** | ||
| * Race a promise against a timeout. On timeout the timer rejects with an | ||
| * Error so the caller can catch and fall back. Always clears the timer. | ||
| */ | ||
| export async function withTimeout<T>( | ||
| p: Promise<T>, | ||
| ms: number, | ||
| label: string | ||
| ): Promise<T> { | ||
| let timer: ReturnType<typeof setTimeout>; | ||
| const timeout = new Promise<never>((_, rej) => { | ||
| timer = setTimeout(() => rej(new Error(`${label} timed out after ${ms}ms`)), ms); | ||
| }); | ||
| try { | ||
| return await Promise.race([p, timeout]); | ||
| } finally { | ||
| clearTimeout(timer!); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Fetch with an AbortController timeout. Returns the response on success, | ||
| * or undefined on timeout/network error (never throws). | ||
| */ | ||
| export async function fetchWithTimeout( | ||
| url: string, | ||
| init: RequestInit = {}, | ||
| timeoutMs: number = 8_000 | ||
| ): Promise<Response | undefined> { | ||
| const controller = new AbortController(); | ||
| const timer = setTimeout(() => controller.abort(), timeoutMs); | ||
| try { | ||
| return await fetch(url, { ...init, signal: controller.signal }); | ||
| } catch { | ||
| return undefined; | ||
| } finally { | ||
| clearTimeout(timer); | ||
| } | ||
| } | ||
|
Comment on lines
+57
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In To support defensive programming and prevent ignoring caller-driven aborts, we should listen to the caller's signal and abort our internal controller if the caller's signal aborts. export async function fetchWithTimeout(
url: string,
init: RequestInit = {},
timeoutMs: number = 8_000
): Promise<Response | undefined> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
if (init.signal) {
if (init.signal.aborted) {
controller.abort();
} else {
init.signal.addEventListener('abort', () => controller.abort());
}
}
try {
return await fetch(url, { ...init, signal: controller.signal });
} catch {
return undefined;
} finally {
clearTimeout(timer);
}
} |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| export { createDelivery } from './delivery.js'; | ||
| export { | ||
| resolveDeliveryTargets, | ||
| slackChannel, | ||
| telegramChat, | ||
| type DeliveryClient, | ||
| type DeliveryOptions, | ||
| type DeliveryResult, | ||
| type DeliveryTransports, | ||
| type MessageRef, | ||
| type SlackRef, | ||
| type TelegramRef | ||
| } from './types.js'; | ||
|
|
||
| export { input, list, withTimeout, fetchWithTimeout } from './helpers.js'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a pnpm monorepo, internal workspace dependencies should use the
"workspace:*"specifier rather than a hardcoded version like"^4.0.0". This ensures consistency across packages and correct resolution during local development and publishing.