kimi-agent-swarm-cli 0.7.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.
@@ -0,0 +1,325 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import { loadConfig, resolveProviderCredential } from "../config";
5
+ import { createSearchProvider } from "../providers";
6
+ import { scoreSource } from "../scorer";
7
+ import { verifyRun } from "../verifier";
8
+ import type { QueueAdapter } from "./queue-adapter";
9
+ import { MemoryQueueAdapter } from "./memory-adapter";
10
+ import { RedisQueueAdapter } from "./redis-adapter";
11
+ import {
12
+ buildTasksFromPlans,
13
+ splitFixtureTasks,
14
+ splitWebSearchTasks,
15
+ } from "./task-splitter";
16
+ import type {
17
+ Claim,
18
+ ClaimConfidence,
19
+ ClaimFreshness,
20
+ DistributedJob,
21
+ DistributedRunOptions,
22
+ DistributedTask,
23
+ EnrichedSource,
24
+ ExecutionProfile,
25
+ ResearchPlan,
26
+ Run,
27
+ RunWideSearchOptions,
28
+ RunWideSearchResult,
29
+ SearchDepth,
30
+ Source,
31
+ UsageMetrics,
32
+ WorkerResult,
33
+ } from "../types";
34
+
35
+ function makeRunId(): string {
36
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
37
+ const random = Math.random().toString(36).slice(2, 8);
38
+ return `${timestamp}-${random}`;
39
+ }
40
+
41
+ function claimConfidence(source: EnrichedSource): ClaimConfidence {
42
+ const score = source.scores?.authority ?? 0;
43
+ if (score >= 4.5) return "high";
44
+ if (score >= 3) return "medium";
45
+ return "low";
46
+ }
47
+
48
+ function claimFreshness(source: EnrichedSource): ClaimFreshness {
49
+ if (!source.publishedAt || source.publishedAt === "unknown") return "unknown";
50
+ const now = new Date();
51
+ const oneYearAgo = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate())
52
+ .toISOString()
53
+ .split("T")[0];
54
+ return source.publishedAt >= oneYearAgo ? "current" : "stale";
55
+ }
56
+
57
+ function renderSynthesis({
58
+ objective,
59
+ profile,
60
+ sources,
61
+ claims,
62
+ verification,
63
+ }: {
64
+ objective: string;
65
+ profile: ExecutionProfile;
66
+ sources: EnrichedSource[];
67
+ claims: Claim[];
68
+ verification: { status: string; acceptedSources: number; rejectedSources: number };
69
+ }): string {
70
+ const acceptedSources = sources.filter((s) => s.decision === "accepted");
71
+ const acceptedClaims = claims;
72
+ const lines = [
73
+ `# Synthesis: ${objective}`,
74
+ "",
75
+ `**Profile:** ${profile}`,
76
+ `**Accepted sources:** ${acceptedSources.length} | **Rejected sources:** ${sources.length - acceptedSources.length}`,
77
+ `**Accepted claims:** ${acceptedClaims.length}`,
78
+ `**Verification:** ${verification.status}`,
79
+ "",
80
+ "## Accepted sources",
81
+ ...acceptedSources.map((source) => `- [${source.sourceClass}] [${source.title}](${source.url}) (${source.decision})`),
82
+ "",
83
+ "## Claims",
84
+ ...acceptedClaims.map((claim) => `- ${claim.claim} [confidence: ${claim.confidence}, freshness: ${claim.freshness}]`),
85
+ "",
86
+ "## Method notes",
87
+ "This run used distributed execution. Results were aggregated from multiple worker tasks.",
88
+ ];
89
+ return lines.join("\n");
90
+ }
91
+
92
+ function createQueueAdapter(
93
+ options: DistributedRunOptions,
94
+ workDir: string,
95
+ ): QueueAdapter {
96
+ if (options.queueType === "redis") {
97
+ return new RedisQueueAdapter();
98
+ }
99
+ return new MemoryQueueAdapter({ workDir });
100
+ }
101
+
102
+ async function executeTask(
103
+ task: DistributedTask,
104
+ profile: ExecutionProfile,
105
+ providerName: string,
106
+ searchDepth: SearchDepth,
107
+ metrics: UsageMetrics,
108
+ ): Promise<WorkerResult> {
109
+ if (profile.startsWith("fixture")) {
110
+ const sourceIds = task.query.split(",").map((id) => id.trim());
111
+ const allSources = await loadFixtureSources(profile);
112
+ const selected = allSources.filter((s) => sourceIds.includes(s.id));
113
+ return { sources: selected, usageMetrics: { ...metrics } };
114
+ }
115
+
116
+ const config = await loadConfig();
117
+ const credential = resolveProviderCredential(config, providerName);
118
+ const provider = createSearchProvider(providerName, { credential, metrics });
119
+ const maxResults = Math.ceil(25 / 5); // split results across families
120
+ const sources = await provider.search({
121
+ objective: task.query,
122
+ depth: searchDepth,
123
+ maxResults,
124
+ });
125
+ return { sources, usageMetrics: { ...metrics } };
126
+ }
127
+
128
+ async function loadFixtureSources(profile: ExecutionProfile): Promise<Source[]> {
129
+ const FIXTURE_FILE_MAP: Record<string, string> = {
130
+ fixture: "basic-sources.json",
131
+ "fixture-asset-mgmt": "asset-mgmt-roles.json",
132
+ "fixture-sellside-research": "sellside-research-roles.json",
133
+ "fixture-youtube-niche": "youtube-niche.json",
134
+ "fixture-paul-graham-corpus": "paul-graham-corpus.json",
135
+ "fixture-github-repo-landscape": "github-repo-landscape.json",
136
+ "fixture-market-scan": "market-scan.json",
137
+ };
138
+
139
+ const fileName = FIXTURE_FILE_MAP[profile];
140
+ if (!fileName) {
141
+ throw new Error(`No fixture file mapping for profile: ${profile}`);
142
+ }
143
+
144
+ const text = await readFile(join(import.meta.dir, "../../fixtures", fileName), "utf8");
145
+ const data = JSON.parse(text) as { sources: Source[] };
146
+ return data.sources;
147
+ }
148
+
149
+ export async function workerLoop(
150
+ adapter: QueueAdapter,
151
+ jobId: string,
152
+ workerId: string,
153
+ profile: ExecutionProfile,
154
+ providerName: string,
155
+ searchDepth: SearchDepth,
156
+ metrics: UsageMetrics,
157
+ ): Promise<void> {
158
+ // eslint-disable-next-line no-constant-condition
159
+ while (true) {
160
+ const pending = await adapter.getPendingTaskCount(jobId);
161
+ const running = await adapter.getRunningTaskCount(jobId);
162
+ if (pending === 0 && running === 0) {
163
+ return;
164
+ }
165
+
166
+ const task = await adapter.claimNextTask(jobId, workerId);
167
+ if (!task) {
168
+ // No task claimed but work may remain; brief backoff.
169
+ await new Promise((resolve) => setTimeout(resolve, 50));
170
+ continue;
171
+ }
172
+
173
+ try {
174
+ const result = await executeTask(task, profile, providerName, searchDepth, metrics);
175
+ await adapter.completeTask(task.taskId, result);
176
+ } catch (error) {
177
+ const message = error instanceof Error ? error.message : String(error);
178
+ await adapter.failTask(task.taskId, message);
179
+ }
180
+ }
181
+ }
182
+
183
+ export async function runDistributedWideSearch({
184
+ objective,
185
+ profile = "fixture",
186
+ providerName,
187
+ searchDepth = "standard",
188
+ workDir = process.cwd(),
189
+ distributed = { enabled: true },
190
+ }: RunWideSearchOptions): Promise<RunWideSearchResult> {
191
+ const runId = makeRunId();
192
+ const runDir = join(workDir, ".runs", "wide-search", runId);
193
+ await mkdir(runDir, { recursive: true });
194
+
195
+ const effectiveProviderName = profile === "web-search" ? (providerName ?? "mock") : "mock";
196
+ const maxRetries = distributed.maxRetries ?? 3;
197
+ const workers = distributed.workers ?? 4;
198
+
199
+ const adapter = createQueueAdapter(distributed, workDir);
200
+
201
+ let job: DistributedJob;
202
+ if (distributed.resumeJobId) {
203
+ const loaded = await adapter.getJob(distributed.resumeJobId);
204
+ if (!loaded) {
205
+ throw new Error(`Job not found for resume: ${distributed.resumeJobId}`);
206
+ }
207
+ job = loaded;
208
+ } else {
209
+ const plans = profile.startsWith("fixture")
210
+ ? await splitFixtureTasks(profile, join(import.meta.dir, "../../fixtures"))
211
+ : splitWebSearchTasks(objective ?? "");
212
+ const tasks = buildTasksFromPlans("placeholder", plans, maxRetries);
213
+ job = await adapter.createJob({
214
+ objective: objective ?? "",
215
+ executionProfile: profile,
216
+ providerName: effectiveProviderName,
217
+ searchDepth,
218
+ queueType: distributed.queueType ?? "memory",
219
+ status: "pending",
220
+ tasks: tasks.map((t) => ({ ...t, taskId: `${runId}-${t.taskId}` })),
221
+ });
222
+ }
223
+
224
+ job.status = "running";
225
+ await adapter.saveJob(job);
226
+
227
+ const workerMetrics: UsageMetrics[] = [];
228
+ const workerPromises: Promise<void>[] = [];
229
+ for (let i = 0; i < workers; i += 1) {
230
+ const metrics: UsageMetrics = { providerCalls: 0, apiCalls: 0 };
231
+ workerMetrics.push(metrics);
232
+ workerPromises.push(
233
+ workerLoop(
234
+ adapter,
235
+ job.jobId,
236
+ `worker-${i + 1}`,
237
+ profile,
238
+ effectiveProviderName,
239
+ searchDepth,
240
+ metrics,
241
+ ),
242
+ );
243
+ }
244
+
245
+ await Promise.all(workerPromises);
246
+
247
+ const completedJob = await adapter.getJob(job.jobId);
248
+ if (!completedJob) {
249
+ throw new Error("Job disappeared during distributed run");
250
+ }
251
+
252
+ const aggregatedSources: Source[] = [];
253
+ for (const task of completedJob.tasks) {
254
+ if (task.status === "completed" && task.result) {
255
+ aggregatedSources.push(...task.result.sources);
256
+ }
257
+ }
258
+
259
+ if (aggregatedSources.length === 0) {
260
+ throw new Error("Distributed run produced no accepted sources");
261
+ }
262
+
263
+ const sources: EnrichedSource[] = aggregatedSources.map((source) => scoreSource(source));
264
+
265
+ const claims: Claim[] = [];
266
+ for (const source of sources.filter((item) => item.decision === "accepted")) {
267
+ for (const claim of source.claims ?? []) {
268
+ claims.push({
269
+ id: `C${String(claims.length + 1).padStart(3, "0")}`,
270
+ claim,
271
+ sourceIds: [source.id],
272
+ confidence: claimConfidence(source),
273
+ freshness: claimFreshness(source),
274
+ });
275
+ }
276
+ }
277
+
278
+ const totalMetrics: UsageMetrics = workerMetrics.reduce(
279
+ (acc, m) => ({
280
+ providerCalls: acc.providerCalls + m.providerCalls,
281
+ apiCalls: acc.apiCalls + m.apiCalls,
282
+ }),
283
+ { providerCalls: 0, apiCalls: 0 },
284
+ );
285
+
286
+ const run: Run = {
287
+ runId,
288
+ objective: objective ?? "",
289
+ executionProfile: profile,
290
+ status: "completed",
291
+ createdAt: new Date().toISOString(),
292
+ usageMetrics: totalMetrics,
293
+ };
294
+
295
+ const researchPlan: ResearchPlan = {
296
+ objective: objective ?? "",
297
+ searchDepth,
298
+ executionProfile: profile,
299
+ queryFamilies: completedJob.tasks.map((t) => t.queryFamily),
300
+ sourceTargets: ["official", "community", "secondary"],
301
+ stopConditions: ["all distributed tasks completed"],
302
+ };
303
+
304
+ await writeFile(join(runDir, "run.json"), `${JSON.stringify(run, null, 2)}\n`);
305
+ await writeFile(join(runDir, "research-plan.json"), `${JSON.stringify(researchPlan, null, 2)}\n`);
306
+ await writeFile(
307
+ join(runDir, "source-ledger.jsonl"),
308
+ `${sources.map((source) => JSON.stringify(source)).join("\n")}\n`,
309
+ );
310
+ await writeFile(
311
+ join(runDir, "claim-ledger.jsonl"),
312
+ `${claims.map((claim) => JSON.stringify(claim)).join("\n")}\n`,
313
+ );
314
+ await writeFile(join(runDir, "distributed-job.json"), `${JSON.stringify(completedJob, null, 2)}\n`);
315
+
316
+ const verification = await verifyRun({ runDir, minAcceptedSources: 1 });
317
+ const synthesis = renderSynthesis({ objective: objective ?? "", profile, sources, claims, verification });
318
+ await writeFile(join(runDir, "synthesis.md"), synthesis);
319
+
320
+ return {
321
+ runId,
322
+ runDir,
323
+ verification,
324
+ };
325
+ }
@@ -0,0 +1,78 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import type { DistributedTask, ExecutionProfile, Source } from "../types";
5
+
6
+ export interface TaskPlan {
7
+ queryFamily: string;
8
+ query: string;
9
+ }
10
+
11
+ const FIXTURE_FILE_MAP: Record<string, string> = {
12
+ fixture: "basic-sources.json",
13
+ "fixture-asset-mgmt": "asset-mgmt-roles.json",
14
+ "fixture-sellside-research": "sellside-research-roles.json",
15
+ "fixture-youtube-niche": "youtube-niche.json",
16
+ "fixture-paul-graham-corpus": "paul-graham-corpus.json",
17
+ "fixture-github-repo-landscape": "github-repo-landscape.json",
18
+ "fixture-market-scan": "market-scan.json",
19
+ };
20
+
21
+ export function splitWebSearchTasks(objective: string): TaskPlan[] {
22
+ const queries: TaskPlan[] = [
23
+ { queryFamily: "primary", query: objective },
24
+ { queryFamily: "best-of", query: `best ${objective}` },
25
+ { queryFamily: "comparison", query: `${objective} comparison` },
26
+ { queryFamily: "github", query: `${objective} github` },
27
+ { queryFamily: "latest", query: `${objective} 2026` },
28
+ ];
29
+
30
+ // Deduplicate while preserving order.
31
+ const seen = new Set<string>();
32
+ return queries.filter((q) => {
33
+ if (seen.has(q.query)) return false;
34
+ seen.add(q.query);
35
+ return true;
36
+ });
37
+ }
38
+
39
+ export async function splitFixtureTasks(
40
+ profile: ExecutionProfile,
41
+ fixturesDir: string,
42
+ batchSize = 5,
43
+ ): Promise<TaskPlan[]> {
44
+ const fileName = FIXTURE_FILE_MAP[profile];
45
+ if (!fileName) {
46
+ throw new Error(`No fixture file mapping for profile: ${profile}`);
47
+ }
48
+
49
+ const text = await readFile(join(fixturesDir, fileName), "utf8");
50
+ const { sources } = JSON.parse(text) as { sources: Source[] };
51
+
52
+ const tasks: TaskPlan[] = [];
53
+ for (let i = 0; i < sources.length; i += batchSize) {
54
+ const batch = sources.slice(i, i + batchSize);
55
+ tasks.push({
56
+ queryFamily: `fixture-batch-${Math.floor(i / batchSize) + 1}`,
57
+ query: batch.map((s) => s.id).join(","),
58
+ });
59
+ }
60
+
61
+ return tasks;
62
+ }
63
+
64
+ export function buildTasksFromPlans(
65
+ jobId: string,
66
+ plans: TaskPlan[],
67
+ maxRetries: number,
68
+ ): DistributedTask[] {
69
+ return plans.map((plan, index) => ({
70
+ taskId: `${jobId}-task-${String(index + 1).padStart(4, "0")}`,
71
+ jobId,
72
+ queryFamily: plan.queryFamily,
73
+ query: plan.query,
74
+ status: "pending",
75
+ attempts: 0,
76
+ maxRetries,
77
+ }));
78
+ }
package/src/export.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import type { Claim, EnrichedSource, ExportFormat, ExportOptions, Run } from "./types";
5
+
6
+ async function readJsonl<T>(path: string): Promise<T[]> {
7
+ const text = await readFile(path, "utf8");
8
+ return text
9
+ .split(/\r?\n/)
10
+ .filter(Boolean)
11
+ .map((line) => JSON.parse(line) as T);
12
+ }
13
+
14
+ function escapeCsvField(field: string): string {
15
+ if (field.includes(",") || field.includes('"') || field.includes("\n") || field.includes("\r")) {
16
+ return `"${field.replace(/"/g, '""')}"`;
17
+ }
18
+ return field;
19
+ }
20
+
21
+ function sourceUrlForClaim(claim: Claim, sources: EnrichedSource[]): string {
22
+ const source = sources.find((s) => claim.sourceIds.includes(s.id));
23
+ return source?.url ?? "";
24
+ }
25
+
26
+ export async function exportRun({ runDir, format, outPath }: ExportOptions): Promise<string> {
27
+ const run = JSON.parse(await readFile(join(runDir, "run.json"), "utf8")) as Run;
28
+ const sources = await readJsonl<EnrichedSource>(join(runDir, "source-ledger.jsonl"));
29
+ const claims = await readJsonl<Claim>(join(runDir, "claim-ledger.jsonl"));
30
+
31
+ const destination = outPath ?? join(runDir, `export.${format}`);
32
+
33
+ if (format === "json") {
34
+ const payload = {
35
+ runId: run.runId,
36
+ objective: run.objective,
37
+ executionProfile: run.executionProfile,
38
+ status: run.status,
39
+ createdAt: run.createdAt,
40
+ usageMetrics: run.usageMetrics,
41
+ sources,
42
+ claims,
43
+ };
44
+ await writeFile(destination, `${JSON.stringify(payload, null, 2)}\n`);
45
+ return destination;
46
+ }
47
+
48
+ if (format === "csv") {
49
+ const header = ["claim_id", "claim", "source_ids", "confidence", "freshness", "url"];
50
+ const rows = claims.map((claim) => [
51
+ claim.id,
52
+ claim.claim,
53
+ claim.sourceIds.join(";"),
54
+ claim.confidence,
55
+ claim.freshness,
56
+ sourceUrlForClaim(claim, sources),
57
+ ]);
58
+ const csv = [header, ...rows]
59
+ .map((row) => row.map(escapeCsvField).join(","))
60
+ .join("\n");
61
+ await writeFile(destination, `${csv}\n`);
62
+ return destination;
63
+ }
64
+
65
+ throw new Error(`Unsupported export format: ${format}`);
66
+ }
67
+
68
+ export function supportedExportFormats(): ExportFormat[] {
69
+ return ["json", "csv"];
70
+ }
package/src/init.ts ADDED
@@ -0,0 +1,138 @@
1
+ import { createInterface, type Interface } from "node:readline/promises";
2
+
3
+ import { getGlobalConfigPath, writeConfig, type KaswConfig, type ProviderConfig } from "./config";
4
+
5
+ export interface InitOptions {
6
+ nonInteractive?: boolean;
7
+ global?: boolean;
8
+ workDir?: string;
9
+ }
10
+
11
+ interface ProviderPrompt {
12
+ name: string;
13
+ label: string;
14
+ credentialType: "apiKey" | "token";
15
+ envVar: string;
16
+ }
17
+
18
+ const PROVIDERS: ProviderPrompt[] = [
19
+ { name: "serper", label: "Serper.dev (Google Search)", credentialType: "apiKey", envVar: "SERPER_API_KEY" },
20
+ { name: "tavily", label: "Tavily", credentialType: "apiKey", envVar: "TAVILY_API_KEY" },
21
+ { name: "brave", label: "Brave Search", credentialType: "apiKey", envVar: "BRAVE_API_KEY" },
22
+ { name: "github", label: "GitHub", credentialType: "token", envVar: "GITHUB_TOKEN" },
23
+ ];
24
+
25
+ async function prompt(question: string, reader?: Interface): Promise<string> {
26
+ if (!reader) {
27
+ return "";
28
+ }
29
+ const answer = await reader.question(question);
30
+ return answer.trim();
31
+ }
32
+
33
+ async function promptSecret(question: string, reader?: Interface): Promise<string> {
34
+ if (!reader) {
35
+ return "";
36
+ }
37
+ const stdin = process.stdin;
38
+ const stdout = process.stdout;
39
+ stdout.write(question);
40
+
41
+ // Disable echo for secret input
42
+ stdin.setRawMode?.(true);
43
+ const chunks: string[] = [];
44
+ for await (const chunk of stdin) {
45
+ const text = chunk.toString();
46
+ for (const char of text) {
47
+ if (char === "\n" || char === "\r" || char === "\u0004") {
48
+ stdin.setRawMode?.(false);
49
+ stdout.write("\n");
50
+ return chunks.join("").trim();
51
+ }
52
+ if (char === "\u0003") {
53
+ stdin.setRawMode?.(false);
54
+ process.exit(1);
55
+ }
56
+ if (char === "\u007f") {
57
+ chunks.pop();
58
+ stdout.write("\b \b");
59
+ } else {
60
+ chunks.push(char);
61
+ stdout.write("*");
62
+ }
63
+ }
64
+ }
65
+ return chunks.join("").trim();
66
+ }
67
+
68
+ export async function runInit(options: InitOptions = {}): Promise<{ configPath: string; wrote: string[] }> {
69
+ const wrote: string[] = [];
70
+ const config: KaswConfig = {
71
+ providers: {},
72
+ defaults: {
73
+ provider: "mock",
74
+ depth: "standard",
75
+ profile: "fixture",
76
+ },
77
+ };
78
+
79
+ let reader: Interface | undefined;
80
+ if (!options.nonInteractive && process.stdin.isTTY) {
81
+ reader = createInterface({ input: process.stdin, output: process.stdout });
82
+ }
83
+
84
+ try {
85
+ for (const provider of PROVIDERS) {
86
+ const envValue = process.env[provider.envVar];
87
+ if (envValue) {
88
+ config.providers[provider.name] = {
89
+ [provider.credentialType]: envValue,
90
+ };
91
+ wrote.push(`${provider.name} (from ${provider.envVar})`);
92
+ continue;
93
+ }
94
+
95
+ if (options.nonInteractive || !reader) {
96
+ continue;
97
+ }
98
+
99
+ const enable = await prompt(`Enable ${provider.label}? (y/N) `, reader);
100
+ if (enable.toLowerCase() !== "y") {
101
+ continue;
102
+ }
103
+
104
+ const value = await promptSecret(`Enter ${provider.label} ${provider.credentialType === "apiKey" ? "API key" : "token"}: `, reader);
105
+ if (value) {
106
+ const providerConfig: ProviderConfig = {
107
+ [provider.credentialType]: value,
108
+ };
109
+ config.providers[provider.name] = providerConfig;
110
+ wrote.push(provider.name);
111
+ }
112
+ }
113
+
114
+ const configPath = await writeConfig(config, {
115
+ global: options.global ?? true,
116
+ workDir: options.workDir,
117
+ });
118
+
119
+ return { configPath, wrote };
120
+ } finally {
121
+ reader?.close();
122
+ }
123
+ }
124
+
125
+ export function getInitInstructions(): string {
126
+ const configPath = getGlobalConfigPath();
127
+ return `Configuration written to ${configPath}.
128
+
129
+ You can also set API keys via environment variables:
130
+ SERPER_API_KEY, TAVILY_API_KEY, BRAVE_API_KEY, GITHUB_TOKEN
131
+
132
+ Run a quick demo:
133
+ ./bin/kasw research "AI browser agent repos" --profile fixture
134
+
135
+ Or run a live search:
136
+ ./bin/kasw research "AI browser agent repos" --profile web-search --provider tavily
137
+ `;
138
+ }