diff --git a/biome.json b/biome.json index eb60de4..8387b60 100644 --- a/biome.json +++ b/biome.json @@ -19,7 +19,7 @@ }, "overrides": [ { - "include": ["tsup.config.ts", "*/rslib.config.ts"], + "include": ["tsup.config.ts", "*/rslib.config.ts", "packages/khala/src/index.ts"], "linter": { "rules": { "style": { diff --git a/packages/khala/src/__tests__/result.test.ts b/packages/khala/src/__tests__/result.test.ts new file mode 100644 index 0000000..b183df5 --- /dev/null +++ b/packages/khala/src/__tests__/result.test.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from "vitest"; +import { ok, err } from "../result.js"; + +describe("Result", () => { + it("ok wraps a value", () => { + const r = ok(42); + expect(r.ok).toBe(true); + if (r.ok) expect(r.value).toBe(42); + }); + + it("err wraps an error", () => { + const r = err("boom"); + expect(r.ok).toBe(false); + if (!r.ok) expect(r.error).toBe("boom"); + }); +}); diff --git a/packages/khala/src/auth.ts b/packages/khala/src/auth.ts index 21e23d3..a8e435f 100644 --- a/packages/khala/src/auth.ts +++ b/packages/khala/src/auth.ts @@ -33,9 +33,10 @@ export const agentAuth = createMiddleware<{ return next(); }); -export function requireAdmin( - c: { req: { header: (k: string) => string | undefined }; env: KhalaBindings }, -): boolean { +export function requireAdmin(c: { + req: { header: (k: string) => string | undefined }; + env: KhalaBindings; +}): boolean { const header = c.req.header("Authorization"); const expected = c.env.ADMIN_SECRET; if (expected.length === 0) { diff --git a/packages/khala/src/db.ts b/packages/khala/src/db.ts index cc084a8..c49db59 100644 --- a/packages/khala/src/db.ts +++ b/packages/khala/src/db.ts @@ -1,7 +1,7 @@ -import { monotonicFactory, type ULID } from "ulidx"; -import type { Agent, GetThreadMessagesOpts, Message, Task, Thread } from "./types.js"; +import { type ULID, monotonicFactory } from "ulidx"; import type { Result } from "./result.js"; import { err, ok } from "./result.js"; +import type { Agent, GetThreadMessagesOpts, Message, Task, Thread } from "./types.js"; const generateUlid = monotonicFactory((): number => Date.now()); @@ -70,9 +70,7 @@ export async function setThreadResult( status: "completed" | "failed", ): Promise { await db - .prepare( - `UPDATE threads SET result = ?, status = ?, updated_at = datetime('now') WHERE id = ?`, - ) + .prepare(`UPDATE threads SET result = ?, status = ?, updated_at = datetime('now') WHERE id = ?`) .bind(result, status, id) .run(); } @@ -98,9 +96,7 @@ export async function appendMessage( .bind(threadId, role, content, meta, step, agentId) .run(); const row = await db - .prepare( - `SELECT * FROM messages WHERE thread_id = ? AND step = ? ORDER BY id DESC LIMIT 1`, - ) + .prepare("SELECT * FROM messages WHERE thread_id = ? AND step = ? ORDER BY id DESC LIMIT 1") .bind(threadId, step) .first(); if (!row) { @@ -219,7 +215,10 @@ type TaskRow = { function rowToTask(row: TaskRow): Task { const status = - row.status === "open" || row.status === "claimed" || row.status === "completed" || row.status === "expired" + row.status === "open" || + row.status === "claimed" || + row.status === "completed" || + row.status === "expired" ? row.status : "open"; return { @@ -265,7 +264,11 @@ export async function getTaskById(db: D1Database, id: string): Promise { +export async function completeTask( + db: D1Database, + taskId: string, + claimId: string, +): Promise { const res = await db .prepare( `UPDATE tasks SET status = 'completed' WHERE id = ? AND claim_id = ? AND status = 'claimed'`, @@ -277,7 +280,11 @@ export async function completeTask(db: D1Database, taskId: string, claimId: stri return res.success === true; } -export async function releaseTask(db: D1Database, taskId: string, claimId: string): Promise { +export async function releaseTask( + db: D1Database, + taskId: string, + claimId: string, +): Promise { const res = await db .prepare( `UPDATE tasks @@ -314,9 +321,7 @@ export async function getOpenTasks( const cap = Math.min(500, Math.max(1, limit)); if (workflow === null) { const st = await db - .prepare( - `SELECT t.* FROM tasks t WHERE t.status = 'open' ORDER BY t.created_at ASC LIMIT ?`, - ) + .prepare(`SELECT t.* FROM tasks t WHERE t.status = 'open' ORDER BY t.created_at ASC LIMIT ?`) .bind(cap) .all(); const results = (st.results as TaskRow[] | null) ?? []; @@ -350,10 +355,7 @@ export async function registerAgent( .prepare("INSERT INTO agents (id, token_hash, created_at) VALUES (?, ?, datetime('now'))") .bind(id, tokenHash) .run(); - const row = await db - .prepare("SELECT * FROM agents WHERE id = ?") - .bind(id) - .first(); + const row = await db.prepare("SELECT * FROM agents WHERE id = ?").bind(id).first(); if (!row) return err("registerAgent: insert failed"); return ok(row); } diff --git a/packages/khala/src/index.ts b/packages/khala/src/index.ts index 6dd081f..02d3383 100644 --- a/packages/khala/src/index.ts +++ b/packages/khala/src/index.ts @@ -1,18 +1,17 @@ import { Hono } from "hono"; +import { agentAuth } from "./auth.js"; +import { + claimTask, + createThread, + expireTimedOutTasks, + getOpenTasks, + getTaskById, + getThread, + releaseTask, +} from "./db.js"; import type { KhalaBindings } from "./env.js"; import { admin } from "./routes/admin.js"; -import { agentAuth } from "./auth.js"; import { ThreadDO } from "./thread-do.js"; -import { - createThread, - getThread, - getThreadMessages, - getOpenTasks, - claimTask, - releaseTask, - getTaskById, - expireTimedOutTasks, -} from "./db.js"; import { getWorkflow } from "./workflows.js"; // Register built-in workflows diff --git a/packages/khala/src/routes/admin.ts b/packages/khala/src/routes/admin.ts index 8f0bc57..1e231ca 100644 --- a/packages/khala/src/routes/admin.ts +++ b/packages/khala/src/routes/admin.ts @@ -1,8 +1,8 @@ import { Hono } from "hono"; import { requireAdmin, sha256Hex } from "../auth.js"; -import { registerAgent, deleteAgent, listAgents } from "../db.js"; -import type { Result } from "../result.js"; +import { deleteAgent, listAgents, registerAgent } from "../db.js"; import type { KhalaBindings } from "../env.js"; +import type { Result } from "../result.js"; const admin = new Hono<{ Bindings: KhalaBindings }>(); @@ -32,7 +32,12 @@ admin.post("/agents", async (c) => { const body = raw as Readonly>; const id = body.id; const token = body.token; - if (typeof id !== "string" || id.length === 0 || typeof token !== "string" || token.length === 0) { + if ( + typeof id !== "string" || + id.length === 0 || + typeof token !== "string" || + token.length === 0 + ) { return c.json({ error: "id and token required" }, 400); } const hash = await sha256Hex(token); diff --git a/packages/khala/src/thread-do.ts b/packages/khala/src/thread-do.ts index 3c4b8c4..9f5f187 100644 --- a/packages/khala/src/thread-do.ts +++ b/packages/khala/src/thread-do.ts @@ -34,13 +34,15 @@ function buildSteps(msgs: Awaited>): StepRo .map((m) => ({ role: m.role, content: m.content, - meta: m.meta ? (() => { - try { - return JSON.parse(m.meta) as unknown; - } catch { - return m.meta; - } - })() : null, + meta: m.meta + ? (() => { + try { + return JSON.parse(m.meta) as unknown; + } catch { + return m.meta; + } + })() + : null, })); } @@ -118,8 +120,16 @@ export class ThreadDO { const content = body.content; const claimId = body.claimId; const agentId = body.agentId; - if (typeof taskId !== "string" || typeof content !== "string" || typeof claimId !== "string" || typeof agentId !== "string") { - return Response.json({ error: "taskId, content, claimId, agentId required" }, { status: 400 }); + if ( + typeof taskId !== "string" || + typeof content !== "string" || + typeof claimId !== "string" || + typeof agentId !== "string" + ) { + return Response.json( + { error: "taskId, content, claimId, agentId required" }, + { status: 400 }, + ); } const db = this.env.DB; const task = await getTaskForThreadAndClaim(db, threadId, taskId, claimId); @@ -157,10 +167,7 @@ export class ThreadDO { return Response.json({ ok: true }); } - private async listMessages( - url: URL, - threadId: string, - ): Promise { + private async listMessages(url: URL, threadId: string): Promise { const roleP = url.searchParams.get("role"); const minStepRaw = url.searchParams.get("minStep"); const maxStepRaw = url.searchParams.get("maxStep"); diff --git a/packages/khala/worker-configuration.d.ts b/packages/khala/worker-configuration.d.ts index 0c14531..ee83009 100644 --- a/packages/khala/worker-configuration.d.ts +++ b/packages/khala/worker-configuration.d.ts @@ -13,5 +13,3 @@ declare module "cloudflare:workers" { } interface ProvidedEnv extends Env {} } - -export {};