@messenger-agent/codex-agent 0.24.0-alpha.2
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/dist/app-server-client.d.ts +32 -0
- package/dist/app-server-client.js +316 -0
- package/dist/app-server-protocol.d.ts +325 -0
- package/dist/app-server-protocol.js +1 -0
- package/dist/app.d.ts +3 -0
- package/dist/app.js +15 -0
- package/dist/assets/codex-home/AGENTS.md +50 -0
- package/dist/codex.d.ts +63 -0
- package/dist/codex.js +461 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.js +154 -0
- package/dist/db.d.ts +10 -0
- package/dist/db.js +32 -0
- package/dist/dynamic-tools.d.ts +14 -0
- package/dist/dynamic-tools.js +172 -0
- package/dist/git-init.d.ts +1 -0
- package/dist/git-init.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +19 -0
- package/dist/platform-instructions.d.ts +2 -0
- package/dist/platform-instructions.js +21 -0
- package/dist/prompts/plan-mode-system-reminder.txt +26 -0
- package/dist/prompts/question-tool-description.txt +12 -0
- package/dist/routes/chat.d.ts +27 -0
- package/dist/routes/chat.js +909 -0
- package/dist/routes/shared.d.ts +37 -0
- package/dist/routes/shared.js +218 -0
- package/dist/schemas.d.ts +112 -0
- package/dist/schemas.js +72 -0
- package/dist/tunnel-client.d.ts +15 -0
- package/dist/tunnel-client.js +232 -0
- package/package.json +36 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CommandExecutionItem, DynamicToolCallItem, FileChangeItem, McpToolCallItem, ThreadItem, TurnStartParams, Usage, WebSearchItem } from "../app-server-protocol.js";
|
|
2
|
+
export type TokenUsageBreakdown = {
|
|
3
|
+
input_tokens: number;
|
|
4
|
+
output_tokens: number;
|
|
5
|
+
total_tokens: number;
|
|
6
|
+
};
|
|
7
|
+
export type NormalizedTokenUsage = {
|
|
8
|
+
total: TokenUsageBreakdown;
|
|
9
|
+
last: TokenUsageBreakdown;
|
|
10
|
+
context?: {
|
|
11
|
+
used_tokens: number;
|
|
12
|
+
window_tokens: number;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export declare function addTokenUsage(left: TokenUsageBreakdown | undefined, right: TokenUsageBreakdown): TokenUsageBreakdown | undefined;
|
|
16
|
+
export declare function toUsage(usage: Usage | null | undefined): NormalizedTokenUsage | undefined;
|
|
17
|
+
export declare function textFromContent(content: string | Array<{
|
|
18
|
+
type: string;
|
|
19
|
+
text?: string;
|
|
20
|
+
}>): string;
|
|
21
|
+
export declare function inputToPrompt(input: unknown, instructions: string | undefined, isNewSession: boolean, systemReminder?: string): string;
|
|
22
|
+
export declare function turnOptions(agentMode: string | undefined, model: string): {
|
|
23
|
+
collaborationMode: TurnStartParams["collaborationMode"];
|
|
24
|
+
};
|
|
25
|
+
export declare function itemText(item: ThreadItem): string | undefined;
|
|
26
|
+
export declare function commandExecutionItem(item: ThreadItem): CommandExecutionItem | undefined;
|
|
27
|
+
export declare function fileChangeItem(item: ThreadItem): FileChangeItem | undefined;
|
|
28
|
+
export declare function mcpToolCallItem(item: ThreadItem): McpToolCallItem | undefined;
|
|
29
|
+
export declare function dynamicToolCallItem(item: ThreadItem): DynamicToolCallItem | undefined;
|
|
30
|
+
export declare function webSearchItem(item: ThreadItem): WebSearchItem | undefined;
|
|
31
|
+
export declare function mcpToolCallOutput(item: McpToolCallItem): string;
|
|
32
|
+
export declare function displayCommand(command: string): string;
|
|
33
|
+
export declare function dynamicToolCallOutput(item: DynamicToolCallItem): string;
|
|
34
|
+
export declare function webSearchPayload(item: WebSearchItem): {
|
|
35
|
+
query: string;
|
|
36
|
+
action?: WebSearchItem["action"];
|
|
37
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { HTTPException } from "hono/http-exception";
|
|
2
|
+
export function addTokenUsage(left, right) {
|
|
3
|
+
const inputTokens = (left?.input_tokens ?? 0) + right.input_tokens;
|
|
4
|
+
const outputTokens = (left?.output_tokens ?? 0) + right.output_tokens;
|
|
5
|
+
const totalTokens = (left?.total_tokens ?? 0) + right.total_tokens;
|
|
6
|
+
if (![inputTokens, outputTokens, totalTokens].every(Number.isSafeInteger))
|
|
7
|
+
return undefined;
|
|
8
|
+
return { input_tokens: inputTokens, output_tokens: outputTokens, total_tokens: totalTokens };
|
|
9
|
+
}
|
|
10
|
+
export function toUsage(usage) {
|
|
11
|
+
const normalizeBreakdown = (breakdown) => {
|
|
12
|
+
const inputTokens = breakdown?.input_tokens;
|
|
13
|
+
const outputTokens = breakdown?.output_tokens;
|
|
14
|
+
const totalTokens = breakdown?.total_tokens;
|
|
15
|
+
if (typeof inputTokens !== "number" ||
|
|
16
|
+
typeof outputTokens !== "number" ||
|
|
17
|
+
typeof totalTokens !== "number" ||
|
|
18
|
+
!Number.isSafeInteger(inputTokens) ||
|
|
19
|
+
inputTokens < 0 ||
|
|
20
|
+
!Number.isSafeInteger(outputTokens) ||
|
|
21
|
+
outputTokens < 0 ||
|
|
22
|
+
!Number.isSafeInteger(totalTokens) ||
|
|
23
|
+
totalTokens < 0) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return { input_tokens: inputTokens, output_tokens: outputTokens, total_tokens: totalTokens };
|
|
27
|
+
};
|
|
28
|
+
const total = normalizeBreakdown(usage?.total);
|
|
29
|
+
const last = normalizeBreakdown(usage?.last);
|
|
30
|
+
const modelContextWindow = usage?.model_context_window;
|
|
31
|
+
if (!total || !last)
|
|
32
|
+
return undefined;
|
|
33
|
+
return {
|
|
34
|
+
total,
|
|
35
|
+
last,
|
|
36
|
+
...(typeof modelContextWindow === "number" && Number.isSafeInteger(modelContextWindow) && modelContextWindow > 0
|
|
37
|
+
? { context: { used_tokens: last.total_tokens, window_tokens: modelContextWindow } }
|
|
38
|
+
: {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function textFromContent(content) {
|
|
42
|
+
if (typeof content === "string")
|
|
43
|
+
return content;
|
|
44
|
+
return content
|
|
45
|
+
.filter((part) => part.type === "input_text")
|
|
46
|
+
.map((part) => part.text ?? "")
|
|
47
|
+
.join("\n");
|
|
48
|
+
}
|
|
49
|
+
export function inputToPrompt(input, instructions, isNewSession, systemReminder) {
|
|
50
|
+
const leadingTexts = (isNewSession ? [systemReminder, instructions] : [systemReminder]).filter((text) => Boolean(text));
|
|
51
|
+
const prefix = leadingTexts.length > 0 ? `${leadingTexts.join("\n\n")}\n\n` : "";
|
|
52
|
+
if (typeof input === "string")
|
|
53
|
+
return `${prefix}${input}`;
|
|
54
|
+
if (!Array.isArray(input)) {
|
|
55
|
+
throw new HTTPException(400, { message: "Invalid input" });
|
|
56
|
+
}
|
|
57
|
+
const lastUserMsg = [...input]
|
|
58
|
+
.reverse()
|
|
59
|
+
.find((message) => {
|
|
60
|
+
return Boolean(message && typeof message === "object" && "role" in message && message.role === "user");
|
|
61
|
+
});
|
|
62
|
+
if (!lastUserMsg) {
|
|
63
|
+
throw new HTTPException(400, { message: "No user message found" });
|
|
64
|
+
}
|
|
65
|
+
let systemText = instructions;
|
|
66
|
+
if (isNewSession && !systemText) {
|
|
67
|
+
const sysMsg = [...input]
|
|
68
|
+
.reverse()
|
|
69
|
+
.find((message) => {
|
|
70
|
+
return (Boolean(message && typeof message === "object" && "role" in message) &&
|
|
71
|
+
(message.role === "system" || message.role === "developer"));
|
|
72
|
+
});
|
|
73
|
+
if (sysMsg) {
|
|
74
|
+
systemText = textFromContent(sysMsg.content);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const arrayLeadingTexts = isNewSession
|
|
78
|
+
? [systemReminder, systemText].filter((text) => Boolean(text))
|
|
79
|
+
: [];
|
|
80
|
+
return `${arrayLeadingTexts.length > 0 ? `${arrayLeadingTexts.join("\n\n")}\n\n` : ""}${textFromContent(lastUserMsg.content)}`;
|
|
81
|
+
}
|
|
82
|
+
export function turnOptions(agentMode, model) {
|
|
83
|
+
return {
|
|
84
|
+
collaborationMode: {
|
|
85
|
+
mode: agentMode === "plan" ? "plan" : "default",
|
|
86
|
+
settings: { model: model || null, reasoning_effort: "medium", developer_instructions: null },
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function itemText(item) {
|
|
91
|
+
if (item.type === "agentMessage") {
|
|
92
|
+
return item.text;
|
|
93
|
+
}
|
|
94
|
+
if (item.type === "reasoning")
|
|
95
|
+
return item.summary.join("\n") || item.content.join("\n");
|
|
96
|
+
if (item.type === "plan")
|
|
97
|
+
return item.text;
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
export function commandExecutionItem(item) {
|
|
101
|
+
if (item.type !== "commandExecution") {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return item;
|
|
105
|
+
}
|
|
106
|
+
export function fileChangeItem(item) {
|
|
107
|
+
if (item.type !== "fileChange") {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
return item;
|
|
111
|
+
}
|
|
112
|
+
export function mcpToolCallItem(item) {
|
|
113
|
+
if (item.type !== "mcpToolCall") {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
return item;
|
|
117
|
+
}
|
|
118
|
+
export function dynamicToolCallItem(item) {
|
|
119
|
+
if (item.type !== "dynamicToolCall") {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
return item;
|
|
123
|
+
}
|
|
124
|
+
export function webSearchItem(item) {
|
|
125
|
+
if (item.type !== "webSearch") {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
return item;
|
|
129
|
+
}
|
|
130
|
+
function textFromMcpContent(content) {
|
|
131
|
+
if (!Array.isArray(content))
|
|
132
|
+
return undefined;
|
|
133
|
+
const text = content
|
|
134
|
+
.map((part) => {
|
|
135
|
+
if (part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part) {
|
|
136
|
+
return typeof part.text === "string" ? part.text : "";
|
|
137
|
+
}
|
|
138
|
+
return "";
|
|
139
|
+
})
|
|
140
|
+
.filter((part) => part.length > 0)
|
|
141
|
+
.join("\n");
|
|
142
|
+
return text.length > 0 ? text : undefined;
|
|
143
|
+
}
|
|
144
|
+
export function mcpToolCallOutput(item) {
|
|
145
|
+
const structuredContent = item.result?.structuredContent;
|
|
146
|
+
if (structuredContent &&
|
|
147
|
+
typeof structuredContent === "object" &&
|
|
148
|
+
"filePath" in structuredContent &&
|
|
149
|
+
typeof structuredContent.filePath === "string") {
|
|
150
|
+
return structuredContent.filePath;
|
|
151
|
+
}
|
|
152
|
+
return textFromMcpContent(item.result?.content) ?? JSON.stringify(item.result ?? null);
|
|
153
|
+
}
|
|
154
|
+
function parseShellWord(input) {
|
|
155
|
+
let index = 0;
|
|
156
|
+
let value = "";
|
|
157
|
+
while (index < input.length) {
|
|
158
|
+
const char = input[index];
|
|
159
|
+
if (/\s/.test(char))
|
|
160
|
+
break;
|
|
161
|
+
if (char === "'") {
|
|
162
|
+
index += 1;
|
|
163
|
+
while (index < input.length) {
|
|
164
|
+
if (input[index] === "'") {
|
|
165
|
+
index += 1;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
value += input[index];
|
|
169
|
+
index += 1;
|
|
170
|
+
}
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (char === '"') {
|
|
174
|
+
index += 1;
|
|
175
|
+
while (index < input.length) {
|
|
176
|
+
const innerChar = input[index];
|
|
177
|
+
if (innerChar === '"') {
|
|
178
|
+
index += 1;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
if (innerChar === "\\" && index + 1 < input.length) {
|
|
182
|
+
value += input[index + 1];
|
|
183
|
+
index += 2;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
value += innerChar;
|
|
187
|
+
index += 1;
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (char === "\\" && index + 1 < input.length) {
|
|
192
|
+
value += input[index + 1];
|
|
193
|
+
index += 2;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
value += char;
|
|
197
|
+
index += 1;
|
|
198
|
+
}
|
|
199
|
+
return input.slice(index).trim() === "" ? value : undefined;
|
|
200
|
+
}
|
|
201
|
+
export function displayCommand(command) {
|
|
202
|
+
const match = command.match(/^\/bin\/bash\s+-lc\s+([\s\S]+)$/);
|
|
203
|
+
if (!match)
|
|
204
|
+
return command;
|
|
205
|
+
return parseShellWord(match[1].trim()) ?? command;
|
|
206
|
+
}
|
|
207
|
+
export function dynamicToolCallOutput(item) {
|
|
208
|
+
const texts = (item.contentItems ?? [])
|
|
209
|
+
.map((content) => (content.type === "inputText" ? content.text : content.imageUrl))
|
|
210
|
+
.filter((text) => text.length > 0);
|
|
211
|
+
return texts.join("\n");
|
|
212
|
+
}
|
|
213
|
+
export function webSearchPayload(item) {
|
|
214
|
+
return {
|
|
215
|
+
query: item.query,
|
|
216
|
+
...(item.action ? { action: item.action } : null),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const ChatTextPartSchema: z.ZodObject<{
|
|
3
|
+
type: z.ZodLiteral<"text">;
|
|
4
|
+
text: z.ZodString;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export declare const ChatFilePartSchema: z.ZodObject<{
|
|
7
|
+
type: z.ZodLiteral<"file">;
|
|
8
|
+
mediaType: z.ZodString;
|
|
9
|
+
filename: z.ZodOptional<z.ZodString>;
|
|
10
|
+
url: z.ZodString;
|
|
11
|
+
}, z.core.$strip>;
|
|
12
|
+
export declare const ChatStepStartPartSchema: z.ZodObject<{
|
|
13
|
+
type: z.ZodLiteral<"step-start">;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
export declare const ChatToolPartSchema: z.ZodObject<{
|
|
16
|
+
type: z.ZodLiteral<"tool">;
|
|
17
|
+
toolCallId: z.ZodString;
|
|
18
|
+
toolName: z.ZodString;
|
|
19
|
+
title: z.ZodOptional<z.ZodString>;
|
|
20
|
+
state: z.ZodString;
|
|
21
|
+
input: z.ZodOptional<z.ZodUnknown>;
|
|
22
|
+
output: z.ZodOptional<z.ZodUnknown>;
|
|
23
|
+
approval: z.ZodOptional<z.ZodObject<{
|
|
24
|
+
id: z.ZodString;
|
|
25
|
+
approved: z.ZodOptional<z.ZodBoolean>;
|
|
26
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
27
|
+
}, z.core.$strip>>;
|
|
28
|
+
}, z.core.$strip>;
|
|
29
|
+
export declare const ChatCustomPartSchema: z.ZodObject<{
|
|
30
|
+
type: z.ZodString;
|
|
31
|
+
}, z.core.$loose>;
|
|
32
|
+
export declare const CodexChatForkSchema: z.ZodObject<{
|
|
33
|
+
threadId: z.ZodString;
|
|
34
|
+
turnId: z.ZodString;
|
|
35
|
+
}, z.core.$strict>;
|
|
36
|
+
export declare const ClaudeChatForkSchema: z.ZodObject<{
|
|
37
|
+
sessionId: z.ZodString;
|
|
38
|
+
messageId: z.ZodString;
|
|
39
|
+
}, z.core.$strict>;
|
|
40
|
+
export declare const ChatForkSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
41
|
+
threadId: z.ZodString;
|
|
42
|
+
turnId: z.ZodString;
|
|
43
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
44
|
+
sessionId: z.ZodString;
|
|
45
|
+
messageId: z.ZodString;
|
|
46
|
+
}, z.core.$strict>]>;
|
|
47
|
+
export declare const ChatBodySchema: z.ZodObject<{
|
|
48
|
+
conversationId: z.ZodOptional<z.ZodString>;
|
|
49
|
+
fork: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
50
|
+
threadId: z.ZodString;
|
|
51
|
+
turnId: z.ZodString;
|
|
52
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
53
|
+
sessionId: z.ZodString;
|
|
54
|
+
messageId: z.ZodString;
|
|
55
|
+
}, z.core.$strict>]>>;
|
|
56
|
+
model: z.ZodString;
|
|
57
|
+
message: z.ZodObject<{
|
|
58
|
+
id: z.ZodOptional<z.ZodString>;
|
|
59
|
+
role: z.ZodOptional<z.ZodEnum<{
|
|
60
|
+
user: "user";
|
|
61
|
+
assistant: "assistant";
|
|
62
|
+
}>>;
|
|
63
|
+
parts: z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
64
|
+
type: z.ZodLiteral<"text">;
|
|
65
|
+
text: z.ZodString;
|
|
66
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
67
|
+
type: z.ZodLiteral<"file">;
|
|
68
|
+
mediaType: z.ZodString;
|
|
69
|
+
filename: z.ZodOptional<z.ZodString>;
|
|
70
|
+
url: z.ZodString;
|
|
71
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
72
|
+
type: z.ZodLiteral<"step-start">;
|
|
73
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
74
|
+
type: z.ZodLiteral<"tool">;
|
|
75
|
+
toolCallId: z.ZodString;
|
|
76
|
+
toolName: z.ZodString;
|
|
77
|
+
title: z.ZodOptional<z.ZodString>;
|
|
78
|
+
state: z.ZodString;
|
|
79
|
+
input: z.ZodOptional<z.ZodUnknown>;
|
|
80
|
+
output: z.ZodOptional<z.ZodUnknown>;
|
|
81
|
+
approval: z.ZodOptional<z.ZodObject<{
|
|
82
|
+
id: z.ZodString;
|
|
83
|
+
approved: z.ZodOptional<z.ZodBoolean>;
|
|
84
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
85
|
+
}, z.core.$strip>>;
|
|
86
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
87
|
+
type: z.ZodString;
|
|
88
|
+
}, z.core.$loose>]>>;
|
|
89
|
+
}, z.core.$strip>;
|
|
90
|
+
}, z.core.$strip>;
|
|
91
|
+
export type ChatBody = z.infer<typeof ChatBodySchema>;
|
|
92
|
+
export type ChatMessagePart = ChatBody["message"]["parts"][number];
|
|
93
|
+
export declare const ToolRequestUserInputAnswerSchema: z.ZodObject<{
|
|
94
|
+
answers: z.ZodArray<z.ZodString>;
|
|
95
|
+
}, z.core.$strip>;
|
|
96
|
+
export declare const ToolRequestUserInputResponseSchema: z.ZodObject<{
|
|
97
|
+
answers: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
98
|
+
answers: z.ZodArray<z.ZodString>;
|
|
99
|
+
}, z.core.$strip>>;
|
|
100
|
+
}, z.core.$strip>;
|
|
101
|
+
export declare const RequestUserInputToolPartSchema: z.ZodObject<{
|
|
102
|
+
type: z.ZodLiteral<"tool">;
|
|
103
|
+
toolCallId: z.ZodString;
|
|
104
|
+
toolName: z.ZodLiteral<"request_user_input">;
|
|
105
|
+
state: z.ZodLiteral<"output-available">;
|
|
106
|
+
sourceTurnId: z.ZodOptional<z.ZodString>;
|
|
107
|
+
output: z.ZodObject<{
|
|
108
|
+
answers: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
109
|
+
answers: z.ZodArray<z.ZodString>;
|
|
110
|
+
}, z.core.$strip>>;
|
|
111
|
+
}, z.core.$strip>;
|
|
112
|
+
}, z.core.$strip>;
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const ChatTextPartSchema = z.object({
|
|
3
|
+
type: z.literal("text"),
|
|
4
|
+
text: z.string(),
|
|
5
|
+
});
|
|
6
|
+
export const ChatFilePartSchema = z.object({
|
|
7
|
+
type: z.literal("file"),
|
|
8
|
+
mediaType: z.string(),
|
|
9
|
+
filename: z.string().optional(),
|
|
10
|
+
url: z.string(),
|
|
11
|
+
});
|
|
12
|
+
export const ChatStepStartPartSchema = z.object({
|
|
13
|
+
type: z.literal("step-start"),
|
|
14
|
+
});
|
|
15
|
+
export const ChatToolPartSchema = z.object({
|
|
16
|
+
type: z.literal("tool"),
|
|
17
|
+
toolCallId: z.string(),
|
|
18
|
+
toolName: z.string(),
|
|
19
|
+
title: z.string().optional(),
|
|
20
|
+
state: z.string(),
|
|
21
|
+
input: z.unknown().optional(),
|
|
22
|
+
output: z.unknown().optional(),
|
|
23
|
+
approval: z
|
|
24
|
+
.object({
|
|
25
|
+
id: z.string(),
|
|
26
|
+
approved: z.boolean().optional(),
|
|
27
|
+
reason: z.string().optional(),
|
|
28
|
+
})
|
|
29
|
+
.optional(),
|
|
30
|
+
});
|
|
31
|
+
export const ChatCustomPartSchema = z.looseObject({
|
|
32
|
+
type: z.string(),
|
|
33
|
+
});
|
|
34
|
+
export const CodexChatForkSchema = z.strictObject({
|
|
35
|
+
threadId: z.string().min(1),
|
|
36
|
+
turnId: z.string().min(1),
|
|
37
|
+
});
|
|
38
|
+
export const ClaudeChatForkSchema = z.strictObject({
|
|
39
|
+
sessionId: z.string().min(1),
|
|
40
|
+
messageId: z.string().min(1),
|
|
41
|
+
});
|
|
42
|
+
export const ChatForkSchema = z.union([CodexChatForkSchema, ClaudeChatForkSchema]);
|
|
43
|
+
export const ChatBodySchema = z.object({
|
|
44
|
+
conversationId: z.string().optional(),
|
|
45
|
+
fork: ChatForkSchema.optional(),
|
|
46
|
+
model: z.string(),
|
|
47
|
+
message: z.object({
|
|
48
|
+
id: z.string().optional(),
|
|
49
|
+
role: z.enum(["user", "assistant"]).optional(),
|
|
50
|
+
parts: z.array(z.union([
|
|
51
|
+
ChatTextPartSchema,
|
|
52
|
+
ChatFilePartSchema,
|
|
53
|
+
ChatStepStartPartSchema,
|
|
54
|
+
ChatToolPartSchema,
|
|
55
|
+
ChatCustomPartSchema,
|
|
56
|
+
])),
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
export const ToolRequestUserInputAnswerSchema = z.object({
|
|
60
|
+
answers: z.array(z.string()),
|
|
61
|
+
});
|
|
62
|
+
export const ToolRequestUserInputResponseSchema = z.object({
|
|
63
|
+
answers: z.record(z.string(), ToolRequestUserInputAnswerSchema),
|
|
64
|
+
});
|
|
65
|
+
export const RequestUserInputToolPartSchema = z.object({
|
|
66
|
+
type: z.literal("tool"),
|
|
67
|
+
toolCallId: z.string(),
|
|
68
|
+
toolName: z.literal("request_user_input"),
|
|
69
|
+
state: z.literal("output-available"),
|
|
70
|
+
sourceTurnId: z.string().min(1).optional(),
|
|
71
|
+
output: ToolRequestUserInputResponseSchema,
|
|
72
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type AppConfig } from "./config.js";
|
|
2
|
+
export type TunnelClientConfig = AppConfig["tunnel"];
|
|
3
|
+
export declare class CodexTunnelClient {
|
|
4
|
+
private readonly config;
|
|
5
|
+
private stopped;
|
|
6
|
+
private socket;
|
|
7
|
+
private running;
|
|
8
|
+
constructor(config: TunnelClientConfig);
|
|
9
|
+
start(): Promise<void>;
|
|
10
|
+
stop(): void;
|
|
11
|
+
private connectOnce;
|
|
12
|
+
private send;
|
|
13
|
+
private handleRequest;
|
|
14
|
+
}
|
|
15
|
+
export declare function startCodexTunnelClient(config?: TunnelClientConfig): CodexTunnelClient | undefined;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import WebSocket from "ws";
|
|
3
|
+
import { ChatBodySchema } from "./schemas.js";
|
|
4
|
+
import { appConfig } from "./config.js";
|
|
5
|
+
import { cancelCodexConversation, ChatRequestError, handleCodexChatStream } from "./routes/chat.js";
|
|
6
|
+
import { logger } from "@messenger-agent/shared/logger";
|
|
7
|
+
import { sendHttpTunnelResponse } from "@messenger-agent/shared/tunnel-http";
|
|
8
|
+
import { CODING_AGENT_TUNNEL_DUPLICATE_CLOSE_CODE, CODING_AGENT_TUNNEL_PROTOCOL_VERSION, } from "@messenger-agent/shared/tunnel-protocol";
|
|
9
|
+
const clientVersion = createRequire(import.meta.url)("../package.json").version;
|
|
10
|
+
function delay(ms) {
|
|
11
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
function parseServerMessage(raw) {
|
|
14
|
+
try {
|
|
15
|
+
const value = JSON.parse(raw.toString());
|
|
16
|
+
if (!value || typeof value !== "object" || !("type" in value))
|
|
17
|
+
return undefined;
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function headersFromRecord(headers) {
|
|
25
|
+
const result = new Headers();
|
|
26
|
+
for (const [key, value] of Object.entries(headers ?? {})) {
|
|
27
|
+
result.set(key, value);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
function cancelConversationId(message) {
|
|
32
|
+
if (message.method !== "POST")
|
|
33
|
+
return undefined;
|
|
34
|
+
try {
|
|
35
|
+
const pathname = new URL(message.path, "http://coding-agent-tunnel.local").pathname;
|
|
36
|
+
const match = /^\/v1\/chat\/([^/]+)\/cancel$/.exec(pathname);
|
|
37
|
+
return match ? decodeURIComponent(match[1]) : undefined;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export class CodexTunnelClient {
|
|
44
|
+
config;
|
|
45
|
+
stopped = false;
|
|
46
|
+
socket;
|
|
47
|
+
running = new Map();
|
|
48
|
+
constructor(config) {
|
|
49
|
+
this.config = config;
|
|
50
|
+
}
|
|
51
|
+
async start() {
|
|
52
|
+
if (!this.config.enabled)
|
|
53
|
+
return;
|
|
54
|
+
if (!this.config.serverUrl || !this.config.tunnelId || !this.config.token) {
|
|
55
|
+
throw new Error("Tunnel config requires server_url, tunnel_id, and token when enabled");
|
|
56
|
+
}
|
|
57
|
+
let backoffMs = this.config.reconnectInitialMs;
|
|
58
|
+
while (!this.stopped) {
|
|
59
|
+
try {
|
|
60
|
+
await this.connectOnce();
|
|
61
|
+
backoffMs = this.config.reconnectInitialMs;
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (this.stopped)
|
|
65
|
+
break;
|
|
66
|
+
logger.warn("[TunnelClient] websocket tunnel connection failed:", err);
|
|
67
|
+
}
|
|
68
|
+
if (!this.stopped) {
|
|
69
|
+
await delay(backoffMs);
|
|
70
|
+
backoffMs = Math.min(backoffMs * 2, this.config.reconnectMaxMs);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
stop() {
|
|
75
|
+
this.stopped = true;
|
|
76
|
+
for (const request of this.running.values()) {
|
|
77
|
+
request.abortController.abort();
|
|
78
|
+
}
|
|
79
|
+
this.running.clear();
|
|
80
|
+
this.socket?.close();
|
|
81
|
+
}
|
|
82
|
+
connectOnce() {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const headers = { Authorization: `Bearer ${this.config.token}` };
|
|
85
|
+
const ws = new WebSocket(this.config.serverUrl, { headers });
|
|
86
|
+
this.socket = ws;
|
|
87
|
+
let heartbeatTimer;
|
|
88
|
+
let settled = false;
|
|
89
|
+
const cleanup = () => {
|
|
90
|
+
if (heartbeatTimer)
|
|
91
|
+
clearInterval(heartbeatTimer);
|
|
92
|
+
if (this.socket === ws)
|
|
93
|
+
this.socket = undefined;
|
|
94
|
+
for (const request of this.running.values()) {
|
|
95
|
+
request.abortController.abort(new Error("Tunnel connection closed"));
|
|
96
|
+
}
|
|
97
|
+
this.running.clear();
|
|
98
|
+
};
|
|
99
|
+
ws.on("open", () => {
|
|
100
|
+
this.send({
|
|
101
|
+
type: "hello",
|
|
102
|
+
protocolVersion: CODING_AGENT_TUNNEL_PROTOCOL_VERSION,
|
|
103
|
+
tunnelId: this.config.tunnelId,
|
|
104
|
+
agentType: "codex",
|
|
105
|
+
clientVersion,
|
|
106
|
+
});
|
|
107
|
+
heartbeatTimer = setInterval(() => {
|
|
108
|
+
this.send({ type: "heartbeat" });
|
|
109
|
+
}, this.config.heartbeatIntervalMs);
|
|
110
|
+
logger.info(`[TunnelClient] connected to ${this.config.serverUrl} as ${this.config.tunnelId}`);
|
|
111
|
+
});
|
|
112
|
+
ws.on("message", (raw) => {
|
|
113
|
+
const message = parseServerMessage(raw);
|
|
114
|
+
if (!message) {
|
|
115
|
+
logger.warn("[TunnelClient] received invalid websocket message");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (message.type === "heartbeat")
|
|
119
|
+
return;
|
|
120
|
+
if (message.type === "cancel") {
|
|
121
|
+
this.running.get(message.id)?.abortController.abort();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (message.type === "request") {
|
|
125
|
+
this.handleRequest(message).catch((err) => {
|
|
126
|
+
logger.error(`[TunnelClient] request ${message.id} failed:`, err);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
ws.once("error", (err) => {
|
|
131
|
+
if (!settled) {
|
|
132
|
+
settled = true;
|
|
133
|
+
cleanup();
|
|
134
|
+
reject(err);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
ws.once("close", (code, reason) => {
|
|
138
|
+
cleanup();
|
|
139
|
+
if (code === CODING_AGENT_TUNNEL_DUPLICATE_CLOSE_CODE) {
|
|
140
|
+
this.stopped = true;
|
|
141
|
+
logger.warn(`[TunnelClient] duplicate websocket tunnel connection rejected; reconnect disabled: ${reason.toString()}`);
|
|
142
|
+
}
|
|
143
|
+
if (!settled) {
|
|
144
|
+
settled = true;
|
|
145
|
+
resolve();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
send(message) {
|
|
151
|
+
const socket = this.socket;
|
|
152
|
+
if (!socket || socket.readyState !== WebSocket.OPEN)
|
|
153
|
+
return;
|
|
154
|
+
socket.send(JSON.stringify(message));
|
|
155
|
+
}
|
|
156
|
+
async handleRequest(message) {
|
|
157
|
+
const conversationId = cancelConversationId(message);
|
|
158
|
+
if (conversationId) {
|
|
159
|
+
let cancelled;
|
|
160
|
+
try {
|
|
161
|
+
cancelled = await cancelCodexConversation(conversationId);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
const errorText = err instanceof Error ? err.message : "Failed to interrupt Codex conversation";
|
|
165
|
+
const response = Response.json({ error: { type: "server_error", code: "cancel_failed", message: errorText } }, { status: 500 });
|
|
166
|
+
await sendHttpTunnelResponse(message.id, response, (responseMessage) => this.send(responseMessage));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const response = Response.json(cancelled
|
|
170
|
+
? { success: true }
|
|
171
|
+
: {
|
|
172
|
+
error: {
|
|
173
|
+
type: "not_found_error",
|
|
174
|
+
code: "conversation_not_found",
|
|
175
|
+
message: `Conversation not found or already completed: ${conversationId}`,
|
|
176
|
+
param: "conversation_id",
|
|
177
|
+
},
|
|
178
|
+
}, { status: cancelled ? 200 : 404 });
|
|
179
|
+
await sendHttpTunnelResponse(message.id, response, (responseMessage) => this.send(responseMessage));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (message.method !== "POST" || message.path !== "/v1/chat/stream") {
|
|
183
|
+
this.send({
|
|
184
|
+
type: "response.error",
|
|
185
|
+
id: message.id,
|
|
186
|
+
code: "unsupported_request",
|
|
187
|
+
errorText: `Unsupported tunnel request: ${message.method} ${message.path}`,
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const body = ChatBodySchema.safeParse(message.body);
|
|
192
|
+
if (!body.success) {
|
|
193
|
+
this.send({
|
|
194
|
+
type: "response.error",
|
|
195
|
+
id: message.id,
|
|
196
|
+
code: "invalid_request",
|
|
197
|
+
errorText: `Invalid chat request body: ${body.error.message}`,
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const abortController = new AbortController();
|
|
202
|
+
this.running.set(message.id, { abortController });
|
|
203
|
+
this.send({ type: "response.start", id: message.id, mode: "sse" });
|
|
204
|
+
try {
|
|
205
|
+
await handleCodexChatStream({
|
|
206
|
+
body: body.data,
|
|
207
|
+
headers: headersFromRecord(message.headers),
|
|
208
|
+
signal: abortController.signal,
|
|
209
|
+
}, {
|
|
210
|
+
writeData: async (data) => this.send({ type: "response.chunk", id: message.id, data }),
|
|
211
|
+
writeDone: async () => this.send({ type: "response.end", id: message.id }),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
const errorText = err instanceof Error ? err.message : "Tunnel request failed";
|
|
216
|
+
const code = err instanceof ChatRequestError ? `http_${err.status}` : "request_failed";
|
|
217
|
+
this.send({ type: "response.error", id: message.id, code, errorText });
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
this.running.delete(message.id);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
export function startCodexTunnelClient(config = appConfig.tunnel) {
|
|
225
|
+
if (!config.enabled)
|
|
226
|
+
return undefined;
|
|
227
|
+
const client = new CodexTunnelClient(config);
|
|
228
|
+
client.start().catch((err) => {
|
|
229
|
+
logger.error("[TunnelClient] stopped unexpectedly:", err);
|
|
230
|
+
});
|
|
231
|
+
return client;
|
|
232
|
+
}
|