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,812 @@
1
+ import { loadModels } from "./config.js";
2
+ import { appendConversationLog, appendEventLog, readRunLog } from "./logger.js";
3
+ import { getAiProvider, } from "./ai-provider.js";
4
+ import { listSkillManifests } from "./skill-registry.js";
5
+ import { runSkill, SkillRunError } from "./runtime.js";
6
+ import { buildDraftWithAgent, prepareRepairDraft, } from "../builder/builder-session.js";
7
+ import { formatProfileMemoryPromptSection, readProfileMemoryForPrompt, } from "./profile-memory.js";
8
+ import { maybePromoteDailyMemory } from "./daily-memory-promotion.js";
9
+ import { l, t } from "./i18n.js";
10
+ export const HISTORY_LIMIT = 20;
11
+ export const TOOL_CALL_MAX_ITERATIONS = 3;
12
+ const SKILL_CALL_PATTERN = /```skill-call\s*\n([\s\S]*?)\n```/g;
13
+ const BUILD_SUGGESTION_PATTERN = /```agent-sin-build-suggestion\s*\n([\s\S]*?)\n```/g;
14
+ const REPAIR_FAILURE_PATTERN = /(traceback|exception|runtimeerror|syntaxerror|typeerror|nameerror|valueerror|importerror|module not found|exited with code|did not return valid json|handler not found|entry file not found|実行時|例外|エラー|失敗|読み取れません|できませんでした)/i;
15
+ const USER_FIXABLE_FAILURE_PATTERN = /(missing required env vars|invalid input for|is disabled|skill not found|not allowed|設定してください|必要な設定|未入力|アプリパスワード|api[_ -]?key|token|credentials?)/i;
16
+ export async function chatRespond(config, userText, history, options = {}) {
17
+ const formatNarrative = options.formatNarrative ?? ((text) => text);
18
+ const spinner = options.spinner;
19
+ const eventSource = options.eventSource ?? "chat";
20
+ const emitProgress = (event) => {
21
+ if (!options.onChatProgress)
22
+ return;
23
+ try {
24
+ options.onChatProgress(event);
25
+ }
26
+ catch {
27
+ // progress callbacks must not break the chat flow
28
+ }
29
+ };
30
+ const skills = await listSkillManifests(config.skills_dir);
31
+ const tools = skills.filter(isToolEligible);
32
+ await maybePromoteDailyMemory(config, { eventSource });
33
+ const profileMemory = await readProfileMemoryForPrompt(config);
34
+ const systemPrompt = buildSystemPrompt(tools, options.preferredSkillId, profileMemory);
35
+ const modelDisplay = await resolveDisplayModelName(config);
36
+ appendHistory(history, { role: "user", content: userText });
37
+ const userTurnIndex = history.length - 1;
38
+ const userImages = options.userImages || [];
39
+ await appendEventLog(config, {
40
+ level: "info",
41
+ source: eventSource,
42
+ event: "user_input",
43
+ message: userText.slice(0, 200),
44
+ details: { model_id: config.chat_model_id, length: userText.length },
45
+ });
46
+ await appendConversationLog(config, {
47
+ source: "chat",
48
+ role: "user",
49
+ content: userText,
50
+ model_id: config.chat_model_id,
51
+ });
52
+ const lines = [];
53
+ const ensureVisibleReply = () => {
54
+ if (lines.some((line) => line.trim().length > 0)) {
55
+ return lines;
56
+ }
57
+ lines.push(formatNarrative(emptyChatFallback()));
58
+ return lines;
59
+ };
60
+ const completedCallKeys = new Set();
61
+ let lastCompletedSummary = "";
62
+ let pendingRawRangeStart = null;
63
+ for (let iteration = 0; iteration < TOOL_CALL_MAX_ITERATIONS; iteration += 1) {
64
+ let assistantText;
65
+ let buildSuggestion = null;
66
+ const baseLabel = `${modelDisplay}: ${t("spinner.thinking")}`;
67
+ if (spinner)
68
+ spinner.start(baseLabel);
69
+ emitProgress({ kind: "thinking", iteration });
70
+ const spinnerProgress = spinner ? makeSpinnerProgress(spinner, baseLabel) : null;
71
+ const providerProgress = spinnerProgress || options.onAiProgress
72
+ ? (event) => {
73
+ if (spinnerProgress) {
74
+ spinnerProgress(event);
75
+ }
76
+ if (options.onAiProgress) {
77
+ options.onAiProgress(event);
78
+ }
79
+ }
80
+ : undefined;
81
+ try {
82
+ const messages = [
83
+ { role: "system", content: systemPrompt },
84
+ ...toAiMessages(history, userImages.length > 0 ? { index: userTurnIndex, images: userImages } : undefined),
85
+ ];
86
+ const provider = getAiProvider();
87
+ const response = await provider(config, {
88
+ model_id: config.chat_model_id,
89
+ messages,
90
+ onProgress: providerProgress,
91
+ });
92
+ buildSuggestion = parseBuildSuggestion(response.text);
93
+ assistantText = stripBuildSuggestions(response.text);
94
+ if (shouldRetryEmptyAssistantReply(assistantText, buildSuggestion)) {
95
+ await appendEventLog(config, {
96
+ level: "warn",
97
+ source: eventSource,
98
+ event: "empty_model_reply_retry",
99
+ message: "model returned an empty chat reply; retrying once",
100
+ details: { model_id: config.chat_model_id, iteration },
101
+ });
102
+ try {
103
+ const retryResponse = await provider(config, {
104
+ model_id: config.chat_model_id,
105
+ messages: [
106
+ ...messages,
107
+ { role: "system", content: emptyReplyRetryPrompt() },
108
+ ],
109
+ onProgress: providerProgress,
110
+ });
111
+ buildSuggestion = parseBuildSuggestion(retryResponse.text);
112
+ assistantText = stripBuildSuggestions(retryResponse.text);
113
+ }
114
+ catch (retryError) {
115
+ const retryMessage = retryError instanceof Error ? retryError.message : String(retryError);
116
+ await appendEventLog(config, {
117
+ level: "warn",
118
+ source: eventSource,
119
+ event: "empty_model_reply_retry_failed",
120
+ message: retryMessage,
121
+ details: { model_id: config.chat_model_id, iteration },
122
+ });
123
+ }
124
+ }
125
+ if (buildSuggestion && options.onBuildSuggestion) {
126
+ try {
127
+ options.onBuildSuggestion(buildSuggestion);
128
+ }
129
+ catch {
130
+ // Build suggestions are optional metadata; never break chat.
131
+ }
132
+ }
133
+ }
134
+ catch (error) {
135
+ if (spinner)
136
+ spinner.stop();
137
+ const message = error instanceof Error ? error.message : String(error);
138
+ history.pop();
139
+ await appendEventLog(config, {
140
+ level: "error",
141
+ source: eventSource,
142
+ event: "model_failed",
143
+ message,
144
+ details: { model_id: config.chat_model_id },
145
+ });
146
+ emitProgress({ kind: "model_failed", message });
147
+ return [t("chat.model_unreachable", { model: config.chat_model_id, message })];
148
+ }
149
+ if (spinner)
150
+ spinner.stop();
151
+ const calls = parseSkillCalls(assistantText);
152
+ const narrative = stripSkillCalls(assistantText).trim();
153
+ // When the response invokes a side-effect skill (add/delete/send/save),
154
+ // drop the LLM's narrative entirely. The deterministic skill result is
155
+ // the only record — both for the user and for future-turn history reads.
156
+ // This stops "今から追加します" from sitting next to a successful tool
157
+ // result and being misread later as "not done yet".
158
+ const hasSideEffectCall = calls.some((call) => {
159
+ const tool = tools.find((skill) => skill.id === call.id);
160
+ return tool?.side_effect === true;
161
+ });
162
+ const recordedAssistantText = hasSideEffectCall && narrative
163
+ ? extractSkillCallBlocks(assistantText)
164
+ : assistantText;
165
+ appendHistory(history, { role: "assistant", content: recordedAssistantText });
166
+ if (pendingRawRangeStart !== null) {
167
+ if (calls.length > 0) {
168
+ // Previous turn ran an output_mode: raw skill, but the model wants to
169
+ // call another tool (e.g. todo-list -> todo-done). The intermediate
170
+ // result is plumbing the user does not need to see — drop it.
171
+ lines.length = pendingRawRangeStart;
172
+ pendingRawRangeStart = null;
173
+ }
174
+ else {
175
+ // Previous raw skill result is the final answer; skip the LLM's
176
+ // narrative-only follow-up so the raw output stands alone.
177
+ return ensureVisibleReply();
178
+ }
179
+ }
180
+ if (narrative && !hasSideEffectCall) {
181
+ lines.push(formatNarrative(narrative));
182
+ }
183
+ else if (!narrative && calls.length === 0 && buildSuggestion) {
184
+ const prompt = buildSuggestion.type === "edit"
185
+ ? l("I can fix that in build mode. Should I continue?", "ビルドモードで直せます。進めますか?")
186
+ : l("I can create that in build mode. Should I continue?", "ビルドモードで作れます。進めますか?");
187
+ lines.push(formatNarrative(prompt));
188
+ }
189
+ else if (!narrative && calls.length === 0) {
190
+ lines.push(formatNarrative(emptyChatFallback()));
191
+ }
192
+ await appendEventLog(config, {
193
+ level: "info",
194
+ source: eventSource,
195
+ event: "assistant_reply",
196
+ message: narrative.slice(0, 200) || undefined,
197
+ details: { model_id: config.chat_model_id, iteration, skill_calls: calls.map((call) => call.id) },
198
+ });
199
+ await appendConversationLog(config, {
200
+ source: "chat",
201
+ role: "assistant",
202
+ content: assistantText,
203
+ model_id: config.chat_model_id,
204
+ details: { iteration, skill_calls: calls.map((call) => call.id) },
205
+ });
206
+ if (calls.length === 0) {
207
+ return ensureVisibleReply();
208
+ }
209
+ const callsToRun = calls.filter((call) => !completedCallKeys.has(skillCallKey(call)));
210
+ if (callsToRun.length === 0) {
211
+ if (!narrative && lastCompletedSummary) {
212
+ lines.push(formatNarrative(lastCompletedSummary));
213
+ }
214
+ await appendEventLog(config, {
215
+ level: "info",
216
+ source: eventSource,
217
+ event: "tool_repeat_skipped",
218
+ message: `model repeated already-completed skill calls: ${calls.map((call) => call.id).join(", ")}`,
219
+ details: { model_id: config.chat_model_id, iteration },
220
+ });
221
+ return ensureVisibleReply();
222
+ }
223
+ const rawRangeStart = lines.length;
224
+ let allRawOk = callsToRun.length > 0;
225
+ for (const call of callsToRun) {
226
+ const tool = tools.find((skill) => skill.id === call.id);
227
+ if (!tool) {
228
+ const message = `[skill not allowed: ${call.id}]`;
229
+ lines.push(message);
230
+ appendHistory(history, { role: "tool", content: toolResultJson(call.id, "error", message) });
231
+ await appendEventLog(config, {
232
+ level: "warn",
233
+ source: eventSource,
234
+ event: "skill_blocked",
235
+ message,
236
+ details: { skill_id: call.id },
237
+ });
238
+ allRawOk = false;
239
+ continue;
240
+ }
241
+ const isRawMode = tool.output_mode === "raw";
242
+ if (!isRawMode) {
243
+ lines.push(t("chat.tool_call_announce", { skill: call.id }));
244
+ }
245
+ const execution = await runSkillCallWithSelfRepair(config, tool, call, userText, history, {
246
+ spinner,
247
+ emitProgress,
248
+ eventSource,
249
+ });
250
+ lines.push(...execution.repairLines);
251
+ if (execution.response) {
252
+ const result = execution.response;
253
+ const summary = [result.result.title, result.result.summary].filter(Boolean).join(" / ");
254
+ const display = result.result.summary || result.result.title;
255
+ const saved = result.saved_outputs.filter((item) => item.show_saved !== false).map((item) => item.path);
256
+ if (display) {
257
+ lines.push(display);
258
+ }
259
+ for (const savedPath of saved) {
260
+ lines.push(`saved: ${savedPath}`);
261
+ }
262
+ if (result.result.status === "ok") {
263
+ completedCallKeys.add(skillCallKey(call));
264
+ if (display) {
265
+ lastCompletedSummary = display;
266
+ }
267
+ }
268
+ if (!isRawMode || result.result.status !== "ok") {
269
+ allRawOk = false;
270
+ }
271
+ const historyData = isRawMode ? result.result.data : undefined;
272
+ const historyContent = toolResultJson(call.id, result.result.status, summary, saved, historyData);
273
+ appendHistory(history, {
274
+ role: "tool",
275
+ content: historyContent,
276
+ });
277
+ await appendConversationLog(config, {
278
+ source: "chat",
279
+ role: "tool",
280
+ content: historyContent,
281
+ skill_id: call.id,
282
+ details: { status: result.result.status, run_id: result.run_id, saved },
283
+ });
284
+ }
285
+ else {
286
+ const message = execution.errorMessage;
287
+ lines.push(`[skill error: ${message}]`);
288
+ appendHistory(history, { role: "tool", content: toolResultJson(call.id, "error", message) });
289
+ await appendEventLog(config, {
290
+ level: "error",
291
+ source: eventSource,
292
+ event: "skill_error",
293
+ message,
294
+ details: { skill_id: call.id, args: call.args },
295
+ });
296
+ allRawOk = false;
297
+ }
298
+ }
299
+ if (allRawOk) {
300
+ pendingRawRangeStart = rawRangeStart;
301
+ }
302
+ }
303
+ if (pendingRawRangeStart !== null) {
304
+ return ensureVisibleReply();
305
+ }
306
+ lines.push("[tool call iterations exhausted]");
307
+ await appendEventLog(config, {
308
+ level: "warn",
309
+ source: eventSource,
310
+ event: "tool_iterations_exhausted",
311
+ details: { model_id: config.chat_model_id, max: TOOL_CALL_MAX_ITERATIONS },
312
+ });
313
+ return lines;
314
+ }
315
+ async function runSkillCallWithSelfRepair(config, tool, call, userText, history, options) {
316
+ const firstAttempt = await attemptSkillRun(config, call, options);
317
+ const firstFailure = failureFromAttempt(firstAttempt);
318
+ if (!firstFailure || !shouldAttemptSelfRepair(firstFailure)) {
319
+ return firstAttempt.ok
320
+ ? { response: firstAttempt.response, repairLines: [] }
321
+ : { errorMessage: firstAttempt.message, repairLines: [] };
322
+ }
323
+ const repairLines = [t("chat.skill_repair_started", { skill: call.id })];
324
+ const failureContext = buildFailureContext(firstAttempt, firstFailure);
325
+ const repair = await repairSkillAfterFailure(config, tool, call, userText, history, firstFailure, failureContext, options);
326
+ if (!repair.ok) {
327
+ repairLines.push(t("chat.skill_repair_failed", { message: repair.message }));
328
+ return firstAttempt.ok
329
+ ? { response: firstAttempt.response, repairLines }
330
+ : { errorMessage: firstAttempt.message, repairLines };
331
+ }
332
+ const secondAttempt = await attemptSkillRun(config, call, options);
333
+ const secondFailure = failureFromAttempt(secondAttempt);
334
+ if (!secondFailure && secondAttempt.ok) {
335
+ repairLines.push(t("chat.skill_repair_done"));
336
+ return { response: secondAttempt.response, repairLines };
337
+ }
338
+ const stillFailure = secondFailure || "Skill failed after repair";
339
+ repairLines.push(t("chat.skill_repair_still_failed", { message: shortError(stillFailure) }));
340
+ return secondAttempt.ok
341
+ ? { response: secondAttempt.response, repairLines }
342
+ : { errorMessage: secondAttempt.message, repairLines };
343
+ }
344
+ async function attemptSkillRun(config, call, options) {
345
+ const spinner = options.spinner;
346
+ if (spinner)
347
+ spinner.start(t("spinner.skill_running", { skill: call.id }));
348
+ options.emitProgress({ kind: "tool_running", skill_id: call.id });
349
+ try {
350
+ const response = await runSkill(config, call.id, call.args, { approved: true });
351
+ options.emitProgress({ kind: "tool_done", skill_id: call.id, status: response.result.status });
352
+ const runLog = await readRunLog(config, response.run_id).catch(() => undefined);
353
+ return { ok: true, response, runLog };
354
+ }
355
+ catch (error) {
356
+ const message = error instanceof Error ? error.message : String(error);
357
+ options.emitProgress({ kind: "tool_done", skill_id: call.id, status: "error" });
358
+ if (error instanceof SkillRunError) {
359
+ const runLog = await readRunLog(config, error.runId).catch(() => undefined);
360
+ return {
361
+ ok: false,
362
+ message,
363
+ runLog,
364
+ runId: error.runId,
365
+ logPath: error.logPath,
366
+ };
367
+ }
368
+ return { ok: false, message };
369
+ }
370
+ finally {
371
+ if (spinner)
372
+ spinner.stop();
373
+ }
374
+ }
375
+ async function repairSkillAfterFailure(config, tool, call, userText, history, failure, failureContext, options) {
376
+ const spinner = options.spinner;
377
+ const baseLabel = t("spinner.skill_repairing", { skill: call.id });
378
+ if (spinner)
379
+ spinner.start(baseLabel);
380
+ options.emitProgress({ kind: "tool_repairing", skill_id: call.id });
381
+ await appendEventLog(config, {
382
+ level: "warn",
383
+ source: options.eventSource,
384
+ event: "skill_self_repair_started",
385
+ message: shortError(failure),
386
+ details: { skill_id: call.id, args: call.args },
387
+ });
388
+ try {
389
+ await prepareRepairDraft(config, tool);
390
+ const result = await buildDraftWithAgent(config, call.id, buildRepairPrompt(tool, call, userText, failure, failureContext), {
391
+ runtime: tool.runtime,
392
+ handoff: buildRepairHandoff(history, call, failureContext),
393
+ onProgress: spinner ? makeSpinnerProgress(spinner, baseLabel) : undefined,
394
+ });
395
+ await appendEventLog(config, {
396
+ level: "info",
397
+ source: options.eventSource,
398
+ event: "skill_self_repair_finished",
399
+ message: result.summary.slice(0, 200),
400
+ details: {
401
+ skill_id: call.id,
402
+ files_written: result.files_written,
403
+ status: result.state.status,
404
+ },
405
+ });
406
+ return { ok: true, summary: result.summary };
407
+ }
408
+ catch (error) {
409
+ const message = error instanceof Error ? error.message : String(error);
410
+ await appendEventLog(config, {
411
+ level: "error",
412
+ source: options.eventSource,
413
+ event: "skill_self_repair_failed",
414
+ message,
415
+ details: { skill_id: call.id },
416
+ });
417
+ return { ok: false, message: shortError(message) };
418
+ }
419
+ finally {
420
+ if (spinner)
421
+ spinner.stop();
422
+ }
423
+ }
424
+ function failureFromAttempt(attempt) {
425
+ if (!attempt.ok) {
426
+ return attempt.message;
427
+ }
428
+ const result = attempt.response.result;
429
+ const summary = [result.title, result.summary].filter(Boolean).join(" / ");
430
+ if (result.status === "error") {
431
+ return summary || "Skill returned error";
432
+ }
433
+ if (result.status === "skipped" && REPAIR_FAILURE_PATTERN.test(summary) && !USER_FIXABLE_FAILURE_PATTERN.test(summary)) {
434
+ return summary;
435
+ }
436
+ return null;
437
+ }
438
+ function buildFailureContext(attempt, failure) {
439
+ const parts = [`failure: ${failure}`];
440
+ if (!attempt.ok) {
441
+ if (attempt.runId) {
442
+ parts.push(`run_id: ${attempt.runId}`);
443
+ }
444
+ if (attempt.logPath) {
445
+ parts.push(`log_path: ${attempt.logPath}`);
446
+ }
447
+ if (attempt.runLog) {
448
+ parts.push("run_log:");
449
+ parts.push(formatRunLogForRepair(attempt.runLog));
450
+ }
451
+ return parts.join("\n");
452
+ }
453
+ const response = attempt.response;
454
+ parts.push(`run_id: ${response.run_id}`);
455
+ parts.push(`log_path: ${response.log_path}`);
456
+ if (attempt.runLog) {
457
+ parts.push("run_log:");
458
+ parts.push(formatRunLogForRepair(attempt.runLog));
459
+ }
460
+ else {
461
+ parts.push(safeJson({
462
+ status: response.result.status,
463
+ attempts: response.attempts,
464
+ result: redactSensitiveValues(response.result),
465
+ saved_outputs: response.saved_outputs.map((item) => item.path),
466
+ }));
467
+ }
468
+ return parts.join("\n");
469
+ }
470
+ function formatRunLogForRepair(record) {
471
+ return safeJson({
472
+ run_id: record.run_id,
473
+ skill_id: record.skill_id,
474
+ status: record.status,
475
+ started_at: record.started_at,
476
+ finished_at: record.finished_at,
477
+ attempts: record.attempts,
478
+ input: redactSensitiveValues(record.input),
479
+ result: redactSensitiveValues(record.result),
480
+ error: record.error,
481
+ ctx_logs: record.ctx_logs,
482
+ dry_run: record.dry_run,
483
+ });
484
+ }
485
+ function redactSensitiveValues(value) {
486
+ if (Array.isArray(value)) {
487
+ return value.map((item) => redactSensitiveValues(item));
488
+ }
489
+ if (!value || typeof value !== "object") {
490
+ if (typeof value === "string" && value.length > 2000) {
491
+ return `${value.slice(0, 2000)}...`;
492
+ }
493
+ return value;
494
+ }
495
+ const out = {};
496
+ for (const [key, item] of Object.entries(value)) {
497
+ if (/(api[_-]?key|token|secret|password|credential|authorization|cookie)/i.test(key)) {
498
+ out[key] = "[redacted]";
499
+ continue;
500
+ }
501
+ out[key] = redactSensitiveValues(item);
502
+ }
503
+ return out;
504
+ }
505
+ function shouldAttemptSelfRepair(failure) {
506
+ if (!failure.trim())
507
+ return false;
508
+ if (USER_FIXABLE_FAILURE_PATTERN.test(failure))
509
+ return false;
510
+ return REPAIR_FAILURE_PATTERN.test(failure);
511
+ }
512
+ function buildRepairPrompt(tool, call, userText, failure, failureContext) {
513
+ return [
514
+ `${tool.id} failed while running. Fix the cause without asking the user again, while preserving the original goal.`,
515
+ "",
516
+ "User request:",
517
+ truncateForPrompt(userText, 2000),
518
+ "",
519
+ "Skill call arguments:",
520
+ truncateForPrompt(safeJson(call.args), 2000),
521
+ "",
522
+ "Failure:",
523
+ truncateForPrompt(failure, 8000),
524
+ "",
525
+ "Run logs and diagnostics:",
526
+ truncateForPrompt(failureContext, 12000),
527
+ "",
528
+ "Repair policy:",
529
+ "- Fix root causes such as tracebacks, runtime exceptions, invalid JSON, or failures to parse natural language.",
530
+ "- If credentials are the only missing piece, declare required_env correctly and do not break the implementation.",
531
+ "- When logs are available, use input / result / error / ctx_logs as evidence.",
532
+ "- Do not remove existing capabilities.",
533
+ "- Make the skill runnable again with the same arguments.",
534
+ "- Keep the user-facing response short, in the user's language, and say only what was fixed.",
535
+ ].join("\n");
536
+ }
537
+ function buildRepairHandoff(history, call, failure) {
538
+ const recent = history.slice(-8).map((turn) => ({
539
+ role: turn.role,
540
+ content: truncateForPrompt(turn.content, 4000),
541
+ }));
542
+ return [
543
+ ...recent,
544
+ {
545
+ role: "tool",
546
+ content: toolResultJson(call.id, "error", truncateForPrompt(failure, 8000)),
547
+ },
548
+ ];
549
+ }
550
+ function safeJson(value) {
551
+ try {
552
+ return JSON.stringify(value, null, 2);
553
+ }
554
+ catch {
555
+ return String(value);
556
+ }
557
+ }
558
+ function truncateForPrompt(text, max) {
559
+ if (text.length <= max)
560
+ return text;
561
+ return `${text.slice(0, max)}\n...`;
562
+ }
563
+ function shortError(text) {
564
+ const compact = text.replace(/\s+/g, " ").trim();
565
+ return compact.length > 240 ? `${compact.slice(0, 240)}...` : compact;
566
+ }
567
+ async function resolveDisplayModelName(config) {
568
+ const entryId = config.chat_model_id;
569
+ try {
570
+ const models = await loadModels(config.workspace);
571
+ const entry = models.models?.[entryId];
572
+ if (entry?.model)
573
+ return entry.model;
574
+ }
575
+ catch {
576
+ // models.yaml may be unreadable in degraded environments — fall through.
577
+ }
578
+ return entryId;
579
+ }
580
+ export function makeSpinnerProgress(spinner, baseLabel) {
581
+ return (event) => {
582
+ if (!spinner.isActive()) {
583
+ return;
584
+ }
585
+ const label = formatProgressLabel(baseLabel, event);
586
+ if (label) {
587
+ spinner.update(label);
588
+ }
589
+ };
590
+ }
591
+ export function formatProgressLabel(baseLabel, event) {
592
+ switch (event.kind) {
593
+ case "thinking":
594
+ return `${baseLabel} — ${l("thinking", "思考中")}${event.text ? `: ${event.text}` : ""}`;
595
+ case "tool":
596
+ return `${baseLabel} — tool: ${event.name || "?"}${event.text ? ` ${event.text}` : ""}`;
597
+ case "message":
598
+ return event.text ? `${baseLabel} — ${l("response", "応答")}: ${event.text}` : null;
599
+ case "info":
600
+ return `${baseLabel} — ${event.text}`;
601
+ case "stderr":
602
+ return `${baseLabel} — ${event.text}`;
603
+ default:
604
+ return null;
605
+ }
606
+ }
607
+ export function isToolEligible(skill) {
608
+ return skill.enabled !== false;
609
+ }
610
+ export function buildSystemPrompt(skills, preferredSkillId, profileMemory) {
611
+ const lines = [
612
+ "You are Agent-Sin conversation mode. Talk naturally with the user and call the registered skills below when useful.",
613
+ "You cannot call unregistered skills or run arbitrary CLI commands directly.",
614
+ "",
615
+ "Output language: respond in the same language as the user's most recent message. If the user wrote in Japanese, respond in Japanese; otherwise respond in English. Match the user's level of formality.",
616
+ "",
617
+ "Important constraint: conversation mode cannot rewrite files.",
618
+ "- Conversation mode runs in a read-only sandbox. Do not try to edit, create, or delete skill.yaml, main.py, or arbitrary files.",
619
+ "- If a skill needs to be fixed or a new skill should be created, do not edit it yourself. Hand it to build mode after briefly confirming the direction.",
620
+ "- Do not say that you identified edits but were blocked, or that you could update it if write access were available. Ask only for build-mode handoff when editing is needed.",
621
+ "",
622
+ "Response style:",
623
+ "- Always respond to the user in plain text. Do not use Markdown styling, headings, bullet markers, numbered lists, inline code, fenced code blocks, links, or tables. If content is list-like, write it as plain sentences or simple line breaks. The only exception is the hidden internal fenced block described below.",
624
+ "- Do not volunteer command syntax or usage lists. For typo-like input, infer the intent and answer directly, or ask one short question if needed.",
625
+ "- If the user is naturally asking to create a new skill or edit an existing one, do not start implementing inside conversation mode. Briefly say what should be built or changed, then end with a one-line confirmation in the user's language (e.g. 'Should I switch to build mode to fix this?' / 'ビルドモードに入って直しますか?').",
626
+ "- On any turn that asks for that confirmation, or that says you are handing off to build mode, include one hidden agent-sin-build-suggestion block at the end. If you mention a handoff without the block, no handoff happens.",
627
+ "- If the user has already agreed with a short approval such as 'yes', 'go ahead', 'please do it', はい, お願い, 進めて, you may proceed with the handoff, but the same response must still include the block.",
628
+ "- For agent-sin-build-suggestion, use type=create for a new skill and type=edit for an existing skill. skill_id must be a short kebab-case id. For edits, use an id from the available skills list.",
629
+ "- For edits, never invent umbrella ids such as 'todo'. If the request spans several available skills, explain the split briefly and ask which exact skill should be changed first.",
630
+ "- Do not explain internal storage details such as builtin packaging, workspace copies, or override flags unless the user explicitly asks.",
631
+ "- If the user explicitly asks for help (!help, /help, etc.), another route shows help text. You do not need to create a command list yourself.",
632
+ "",
633
+ "When to call skills:",
634
+ "- Call skills with side effects such as saving, recording, sending, deleting, or registering only when the user explicitly asks.",
635
+ "- Especially for memo-save and other memo-writing skills, call them only when the user directly says to save, record, or memo something. Do not save just because content seems important.",
636
+ "- Save to soul.md or user.md only when the user explicitly asks to write there.",
637
+ "- Add to memory.md only when the conversation contains a strong long-term fact worth preserving, and at most one item per turn:",
638
+ " - A durable preference, style, or policy the user explicitly wants kept",
639
+ " - A continuing specification decision, agreement, or operating rule",
640
+ " - Fixed context that future conversations should repeatedly assume, such as role, environment, stakeholders, or stable personal attributes",
641
+ "- Never write to memory.md for same-day chat, feelings, recent task progress, tool output, logs, code snippets, temporary mood or health, secrets/API keys/tokens, or facts already covered by the long-term profile above.",
642
+ "- A memory.md entry must be one generic sentence or fact. Do not include dates or 'today I...' style wording. If unsure, do not write it.",
643
+ "- Do not make a big announcement when memory.md is updated. Keep the normal response natural.",
644
+ "- If the user pasted a spec, idea, or long text, respond conversationally first and do not decide to save it on your own. If needed, ask briefly whether to save it.",
645
+ "- Call read/search skills only when needed to answer the user's question.",
646
+ "",
647
+ "Tool result handling:",
648
+ "- If the immediately previous turn has a tool result (skill-result block), do not emit the same skill-call again. Use the result and give only the short conclusion in the user's language.",
649
+ "- Do not call the same skill with the same arguments more than once for one request. Once it succeeds, stop and tell the user it is done.",
650
+ "- skill-call blocks in history are past call records. Do not copy them. Emit a new call only when truly needed.",
651
+ "- skill-result JSON may include a data field in addition to summary. If a later skill call needs values such as ids, read them from data instead of asking the user.",
652
+ "- If history already contains the same id and args with a status: ok skill-result, it has already run. Do not rerun it; just say it is already done or already registered.",
653
+ "",
654
+ "How to call side-effect skills:",
655
+ "- For skills marked '(side effect)', do not write a preface or completion text in the response body. Output only the skill-call block.",
656
+ "- Do not write narration such as 'I will add it now', 'Added', or 'I will register it' together with a skill-call. The skill result provides the definitive completion text.",
657
+ "- If arguments need confirmation, do not emit a skill-call. Ask briefly in the body, wait for yes, then emit the call in the next response.",
658
+ "",
659
+ "ToDo handling:",
660
+ "- When the user wants to complete/delete a specific ToDo, first call todo-list, identify the matching item id from data.items, then call todo-done or todo-delete with that id. Do not ask the user for the id.",
661
+ "- Ask briefly only when the matching ToDo cannot be narrowed to one item.",
662
+ ];
663
+ const profileLines = formatProfileMemoryPromptSection(profileMemory);
664
+ if (profileLines.length > 0) {
665
+ lines.push("", "Long-term profile:", ...profileLines);
666
+ }
667
+ if (preferredSkillId && skills.some((skill) => skill.id === preferredSkillId)) {
668
+ lines.push(`- This input has been preclassified as intent to run ${preferredSkillId}. If the needed arguments can be inferred from the conversation, call that skill. Ask briefly only when required arguments are missing.`);
669
+ }
670
+ lines.push("", "Available skills:");
671
+ if (skills.length === 0) {
672
+ lines.push(" (none)");
673
+ }
674
+ else {
675
+ for (const skill of skills) {
676
+ const tag = skill.side_effect ? " (side effect)" : "";
677
+ lines.push(`- ${skill.id}${tag}: ${skill.description || skill.name}`);
678
+ const phrases = skill.invocation?.phrases?.filter((p) => typeof p === "string" && p.trim().length > 0) || [];
679
+ if (phrases.length > 0) {
680
+ lines.push(` Example phrases: ${phrases.slice(0, 5).join(" / ")}`);
681
+ }
682
+ lines.push(` Input schema: ${JSON.stringify(skill.input.schema)}`);
683
+ }
684
+ }
685
+ lines.push("");
686
+ lines.push("When calling a skill, include a fenced block in this format:");
687
+ lines.push("```skill-call");
688
+ lines.push('{"id": "<skill-id>", "args": { ... }}');
689
+ lines.push("```");
690
+ lines.push("Multiple blocks are allowed. Run results are passed in the next turn with the `tool` role.");
691
+ lines.push("When asking to create or edit a skill, include exactly one internal block in this format. Without it, build-mode handoff will not happen:");
692
+ lines.push("```agent-sin-build-suggestion");
693
+ lines.push('{"type":"create|edit","skill_id":"<kebab-case-id>","reason":"<short reason>"}');
694
+ lines.push("```");
695
+ lines.push("Conversation mode cannot rewrite files directly. When a skill must be fixed or created, do not write it yourself; ask for confirmation with the block above and hand off to build mode after the user's approval.");
696
+ lines.push("If no skill call is needed, reply naturally in the user's language.");
697
+ return lines.join("\n");
698
+ }
699
+ function skillCallKey(call) {
700
+ return `${call.id}:${stableStringify(call.args)}`;
701
+ }
702
+ function emptyChatFallback() {
703
+ return l("I could not produce a reply. Please send it once more.", "返答を作れませんでした。もう一度送ってください。");
704
+ }
705
+ function emptyReplyRetryPrompt() {
706
+ return "Your previous response was empty. Use the user's latest message and the conversation context to produce a short, useful reply now in the user's language. If a skill call is required, emit a valid skill-call block. If build mode is required, include a short visible confirmation question and the hidden build-suggestion block. Do not return an empty response.";
707
+ }
708
+ function shouldRetryEmptyAssistantReply(assistantText, buildSuggestion) {
709
+ return !buildSuggestion && parseSkillCalls(assistantText).length === 0 && stripSkillCalls(assistantText).trim().length === 0;
710
+ }
711
+ function stableStringify(value) {
712
+ if (value === null || typeof value !== "object") {
713
+ return JSON.stringify(value);
714
+ }
715
+ if (Array.isArray(value)) {
716
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
717
+ }
718
+ const entries = Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
719
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
720
+ }
721
+ export function parseSkillCalls(text) {
722
+ const calls = [];
723
+ SKILL_CALL_PATTERN.lastIndex = 0;
724
+ let match;
725
+ while ((match = SKILL_CALL_PATTERN.exec(text)) !== null) {
726
+ try {
727
+ const parsed = JSON.parse(match[1]);
728
+ if (typeof parsed.id !== "string") {
729
+ continue;
730
+ }
731
+ const args = parsed.args && typeof parsed.args === "object" && !Array.isArray(parsed.args)
732
+ ? parsed.args
733
+ : {};
734
+ calls.push({ id: parsed.id, args });
735
+ }
736
+ catch {
737
+ continue;
738
+ }
739
+ }
740
+ return calls;
741
+ }
742
+ export function stripSkillCalls(text) {
743
+ return text.replace(SKILL_CALL_PATTERN, "").trim();
744
+ }
745
+ export function extractSkillCallBlocks(text) {
746
+ const matches = text.match(SKILL_CALL_PATTERN);
747
+ return matches ? matches.join("\n") : "";
748
+ }
749
+ export function parseBuildSuggestion(text) {
750
+ BUILD_SUGGESTION_PATTERN.lastIndex = 0;
751
+ const match = BUILD_SUGGESTION_PATTERN.exec(text);
752
+ if (!match)
753
+ return null;
754
+ try {
755
+ const parsed = JSON.parse(match[1]);
756
+ const type = parsed.type === "edit" ? "edit" : "create";
757
+ const skillId = sanitizeSuggestedSkillId(String(parsed.skill_id || ""));
758
+ if (!skillId)
759
+ return null;
760
+ return {
761
+ type,
762
+ skill_id: skillId,
763
+ reason: typeof parsed.reason === "string" ? parsed.reason.slice(0, 240) : undefined,
764
+ };
765
+ }
766
+ catch {
767
+ return null;
768
+ }
769
+ }
770
+ export function stripBuildSuggestions(text) {
771
+ return text.replace(BUILD_SUGGESTION_PATTERN, "").trim();
772
+ }
773
+ function sanitizeSuggestedSkillId(raw) {
774
+ return raw
775
+ .toLowerCase()
776
+ .replaceAll("_", "-")
777
+ .replace(/[^a-z0-9-]/g, "-")
778
+ .replace(/-+/g, "-")
779
+ .replace(/^-+|-+$/g, "")
780
+ .slice(0, 48);
781
+ }
782
+ export function toolResultJson(id, status, summary, saved = [], data) {
783
+ const payload = { id, status, summary, saved };
784
+ if (data !== undefined) {
785
+ payload.data = data;
786
+ }
787
+ return ["```skill-result", JSON.stringify(payload), "```"].join("\n");
788
+ }
789
+ export function toAiMessages(history, multimodalTurn) {
790
+ return history.map((turn, index) => {
791
+ const role = turn.role === "tool" ? "tool" : turn.role;
792
+ if (multimodalTurn && index === multimodalTurn.index && turn.role === "user") {
793
+ return {
794
+ role,
795
+ content: [
796
+ { type: "text", text: turn.content },
797
+ ...multimodalTurn.images,
798
+ ],
799
+ };
800
+ }
801
+ return {
802
+ role,
803
+ content: turn.content,
804
+ };
805
+ });
806
+ }
807
+ export function appendHistory(history, turn) {
808
+ history.push(turn);
809
+ while (history.length > HISTORY_LIMIT) {
810
+ history.shift();
811
+ }
812
+ }