diff --git a/packages/agents/flue.config.ts b/packages/agents/flue.config.ts index c9fc0fb..1c3f0c7 100644 --- a/packages/agents/flue.config.ts +++ b/packages/agents/flue.config.ts @@ -1,5 +1,5 @@ -import { defineConfig } from '@flue/cli/config'; +import { defineConfig } from "@flue/cli/config"; export default defineConfig({ - target: 'node', + target: "node", }); diff --git a/packages/agents/package.json b/packages/agents/package.json index 5f2fbc6..7e77761 100644 --- a/packages/agents/package.json +++ b/packages/agents/package.json @@ -4,14 +4,15 @@ "private": true, "type": "module", "scripts": { - "build": "bun --env-file=../../.env flue build", + "build": "node --env-file=../../.env node_modules/@flue/cli/bin/flue.mjs build", + "start": "node --env-file=../../.env dist/server.mjs", "check-types": "tsc --noEmit", - "dev": "bun --env-file=../../.env flue dev", - "dev:tailscale": "bun --env-file=../../.env flue dev", - "run": "bun --env-file=../../.env flue run", - "runner": "bun --env-file=../../.env src/runner.ts", - "run:zopu": "bun --env-file=../../.env flue run zopu", - "run:work-planner": "bun --env-file=../../.env flue run work-planner" + "dev": "node --env-file=../../.env node_modules/@flue/cli/bin/flue.mjs dev", + "dev:tailscale": "node --env-file=../../.env node_modules/@flue/cli/bin/flue.mjs dev", + "run": "node --env-file=../../.env node_modules/@flue/cli/bin/flue.mjs run", + "runner": "node --env-file=../../.env src/runner.ts", + "run:zopu": "node --env-file=../../.env node_modules/@flue/cli/bin/flue.mjs run zopu", + "run:work-planner": "node --env-file=../../.env node_modules/@flue/cli/bin/flue.mjs run work-planner" }, "dependencies": { "@agentos-software/git": "0.3.3", @@ -30,6 +31,10 @@ "@code/config": "workspace:*", "@flue/cli": "latest", "@types/bun": "catalog:", + "@types/node": "catalog:", "typescript": "catalog:" + }, + "engines": { + "node": ">=22.18 <23 || >=23.6" } } diff --git a/packages/agents/src/agents/zopu.ts b/packages/agents/src/agents/zopu.ts index d664606..c04c1f7 100644 --- a/packages/agents/src/agents/zopu.ts +++ b/packages/agents/src/agents/zopu.ts @@ -1,54 +1,9 @@ import { parseAgentEnv } from "@code/env/agent"; import { defineAgent } from "@flue/runtime"; +import INSTRUCTIONS from "../prompts/zopu-instructions.md" with { type: "markdown" }; import { createSliceOneTools } from "../tools/slice-one"; -const INSTRUCTIONS = `You are Zopu for product Slice 1 and the Work planning handoff. - -## Your role - -The application stores each user message as exact evidence before you process it. You may interpret that evidence, but never supply, rewrite, or invent source text. - -## Work routing loop - -When a user sends a message, follow this decision flow: - -1. **Assess actionability.** Does the message contain a concrete problem, request, blocker, opportunity, or decision that warrants a work unit? Greetings, questions about the system, casual conversation, and exploration do NOT create work. If the message is casual conversation, respond naturally and do nothing else. - - Direct questions, casual conversation, and image-reading requests must be answered immediately without calling any tools. - -2. **Identify project context.** Call list_projects. If there is one project, use it. If there are several and the request is ambiguous, ask one focused question. - -3. **Create a Signal (only when actionable).** When the message is actionable: - a. Call list_signal_evidence to see the exact admitted user messages. - b. Select the message IDs that compose the problem statement. - c. Call create_signal with a structured problem statement (title, summary, desiredOutcome, constraints). The problem statement must faithfully represent the user's own intent. Do not invent scope they did not mention. - d. Include the projectId when the project is known. - -4. **Route the Signal.** After creating the Signal: - a. Call list_proposed_work for the project. - b. If the Signal clearly describes the same desired outcome as existing Work, call attach_signal_to_work. - c. Otherwise call create_work_from_signal. - e. If genuinely uncertain whether to attach or create, ask one focused question. - -5. **Explain the outcome.** Tell the user clearly what happened: - - "Captured [Signal title] and linked it to [Work title]." - - "Captured [Signal title] and proposed [Work title]." - - Keep the response brief; the product renders the durable Work card separately. - -## Rules - -- Never supply or rewrite the raw source message text. The control plane copies it server-side. -- You receive and can see images attached to the current user message. This model supports image input. Never claim images are unavailable, omitted, or unsupported. If a message has attached images, inspect them before responding. -- Never create Work from casual chat. -- Ask at most one focused clarification when genuinely ambiguous. -- Preserve project and organization scope at all times. -- Repeated delivery of the same message must not create duplicate Signals or attachments. The backend is idempotent. -- Never claim you created a Signal until the tool call returns successfully. -- Do not start implementation, planning, sandboxes, Git, verification, or delivery. Those are explicitly outside Slice 1. - - After creating proposed Work, the system may invoke the private work-planner. Never claim that a Definition, Design, approval, or implementation exists unless Convex reports it. - - Proposed Work is the only Work status you directly create.`; - export { convexAgentRoute as attachments, convexAgentRoute as route, diff --git a/packages/agents/src/prompts/zopu-instructions.md b/packages/agents/src/prompts/zopu-instructions.md new file mode 100644 index 0000000..4a287c6 --- /dev/null +++ b/packages/agents/src/prompts/zopu-instructions.md @@ -0,0 +1,45 @@ +You are Zopu for product Slice 1 and the Work planning handoff. + +## Your role + +The application stores each user message as exact evidence before you process it. You may interpret that evidence, but never supply, rewrite, or invent source text. + +## Work routing loop + +When a user sends a message, follow this decision flow: + +1. **Assess actionability.** Does the message contain a concrete problem, request, blocker, opportunity, or decision that warrants a work unit? Greetings, questions about the system, casual conversation, and exploration do NOT create work. If the message is casual conversation, respond naturally and do nothing else. + + Direct questions, casual conversation, and image-reading requests must be answered immediately without calling any tools. + +2. **Identify project context.** Call list_projects. If there is one project, use it. If there are several and the request is ambiguous, ask one focused question. + +3. **Create a Signal (only when actionable).** When the message is actionable: + a. Call list_signal_evidence to see the exact admitted user messages. + b. Select the message IDs that compose the problem statement. + c. Call create_signal with a structured problem statement (title, summary, desiredOutcome, constraints). The problem statement must faithfully represent the user's own intent. Do not invent scope they did not mention. + d. Include the projectId when the project is known. + +4. **Route the Signal.** After creating the Signal: + a. Call list_proposed_work for the project. + b. If the Signal clearly describes the same desired outcome as existing Work, call attach_signal_to_work. + c. Otherwise call create_work_from_signal. + e. If genuinely uncertain whether to attach or create, ask one focused question. + +5. **Explain the outcome.** Tell the user clearly what happened: + - "Captured [Signal title] and linked it to [Work title]." + - "Captured [Signal title] and proposed [Work title]." + - Keep the response brief; the product renders the durable Work card separately. + +## Rules + +- Never supply or rewrite the raw source message text. The control plane copies it server-side. +- You receive and can see images attached to the current user message. This model supports image input. Never claim images are unavailable, omitted, or unsupported. If a message has attached images, inspect them before responding. +- Never create Work from casual chat. +- Ask at most one focused clarification when genuinely ambiguous. +- Preserve project and organization scope at all times. +- Repeated delivery of the same message must not create duplicate Signals or attachments. The backend is idempotent. +- Never claim you created a Signal until the tool call returns successfully. +- Do not start implementation, planning, sandboxes, Git, verification, or delivery. Those are explicitly outside Slice 1. + - After creating proposed Work, the system may invoke the private work-planner. Never claim that a Definition, Design, approval, or implementation exists unless Convex reports it. + - Proposed Work is the only Work status you directly create. \ No newline at end of file diff --git a/packages/agents/src/runtime/agent-os.ts b/packages/agents/src/runtime/agent-os.ts index dd870ac..3c44b72 100644 --- a/packages/agents/src/runtime/agent-os.ts +++ b/packages/agents/src/runtime/agent-os.ts @@ -1,22 +1,20 @@ import { parseAgentEnv } from "@code/env/agent"; +import { + makePiAgentOsConfig, + makePiHomeFiles, + decodeWorkAttemptExecutionInput, + WorkAttemptExecutionError, + piSessionEnv, +} from "@code/primitives"; +import type { + ExecutionEvent, + WorkAttemptExecutionResult, +} from "@code/primitives"; import { agentOS, setup } from "@rivet-dev/agentos"; import { createHostDirBackend } from "@rivet-dev/agentos-core"; import { createClient } from "@rivet-dev/agentos/client"; import { Effect } from "effect"; -import { - makePiAgentOsConfig, - makePiHomeFiles, - piSessionEnv, -} from "../../../primitives/src/agent-os"; -import { - decodeWorkAttemptExecutionInput, - WorkAttemptExecutionError, -} from "../../../primitives/src/execution-runtime"; -import type { - ExecutionEvent, - WorkAttemptExecutionResult, -} from "../../../primitives/src/execution-runtime"; import { HostRepositoryWorkspace } from "./host-repository"; const piConfig = makePiAgentOsConfig(); @@ -116,40 +114,53 @@ export const executeAgentOsAttempt = async ( : "Isolated Zopu worktree recreated" ) ); - await Promise.all([ - vm.mountFs({ + const mounts = [ + { + hostPath: prepared.checkoutPath, path: "/workspace/repository", - plugin: createHostDirBackend({ - hostPath: prepared.checkoutPath, - readOnly: false, - }), readOnly: false, - }), - vm.mountFs({ - path: `${prepared.sourceRepositoryPath}/.git`, - plugin: createHostDirBackend({ - hostPath: `${prepared.sourceRepositoryPath}/.git`, - readOnly: false, - }), + }, + { + hostPath: prepared.sourceRepositoryPath, + path: prepared.sourceRepositoryPath, readOnly: false, - }), - vm.mountFs({ + }, + { + hostPath: prepared.piHomePath, path: "/home/zopu", - plugin: createHostDirBackend({ - hostPath: prepared.piHomePath, - readOnly: false, - }), readOnly: false, - }), - vm.mountFs({ + }, + { + hostPath: prepared.toolsPath, path: "/opt/zopu-tools", - plugin: createHostDirBackend({ - hostPath: prepared.toolsPath, - readOnly: true, - }), readOnly: true, - }), - ]); + }, + ] as const; + const mountNext = async (index: number): Promise => { + const mount = mounts[index]; + if (!mount) { + return; + } + try { + await vm.mountFs({ + path: mount.path, + plugin: createHostDirBackend({ + hostPath: mount.hostPath, + readOnly: mount.readOnly, + }), + readOnly: mount.readOnly, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw executionError( + `Failed to mount ${mount.hostPath} at ${mount.path}: ${message}`, + "HarnessFailed", + false + ); + } + await mountNext(index + 1); + }; + await mountNext(0); const { baseRevision } = prepared; events.push( event(2, "repository.ready", "Repository checkout is ready", { diff --git a/packages/agents/src/runtime/host-repository.ts b/packages/agents/src/runtime/host-repository.ts index 0804090..f96519c 100644 --- a/packages/agents/src/runtime/host-repository.ts +++ b/packages/agents/src/runtime/host-repository.ts @@ -11,7 +11,7 @@ import { } from "node:fs/promises"; import path from "node:path"; -import type { PiHomeFiles } from "../../../primitives/src/agent-os"; +import type { PiHomeFiles } from "@code/primitives"; interface PrepareRepositoryInput { attemptId: string; @@ -159,6 +159,10 @@ export class HostRepositoryWorkspace { "Zopu worktree creation" ); + const bunExecutable = + process.env.BUN_EXECUTABLE ?? + execFileSync("which", ["bun"], { encoding: "utf-8" }).trim(); + const sourceEnvPath = path.join(this.#sourceRepositoryPath, ".env"); if (await pathExists(sourceEnvPath)) { await copyFile(sourceEnvPath, path.join(checkoutPath, ".env")); @@ -166,7 +170,7 @@ export class HostRepositoryWorkspace { if (this.#installDependencies) { requireSuccess( await runProcess( - "bun", + bunExecutable, ["install", "--frozen-lockfile"], checkoutPath, { CI: "1" } @@ -177,9 +181,6 @@ export class HostRepositoryWorkspace { const toolsBinPath = path.join(toolsPath, "bin"); await mkdir(toolsBinPath, { recursive: true }); - const bunExecutable = execFileSync("which", ["bun"], { - encoding: "utf-8", - }).trim(); const bunPath = path.join(toolsBinPath, "bun"); await copyFile(bunExecutable, bunPath); await chmod(bunPath, 0o755);