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,53 @@
1
+ export type CodexProgressEvent = {
2
+ kind: "thinking";
3
+ text?: string;
4
+ } | {
5
+ kind: "tool";
6
+ name?: string;
7
+ text?: string;
8
+ } | {
9
+ kind: "message";
10
+ text?: string;
11
+ } | {
12
+ kind: "info";
13
+ text: string;
14
+ };
15
+ export type CodexProgressHandler = (event: CodexProgressEvent) => void;
16
+ export type CodexSandboxMode = "read-only" | "workspace-write" | "danger-full-access";
17
+ export type CodexApprovalPolicy = "untrusted" | "on-failure" | "on-request" | "never";
18
+ export interface CodexTurnOptions {
19
+ cwd?: string;
20
+ effort?: "low" | "medium" | "high" | "xhigh";
21
+ sandbox?: CodexSandboxMode;
22
+ approvalPolicy?: CodexApprovalPolicy;
23
+ onProgress?: CodexProgressHandler;
24
+ }
25
+ export interface CodexAppServerOptions {
26
+ bin?: string;
27
+ args?: string[];
28
+ model?: string;
29
+ startupTimeoutMs?: number;
30
+ turnTimeoutMs?: number;
31
+ onStderr?: (chunk: string) => void;
32
+ }
33
+ export declare function getSharedCodexAppServer(model?: string): CodexAppServerSession;
34
+ export declare function shutdownSharedCodexAppServer(): Promise<void>;
35
+ export declare class CodexAppServerSession {
36
+ private child;
37
+ private buffer;
38
+ private nextId;
39
+ private readonly pending;
40
+ private readonly notificationHandlers;
41
+ private starting;
42
+ private readonly options;
43
+ private exitReason;
44
+ constructor(options?: CodexAppServerOptions);
45
+ sendTurn(text: string, options?: CodexTurnOptions): Promise<string>;
46
+ stop(): Promise<void>;
47
+ isRunning(): boolean;
48
+ private ensureStarted;
49
+ private start;
50
+ private consumeStdout;
51
+ private send;
52
+ private runTurn;
53
+ }
@@ -0,0 +1,305 @@
1
+ import { spawn } from "node:child_process";
2
+ import { l } from "../core/i18n.js";
3
+ const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
4
+ const DEFAULT_TURN_TIMEOUT_MS = 5 * 60_000;
5
+ function snippet(value) {
6
+ if (typeof value !== "string") {
7
+ return undefined;
8
+ }
9
+ const flat = value.replaceAll("\n", " ").trim();
10
+ if (!flat) {
11
+ return undefined;
12
+ }
13
+ return flat.length > 80 ? `${flat.slice(0, 77)}…` : flat;
14
+ }
15
+ const singletons = new Map();
16
+ const DEFAULT_KEY = "__default__";
17
+ export function getSharedCodexAppServer(model) {
18
+ const key = model && model.length > 0 ? model : DEFAULT_KEY;
19
+ let session = singletons.get(key);
20
+ if (!session) {
21
+ session = new CodexAppServerSession(model ? { model } : {});
22
+ singletons.set(key, session);
23
+ }
24
+ return session;
25
+ }
26
+ export async function shutdownSharedCodexAppServer() {
27
+ const sessions = [...singletons.values()];
28
+ singletons.clear();
29
+ await Promise.all(sessions.map((session) => session.stop().catch(() => undefined)));
30
+ }
31
+ export class CodexAppServerSession {
32
+ child = null;
33
+ buffer = "";
34
+ nextId = 1;
35
+ pending = new Map();
36
+ notificationHandlers = new Set();
37
+ starting = null;
38
+ options;
39
+ exitReason = null;
40
+ constructor(options = {}) {
41
+ const baseArgs = options.args ? [...options.args] : ["app-server"];
42
+ const model = options.model || process.env.AGENT_SIN_CODEX_MODEL;
43
+ if (model && !baseArgs.includes("--model") && !baseArgs.includes("-m")) {
44
+ baseArgs.push("--model", model);
45
+ }
46
+ this.options = {
47
+ bin: options.bin || process.env.AGENT_SIN_CODEX_BIN || "codex",
48
+ args: baseArgs,
49
+ model,
50
+ startupTimeoutMs: options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS,
51
+ turnTimeoutMs: options.turnTimeoutMs ?? DEFAULT_TURN_TIMEOUT_MS,
52
+ onStderr: options.onStderr,
53
+ };
54
+ }
55
+ async sendTurn(text, options = {}) {
56
+ await this.ensureStarted();
57
+ const startResponse = (await this.send("thread/start", {
58
+ cwd: options.cwd,
59
+ ...(options.sandbox ? { sandbox: options.sandbox } : {}),
60
+ ...(options.approvalPolicy ? { approvalPolicy: options.approvalPolicy } : {}),
61
+ }));
62
+ const threadId = startResponse?.thread?.id;
63
+ if (!threadId) {
64
+ throw new Error("codex app-server: thread/start did not return a thread id");
65
+ }
66
+ return await this.runTurn(threadId, text, options);
67
+ }
68
+ async stop() {
69
+ if (!this.child) {
70
+ return;
71
+ }
72
+ const child = this.child;
73
+ this.child = null;
74
+ this.starting = null;
75
+ this.notificationHandlers.clear();
76
+ for (const pending of this.pending.values()) {
77
+ pending.reject(new Error("codex app-server: shutting down"));
78
+ }
79
+ this.pending.clear();
80
+ child.stdin.end();
81
+ await new Promise((resolve) => {
82
+ const timer = setTimeout(() => {
83
+ child.kill("SIGTERM");
84
+ }, 1000);
85
+ child.once("exit", () => {
86
+ clearTimeout(timer);
87
+ resolve();
88
+ });
89
+ });
90
+ }
91
+ isRunning() {
92
+ return this.child !== null && this.child.exitCode === null;
93
+ }
94
+ async ensureStarted() {
95
+ if (this.isRunning() && !this.starting) {
96
+ return;
97
+ }
98
+ if (this.starting) {
99
+ return this.starting;
100
+ }
101
+ this.starting = this.start().finally(() => {
102
+ this.starting = null;
103
+ });
104
+ await this.starting;
105
+ }
106
+ async start() {
107
+ const child = spawn(this.options.bin, this.options.args, {
108
+ stdio: ["pipe", "pipe", "pipe"],
109
+ });
110
+ this.child = child;
111
+ this.exitReason = null;
112
+ child.stdout.setEncoding("utf8");
113
+ child.stderr.setEncoding("utf8");
114
+ child.stdout.on("data", (chunk) => this.consumeStdout(chunk));
115
+ child.stderr.on("data", (chunk) => {
116
+ if (this.options.onStderr) {
117
+ this.options.onStderr(chunk);
118
+ }
119
+ });
120
+ child.on("exit", (code, signal) => {
121
+ this.exitReason = `exited code=${code} signal=${signal}`;
122
+ const error = new Error(`codex app-server: ${this.exitReason}`);
123
+ for (const pending of this.pending.values()) {
124
+ pending.reject(error);
125
+ }
126
+ this.pending.clear();
127
+ this.notificationHandlers.clear();
128
+ this.child = null;
129
+ });
130
+ child.on("error", (error) => {
131
+ this.exitReason = `spawn error: ${error.message}`;
132
+ for (const pending of this.pending.values()) {
133
+ pending.reject(error);
134
+ }
135
+ this.pending.clear();
136
+ });
137
+ const initTimer = setTimeout(() => {
138
+ child.kill("SIGTERM");
139
+ }, this.options.startupTimeoutMs);
140
+ try {
141
+ await this.send("initialize", {
142
+ clientInfo: { name: "agent-sin", version: "0.1.0" },
143
+ });
144
+ }
145
+ finally {
146
+ clearTimeout(initTimer);
147
+ }
148
+ }
149
+ consumeStdout(chunk) {
150
+ this.buffer += chunk;
151
+ let newlineIndex;
152
+ while ((newlineIndex = this.buffer.indexOf("\n")) >= 0) {
153
+ const line = this.buffer.slice(0, newlineIndex);
154
+ this.buffer = this.buffer.slice(newlineIndex + 1);
155
+ if (!line.trim()) {
156
+ continue;
157
+ }
158
+ let message;
159
+ try {
160
+ message = JSON.parse(line);
161
+ }
162
+ catch {
163
+ continue;
164
+ }
165
+ if (message.id !== undefined && this.pending.has(message.id)) {
166
+ const handler = this.pending.get(message.id);
167
+ this.pending.delete(message.id);
168
+ if (handler) {
169
+ if (message.error) {
170
+ handler.reject(new Error(`codex app-server error: ${message.error.message || JSON.stringify(message.error)}`));
171
+ }
172
+ else {
173
+ handler.resolve(message.result);
174
+ }
175
+ }
176
+ continue;
177
+ }
178
+ if (message.method) {
179
+ for (const handler of this.notificationHandlers) {
180
+ try {
181
+ handler(message.method, message.params);
182
+ }
183
+ catch {
184
+ // Notification handlers should not throw, but we ignore if they do.
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+ send(method, params) {
191
+ if (!this.child) {
192
+ return Promise.reject(new Error("codex app-server: not running"));
193
+ }
194
+ return new Promise((resolve, reject) => {
195
+ const id = this.nextId;
196
+ this.nextId += 1;
197
+ this.pending.set(id, { resolve, reject });
198
+ const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} });
199
+ this.child.stdin.write(`${payload}\n`, (error) => {
200
+ if (error) {
201
+ this.pending.delete(id);
202
+ reject(error);
203
+ }
204
+ });
205
+ });
206
+ }
207
+ runTurn(threadId, text, options) {
208
+ return new Promise((resolve, reject) => {
209
+ let assistantText = "";
210
+ let settled = false;
211
+ const emit = (event) => {
212
+ if (!options.onProgress) {
213
+ return;
214
+ }
215
+ try {
216
+ options.onProgress(event);
217
+ }
218
+ catch {
219
+ // Progress handlers must not throw; ignore.
220
+ }
221
+ };
222
+ const handler = (method, params) => {
223
+ if (settled || !params) {
224
+ return;
225
+ }
226
+ const eventThreadId = params.threadId;
227
+ if (eventThreadId && eventThreadId !== threadId) {
228
+ return;
229
+ }
230
+ if (method === "item/started" || method === "item/updated") {
231
+ const item = params.item;
232
+ if (item) {
233
+ if (item.type === "agentMessage") {
234
+ emit({ kind: "message", text: snippet(item.text) });
235
+ }
236
+ else if (item.type === "reasoning" || item.type === "thinking") {
237
+ emit({ kind: "thinking", text: snippet(item.text) });
238
+ }
239
+ else if (item.type === "command" ||
240
+ item.type === "command_execution" ||
241
+ item.type === "tool_call" ||
242
+ item.type === "tool_use") {
243
+ emit({ kind: "tool", name: item.name || item.type, text: snippet(item.text) });
244
+ }
245
+ else if (item.type) {
246
+ emit({ kind: "info", text: `${item.type}${item.name ? `: ${item.name}` : ""}` });
247
+ }
248
+ }
249
+ }
250
+ else if (method === "item/completed") {
251
+ const item = params.item;
252
+ if (item && item.type === "agentMessage" && typeof item.text === "string") {
253
+ assistantText = item.text;
254
+ emit({ kind: "message", text: snippet(item.text) });
255
+ }
256
+ else if (item && (item.type === "command" || item.type === "tool_call")) {
257
+ emit({ kind: "tool", name: item.name || item.type, text: l("done", "完了") });
258
+ }
259
+ }
260
+ else if (method === "turn/completed") {
261
+ const turn = params.turn;
262
+ if (turn && Array.isArray(turn.items)) {
263
+ const fromTurn = turn.items
264
+ .filter((entry) => entry && entry.type === "agentMessage" && typeof entry.text === "string")
265
+ .map((entry) => entry.text)
266
+ .join("\n");
267
+ if (fromTurn) {
268
+ assistantText = fromTurn;
269
+ }
270
+ }
271
+ finish(true, undefined);
272
+ }
273
+ else if (method === "turn/failed" || method === "error") {
274
+ const reason = JSON.stringify(params).slice(0, 400);
275
+ finish(false, new Error(`codex app-server: turn failed: ${reason}`));
276
+ }
277
+ };
278
+ const finish = (ok, error) => {
279
+ if (settled) {
280
+ return;
281
+ }
282
+ settled = true;
283
+ clearTimeout(timeout);
284
+ this.notificationHandlers.delete(handler);
285
+ if (ok) {
286
+ resolve(assistantText);
287
+ }
288
+ else {
289
+ reject(error || new Error("codex app-server: unknown failure"));
290
+ }
291
+ };
292
+ const timeout = setTimeout(() => {
293
+ finish(false, new Error(`codex app-server: turn timed out after ${this.options.turnTimeoutMs}ms`));
294
+ }, this.options.turnTimeoutMs);
295
+ this.notificationHandlers.add(handler);
296
+ this.send("turn/start", {
297
+ threadId,
298
+ input: [{ type: "text", text }],
299
+ ...(options.effort ? { effort: options.effort } : {}),
300
+ }).catch((sendError) => {
301
+ finish(false, sendError instanceof Error ? sendError : new Error(String(sendError)));
302
+ });
303
+ });
304
+ }
305
+ }
@@ -0,0 +1,7 @@
1
+ import { type SkillManifest } from "../core/skill-registry.js";
2
+ import type { SkillInput } from "../skills-sdk/types.js";
3
+ import type { SkillExecution } from "../core/runtime.js";
4
+ import type { AppConfig } from "../core/config.js";
5
+ export declare function candidatePythonInterpreters(config: AppConfig, platform?: NodeJS.Platform): string[];
6
+ export declare function resolvePythonInterpreter(config: AppConfig): string;
7
+ export declare function runPythonSkill(config: AppConfig, manifest: SkillManifest, input: SkillInput): Promise<SkillExecution>;
@@ -0,0 +1,302 @@
1
+ import { spawn } from "node:child_process";
2
+ import { statSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { resolveSkillEntryPath } from "../core/skill-registry.js";
5
+ import { getAiProvider } from "../core/ai-provider.js";
6
+ import { notify as runNotify } from "../core/notifier.js";
7
+ const AI_REQUEST_PREFIX = "AGENT_SIN_AI_REQUEST::";
8
+ const AI_RESPONSE_PREFIX = "AGENT_SIN_AI_RESPONSE::";
9
+ const NOTIFY_REQUEST_PREFIX = "AGENT_SIN_NOTIFY_REQUEST::";
10
+ const NOTIFY_RESPONSE_PREFIX = "AGENT_SIN_NOTIFY_RESPONSE::";
11
+ const CTX_LOG_PATTERN = /^\[(info|warn|error)\]\s+([\s\S]*)$/;
12
+ export function candidatePythonInterpreters(config, platform = process.platform) {
13
+ const venvDir = path.join(config.workspace, ".venv");
14
+ const fallback = platform === "win32" ? "python" : "python3";
15
+ if (platform === "win32") {
16
+ return [
17
+ path.join(venvDir, "Scripts", "python.exe"),
18
+ path.join(venvDir, "Scripts", "python"),
19
+ fallback,
20
+ ];
21
+ }
22
+ return [path.join(venvDir, "bin", "python"), fallback];
23
+ }
24
+ export function resolvePythonInterpreter(config) {
25
+ if (process.env.AGENT_SIN_PYTHON) {
26
+ return process.env.AGENT_SIN_PYTHON;
27
+ }
28
+ const candidates = candidatePythonInterpreters(config);
29
+ for (const candidate of candidates.slice(0, -1)) {
30
+ try {
31
+ const info = statSync(candidate);
32
+ if (info.isFile()) {
33
+ return candidate;
34
+ }
35
+ }
36
+ catch {
37
+ // Candidate not present; try the next one.
38
+ }
39
+ }
40
+ return candidates[candidates.length - 1] || "python3";
41
+ }
42
+ export async function runPythonSkill(config, manifest, input) {
43
+ const entry = await resolveSkillEntryPath(manifest);
44
+ const python = resolvePythonInterpreter(config);
45
+ const runtimePolicy = JSON.stringify({
46
+ ai_steps: manifest.ai_steps || [],
47
+ memory: manifest.memory || {},
48
+ });
49
+ const child = spawn(python, ["-c", runnerSource(), entry, manifest.handler, runtimePolicy], {
50
+ stdio: ["pipe", "pipe", "pipe"],
51
+ env: {
52
+ ...process.env,
53
+ PYTHONDONTWRITEBYTECODE: "1",
54
+ },
55
+ });
56
+ const stdout = [];
57
+ const stderrLines = [];
58
+ const ctxLogs = [];
59
+ let stderrBuffer = "";
60
+ child.stdin.on("error", () => { });
61
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
62
+ child.stderr.on("data", (chunk) => {
63
+ stderrBuffer += chunk.toString("utf8");
64
+ let newlineIndex;
65
+ while ((newlineIndex = stderrBuffer.indexOf("\n")) >= 0) {
66
+ const line = stderrBuffer.slice(0, newlineIndex);
67
+ stderrBuffer = stderrBuffer.slice(newlineIndex + 1);
68
+ handleStderrLine(line);
69
+ }
70
+ });
71
+ function handleStderrLine(line) {
72
+ if (line.startsWith(AI_REQUEST_PREFIX)) {
73
+ const payloadJson = line.slice(AI_REQUEST_PREFIX.length);
74
+ handleAiRequest(payloadJson).catch((error) => {
75
+ stderrLines.push(`[ai-error] ${error instanceof Error ? error.message : String(error)}`);
76
+ });
77
+ return;
78
+ }
79
+ if (line.startsWith(NOTIFY_REQUEST_PREFIX)) {
80
+ const payloadJson = line.slice(NOTIFY_REQUEST_PREFIX.length);
81
+ handleNotifyRequest(payloadJson).catch((error) => {
82
+ stderrLines.push(`[notify-error] ${error instanceof Error ? error.message : String(error)}`);
83
+ });
84
+ return;
85
+ }
86
+ const ctxMatch = line.match(CTX_LOG_PATTERN);
87
+ if (ctxMatch) {
88
+ ctxLogs.push({ level: ctxMatch[1], message: ctxMatch[2] });
89
+ return;
90
+ }
91
+ stderrLines.push(line);
92
+ }
93
+ async function handleAiRequest(payloadJson) {
94
+ let request;
95
+ try {
96
+ request = JSON.parse(payloadJson);
97
+ }
98
+ catch (error) {
99
+ child.stdin.write(`${AI_RESPONSE_PREFIX}${JSON.stringify({ id: "?", error: `Invalid AI request JSON: ${String(error)}` })}\n`);
100
+ return;
101
+ }
102
+ try {
103
+ const response = await getAiProvider()(config, {
104
+ model_id: request.model_id,
105
+ messages: request.messages,
106
+ });
107
+ child.stdin.write(`${AI_RESPONSE_PREFIX}${JSON.stringify({ id: request.id, ok: true, response })}\n`);
108
+ }
109
+ catch (error) {
110
+ child.stdin.write(`${AI_RESPONSE_PREFIX}${JSON.stringify({
111
+ id: request.id,
112
+ ok: false,
113
+ error: error instanceof Error ? error.message : String(error),
114
+ })}\n`);
115
+ }
116
+ }
117
+ async function handleNotifyRequest(payloadJson) {
118
+ let request;
119
+ try {
120
+ request = JSON.parse(payloadJson);
121
+ }
122
+ catch (error) {
123
+ child.stdin.write(`${NOTIFY_RESPONSE_PREFIX}${JSON.stringify({ id: "?", ok: false, detail: `Invalid notify request JSON: ${String(error)}` })}\n`);
124
+ return;
125
+ }
126
+ try {
127
+ const args = request.args || {};
128
+ const result = await runNotify({
129
+ title: String(args.title ?? ""),
130
+ body: String(args.body ?? ""),
131
+ subtitle: typeof args.subtitle === "string" ? args.subtitle : undefined,
132
+ sound: Boolean(args.sound),
133
+ channel: typeof args.channel === "string" ? args.channel : undefined,
134
+ to: typeof args.to === "string" ? args.to : undefined,
135
+ discordThreadId: typeof args.discordThreadId === "string" ? args.discordThreadId : undefined,
136
+ telegramThreadId: typeof args.telegramThreadId === "string" ? args.telegramThreadId : undefined,
137
+ });
138
+ child.stdin.write(`${NOTIFY_RESPONSE_PREFIX}${JSON.stringify({ id: request.id, ok: result.ok, channel: result.channel, detail: result.detail })}\n`);
139
+ }
140
+ catch (error) {
141
+ child.stdin.write(`${NOTIFY_RESPONSE_PREFIX}${JSON.stringify({
142
+ id: request.id,
143
+ ok: false,
144
+ detail: error instanceof Error ? error.message : String(error),
145
+ })}\n`);
146
+ }
147
+ }
148
+ child.stdin.write(JSON.stringify(input));
149
+ child.stdin.write("\n");
150
+ const code = await new Promise((resolve, reject) => {
151
+ child.on("error", reject);
152
+ child.on("close", resolve);
153
+ });
154
+ if (stderrBuffer) {
155
+ handleStderrLine(stderrBuffer);
156
+ stderrBuffer = "";
157
+ }
158
+ const err = stderrLines.join("\n").trim();
159
+ const out = Buffer.concat(stdout).toString("utf8").trim();
160
+ if (code !== 0) {
161
+ throw new Error(err || `Python skill exited with code ${code}`);
162
+ }
163
+ try {
164
+ const parsed = JSON.parse(out);
165
+ if (isRunnerEnvelope(parsed)) {
166
+ return {
167
+ result: parsed.result,
168
+ memory_updates: parsed.memory_updates,
169
+ logs: ctxLogs,
170
+ };
171
+ }
172
+ return { result: parsed, logs: ctxLogs };
173
+ }
174
+ catch {
175
+ throw new Error(`Python skill did not return valid JSON${err ? `: ${err}` : ""}`);
176
+ }
177
+ }
178
+ function isRunnerEnvelope(value) {
179
+ return Boolean(value) && typeof value === "object" && value.__agent_sin_runner === 1;
180
+ }
181
+ function runnerSource() {
182
+ return String.raw `
183
+ import asyncio
184
+ import importlib.util
185
+ import json
186
+ import sys
187
+ import uuid
188
+ from datetime import datetime, timezone
189
+
190
+ skill_path = sys.argv[1]
191
+ handler_name = sys.argv[2]
192
+ policy = json.loads(sys.argv[3])
193
+ payload = json.loads(sys.stdin.readline())
194
+ allowed_ai_steps = {step.get("id"): step for step in policy.get("ai_steps", [])}
195
+ memory_policy = policy.get("memory") or {}
196
+
197
+ class Log:
198
+ def info(self, message):
199
+ print(f"[info] {message}", file=sys.stderr, flush=True)
200
+ def warn(self, message):
201
+ print(f"[warn] {message}", file=sys.stderr, flush=True)
202
+ def error(self, message):
203
+ print(f"[error] {message}", file=sys.stderr, flush=True)
204
+
205
+ class AI:
206
+ async def run(self, step_id, ai_payload):
207
+ step = allowed_ai_steps.get(step_id)
208
+ if not step:
209
+ raise RuntimeError(f"AI step is not defined for this skill: {step_id}")
210
+ model_id = step.get("model") or ""
211
+ if not model_id:
212
+ raise RuntimeError(f"AI step has no model: {step_id}")
213
+ prompt = ai_payload if isinstance(ai_payload, str) else json.dumps(ai_payload, ensure_ascii=False)
214
+ messages = [
215
+ {"role": "system", "content": f'You are an AI step "{step_id}" of an Agent-Sin skill. Purpose: ' + (step.get("purpose") or "")},
216
+ {"role": "user", "content": prompt},
217
+ ]
218
+ request_id = str(uuid.uuid4())
219
+ request = {"id": request_id, "step_id": step_id, "model_id": model_id, "messages": messages}
220
+ print("AGENT_SIN_AI_REQUEST::" + json.dumps(request, ensure_ascii=False), file=sys.stderr, flush=True)
221
+ line = sys.stdin.readline()
222
+ if not line:
223
+ raise RuntimeError("AI provider channel closed")
224
+ marker = "AGENT_SIN_AI_RESPONSE::"
225
+ idx = line.find(marker)
226
+ if idx < 0:
227
+ raise RuntimeError(f"Unexpected AI response: {line.strip()}")
228
+ envelope = json.loads(line[idx + len(marker):])
229
+ if not envelope.get("ok"):
230
+ error_message = envelope.get("error") or "AI provider error"
231
+ if step.get("optional"):
232
+ return {"status": "skipped", "step_id": step_id, "reason": error_message, "payload": ai_payload}
233
+ raise RuntimeError(error_message)
234
+ response = envelope.get("response") or {}
235
+ return {
236
+ "status": "ok",
237
+ "step_id": step_id,
238
+ "model_id": response.get("model_id"),
239
+ "provider": response.get("provider"),
240
+ "text": response.get("text", ""),
241
+ }
242
+
243
+ class Memory:
244
+ def __init__(self):
245
+ self.values = dict(payload.get("memory") or {})
246
+ self.updates = {}
247
+ async def get(self, key):
248
+ if not memory_policy.get("read"):
249
+ return None
250
+ return self.values.get(key)
251
+ async def set(self, key, value):
252
+ if not memory_policy.get("write"):
253
+ raise RuntimeError("Memory write is not allowed for this skill")
254
+ self.values[key] = value
255
+ self.updates[key] = value
256
+ return True
257
+
258
+ async def notify_call(args):
259
+ if not isinstance(args, dict):
260
+ raise RuntimeError("ctx.notify requires a dict argument")
261
+ request_id = str(uuid.uuid4())
262
+ request = {"id": request_id, "args": args}
263
+ print("AGENT_SIN_NOTIFY_REQUEST::" + json.dumps(request, ensure_ascii=False), file=sys.stderr, flush=True)
264
+ line = sys.stdin.readline()
265
+ if not line:
266
+ raise RuntimeError("Notify channel closed")
267
+ marker = "AGENT_SIN_NOTIFY_RESPONSE::"
268
+ idx = line.find(marker)
269
+ if idx < 0:
270
+ raise RuntimeError(f"Unexpected notify response: {line.strip()}")
271
+ envelope = json.loads(line[idx + len(marker):])
272
+ return {
273
+ "ok": bool(envelope.get("ok")),
274
+ "channel": envelope.get("channel"),
275
+ "detail": envelope.get("detail"),
276
+ }
277
+
278
+ class Ctx:
279
+ def __init__(self):
280
+ self.log = Log()
281
+ self.ai = AI()
282
+ self.memory = Memory()
283
+ def now(self):
284
+ return datetime.now(timezone.utc).isoformat()
285
+ async def notify(self, args):
286
+ return await notify_call(args)
287
+
288
+ spec = importlib.util.spec_from_file_location("agent_sin_skill", skill_path)
289
+ module = importlib.util.module_from_spec(spec)
290
+ spec.loader.exec_module(module)
291
+ handler = getattr(module, handler_name)
292
+ ctx = Ctx()
293
+ result = handler(ctx, payload)
294
+ if asyncio.iscoroutine(result):
295
+ result = asyncio.run(result)
296
+ print(json.dumps({
297
+ "__agent_sin_runner": 1,
298
+ "result": result,
299
+ "memory_updates": ctx.memory.updates
300
+ }, ensure_ascii=False))
301
+ `;
302
+ }
@@ -0,0 +1,5 @@
1
+ import { type SkillManifest } from "../core/skill-registry.js";
2
+ import type { SkillInput } from "../skills-sdk/types.js";
3
+ import type { SkillExecution } from "../core/runtime.js";
4
+ import type { AppConfig } from "../core/config.js";
5
+ export declare function runTypeScriptSkill(config: AppConfig, manifest: SkillManifest, input: SkillInput): Promise<SkillExecution>;