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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
Warnings:

- You are about to drop the `Session` table. If the table is not empty, all the data it contains will be lost.

*/
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "semanticMem" SET DEFAULT '';

-- DropTable
DROP TABLE "Session";
7 changes: 0 additions & 7 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,4 @@ model RunEvent {
createdAt DateTime @default(now())

@@index([runId, createdAt])
}

model Session{
id String @id @default(uuid())
session Json[]
context Json[]
iteration Int
}
1 change: 1 addition & 0 deletions apps/backend/src/modules/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ projectRouter.get("/", auth, async (req: AuthRequest, res: Response) => {

res.status(401).json({success: false, message: `UserId not given`})
}
await prisma.project.findMany({where: {userId: userId}})
const projects = await prisma.project.findMany({where: {userId: userId}})

if(!projects){
Expand Down
5 changes: 4 additions & 1 deletion apps/backend/src/modules/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ runRouter.get("/:projectId/runs/:runId", auth, async (req: Request, res: Respons

runRouter.get("/:projectId/:runId/todos", auth, async (req: Request, res: Response) => {
const { projectId, runId } = req.params;

if (
typeof projectId !== "string" ||
typeof runId !== "string"
Expand Down Expand Up @@ -214,6 +213,8 @@ runRouter.get("/:projectId/:runId/summaries", auth, async (req: Request, res: Re


runRouter.post("/:projectId/:runId/todos", internalAuth, async (req: Request, res: Response) => {
logger.info(`Saving todos to the db`)

const { projectId, runId } = req.params;
const { todos } = req.body as {
todos: {id: number, task: string, agent: AgentType, status: "pending" | "completed", dependency: number[], designNeeded?: boolean}[]
Expand All @@ -233,6 +234,8 @@ runRouter.post("/:projectId/:runId/todos", internalAuth, async (req: Request, re
}

try {
logger.info(`calling promises to save todos`)

const created = await Promise.all(todos.map((t) =>
prisma.todo.create({
data: {
Expand Down
59 changes: 39 additions & 20 deletions apps/backend/src/modules/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { internalAuth } from "./middleware";
import { prisma } from "../prisma";
import { randomUUIDv7 } from "bun";
import type { OrchestratorEvent } from "../../../../packages/agents";
import { logger } from "./utils";

/*
POST /internal/sessions/:runId/events
Expand All @@ -16,34 +17,52 @@ sessionRouter.post('/:runId/events', internalAuth, async (req: Request, res: Res
const event: OrchestratorEvent = req.body

if(typeof runId !== 'string'){
return res.send({message: `Invalid runId type`}).status(400)
return res.status(400).json({success: false, message: `Invalid runId type`})
}

try{
await prisma.runEvent.create({data: {
id: randomUUIDv7(),
runId: runId,
type: event.type,
createdAt: new Date(),
}})
return res.status(200).json({success: true, message: `event saved`})
} catch(e){
logger.error(`Failed to save event for run ${runId}: ${e}`)
return res.status(500).json({success: false, message: `Internal server error`})
}
const db = await prisma.runEvent.create({data: {
id: randomUUIDv7(),
runId: runId,
type: event.type,
createdAt: new Date(),
}})

return res.status(200).json({success: true, message: `event saved`})
})

// Run already carries dedicated snapshot columns for exactly this — no
// separate table needed. Both fields are `String? @db.Text`, so the
// snapshots (which can be arrays or single objects depending on the caller)
// must arrive JSON.stringify'd; we don't re-stringify here since that'd
// double-encode whatever the caller already sent.
sessionRouter.post('/:runId/state', internalAuth, async (req: Request, res: Response) =>{

const {runId} = req.params
const data = req.body
const {context_snapshot, session_snapshot, iteration} = req.body as {
context_snapshot?: string, session_snapshot?: string, iteration?: number
}

if(typeof runId !== 'string'){
return res.send({message: `Invalid runId type`}).status(400)
return res.status(400).json({success: false, message: `Invalid runId type`})
}

try{
await prisma.run.update({
where: {id: runId},
data: {
contextSnapshot: context_snapshot,
sessionSnapshot: session_snapshot,
currentStep: iteration !== undefined ? String(iteration) : undefined,
}
})
return res.status(200).json({success: true, message: `session and context state saved`})
} catch(e){
logger.error(`Failed to save session state for run ${runId}: ${e}`)
return res.status(500).json({success: false, message: `Internal server error`})
}
const db = await prisma.session.create({data: {
id: randomUUIDv7(),
session: data.session_snapshot,
context: data.context_snapshot,
iteration: data.iteration
}})

return res.status(200).json({success: true, message: `session and context state to db`})
})

export default sessionRouter;
11 changes: 5 additions & 6 deletions packages/agents/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export class OrchestratorAgent{
const savedQuestions = questionRes.data.data
const questions: Question[] = savedQuestions.map((q) => ({question: q.question, option: q.options}))
const designs = designRes.data.data
logger.info(`${questions} and ${designs} are response from backend`)
logger.info(`Fetched ${questions.length} saved question(s) and ${designs.length} saved design(s)`)
if(answers){
logger.info(`Answer added to user prompt`)
userPrompt += `Answers for these ${questions} are: ${answers}`
Expand Down Expand Up @@ -330,19 +330,18 @@ export class OrchestratorAgent{
let summaries: string[] = []

for(let i = 0 ; i < sequentialTodos.length; i++){
logger.info(`Starting task ${sequentialTodos[i]?.task}`)
const todo = sequentialTodos[i];
logger.info(`Task ${todo?.id}: ${todo?.task}`)
// #TODO: Failure handling of planner
if (!todo?.agent){
console.warn(`This ${todo} is not assigned with any agent.`)
logger.warn(`Task ${todo?.id} has no agent assigned, stopping DAG execution`)
break;
}

const agentType = todo?.agent
const input = this.inputBuilders[agentType](todo, this.context, this.state, this.semanticMem)
logger.info(`${input} is the input made for ${agentType}`)
const subagent = new SubAgent(agentType, input, this.userId, this.projectId, this.runId, this.sandbox, this.selectedDesign)
logger.info(`Starting runloop for the subagent`)
logger.info(`Starting runloop for ${agentType} (task ${todo.id})`)
const result = await subagent.runLoop()
summaries.push(result.summary)

Expand Down Expand Up @@ -479,7 +478,7 @@ export class OrchestratorAgent{
}
}
catch(e){
console.error(e)
logger.error(`TesterDebuggerLoop failed: ${e}`)
return{
success: false,
summaries,
Expand Down
2 changes: 2 additions & 0 deletions packages/agents/agent/config/systemConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export const PORT = 3000
export const SANDBOX_HOME = '/home/user'
export const PROJECT_ROOT = `${SANDBOX_HOME}/app`
export const MAX_BOOT_WAIT_MS = 20000
export const POLL_INTERVAL_MS = 500
export const BACKEND_URL = process.env.BACKEND_URL ?? `http://localhost:3000`
Expand Down
4 changes: 2 additions & 2 deletions packages/agents/agent/mainAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ export class MainAgent{
async saveSessionState(){
try{
await axios.post(`${BACKEND_URL}/internal/session/${this.runId}/state`, {
context_snapshot: this.context,
session_snapshot: this.session,
context_snapshot: JSON.stringify(this.context),
session_snapshot: JSON.stringify(this.session),
iteration: this.iterations,
}, {
headers: internalAuthHeader(),
Expand Down
45 changes: 45 additions & 0 deletions packages/agents/agent/skills/acceptance-criteria/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
name: derive-acceptance-criteria
description: Turns a task description into concrete, checkable pass/fail assertions before testing begins. Use before writing or running any test, and whenever a task's "done" condition is ambiguous or implicit.
---

# Derive Acceptance Criteria

Before running anything, convert the task into a list of assertions that
are each independently checkable as true/false. If you can't tell whether
an assertion passed or failed by inspection or a command's output, rewrite
it until you can.

## Procedure

1. Read the original task/task-node description and the design record (if
any UI is involved).
2. Write out explicit criteria covering:
- **Functional** — what the feature must actually do (e.g. "submitting
the form with valid input creates a record and redirects")
- **Structural** — build passes, no type errors, no unresolved imports
- **Visual** (if UI involved) — matches the design record's tokens, no
layout overflow, responsive at standard breakpoints
- **Negative cases** — what must NOT happen (e.g. "invalid input does
not submit," "no console errors on load")
3. Do not invent criteria the task never implied — stick to what was asked
plus baseline quality bars from `smoke-checklist`.
4. If the task is genuinely ambiguous about what "done" means, surface that
rather than guessing a scope.

## Format

Write each criterion as a single falsifiable statement, not a vague goal.

Wrong: "The form should work well."
Right: "Submitting the form with all required fields filled shows a success
state and clears the form. Submitting with a required field empty shows an
inline error and does not submit."

## Do not

- Test only the happy path because it's what the task description led with.
- Treat "it renders without crashing" as sufficient acceptance criteria for
anything with actual logic in it.
- Skip this step for small tasks — a one-line criteria list still catches
more than testing on vibes.
45 changes: 45 additions & 0 deletions packages/agents/agent/skills/acceptance-test/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
name: derive-acceptance-criteria
description: Turns a task description into concrete, checkable pass/fail assertions before testing begins. Use before writing or running any test, and whenever a task's "done" condition is ambiguous or implicit.
---

# Derive Acceptance Criteria

Before running anything, convert the task into a list of assertions that
are each independently checkable as true/false. If you can't tell whether
an assertion passed or failed by inspection or a command's output, rewrite
it until you can.

## Procedure

1. Read the original task/task-node description and the design record (if
any UI is involved).
2. Write out explicit criteria covering:
- **Functional** — what the feature must actually do (e.g. "submitting
the form with valid input creates a record and redirects")
- **Structural** — build passes, no type errors, no unresolved imports
- **Visual** (if UI involved) — matches the design record's tokens, no
layout overflow, responsive at standard breakpoints
- **Negative cases** — what must NOT happen (e.g. "invalid input does
not submit," "no console errors on load")
3. Do not invent criteria the task never implied — stick to what was asked
plus baseline quality bars from `smoke-checklist`.
4. If the task is genuinely ambiguous about what "done" means, surface that
rather than guessing a scope.

## Format

Write each criterion as a single falsifiable statement, not a vague goal.

Wrong: "The form should work well."
Right: "Submitting the form with all required fields filled shows a success
state and clears the form. Submitting with a required field empty shows an
inline error and does not submit."

## Do not

- Test only the happy path because it's what the task description led with.
- Treat "it renders without crashing" as sufficient acceptance criteria for
anything with actual logic in it.
- Skip this step for small tasks — a one-line criteria list still catches
more than testing on vibes.
32 changes: 32 additions & 0 deletions packages/agents/agent/skills/add-a-route/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
name: add-a-route
description: Full checklist and conventions for adding a new page or route. Use whenever a task involves creating a new page, screen, or user-reachable endpoint — not for editing an existing route.
---

# Add a Route

## Steps (all required, in order)

1. Create the route file following `project-conventions` naming/location.
2. Register the route in the router configuration.
3. If the route is user-reachable (not an internal/API-only route), add the
nav entry linking to it.
4. Add loading and error states for any data the route fetches — never ship
a route that can render a bare unhandled fetch or a blank screen on error.
5. If the route requires auth, apply the project's existing auth-guard
pattern rather than inventing a new one.
6. Run the checklist in `smoke-checklist` before reporting done.

## Conventions

- Route components live in `src/pages/`, one file per route.
- Route-level data fetching happens in the route component, not buried in
a deeply nested child.
- Dynamic route params are typed, not accessed as untyped strings.

## Do not

- Register a route without a corresponding nav entry when it's meant to be
user-reachable — this is the single most common incomplete-route failure.
- Duplicate an existing route's path.
- Fetch data with no loading/error handling "because it'll usually work."
50 changes: 50 additions & 0 deletions packages/agents/agent/skills/api-route-convention/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
name: api-route-conventions
description: Error shape, status codes, and the streaming/SSE pattern for backend routes. Use whenever writing or editing an API or server route.
---

# API Route Conventions

## Error responses

Use a consistent error shape across every route:

```json
{ "error": { "message": "<human readable>", "code": "<machine readable>" } }
```

Never return a bare string, a stack trace, or an inconsistent shape from
one route to another.

## Status codes

- 200 — success
- 201 — resource created
- 400 — invalid input (validation failure)
- 401 — not authenticated
- 403 — authenticated but not authorized
- 404 — resource not found
- 500 — unexpected server error (should be rare; most failures should be
caught and mapped to a specific 4xx)

## Streaming / SSE

Follow the project's existing SSE pattern exactly if one exists — do not
introduce a second streaming mechanism. If none exists yet and the task
requires one, keep the event backend-owned and stream through the existing
relay path rather than having the client connect directly to any sandboxed
process.

## Validation

Validate and parse all inputs at the top of the route handler before any
business logic runs. Don't validate halfway through.

## Do not

- Return different error shapes from different routes.
- Use 200 with an `error` field in the body instead of an actual error
status code.
- Let an unhandled exception fall through to a generic 500 without at least
logging enough to debug it later.
- Introduce a second streaming mechanism alongside an existing SSE setup.
Loading