@alejandroroman/agent-kit 0.1.4 → 0.2.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.
- package/dist/_memory/dist/server.js +0 -0
- package/dist/_memory/server.js +0 -0
- package/dist/agent/loop.js +210 -111
- package/dist/api/errors.d.ts +3 -0
- package/dist/api/errors.js +37 -0
- package/dist/api/events.d.ts +5 -0
- package/dist/api/events.js +28 -0
- package/dist/api/router.js +10 -0
- package/dist/api/traces.d.ts +3 -0
- package/dist/api/traces.js +35 -0
- package/dist/api/types.d.ts +2 -0
- package/dist/bootstrap.d.ts +3 -1
- package/dist/bootstrap.js +26 -7
- package/dist/cli/chat.js +3 -1
- package/dist/cli/claude-md-template.d.ts +5 -0
- package/dist/cli/claude-md-template.js +220 -0
- package/dist/cli/config-writer.js +3 -0
- package/dist/cli/env.d.ts +14 -0
- package/dist/cli/env.js +68 -0
- package/dist/cli/init.js +10 -0
- package/dist/cli/slack-setup.d.ts +6 -0
- package/dist/cli/slack-setup.js +234 -0
- package/dist/cli/start.js +65 -16
- package/dist/cli/ui.d.ts +2 -0
- package/dist/cli/ui.js +4 -1
- package/dist/cli/whats-new.d.ts +1 -0
- package/dist/cli/whats-new.js +69 -0
- package/dist/cli.js +14 -0
- package/dist/config/resolve.d.ts +1 -0
- package/dist/config/resolve.js +1 -0
- package/dist/config/schema.d.ts +2 -0
- package/dist/config/schema.js +1 -0
- package/dist/config/writer.d.ts +18 -0
- package/dist/config/writer.js +85 -0
- package/dist/cron/scheduler.d.ts +4 -1
- package/dist/cron/scheduler.js +99 -52
- package/dist/gateways/slack/client.d.ts +1 -0
- package/dist/gateways/slack/client.js +9 -0
- package/dist/gateways/slack/handler.js +2 -1
- package/dist/gateways/slack/index.js +75 -29
- package/dist/gateways/slack/listener.d.ts +8 -1
- package/dist/gateways/slack/listener.js +36 -10
- package/dist/heartbeat/runner.js +99 -82
- package/dist/llm/anthropic.d.ts +1 -0
- package/dist/llm/anthropic.js +11 -2
- package/dist/llm/fallback.js +34 -2
- package/dist/llm/openai.d.ts +2 -0
- package/dist/llm/openai.js +33 -2
- package/dist/llm/types.d.ts +16 -2
- package/dist/llm/types.js +9 -0
- package/dist/logger.js +8 -0
- package/dist/media/sanitize.d.ts +5 -0
- package/dist/media/sanitize.js +53 -0
- package/dist/multi/spawn.js +29 -10
- package/dist/session/compaction.js +3 -1
- package/dist/session/prune-images.d.ts +9 -0
- package/dist/session/prune-images.js +42 -0
- package/dist/skills/activate.d.ts +6 -0
- package/dist/skills/activate.js +72 -27
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/telemetry/db.d.ts +63 -0
- package/dist/telemetry/db.js +193 -0
- package/dist/telemetry/index.d.ts +17 -0
- package/dist/telemetry/index.js +82 -0
- package/dist/telemetry/sanitize.d.ts +6 -0
- package/dist/telemetry/sanitize.js +48 -0
- package/dist/telemetry/sqlite-processor.d.ts +11 -0
- package/dist/telemetry/sqlite-processor.js +108 -0
- package/dist/telemetry/types.d.ts +30 -0
- package/dist/telemetry/types.js +31 -0
- package/dist/tools/builtin/index.d.ts +2 -0
- package/dist/tools/builtin/index.js +2 -0
- package/dist/tools/builtin/self-config.d.ts +4 -0
- package/dist/tools/builtin/self-config.js +182 -0
- package/package.json +25 -18
package/dist/cli/start.js
CHANGED
|
@@ -7,13 +7,38 @@ import { createLogger } from "../logger.js";
|
|
|
7
7
|
import { buildAgentRuntime, initUsageStore, createAgentExecutor, } from "../bootstrap.js";
|
|
8
8
|
import { ensureOllama } from "./ollama.js";
|
|
9
9
|
import { resolveApiKey } from "./ui.js";
|
|
10
|
+
import { showWhatsNew } from "./whats-new.js";
|
|
11
|
+
import { ConfigWriter } from "../config/writer.js";
|
|
10
12
|
import { CONFIG_PATH, DATA_DIR, SKILLS_DIR } from "./paths.js";
|
|
11
13
|
import { startRepl } from "./repl.js";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
import { loadEnvIntoProcess } from "./env.js";
|
|
16
|
+
import { initTelemetry, getTelemetryDb } from "../telemetry/index.js";
|
|
12
17
|
const log = createLogger("start");
|
|
13
18
|
const DEFAULT_AGENT = "default";
|
|
14
19
|
export async function start() {
|
|
15
20
|
await resolveApiKey({ save: false });
|
|
21
|
+
showWhatsNew();
|
|
22
|
+
// Telemetry (OpenTelemetry + Sentry)
|
|
23
|
+
const shutdownTelemetry = initTelemetry({
|
|
24
|
+
dbPath: path.join(DATA_DIR, "telemetry.db"),
|
|
25
|
+
});
|
|
26
|
+
// Prune telemetry data older than 30 days
|
|
27
|
+
const telDb = getTelemetryDb();
|
|
28
|
+
if (telDb) {
|
|
29
|
+
try {
|
|
30
|
+
telDb.cleanup(30);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
log.warn({ err }, "telemetry cleanup failed");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Load Slack tokens from .env (if not already in process.env)
|
|
37
|
+
const envPath = path.join(process.cwd(), ".env");
|
|
38
|
+
loadEnvIntoProcess(envPath, ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"]);
|
|
16
39
|
const config = loadConfig(CONFIG_PATH);
|
|
40
|
+
const configWriter = new ConfigWriter(CONFIG_PATH);
|
|
41
|
+
configWriter.watch();
|
|
17
42
|
// Ollama check (warn only)
|
|
18
43
|
if (config.defaults.memory) {
|
|
19
44
|
const ollama = await ensureOllama();
|
|
@@ -27,6 +52,7 @@ export async function start() {
|
|
|
27
52
|
configPath: CONFIG_PATH,
|
|
28
53
|
usageStore,
|
|
29
54
|
dataDir: DATA_DIR,
|
|
55
|
+
telemetryDb: getTelemetryDb(),
|
|
30
56
|
}, apiPort);
|
|
31
57
|
// Build primary agent runtime (for REPL)
|
|
32
58
|
const agentNames = Object.keys(config.agents);
|
|
@@ -36,17 +62,18 @@ export async function start() {
|
|
|
36
62
|
console.error(`Agent "${agentName}" not found. Available: ${names}`);
|
|
37
63
|
process.exit(1);
|
|
38
64
|
}
|
|
39
|
-
const runtime = buildAgentRuntime(agentName, config, {
|
|
65
|
+
const runtime = await buildAgentRuntime(agentName, config, {
|
|
40
66
|
dataDir: DATA_DIR,
|
|
41
67
|
skillsDir: SKILLS_DIR,
|
|
42
68
|
usageStore,
|
|
69
|
+
configPath: CONFIG_PATH,
|
|
43
70
|
});
|
|
44
71
|
const warnings = runtime.toolRegistry.validateAgents(config.agents);
|
|
45
72
|
for (const w of warnings)
|
|
46
73
|
log.warn(w);
|
|
47
74
|
const { resolved, toolRegistry, agentRegistry, promptFragments, skillsIndex, soul, session } = runtime;
|
|
48
75
|
// Cron scheduler
|
|
49
|
-
const scheduler = new CronScheduler(config, toolRegistry, agentRegistry, DATA_DIR, SKILLS_DIR, usageStore);
|
|
76
|
+
const scheduler = new CronScheduler(config, toolRegistry, agentRegistry, DATA_DIR, SKILLS_DIR, usageStore, configWriter);
|
|
50
77
|
const enabledJobs = scheduler.getJobs().filter((j) => j.enabled);
|
|
51
78
|
// Agent executor for Slack
|
|
52
79
|
const executeAgent = createAgentExecutor(config, {
|
|
@@ -55,10 +82,14 @@ export async function start() {
|
|
|
55
82
|
agentRegistry,
|
|
56
83
|
usageStore,
|
|
57
84
|
source: "slack",
|
|
85
|
+
configPath: CONFIG_PATH,
|
|
58
86
|
});
|
|
59
87
|
// Slack gateway
|
|
60
88
|
let gateway;
|
|
61
89
|
const hasSlackBindings = Object.values(config.agents).some((a) => a.slack);
|
|
90
|
+
if (hasSlackBindings && (!process.env.SLACK_BOT_TOKEN || !process.env.SLACK_APP_TOKEN)) {
|
|
91
|
+
log.warn("Slack bindings configured but SLACK_BOT_TOKEN/SLACK_APP_TOKEN not found — run `agent-kit slack-setup`");
|
|
92
|
+
}
|
|
62
93
|
if (hasSlackBindings && process.env.SLACK_BOT_TOKEN && process.env.SLACK_APP_TOKEN) {
|
|
63
94
|
try {
|
|
64
95
|
gateway = createSlackGateway(config, { onAgentRequest: executeAgent });
|
|
@@ -79,22 +110,35 @@ export async function start() {
|
|
|
79
110
|
log.warn({ err }, "failed to start API server");
|
|
80
111
|
}
|
|
81
112
|
// Cron
|
|
113
|
+
const cronCallbacks = {
|
|
114
|
+
onResult: (jobId, agentName, result) => {
|
|
115
|
+
log.info({ jobId, tokens: result.usage.inputTokens + result.usage.outputTokens }, "cron job completed");
|
|
116
|
+
process.stdout.write("You: ");
|
|
117
|
+
const channelOverride = config.cron.find((j) => j.id === jobId)?.slack?.channelId;
|
|
118
|
+
gateway?.onJobResult(agentName, jobId, result, channelOverride).catch((err) => log.warn({ err, jobId }, "failed to post cron result to Slack"));
|
|
119
|
+
},
|
|
120
|
+
onError: (jobId, agentName, error) => {
|
|
121
|
+
log.error({ err: error, jobId }, "cron job failed");
|
|
122
|
+
process.stdout.write("You: ");
|
|
123
|
+
const channelOverride = config.cron.find((j) => j.id === jobId)?.slack?.channelId;
|
|
124
|
+
gateway?.onJobError(agentName, jobId, error, channelOverride).catch((err) => log.warn({ err, jobId }, "failed to post cron error to Slack"));
|
|
125
|
+
},
|
|
126
|
+
};
|
|
82
127
|
if (enabledJobs.length > 0) {
|
|
83
|
-
scheduler.start(
|
|
84
|
-
onResult: (jobId, agentName, result) => {
|
|
85
|
-
log.info({ jobId, tokens: result.usage.inputTokens + result.usage.outputTokens }, "cron job completed");
|
|
86
|
-
process.stdout.write("You: ");
|
|
87
|
-
const channelOverride = config.cron.find((j) => j.id === jobId)?.slack?.channelId;
|
|
88
|
-
gateway?.onJobResult(agentName, jobId, result, channelOverride).catch((err) => log.warn({ err, jobId }, "failed to post cron result to Slack"));
|
|
89
|
-
},
|
|
90
|
-
onError: (jobId, agentName, error) => {
|
|
91
|
-
log.error({ err: error, jobId }, "cron job failed");
|
|
92
|
-
process.stdout.write("You: ");
|
|
93
|
-
const channelOverride = config.cron.find((j) => j.id === jobId)?.slack?.channelId;
|
|
94
|
-
gateway?.onJobError(agentName, jobId, error, channelOverride).catch((err) => log.warn({ err, jobId }, "failed to post cron error to Slack"));
|
|
95
|
-
},
|
|
96
|
-
});
|
|
128
|
+
scheduler.start(cronCallbacks);
|
|
97
129
|
}
|
|
130
|
+
// Hot-reload: restart cron when config changes
|
|
131
|
+
configWriter.on("config:changed", () => {
|
|
132
|
+
try {
|
|
133
|
+
const newConfig = loadConfig(CONFIG_PATH);
|
|
134
|
+
scheduler.reload(newConfig);
|
|
135
|
+
scheduler.start(cronCallbacks);
|
|
136
|
+
log.info("hot-reload: cron scheduler restarted");
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
log.error({ err }, "hot-reload: failed to reload config");
|
|
140
|
+
}
|
|
141
|
+
});
|
|
98
142
|
// Heartbeat
|
|
99
143
|
const heartbeat = new HeartbeatRunner(config, agentRegistry, DATA_DIR, SKILLS_DIR, usageStore);
|
|
100
144
|
const heartbeatAgents = heartbeat.getHeartbeatAgents();
|
|
@@ -176,6 +220,7 @@ export async function start() {
|
|
|
176
220
|
},
|
|
177
221
|
},
|
|
178
222
|
onQuit: async () => {
|
|
223
|
+
configWriter.stopWatching();
|
|
179
224
|
scheduler.stop();
|
|
180
225
|
heartbeat.stop();
|
|
181
226
|
await gateway?.stop();
|
|
@@ -184,6 +229,10 @@ export async function start() {
|
|
|
184
229
|
usageStore?.close();
|
|
185
230
|
}
|
|
186
231
|
catch { }
|
|
232
|
+
await Promise.race([
|
|
233
|
+
shutdownTelemetry(),
|
|
234
|
+
new Promise((r) => setTimeout(r, 5000)),
|
|
235
|
+
]);
|
|
187
236
|
},
|
|
188
237
|
});
|
|
189
238
|
}
|
package/dist/cli/ui.d.ts
CHANGED
package/dist/cli/ui.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
1
2
|
import * as fs from "fs";
|
|
2
3
|
import * as path from "path";
|
|
3
4
|
import * as p from "@clack/prompts";
|
|
4
|
-
const
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const { version: VERSION } = require("../../package.json");
|
|
7
|
+
export { VERSION };
|
|
5
8
|
export function banner() {
|
|
6
9
|
console.log();
|
|
7
10
|
p.intro(`agent-kit v${VERSION}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function showWhatsNew(): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { VERSION } from "./ui.js";
|
|
5
|
+
const RELEASE_NOTES = {
|
|
6
|
+
"0.1.6": [
|
|
7
|
+
"\"What's new\" notification after CLI updates",
|
|
8
|
+
"Auto-activate skills per agent",
|
|
9
|
+
"Hot-reload on external config file changes",
|
|
10
|
+
],
|
|
11
|
+
"0.1.5": [
|
|
12
|
+
"Agent self-configuration tools",
|
|
13
|
+
"Bookkeeper: bump maxIterations, filter uncategorized by year",
|
|
14
|
+
"Generate CLAUDE.md and fix pnpm native deps on init",
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
const VERSION_FILE = path.join(os.homedir(), ".agent-kit", ".last-version");
|
|
18
|
+
function getLastSeenVersion() {
|
|
19
|
+
try {
|
|
20
|
+
return fs.readFileSync(VERSION_FILE, "utf-8").trim() || null;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function setLastSeenVersion(version) {
|
|
27
|
+
try {
|
|
28
|
+
fs.mkdirSync(path.dirname(VERSION_FILE), { recursive: true });
|
|
29
|
+
fs.writeFileSync(VERSION_FILE, version);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// never crash startup
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function showWhatsNew() {
|
|
36
|
+
try {
|
|
37
|
+
const last = getLastSeenVersion();
|
|
38
|
+
// First run — just record the version, don't spam
|
|
39
|
+
if (last === null) {
|
|
40
|
+
setLastSeenVersion(VERSION);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (last === VERSION)
|
|
44
|
+
return;
|
|
45
|
+
// Collect notes for versions newer than last-seen
|
|
46
|
+
const entries = [];
|
|
47
|
+
for (const [ver, notes] of Object.entries(RELEASE_NOTES)) {
|
|
48
|
+
if (ver > last)
|
|
49
|
+
entries.push([ver, notes]);
|
|
50
|
+
}
|
|
51
|
+
if (entries.length > 0) {
|
|
52
|
+
// Sort descending
|
|
53
|
+
entries.sort((a, b) => (b[0] > a[0] ? 1 : -1));
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(" \u2728 What's new in agent-kit");
|
|
56
|
+
for (const [ver, notes] of entries) {
|
|
57
|
+
console.log(`\n v${ver}`);
|
|
58
|
+
for (const note of notes) {
|
|
59
|
+
console.log(` • ${note}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
console.log();
|
|
63
|
+
}
|
|
64
|
+
setLastSeenVersion(VERSION);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// never crash startup
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -29,6 +29,19 @@ async function main() {
|
|
|
29
29
|
await chat(args[0]);
|
|
30
30
|
break;
|
|
31
31
|
}
|
|
32
|
+
case "slack-setup": {
|
|
33
|
+
const fs = await import("fs");
|
|
34
|
+
const { setupSlack } = await import("./cli/slack-setup.js");
|
|
35
|
+
const { banner } = await import("./cli/ui.js");
|
|
36
|
+
const { CONFIG_PATH } = await import("./cli/paths.js");
|
|
37
|
+
banner();
|
|
38
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
39
|
+
console.error("No agent-kit.json found. Run `agent-kit init` first.");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
await setupSlack({ configPath: CONFIG_PATH, fromInit: false });
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
32
45
|
case "validate": {
|
|
33
46
|
const { validate } = await import("./cli/validate.js");
|
|
34
47
|
validate(args[0]);
|
|
@@ -44,6 +57,7 @@ async function main() {
|
|
|
44
57
|
agent-kit create AI-powered agent creation
|
|
45
58
|
agent-kit list Show configured agents
|
|
46
59
|
agent-kit chat <agent> Lightweight single-agent chat
|
|
60
|
+
agent-kit slack-setup Interactive Slack setup
|
|
47
61
|
agent-kit validate [agent] Validate config
|
|
48
62
|
|
|
49
63
|
Options:
|
package/dist/config/resolve.d.ts
CHANGED
package/dist/config/resolve.js
CHANGED
|
@@ -34,6 +34,7 @@ export function resolveAgent(name, config, toolRegistry, skillsDir) {
|
|
|
34
34
|
tools: toolRegistry.resolve(agentDef.tools),
|
|
35
35
|
skills,
|
|
36
36
|
canSpawn: agentDef.can_spawn,
|
|
37
|
+
autoActivateSkills: agentDef.autoActivateSkills,
|
|
37
38
|
maxIterations: agentDef.maxIterations ?? config.defaults.maxIterations,
|
|
38
39
|
maxTokens: agentDef.maxTokens ?? config.defaults.maxTokens,
|
|
39
40
|
compactionThreshold: config.defaults.compactionThreshold,
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -56,6 +56,7 @@ declare const AgentSchema: z.ZodObject<{
|
|
|
56
56
|
model: z.ZodOptional<z.ZodString>;
|
|
57
57
|
tools: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
58
58
|
skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
59
|
+
autoActivateSkills: z.ZodDefault<z.ZodBoolean>;
|
|
59
60
|
spawn_only: z.ZodDefault<z.ZodBoolean>;
|
|
60
61
|
can_spawn: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
61
62
|
agent: z.ZodString;
|
|
@@ -150,6 +151,7 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
150
151
|
model: z.ZodOptional<z.ZodString>;
|
|
151
152
|
tools: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
152
153
|
skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
154
|
+
autoActivateSkills: z.ZodDefault<z.ZodBoolean>;
|
|
153
155
|
spawn_only: z.ZodDefault<z.ZodBoolean>;
|
|
154
156
|
can_spawn: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
155
157
|
agent: z.ZodString;
|
package/dist/config/schema.js
CHANGED
|
@@ -83,6 +83,7 @@ const AgentSchema = z.object({
|
|
|
83
83
|
model: z.string().min(1).optional(),
|
|
84
84
|
tools: z.array(z.string()).default([]),
|
|
85
85
|
skills: z.array(z.string().min(1)).default([]),
|
|
86
|
+
autoActivateSkills: z.boolean().default(false),
|
|
86
87
|
spawn_only: z.boolean().default(false),
|
|
87
88
|
can_spawn: z.array(SpawnTargetSchema).default([]),
|
|
88
89
|
maxIterations: z.number().positive().int().optional(),
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
export declare class ConfigWriter extends EventEmitter {
|
|
3
|
+
private configPath;
|
|
4
|
+
private lockPromise;
|
|
5
|
+
private watcher?;
|
|
6
|
+
private debounceTimer?;
|
|
7
|
+
private lastSelfWriteMs;
|
|
8
|
+
constructor(configPath: string);
|
|
9
|
+
/** Read the current config from disk (fresh read, no cache). */
|
|
10
|
+
read(): Record<string, any>;
|
|
11
|
+
/** Write config atomically (temp file + rename). Emits config:changed. */
|
|
12
|
+
write(config: Record<string, any>): void;
|
|
13
|
+
/** Watch the config file for external changes. Debounced, ignores self-writes. */
|
|
14
|
+
watch(): void;
|
|
15
|
+
stopWatching(): void;
|
|
16
|
+
/** Mutex-protected read-modify-write. The callback mutates config in place. */
|
|
17
|
+
mutate(fn: (config: Record<string, any>) => void): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { EventEmitter } from "events";
|
|
5
|
+
import { createLogger } from "../logger.js";
|
|
6
|
+
const log = createLogger("config:writer");
|
|
7
|
+
export class ConfigWriter extends EventEmitter {
|
|
8
|
+
configPath;
|
|
9
|
+
lockPromise = Promise.resolve();
|
|
10
|
+
watcher;
|
|
11
|
+
debounceTimer;
|
|
12
|
+
lastSelfWriteMs = 0;
|
|
13
|
+
constructor(configPath) {
|
|
14
|
+
super();
|
|
15
|
+
this.configPath = configPath;
|
|
16
|
+
}
|
|
17
|
+
/** Read the current config from disk (fresh read, no cache). */
|
|
18
|
+
read() {
|
|
19
|
+
const raw = fs.readFileSync(this.configPath, "utf-8");
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
/** Write config atomically (temp file + rename). Emits config:changed. */
|
|
23
|
+
write(config) {
|
|
24
|
+
const tmp = path.join(path.dirname(this.configPath), `.agent-kit-${process.pid}-${Date.now()}.tmp`);
|
|
25
|
+
fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + os.EOL);
|
|
26
|
+
try {
|
|
27
|
+
fs.renameSync(tmp, this.configPath);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
try {
|
|
31
|
+
fs.unlinkSync(tmp);
|
|
32
|
+
}
|
|
33
|
+
catch { /* ignore cleanup error */ }
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
log.info("config written");
|
|
37
|
+
this.lastSelfWriteMs = Date.now();
|
|
38
|
+
this.emit("config:changed", config);
|
|
39
|
+
}
|
|
40
|
+
/** Watch the config file for external changes. Debounced, ignores self-writes. */
|
|
41
|
+
watch() {
|
|
42
|
+
if (this.watcher)
|
|
43
|
+
return;
|
|
44
|
+
const DEBOUNCE_MS = 250;
|
|
45
|
+
const SELF_WRITE_WINDOW_MS = 1000;
|
|
46
|
+
this.watcher = fs.watch(this.configPath, () => {
|
|
47
|
+
if (this.debounceTimer)
|
|
48
|
+
clearTimeout(this.debounceTimer);
|
|
49
|
+
this.debounceTimer = setTimeout(() => {
|
|
50
|
+
// Ignore fs events triggered by our own write()
|
|
51
|
+
if (Date.now() - this.lastSelfWriteMs < SELF_WRITE_WINDOW_MS)
|
|
52
|
+
return;
|
|
53
|
+
try {
|
|
54
|
+
const config = this.read();
|
|
55
|
+
log.info("external config change detected");
|
|
56
|
+
this.emit("config:changed", config);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
log.error({ err }, "failed to read config after external change");
|
|
60
|
+
}
|
|
61
|
+
}, DEBOUNCE_MS);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
stopWatching() {
|
|
65
|
+
if (this.debounceTimer)
|
|
66
|
+
clearTimeout(this.debounceTimer);
|
|
67
|
+
this.watcher?.close();
|
|
68
|
+
this.watcher = undefined;
|
|
69
|
+
}
|
|
70
|
+
/** Mutex-protected read-modify-write. The callback mutates config in place. */
|
|
71
|
+
async mutate(fn) {
|
|
72
|
+
const prev = this.lockPromise;
|
|
73
|
+
let resolve;
|
|
74
|
+
this.lockPromise = new Promise((r) => { resolve = r; });
|
|
75
|
+
await prev;
|
|
76
|
+
try {
|
|
77
|
+
const config = this.read();
|
|
78
|
+
fn(config);
|
|
79
|
+
this.write(config);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
resolve();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
package/dist/cron/scheduler.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Config, CronJobDef } from "../config/schema.js";
|
|
|
2
2
|
import type { ToolRegistry } from "../tools/registry.js";
|
|
3
3
|
import type { AgentResult } from "../agent/types.js";
|
|
4
4
|
import type { AgentRegistry } from "../multi/registry.js";
|
|
5
|
+
import type { ConfigWriter } from "../config/writer.js";
|
|
5
6
|
import type { UsageStore } from "../usage/store.js";
|
|
6
7
|
export declare class CronScheduler {
|
|
7
8
|
private tasks;
|
|
@@ -11,12 +12,14 @@ export declare class CronScheduler {
|
|
|
11
12
|
private dataDir;
|
|
12
13
|
private skillsDir;
|
|
13
14
|
private usageStore?;
|
|
14
|
-
|
|
15
|
+
private configWriter?;
|
|
16
|
+
constructor(config: Config, toolRegistry: ToolRegistry, agentRegistry: AgentRegistry, dataDir: string, skillsDir?: string, usageStore?: UsageStore, configWriter?: ConfigWriter);
|
|
15
17
|
getJobs(): CronJobDef[];
|
|
16
18
|
runJob(jobId: string): Promise<AgentResult | undefined>;
|
|
17
19
|
start(callbacks?: {
|
|
18
20
|
onResult?: (jobId: string, agentName: string, result: AgentResult) => void;
|
|
19
21
|
onError?: (jobId: string, agentName: string, error: Error) => void;
|
|
20
22
|
}): void;
|
|
23
|
+
reload(newConfig: Config): void;
|
|
21
24
|
stop(): void;
|
|
22
25
|
}
|
package/dist/cron/scheduler.js
CHANGED
|
@@ -4,10 +4,13 @@ import { setupAgentSession } from "../agent/setup.js";
|
|
|
4
4
|
import { resolveAgent, resolveWebSearch } from "../config/resolve.js";
|
|
5
5
|
import { createBuiltinRegistry } from "../tools/builtin/index.js";
|
|
6
6
|
import { registerSpawnWrappers } from "../tools/builtin/spawn.js";
|
|
7
|
+
import { createUpdateAgentConfigTool, createManageCronTool } from "../tools/builtin/self-config.js";
|
|
7
8
|
import * as path from "path";
|
|
8
|
-
import { createActivateSkillTool } from "../skills/index.js";
|
|
9
|
+
import { createActivateSkillTool, preActivateSkills } from "../skills/index.js";
|
|
9
10
|
import { createLogger } from "../logger.js";
|
|
10
11
|
import { dateContext } from "../text.js";
|
|
12
|
+
import { context, trace, SpanStatusCode } from "@opentelemetry/api";
|
|
13
|
+
import { getTracer, ATTR } from "../telemetry/index.js";
|
|
11
14
|
const log = createLogger("cron");
|
|
12
15
|
export class CronScheduler {
|
|
13
16
|
tasks = [];
|
|
@@ -17,13 +20,15 @@ export class CronScheduler {
|
|
|
17
20
|
dataDir;
|
|
18
21
|
skillsDir;
|
|
19
22
|
usageStore;
|
|
20
|
-
|
|
23
|
+
configWriter;
|
|
24
|
+
constructor(config, toolRegistry, agentRegistry, dataDir, skillsDir, usageStore, configWriter) {
|
|
21
25
|
this.config = config;
|
|
22
26
|
this.toolRegistry = toolRegistry;
|
|
23
27
|
this.agentRegistry = agentRegistry;
|
|
24
28
|
this.dataDir = dataDir;
|
|
25
29
|
this.skillsDir = skillsDir ?? path.join(process.cwd(), "skills");
|
|
26
30
|
this.usageStore = usageStore;
|
|
31
|
+
this.configWriter = configWriter;
|
|
27
32
|
}
|
|
28
33
|
getJobs() {
|
|
29
34
|
return this.config.cron;
|
|
@@ -32,59 +37,96 @@ export class CronScheduler {
|
|
|
32
37
|
const job = this.config.cron.find((j) => j.id === jobId);
|
|
33
38
|
if (!job)
|
|
34
39
|
return undefined;
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
webSearch: resolveWebSearch(job.agent, this.config),
|
|
40
|
+
const tracer = getTracer("cron");
|
|
41
|
+
const span = tracer.startSpan("source.cron", {
|
|
42
|
+
attributes: {
|
|
43
|
+
[ATTR.JOB_ID]: job.id,
|
|
44
|
+
[ATTR.AGENT]: job.agent,
|
|
45
|
+
[ATTR.SCHEDULE]: job.schedule,
|
|
46
|
+
[ATTR.SOURCE]: "cron",
|
|
47
|
+
},
|
|
44
48
|
});
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
const spanCtx = trace.setSpan(context.active(), span);
|
|
50
|
+
try {
|
|
51
|
+
return await context.with(spanCtx, async () => {
|
|
52
|
+
// Resolve sandbox for this job's agent
|
|
53
|
+
const agentDef = this.config.agents[job.agent];
|
|
54
|
+
const sandbox = agentDef?.sandbox ?? this.config.defaults.sandbox;
|
|
55
|
+
const memoryConfig = this.config.defaults.memory;
|
|
56
|
+
const jobRegistry = createBuiltinRegistry({
|
|
57
|
+
allowedCommands: sandbox?.allowedCommands,
|
|
58
|
+
allowedPaths: sandbox?.allowedPaths,
|
|
59
|
+
memoryConfig,
|
|
60
|
+
webSearch: resolveWebSearch(job.agent, this.config),
|
|
61
|
+
});
|
|
62
|
+
const resolved = resolveAgent(job.agent, this.config, jobRegistry, this.skillsDir);
|
|
63
|
+
// Skills setup for this job
|
|
64
|
+
const promptFragments = [];
|
|
65
|
+
let skillsIndex = "";
|
|
66
|
+
if (resolved.skills.length > 0) {
|
|
67
|
+
const ctx = {
|
|
68
|
+
manifests: resolved.skills,
|
|
69
|
+
skillsDir: this.skillsDir,
|
|
70
|
+
toolRegistry: jobRegistry,
|
|
71
|
+
promptFragments,
|
|
72
|
+
activatedSkills: new Set(),
|
|
73
|
+
};
|
|
74
|
+
const activateTool = createActivateSkillTool(ctx);
|
|
75
|
+
jobRegistry.register(activateTool);
|
|
76
|
+
if (resolved.autoActivateSkills) {
|
|
77
|
+
skillsIndex = await preActivateSkills(ctx, activateTool, log);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
skillsIndex = "\n\nYou have the following skills available:\n\n"
|
|
81
|
+
+ resolved.skills.map((s) => `- **${s.name}**: ${s.description}`).join("\n")
|
|
82
|
+
+ "\n\nTo use a skill, call the `activate_skill` tool with the skill name.";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Spawn wrapper registration
|
|
86
|
+
if (resolved.canSpawn.length > 0) {
|
|
87
|
+
registerSpawnWrappers(resolved.canSpawn, this.config, this.agentRegistry, jobRegistry, this.usageStore);
|
|
88
|
+
}
|
|
89
|
+
// Self-config tool registration
|
|
90
|
+
if (this.configWriter && agentDef) {
|
|
91
|
+
if (agentDef.tools.includes("update_agent_config")) {
|
|
92
|
+
jobRegistry.register(createUpdateAgentConfigTool(job.agent, this.configWriter));
|
|
93
|
+
}
|
|
94
|
+
if (agentDef.tools.includes("manage_cron")) {
|
|
95
|
+
jobRegistry.register(createManageCronTool(job.agent, this.configWriter));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const sessionId = `cron-${job.id}`;
|
|
99
|
+
const { soul, session } = setupAgentSession(this.dataDir, job.agent, sessionId);
|
|
100
|
+
const userMsg = { role: "user", content: job.prompt };
|
|
101
|
+
session.append(userMsg);
|
|
102
|
+
const systemPrompt = [soul, dateContext(), skillsIndex, ...promptFragments]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join("\n\n") || undefined;
|
|
105
|
+
const result = await runAgentLoop([userMsg], {
|
|
106
|
+
model: resolved.model,
|
|
107
|
+
fallbacks: resolved.fallbacks,
|
|
108
|
+
systemPrompt,
|
|
109
|
+
toolRegistry: jobRegistry,
|
|
110
|
+
maxIterations: resolved.maxIterations,
|
|
111
|
+
compactionThreshold: resolved.compactionThreshold,
|
|
112
|
+
maxToolResultSize: resolved.maxToolResultSize,
|
|
113
|
+
agentName: job.agent,
|
|
114
|
+
usageStore: this.usageStore,
|
|
115
|
+
source: "cron",
|
|
116
|
+
});
|
|
117
|
+
const lastMsg = result.messages[result.messages.length - 1];
|
|
118
|
+
session.append(lastMsg);
|
|
119
|
+
return result;
|
|
120
|
+
});
|
|
61
121
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
122
|
+
catch (err) {
|
|
123
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) });
|
|
124
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
span.end();
|
|
65
129
|
}
|
|
66
|
-
const sessionId = `cron-${job.id}`;
|
|
67
|
-
const { soul, session } = setupAgentSession(this.dataDir, job.agent, sessionId);
|
|
68
|
-
const userMsg = { role: "user", content: job.prompt };
|
|
69
|
-
session.append(userMsg);
|
|
70
|
-
const systemPrompt = [soul, dateContext(), skillsIndex, ...promptFragments]
|
|
71
|
-
.filter(Boolean)
|
|
72
|
-
.join("\n\n") || undefined;
|
|
73
|
-
const result = await runAgentLoop([userMsg], {
|
|
74
|
-
model: resolved.model,
|
|
75
|
-
fallbacks: resolved.fallbacks,
|
|
76
|
-
systemPrompt,
|
|
77
|
-
toolRegistry: jobRegistry,
|
|
78
|
-
maxIterations: resolved.maxIterations,
|
|
79
|
-
compactionThreshold: resolved.compactionThreshold,
|
|
80
|
-
maxToolResultSize: resolved.maxToolResultSize,
|
|
81
|
-
agentName: job.agent,
|
|
82
|
-
usageStore: this.usageStore,
|
|
83
|
-
source: "cron",
|
|
84
|
-
});
|
|
85
|
-
const lastMsg = result.messages[result.messages.length - 1];
|
|
86
|
-
session.append(lastMsg);
|
|
87
|
-
return result;
|
|
88
130
|
}
|
|
89
131
|
start(callbacks) {
|
|
90
132
|
for (const job of this.config.cron) {
|
|
@@ -107,6 +149,11 @@ export class CronScheduler {
|
|
|
107
149
|
this.tasks.push(task);
|
|
108
150
|
}
|
|
109
151
|
}
|
|
152
|
+
reload(newConfig) {
|
|
153
|
+
this.stop();
|
|
154
|
+
this.config = newConfig;
|
|
155
|
+
log.info({ jobs: newConfig.cron.length }, "config reloaded");
|
|
156
|
+
}
|
|
110
157
|
stop() {
|
|
111
158
|
for (const task of this.tasks)
|
|
112
159
|
task.stop();
|
|
@@ -8,6 +8,7 @@ export interface SlackClient {
|
|
|
8
8
|
postMessage(channelId: string, text: string, blocks?: unknown[], threadTs?: string): Promise<{
|
|
9
9
|
ok: boolean;
|
|
10
10
|
}>;
|
|
11
|
+
downloadFile(urlPrivateDownload: string): Promise<Buffer>;
|
|
11
12
|
app: import("@slack/bolt").App;
|
|
12
13
|
}
|
|
13
14
|
export declare function createSlackClient(options: SlackClientOptions): SlackClient;
|
|
@@ -28,6 +28,15 @@ export function createSlackClient(options) {
|
|
|
28
28
|
async stop() {
|
|
29
29
|
await app.stop();
|
|
30
30
|
},
|
|
31
|
+
async downloadFile(urlPrivateDownload) {
|
|
32
|
+
const response = await fetch(urlPrivateDownload, {
|
|
33
|
+
headers: { Authorization: `Bearer ${options.botToken}` },
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`Failed to download Slack file: ${response.status} ${response.statusText}`);
|
|
37
|
+
}
|
|
38
|
+
return Buffer.from(await response.arrayBuffer());
|
|
39
|
+
},
|
|
31
40
|
async postMessage(channelId, text, blocks, threadTs) {
|
|
32
41
|
const result = await app.client.chat.postMessage({
|
|
33
42
|
channel: channelId,
|