feat: introduce workflow job management and agent orchestration
- Added workflow job actor to manage job application workflows. - Implemented agent catalog for various workflow agents. - Created service agents for interview, roleplay, and Q-Score functionalities. - Enhanced user authentication to automatically create users if they do not exist. - Updated configuration to support new LLM provider and API keys. - Introduced new routes for agent and workflow management. - Refactored Docker management to improve Gitea admin user creation and token generation. - Removed deprecated Anthropics SDK integration.
This commit is contained in:
22
.env.example
22
.env.example
@@ -9,20 +9,30 @@ POSTGRES_PASSWORD=growqr
|
|||||||
POSTGRES_DB=growqr
|
POSTGRES_DB=growqr
|
||||||
|
|
||||||
# Clerk auth — get from dashboard.clerk.com → API Keys
|
# Clerk auth — get from dashboard.clerk.com → API Keys
|
||||||
CLERK_SECRET_KEY=sk_test_REPLACE_ME
|
CLERK_SECRET_KEY=clerk_key
|
||||||
CLERK_PUBLISHABLE_KEY=pk_test_REPLACE_ME
|
CLERK_PUBLISHABLE_KEY=clerk_publishable_key
|
||||||
|
|
||||||
# Anthropic — get from console.anthropic.com → API Keys
|
# OpenCode Zen LLM gateway — get from opencode.ai/auth
|
||||||
ANTHROPIC_API_KEY=sk-ant-REPLACE_ME
|
OPENCODE_API_KEY=sk-REPLACE_ME
|
||||||
GROW_AGENT_MODEL=claude-opus-4-7
|
LLM_PROVIDER=opencode
|
||||||
SUB_AGENT_MODEL=claude-sonnet-4-6
|
LLM_BASE_URL=https://opencode.ai/zen/v1
|
||||||
|
LLM_MODEL=kimi-k2.6
|
||||||
|
GROW_AGENT_MODEL=kimi-k2.6
|
||||||
|
SUB_AGENT_MODEL=kimi-k2.6
|
||||||
MAX_AGENT_TOKENS=4096
|
MAX_AGENT_TOKENS=4096
|
||||||
|
|
||||||
# Shared secret for actor → backend service calls (rotate in prod)
|
# Shared secret for actor → backend service calls (rotate in prod)
|
||||||
SERVICE_TOKEN=dev-service-token-REPLACE_ME
|
SERVICE_TOKEN=dev-service-token-REPLACE_ME
|
||||||
|
A2A_ALLOWED_KEY=dev-a2a-key
|
||||||
|
|
||||||
# Rivet Kit engine (self-hosted in docker-compose)
|
# Rivet Kit engine (self-hosted in docker-compose)
|
||||||
RIVET_ENDPOINT=http://localhost:6420
|
RIVET_ENDPOINT=http://localhost:6420
|
||||||
|
RIVET_CLIENT_ENDPOINT=http://127.0.0.1:4000/api/rivet
|
||||||
|
|
||||||
|
# Product microservice sub-agent URLs
|
||||||
|
INTERVIEW_SERVICE_URL=http://localhost:8007
|
||||||
|
ROLEPLAY_SERVICE_URL=http://localhost:8008
|
||||||
|
QSCORE_SERVICE_URL=http://localhost:8000
|
||||||
|
|
||||||
# Per-user container images
|
# Per-user container images
|
||||||
GITEA_IMAGE=gitea/gitea:1.22
|
GITEA_IMAGE=gitea/gitea:1.22
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
.data
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ A multi-agent platform where every user gets a private Grow Agent (Rivet Kit act
|
|||||||
|
|
||||||
- **Auth**: Clerk (frontend + backend JWT verification).
|
- **Auth**: Clerk (frontend + backend JWT verification).
|
||||||
- **DB**: Postgres + Drizzle (users, actor registry, container mappings, repos, OpenCode sessions, events).
|
- **DB**: Postgres + Drizzle (users, actor registry, container mappings, repos, OpenCode sessions, events).
|
||||||
- **Actors**: Rivet Kit — `growAgent` per user (master) and `subAgent` (worker), with a real Anthropic Claude tool-use loop.
|
- **Actors**: Rivet Kit — `growAgent` per user (master) and `subAgent` (worker), with a real OpenCode Zen / OpenAI-compatible tool-use loop.
|
||||||
- **Per-user containers**: Gitea (memory repo) + OpenCode (workflow execution), spawned via `dockerode`, with admin user + access token bootstrap, ports allocated from a managed pool, lifecycle reconciled on backend boot.
|
- **Per-user containers**: Gitea (memory repo) + OpenCode (workflow execution), spawned via `dockerode`, with admin user + access token bootstrap, ports allocated from a managed pool, lifecycle reconciled on backend boot.
|
||||||
- **Frontend**: Next.js 16 with `@clerk/nextjs` for auth and `rivetkit/client` for direct actor connections + event streaming.
|
- **Frontend**: Next.js 16 with `@clerk/nextjs` for auth and `rivetkit/client` for direct actor connections + event streaming.
|
||||||
- **Tool surface available to the Grow Agent**: `spawn_sub_agent`, `commit_memory`, `read_memory`, `list_memory`.
|
- **Tool surface available to the Grow Agent**: `spawn_sub_agent`, `commit_memory`, `read_memory`, `list_memory`.
|
||||||
@@ -16,7 +16,7 @@ A multi-agent platform where every user gets a private Grow Agent (Rivet Kit act
|
|||||||
You need three external accounts before running:
|
You need three external accounts before running:
|
||||||
|
|
||||||
1. **Clerk** — create an app at https://dashboard.clerk.com → copy the publishable + secret keys.
|
1. **Clerk** — create an app at https://dashboard.clerk.com → copy the publishable + secret keys.
|
||||||
2. **Anthropic** — create an API key at https://console.anthropic.com.
|
2. **OpenCode Zen** — create an API key at https://opencode.ai/auth.
|
||||||
3. **Docker** — Docker Desktop (or any daemon `dockerode` can reach via `/var/run/docker.sock`).
|
3. **Docker** — Docker Desktop (or any daemon `dockerode` can reach via `/var/run/docker.sock`).
|
||||||
|
|
||||||
Then:
|
Then:
|
||||||
@@ -24,7 +24,7 @@ Then:
|
|||||||
```bash
|
```bash
|
||||||
# Backend env
|
# Backend env
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# fill in CLERK_SECRET_KEY, CLERK_PUBLISHABLE_KEY, ANTHROPIC_API_KEY
|
# fill in CLERK_SECRET_KEY, CLERK_PUBLISHABLE_KEY, OPENCODE_API_KEY
|
||||||
|
|
||||||
# Frontend env
|
# Frontend env
|
||||||
cd growqr-frontend
|
cd growqr-frontend
|
||||||
@@ -73,7 +73,7 @@ Next.js (3000) ──fetch──▶ /users/bootstrap, /users/me (Hono on 40
|
|||||||
▼
|
▼
|
||||||
Grow Agent actor (one per user)
|
Grow Agent actor (one per user)
|
||||||
│
|
│
|
||||||
├─ Anthropic (Opus 4.7 + tool use)
|
├─ OpenCode Zen (Kimi K2.6 + tool use)
|
||||||
├─ commit_memory ────▶ Gitea container (per user)
|
├─ commit_memory ────▶ Gitea container (per user)
|
||||||
└─ spawn_sub_agent ────▶ OpenCode container (per user)
|
└─ spawn_sub_agent ────▶ OpenCode container (per user)
|
||||||
(multiplexed sessions)
|
(multiplexed sessions)
|
||||||
|
|||||||
@@ -54,12 +54,20 @@ services:
|
|||||||
RIVET_ENDPOINT: http://rivet-engine:6420
|
RIVET_ENDPOINT: http://rivet-engine:6420
|
||||||
CLERK_SECRET_KEY: ${CLERK_SECRET_KEY}
|
CLERK_SECRET_KEY: ${CLERK_SECRET_KEY}
|
||||||
CLERK_PUBLISHABLE_KEY: ${CLERK_PUBLISHABLE_KEY}
|
CLERK_PUBLISHABLE_KEY: ${CLERK_PUBLISHABLE_KEY}
|
||||||
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
|
OPENCODE_API_KEY: ${OPENCODE_API_KEY}
|
||||||
GROW_AGENT_MODEL: ${GROW_AGENT_MODEL:-claude-opus-4-7}
|
LLM_PROVIDER: ${LLM_PROVIDER:-opencode}
|
||||||
SUB_AGENT_MODEL: ${SUB_AGENT_MODEL:-claude-sonnet-4-6}
|
LLM_BASE_URL: ${LLM_BASE_URL:-https://opencode.ai/zen/v1}
|
||||||
|
LLM_MODEL: ${LLM_MODEL:-kimi-k2.6}
|
||||||
|
GROW_AGENT_MODEL: ${GROW_AGENT_MODEL:-kimi-k2.6}
|
||||||
|
SUB_AGENT_MODEL: ${SUB_AGENT_MODEL:-kimi-k2.6}
|
||||||
SERVICE_TOKEN: ${SERVICE_TOKEN:-dev-service-token}
|
SERVICE_TOKEN: ${SERVICE_TOKEN:-dev-service-token}
|
||||||
|
A2A_ALLOWED_KEY: ${A2A_ALLOWED_KEY:-dev-a2a-key}
|
||||||
|
RIVET_CLIENT_ENDPOINT: ${RIVET_CLIENT_ENDPOINT:-http://127.0.0.1:4000/api/rivet}
|
||||||
GITEA_IMAGE: ${GITEA_IMAGE:-gitea/gitea:1.22}
|
GITEA_IMAGE: ${GITEA_IMAGE:-gitea/gitea:1.22}
|
||||||
OPENCODE_IMAGE: ${OPENCODE_IMAGE:-ghcr.io/sst/opencode:latest}
|
OPENCODE_IMAGE: ${OPENCODE_IMAGE:-ghcr.io/anomalyco/opencode:latest}
|
||||||
|
INTERVIEW_SERVICE_URL: ${INTERVIEW_SERVICE_URL:-http://host.docker.internal:8007}
|
||||||
|
ROLEPLAY_SERVICE_URL: ${ROLEPLAY_SERVICE_URL:-http://host.docker.internal:8008}
|
||||||
|
QSCORE_SERVICE_URL: ${QSCORE_SERVICE_URL:-http://host.docker.internal:8000}
|
||||||
USER_CONTAINER_HOST: ${USER_CONTAINER_HOST:-host.docker.internal}
|
USER_CONTAINER_HOST: ${USER_CONTAINER_HOST:-host.docker.internal}
|
||||||
USER_DATA_ROOT: /data/users
|
USER_DATA_ROOT: /data/users
|
||||||
USER_PORT_RANGE_START: 20000
|
USER_PORT_RANGE_START: 20000
|
||||||
|
|||||||
@@ -366,7 +366,7 @@
|
|||||||
<span class="swatch" style="background: var(--c-memory-bg)"></span>
|
<span class="swatch" style="background: var(--c-memory-bg)"></span>
|
||||||
<div>
|
<div>
|
||||||
<h3>Memory API</h3>
|
<h3>Memory API</h3>
|
||||||
<p style="color: var(--muted)">Three-layer memory surface exposed to Claude as tools (<code>commit_memory</code>, <code>read_memory</code>, <code>list_memory</code>). L1 in-actor state, L2 session in Postgres, L3 long-term in the user's Gitea repo.</p>
|
<p style="color: var(--muted)">Three-layer memory surface exposed to the configured OpenCode Zen model as tools (<code>commit_memory</code>, <code>read_memory</code>, <code>list_memory</code>). L1 in-actor state, L2 session in Postgres, L3 long-term in the user's Gitea repo.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item" style="color: var(--c-git)">
|
<div class="item" style="color: var(--c-git)">
|
||||||
|
|||||||
156
package-lock.json
generated
156
package-lock.json
generated
@@ -8,7 +8,6 @@
|
|||||||
"name": "growqr-backend",
|
"name": "growqr-backend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.96.0",
|
|
||||||
"@clerk/backend": "^1.21.0",
|
"@clerk/backend": "^1.21.0",
|
||||||
"@hono/node-server": "^1.13.7",
|
"@hono/node-server": "^1.13.7",
|
||||||
"dockerode": "^4.0.7",
|
"dockerode": "^4.0.7",
|
||||||
@@ -38,36 +37,6 @@
|
|||||||
"zod": "^3.25.0 || ^4.0.0"
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@anthropic-ai/sdk": {
|
|
||||||
"version": "0.96.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.96.0.tgz",
|
|
||||||
"integrity": "sha512-KlCsODtTyb17bLUVCSDC2HtSvAbJf60sEiPEax9dInF+aDF92vS4TZJ5XD7YCQXNb1/5icYaw8Y7wMjPlIV9Zg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"json-schema-to-ts": "^3.1.1",
|
|
||||||
"standardwebhooks": "^1.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"anthropic-ai-sdk": "bin/cli"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"zod": "^3.25.0 || ^4.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"zod": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/runtime": {
|
|
||||||
"version": "7.29.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
|
||||||
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@balena/dockerignore": {
|
"node_modules/@balena/dockerignore": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
|
||||||
@@ -244,7 +213,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -261,7 +229,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -278,7 +245,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -295,7 +261,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -312,7 +277,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -329,7 +293,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -346,7 +309,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -363,7 +325,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -380,7 +341,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -397,7 +357,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -414,7 +373,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -431,7 +389,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -448,7 +405,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -465,7 +421,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -482,7 +437,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -499,7 +453,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -516,7 +469,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -533,7 +485,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -550,7 +501,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -567,7 +517,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -584,7 +533,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -601,7 +549,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -668,7 +615,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -685,7 +631,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -702,7 +647,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -719,7 +663,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -736,7 +679,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -753,7 +695,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -770,7 +711,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -787,7 +727,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -804,7 +743,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -821,7 +759,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -838,7 +775,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -855,7 +791,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -872,7 +807,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -889,7 +823,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -906,7 +839,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -923,7 +855,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -940,7 +871,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -957,7 +887,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -974,7 +903,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -991,7 +919,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1008,7 +935,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1025,7 +951,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1042,7 +967,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1059,7 +983,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1076,7 +999,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1093,7 +1015,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1157,7 +1078,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
|
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
|
||||||
"integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==",
|
"integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.14.1"
|
"node": ">=18.14.1"
|
||||||
},
|
},
|
||||||
@@ -2096,12 +2016,6 @@
|
|||||||
"linux"
|
"linux"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@stablelib/base64": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@standard-schema/spec": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||||
@@ -2305,7 +2219,6 @@
|
|||||||
"integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==",
|
"integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bindings": "^1.5.0",
|
"bindings": "^1.5.0",
|
||||||
"prebuild-install": "^7.1.1"
|
"prebuild-install": "^7.1.1"
|
||||||
@@ -2960,7 +2873,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.12.tgz",
|
||||||
"integrity": "sha512-/bCZd6KlGcjZO8Buqmi/vXuqEGVEZ0PNjx/biBNqJD3MhK9DmdiAuKxqfNhflgDESDIiBz3qF+0e55+CpnrUcw==",
|
"integrity": "sha512-/bCZd6KlGcjZO8Buqmi/vXuqEGVEZ0PNjx/biBNqJD3MhK9DmdiAuKxqfNhflgDESDIiBz3qF+0e55+CpnrUcw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@balena/dockerignore": "^1.0.2",
|
"@balena/dockerignore": "^1.0.2",
|
||||||
"@grpc/grpc-js": "^1.11.1",
|
"@grpc/grpc-js": "^1.11.1",
|
||||||
@@ -3014,7 +2926,6 @@
|
|||||||
"integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==",
|
"integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@drizzle-team/brocli": "^0.10.2",
|
"@drizzle-team/brocli": "^0.10.2",
|
||||||
"@esbuild-kit/esm-loader": "^2.5.5",
|
"@esbuild-kit/esm-loader": "^2.5.5",
|
||||||
@@ -3032,7 +2943,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3049,7 +2959,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3066,7 +2975,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3083,7 +2991,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3100,7 +3007,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3117,7 +3023,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3134,7 +3039,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3151,7 +3055,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3168,7 +3071,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3185,7 +3087,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3202,7 +3103,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3219,7 +3119,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3236,7 +3135,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3253,7 +3151,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3270,7 +3167,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3287,7 +3183,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3304,7 +3199,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3321,7 +3215,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3338,7 +3231,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3355,7 +3247,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3372,7 +3263,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3389,7 +3279,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3406,7 +3295,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3423,7 +3311,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3440,7 +3327,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3457,7 +3343,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3811,12 +3696,6 @@
|
|||||||
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
|
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-sha256": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
|
||||||
"license": "Unlicense"
|
|
||||||
},
|
|
||||||
"node_modules/fdb-tuple": {
|
"node_modules/fdb-tuple": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdb-tuple/-/fdb-tuple-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdb-tuple/-/fdb-tuple-1.0.0.tgz",
|
||||||
@@ -3870,7 +3749,6 @@
|
|||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -3937,7 +3815,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz",
|
||||||
"integrity": "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==",
|
"integrity": "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
},
|
},
|
||||||
@@ -4091,7 +3968,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.19.tgz",
|
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.19.tgz",
|
||||||
"integrity": "sha512-xa3eYXYXx68XTT4hZ7dRzsXBhaq85ToSrlUJNoR0gwz/1Ap/CNwX47wfvV7pc/xWhjKVVkLT7zBJy8chhNguqQ==",
|
"integrity": "sha512-xa3eYXYXx68XTT4hZ7dRzsXBhaq85ToSrlUJNoR0gwz/1Ap/CNwX47wfvV7pc/xWhjKVVkLT7zBJy8chhNguqQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.9.0"
|
"node": ">=16.9.0"
|
||||||
}
|
}
|
||||||
@@ -4314,19 +4190,6 @@
|
|||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/json-schema-to-ts": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.18.3",
|
|
||||||
"ts-algebra": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
@@ -4888,7 +4751,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.9.tgz",
|
"resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.9.tgz",
|
||||||
"integrity": "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==",
|
"integrity": "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -5380,7 +5242,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -5774,16 +5635,6 @@
|
|||||||
"nan": "^2.23.0"
|
"nan": "^2.23.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/standardwebhooks": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@stablelib/base64": "^1.0.0",
|
|
||||||
"fast-sha256": "^1.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/std-env": {
|
"node_modules/std-env": {
|
||||||
"version": "3.10.0",
|
"version": "3.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||||
@@ -5972,12 +5823,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ts-algebra": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
@@ -6307,7 +6152,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
"compose:down": "docker compose down"
|
"compose:down": "docker compose down"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.96.0",
|
|
||||||
"@clerk/backend": "^1.21.0",
|
"@clerk/backend": "^1.21.0",
|
||||||
"@hono/node-server": "^1.13.7",
|
"@hono/node-server": "^1.13.7",
|
||||||
"dockerode": "^4.0.7",
|
"dockerode": "^4.0.7",
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { actor } from "rivetkit";
|
import { actor } from "rivetkit";
|
||||||
import type Anthropic from "@anthropic-ai/sdk";
|
|
||||||
import { log } from "../log.js";
|
import { log } from "../log.js";
|
||||||
import { config } from "../config.js";
|
import { config } from "../config.js";
|
||||||
import {
|
import {
|
||||||
anthropic,
|
createChatCompletion,
|
||||||
GROW_AGENT_SYSTEM,
|
GROW_AGENT_SYSTEM,
|
||||||
growAgentTools,
|
growAgentTools,
|
||||||
} from "../lib/anthropic.js";
|
type LlmMessage,
|
||||||
|
type LlmToolCall,
|
||||||
|
} from "../lib/llm.js";
|
||||||
import {
|
import {
|
||||||
provisionUserStack,
|
provisionUserStack,
|
||||||
getUserStack,
|
getUserStack,
|
||||||
@@ -18,9 +19,10 @@ import { db } from "../db/client.js";
|
|||||||
import { actors as actorsTable, events as eventsTable } from "../db/schema.js";
|
import { actors as actorsTable, events as eventsTable } from "../db/schema.js";
|
||||||
|
|
||||||
type ChatTurn = {
|
type ChatTurn = {
|
||||||
role: "user" | "assistant";
|
role: "user" | "assistant" | "tool";
|
||||||
// Anthropic content blocks; "user" turns may also be plain strings.
|
content: string;
|
||||||
content: string | Anthropic.ContentBlockParam[];
|
toolCallId?: string;
|
||||||
|
toolCalls?: LlmToolCall[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type GrowAgentState = {
|
type GrowAgentState = {
|
||||||
@@ -73,7 +75,7 @@ export const growAgent = actor({
|
|||||||
return stack;
|
return stack;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Main chat entry point. Runs the full agentic loop with Claude.
|
// Main chat entry point. Runs the full agentic loop through the configured LLM.
|
||||||
receiveMessage: async (c, msg: { text: string }) => {
|
receiveMessage: async (c, msg: { text: string }) => {
|
||||||
if (!c.state.userId) {
|
if (!c.state.userId) {
|
||||||
throw new Error("Grow Agent not initialized");
|
throw new Error("Grow Agent not initialized");
|
||||||
@@ -126,7 +128,8 @@ export const growAgent = actor({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// The agentic loop. Keeps calling Claude with tools until stop_reason === "end_turn".
|
// The agentic loop. Keeps calling the configured LLM with tools until the model
|
||||||
|
// returns a normal assistant turn.
|
||||||
async function runAgentLoop(
|
async function runAgentLoop(
|
||||||
c: {
|
c: {
|
||||||
state: GrowAgentState;
|
state: GrowAgentState;
|
||||||
@@ -134,9 +137,9 @@ async function runAgentLoop(
|
|||||||
},
|
},
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!config.anthropicApiKey) {
|
if (!config.llmApiKey) {
|
||||||
const reply =
|
const reply =
|
||||||
"ANTHROPIC_API_KEY is not configured on the backend — set it to enable the Grow Agent.";
|
"LLM_API_KEY or OPENCODE_API_KEY is not configured on the backend - set it to enable the Grow Agent.";
|
||||||
c.state.history.push({ role: "assistant", content: reply });
|
c.state.history.push({ role: "assistant", content: reply });
|
||||||
c.broadcast("message", { role: "agent", text: reply });
|
c.broadcast("message", { role: "agent", text: reply });
|
||||||
return reply;
|
return reply;
|
||||||
@@ -148,78 +151,80 @@ async function runAgentLoop(
|
|||||||
let assistantTextOut = "";
|
let assistantTextOut = "";
|
||||||
|
|
||||||
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
||||||
const response = await anthropic.messages.create({
|
const response = await createChatCompletion({
|
||||||
model: config.growAgentModel,
|
model: config.growAgentModel,
|
||||||
max_tokens: config.maxAgentTokens,
|
maxTokens: config.maxAgentTokens,
|
||||||
system: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: GROW_AGENT_SYSTEM,
|
|
||||||
cache_control: { type: "ephemeral" },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
tools: growAgentTools,
|
tools: growAgentTools,
|
||||||
thinking: { type: "adaptive" },
|
|
||||||
messages: messagesForApi(c.state.history),
|
messages: messagesForApi(c.state.history),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Capture assistant text for streaming-style broadcast.
|
// Capture assistant text for streaming-style broadcast.
|
||||||
for (const block of response.content) {
|
if (response.content) {
|
||||||
if (block.type === "text" && block.text) {
|
assistantTextOut += (assistantTextOut ? "\n\n" : "") + response.content;
|
||||||
assistantTextOut += (assistantTextOut ? "\n\n" : "") + block.text;
|
c.broadcast("message", { role: "agent", text: response.content });
|
||||||
c.broadcast("message", { role: "agent", text: block.text });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist the full assistant turn (so subsequent loops keep tool_use blocks).
|
// Persist the assistant turn, including tool calls for the next tool result turn.
|
||||||
c.state.history.push({
|
c.state.history.push({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: response.content as Anthropic.ContentBlockParam[],
|
content: response.content,
|
||||||
|
toolCalls: response.toolCalls,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.stop_reason !== "tool_use") {
|
if (response.toolCalls.length === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolUses = response.content.filter(
|
for (const call of response.toolCalls) {
|
||||||
(b): b is Anthropic.ToolUseBlock => b.type === "tool_use",
|
|
||||||
);
|
|
||||||
if (toolUses.length === 0) break;
|
|
||||||
|
|
||||||
const toolResults: Anthropic.ToolResultBlockParam[] = [];
|
|
||||||
for (const call of toolUses) {
|
|
||||||
try {
|
try {
|
||||||
const result = await dispatchTool(c, userId, call);
|
const result = await dispatchTool(c, userId, call);
|
||||||
toolResults.push({
|
c.state.history.push({
|
||||||
type: "tool_result",
|
role: "tool",
|
||||||
tool_use_id: call.id,
|
toolCallId: call.id,
|
||||||
content: typeof result === "string" ? result : JSON.stringify(result),
|
content: typeof result === "string" ? result : JSON.stringify(result),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error({ err, tool: call.name }, "tool dispatch failed");
|
log.error({ err, tool: call.name }, "tool dispatch failed");
|
||||||
toolResults.push({
|
c.state.history.push({
|
||||||
type: "tool_result",
|
role: "tool",
|
||||||
tool_use_id: call.id,
|
toolCallId: call.id,
|
||||||
content: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
content: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
is_error: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.state.history.push({ role: "user", content: toolResults });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.broadcast("agent-thinking", { state: "idle" });
|
c.broadcast("agent-thinking", { state: "idle" });
|
||||||
return assistantTextOut || "(no response)";
|
return assistantTextOut || "(no response)";
|
||||||
}
|
}
|
||||||
|
|
||||||
function messagesForApi(
|
function messagesForApi(history: ChatTurn[]): LlmMessage[] {
|
||||||
history: ChatTurn[],
|
const messages: LlmMessage[] = [
|
||||||
): Anthropic.MessageParam[] {
|
{ role: "system", content: GROW_AGENT_SYSTEM },
|
||||||
return history.map((t) => ({
|
];
|
||||||
role: t.role,
|
for (const turn of history) {
|
||||||
content: t.content,
|
if (turn.role === "tool") {
|
||||||
})) as Anthropic.MessageParam[];
|
messages.push({
|
||||||
|
role: "tool",
|
||||||
|
content: turn.content,
|
||||||
|
tool_call_id: turn.toolCallId,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
messages.push({
|
||||||
|
role: turn.role,
|
||||||
|
content: turn.content,
|
||||||
|
tool_calls: turn.toolCalls?.map((call) => ({
|
||||||
|
id: call.id,
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: call.name,
|
||||||
|
arguments: JSON.stringify(call.arguments),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dispatchTool(
|
async function dispatchTool(
|
||||||
@@ -228,9 +233,9 @@ async function dispatchTool(
|
|||||||
state: GrowAgentState;
|
state: GrowAgentState;
|
||||||
},
|
},
|
||||||
userId: string,
|
userId: string,
|
||||||
call: Anthropic.ToolUseBlock,
|
call: LlmToolCall,
|
||||||
): Promise<unknown> {
|
): Promise<unknown> {
|
||||||
const input = call.input as Record<string, unknown>;
|
const input = call.arguments;
|
||||||
switch (call.name) {
|
switch (call.name) {
|
||||||
case "spawn_sub_agent": {
|
case "spawn_sub_agent": {
|
||||||
const type = String(input.type ?? "generic");
|
const type = String(input.type ?? "generic");
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { setup } from "rivetkit";
|
import { setup } from "rivetkit";
|
||||||
import { growAgent } from "./grow-agent.js";
|
import { growAgent } from "./grow-agent.js";
|
||||||
import { subAgent } from "./sub-agent.js";
|
import { subAgent } from "./sub-agent.js";
|
||||||
|
import { workflowJob } from "./workflow-job.js";
|
||||||
|
|
||||||
export const registry = setup({
|
export const registry = setup({
|
||||||
use: {
|
use: {
|
||||||
growAgent,
|
growAgent,
|
||||||
subAgent,
|
subAgent,
|
||||||
|
workflowJob,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
292
src/actors/workflow-job.ts
Normal file
292
src/actors/workflow-job.ts
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
import { actor } from "rivetkit";
|
||||||
|
import {
|
||||||
|
agentCatalog,
|
||||||
|
getAgentProfile,
|
||||||
|
jobApplicationAgentIds,
|
||||||
|
type AgentProfile,
|
||||||
|
} from "../agents/catalog.js";
|
||||||
|
import { runServiceAgentProbe, type ServiceAgentResult } from "../services/service-agents.js";
|
||||||
|
|
||||||
|
type WorkflowStatus = "draft" | "running" | "paused" | "completed";
|
||||||
|
type AgentStatus = "idle" | "running" | "blocked" | "done";
|
||||||
|
|
||||||
|
type AgentScorecard = {
|
||||||
|
id: string;
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
score: number;
|
||||||
|
notes?: string;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkflowAgentState = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
kind: AgentProfile["kind"];
|
||||||
|
service?: AgentProfile["service"];
|
||||||
|
status: AgentStatus;
|
||||||
|
summary: string;
|
||||||
|
lastResult?: ServiceAgentResult;
|
||||||
|
scorecards: AgentScorecard[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkflowEvent = {
|
||||||
|
id: string;
|
||||||
|
ts: string;
|
||||||
|
agentId: string;
|
||||||
|
agentName: string;
|
||||||
|
type: "workflow" | "agent" | "score";
|
||||||
|
message: string;
|
||||||
|
payload?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkflowJobState = {
|
||||||
|
workflowId: string;
|
||||||
|
userId: string;
|
||||||
|
type: "job-application";
|
||||||
|
status: WorkflowStatus;
|
||||||
|
goal: string;
|
||||||
|
agents: WorkflowAgentState[];
|
||||||
|
timeline: WorkflowEvent[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const now = () => new Date().toISOString();
|
||||||
|
|
||||||
|
const initialState: WorkflowJobState = {
|
||||||
|
workflowId: "",
|
||||||
|
userId: "",
|
||||||
|
type: "job-application",
|
||||||
|
status: "draft",
|
||||||
|
goal: "",
|
||||||
|
agents: [],
|
||||||
|
timeline: [],
|
||||||
|
createdAt: "",
|
||||||
|
updatedAt: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
function eventId() {
|
||||||
|
return `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeAgents(): WorkflowAgentState[] {
|
||||||
|
return jobApplicationAgentIds()
|
||||||
|
.map((id) => getAgentProfile(id))
|
||||||
|
.filter((agent): agent is AgentProfile => Boolean(agent))
|
||||||
|
.map((agent) => ({
|
||||||
|
id: agent.id,
|
||||||
|
name: agent.name,
|
||||||
|
role: agent.role,
|
||||||
|
kind: agent.kind,
|
||||||
|
service: agent.service,
|
||||||
|
status: "idle",
|
||||||
|
summary: agent.description,
|
||||||
|
scorecards: [],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendEvent(
|
||||||
|
state: WorkflowJobState,
|
||||||
|
agent: Pick<WorkflowAgentState, "id" | "name">,
|
||||||
|
type: WorkflowEvent["type"],
|
||||||
|
message: string,
|
||||||
|
payload?: unknown,
|
||||||
|
) {
|
||||||
|
const ev: WorkflowEvent = {
|
||||||
|
id: eventId(),
|
||||||
|
ts: now(),
|
||||||
|
agentId: agent.id,
|
||||||
|
agentName: agent.name,
|
||||||
|
type,
|
||||||
|
message,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
state.timeline.unshift(ev);
|
||||||
|
state.timeline = state.timeline.slice(0, 100);
|
||||||
|
state.updatedAt = ev.ts;
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
|
||||||
|
function localAgentResult(agent: WorkflowAgentState, goal: string): ServiceAgentResult {
|
||||||
|
const goalText = goal || "job application workflow";
|
||||||
|
switch (agent.id) {
|
||||||
|
case "grow":
|
||||||
|
return {
|
||||||
|
status: "local",
|
||||||
|
summary: `Grow Agent initialized the ${goalText} workflow and assigned specialist agents.`,
|
||||||
|
};
|
||||||
|
case "resume":
|
||||||
|
return {
|
||||||
|
status: "local",
|
||||||
|
summary: `Resume Agent prepared a resume-improvement pass for ${goalText}.`,
|
||||||
|
};
|
||||||
|
case "job-search":
|
||||||
|
return {
|
||||||
|
status: "local",
|
||||||
|
summary: `Job Search Agent created the opportunity discovery lane for ${goalText}.`,
|
||||||
|
};
|
||||||
|
case "job-apply":
|
||||||
|
return {
|
||||||
|
status: "local",
|
||||||
|
summary: `Job Apply Agent prepared the application tracking lane for ${goalText}.`,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
status: "local",
|
||||||
|
summary: `${agent.name} completed a local workflow step.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const workflowJob = actor({
|
||||||
|
options: {
|
||||||
|
actionTimeout: 600_000,
|
||||||
|
noSleep: true,
|
||||||
|
},
|
||||||
|
state: initialState,
|
||||||
|
actions: {
|
||||||
|
init: async (
|
||||||
|
c,
|
||||||
|
input: {
|
||||||
|
userId: string;
|
||||||
|
goal?: string;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
if (c.state.userId && c.state.userId !== input.userId) {
|
||||||
|
throw new Error("Workflow already belongs to another user");
|
||||||
|
}
|
||||||
|
if (!c.state.workflowId) {
|
||||||
|
const ts = now();
|
||||||
|
c.state.workflowId = `job-application:${input.userId}`;
|
||||||
|
c.state.userId = input.userId;
|
||||||
|
c.state.type = "job-application";
|
||||||
|
c.state.goal = input.goal ?? "Find and apply to high-fit jobs";
|
||||||
|
c.state.status = "draft";
|
||||||
|
c.state.agents = makeAgents();
|
||||||
|
c.state.createdAt = ts;
|
||||||
|
c.state.updatedAt = ts;
|
||||||
|
appendEvent(
|
||||||
|
c.state,
|
||||||
|
{ id: "grow", name: "Grow Agent" },
|
||||||
|
"workflow",
|
||||||
|
"Job application workflow created.",
|
||||||
|
{ catalog: agentCatalog },
|
||||||
|
);
|
||||||
|
} else if (input.goal) {
|
||||||
|
c.state.goal = input.goal;
|
||||||
|
c.state.updatedAt = now();
|
||||||
|
}
|
||||||
|
c.broadcast("workflow.updated", c.state);
|
||||||
|
return c.state;
|
||||||
|
},
|
||||||
|
|
||||||
|
start: async (c) => {
|
||||||
|
c.state.status = "running";
|
||||||
|
appendEvent(
|
||||||
|
c.state,
|
||||||
|
{ id: "grow", name: "Grow Agent" },
|
||||||
|
"workflow",
|
||||||
|
"Workflow started.",
|
||||||
|
);
|
||||||
|
c.broadcast("workflow.updated", c.state);
|
||||||
|
return c.state;
|
||||||
|
},
|
||||||
|
|
||||||
|
pause: async (c) => {
|
||||||
|
c.state.status = "paused";
|
||||||
|
appendEvent(
|
||||||
|
c.state,
|
||||||
|
{ id: "grow", name: "Grow Agent" },
|
||||||
|
"workflow",
|
||||||
|
"Workflow paused.",
|
||||||
|
);
|
||||||
|
c.broadcast("workflow.updated", c.state);
|
||||||
|
return c.state;
|
||||||
|
},
|
||||||
|
|
||||||
|
resume: async (c) => {
|
||||||
|
c.state.status = "running";
|
||||||
|
appendEvent(
|
||||||
|
c.state,
|
||||||
|
{ id: "grow", name: "Grow Agent" },
|
||||||
|
"workflow",
|
||||||
|
"Workflow resumed.",
|
||||||
|
);
|
||||||
|
c.broadcast("workflow.updated", c.state);
|
||||||
|
return c.state;
|
||||||
|
},
|
||||||
|
|
||||||
|
runAgent: async (c, input: { agentId: string }) => {
|
||||||
|
const agent = c.state.agents.find((item) => item.id === input.agentId);
|
||||||
|
if (!agent) throw new Error(`Unknown workflow agent: ${input.agentId}`);
|
||||||
|
|
||||||
|
agent.status = "running";
|
||||||
|
appendEvent(c.state, agent, "agent", `${agent.name} started.`);
|
||||||
|
c.broadcast("workflow.updated", c.state);
|
||||||
|
|
||||||
|
const profile = getAgentProfile(agent.id);
|
||||||
|
if (profile?.service != null) {
|
||||||
|
const userId = c.state.userId;
|
||||||
|
const goal = c.state.goal;
|
||||||
|
c.waitUntil(
|
||||||
|
(async () => {
|
||||||
|
const result = await runServiceAgentProbe(profile, {
|
||||||
|
userId,
|
||||||
|
goal,
|
||||||
|
});
|
||||||
|
agent.lastResult = result;
|
||||||
|
agent.status = result.status === "unavailable" ? "blocked" : "done";
|
||||||
|
appendEvent(c.state, agent, "agent", result.summary, result.detail);
|
||||||
|
c.broadcast("workflow.updated", c.state);
|
||||||
|
await c.saveState({ immediate: true });
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
return c.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = localAgentResult(agent, c.state.goal);
|
||||||
|
|
||||||
|
agent.lastResult = result;
|
||||||
|
agent.status = result.status === "unavailable" ? "blocked" : "done";
|
||||||
|
appendEvent(c.state, agent, "agent", result.summary, result.detail);
|
||||||
|
c.broadcast("workflow.updated", c.state);
|
||||||
|
return c.state;
|
||||||
|
},
|
||||||
|
|
||||||
|
recordQaScore: async (
|
||||||
|
c,
|
||||||
|
input: {
|
||||||
|
agentId: string;
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
score: number;
|
||||||
|
notes?: string;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const agent = c.state.agents.find((item) => item.id === input.agentId);
|
||||||
|
if (!agent) throw new Error(`Unknown workflow agent: ${input.agentId}`);
|
||||||
|
const card: AgentScorecard = {
|
||||||
|
id: `score_${Date.now()}`,
|
||||||
|
question: input.question,
|
||||||
|
answer: input.answer,
|
||||||
|
score: Math.max(0, Math.min(100, Number(input.score))),
|
||||||
|
notes: input.notes,
|
||||||
|
createdAt: now(),
|
||||||
|
};
|
||||||
|
agent.scorecards.unshift(card);
|
||||||
|
appendEvent(
|
||||||
|
c.state,
|
||||||
|
agent,
|
||||||
|
"score",
|
||||||
|
`${agent.name} recorded Q&A score ${card.score}.`,
|
||||||
|
card,
|
||||||
|
);
|
||||||
|
c.broadcast("workflow.updated", c.state);
|
||||||
|
return c.state;
|
||||||
|
},
|
||||||
|
|
||||||
|
getStatus: async (c) => c.state,
|
||||||
|
},
|
||||||
|
});
|
||||||
94
src/agents/catalog.ts
Normal file
94
src/agents/catalog.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
export type AgentKind =
|
||||||
|
| "master"
|
||||||
|
| "workflow"
|
||||||
|
| "microservice"
|
||||||
|
| "score";
|
||||||
|
|
||||||
|
export type AgentProfile = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
kind: AgentKind;
|
||||||
|
description: string;
|
||||||
|
service?: "interview-service" | "roleplay-service" | "qscore-service";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const agentCatalog = [
|
||||||
|
{
|
||||||
|
id: "grow",
|
||||||
|
name: "Grow Agent",
|
||||||
|
role: "Master Orchestrator",
|
||||||
|
kind: "master",
|
||||||
|
description:
|
||||||
|
"Owns user context, routes work to sub-agents, commits durable memory, and tracks workflow progress.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "resume",
|
||||||
|
name: "Resume Agent",
|
||||||
|
role: "Resume Builder",
|
||||||
|
kind: "workflow",
|
||||||
|
description:
|
||||||
|
"Turns profile context, Q-Score gaps, and target roles into resume edits and application collateral.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "job-search",
|
||||||
|
name: "Job Search Agent",
|
||||||
|
role: "Opportunity Scout",
|
||||||
|
kind: "workflow",
|
||||||
|
description:
|
||||||
|
"Finds relevant jobs, ranks opportunities, and prepares a shortlist for the application workflow.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "job-apply",
|
||||||
|
name: "Job Apply Agent",
|
||||||
|
role: "Application Operator",
|
||||||
|
kind: "workflow",
|
||||||
|
description:
|
||||||
|
"Prepares tailored applications, tracks submissions, and records follow-up tasks.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sara",
|
||||||
|
name: "Sara",
|
||||||
|
role: "Interview Agent",
|
||||||
|
kind: "microservice",
|
||||||
|
service: "interview-service",
|
||||||
|
description:
|
||||||
|
"Runs interview practice through the interview-service microservice and owns interview Q&A feedback.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "emily",
|
||||||
|
name: "Emily",
|
||||||
|
role: "Roleplay Agent",
|
||||||
|
kind: "microservice",
|
||||||
|
service: "roleplay-service",
|
||||||
|
description:
|
||||||
|
"Runs roleplay practice through the roleplay-service microservice and owns scenario feedback.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "qscore",
|
||||||
|
name: "Quinn",
|
||||||
|
role: "Q-Score Agent",
|
||||||
|
kind: "score",
|
||||||
|
service: "qscore-service",
|
||||||
|
description:
|
||||||
|
"Computes and explains Q-Score changes, then displays Q&A and scores under the owning agent.",
|
||||||
|
},
|
||||||
|
] as const satisfies AgentProfile[];
|
||||||
|
|
||||||
|
export type AgentId = (typeof agentCatalog)[number]["id"];
|
||||||
|
|
||||||
|
export function getAgentProfile(id: string): AgentProfile | undefined {
|
||||||
|
return agentCatalog.find((agent) => agent.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jobApplicationAgentIds(): AgentId[] {
|
||||||
|
return [
|
||||||
|
"grow",
|
||||||
|
"resume",
|
||||||
|
"job-search",
|
||||||
|
"job-apply",
|
||||||
|
"sara",
|
||||||
|
"emily",
|
||||||
|
"qscore",
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -34,7 +34,19 @@ export const requireUser = createMiddleware<AuthContext>(async (c, next) => {
|
|||||||
c.req.header("x-growqr-user")
|
c.req.header("x-growqr-user")
|
||||||
) {
|
) {
|
||||||
const userId = c.req.header("x-growqr-user")!;
|
const userId = c.req.header("x-growqr-user")!;
|
||||||
const row = await db.query.users.findFirst({ where: eq(users.id, userId) });
|
let row = await db.query.users.findFirst({ where: eq(users.id, userId) });
|
||||||
|
if (!row) {
|
||||||
|
const inserted = await db
|
||||||
|
.insert(users)
|
||||||
|
.values({
|
||||||
|
id: userId,
|
||||||
|
email: `${userId}@service.local`,
|
||||||
|
displayName: userId,
|
||||||
|
})
|
||||||
|
.onConflictDoNothing()
|
||||||
|
.returning();
|
||||||
|
row = inserted[0];
|
||||||
|
}
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new HTTPException(401, { message: "service token references unknown user" });
|
throw new HTTPException(401, { message: "service token references unknown user" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,21 +23,46 @@ export const config = {
|
|||||||
clerkPublishableKey: process.env.CLERK_PUBLISHABLE_KEY ?? "",
|
clerkPublishableKey: process.env.CLERK_PUBLISHABLE_KEY ?? "",
|
||||||
// Optional: lock service-to-service calls (actor → backend).
|
// Optional: lock service-to-service calls (actor → backend).
|
||||||
serviceToken: process.env.SERVICE_TOKEN ?? "",
|
serviceToken: process.env.SERVICE_TOKEN ?? "",
|
||||||
|
a2aAllowedKey: process.env.A2A_ALLOWED_KEY ?? "dev-a2a-key",
|
||||||
|
|
||||||
// Anthropic for Grow Agent + sub-agent LLM calls.
|
// LLM gateway for Grow Agent + sub-agent planning calls.
|
||||||
anthropicApiKey: process.env.ANTHROPIC_API_KEY ?? "",
|
llmProvider: process.env.LLM_PROVIDER ?? "opencode",
|
||||||
|
llmApiKey:
|
||||||
|
process.env.LLM_API_KEY ??
|
||||||
|
process.env.OPENCODE_API_KEY ??
|
||||||
|
"",
|
||||||
|
llmBaseUrl:
|
||||||
|
process.env.LLM_BASE_URL ??
|
||||||
|
process.env.OPENCODE_BASE_URL ??
|
||||||
|
"https://opencode.ai/zen/v1",
|
||||||
|
opencodeApiKey: process.env.OPENCODE_API_KEY ?? "",
|
||||||
growAgentModel:
|
growAgentModel:
|
||||||
process.env.GROW_AGENT_MODEL ?? "claude-opus-4-7",
|
process.env.GROW_AGENT_MODEL ??
|
||||||
|
process.env.LLM_MODEL ??
|
||||||
|
"kimi-k2.6",
|
||||||
subAgentModel:
|
subAgentModel:
|
||||||
process.env.SUB_AGENT_MODEL ?? "claude-sonnet-4-6",
|
process.env.SUB_AGENT_MODEL ??
|
||||||
|
process.env.LLM_MODEL ??
|
||||||
|
"kimi-k2.6",
|
||||||
|
|
||||||
// Rivet Kit engine endpoint (self-hosted in docker-compose).
|
// Rivet Kit engine endpoint (self-hosted in docker-compose).
|
||||||
rivetEndpoint: process.env.RIVET_ENDPOINT ?? "http://localhost:6420",
|
rivetEndpoint: process.env.RIVET_ENDPOINT ?? "http://localhost:6420",
|
||||||
|
rivetClientEndpoint:
|
||||||
|
process.env.RIVET_CLIENT_ENDPOINT ??
|
||||||
|
`http://127.0.0.1:${Number(process.env.PORT ?? 4000)}/api/rivet`,
|
||||||
|
|
||||||
|
// Product microservices exposed as sub-agent surfaces.
|
||||||
|
interviewServiceUrl:
|
||||||
|
process.env.INTERVIEW_SERVICE_URL ?? "http://localhost:8007",
|
||||||
|
roleplayServiceUrl:
|
||||||
|
process.env.ROLEPLAY_SERVICE_URL ?? "http://localhost:8008",
|
||||||
|
qscoreServiceUrl:
|
||||||
|
process.env.QSCORE_SERVICE_URL ?? "http://localhost:8000",
|
||||||
|
|
||||||
// Per-user container images.
|
// Per-user container images.
|
||||||
giteaImage: process.env.GITEA_IMAGE ?? "gitea/gitea:1.22",
|
giteaImage: process.env.GITEA_IMAGE ?? "gitea/gitea:1.22",
|
||||||
opencodeImage:
|
opencodeImage:
|
||||||
process.env.OPENCODE_IMAGE ?? "ghcr.io/sst/opencode:latest",
|
process.env.OPENCODE_IMAGE ?? "ghcr.io/anomalyco/opencode:latest",
|
||||||
|
|
||||||
// Host that user containers expose ports on (the host running Docker).
|
// Host that user containers expose ports on (the host running Docker).
|
||||||
userContainerHost: process.env.USER_CONTAINER_HOST ?? "127.0.0.1",
|
userContainerHost: process.env.USER_CONTAINER_HOST ?? "127.0.0.1",
|
||||||
@@ -49,7 +74,7 @@ export const config = {
|
|||||||
frontendOrigin:
|
frontendOrigin:
|
||||||
process.env.FRONTEND_ORIGIN ?? "http://localhost:3000",
|
process.env.FRONTEND_ORIGIN ?? "http://localhost:3000",
|
||||||
|
|
||||||
// Used by Anthropic SDK extended thinking / streaming budgets.
|
// Used by LLM requests.
|
||||||
maxAgentTokens: Number(process.env.MAX_AGENT_TOKENS ?? 4096),
|
maxAgentTokens: Number(process.env.MAX_AGENT_TOKENS ?? 4096),
|
||||||
|
|
||||||
required, // exported so other modules can fail fast on boot
|
required, // exported so other modules can fail fast on boot
|
||||||
|
|||||||
@@ -133,42 +133,95 @@ async function startGiteaContainer(opts: {
|
|||||||
return { id: container.id, name };
|
return { id: container.id, name };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shellQuote(value: string): string {
|
||||||
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function execGiteaCli(containerId: string, args: string[]): Promise<string> {
|
||||||
|
const container = docker.getContainer(containerId);
|
||||||
|
const command = [
|
||||||
|
"gitea",
|
||||||
|
"--work-path",
|
||||||
|
"/data/gitea",
|
||||||
|
"--config",
|
||||||
|
"/data/gitea/conf/app.ini",
|
||||||
|
...args,
|
||||||
|
]
|
||||||
|
.map(shellQuote)
|
||||||
|
.join(" ");
|
||||||
|
const exec = await container.exec({
|
||||||
|
Cmd: ["su", "git", "-c", command],
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true,
|
||||||
|
WorkingDir: "/data/gitea",
|
||||||
|
});
|
||||||
|
const stream = await exec.start({ Detach: false, Tty: false });
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
stream.on("data", (chunk: Buffer) => chunks.push(Buffer.from(chunk)));
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
stream.on("end", () => resolve());
|
||||||
|
stream.on("close", () => resolve());
|
||||||
|
});
|
||||||
|
const output = Buffer.concat(chunks).toString("utf8");
|
||||||
|
const info = await exec.inspect();
|
||||||
|
if (info.ExitCode && info.ExitCode !== 0) {
|
||||||
|
throw new Error(`gitea cli failed (${info.ExitCode}): ${output}`);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
// Runs `gitea admin user create --admin ...` inside the container.
|
// Runs `gitea admin user create --admin ...` inside the container.
|
||||||
// Idempotent: returns existing creds if the user already exists.
|
// Idempotent: the CLI returns non-zero if the user already exists, which is fine.
|
||||||
async function ensureGiteaAdmin(opts: {
|
async function ensureGiteaAdmin(opts: {
|
||||||
containerId: string;
|
containerId: string;
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
email: string;
|
email: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const container = docker.getContainer(opts.containerId);
|
try {
|
||||||
const exec = await container.exec({
|
await execGiteaCli(opts.containerId, [
|
||||||
Cmd: [
|
"admin",
|
||||||
"su",
|
"user",
|
||||||
"git",
|
"create",
|
||||||
"-c",
|
"--admin",
|
||||||
`gitea admin user create --admin --username ${opts.username} --password '${opts.password.replace(/'/g, "'\\''")}' --email ${opts.email} --must-change-password=false`,
|
"--username",
|
||||||
],
|
opts.username,
|
||||||
AttachStdout: true,
|
"--password",
|
||||||
AttachStderr: true,
|
opts.password,
|
||||||
WorkingDir: "/var/lib/gitea",
|
"--email",
|
||||||
});
|
opts.email,
|
||||||
const stream = await exec.start({ Detach: false, Tty: false });
|
"--must-change-password=false",
|
||||||
await new Promise<void>((resolve) => {
|
]);
|
||||||
stream.on("end", () => resolve());
|
} catch (err) {
|
||||||
stream.on("close", () => resolve());
|
|
||||||
stream.resume();
|
|
||||||
});
|
|
||||||
const info = await exec.inspect();
|
|
||||||
if (info.ExitCode && info.ExitCode !== 0) {
|
|
||||||
// Most common non-zero: "user already exists" — that's fine.
|
|
||||||
log.debug(
|
log.debug(
|
||||||
{ exitCode: info.ExitCode },
|
{ err },
|
||||||
"gitea admin user create returned non-zero (likely already exists)",
|
"gitea admin user create returned non-zero (likely already exists)",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function generateGiteaToken(opts: {
|
||||||
|
containerId: string;
|
||||||
|
username: string;
|
||||||
|
scopes: string[];
|
||||||
|
}): Promise<string> {
|
||||||
|
const output = await execGiteaCli(opts.containerId, [
|
||||||
|
"admin",
|
||||||
|
"user",
|
||||||
|
"generate-access-token",
|
||||||
|
"--username",
|
||||||
|
opts.username,
|
||||||
|
"--token-name",
|
||||||
|
`growqr-backend-${Date.now()}`,
|
||||||
|
"--scopes",
|
||||||
|
opts.scopes.join(","),
|
||||||
|
"--raw",
|
||||||
|
]);
|
||||||
|
const token = output.match(/[a-f0-9]{40}/i)?.[0];
|
||||||
|
if (!token) throw new Error("gitea token generation returned an empty token");
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
async function startOpencodeContainer(opts: {
|
async function startOpencodeContainer(opts: {
|
||||||
userId: string;
|
userId: string;
|
||||||
httpPort: number;
|
httpPort: number;
|
||||||
@@ -298,15 +351,11 @@ export async function provisionUserStack(userId: string): Promise<UserStack> {
|
|||||||
email: adminEmail,
|
email: adminEmail,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mint a token using basic auth.
|
// Mint a token via Gitea's CLI so retries do not depend on a transient
|
||||||
const basicClient = new GiteaClient(giteaBase, {
|
// bootstrap password from a previous provisioning attempt.
|
||||||
kind: "basic",
|
const token = await generateGiteaToken({
|
||||||
|
containerId: gitea.id,
|
||||||
username: adminUsername,
|
username: adminUsername,
|
||||||
password: adminPassword,
|
|
||||||
});
|
|
||||||
const token = await basicClient.ensureAccessToken({
|
|
||||||
username: adminUsername,
|
|
||||||
name: "growqr-backend",
|
|
||||||
scopes: ["write:repository", "write:user", "write:issue"],
|
scopes: ["write:repository", "write:user", "write:issue"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { actorRoutes } from "./routes/actors.js";
|
|||||||
import { opencodeRoutes } from "./routes/opencode.js";
|
import { opencodeRoutes } from "./routes/opencode.js";
|
||||||
import { gitRoutes } from "./routes/git.js";
|
import { gitRoutes } from "./routes/git.js";
|
||||||
import { userRoutes } from "./routes/users.js";
|
import { userRoutes } from "./routes/users.js";
|
||||||
|
import { agentRoutes } from "./routes/agents.js";
|
||||||
|
import { workflowRoutes } from "./routes/workflows.js";
|
||||||
import { db } from "./db/client.js";
|
import { db } from "./db/client.js";
|
||||||
import { hydratePortAllocator, reconcileOnBoot } from "./docker/manager.js";
|
import { hydratePortAllocator, reconcileOnBoot } from "./docker/manager.js";
|
||||||
|
|
||||||
@@ -58,10 +60,17 @@ async function main() {
|
|||||||
|
|
||||||
// PRD HTTP control plane (auth-gated).
|
// PRD HTTP control plane (auth-gated).
|
||||||
app.route("/users", userRoutes());
|
app.route("/users", userRoutes());
|
||||||
|
app.route("/agents", agentRoutes());
|
||||||
|
app.route("/workflows", workflowRoutes());
|
||||||
app.route("/actors", actorRoutes());
|
app.route("/actors", actorRoutes());
|
||||||
app.route("/opencode", opencodeRoutes());
|
app.route("/opencode", opencodeRoutes());
|
||||||
app.route("/git", gitRoutes());
|
app.route("/git", gitRoutes());
|
||||||
|
|
||||||
|
if (process.env.RIVET_RUN_ENGINE === "1") {
|
||||||
|
delete process.env.RIVET_ENDPOINT;
|
||||||
|
}
|
||||||
|
registry.startRunner();
|
||||||
|
|
||||||
serve({ fetch: app.fetch, port: config.port }, (info) => {
|
serve({ fetch: app.fetch, port: config.port }, (info) => {
|
||||||
log.info(
|
log.info(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
import Anthropic from "@anthropic-ai/sdk";
|
|
||||||
import { config } from "../config.js";
|
|
||||||
|
|
||||||
export const anthropic = new Anthropic({
|
|
||||||
apiKey: config.anthropicApiKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const GROW_AGENT_SYSTEM = `You are a Grow Agent — a user's master AI orchestrator on the GrowQR platform.
|
|
||||||
|
|
||||||
You own this user's long-running context, memory, and workspace. You coordinate specialized sub-agents (coding, repo, quest, product-flow, etc.), keep durable state in the user's Gitea memory repository, and execute workflows via the user's OpenCode sandbox.
|
|
||||||
|
|
||||||
Operating principles:
|
|
||||||
- Be concise and direct. The user sees your messages in a Slack-like chat.
|
|
||||||
- Maintain durable memory: commit important decisions, goals, and progress to the user's memory repo using \`commit_memory\`. Read existing context with \`read_memory\` before making suggestions that depend on history.
|
|
||||||
- For anything that requires code, shell, file edits, or generated artifacts, spawn a sub-agent via \`spawn_sub_agent\`. The sub-agent runs through the user's OpenCode container.
|
|
||||||
- Track active goals and quests. Surface progress proactively when the user returns.
|
|
||||||
- Prefer one small commit per meaningful state change over batching.
|
|
||||||
- Never invent tool names. Only use the tools provided.
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type GrowAgentTool =
|
|
||||||
| "spawn_sub_agent"
|
|
||||||
| "commit_memory"
|
|
||||||
| "read_memory"
|
|
||||||
| "list_memory";
|
|
||||||
|
|
||||||
export const growAgentTools: Anthropic.Tool[] = [
|
|
||||||
{
|
|
||||||
name: "spawn_sub_agent",
|
|
||||||
description:
|
|
||||||
"Spawn a specialized sub-agent to run a bounded task through the user's OpenCode container. Use for anything that requires running code, editing files, or producing artifacts.",
|
|
||||||
input_schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
type: {
|
|
||||||
type: "string",
|
|
||||||
description:
|
|
||||||
"Sub-agent type: 'coding', 'repo', 'migration', 'quest', 'product', 'backend', 'frontend', or another short identifier.",
|
|
||||||
},
|
|
||||||
prompt: {
|
|
||||||
type: "string",
|
|
||||||
description:
|
|
||||||
"The full task prompt for the sub-agent. Include all context it needs — sub-agents do not see this conversation.",
|
|
||||||
},
|
|
||||||
channelId: {
|
|
||||||
type: "string",
|
|
||||||
description:
|
|
||||||
"Optional channel/thread id the sub-agent should report into. Generated if omitted.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["type", "prompt"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "commit_memory",
|
|
||||||
description:
|
|
||||||
"Write or update a file in the user's Gitea memory repository. Use for goals, decisions, progress notes, plans, and durable summaries.",
|
|
||||||
input_schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
path: {
|
|
||||||
type: "string",
|
|
||||||
description:
|
|
||||||
"Repo-relative path, e.g. 'goals/active.md' or 'decisions/2026-05-19-architecture.md'.",
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
type: "string",
|
|
||||||
description: "Full UTF-8 file content to write.",
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
type: "string",
|
|
||||||
description: "Commit message describing the change.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["path", "content", "message"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "read_memory",
|
|
||||||
description: "Read a single file from the user's memory repo. Returns null if missing.",
|
|
||||||
input_schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
path: { type: "string" },
|
|
||||||
},
|
|
||||||
required: ["path"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "list_memory",
|
|
||||||
description:
|
|
||||||
"List files at a path prefix in the user's memory repo. Use to discover what context already exists.",
|
|
||||||
input_schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
pathPrefix: {
|
|
||||||
type: "string",
|
|
||||||
description: "Repo-relative directory, e.g. 'goals' or '' for root.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["pathPrefix"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
229
src/lib/llm.ts
Normal file
229
src/lib/llm.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import { config } from "../config.js";
|
||||||
|
|
||||||
|
export const GROW_AGENT_SYSTEM = `You are a Grow Agent - a user's master AI orchestrator on the GrowQR platform.
|
||||||
|
|
||||||
|
You own this user's long-running context, memory, and workspace. You coordinate specialized sub-agents (coding, repo, quest, product-flow, etc.), keep durable state in the user's Gitea memory repository, and execute workflows via the user's OpenCode sandbox.
|
||||||
|
|
||||||
|
Operating principles:
|
||||||
|
- Be concise and direct. The user sees your messages in a Slack-like chat.
|
||||||
|
- Maintain durable memory: commit important decisions, goals, and progress to the user's memory repo using \`commit_memory\`. Read existing context with \`read_memory\` before making suggestions that depend on history.
|
||||||
|
- For anything that requires code, shell, file edits, or generated artifacts, spawn a sub-agent via \`spawn_sub_agent\`. The sub-agent runs through the user's OpenCode container.
|
||||||
|
- Track active goals and quests. Surface progress proactively when the user returns.
|
||||||
|
- Prefer one small commit per meaningful state change over batching.
|
||||||
|
- Never invent tool names. Only use the tools provided.
|
||||||
|
`;
|
||||||
|
|
||||||
|
export type GrowAgentTool =
|
||||||
|
| "spawn_sub_agent"
|
||||||
|
| "commit_memory"
|
||||||
|
| "read_memory"
|
||||||
|
| "list_memory";
|
||||||
|
|
||||||
|
export type LlmTool = {
|
||||||
|
type: "function";
|
||||||
|
function: {
|
||||||
|
name: GrowAgentTool;
|
||||||
|
description: string;
|
||||||
|
parameters: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LlmToolCall = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
arguments: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LlmMessage = {
|
||||||
|
role: "system" | "user" | "assistant" | "tool";
|
||||||
|
content: string | null;
|
||||||
|
tool_call_id?: string;
|
||||||
|
tool_calls?: Array<{
|
||||||
|
id: string;
|
||||||
|
type: "function";
|
||||||
|
function: {
|
||||||
|
name: string;
|
||||||
|
arguments: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const growAgentTools: LlmTool[] = [
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "spawn_sub_agent",
|
||||||
|
description:
|
||||||
|
"Spawn a specialized sub-agent to run a bounded task through the user's OpenCode container. Use for anything that requires running code, editing files, or producing artifacts.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
type: {
|
||||||
|
type: "string",
|
||||||
|
description:
|
||||||
|
"Sub-agent type: 'coding', 'repo', 'migration', 'quest', 'product', 'backend', 'frontend', or another short identifier.",
|
||||||
|
},
|
||||||
|
prompt: {
|
||||||
|
type: "string",
|
||||||
|
description:
|
||||||
|
"The full task prompt for the sub-agent. Include all context it needs - sub-agents do not see this conversation.",
|
||||||
|
},
|
||||||
|
channelId: {
|
||||||
|
type: "string",
|
||||||
|
description:
|
||||||
|
"Optional channel/thread id the sub-agent should report into. Generated if omitted.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["type", "prompt"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "commit_memory",
|
||||||
|
description:
|
||||||
|
"Write or update a file in the user's Gitea memory repository. Use for goals, decisions, progress notes, plans, and durable summaries.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
path: {
|
||||||
|
type: "string",
|
||||||
|
description:
|
||||||
|
"Repo-relative path, e.g. 'goals/active.md' or 'decisions/2026-05-19-architecture.md'.",
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: "string",
|
||||||
|
description: "Full UTF-8 file content to write.",
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: "string",
|
||||||
|
description: "Commit message describing the change.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["path", "content", "message"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "read_memory",
|
||||||
|
description: "Read a single file from the user's memory repo. Returns null if missing.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
path: { type: "string" },
|
||||||
|
},
|
||||||
|
required: ["path"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "list_memory",
|
||||||
|
description:
|
||||||
|
"List files at a path prefix in the user's memory repo. Use to discover what context already exists.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
pathPrefix: {
|
||||||
|
type: "string",
|
||||||
|
description: "Repo-relative directory, e.g. 'goals' or '' for root.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["pathPrefix"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type ChatCompletionsResponse = {
|
||||||
|
choices?: Array<{
|
||||||
|
finish_reason?: string;
|
||||||
|
message?: {
|
||||||
|
content?: string | null;
|
||||||
|
tool_calls?: Array<{
|
||||||
|
id: string;
|
||||||
|
type: "function";
|
||||||
|
function: {
|
||||||
|
name: string;
|
||||||
|
arguments?: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
error?: {
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeModel(model: string): string {
|
||||||
|
if (config.llmProvider === "opencode" && model.startsWith("opencode/")) {
|
||||||
|
return model.slice("opencode/".length);
|
||||||
|
}
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
function endpoint(path: string): string {
|
||||||
|
return `${config.llmBaseUrl.replace(/\/$/, "")}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseToolArguments(raw: string | undefined): Record<string, unknown> {
|
||||||
|
if (!raw) return {};
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return parsed && typeof parsed === "object"
|
||||||
|
? (parsed as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createChatCompletion(opts: {
|
||||||
|
model: string;
|
||||||
|
messages: LlmMessage[];
|
||||||
|
tools: LlmTool[];
|
||||||
|
maxTokens: number;
|
||||||
|
}): Promise<{
|
||||||
|
content: string;
|
||||||
|
toolCalls: LlmToolCall[];
|
||||||
|
finishReason?: string;
|
||||||
|
}> {
|
||||||
|
const res = await fetch(endpoint("/chat/completions"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${config.llmApiKey}`,
|
||||||
|
"content-type": "application/json",
|
||||||
|
accept: "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: normalizeModel(opts.model),
|
||||||
|
messages: opts.messages,
|
||||||
|
tools: opts.tools,
|
||||||
|
tool_choice: "auto",
|
||||||
|
max_tokens: opts.maxTokens,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = (await res.json().catch(() => ({}))) as ChatCompletionsResponse;
|
||||||
|
if (!res.ok) {
|
||||||
|
const detail = json.error?.message ?? JSON.stringify(json);
|
||||||
|
throw new Error(`LLM request failed: ${res.status} ${res.statusText}: ${detail}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const choice = json.choices?.[0];
|
||||||
|
const message = choice?.message;
|
||||||
|
return {
|
||||||
|
content: message?.content ?? "",
|
||||||
|
finishReason: choice?.finish_reason,
|
||||||
|
toolCalls:
|
||||||
|
message?.tool_calls?.map((call) => ({
|
||||||
|
id: call.id,
|
||||||
|
name: call.function.name,
|
||||||
|
arguments: parseToolArguments(call.function.arguments),
|
||||||
|
})) ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
12
src/routes/agents.ts
Normal file
12
src/routes/agents.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { agentCatalog } from "../agents/catalog.js";
|
||||||
|
import { requireUser, type AuthContext } from "../auth/clerk.js";
|
||||||
|
|
||||||
|
export function agentRoutes() {
|
||||||
|
const app = new Hono<AuthContext>();
|
||||||
|
app.use("*", requireUser);
|
||||||
|
|
||||||
|
app.get("/catalog", (c) => c.json({ agents: agentCatalog }));
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
74
src/routes/workflows.ts
Normal file
74
src/routes/workflows.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { createClient } from "rivetkit/client";
|
||||||
|
import { config } from "../config.js";
|
||||||
|
import { requireUser, type AuthContext } from "../auth/clerk.js";
|
||||||
|
import type { Registry } from "../actors/registry.js";
|
||||||
|
|
||||||
|
const client = createClient<Registry>(config.rivetEndpoint);
|
||||||
|
|
||||||
|
function jobWorkflowFor(userId: string) {
|
||||||
|
return client.workflowJob.getOrCreate([userId, "job-application"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function workflowRoutes() {
|
||||||
|
const app = new Hono<AuthContext>();
|
||||||
|
app.use("*", requireUser);
|
||||||
|
|
||||||
|
app.post("/job-application", async (c) => {
|
||||||
|
const userId = c.get("userId");
|
||||||
|
const body = z
|
||||||
|
.object({ goal: z.string().min(1).optional() })
|
||||||
|
.parse(await c.req.json().catch(() => ({})));
|
||||||
|
const handle = jobWorkflowFor(userId);
|
||||||
|
const state = await handle.init({ userId, goal: body.goal });
|
||||||
|
const started = await handle.start();
|
||||||
|
return c.json({ workflow: started ?? state });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/job-application", async (c) => {
|
||||||
|
const userId = c.get("userId");
|
||||||
|
const handle = jobWorkflowFor(userId);
|
||||||
|
const state = await handle.init({ userId });
|
||||||
|
return c.json({ workflow: state });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/job-application/pause", async (c) => {
|
||||||
|
const userId = c.get("userId");
|
||||||
|
const workflow = await jobWorkflowFor(userId).pause();
|
||||||
|
return c.json({ workflow });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/job-application/resume", async (c) => {
|
||||||
|
const userId = c.get("userId");
|
||||||
|
const workflow = await jobWorkflowFor(userId).resume();
|
||||||
|
return c.json({ workflow });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/job-application/agents/:agentId/run", async (c) => {
|
||||||
|
const userId = c.get("userId");
|
||||||
|
const agentId = c.req.param("agentId");
|
||||||
|
const workflow = await jobWorkflowFor(userId).runAgent({ agentId });
|
||||||
|
return c.json({ workflow });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/job-application/agents/:agentId/score", async (c) => {
|
||||||
|
const userId = c.get("userId");
|
||||||
|
const agentId = c.req.param("agentId");
|
||||||
|
const body = z
|
||||||
|
.object({
|
||||||
|
question: z.string().min(1),
|
||||||
|
answer: z.string().min(1),
|
||||||
|
score: z.number().min(0).max(100),
|
||||||
|
notes: z.string().optional(),
|
||||||
|
})
|
||||||
|
.parse(await c.req.json());
|
||||||
|
const workflow = await jobWorkflowFor(userId).recordQaScore({
|
||||||
|
agentId,
|
||||||
|
...body,
|
||||||
|
});
|
||||||
|
return c.json({ workflow });
|
||||||
|
});
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
241
src/services/service-agents.ts
Normal file
241
src/services/service-agents.ts
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
import { config } from "../config.js";
|
||||||
|
import type { AgentProfile } from "../agents/catalog.js";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
|
||||||
|
export type ServiceAgentResult = {
|
||||||
|
status: "ok" | "unavailable" | "local";
|
||||||
|
summary: string;
|
||||||
|
detail?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServiceAgentContext = {
|
||||||
|
userId: string;
|
||||||
|
orgId?: string;
|
||||||
|
goal: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function stableUuid(input: string): string {
|
||||||
|
const hex = createHash("sha256").update(input).digest("hex").slice(0, 32);
|
||||||
|
return [
|
||||||
|
hex.slice(0, 8),
|
||||||
|
hex.slice(8, 12),
|
||||||
|
`4${hex.slice(13, 16)}`,
|
||||||
|
`8${hex.slice(17, 20)}`,
|
||||||
|
hex.slice(20, 32),
|
||||||
|
].join("-");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function serviceJson<T>(
|
||||||
|
baseUrl: string,
|
||||||
|
path: string,
|
||||||
|
init?: RequestInit,
|
||||||
|
): Promise<T> {
|
||||||
|
const res = await fetch(`${baseUrl.replace(/\/$/, "")}${path}`, {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
...(config.a2aAllowedKey
|
||||||
|
? { authorization: `Bearer ${config.a2aAllowedKey}` }
|
||||||
|
: {}),
|
||||||
|
...(init?.headers ?? {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const body = await res.text();
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`${path} returned HTTP ${res.status}: ${body}`);
|
||||||
|
}
|
||||||
|
return (body ? JSON.parse(body) : {}) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function healthCheck(baseUrl: string, label: string): Promise<ServiceAgentResult> {
|
||||||
|
try {
|
||||||
|
const detail = await serviceJson<unknown>(baseUrl, "/health", { method: "GET" });
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
summary: `${label} is reachable and ready for workflow handoff.`,
|
||||||
|
detail,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
status: "unavailable",
|
||||||
|
summary: `${label} is not reachable yet: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runSaraInterview(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
|
||||||
|
const payload = {
|
||||||
|
user_id: ctx.userId,
|
||||||
|
org_id: ctx.orgId ?? "growqr",
|
||||||
|
persona_id: "emma",
|
||||||
|
interview_type: "behavioral",
|
||||||
|
duration_minutes: 15,
|
||||||
|
context: {
|
||||||
|
target_role: ctx.goal,
|
||||||
|
company_name: "Target company",
|
||||||
|
difficulty: "medium",
|
||||||
|
source: "growqr-workflow",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const detail = await serviceJson<Record<string, unknown>>(
|
||||||
|
config.interviewServiceUrl,
|
||||||
|
"/api/v1/configure",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
summary: `Sara created interview session ${detail.session_id ?? "(pending id)"} for ${ctx.goal}.`,
|
||||||
|
detail,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runEmilyRoleplay(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
|
||||||
|
const payload = {
|
||||||
|
user_id: ctx.userId,
|
||||||
|
org_id: ctx.orgId ?? "growqr",
|
||||||
|
persona_id: "emma",
|
||||||
|
duration_minutes: 15,
|
||||||
|
roleplay_type: "custom",
|
||||||
|
brief: `Practice a realistic job-application conversation for: ${ctx.goal}. Include objection handling, concise self-pitching, and a closing next step.`,
|
||||||
|
metadata: {
|
||||||
|
target_role: ctx.goal,
|
||||||
|
difficulty: "medium",
|
||||||
|
source: "growqr-workflow",
|
||||||
|
},
|
||||||
|
qscore: {
|
||||||
|
q_score: 78,
|
||||||
|
profession: "student",
|
||||||
|
formula_version: "workflow-demo",
|
||||||
|
quotients: {
|
||||||
|
CQm: { score: 72, active: true },
|
||||||
|
XQ: { score: 80, active: true },
|
||||||
|
VQ: { score: 76, active: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const detail = await serviceJson<Record<string, unknown>>(
|
||||||
|
config.roleplayServiceUrl,
|
||||||
|
"/api/v1/roleplays/configure",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
summary: `Emily created roleplay session ${detail.session_id ?? "(pending id)"} for ${ctx.goal}.`,
|
||||||
|
detail,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runQuinnQScore(ctx: ServiceAgentContext): Promise<ServiceAgentResult> {
|
||||||
|
const orgId = ctx.orgId ?? "growqr";
|
||||||
|
const qscoreUserId = stableUuid(ctx.userId);
|
||||||
|
const signals = [
|
||||||
|
{
|
||||||
|
signal_id: "resume.uploaded",
|
||||||
|
present: true,
|
||||||
|
score: 82,
|
||||||
|
raw: { source: "resume-agent", workflow_goal: ctx.goal },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal_id: "resume.ats_compatibility",
|
||||||
|
present: true,
|
||||||
|
score: 76,
|
||||||
|
raw: { source: "resume-agent", workflow_goal: ctx.goal },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal_id: "engagement.features_used",
|
||||||
|
present: true,
|
||||||
|
score: 88,
|
||||||
|
raw: { source: "grow-agent", workflow_goal: ctx.goal },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signal_id: "goals.goal_clarity",
|
||||||
|
present: true,
|
||||||
|
score: 90,
|
||||||
|
raw: { source: "grow-agent", workflow_goal: ctx.goal },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ingest = await serviceJson<Record<string, unknown>>(
|
||||||
|
config.qscoreServiceUrl,
|
||||||
|
"/v1/signals/ingest",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
org_id: orgId,
|
||||||
|
user_id: qscoreUserId,
|
||||||
|
profession: "student",
|
||||||
|
source: "growqr-workflow",
|
||||||
|
signals,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let compute: Record<string, unknown> | undefined;
|
||||||
|
try {
|
||||||
|
compute = await serviceJson<Record<string, unknown>>(
|
||||||
|
config.qscoreServiceUrl,
|
||||||
|
"/v1/qscore/compute",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
org_id: orgId,
|
||||||
|
user_id: qscoreUserId,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
status: "unavailable",
|
||||||
|
summary:
|
||||||
|
"Quinn ingested Q-Score signals, but computation is waiting for the QScore worker or formula store.",
|
||||||
|
detail: {
|
||||||
|
ingest,
|
||||||
|
compute_error: err instanceof Error ? err.message : String(err),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "ok",
|
||||||
|
summary: `Quinn computed Q-Score ${compute.q_score ?? "(unknown)"} for ${ctx.goal}.`,
|
||||||
|
detail: { ingest, compute, qscore_user_id: qscoreUserId },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runServiceAgentProbe(
|
||||||
|
agent: AgentProfile,
|
||||||
|
ctx?: ServiceAgentContext,
|
||||||
|
): Promise<ServiceAgentResult> {
|
||||||
|
try {
|
||||||
|
switch (agent.service) {
|
||||||
|
case "interview-service":
|
||||||
|
return ctx
|
||||||
|
? await runSaraInterview(ctx)
|
||||||
|
: healthCheck(config.interviewServiceUrl, "Sara / interview-service");
|
||||||
|
case "roleplay-service":
|
||||||
|
return ctx
|
||||||
|
? await runEmilyRoleplay(ctx)
|
||||||
|
: healthCheck(config.roleplayServiceUrl, "Emily / roleplay-service");
|
||||||
|
case "qscore-service":
|
||||||
|
return ctx
|
||||||
|
? await runQuinnQScore(ctx)
|
||||||
|
: healthCheck(config.qscoreServiceUrl, "Quinn / qscore-service");
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
status: "local",
|
||||||
|
summary: `${agent.name} is a local workflow agent managed by Rivet.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
status: "unavailable",
|
||||||
|
summary: `${agent.name} could not complete its service handoff: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user