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.
package/src/costs.ts ADDED
@@ -0,0 +1,134 @@
1
+ import type { BudgetOptions, CostEstimate, SearchDepth, UsageMetrics } from "./types";
2
+
3
+ export interface ProviderPricing {
4
+ perCallUsd: number;
5
+ per1kTokensUsd?: number;
6
+ }
7
+
8
+ export const PROVIDER_PRICING: Record<string, ProviderPricing> = {
9
+ mock: { perCallUsd: 0 },
10
+ serper: { perCallUsd: 0.001 }, // approx $1 per 1,000 searches
11
+ tavily: { perCallUsd: 0.005 }, // starter tier approx
12
+ brave: { perCallUsd: 0.003 }, // approx based on paid tier volume
13
+ github: { perCallUsd: 0 }, // free tier, token raises rate limit only
14
+ };
15
+
16
+ export function maxResultsForDepth(depth: SearchDepth): number {
17
+ switch (depth) {
18
+ case "light":
19
+ return 10;
20
+ case "standard":
21
+ return 25;
22
+ case "deep":
23
+ return 75;
24
+ case "maximum":
25
+ return 100;
26
+ default:
27
+ return 25;
28
+ }
29
+ }
30
+
31
+ export function getProviderPricing(providerName: string): ProviderPricing {
32
+ return PROVIDER_PRICING[providerName] ?? { perCallUsd: 0 };
33
+ }
34
+
35
+ export function estimateRunCost(
36
+ providerName: string,
37
+ depth: SearchDepth = "standard",
38
+ ): CostEstimate {
39
+ const pricing = getProviderPricing(providerName);
40
+ const estimatedProviderCalls = 1;
41
+ const estimatedApiCalls = 1;
42
+ const estimatedCostUsd = pricing.perCallUsd * estimatedProviderCalls;
43
+
44
+ return {
45
+ providerName,
46
+ depth,
47
+ estimatedProviderCalls,
48
+ estimatedApiCalls,
49
+ estimatedCostUsd,
50
+ };
51
+ }
52
+
53
+ export function calculateActualCost(providerName: string, metrics: UsageMetrics): number {
54
+ const pricing = getProviderPricing(providerName);
55
+ const callCost = pricing.perCallUsd * metrics.providerCalls;
56
+ const tokenCost = pricing.per1kTokensUsd
57
+ ? (pricing.per1kTokensUsd * (metrics.estimatedTokens ?? 0)) / 1000
58
+ : 0;
59
+ return Number((callCost + tokenCost).toFixed(6));
60
+ }
61
+
62
+ export class BudgetExceededError extends Error {
63
+ constructor(message: string) {
64
+ super(message);
65
+ this.name = "BudgetExceededError";
66
+ }
67
+ }
68
+
69
+ export function checkBudget(
70
+ providerName: string,
71
+ metrics: UsageMetrics,
72
+ budget: BudgetOptions = {},
73
+ ): void {
74
+ const actualCost = calculateActualCost(providerName, metrics);
75
+
76
+ if (budget.maxCostUsd !== undefined && actualCost > budget.maxCostUsd) {
77
+ throw new BudgetExceededError(
78
+ `Actual cost $${actualCost.toFixed(4)} exceeds budget $${budget.maxCostUsd.toFixed(4)}`,
79
+ );
80
+ }
81
+
82
+ if (budget.maxProviderCalls !== undefined && metrics.providerCalls > budget.maxProviderCalls) {
83
+ throw new BudgetExceededError(
84
+ `Provider calls ${metrics.providerCalls} exceed budget ${budget.maxProviderCalls}`,
85
+ );
86
+ }
87
+
88
+ if (budget.maxApiCalls !== undefined && metrics.apiCalls > budget.maxApiCalls) {
89
+ throw new BudgetExceededError(
90
+ `API calls ${metrics.apiCalls} exceed budget ${budget.maxApiCalls}`,
91
+ );
92
+ }
93
+ }
94
+
95
+ export function checkEstimatedBudget(
96
+ estimate: CostEstimate,
97
+ budget: BudgetOptions = {},
98
+ ): void {
99
+ if (budget.maxCostUsd !== undefined && estimate.estimatedCostUsd > budget.maxCostUsd) {
100
+ throw new BudgetExceededError(
101
+ `Estimated cost $${estimate.estimatedCostUsd.toFixed(4)} exceeds budget $${budget.maxCostUsd.toFixed(4)}. Use --dry-run to inspect or raise the budget.`,
102
+ );
103
+ }
104
+
105
+ if (
106
+ budget.maxProviderCalls !== undefined &&
107
+ estimate.estimatedProviderCalls > budget.maxProviderCalls
108
+ ) {
109
+ throw new BudgetExceededError(
110
+ `Estimated provider calls ${estimate.estimatedProviderCalls} exceed budget ${budget.maxProviderCalls}`,
111
+ );
112
+ }
113
+
114
+ if (budget.maxApiCalls !== undefined && estimate.estimatedApiCalls > budget.maxApiCalls) {
115
+ throw new BudgetExceededError(
116
+ `Estimated API calls ${estimate.estimatedApiCalls} exceed budget ${budget.maxApiCalls}`,
117
+ );
118
+ }
119
+ }
120
+
121
+ export function formatCostReport(providerName: string, metrics: UsageMetrics): string {
122
+ const actualCost = calculateActualCost(providerName, metrics);
123
+ return [
124
+ `Provider: ${providerName}`,
125
+ `Provider calls: ${metrics.providerCalls}`,
126
+ `API calls: ${metrics.apiCalls}`,
127
+ metrics.estimatedTokens !== undefined ? `Estimated tokens: ${metrics.estimatedTokens}` : null,
128
+ `Estimated cost: $${metrics.estimatedCostUsd?.toFixed(4) ?? "0.0000"}`,
129
+ `Actual cost: $${actualCost.toFixed(4)}`,
130
+ metrics.notes ? `Notes: ${metrics.notes}` : null,
131
+ ]
132
+ .filter(Boolean)
133
+ .join("\n");
134
+ }
@@ -0,0 +1,152 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import type { QueueAdapter } from "./queue-adapter";
5
+ import type { DistributedJob, DistributedTask, WorkerResult } from "../types";
6
+
7
+ export interface MemoryAdapterOptions {
8
+ workDir?: string;
9
+ }
10
+
11
+ export class MemoryQueueAdapter implements QueueAdapter {
12
+ readonly type = "memory";
13
+ private readonly jobs = new Map<string, DistributedJob>();
14
+ private readonly workDir: string;
15
+
16
+ constructor(options: MemoryAdapterOptions = {}) {
17
+ this.workDir = options.workDir ?? process.cwd();
18
+ }
19
+
20
+ private jobFilePath(jobId: string): string {
21
+ return join(this.workDir, ".runs", "wide-search", "jobs", `${jobId}.json`);
22
+ }
23
+
24
+ async createJob(
25
+ job: Omit<DistributedJob, "jobId" | "createdAt" | "updatedAt">,
26
+ ): Promise<DistributedJob> {
27
+ const now = new Date().toISOString();
28
+ const created: DistributedJob = {
29
+ ...job,
30
+ jobId: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
31
+ createdAt: now,
32
+ updatedAt: now,
33
+ };
34
+ this.jobs.set(created.jobId, created);
35
+ await this.saveJob(created);
36
+ return created;
37
+ }
38
+
39
+ async getJob(jobId: string): Promise<DistributedJob | undefined> {
40
+ const cached = this.jobs.get(jobId);
41
+ if (cached) {
42
+ return cached;
43
+ }
44
+
45
+ try {
46
+ const text = await readFile(this.jobFilePath(jobId), "utf8");
47
+ const loaded = JSON.parse(text) as DistributedJob;
48
+ this.jobs.set(jobId, loaded);
49
+ return loaded;
50
+ } catch (error) {
51
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
52
+ return undefined;
53
+ }
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ async saveJob(job: DistributedJob): Promise<void> {
59
+ const path = this.jobFilePath(job.jobId);
60
+ await mkdir(join(path, ".."), { recursive: true });
61
+ const updated = { ...job, updatedAt: new Date().toISOString() };
62
+ this.jobs.set(job.jobId, updated);
63
+ await writeFile(path, `${JSON.stringify(updated, null, 2)}\n`);
64
+ }
65
+
66
+ async claimNextTask(
67
+ jobId: string,
68
+ workerId: string,
69
+ ): Promise<DistributedTask | undefined> {
70
+ const job = await this.getJob(jobId);
71
+ if (!job) return undefined;
72
+
73
+ const task = job.tasks.find(
74
+ (t) => t.status === "pending" && t.attempts < t.maxRetries,
75
+ );
76
+ if (!task) return undefined;
77
+
78
+ task.status = "running";
79
+ task.workerId = workerId;
80
+ task.attempts += 1;
81
+ task.startedAt = new Date().toISOString();
82
+ await this.saveJob(job);
83
+ return task;
84
+ }
85
+
86
+ async completeTask(taskId: string, result: WorkerResult): Promise<void> {
87
+ const { job, task } = await this.findTask(taskId);
88
+ if (!task || !job) return;
89
+
90
+ task.status = "completed";
91
+ task.result = result;
92
+ task.completedAt = new Date().toISOString();
93
+ task.error = undefined;
94
+
95
+ this.updateJobStatus(job);
96
+ await this.saveJob(job);
97
+ }
98
+
99
+ async failTask(taskId: string, error: string): Promise<void> {
100
+ const { job, task } = await this.findTask(taskId);
101
+ if (!task || !job) return;
102
+
103
+ task.error = error;
104
+ if (task.attempts >= task.maxRetries) {
105
+ task.status = "failed";
106
+ task.completedAt = new Date().toISOString();
107
+ } else {
108
+ task.status = "pending";
109
+ task.workerId = undefined;
110
+ task.startedAt = undefined;
111
+ }
112
+
113
+ this.updateJobStatus(job);
114
+ await this.saveJob(job);
115
+ }
116
+
117
+ async getPendingTaskCount(jobId: string): Promise<number> {
118
+ const job = await this.getJob(jobId);
119
+ if (!job) return 0;
120
+ return job.tasks.filter((t) => t.status === "pending").length;
121
+ }
122
+
123
+ async getRunningTaskCount(jobId: string): Promise<number> {
124
+ const job = await this.getJob(jobId);
125
+ if (!job) return 0;
126
+ return job.tasks.filter((t) => t.status === "running").length;
127
+ }
128
+
129
+ private async findTask(
130
+ taskId: string,
131
+ ): Promise<{ job?: DistributedJob; task?: DistributedTask }> {
132
+ for (const job of this.jobs.values()) {
133
+ const task = job.tasks.find((t) => t.taskId === taskId);
134
+ if (task) {
135
+ return { job, task };
136
+ }
137
+ }
138
+ return {};
139
+ }
140
+
141
+ private updateJobStatus(job: DistributedJob): void {
142
+ if (job.tasks.every((t) => t.status === "completed")) {
143
+ job.status = "completed";
144
+ } else if (job.tasks.some((t) => t.status === "running")) {
145
+ job.status = "running";
146
+ } else if (job.tasks.every((t) => t.status === "failed")) {
147
+ job.status = "failed";
148
+ } else {
149
+ job.status = "running";
150
+ }
151
+ }
152
+ }
@@ -0,0 +1,29 @@
1
+ import type { DistributedJob, DistributedTask, WorkerResult } from "../types";
2
+
3
+ export interface QueueAdapter {
4
+ readonly type: string;
5
+
6
+ createJob(
7
+ job: Omit<DistributedJob, "jobId" | "createdAt" | "updatedAt">,
8
+ ): Promise<DistributedJob>;
9
+
10
+ getJob(jobId: string): Promise<DistributedJob | undefined>;
11
+ saveJob(job: DistributedJob): Promise<void>;
12
+
13
+ claimNextTask(jobId: string, workerId: string): Promise<DistributedTask | undefined>;
14
+ completeTask(taskId: string, result: WorkerResult): Promise<void>;
15
+ failTask(taskId: string, error: string): Promise<void>;
16
+
17
+ getPendingTaskCount(jobId: string): Promise<number>;
18
+ getRunningTaskCount(jobId: string): Promise<number>;
19
+ }
20
+
21
+ export function makeJobId(): string {
22
+ const timestamp = Date.now().toString(36);
23
+ const random = Math.random().toString(36).slice(2, 8);
24
+ return `job-${timestamp}-${random}`;
25
+ }
26
+
27
+ export function makeTaskId(jobId: string, index: number): string {
28
+ return `${jobId}-task-${String(index + 1).padStart(4, "0")}`;
29
+ }
@@ -0,0 +1,185 @@
1
+ import type { QueueAdapter } from "./queue-adapter";
2
+ import type { DistributedJob, DistributedTask, WorkerResult } from "../types";
3
+
4
+ export interface RedisAdapterOptions {
5
+ redisUrl?: string;
6
+ }
7
+
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ let Redis: any;
10
+
11
+ try {
12
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
13
+ Redis = require("ioredis");
14
+ } catch {
15
+ Redis = undefined;
16
+ }
17
+
18
+ export class RedisQueueAdapter implements QueueAdapter {
19
+ readonly type = "redis";
20
+ private readonly redisUrl: string;
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ private client?: any;
23
+
24
+ constructor(options: RedisAdapterOptions = {}) {
25
+ this.redisUrl = options.redisUrl ?? process.env.REDIS_URL ?? "redis://localhost:6379";
26
+ if (!Redis) {
27
+ throw new Error(
28
+ "Redis adapter requires ioredis. Install it with: bun add ioredis",
29
+ );
30
+ }
31
+ }
32
+
33
+ private async getClient() {
34
+ if (!this.client) {
35
+ this.client = new Redis(this.redisUrl);
36
+ }
37
+ return this.client;
38
+ }
39
+
40
+ private jobKey(jobId: string): string {
41
+ return `kasw:job:${jobId}`;
42
+ }
43
+
44
+ private taskKey(taskId: string): string {
45
+ return `kasw:task:${taskId}`;
46
+ }
47
+
48
+ private queueKey(jobId: string): string {
49
+ return `kasw:queue:${jobId}`;
50
+ }
51
+
52
+ async createJob(
53
+ job: Omit<DistributedJob, "jobId" | "createdAt" | "updatedAt">,
54
+ ): Promise<DistributedJob> {
55
+ const client = await this.getClient();
56
+ const now = new Date().toISOString();
57
+ const created: DistributedJob = {
58
+ ...job,
59
+ jobId: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
60
+ createdAt: now,
61
+ updatedAt: now,
62
+ };
63
+
64
+ await client.set(this.jobKey(created.jobId), JSON.stringify(created));
65
+ for (const task of created.tasks) {
66
+ await client.set(this.taskKey(task.taskId), JSON.stringify(task));
67
+ await client.rpush(this.queueKey(created.jobId), task.taskId);
68
+ }
69
+
70
+ return created;
71
+ }
72
+
73
+ async getJob(jobId: string): Promise<DistributedJob | undefined> {
74
+ const client = await this.getClient();
75
+ const text = await client.get(this.jobKey(jobId));
76
+ if (!text) return undefined;
77
+ return JSON.parse(text) as DistributedJob;
78
+ }
79
+
80
+ async saveJob(job: DistributedJob): Promise<void> {
81
+ const client = await this.getClient();
82
+ const updated = { ...job, updatedAt: new Date().toISOString() };
83
+ await client.set(this.jobKey(job.jobId), JSON.stringify(updated));
84
+ for (const task of updated.tasks) {
85
+ await client.set(this.taskKey(task.taskId), JSON.stringify(task));
86
+ }
87
+ }
88
+
89
+ async claimNextTask(
90
+ jobId: string,
91
+ workerId: string,
92
+ ): Promise<DistributedTask | undefined> {
93
+ const client = await this.getClient();
94
+ const taskId = await client.lpop(this.queueKey(jobId));
95
+ if (!taskId) return undefined;
96
+
97
+ const taskText = await client.get(this.taskKey(taskId));
98
+ if (!taskText) return undefined;
99
+
100
+ const task = JSON.parse(taskText) as DistributedTask;
101
+ task.status = "running";
102
+ task.workerId = workerId;
103
+ task.attempts += 1;
104
+ task.startedAt = new Date().toISOString();
105
+
106
+ await client.set(this.taskKey(task.taskId), JSON.stringify(task));
107
+ return task;
108
+ }
109
+
110
+ async completeTask(taskId: string, result: WorkerResult): Promise<void> {
111
+ const client = await this.getClient();
112
+ const taskText = await client.get(this.taskKey(taskId));
113
+ if (!taskText) return;
114
+
115
+ const task = JSON.parse(taskText) as DistributedTask;
116
+ task.status = "completed";
117
+ task.result = result;
118
+ task.completedAt = new Date().toISOString();
119
+ task.error = undefined;
120
+ await client.set(this.taskKey(taskId), JSON.stringify(task));
121
+
122
+ const job = await this.getJob(task.jobId);
123
+ if (job) {
124
+ const idx = job.tasks.findIndex((t) => t.taskId === taskId);
125
+ if (idx >= 0) {
126
+ job.tasks[idx] = task;
127
+ this.updateJobStatus(job);
128
+ await this.saveJob(job);
129
+ }
130
+ }
131
+ }
132
+
133
+ async failTask(taskId: string, error: string): Promise<void> {
134
+ const client = await this.getClient();
135
+ const taskText = await client.get(this.taskKey(taskId));
136
+ if (!taskText) return;
137
+
138
+ const task = JSON.parse(taskText) as DistributedTask;
139
+ task.error = error;
140
+
141
+ if (task.attempts >= task.maxRetries) {
142
+ task.status = "failed";
143
+ task.completedAt = new Date().toISOString();
144
+ } else {
145
+ task.status = "pending";
146
+ task.workerId = undefined;
147
+ task.startedAt = undefined;
148
+ await client.rpush(this.queueKey(task.jobId), task.taskId);
149
+ }
150
+
151
+ await client.set(this.taskKey(taskId), JSON.stringify(task));
152
+
153
+ const job = await this.getJob(task.jobId);
154
+ if (job) {
155
+ const idx = job.tasks.findIndex((t) => t.taskId === taskId);
156
+ if (idx >= 0) {
157
+ job.tasks[idx] = task;
158
+ this.updateJobStatus(job);
159
+ await this.saveJob(job);
160
+ }
161
+ }
162
+ }
163
+
164
+ async getPendingTaskCount(jobId: string): Promise<number> {
165
+ const client = await this.getClient();
166
+ return client.llen(this.queueKey(jobId));
167
+ }
168
+
169
+ async getRunningTaskCount(_jobId: string): Promise<number> {
170
+ // Running tasks are not tracked separately in this simple Redis adapter.
171
+ return 0;
172
+ }
173
+
174
+ private updateJobStatus(job: DistributedJob): void {
175
+ if (job.tasks.every((t) => t.status === "completed")) {
176
+ job.status = "completed";
177
+ } else if (job.tasks.some((t) => t.status === "running")) {
178
+ job.status = "running";
179
+ } else if (job.tasks.every((t) => t.status === "failed")) {
180
+ job.status = "failed";
181
+ } else {
182
+ job.status = "running";
183
+ }
184
+ }
185
+ }