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
182 changes: 178 additions & 4 deletions bun.lock

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions deploy/policy.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
turns:
interactive_timeout_ms: 600000
stall_timeout_ms: 120000
timeout_ms: 600000

executions:
max_concurrent: 4
max_turns: 20
stall_timeout_ms: 300000
turn_timeout_ms: 1800000
max_attempts: 3

tasks:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"check": "bun run typecheck && bun run lint && bun run fmt:check && bun run knip"
},
"dependencies": {
"@bevyl-ai/agent-tools": "0.13.0",
"@bevyl-ai/agent-tools": "0.14.4",
"@slack/socket-mode": "2.0.7",
"@slack/web-api": "^8.1.1",
"drizzle-orm": "^0.45.2",
Expand Down
108 changes: 54 additions & 54 deletions src/codex.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
import {
AppServerSession,
maybeRotateGateway,
scrubSecrets,
type CodexConfig,
type DynamicTool,
} from "@bevyl-ai/agent-tools";
import { inject, injectAll, singleton } from "tsyringe";
import { log } from "./log";
import { codexThread, maybeRotateGateway, type Tools } from "@bevyl-ai/agent-tools";
import { inject, singleton } from "tsyringe";
import { LedgerService } from "./ledger-service";
import type { Task } from "./ledger/schema";
import { log } from "./log";
import { POLICY, type Policy } from "./policy";
import { LedgerService } from "./ledger-service";
import { Soul } from "./soul";
import { earTools, residentTools, workerTools } from "./tools";
import { Workspaces, type Role } from "./workspaces";
import { TOOL } from "./tokens";
import { taskTools, verdictTool } from "./tools";

const SPEAKING = new Set(["reply", "react"]);
type Tier = Policy["models"]["low"];
type Thread = Awaited<ReturnType<typeof codexThread>>["thread"];

@singleton()
export class Codex {
Expand All @@ -24,60 +18,66 @@ export class Codex {
private readonly soul: Soul,
private readonly ledger: LedgerService,
private readonly workspaces: Workspaces,
@injectAll(TOOL) private readonly tools: DynamicTool[],
) {}

respond(prompt: string): Promise<void> {
const { turns } = this.policy;
return this.session("resident", this.tools, {
turnTimeoutMs: turns.interactive_timeout_ms,
stallTimeoutMs: turns.stall_timeout_ms,
}).runOnce(prompt);
return this.once("resident", residentTools, {}, this.policy.turns.timeout_ms, prompt);
}

async shouldAgentRespond(prompt: string): Promise<boolean> {
const { turns, models } = this.policy;
await this.session("ear", [verdictTool()], {
...models.low,
turnTimeoutMs: turns.interactive_timeout_ms,
stallTimeoutMs: turns.stall_timeout_ms,
}).runOnce(prompt);
await this.once("ear", earTools, this.policy.models.low, this.policy.turns.timeout_ms, prompt);
return this.ledger.wantsResponse();
}

runWorker(taskId: string, tier: Task["tier"], next: () => string | null): Promise<void> {
async runWorker(taskId: string, tier: Task["tier"], next: () => string | null): Promise<void> {
const { executions, models } = this.policy;
const voiceless = this.tools.filter((t) => !SPEAKING.has(t.name));
return this.session(
"worker",
[...taskTools(taskId), ...voiceless],
{ ...models[tier], stallTimeoutMs: executions.stall_timeout_ms, title: taskId },
() => {
this.ledger.interrupt(taskId);
},
).runTurns(next);
const { thread, close } = await this.thread("worker", workerTools(taskId), models[tier]);
try {
for (let prompt = next(); prompt !== null; prompt = next())
await this.turn(thread, taskId, prompt, executions.turn_timeout_ms).catch(
(error: unknown) => {
this.ledger.interrupt(taskId);
throw error;
},
);
} finally {
close();
}
}

private session(
role: Role,
tools: DynamicTool[],
config: Partial<CodexConfig>,
onTurnError: () => void = () => {},
): AppServerSession {
private async once(role: Role, tools: Tools, tier: Tier, timeoutMs: number, prompt: string) {
const { thread, close } = await this.thread(role, tools, tier);
try {
await this.turn(thread, role, prompt, timeoutMs);
} finally {
close();
}
}

private thread(role: Role, tools: Tools, tier: Tier) {
this.soul.refresh();
return new AppServerSession(
{ cwd: this.workspaces[role], title: role, ...config },
return codexThread({
tools,
(event) => {
if (event.log) log.info(role, { line: event.log });
},
{
scrubEnv: scrubSecrets,
onTurnError: (error) => {
maybeRotateGateway({ reason: String(error) });
onTurnError();
},
},
);
workingDirectory: this.workspaces[role],
sandboxMode: "workspace-write",
networkAccessEnabled: true,
...(tier.model ? { model: tier.model } : {}),
...(tier.effort ? { modelReasoningEffort: tier.effort } : {}),
});
}

private async turn(thread: Thread, label: string, prompt: string, timeoutMs: number) {
const { events } = await thread.runStreamed(prompt, { signal: AbortSignal.timeout(timeoutMs) });
for await (const event of events) {
if (event.type === "item.completed") {
const { item } = event;
if (item.type === "command_execution") log.info(label, { line: `$ ${item.command}` });
else if (item.type === "mcp_tool_call") log.info(label, { line: `⚙ ${item.tool}` });
else if (item.type === "agent_message") log.info(label, { line: `● ${item.text}` });
} else if (event.type === "turn.failed") {
maybeRotateGateway({ reason: event.error.message });
throw new Error(event.error.message);
} else if (event.type === "error") throw new Error(event.message);
}
}
}
14 changes: 6 additions & 8 deletions src/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,22 @@ import { z } from "zod";
import type { InjectionToken } from "tsyringe";

const ModelTier = z
.object({ model: z.string().optional(), effort: z.string().optional() })
.object({
model: z.string().optional(),
effort: z.enum(["minimal", "low", "medium", "high", "xhigh"]).optional(),
})
.prefault({});

const PolicySchema = z.object({
persona: z.string().optional(),
venue_instructions: z.record(z.string(), z.string()).default({}),
ear_debounce_ms: z.number().default(45_000),
turns: z
.object({
interactive_timeout_ms: z.number().default(120_000),
stall_timeout_ms: z.number().default(45_000),
})
.prefault({}),
turns: z.object({ timeout_ms: z.number().default(600_000) }).prefault({}),
executions: z
.object({
max_concurrent: z.number().default(4),
max_turns: z.number().default(40),
stall_timeout_ms: z.number().default(5 * 60 * 1000),
turn_timeout_ms: z.number().default(30 * 60 * 1000),
max_attempts: z.number().default(3),
backoff_ms: z.number().default(30_000),
})
Expand Down
1 change: 0 additions & 1 deletion src/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { loadPolicy, POLICY, POLICY_PATH, type Policy } from "./policy";
import { PromptRenderer } from "./prompt-renderer";
import { BOT_USER_ID, requireEnv, WORKSPACE } from "./tokens";
import { Voice } from "./voice";
import "./tools";

const BATCH = 8;

Expand Down
2 changes: 0 additions & 2 deletions src/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { DynamicTool } from "@bevyl-ai/agent-tools";
import type { InjectionToken } from "tsyringe";

export function requireEnv(name: string): string {
Expand All @@ -7,6 +6,5 @@ export function requireEnv(name: string): string {
return value;
}

export const TOOL: InjectionToken<DynamicTool> = Symbol("tool");
export const BOT_USER_ID: InjectionToken<string> = Symbol("botUserId");
export const WORKSPACE: InjectionToken<string> = Symbol("workspace");
Loading
Loading