Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 7 additions & 2 deletions apps/cli/src/commands/service/worker.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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 { 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 +24,14 @@ 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 initDatabase();
Comment thread
soorya-u marked this conversation as resolved.
Resolved

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

const signalingSession = await connectSignaling({
Expand All @@ -53,6 +57,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
55 changes: 41 additions & 14 deletions apps/cli/src/handlers/controller/chat.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,58 @@
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,
Expand All @@ -34,7 +61,7 @@ export function chatHandlers({ os, runtime }: ControllerDeps) {
message
);
for await (const event of gen) {
yield emit(event);
yield await emit(event);
await Bun.sleep(env.CYRUS_STREAM_THROTTLING_MS);
}
}),
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