agent-sin 0.1.0

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.
Files changed (150) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +81 -0
  4. package/assets/logo.png +0 -0
  5. package/builtin-skills/_shared/_models_lib.py +227 -0
  6. package/builtin-skills/_shared/_profile_lib.py +98 -0
  7. package/builtin-skills/_shared/_schedules_lib.py +313 -0
  8. package/builtin-skills/_shared/_skill_settings_lib.py +153 -0
  9. package/builtin-skills/_shared/i18n.py +26 -0
  10. package/builtin-skills/memo-delete/main.py +155 -0
  11. package/builtin-skills/memo-delete/skill.yaml +57 -0
  12. package/builtin-skills/memo-index/main.py +178 -0
  13. package/builtin-skills/memo-index/skill.yaml +53 -0
  14. package/builtin-skills/memo-save/README.md +5 -0
  15. package/builtin-skills/memo-save/main.py +74 -0
  16. package/builtin-skills/memo-save/skill.yaml +52 -0
  17. package/builtin-skills/memo-search/README.md +10 -0
  18. package/builtin-skills/memo-search/main.py +97 -0
  19. package/builtin-skills/memo-search/skill.yaml +51 -0
  20. package/builtin-skills/memo-vector-search/main.py +121 -0
  21. package/builtin-skills/memo-vector-search/skill.yaml +53 -0
  22. package/builtin-skills/model-add/main.py +180 -0
  23. package/builtin-skills/model-add/skill.yaml +112 -0
  24. package/builtin-skills/model-list/main.py +93 -0
  25. package/builtin-skills/model-list/skill.yaml +48 -0
  26. package/builtin-skills/model-set/main.py +123 -0
  27. package/builtin-skills/model-set/skill.yaml +69 -0
  28. package/builtin-skills/profile-delete/_profile_lib.py +98 -0
  29. package/builtin-skills/profile-delete/main.py +98 -0
  30. package/builtin-skills/profile-delete/skill.yaml +64 -0
  31. package/builtin-skills/profile-edit/_profile_lib.py +98 -0
  32. package/builtin-skills/profile-edit/main.py +97 -0
  33. package/builtin-skills/profile-edit/skill.yaml +72 -0
  34. package/builtin-skills/profile-save/main.py +52 -0
  35. package/builtin-skills/profile-save/skill.yaml +69 -0
  36. package/builtin-skills/schedule-add/_schedules_lib.py +303 -0
  37. package/builtin-skills/schedule-add/main.py +137 -0
  38. package/builtin-skills/schedule-add/skill.yaml +94 -0
  39. package/builtin-skills/schedule-list/_schedules_lib.py +303 -0
  40. package/builtin-skills/schedule-list/main.py +86 -0
  41. package/builtin-skills/schedule-list/skill.yaml +45 -0
  42. package/builtin-skills/schedule-remove/_schedules_lib.py +303 -0
  43. package/builtin-skills/schedule-remove/main.py +69 -0
  44. package/builtin-skills/schedule-remove/skill.yaml +49 -0
  45. package/builtin-skills/schedule-toggle/_schedules_lib.py +303 -0
  46. package/builtin-skills/schedule-toggle/main.py +78 -0
  47. package/builtin-skills/schedule-toggle/skill.yaml +61 -0
  48. package/builtin-skills/skills-disable/main.py +63 -0
  49. package/builtin-skills/skills-disable/skill.yaml +52 -0
  50. package/builtin-skills/skills-enable/main.py +62 -0
  51. package/builtin-skills/skills-enable/skill.yaml +51 -0
  52. package/builtin-skills/todo-add/main.py +68 -0
  53. package/builtin-skills/todo-add/skill.yaml +53 -0
  54. package/builtin-skills/todo-delete/main.py +65 -0
  55. package/builtin-skills/todo-delete/skill.yaml +47 -0
  56. package/builtin-skills/todo-done/main.py +75 -0
  57. package/builtin-skills/todo-done/skill.yaml +47 -0
  58. package/builtin-skills/todo-list/main.py +91 -0
  59. package/builtin-skills/todo-list/skill.yaml +48 -0
  60. package/builtin-skills/todo-tick/main.py +125 -0
  61. package/builtin-skills/todo-tick/skill.yaml +48 -0
  62. package/dist/builder/build-action-classifier.d.ts +18 -0
  63. package/dist/builder/build-action-classifier.js +142 -0
  64. package/dist/builder/build-commands.d.ts +19 -0
  65. package/dist/builder/build-commands.js +133 -0
  66. package/dist/builder/build-flow.d.ts +72 -0
  67. package/dist/builder/build-flow.js +416 -0
  68. package/dist/builder/builder-session.d.ts +117 -0
  69. package/dist/builder/builder-session.js +1129 -0
  70. package/dist/builder/conversation-router.d.ts +22 -0
  71. package/dist/builder/conversation-router.js +69 -0
  72. package/dist/builder/intent-runtime-store.d.ts +7 -0
  73. package/dist/builder/intent-runtime-store.js +60 -0
  74. package/dist/builder/progress-format.d.ts +7 -0
  75. package/dist/builder/progress-format.js +46 -0
  76. package/dist/cli/index.d.ts +2 -0
  77. package/dist/cli/index.js +2835 -0
  78. package/dist/cli/spinner.d.ts +30 -0
  79. package/dist/cli/spinner.js +164 -0
  80. package/dist/core/ai-provider.d.ts +75 -0
  81. package/dist/core/ai-provider.js +678 -0
  82. package/dist/core/builtin-skills.d.ts +27 -0
  83. package/dist/core/builtin-skills.js +120 -0
  84. package/dist/core/chat-engine.d.ts +70 -0
  85. package/dist/core/chat-engine.js +812 -0
  86. package/dist/core/config.d.ts +127 -0
  87. package/dist/core/config.js +1379 -0
  88. package/dist/core/daily-memory-promotion.d.ts +21 -0
  89. package/dist/core/daily-memory-promotion.js +422 -0
  90. package/dist/core/i18n.d.ts +23 -0
  91. package/dist/core/i18n.js +167 -0
  92. package/dist/core/info-lines.d.ts +5 -0
  93. package/dist/core/info-lines.js +39 -0
  94. package/dist/core/input-schema.d.ts +2 -0
  95. package/dist/core/input-schema.js +156 -0
  96. package/dist/core/intent-router.d.ts +27 -0
  97. package/dist/core/intent-router.js +160 -0
  98. package/dist/core/logger.d.ts +60 -0
  99. package/dist/core/logger.js +240 -0
  100. package/dist/core/memory.d.ts +10 -0
  101. package/dist/core/memory.js +72 -0
  102. package/dist/core/message-utils.d.ts +13 -0
  103. package/dist/core/message-utils.js +104 -0
  104. package/dist/core/notifier.d.ts +17 -0
  105. package/dist/core/notifier.js +424 -0
  106. package/dist/core/output-writer.d.ts +13 -0
  107. package/dist/core/output-writer.js +100 -0
  108. package/dist/core/plan-decision.d.ts +16 -0
  109. package/dist/core/plan-decision.js +88 -0
  110. package/dist/core/profile-memory.d.ts +17 -0
  111. package/dist/core/profile-memory.js +142 -0
  112. package/dist/core/runtime.d.ts +50 -0
  113. package/dist/core/runtime.js +187 -0
  114. package/dist/core/scheduler.d.ts +28 -0
  115. package/dist/core/scheduler.js +155 -0
  116. package/dist/core/secrets.d.ts +31 -0
  117. package/dist/core/secrets.js +214 -0
  118. package/dist/core/service.d.ts +35 -0
  119. package/dist/core/service.js +479 -0
  120. package/dist/core/skill-planner.d.ts +24 -0
  121. package/dist/core/skill-planner.js +100 -0
  122. package/dist/core/skill-registry.d.ts +98 -0
  123. package/dist/core/skill-registry.js +319 -0
  124. package/dist/core/skill-scaffold.d.ts +33 -0
  125. package/dist/core/skill-scaffold.js +256 -0
  126. package/dist/core/skill-settings.d.ts +11 -0
  127. package/dist/core/skill-settings.js +63 -0
  128. package/dist/core/transfer.d.ts +31 -0
  129. package/dist/core/transfer.js +270 -0
  130. package/dist/core/update-notifier.d.ts +2 -0
  131. package/dist/core/update-notifier.js +140 -0
  132. package/dist/discord/bot.d.ts +96 -0
  133. package/dist/discord/bot.js +2424 -0
  134. package/dist/runtimes/codex-app-server.d.ts +53 -0
  135. package/dist/runtimes/codex-app-server.js +305 -0
  136. package/dist/runtimes/python-runner.d.ts +7 -0
  137. package/dist/runtimes/python-runner.js +302 -0
  138. package/dist/runtimes/typescript-runner.d.ts +5 -0
  139. package/dist/runtimes/typescript-runner.js +172 -0
  140. package/dist/skills-sdk/types.d.ts +38 -0
  141. package/dist/skills-sdk/types.js +1 -0
  142. package/dist/telegram/bot.d.ts +94 -0
  143. package/dist/telegram/bot.js +1219 -0
  144. package/install.ps1 +132 -0
  145. package/install.sh +130 -0
  146. package/package.json +60 -0
  147. package/templates/skill-python/main.py +74 -0
  148. package/templates/skill-python/skill.yaml +48 -0
  149. package/templates/skill-typescript/main.ts +87 -0
  150. package/templates/skill-typescript/skill.yaml +42 -0
@@ -0,0 +1,678 @@
1
+ import { spawn } from "node:child_process";
2
+ import { loadModels } from "./config.js";
3
+ import { getApiKeys } from "./secrets.js";
4
+ import { getSharedCodexAppServer } from "../runtimes/codex-app-server.js";
5
+ import { l } from "./i18n.js";
6
+ export class AiProviderError extends Error {
7
+ model_id;
8
+ provider;
9
+ constructor(message, model_id, provider) {
10
+ super(message);
11
+ this.model_id = model_id;
12
+ this.provider = provider;
13
+ }
14
+ }
15
+ let providerOverride = null;
16
+ export function setAiProviderOverride(fn) {
17
+ providerOverride = fn;
18
+ }
19
+ export function getAiProvider() {
20
+ return providerOverride || runChatCompletion;
21
+ }
22
+ // `ai_steps[].model` in skill.yaml accepts both concrete model ids (e.g.
23
+ // codex-low) and logical role names (chat / builder). Role names are resolved
24
+ // to a concrete model id via `roles` in models.yaml.
25
+ export function resolveModelId(modelId, models) {
26
+ if (models.models[modelId]) {
27
+ return modelId;
28
+ }
29
+ const roles = models.roles;
30
+ const mapped = roles?.[modelId];
31
+ return mapped && models.models[mapped] ? mapped : modelId;
32
+ }
33
+ export async function runChatCompletion(config, request) {
34
+ if (process.env.AGENT_SIN_FAKE_PROVIDER === "1") {
35
+ return fakeProvider(request);
36
+ }
37
+ const models = await loadModels(config.workspace);
38
+ const resolvedModelId = resolveModelId(request.model_id, models);
39
+ if (resolvedModelId !== request.model_id) {
40
+ request = { ...request, model_id: resolvedModelId };
41
+ }
42
+ const entry = models.models[resolvedModelId];
43
+ if (!entry) {
44
+ throw new AiProviderError(l(`Unknown model: ${resolvedModelId}`, `不明なモデルです: ${resolvedModelId}`), resolvedModelId, "unknown");
45
+ }
46
+ if (entry.enabled === false) {
47
+ throw new AiProviderError(l(`Model is disabled. Run: agent-sin model set ${resolvedModelId}`, `モデルが無効です。実行: agent-sin model set ${resolvedModelId}`), resolvedModelId, entry.provider || entry.type);
48
+ }
49
+ if (entry.type === "ollama") {
50
+ return dispatchOllama(request, entry);
51
+ }
52
+ if (entry.type === "api") {
53
+ const provider = entry.provider || "openai";
54
+ if (provider === "openai") {
55
+ return dispatchOpenAI(request, entry);
56
+ }
57
+ if (provider === "gemini" || provider === "google") {
58
+ return dispatchGemini(request, entry);
59
+ }
60
+ if (provider === "anthropic" || provider === "claude") {
61
+ return dispatchAnthropic(request, entry);
62
+ }
63
+ throw new AiProviderError(l(`Unsupported api provider: ${provider}`, `未対応の API プロバイダです: ${provider}`), request.model_id, provider);
64
+ }
65
+ // "cli" is the current name; "login" is accepted for backward compatibility.
66
+ if (entry.type === "cli" || entry.type === "login") {
67
+ if (entry.provider === "codex") {
68
+ return dispatchCodex(request, entry);
69
+ }
70
+ if (entry.provider === "claude-code") {
71
+ return dispatchClaudeCode(request, entry);
72
+ }
73
+ throw new AiProviderError(l(`Unsupported cli provider: ${entry.provider}`, `未対応の CLI プロバイダです: ${entry.provider}`), request.model_id, String(entry.provider));
74
+ }
75
+ throw new AiProviderError(l(`Unsupported model type: ${entry.type}`, `未対応のモデル種別です: ${entry.type}`), request.model_id, entry.type);
76
+ }
77
+ let fakeCallIndex = 0;
78
+ let fakeScriptedValue;
79
+ function fakeProvider(request) {
80
+ const scripted = process.env.AGENT_SIN_FAKE_TEXTS;
81
+ if (scripted) {
82
+ if (scripted !== fakeScriptedValue) {
83
+ fakeScriptedValue = scripted;
84
+ fakeCallIndex = 0;
85
+ }
86
+ const parts = scripted.split("|||");
87
+ const text = parts[fakeCallIndex] ?? parts[parts.length - 1] ?? "";
88
+ fakeCallIndex += 1;
89
+ return { text, model_id: request.model_id, provider: "fake" };
90
+ }
91
+ const last = [...request.messages].reverse().find((message) => message.role === "user");
92
+ return {
93
+ text: `[fake:${request.model_id}] ${last ? messageContentToText(last.content) : ""}`.trim(),
94
+ model_id: request.model_id,
95
+ provider: "fake",
96
+ };
97
+ }
98
+ async function dispatchOllama(request, entry) {
99
+ const host = process.env.OLLAMA_HOST || "http://localhost:11434";
100
+ const body = {
101
+ model: entry.model || "qwen",
102
+ messages: toTextChatMessages(request),
103
+ stream: false,
104
+ };
105
+ const response = await fetch(`${host}/api/chat`, {
106
+ method: "POST",
107
+ headers: { "content-type": "application/json" },
108
+ body: JSON.stringify(body),
109
+ });
110
+ if (!response.ok) {
111
+ const detail = await response.text();
112
+ throw new AiProviderError(`Ollama HTTP ${response.status}: ${detail}`, request.model_id, "ollama");
113
+ }
114
+ const json = (await response.json());
115
+ const text = json.message?.content || "";
116
+ return { text, model_id: request.model_id, provider: "ollama" };
117
+ }
118
+ // OpenAI の chat/completions で `temperature` を受け付けるのは
119
+ // 旧世代の chat 系(gpt-4*, gpt-3.5*, およびそれらのファインチューン)のみ。
120
+ // gpt-5 系・o1/o3/o4 系はすべて reasoning モデルで temperature=1 固定。
121
+ // 2026 年以降に追加されるモデルも reasoning が既定になる見込みのため、
122
+ // 「既知のレガシー系だけ temperature を送る」ホワイトリスト方式で将来分も安全側に倒す。
123
+ const OPENAI_LEGACY_TEMPERATURE_PATTERN = /^(ft:)?(gpt-4|gpt-3\.5)/i;
124
+ export function openAIModelAcceptsTemperature(model) {
125
+ if (!model)
126
+ return false;
127
+ return OPENAI_LEGACY_TEMPERATURE_PATTERN.test(model);
128
+ }
129
+ export function buildOpenAIChatBody(request, entry) {
130
+ const model = entry.model || "gpt-5.4-mini";
131
+ const body = {
132
+ model,
133
+ messages: toOpenAIChatMessages(request),
134
+ };
135
+ if (openAIModelAcceptsTemperature(model)) {
136
+ body.temperature = request.temperature ?? 0.7;
137
+ }
138
+ return body;
139
+ }
140
+ // API プロバイダのエラーレスポンスを「人にわかりやすい一文+直し方の手がかり」に整形する。
141
+ // 生 JSON をそのまま投げ返さず、よくあるパターン (モデル名タイポ・認証失敗) は
142
+ // 設定ファイルのどこを見直すべきかまで案内する。
143
+ export function formatProviderApiError(args) {
144
+ const { provider, modelEntryId, modelName, status, rawBody } = args;
145
+ const parsed = tryParseJson(rawBody);
146
+ const messageText = extractProviderErrorMessage(parsed) || rawBody.trim();
147
+ if (isModelNotFoundError(provider, status, parsed, messageText)) {
148
+ const lines = [
149
+ l(`Model "${modelName ?? "(unset)"}" does not exist on ${provider} (HTTP ${status}).`, `モデル "${modelName ?? "(未指定)"}" は ${provider} に存在しません (HTTP ${status})。`),
150
+ l(`Check models.${modelEntryId}.model in ~/.agent-sin/models.yaml.`, `~/.agent-sin/models.yaml の models.${modelEntryId}.model を見直してください。`),
151
+ ];
152
+ if (messageText)
153
+ lines.push(l(`Original: ${truncate(messageText, 200)}`, `原文: ${truncate(messageText, 200)}`));
154
+ return lines.join("\n");
155
+ }
156
+ if (status === 401 || status === 403) {
157
+ const lines = [
158
+ l(`${provider} authentication failed (HTTP ${status}).`, `${provider} の認証に失敗しました (HTTP ${status})。`),
159
+ l("Check the API key in ~/.agent-sin/.env.", "~/.agent-sin/.env の API キーを確認してください。"),
160
+ ];
161
+ if (messageText)
162
+ lines.push(l(`Original: ${truncate(messageText, 200)}`, `原文: ${truncate(messageText, 200)}`));
163
+ return lines.join("\n");
164
+ }
165
+ // それ以外: メッセージだけ抜いて返す。生 JSON はもう吐かない。
166
+ return `${provider} HTTP ${status}: ${truncate(messageText, 400)}`;
167
+ }
168
+ function tryParseJson(raw) {
169
+ try {
170
+ return JSON.parse(raw);
171
+ }
172
+ catch {
173
+ return null;
174
+ }
175
+ }
176
+ function extractProviderErrorMessage(parsed) {
177
+ if (!parsed || typeof parsed !== "object")
178
+ return null;
179
+ const root = parsed;
180
+ const error = root.error;
181
+ if (typeof error === "string")
182
+ return error;
183
+ if (error && typeof error === "object") {
184
+ const msg = error.message;
185
+ if (typeof msg === "string")
186
+ return msg;
187
+ }
188
+ const msg = root.message;
189
+ return typeof msg === "string" ? msg : null;
190
+ }
191
+ function isModelNotFoundError(provider, status, parsed, messageText) {
192
+ if (status === 404)
193
+ return true;
194
+ const message = messageText.toLowerCase();
195
+ if (message.includes("does not exist") ||
196
+ message.includes("not found") ||
197
+ message.includes("is not supported") ||
198
+ message.includes("invalid model")) {
199
+ return true;
200
+ }
201
+ if (parsed && typeof parsed === "object") {
202
+ const error = parsed.error;
203
+ if (error && typeof error === "object") {
204
+ const code = error.code;
205
+ const type = error.type;
206
+ if (code === "model_not_found" || type === "not_found_error")
207
+ return true;
208
+ }
209
+ }
210
+ void provider;
211
+ return false;
212
+ }
213
+ function truncate(text, max) {
214
+ if (text.length <= max)
215
+ return text;
216
+ return text.slice(0, max) + "…";
217
+ }
218
+ async function dispatchOpenAI(request, entry) {
219
+ const keys = getApiKeys("openai");
220
+ if (keys.length === 0) {
221
+ throw new AiProviderError("OPENAI_API_KEY is not set", request.model_id, "openai");
222
+ }
223
+ const body = buildOpenAIChatBody(request, entry);
224
+ return rotateKeys(keys, request.model_id, "openai", async (apiKey) => {
225
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
226
+ method: "POST",
227
+ headers: {
228
+ "content-type": "application/json",
229
+ authorization: `Bearer ${apiKey}`,
230
+ },
231
+ body: JSON.stringify(body),
232
+ });
233
+ if (!response.ok) {
234
+ const text = await response.text();
235
+ const error = new AiProviderError(formatProviderApiError({
236
+ provider: "openai",
237
+ modelEntryId: request.model_id,
238
+ modelName: entry.model,
239
+ status: response.status,
240
+ rawBody: text,
241
+ }), request.model_id, "openai");
242
+ error.status = response.status;
243
+ throw error;
244
+ }
245
+ const json = (await response.json());
246
+ const text = json.choices?.[0]?.message?.content || "";
247
+ return { text, model_id: request.model_id, provider: "openai" };
248
+ });
249
+ }
250
+ async function dispatchGemini(request, entry) {
251
+ const keys = getApiKeys("gemini", ["google"]);
252
+ if (keys.length === 0) {
253
+ throw new AiProviderError("GEMINI_API_KEY (or GOOGLE_API_KEY) is not set", request.model_id, "gemini");
254
+ }
255
+ const model = entry.model || "gemini-2.0-flash";
256
+ const systemInstruction = collectSystem(request);
257
+ const contents = request.messages
258
+ .filter((message) => message.role !== "system")
259
+ .map((message) => ({
260
+ role: message.role === "assistant" ? "model" : "user",
261
+ parts: toGeminiParts(message.content),
262
+ }));
263
+ const body = { contents };
264
+ if (systemInstruction) {
265
+ body.system_instruction = { parts: [{ text: systemInstruction }] };
266
+ }
267
+ if (request.temperature !== undefined) {
268
+ body.generationConfig = { temperature: request.temperature };
269
+ }
270
+ return rotateKeys(keys, request.model_id, "gemini", async (apiKey) => {
271
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(apiKey)}`;
272
+ const response = await fetch(url, {
273
+ method: "POST",
274
+ headers: { "content-type": "application/json" },
275
+ body: JSON.stringify(body),
276
+ });
277
+ if (!response.ok) {
278
+ const text = await response.text();
279
+ const error = new AiProviderError(formatProviderApiError({
280
+ provider: "gemini",
281
+ modelEntryId: request.model_id,
282
+ modelName: model,
283
+ status: response.status,
284
+ rawBody: text,
285
+ }), request.model_id, "gemini");
286
+ error.status = response.status;
287
+ throw error;
288
+ }
289
+ const json = (await response.json());
290
+ const text = json.candidates?.[0]?.content?.parts?.map((part) => part.text || "").join("") || "";
291
+ return { text, model_id: request.model_id, provider: "gemini" };
292
+ });
293
+ }
294
+ async function dispatchAnthropic(request, entry) {
295
+ const keys = getApiKeys("anthropic");
296
+ if (keys.length === 0) {
297
+ throw new AiProviderError("ANTHROPIC_API_KEY is not set", request.model_id, "anthropic");
298
+ }
299
+ const model = entry.model || "claude-opus-4-7";
300
+ const systemInstruction = collectSystem(request);
301
+ // Anthropic は role: "tool" を持たないので tool 結果は user メッセージとして
302
+ // 流す。これで会話列が常に user で終わり、prefill エラー (HTTP 400) を防ぐ。
303
+ const messages = request.messages
304
+ .filter((message) => message.role !== "system")
305
+ .map((message) => ({
306
+ role: message.role === "assistant" ? "assistant" : "user",
307
+ content: toAnthropicContent(message.content),
308
+ }));
309
+ const body = {
310
+ model,
311
+ max_tokens: 1024,
312
+ messages,
313
+ };
314
+ if (systemInstruction) {
315
+ body.system = systemInstruction;
316
+ }
317
+ if (request.temperature !== undefined) {
318
+ body.temperature = request.temperature;
319
+ }
320
+ return rotateKeys(keys, request.model_id, "anthropic", async (apiKey) => {
321
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
322
+ method: "POST",
323
+ headers: {
324
+ "content-type": "application/json",
325
+ "x-api-key": apiKey,
326
+ "anthropic-version": "2023-06-01",
327
+ },
328
+ body: JSON.stringify(body),
329
+ });
330
+ if (!response.ok) {
331
+ const text = await response.text();
332
+ const error = new AiProviderError(formatProviderApiError({
333
+ provider: "anthropic",
334
+ modelEntryId: request.model_id,
335
+ modelName: model,
336
+ status: response.status,
337
+ rawBody: text,
338
+ }), request.model_id, "anthropic");
339
+ error.status = response.status;
340
+ throw error;
341
+ }
342
+ const json = (await response.json());
343
+ const text = (json.content || [])
344
+ .filter((part) => part.type === "text")
345
+ .map((part) => part.text || "")
346
+ .join("");
347
+ return { text, model_id: request.model_id, provider: "anthropic" };
348
+ });
349
+ }
350
+ function toAnthropicContent(content) {
351
+ if (typeof content === "string") {
352
+ return content;
353
+ }
354
+ const parts = [];
355
+ for (const part of content) {
356
+ if (part.type === "text") {
357
+ if (part.text.trim()) {
358
+ parts.push({ type: "text", text: part.text });
359
+ }
360
+ continue;
361
+ }
362
+ const inline = dataUrlToInlineData(part.image_url, part.mime_type);
363
+ if (inline) {
364
+ parts.push({
365
+ type: "image",
366
+ source: { type: "base64", media_type: inline.mime_type, data: inline.data },
367
+ });
368
+ }
369
+ else {
370
+ parts.push({ type: "text", text: imagePartToText(part) });
371
+ }
372
+ }
373
+ if (parts.length === 0) {
374
+ return "";
375
+ }
376
+ return parts;
377
+ }
378
+ async function rotateKeys(keys, modelId, providerLabel, call) {
379
+ let lastError;
380
+ for (let i = 0; i < keys.length; i += 1) {
381
+ try {
382
+ return await call(keys[i]);
383
+ }
384
+ catch (error) {
385
+ lastError = error;
386
+ if (i === keys.length - 1 || !isRateLimitError(error)) {
387
+ throw error;
388
+ }
389
+ }
390
+ }
391
+ throw lastError instanceof Error
392
+ ? lastError
393
+ : new AiProviderError(`${providerLabel} request failed`, modelId, providerLabel);
394
+ }
395
+ function isRateLimitError(error) {
396
+ if (!error || typeof error !== "object") {
397
+ return false;
398
+ }
399
+ const status = error.status;
400
+ if (status === 429 || status === 503) {
401
+ return true;
402
+ }
403
+ const message = error.message;
404
+ if (typeof message !== "string") {
405
+ return false;
406
+ }
407
+ return /\b(429|rate[ _-]?limit|quota|too[ _-]?many[ _-]?requests)\b/i.test(message);
408
+ }
409
+ function collectSystem(request) {
410
+ const parts = [];
411
+ if (request.system) {
412
+ parts.push(request.system);
413
+ }
414
+ for (const message of request.messages) {
415
+ if (message.role === "system") {
416
+ parts.push(messageContentToText(message.content));
417
+ }
418
+ }
419
+ return parts.filter(Boolean).join("\n\n");
420
+ }
421
+ async function dispatchCodex(request, entry) {
422
+ const mode = (process.env.AGENT_SIN_CODEX_MODE || "auto").toLowerCase();
423
+ const prompt = messagesToPrompt(request.messages);
424
+ if (mode === "spawn") {
425
+ return dispatchCodexSpawn(request, prompt, entry);
426
+ }
427
+ // appserver | auto: route through the app-server, including bypass mode (kingcoding-style).
428
+ const sandbox = resolveCodexSandbox(request);
429
+ const approvalPolicy = request.permission_mode === "bypass" ? "never" : undefined;
430
+ const effort = resolveCodexEffort(request, entry);
431
+ try {
432
+ const session = getSharedCodexAppServer(entry.model);
433
+ const text = await session.sendTurn(prompt, {
434
+ effort,
435
+ cwd: request.cwd || process.cwd(),
436
+ sandbox,
437
+ approvalPolicy,
438
+ onProgress: request.onProgress,
439
+ });
440
+ return { text, model_id: request.model_id, provider: "codex" };
441
+ }
442
+ catch (error) {
443
+ if (mode === "appserver") {
444
+ throw new AiProviderError(`codex app-server failed: ${error instanceof Error ? error.message : String(error)}`, request.model_id, "codex");
445
+ }
446
+ // auto: fall back to spawn so chat keeps working even if app-server is unavailable.
447
+ return dispatchCodexSpawn(request, prompt, entry);
448
+ }
449
+ }
450
+ async function dispatchCodexSpawn(request, prompt, entry) {
451
+ const bin = process.env.AGENT_SIN_CODEX_BIN || "codex";
452
+ const extra = splitExtraArgs(process.env.AGENT_SIN_CODEX_ARGS);
453
+ const sandbox = resolveCodexSandbox(request);
454
+ const model = entry.model || process.env.AGENT_SIN_CODEX_MODEL;
455
+ const args = ["exec", "--sandbox", sandbox];
456
+ if (!extra.includes("--skip-git-repo-check")) {
457
+ args.push("--skip-git-repo-check");
458
+ }
459
+ if (model && !extra.includes("--model") && !extra.includes("-m")) {
460
+ args.push("--model", model);
461
+ }
462
+ args.push(...extra, "--", prompt);
463
+ const text = await spawnCli(bin, args, request.model_id, "codex", request.onProgress, request.cwd);
464
+ return { text, model_id: request.model_id, provider: "codex" };
465
+ }
466
+ function resolveCodexSandbox(request) {
467
+ if (request.permission_mode !== "bypass")
468
+ return "read-only";
469
+ // Builder turns must not escape the draft (cwd) directory. workspace-write
470
+ // permits writes only inside cwd; reads remain broad so the builder can
471
+ // study agent-sin source / other skills.
472
+ if (request.role === "builder")
473
+ return "workspace-write";
474
+ return "danger-full-access";
475
+ }
476
+ function resolveCodexEffort(request, entry) {
477
+ const allowed = new Set(["low", "medium", "high", "xhigh"]);
478
+ const role = request.role || "chat";
479
+ const envValue = role === "builder"
480
+ ? process.env.AGENT_SIN_CODEX_BUILDER_EFFORT
481
+ : process.env.AGENT_SIN_CODEX_EFFORT;
482
+ const fallback = role === "builder" ? "xhigh" : "medium";
483
+ const candidate = (entry.effort && allowed.has(entry.effort) ? entry.effort : undefined) ||
484
+ request.effort ||
485
+ envValue ||
486
+ fallback;
487
+ return (allowed.has(candidate) ? candidate : fallback);
488
+ }
489
+ async function dispatchClaudeCode(request, entry) {
490
+ const bin = process.env.AGENT_SIN_CLAUDE_BIN || "claude";
491
+ const extra = splitExtraArgs(process.env.AGENT_SIN_CLAUDE_ARGS);
492
+ const isBuilder = request.role === "builder";
493
+ const tools = request.permission_mode === "bypass" ? ["--tools", "default"] : ["--tools="];
494
+ // For builder turns, prefer acceptEdits + --add-dir so writes are scoped to cwd.
495
+ // bypassPermissions disables every guard so we only keep it for non-builder uses.
496
+ const permission = request.permission_mode === "bypass"
497
+ ? isBuilder
498
+ ? ["--permission-mode", "acceptEdits"]
499
+ : ["--permission-mode", "bypassPermissions"]
500
+ : [];
501
+ const addDirArgs = isBuilder && request.permission_mode === "bypass" && request.cwd && !extra.includes("--add-dir")
502
+ ? ["--add-dir", request.cwd]
503
+ : [];
504
+ const model = entry.model || process.env.AGENT_SIN_CLAUDE_MODEL;
505
+ const modelArgs = model && !extra.includes("--model") && !extra.includes("-m") ? ["--model", model] : [];
506
+ const effort = resolveClaudeEffort(request, entry);
507
+ const effortArgs = effort && !extra.includes("--effort") ? ["--effort", effort] : [];
508
+ const args = [
509
+ "-p",
510
+ ...tools,
511
+ ...permission,
512
+ ...addDirArgs,
513
+ ...modelArgs,
514
+ ...effortArgs,
515
+ ...extra,
516
+ messagesToPrompt(request.messages),
517
+ ];
518
+ const text = await spawnCli(bin, args, request.model_id, "claude-code", request.onProgress, request.cwd);
519
+ return { text, model_id: request.model_id, provider: "claude-code" };
520
+ }
521
+ function resolveClaudeEffort(request, entry) {
522
+ const allowed = new Set(["low", "medium", "high", "xhigh", "max"]);
523
+ const role = request.role || "chat";
524
+ const envValue = role === "builder"
525
+ ? process.env.AGENT_SIN_CLAUDE_BUILDER_EFFORT
526
+ : process.env.AGENT_SIN_CLAUDE_EFFORT;
527
+ const candidate = (entry.effort && allowed.has(entry.effort) ? entry.effort : undefined) ||
528
+ request.effort ||
529
+ envValue;
530
+ if (!candidate) {
531
+ return undefined;
532
+ }
533
+ return allowed.has(candidate) ? candidate : undefined;
534
+ }
535
+ function toChatMessages(request) {
536
+ if (!request.system) {
537
+ return request.messages;
538
+ }
539
+ return [{ role: "system", content: request.system }, ...request.messages];
540
+ }
541
+ function toTextChatMessages(request) {
542
+ return toChatMessages(request).map((message) => ({
543
+ role: message.role,
544
+ content: messageContentToText(message.content),
545
+ }));
546
+ }
547
+ function messagesToPrompt(messages) {
548
+ return messages
549
+ .map((message) => {
550
+ const content = messageContentToText(message.content);
551
+ if (message.role === "system") {
552
+ return `[system]\n${content}`;
553
+ }
554
+ if (message.role === "user") {
555
+ return `[user]\n${content}`;
556
+ }
557
+ if (message.role === "tool") {
558
+ return `[tool-result]\n${content}`;
559
+ }
560
+ return `[assistant]\n${content}`;
561
+ })
562
+ .join("\n\n");
563
+ }
564
+ // OpenAI の chat/completions は `role: "tool"` を直前の assistant メッセージの
565
+ // `tool_calls` への返答としてしか許容しない(無いと HTTP 400)。
566
+ // agent-sin は markdown の skill-call ブロックで独自に skill 呼び出しを行うため
567
+ // tool_calls プロトコルには乗らない。よって OpenAI に投げる際だけ
568
+ // tool ロールは [tool-result] プレフィックス付きの user メッセージに畳む。
569
+ function toOpenAIChatMessages(request) {
570
+ return toChatMessages(request).map((message) => {
571
+ const role = message.role === "tool" ? "user" : message.role;
572
+ const rawContent = Array.isArray(message.content)
573
+ ? message.content
574
+ .map((part) => part.type === "image"
575
+ ? { type: "image_url", image_url: { url: part.image_url } }
576
+ : { type: "text", text: part.text })
577
+ .filter((part) => part.type !== "text" || part.text.trim().length > 0)
578
+ : message.content;
579
+ if (message.role !== "tool") {
580
+ return { role, content: rawContent };
581
+ }
582
+ if (typeof rawContent === "string") {
583
+ return { role, content: `[tool-result]\n${rawContent}` };
584
+ }
585
+ const prefixed = [
586
+ { type: "text", text: "[tool-result]" },
587
+ ...rawContent,
588
+ ];
589
+ return { role, content: prefixed };
590
+ });
591
+ }
592
+ function toGeminiParts(content) {
593
+ if (typeof content === "string") {
594
+ return [{ text: content }];
595
+ }
596
+ const parts = [];
597
+ for (const part of content) {
598
+ if (part.type === "text") {
599
+ if (part.text.trim()) {
600
+ parts.push({ text: part.text });
601
+ }
602
+ continue;
603
+ }
604
+ const inline = dataUrlToInlineData(part.image_url, part.mime_type);
605
+ if (inline) {
606
+ parts.push({ inline_data: inline });
607
+ }
608
+ else {
609
+ parts.push({ text: imagePartToText(part) });
610
+ }
611
+ }
612
+ return parts.length > 0 ? parts : [{ text: "" }];
613
+ }
614
+ function dataUrlToInlineData(value, fallbackMimeType) {
615
+ const match = value.match(/^data:([^;,]+)?;base64,([\s\S]+)$/);
616
+ if (!match) {
617
+ return null;
618
+ }
619
+ return {
620
+ mime_type: match[1] || fallbackMimeType || "image/png",
621
+ data: match[2],
622
+ };
623
+ }
624
+ export function messageContentToText(content) {
625
+ if (typeof content === "string") {
626
+ return content;
627
+ }
628
+ return content
629
+ .map((part) => (part.type === "text" ? part.text : imagePartToText(part)))
630
+ .filter(Boolean)
631
+ .join("\n");
632
+ }
633
+ function imagePartToText(part) {
634
+ const meta = [part.filename, part.mime_type].filter(Boolean).join(" ");
635
+ const url = part.image_url.startsWith("data:") ? "data-url" : part.image_url;
636
+ return `[image${meta ? `: ${meta}` : ""}${url ? ` ${url}` : ""}]`;
637
+ }
638
+ function splitExtraArgs(value) {
639
+ if (!value) {
640
+ return [];
641
+ }
642
+ return value.split(/\s+/).filter(Boolean);
643
+ }
644
+ async function spawnCli(bin, args, modelId, provider, onProgress, cwd) {
645
+ return new Promise((resolve, reject) => {
646
+ const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"], cwd });
647
+ const stdout = [];
648
+ const stderr = [];
649
+ let stderrLine = "";
650
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
651
+ child.stderr.on("data", (chunk) => {
652
+ stderr.push(chunk);
653
+ if (!onProgress) {
654
+ return;
655
+ }
656
+ stderrLine += chunk.toString("utf8");
657
+ let newlineIndex;
658
+ while ((newlineIndex = stderrLine.indexOf("\n")) >= 0) {
659
+ const line = stderrLine.slice(0, newlineIndex).trim();
660
+ stderrLine = stderrLine.slice(newlineIndex + 1);
661
+ if (line) {
662
+ onProgress({ kind: "stderr", text: line.slice(0, 160) });
663
+ }
664
+ }
665
+ });
666
+ child.on("error", (error) => {
667
+ reject(new AiProviderError(`Failed to launch ${bin}: ${error.message}`, modelId, provider));
668
+ });
669
+ child.on("close", (code) => {
670
+ if (code !== 0) {
671
+ const err = Buffer.concat(stderr).toString("utf8").trim();
672
+ reject(new AiProviderError(`${bin} exited with code ${code}${err ? `: ${err}` : ""}`, modelId, provider));
673
+ return;
674
+ }
675
+ resolve(Buffer.concat(stdout).toString("utf8").trim());
676
+ });
677
+ });
678
+ }