@orchestero/codex-gateway 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,54 @@
1
+ # @orchestero/codex-gateway
2
+
3
+ ## 0.0.3
4
+ ### Patch Changes
5
+
6
+
7
+
8
+ - [#15](https://github.com/phuctm97/orchestero/pull/15) [`b90616e`](https://github.com/phuctm97/orchestero/commit/b90616eea13712cb6c0e1610ba95419ad8f37ce6) Thanks [@phuctm97](https://github.com/phuctm97)! - Republish private packages to GitHub Packages.
9
+
10
+ ## 0.0.2
11
+ ### Patch Changes
12
+
13
+
14
+
15
+ - [#14](https://github.com/phuctm97/orchestero/pull/14) [`872581a`](https://github.com/phuctm97/orchestero/commit/872581a23a6a4568a0bb185f6a2647098028980e) Thanks [@phuctm97](https://github.com/phuctm97)! - Republish private packages to GitHub Packages.
16
+
17
+ ## 0.0.1
18
+ ### Patch Changes
19
+
20
+
21
+
22
+ - [#6](https://github.com/phuctm97/orchestero/pull/6) [`63837b6`](https://github.com/phuctm97/orchestero/commit/63837b643087418d5314a2fd8ed3bc84e3250f5f) Thanks [@phuctm97](https://github.com/phuctm97)! - Replace WhatsApp with Zalo in Codex Gateway and on the website.
23
+
24
+
25
+
26
+ - [#11](https://github.com/phuctm97/orchestero/pull/11) [`ef77c71`](https://github.com/phuctm97/orchestero/commit/ef77c71bedd49d27535ec25253fac22d92b56722) Thanks [@phuctm97](https://github.com/phuctm97)! - Add per-agent working directories to Codex Gateway config.
27
+
28
+
29
+
30
+ - [#3](https://github.com/phuctm97/orchestero/pull/3) [`f856102`](https://github.com/phuctm97/orchestero/commit/f8561025acd5bf5704a24c15c3e22531f98c8d90) Thanks [@phuctm97](https://github.com/phuctm97)! - Add Codex gateway executable for chat channel integrations.
31
+
32
+
33
+
34
+ - [#4](https://github.com/phuctm97/orchestero/pull/4) [`82076cb`](https://github.com/phuctm97/orchestero/commit/82076cbac8652e60def881d73baf20996a1c61ed) Thanks [@phuctm97](https://github.com/phuctm97)! - Keep Codex app-server connections alive across chat turns.
35
+
36
+
37
+
38
+ - [#13](https://github.com/phuctm97/orchestero/pull/13) [`7fc080b`](https://github.com/phuctm97/orchestero/commit/7fc080be7e83262447ec40e18c1853dc30f752d0) Thanks [@phuctm97](https://github.com/phuctm97)! - Publish private packages to GitHub Packages.
39
+
40
+
41
+
42
+ - [#10](https://github.com/phuctm97/orchestero/pull/10) [`f0893ad`](https://github.com/phuctm97/orchestero/commit/f0893adb463e1372866ba9fcec6b6a92f9da78d6) Thanks [@phuctm97](https://github.com/phuctm97)! - Avoid wasted Codex Gateway resources during agent setup.
43
+
44
+
45
+
46
+ - [#9](https://github.com/phuctm97/orchestero/pull/9) [`1b3fef1`](https://github.com/phuctm97/orchestero/commit/1b3fef16505282b6efbe4ded7caae4b5a2f3971d) Thanks [@phuctm97](https://github.com/phuctm97)! - Load Codex Gateway agents from file-based channel config.
47
+
48
+
49
+
50
+ - [#12](https://github.com/phuctm97/orchestero/pull/12) [`623028c`](https://github.com/phuctm97/orchestero/commit/623028cfba1dd4157da56b7a31c75dd807d48250) Thanks [@phuctm97](https://github.com/phuctm97)! - Add first-party Zalo adapter support with webhook and long-polling runtime modes.
51
+
52
+
53
+
54
+ - [#5](https://github.com/phuctm97/orchestero/pull/5) [`e94acc4`](https://github.com/phuctm97/orchestero/commit/e94acc43a5947699e29667a82f38fe0b5dcf3425) Thanks [@phuctm97](https://github.com/phuctm97)! - Improve Codex gateway runtime logging and chat error replies.
@@ -0,0 +1,191 @@
1
+ import * as z from "zod";
2
+
3
+ type AgentConfigSource = string | URL;
4
+
5
+ type TelegramConfig = {
6
+ botToken: string;
7
+ secretToken?: string;
8
+ userName?: string;
9
+ };
10
+
11
+ type ZaloConfig = {
12
+ botToken: string;
13
+ longPolling?: ZaloLongPollingConfig;
14
+ mode?: ZaloMode;
15
+ secretToken?: string;
16
+ userName?: string;
17
+ };
18
+
19
+ type ZaloLongPollingConfig = {
20
+ deleteWebhook?: boolean;
21
+ retryDelayMs?: number;
22
+ timeout?: number;
23
+ };
24
+
25
+ type ZaloMode = "auto" | "polling" | "webhook";
26
+
27
+ type AgentConfig = {
28
+ id: string;
29
+ workingDirectory?: string;
30
+ telegram?: TelegramConfig;
31
+ zalo?: ZaloConfig;
32
+ };
33
+
34
+ const defaultSource = "agents.local.json";
35
+ const agentIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
36
+
37
+ function optionalStringSchema(message: string) {
38
+ return z
39
+ .string({ error: message })
40
+ .trim()
41
+ .transform((value) => value || undefined)
42
+ .optional();
43
+ }
44
+
45
+ function requiredStringSchema(message: string) {
46
+ return z.string({ error: message }).trim().min(1, { error: message });
47
+ }
48
+
49
+ const telegramConfigSchema = z.object(
50
+ {
51
+ botToken: requiredStringSchema("Expected Telegram bot token."),
52
+ secretToken: optionalStringSchema("Expected Telegram secret token."),
53
+ userName: optionalStringSchema("Expected Telegram username."),
54
+ },
55
+ { error: "Expected Telegram config." },
56
+ );
57
+
58
+ const zaloConfigSchema = z.object(
59
+ {
60
+ botToken: requiredStringSchema("Expected Zalo bot token."),
61
+ longPolling: z
62
+ .object(
63
+ {
64
+ deleteWebhook: z.boolean().optional(),
65
+ retryDelayMs: z
66
+ .number({ error: "Expected Zalo polling retry delay." })
67
+ .positive({ error: "Expected Zalo polling retry delay." })
68
+ .optional(),
69
+ timeout: z
70
+ .number({ error: "Expected Zalo polling timeout." })
71
+ .positive({ error: "Expected Zalo polling timeout." })
72
+ .optional(),
73
+ },
74
+ { error: "Expected Zalo long polling config." },
75
+ )
76
+ .optional(),
77
+ mode: z.enum(["auto", "polling", "webhook"]).optional(),
78
+ secretToken: optionalStringSchema("Expected Zalo secret token."),
79
+ userName: optionalStringSchema("Expected Zalo username."),
80
+ },
81
+ { error: "Expected Zalo config." },
82
+ );
83
+
84
+ const agentConfigSchema = z
85
+ .object(
86
+ {
87
+ id: requiredStringSchema("Expected agent id.").regex(agentIdPattern, {
88
+ error: "Expected agent id to be a lowercase slug.",
89
+ }),
90
+ workingDirectory: optionalStringSchema("Expected working directory."),
91
+ telegram: telegramConfigSchema.optional(),
92
+ zalo: zaloConfigSchema.optional(),
93
+ },
94
+ { error: "Expected agent id." },
95
+ )
96
+ .refine((value) => value.telegram || value.zalo, {
97
+ message: "Expected agent channel config.",
98
+ });
99
+
100
+ const agentsConfigSchema = z
101
+ .object(
102
+ {
103
+ agents: z
104
+ .array(agentConfigSchema, { error: "Expected agents config." })
105
+ .min(1, { error: "Expected at least one agent config." }),
106
+ },
107
+ { error: "Expected agents config." },
108
+ )
109
+ .superRefine((value, context) => {
110
+ const ids = new Set<string>();
111
+
112
+ for (const agent of value.agents) {
113
+ if (ids.has(agent.id)) {
114
+ context.addIssue({
115
+ code: "custom",
116
+ message: `Expected unique agent id: ${agent.id}.`,
117
+ });
118
+ }
119
+
120
+ ids.add(agent.id);
121
+ }
122
+ });
123
+
124
+ type ParsedTelegramConfig = z.infer<typeof telegramConfigSchema>;
125
+ type ParsedZaloConfig = z.infer<typeof zaloConfigSchema>;
126
+ type ParsedAgentConfig = z.infer<typeof agentConfigSchema>;
127
+
128
+ function getErrorMessage(error: unknown) {
129
+ if (error instanceof Error) {
130
+ return error.message;
131
+ }
132
+
133
+ return String(error);
134
+ }
135
+
136
+ function getValidationMessage(error: z.ZodError) {
137
+ return error.issues.map((issue) => issue.message).join("\n");
138
+ }
139
+
140
+ function normalizeTelegramConfig(config: ParsedTelegramConfig): TelegramConfig {
141
+ return {
142
+ botToken: config.botToken,
143
+ secretToken: config.secretToken,
144
+ userName: config.userName,
145
+ };
146
+ }
147
+
148
+ function normalizeZaloConfig(config: ParsedZaloConfig): ZaloConfig {
149
+ return {
150
+ botToken: config.botToken,
151
+ longPolling: config.longPolling,
152
+ mode: config.mode,
153
+ secretToken: config.secretToken,
154
+ userName: config.userName,
155
+ };
156
+ }
157
+
158
+ function normalizeAgentConfig(config: ParsedAgentConfig): AgentConfig {
159
+ return {
160
+ id: config.id,
161
+ workingDirectory: config.workingDirectory,
162
+ telegram: config.telegram
163
+ ? normalizeTelegramConfig(config.telegram)
164
+ : undefined,
165
+ zalo: config.zalo ? normalizeZaloConfig(config.zalo) : undefined,
166
+ };
167
+ }
168
+
169
+ function parse(value: unknown) {
170
+ const result = agentsConfigSchema.safeParse(value);
171
+ if (!result.success) {
172
+ throw new Error(getValidationMessage(result.error));
173
+ }
174
+
175
+ return result.data.agents.map(normalizeAgentConfig);
176
+ }
177
+
178
+ async function load(source: AgentConfigSource = defaultSource) {
179
+ try {
180
+ return parse(await Bun.file(source).json());
181
+ } catch (error) {
182
+ throw new Error(
183
+ `Failed to load agent config: ${source.toString()}. ${getErrorMessage(
184
+ error,
185
+ )}`,
186
+ { cause: error },
187
+ );
188
+ }
189
+ }
190
+
191
+ export const agentConfigs = { load, parse };
@@ -0,0 +1,287 @@
1
+ import { createTelegramAdapter } from "@chat-adapter/telegram";
2
+ import type { Adapter, Message, StateAdapter, Thread } from "chat";
3
+ import { Chat } from "chat";
4
+ import type { agentConfigs } from "~/lib/agent-configs";
5
+ import { codexAppServer } from "~/lib/codex-app-server";
6
+ import { pgState } from "~/lib/pg-state";
7
+ import { createZaloAdapter } from "~/lib/zalo-adapter";
8
+
9
+ type CodexAppServer = {
10
+ stream: ReturnType<typeof codexAppServer.create>["stream"];
11
+ };
12
+
13
+ type ChatBotAdapters = Partial<{
14
+ telegram: Adapter;
15
+ zalo: Adapter;
16
+ }>;
17
+
18
+ type ChatBotThreadState = {
19
+ codexThreadId?: string;
20
+ };
21
+
22
+ type ChatBotLogContext = Record<string, unknown>;
23
+
24
+ type ChatBotLogger = {
25
+ error: (message: string, context?: ChatBotLogContext) => void;
26
+ info: (message: string, context?: ChatBotLogContext) => void;
27
+ };
28
+
29
+ type ChatBotOptions = {
30
+ adapters?: ChatBotAdapters;
31
+ codex: CodexAppServer;
32
+ logger?: ChatBotLogger;
33
+ state: StateAdapter;
34
+ userName?: string;
35
+ workingDirectory?: string;
36
+ };
37
+
38
+ type ChatBotAgentConfig = ReturnType<typeof agentConfigs.parse>[number];
39
+
40
+ type ChatBotManyOptions = Omit<
41
+ ChatBotOptions,
42
+ "adapters" | "codex" | "state" | "workingDirectory"
43
+ > & {
44
+ agents: ChatBotAgentConfig[];
45
+ codex?: CodexAppServer;
46
+ };
47
+
48
+ type RespondOptions = {
49
+ codex: CodexAppServer;
50
+ logger?: ChatBotLogger;
51
+ message: {
52
+ id?: string;
53
+ raw?: unknown;
54
+ text: string;
55
+ threadId?: string;
56
+ };
57
+ thread: {
58
+ id?: string;
59
+ post: (message: AsyncIterable<string>) => Promise<unknown>;
60
+ readonly state: Promise<ChatBotThreadState | null>;
61
+ setState: (state: Partial<ChatBotThreadState>) => Promise<void>;
62
+ startTyping: (status?: string) => Promise<unknown>;
63
+ };
64
+ workingDirectory?: string;
65
+ };
66
+
67
+ const fallbackMessage = "Sorry, I ran into an issue. Please try again.";
68
+ const unsupportedGroupMessage = "Please send a direct message.";
69
+ const unsupportedMessage = "Please send a text message.";
70
+ const defaultLogger: ChatBotLogger = {
71
+ error(message, context) {
72
+ console.error(message, context);
73
+ },
74
+ info(message, context) {
75
+ console.info(message, context);
76
+ },
77
+ };
78
+
79
+ function getPlatform(threadId?: string) {
80
+ return threadId?.split(":", 1)[0] || "unknown";
81
+ }
82
+
83
+ function getErrorMessage(error: unknown) {
84
+ if (error instanceof Error) {
85
+ return error.message;
86
+ }
87
+
88
+ return String(error);
89
+ }
90
+
91
+ function getZaloChatType(raw: unknown) {
92
+ const rawMessage = Reflect.get(Object(raw ?? {}), "message");
93
+ const chat = Reflect.get(Object(rawMessage ?? {}), "chat");
94
+ const chatType = Reflect.get(Object(chat ?? {}), "chat_type");
95
+
96
+ if (typeof chatType === "string") {
97
+ return chatType;
98
+ }
99
+ }
100
+
101
+ async function postText(thread: RespondOptions["thread"], message: string) {
102
+ await thread.post(
103
+ (async function* () {
104
+ yield message;
105
+ })(),
106
+ );
107
+ }
108
+
109
+ async function startTyping(
110
+ thread: RespondOptions["thread"],
111
+ logger: ChatBotLogger,
112
+ context: ChatBotLogContext,
113
+ ) {
114
+ try {
115
+ await thread.startTyping("Working...");
116
+ } catch (error) {
117
+ logger.info("Ignored chat typing failure.", {
118
+ ...context,
119
+ error: getErrorMessage(error),
120
+ });
121
+ }
122
+ }
123
+
124
+ function createAdapters(agent: ChatBotAgentConfig): ChatBotAdapters {
125
+ const adapters: ChatBotAdapters = {};
126
+
127
+ if (agent.telegram) {
128
+ adapters.telegram = createTelegramAdapter({
129
+ botToken: agent.telegram.botToken,
130
+ secretToken: agent.telegram.secretToken,
131
+ userName: agent.telegram.userName,
132
+ });
133
+ }
134
+
135
+ if (agent.zalo) {
136
+ adapters.zalo = createZaloAdapter({
137
+ botToken: agent.zalo.botToken,
138
+ longPolling: agent.zalo.longPolling,
139
+ mode: agent.zalo.mode,
140
+ secretToken: agent.zalo.secretToken,
141
+ userName: agent.zalo.userName,
142
+ });
143
+ }
144
+
145
+ return adapters;
146
+ }
147
+
148
+ async function respond({
149
+ codex,
150
+ logger = defaultLogger,
151
+ message,
152
+ thread,
153
+ workingDirectory,
154
+ }: RespondOptions) {
155
+ const input = message.text.trim();
156
+ const threadId = message.threadId || thread.id;
157
+ const platform = getPlatform(threadId);
158
+ const zaloChatType = getZaloChatType(message.raw);
159
+ const logContext = {
160
+ messageId: message.id,
161
+ platform,
162
+ threadId,
163
+ };
164
+
165
+ if (platform === "zalo" && zaloChatType === "GROUP") {
166
+ logger.info("Ignored unsupported chat message.", logContext);
167
+ await postText(thread, unsupportedGroupMessage);
168
+ return;
169
+ }
170
+
171
+ if (!input) {
172
+ logger.info("Ignored unsupported chat message.", logContext);
173
+ await postText(thread, unsupportedMessage);
174
+ return;
175
+ }
176
+
177
+ const startedAt = Date.now();
178
+ let codexThreadId: string | undefined;
179
+ let resumed = false;
180
+
181
+ try {
182
+ const state = await thread.state;
183
+ codexThreadId = state?.codexThreadId;
184
+ resumed = Boolean(codexThreadId);
185
+ logger.info("Started Codex gateway turn.", {
186
+ ...logContext,
187
+ codexThreadId,
188
+ resumed,
189
+ });
190
+ await startTyping(thread, logger, logContext);
191
+ await thread.post(
192
+ codex.stream({
193
+ input,
194
+ onThreadId: (nextCodexThreadId) => {
195
+ codexThreadId = nextCodexThreadId;
196
+ return thread.setState({ codexThreadId: nextCodexThreadId });
197
+ },
198
+ threadId: codexThreadId,
199
+ workingDirectory,
200
+ }),
201
+ );
202
+ logger.info("Completed Codex gateway turn.", {
203
+ ...logContext,
204
+ codexThreadId,
205
+ durationMs: Date.now() - startedAt,
206
+ resumed,
207
+ });
208
+ } catch (error) {
209
+ logger.error("Failed Codex gateway turn.", {
210
+ ...logContext,
211
+ codexThreadId,
212
+ durationMs: Date.now() - startedAt,
213
+ error: getErrorMessage(error),
214
+ resumed,
215
+ });
216
+ await postText(thread, fallbackMessage);
217
+ }
218
+ }
219
+
220
+ function create(options: ChatBotOptions) {
221
+ const {
222
+ adapters = {},
223
+ codex,
224
+ logger = defaultLogger,
225
+ state,
226
+ userName = "orchestero",
227
+ workingDirectory,
228
+ } = options;
229
+ const bot = new Chat({
230
+ userName,
231
+ adapters,
232
+ state,
233
+ logger: "info",
234
+ concurrency: "queue",
235
+ fallbackStreamingPlaceholderText: "Working...",
236
+ });
237
+ const handler = (thread: Thread<ChatBotThreadState>, message: Message) =>
238
+ respond({ codex, logger, message, thread, workingDirectory });
239
+
240
+ bot.onDirectMessage(handler);
241
+
242
+ return bot;
243
+ }
244
+
245
+ function createMany(options: ChatBotManyOptions) {
246
+ const { agents, codex, ...botOptions } = options;
247
+ if (agents.length === 0) {
248
+ throw new Error("Expected at least one agent.");
249
+ }
250
+
251
+ const agentIds = new Set<string>();
252
+
253
+ for (const agent of agents) {
254
+ if (!agent.telegram && !agent.zalo) {
255
+ throw new Error("Expected agent channel config.");
256
+ }
257
+
258
+ if (agentIds.has(agent.id)) {
259
+ throw new Error(`Expected unique agent id: ${agent.id}.`);
260
+ }
261
+
262
+ agentIds.add(agent.id);
263
+ }
264
+
265
+ const codexRuntime = codex || codexAppServer.create();
266
+ const bots: Record<string, ReturnType<typeof create>> = {};
267
+
268
+ for (const agent of agents) {
269
+ bots[agent.id] = create({
270
+ ...botOptions,
271
+ adapters: createAdapters(agent),
272
+ codex: codexRuntime,
273
+ state: pgState.create({ keyPrefix: `agent:${agent.id}` }),
274
+ userName: botOptions.userName || `orchestero-${agent.id}`,
275
+ workingDirectory: agent.workingDirectory,
276
+ });
277
+ }
278
+
279
+ return bots;
280
+ }
281
+
282
+ export const chatBot = {
283
+ create,
284
+ createAdapters,
285
+ createMany,
286
+ respond,
287
+ };