# Wire All 4 Microservice Agents Into Chat
Wires all 4 microservice-backed agents into the chat so the LLM can call real services and return session URLs.
---
## Changes
### New
* `src/routes/chat.ts`
* Added a direct HTTP chat endpoint.
* When the LLM calls:
* `start_interview_session`
* `analyze_resume`
* `start_roleplay_session`
* `compute_qscore`
* The route executes real service probes and returns live session URLs.
---
### Fixed
* `src/index.ts`
* Rivet proxy now forwards requests to the engine at `localhost:6420`
instead of using `registry.handler()`.
* Prevents the:
```txt
Runtime already started as runner
```
conflict.
* `src/actors/user-actor.ts`
* `receiveMessage()` now returns:
```ts
{
reply,
sessions: []
}
```
* Includes per-module session URLs in responses.
* `docker-compose.yml`
* Fixed:
* Gitea health check port
* Port mapping
* `A2A_ALLOWED_KEY` default value
* `src/config.ts`
* Added:
```ts
resumeServiceUrl
```
* Configured to use port `8002`.
---
### Rewritten
* `prompts/system.txt`
* Reworked into a conversational step-by-step flow.
* Added explicit rule:
> CALL THE TOOL IMMEDIATELY
---
### Updated
* `agents/*.md` (6 files)
* Updated:
* Domain descriptions
* Trigger phrases
* Agent boundaries
---
## Verified
| Agent | Service | Result |
| ------------- | ------------------------ | --------------------------- |
| Resume (Mira) | `resume-builder:8002` | Real analysis |
| Sara | `interview-service:8007` | Real Gemini session + URL |
| Emily | `roleplay-service:8008` | Real roleplay session + URL |
| Quinn | `qscore-service:8000` | Real Q-Score (~84) |
---
## Outcome
The chat system can now:
* Trigger real backend agent services directly from LLM tool calls
* Return live session URLs
* Maintain structured multi-agent responses
* Avoid Rivet runtime conflicts
* Support end-to-end conversational workflows across all 4 agents
Reviewed-on: puter/growqr-backend#3
Co-authored-by: NinjasPyajamas <divyansh242805@gmail.com>
Co-committed-by: NinjasPyajamas <divyansh242805@gmail.com>
175 lines
5.3 KiB
TypeScript
175 lines
5.3 KiB
TypeScript
import { sql } from "drizzle-orm";
|
|
import {
|
|
pgTable,
|
|
text,
|
|
timestamp,
|
|
integer,
|
|
jsonb,
|
|
uniqueIndex,
|
|
index,
|
|
primaryKey,
|
|
} from "drizzle-orm/pg-core";
|
|
|
|
// Users are mirrored from Clerk on first sign-in.
|
|
// id = Clerk user id (e.g., "user_2abc..."), email is the canonical Clerk email.
|
|
export const users = pgTable(
|
|
"users",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
email: text("email").notNull(),
|
|
displayName: text("display_name"),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => ({
|
|
emailIdx: uniqueIndex("users_email_idx").on(t.email),
|
|
}),
|
|
);
|
|
|
|
// One per user. Tracks the user's unified agent's container stack + Git repo.
|
|
// Per changes.md §2A: per-user Gitea containers removed; central Gitea shared.
|
|
// Per changes.md §5: ONE actor per user manages the full orchestration layer.
|
|
export const userStacks = pgTable(
|
|
"user_stacks",
|
|
{
|
|
userId: text("user_id")
|
|
.primaryKey()
|
|
.references(() => users.id, { onDelete: "cascade" }),
|
|
status: text("status", {
|
|
enum: ["provisioning", "running", "stopped", "error"],
|
|
})
|
|
.notNull()
|
|
.default("provisioning"),
|
|
|
|
// Central Gitea (shared org-wide, changes.md §2A).
|
|
giteaRepoName: text("gitea_repo_name"),
|
|
giteaRepoOwner: text("gitea_repo_owner"),
|
|
|
|
// Per-user OpenCode container (from shared image, changes.md §3).
|
|
opencodeContainerId: text("opencode_container_id"),
|
|
opencodeContainerName: text("opencode_container_name"),
|
|
opencodeHost: text("opencode_host"),
|
|
opencodePort: integer("opencode_port"),
|
|
opencodePassword: text("opencode_password"),
|
|
|
|
workspacePath: text("workspace_path"),
|
|
|
|
// Version tracking for image rollouts (changes.md §9).
|
|
imageVersion: text("image_version"),
|
|
migrationVersion: text("migration_version"),
|
|
promptVersion: text("prompt_version"),
|
|
|
|
lastError: text("last_error"),
|
|
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => ({
|
|
statusIdx: index("user_stacks_status_idx").on(t.status),
|
|
}),
|
|
);
|
|
|
|
// Per changes.md §5: ONE unified actor per user (no separate grow/sub actors).
|
|
// The actor manages: infra state, git state, runtime comms, migrations, API orchestration.
|
|
export const actors = pgTable(
|
|
"actors",
|
|
{
|
|
actorId: text("actor_id").notNull(),
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: "cascade" }),
|
|
kind: text("kind", { enum: ["user"] }).notNull().default("user"),
|
|
status: text("status", {
|
|
enum: ["idle", "running", "done", "error"],
|
|
})
|
|
.notNull()
|
|
.default("idle"),
|
|
lastActivityAt: timestamp("last_activity_at", { withTimezone: true }),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => ({
|
|
pk: primaryKey({ columns: [t.userId, t.actorId] }),
|
|
kindIdx: index("actors_user_kind_idx").on(t.userId, t.kind),
|
|
}),
|
|
);
|
|
|
|
// Per-user repo registry (in addition to the primary memory repo).
|
|
export const repos = pgTable(
|
|
"repos",
|
|
{
|
|
id: text("id").primaryKey(), // `${userId}:${name}`
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: "cascade" }),
|
|
name: text("name").notNull(),
|
|
role: text("role", { enum: ["memory", "project"] }).notNull(),
|
|
giteaOwner: text("gitea_owner").notNull(),
|
|
giteaName: text("gitea_name").notNull(),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => ({
|
|
userRoleIdx: index("repos_user_role_idx").on(t.userId, t.role),
|
|
}),
|
|
);
|
|
|
|
// OpenCode sessions opened by sub-agents.
|
|
export const opencodeSessions = pgTable(
|
|
"opencode_sessions",
|
|
{
|
|
id: text("id").primaryKey(), // OpenCode session id from POST /session
|
|
userId: text("user_id")
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: "cascade" }),
|
|
actorId: text("actor_id"),
|
|
title: text("title"),
|
|
parentId: text("parent_id"),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => ({
|
|
userIdx: index("opencode_sessions_user_idx").on(t.userId),
|
|
}),
|
|
);
|
|
|
|
// Audit/event log — small append-only stream used for debugging + the
|
|
// frontend's "task progress timeline" until we move it to Rivet streams only.
|
|
export const events = pgTable(
|
|
"events",
|
|
{
|
|
id: text("id")
|
|
.primaryKey()
|
|
.default(sql`gen_random_uuid()::text`),
|
|
userId: text("user_id").notNull(),
|
|
actorId: text("actor_id"),
|
|
type: text("type").notNull(),
|
|
payload: jsonb("payload").$type<Record<string, unknown>>(),
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.defaultNow()
|
|
.notNull(),
|
|
},
|
|
(t) => ({
|
|
userIdx: index("events_user_idx").on(t.userId, t.createdAt),
|
|
}),
|
|
);
|
|
|
|
export type User = typeof users.$inferSelect;
|
|
export type NewUser = typeof users.$inferInsert;
|
|
export type UserStack = typeof userStacks.$inferSelect;
|
|
export type NewUserStack = typeof userStacks.$inferInsert;
|
|
export type ActorRow = typeof actors.$inferSelect;
|
|
export type RepoRow = typeof repos.$inferSelect;
|
|
export type OpencodeSessionRow = typeof opencodeSessions.$inferSelect;
|