Skip to content

Commit 429078b

Browse files
authored
feat: add task mode to agent server (#816)
1 parent fc51e7c commit 429078b

File tree

7 files changed

+146
-8
lines changed

7 files changed

+146
-8
lines changed

packages/agent/src/posthog-api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
ArtifactType,
33
PostHogAPIConfig,
44
StoredEntry,
5+
Task,
56
TaskRun,
67
TaskRunArtifact,
78
} from "./types.js";
@@ -90,6 +91,11 @@ export class PostHogAPIClient {
9091
return getLlmGatewayUrl(this.baseUrl);
9192
}
9293

94+
async getTask(taskId: string): Promise<Task> {
95+
const teamId = this.getTeamId();
96+
return this.apiRequest<Task>(`/api/projects/${teamId}/tasks/${taskId}/`);
97+
}
98+
9399
async getTaskRun(taskId: string, runId: string): Promise<TaskRun> {
94100
const teamId = this.getTeamId();
95101
return this.apiRequest<TaskRun>(

packages/agent/src/server/agent-server.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ describe("AgentServer HTTP Mode", () => {
9696
apiUrl: "http://localhost:8000",
9797
apiKey: "test-api-key",
9898
projectId: 1,
99+
mode: "interactive",
100+
taskId: "test-task-id",
101+
runId: "test-run-id",
99102
});
100103
return server;
101104
};
@@ -108,21 +111,22 @@ describe("AgentServer HTTP Mode", () => {
108111
team_id: 1,
109112
user_id: 1,
110113
distinct_id: "test-distinct-id",
114+
mode: "interactive",
111115
...overrides,
112116
},
113117
TEST_PRIVATE_KEY,
114118
);
115119
};
116120

117121
describe("GET /health", () => {
118-
it("returns ok status", async () => {
122+
it("returns ok status with active session", async () => {
119123
await createServer().start();
120124

121125
const response = await fetch(`http://localhost:${port}/health`);
122126
const body = await response.json();
123127

124128
expect(response.status).toBe(200);
125-
expect(body).toEqual({ status: "ok", hasSession: false });
129+
expect(body).toEqual({ status: "ok", hasSession: true });
126130
});
127131
});
128132

@@ -179,9 +183,9 @@ describe("AgentServer HTTP Mode", () => {
179183
expect(response.status).toBe(401);
180184
});
181185

182-
it("returns 400 when no session exists", async () => {
186+
it("returns 400 when run_id does not match active session", async () => {
183187
await createServer().start();
184-
const token = createToken();
188+
const token = createToken({ run_id: "different-run-id" });
185189

186190
const response = await fetch(`http://localhost:${port}/command`, {
187191
method: "POST",

packages/agent/src/server/agent-server.ts

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import { PostHogAPIClient } from "../posthog-api.js";
1414
import { SessionLogWriter } from "../session-log-writer.js";
1515
import { TreeTracker } from "../tree-tracker.js";
16-
import type { DeviceInfo, TreeSnapshotEvent } from "../types.js";
16+
import type { AgentMode, DeviceInfo, TreeSnapshotEvent } from "../types.js";
1717
import { AsyncMutex } from "../utils/async-mutex.js";
1818
import { getLlmGatewayUrl } from "../utils/gateway.js";
1919
import { Logger } from "../utils/logger.js";
@@ -144,13 +144,23 @@ export class AgentServer {
144144
private server: ServerType | null = null;
145145
private session: ActiveSession | null = null;
146146
private app: Hono;
147+
private posthogAPI: PostHogAPIClient;
147148

148149
constructor(config: AgentServerConfig) {
149150
this.config = config;
150151
this.logger = new Logger({ debug: true, prefix: "[AgentServer]" });
152+
this.posthogAPI = new PostHogAPIClient({
153+
apiUrl: config.apiUrl,
154+
projectId: config.projectId,
155+
getApiKey: () => config.apiKey,
156+
});
151157
this.app = this.createApp();
152158
}
153159

160+
private getEffectiveMode(payload: JwtPayload): AgentMode {
161+
return payload.mode ?? this.config.mode;
162+
}
163+
154164
private createApp(): Hono {
155165
const app = new Hono();
156166

@@ -309,7 +319,7 @@ export class AgentServer {
309319
}
310320

311321
async start(): Promise<void> {
312-
return new Promise((resolve) => {
322+
await new Promise<void>((resolve) => {
313323
this.server = serve(
314324
{
315325
fetch: this.app.fetch,
@@ -321,6 +331,26 @@ export class AgentServer {
321331
},
322332
);
323333
});
334+
335+
await this.autoInitializeSession();
336+
}
337+
338+
private async autoInitializeSession(): Promise<void> {
339+
const { taskId, runId, mode, projectId } = this.config;
340+
341+
this.logger.info("Auto-initializing session", { taskId, runId, mode });
342+
343+
// Create a synthetic payload from config (no JWT needed for auto-init)
344+
const payload: JwtPayload = {
345+
task_id: taskId,
346+
run_id: runId,
347+
team_id: projectId,
348+
user_id: 0, // System-initiated
349+
distinct_id: "agent-server",
350+
mode,
351+
};
352+
353+
await this.initializeSession(payload, null);
324354
}
325355

326356
async stop(): Promise<void> {
@@ -409,7 +439,7 @@ export class AgentServer {
409439

410440
private async initializeSession(
411441
payload: JwtPayload,
412-
sseController: SseController,
442+
sseController: SseController | null,
413443
): Promise<void> {
414444
if (this.session) {
415445
await this.cleanupSession();
@@ -506,6 +536,73 @@ export class AgentServer {
506536
};
507537

508538
this.logger.info("Session initialized successfully");
539+
540+
await this.sendInitialTaskMessage(payload);
541+
}
542+
543+
private async sendInitialTaskMessage(payload: JwtPayload): Promise<void> {
544+
if (!this.session) return;
545+
546+
try {
547+
this.logger.info("Fetching task details", { taskId: payload.task_id });
548+
const task = await this.posthogAPI.getTask(payload.task_id);
549+
550+
if (!task.description) {
551+
this.logger.warn("Task has no description, skipping initial message");
552+
return;
553+
}
554+
555+
this.logger.info("Sending initial task message", {
556+
taskId: payload.task_id,
557+
descriptionLength: task.description.length,
558+
});
559+
560+
const result = await this.session.clientConnection.prompt({
561+
sessionId: payload.run_id,
562+
prompt: [{ type: "text", text: task.description }],
563+
});
564+
565+
this.logger.info("Initial task message completed", {
566+
stopReason: result.stopReason,
567+
});
568+
569+
// Only auto-complete for background mode
570+
const mode = this.getEffectiveMode(payload);
571+
if (mode === "background") {
572+
await this.signalTaskComplete(payload, result.stopReason);
573+
} else {
574+
this.logger.info("Interactive mode - staying open for conversation");
575+
}
576+
} catch (error) {
577+
this.logger.error("Failed to send initial task message", error);
578+
// Signal failure for background mode
579+
const mode = this.getEffectiveMode(payload);
580+
if (mode === "background") {
581+
await this.signalTaskComplete(payload, "error");
582+
}
583+
}
584+
}
585+
586+
private async signalTaskComplete(
587+
payload: JwtPayload,
588+
stopReason: string,
589+
): Promise<void> {
590+
const status =
591+
stopReason === "cancelled"
592+
? "cancelled"
593+
: stopReason === "error"
594+
? "failed"
595+
: "completed";
596+
597+
try {
598+
await this.posthogAPI.updateTaskRun(payload.task_id, payload.run_id, {
599+
status,
600+
error_message: stopReason === "error" ? "Agent error" : undefined,
601+
});
602+
this.logger.info("Task completion signaled", { status, stopReason });
603+
} catch (error) {
604+
this.logger.error("Failed to signal task completion", error);
605+
}
509606
}
510607

511608
private configureEnvironment(): void {
@@ -534,11 +631,21 @@ export class AgentServer {
534631
});
535632
}
536633

537-
private createCloudClient(_payload: JwtPayload) {
634+
private createCloudClient(payload: JwtPayload) {
635+
const mode = this.getEffectiveMode(payload);
636+
538637
return {
539638
requestPermission: async (params: {
540639
options: Array<{ kind: string; optionId: string }>;
541640
}) => {
641+
// Background mode: always auto-approve permissions
642+
// Interactive mode: also auto-approve for now (user can monitor via SSE)
643+
// Future: interactive mode could pause and wait for user approval via SSE
644+
this.logger.debug("Permission request", {
645+
mode,
646+
options: params.options,
647+
});
648+
542649
const allowOption = params.options.find(
543650
(o) => o.kind === "allow_once" || o.kind === "allow_always",
544651
);

packages/agent/src/server/bin.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,14 @@ program
3737
.name("agent-server")
3838
.description("PostHog cloud agent server - runs in sandbox environments")
3939
.option("--port <port>", "HTTP server port", "3001")
40+
.option(
41+
"--mode <mode>",
42+
"Execution mode: interactive or background",
43+
"interactive",
44+
)
4045
.requiredOption("--repositoryPath <path>", "Path to the repository")
46+
.requiredOption("--taskId <id>", "Task ID")
47+
.requiredOption("--runId <id>", "Task run ID")
4148
.action(async (options) => {
4249
const envResult = envSchema.safeParse(process.env);
4350

@@ -51,13 +58,18 @@ program
5158

5259
const env = envResult.data;
5360

61+
const mode = options.mode === "background" ? "background" : "interactive";
62+
5463
const server = new AgentServer({
5564
port: parseInt(options.port, 10),
5665
jwtPublicKey: env.JWT_PUBLIC_KEY,
5766
repositoryPath: options.repositoryPath,
5867
apiUrl: env.POSTHOG_API_URL,
5968
apiKey: env.POSTHOG_PERSONAL_API_KEY,
6069
projectId: env.POSTHOG_PROJECT_ID,
70+
mode,
71+
taskId: options.taskId,
72+
runId: options.runId,
6173
});
6274

6375
process.on("SIGINT", async () => {

packages/agent/src/server/jwt.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const userDataSchema = z.object({
99
team_id: z.number(),
1010
user_id: z.number(),
1111
distinct_id: z.string(),
12+
mode: z.enum(["interactive", "background"]).optional().default("interactive"),
1213
});
1314

1415
const jwtPayloadSchema = userDataSchema.extend({

packages/agent/src/server/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import type { AgentMode } from "../types.js";
2+
13
export interface AgentServerConfig {
24
port: number;
35
repositoryPath: string;
46
apiUrl: string;
57
apiKey: string;
68
projectId: number;
79
jwtPublicKey: string; // RS256 public key for JWT verification
10+
mode: AgentMode;
11+
taskId: string;
12+
runId: string;
813
}

packages/agent/src/test/fixtures/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export function createAgentServerConfig(
2525
apiKey: "test-api-key",
2626
projectId: 1,
2727
jwtPublicKey: TEST_PUBLIC_KEY,
28+
mode: "interactive",
29+
taskId: "test-task-id",
30+
runId: "test-run-id",
2831
...overrides,
2932
};
3033
}

0 commit comments

Comments
 (0)