Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"cyrusd": "src/cli.ts"
},
"scripts": {
"cli": "bun src/cli.ts",
"dev": "bun src/cli.ts",
"check:types": "tsc --noEmit",
"build": "bun build src/cli.ts --compile --env 'CLI_PUBLIC_*' --outfile dist/cyrusd"
},
Expand All @@ -16,13 +16,15 @@
"@agentclientprotocol/sdk": "^1.1.0",
"@commander-js/extra-typings": "^15.0.0",
"@cyrus/connections": "workspace:*",
"@cyrus/database": "workspace:*",
"@cyrus/utils": "workspace:*",
"@tursodatabase/database": "^0.6.1",
"@orpc/server": "catalog:",
"@t3-oss/env-core": "catalog:",
"better-auth": "catalog:",
"better-result": "catalog:",
"commander": "^15.0.0",
"diff": "^7.0.0",
"random-word-slugs": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/commands/auth/login.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { generateName } from "@cyrus/utils/identity";
import { Result } from "better-result";
import { authClient } from "@/lib/auth";
import { getOrCreate, set } from "@/store/config";
import { generateName } from "@/utils/identity";
import { blue, bold, cyan, print, underline } from "@/utils/style";

export const CLIENT_ID = "cyrus-cli";
Expand Down
13 changes: 11 additions & 2 deletions apps/cli/src/commands/service/worker.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { connectSignaling } from "@cyrus/connections/rtc/session";
import { serveWorker } from "@cyrus/connections/rtc/worker";
import { connection } from "@cyrus/database/connection";
import { generateName, randomId } from "@cyrus/utils/identity";
import { Result } from "better-result";
import { createWorkerRuntime } from "@/core";
import { createControllerRouter } from "@/handlers/controller";
import { workerRouter } from "@/handlers/worker";
import { authClient } from "@/lib/auth";
import { env } from "@/lib/env";
import { get, getOrCreate } from "@/store/config";
import { generateId, generateName } from "@/utils/identity";
import { initDatabase } from "@/store/database";
import { print } from "@/utils/style";

export async function worker(): Promise<void> {
Expand All @@ -22,12 +25,17 @@ export async function worker(): Promise<void> {
process.exit(1);
}

const id = await getOrCreate("id", generateId);
const id = await getOrCreate("id", randomId);
const name = await getOrCreate("name", generateName);
const room = session.user.id;

const runtime = createWorkerRuntime();

(await Result.tryPromise(() => initDatabase())).tapError((err) => {
print.error`Failed to initialize database: ${String(err)}`;
process.exit(1);
});

print.dim`worker "${name}" joining hub`;

const signalingSession = await connectSignaling({
Expand All @@ -53,6 +61,7 @@ export async function worker(): Promise<void> {
await runtime.agentPool.shutdown();
device.close();
signalingSession.close();
await connection.close();
process.exit(0);
};
process.on("SIGINT", shutdown);
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/constants/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export const CONFIG_FILE = "config.yml";
export const AGENTS_FILE = "agents.yml";
export const WORKER_PID = "worker.pid";
export const WORKER_LOG = "worker.log";
export const DATABASE_FILE = "store.db";
18 changes: 18 additions & 0 deletions apps/cli/src/core/acp/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import {
type AgentEvent,
AgentEventSchema,
ApprovalRequestEventSchema,
MessageCompletedEventSchema,
PlanEventSchema,
PlanRemovedEventSchema,
PlanUpdateEventSchema,
ReasoningCompletedEventSchema,
ThoughtEventSchema,
TokenEventSchema,
ToolCallEventSchema,
Expand All @@ -32,6 +34,14 @@ export function mapRuntimeSessionEvent(
messageId: event.messageId,
}),
];
case "message.completed":
return [
MessageCompletedEventSchema.parse({
type: "message_completed",
text: event.content,
messageId: event.messageId,
}),
];
case "reasoning.delta":
return [
ThoughtEventSchema.parse({
Expand All @@ -40,6 +50,14 @@ export function mapRuntimeSessionEvent(
messageId: event.reasoningId,
}),
];
case "reasoning.completed":
return [
ReasoningCompletedEventSchema.parse({
type: "reasoning_completed",
text: event.content,
messageId: event.reasoningId,
}),
];
case "tool.start":
return [
ToolCallEventSchema.parse({
Expand Down
21 changes: 15 additions & 6 deletions apps/cli/src/core/threads/coordinator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { ModelOption } from "@cyrus/connections/schemas/rtc/catalog";
import type { AgentEvent } from "@cyrus/connections/schemas/rtc/chat";
import type { SelectOption } from "@cyrus/connections/schemas/rtc/common";
import { resolveProjectCwd } from "@cyrus/database/repositories/projects";
import { repositoryErrorMessage } from "@cyrus/database/utils/error";
import type { AgentPool } from "@/core/acp/pool";
import { AgentRuntime } from "@/core/agents/runtime";
import { resolveProjectCwd } from "@/store/projects";

export class ThreadCoordinator {
private readonly agents = new Map<string, AgentRuntime>();
Expand Down Expand Up @@ -44,7 +45,7 @@ export class ThreadCoordinator {
projectId: string,
modelId: string
): Promise<void> {
const cwd = resolveProjectCwd(projectId);
const cwd = await this.resolveCwd(projectId);
await this.getAgent(agentName).setModel(threadId, projectId, cwd, modelId);
}

Expand All @@ -54,7 +55,7 @@ export class ThreadCoordinator {
projectId: string,
modeId: string
): Promise<void> {
const cwd = resolveProjectCwd(projectId);
const cwd = await this.resolveCwd(projectId);
await this.getAgent(agentName).setMode(threadId, projectId, cwd, modeId);
}

Expand All @@ -64,7 +65,7 @@ export class ThreadCoordinator {
projectId: string,
effortId: string
): Promise<void> {
const cwd = resolveProjectCwd(projectId);
const cwd = await this.resolveCwd(projectId);
await this.getAgent(agentName).setEffort(
threadId,
projectId,
Expand All @@ -79,7 +80,7 @@ export class ThreadCoordinator {
projectId: string,
personaId: string
): Promise<void> {
const cwd = resolveProjectCwd(projectId);
const cwd = await this.resolveCwd(projectId);
await this.getAgent(agentName).setPersona(
threadId,
projectId,
Expand All @@ -94,14 +95,22 @@ export class ThreadCoordinator {
projectId: string,
content: string
): AsyncGenerator<AgentEvent> {
const cwd = resolveProjectCwd(projectId);
const cwd = await this.resolveCwd(projectId);
yield* this.getAgent(agentName).prompt(threadId, projectId, cwd, content);
}

async cancel(agentName: string, threadId: string): Promise<void> {
await this.getAgent(agentName).cancel(threadId);
}

private async resolveCwd(projectId: string): Promise<string> {
const result = await resolveProjectCwd(projectId);
if (result.isErr()) {
throw new Error(repositoryErrorMessage(result.error));
}
return result.value;
}

async close(agentName: string, threadId: string): Promise<void> {
await this.getAgent(agentName).close(threadId);
}
Expand Down
65 changes: 49 additions & 16 deletions apps/cli/src/handlers/controller/chat.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,74 @@
import type { ChatChunk } from "@cyrus/connections/schemas/rtc/chat";
import { appendConversation } from "@cyrus/database/repositories/conversations";
import { ensureThread } from "@cyrus/database/repositories/threads";
import { randomId } from "@cyrus/utils/identity";
import { env } from "@/lib/env";
import { appendConversation, ensureThread } from "@/store/threads";
import { throwOrpcFromRepositoryError } from "@/utils/error";
import {
isStreamingDelta,
resolvePersistEvent,
trackDelta,
} from "@/utils/streams";
import type { ControllerDeps } from "./deps";

export function chatHandlers({ os, runtime }: ControllerDeps) {
return {
chat: os.chat.handler(async function* ({ input, context }) {
const {
const { agentName, threadId = randomId(), message, projectId } = input;

const thread = await ensureThread(threadId, projectId, {
agentName,
threadId = Bun.randomUUIDv7(),
message,
projectId,
} = input;
firstMessage: message,
});
if (thread.isErr()) throwOrpcFromRepositoryError(thread.error);

const turnId = randomId();
const messageBuffers = new Map<string, string>();
const thoughtBuffers = new Map<string, string>();

ensureThread(threadId, projectId, { agentName, firstMessage: message });
async function emit(event: ChatChunk["event"]): Promise<ChatChunk> {
trackDelta(event, messageBuffers, thoughtBuffers);

const turnId = Bun.randomUUIDv7();
if (isStreamingDelta(event)) {
const chunk: ChatChunk = { threadId, turnId, seq: 0, event };
context.broadcaster.broadcast(chunk, context.peerId);
return chunk;
}

function emit(event: ChatChunk["event"]): ChatChunk {
const chunk = { threadId, turnId, event };
const persistEvent = resolvePersistEvent(
event,
messageBuffers,
thoughtBuffers
);
const entry = await appendConversation(threadId, {
threadId,
turnId,
event: persistEvent,
});
if (entry.isErr()) throwOrpcFromRepositoryError(entry.error);
const chunk = entry.value.chunk;
context.broadcaster.broadcast(chunk, context.peerId);
appendConversation(threadId, chunk);
return chunk;
}

yield emit({ type: "user_message", content: message });
yield emit({ type: "thread_started", threadId });
yield await emit({ type: "user_message", content: message });
yield await emit({ type: "thread_started", threadId });

const gen = runtime.threadCoordinator.prompt(
agentName,
threadId,
projectId,
message
);
for await (const event of gen) {
yield emit(event);
await Bun.sleep(env.CYRUS_STREAM_THROTTLING_MS);
try {
for await (const event of gen) {
yield await emit(event);
await Bun.sleep(env.CYRUS_STREAM_THROTTLING_MS);
}
yield await emit({ type: "turn_completed" });
} catch (error) {
yield await emit({ type: "turn_interrupted" });
throw error;
}
}),

Expand Down
52 changes: 29 additions & 23 deletions apps/cli/src/handlers/controller/projects.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,49 @@
import { ORPCError } from "@orpc/server";
import { Result } from "better-result";
import {
createProject as createStoredProject,
deleteProject as deleteStoredProject,
listProjects,
renameProject,
} from "@/store/projects";
import { deleteThreadsForProject } from "@/store/threads";
} from "@cyrus/database/repositories/projects";
import { deleteThreadsForProject } from "@cyrus/database/repositories/threads";
import { throwOrpcFromRepositoryError } from "@/utils/error";
import type { ControllerOs } from "./deps";

export function projectsHandlers(os: ControllerOs) {
return {
listProjects: os.listProjects.handler(async () => ({
projects: listProjects(),
})),
listProjects: os.listProjects.handler(async () =>
(await listProjects()).match({
ok: (projects) => ({ projects }),
err: throwOrpcFromRepositoryError,
})
),

createProject: os.createProject.handler(({ input }) => ({
project: createStoredProject(input.name, input.cwd),
})),
createProject: os.createProject.handler(async ({ input }) =>
(await createStoredProject(input.name, input.cwd)).match({
ok: (project) => ({ project }),
err: throwOrpcFromRepositoryError,
})
),

renameProject: os.renameProject.handler(({ input }) =>
Result.try(() => renameProject(input.projectId, input.name)).match({
renameProject: os.renameProject.handler(async ({ input }) =>
(await renameProject(input.projectId, input.name)).match({
ok: () => ({}),
err: () => {
throw new ORPCError("NOT_FOUND", {
message: `project not found: ${input.projectId}`,
});
},
err: throwOrpcFromRepositoryError,
})
),

deleteProject: os.deleteProject.handler(({ input }) => {
const deleted = deleteStoredProject(input.projectId);
if (!deleted)
throw new ORPCError("NOT_FOUND", {
message: `project not found: ${input.projectId}`,
deleteProject: os.deleteProject.handler(async ({ input }) => {
const deleted = await deleteStoredProject(input.projectId);
if (deleted.isErr()) throwOrpcFromRepositoryError(deleted.error);
if (!deleted.value) {
throwOrpcFromRepositoryError({
type: "not_found",
entity: "project",
id: input.projectId,
});
}

deleteThreadsForProject(input.projectId);
const threads = await deleteThreadsForProject(input.projectId);
if (threads.isErr()) throwOrpcFromRepositoryError(threads.error);
return {};
}),
};
Expand Down
Loading
Loading