@posthog/agent 2.1.118 → 2.1.124
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.js +45 -2
- package/dist/agent.js.map +1 -1
- package/dist/posthog-api.d.ts +1 -0
- package/dist/posthog-api.js +11 -1
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.d.ts +7 -0
- package/dist/server/agent-server.js +240 -13
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +240 -13
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/claude/conversion/acp-to-sdk.ts +6 -0
- package/src/adapters/claude/permissions/permission-handlers.ts +7 -1
- package/src/posthog-api.ts +15 -0
- package/src/server/agent-server.test.ts +126 -0
- package/src/server/agent-server.ts +261 -12
- package/src/server/question-relay.test.ts +363 -0
- package/src/session-log-writer.test.ts +19 -0
- package/src/session-log-writer.ts +40 -0
- package/src/test/mocks/msw-handlers.ts +25 -1
package/package.json
CHANGED
|
@@ -87,6 +87,12 @@ export function promptToClaude(prompt: PromptRequest): SDKUserMessage {
|
|
|
87
87
|
const content: ContentBlockParam[] = [];
|
|
88
88
|
const context: ContentBlockParam[] = [];
|
|
89
89
|
|
|
90
|
+
const prContext = (prompt._meta as Record<string, unknown> | undefined)
|
|
91
|
+
?.prContext;
|
|
92
|
+
if (typeof prContext === "string") {
|
|
93
|
+
content.push(sdkText(prContext));
|
|
94
|
+
}
|
|
95
|
+
|
|
90
96
|
for (const chunk of prompt.prompt) {
|
|
91
97
|
processPromptChunk(chunk, content, context);
|
|
92
98
|
}
|
|
@@ -276,9 +276,15 @@ async function handleAskUserQuestionTool(
|
|
|
276
276
|
});
|
|
277
277
|
|
|
278
278
|
if (response.outcome?.outcome !== "selected") {
|
|
279
|
+
const customMessage = (
|
|
280
|
+
response._meta as Record<string, unknown> | undefined
|
|
281
|
+
)?.message;
|
|
279
282
|
return {
|
|
280
283
|
behavior: "deny",
|
|
281
|
-
message:
|
|
284
|
+
message:
|
|
285
|
+
typeof customMessage === "string"
|
|
286
|
+
? customMessage
|
|
287
|
+
: "User cancelled the questions",
|
|
282
288
|
interrupt: true,
|
|
283
289
|
};
|
|
284
290
|
}
|
package/src/posthog-api.ts
CHANGED
|
@@ -137,6 +137,21 @@ export class PostHogAPIClient {
|
|
|
137
137
|
);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
async relayMessage(
|
|
141
|
+
taskId: string,
|
|
142
|
+
runId: string,
|
|
143
|
+
text: string,
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
const teamId = this.getTeamId();
|
|
146
|
+
await this.apiRequest<{ status: string }>(
|
|
147
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/relay_message/`,
|
|
148
|
+
{
|
|
149
|
+
method: "POST",
|
|
150
|
+
body: JSON.stringify({ text }),
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
140
155
|
async uploadTaskArtifacts(
|
|
141
156
|
taskId: string,
|
|
142
157
|
runId: string,
|
|
@@ -3,9 +3,17 @@ import { type SetupServerApi, setupServer } from "msw/node";
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
4
4
|
import { createTestRepo, type TestRepo } from "../test/fixtures/api.js";
|
|
5
5
|
import { createPostHogHandlers } from "../test/mocks/msw-handlers.js";
|
|
6
|
+
import type { TaskRun } from "../types.js";
|
|
6
7
|
import { AgentServer } from "./agent-server.js";
|
|
7
8
|
import { type JwtPayload, SANDBOX_CONNECTION_AUDIENCE } from "./jwt.js";
|
|
8
9
|
|
|
10
|
+
interface TestableServer {
|
|
11
|
+
getInitialPromptOverride(run: TaskRun): string | null;
|
|
12
|
+
detectAndAttachPrUrl(payload: unknown, update: unknown): void;
|
|
13
|
+
detectedPrUrl: string | null;
|
|
14
|
+
buildCloudSystemPrompt(prUrl?: string | null): string;
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
function createTestJwt(
|
|
10
18
|
payload: JwtPayload,
|
|
11
19
|
privateKey: string,
|
|
@@ -217,4 +225,122 @@ describe("AgentServer HTTP Mode", () => {
|
|
|
217
225
|
expect(body.error).toBe("Not found");
|
|
218
226
|
});
|
|
219
227
|
});
|
|
228
|
+
|
|
229
|
+
describe("getInitialPromptOverride", () => {
|
|
230
|
+
it("returns override string from run state", () => {
|
|
231
|
+
const s = createServer();
|
|
232
|
+
const run = {
|
|
233
|
+
state: { initial_prompt_override: "do something else" },
|
|
234
|
+
} as unknown as TaskRun;
|
|
235
|
+
const result = (s as unknown as TestableServer).getInitialPromptOverride(
|
|
236
|
+
run,
|
|
237
|
+
);
|
|
238
|
+
expect(result).toBe("do something else");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("returns null when override is absent", () => {
|
|
242
|
+
const s = createServer();
|
|
243
|
+
const run = { state: {} } as unknown as TaskRun;
|
|
244
|
+
const result = (s as unknown as TestableServer).getInitialPromptOverride(
|
|
245
|
+
run,
|
|
246
|
+
);
|
|
247
|
+
expect(result).toBeNull();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("returns null for whitespace-only override", () => {
|
|
251
|
+
const s = createServer();
|
|
252
|
+
const run = {
|
|
253
|
+
state: { initial_prompt_override: " " },
|
|
254
|
+
} as unknown as TaskRun;
|
|
255
|
+
const result = (s as unknown as TestableServer).getInitialPromptOverride(
|
|
256
|
+
run,
|
|
257
|
+
);
|
|
258
|
+
expect(result).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("returns null for non-string override", () => {
|
|
262
|
+
const s = createServer();
|
|
263
|
+
const run = {
|
|
264
|
+
state: { initial_prompt_override: 42 },
|
|
265
|
+
} as unknown as TaskRun;
|
|
266
|
+
const result = (s as unknown as TestableServer).getInitialPromptOverride(
|
|
267
|
+
run,
|
|
268
|
+
);
|
|
269
|
+
expect(result).toBeNull();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("detectedPrUrl tracking", () => {
|
|
274
|
+
it("stores PR URL when detectAndAttachPrUrl finds a match", () => {
|
|
275
|
+
const s = createServer();
|
|
276
|
+
const payload = {
|
|
277
|
+
task_id: "test-task-id",
|
|
278
|
+
run_id: "test-run-id",
|
|
279
|
+
};
|
|
280
|
+
const update = {
|
|
281
|
+
_meta: {
|
|
282
|
+
claudeCode: {
|
|
283
|
+
toolName: "Bash",
|
|
284
|
+
toolResponse: {
|
|
285
|
+
stdout:
|
|
286
|
+
"https://github.com/PostHog/posthog/pull/42\nCreating pull request...",
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
(s as unknown as TestableServer).detectAndAttachPrUrl(payload, update);
|
|
293
|
+
expect((s as unknown as TestableServer).detectedPrUrl).toBe(
|
|
294
|
+
"https://github.com/PostHog/posthog/pull/42",
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("does not set detectedPrUrl when no PR URL is found", () => {
|
|
299
|
+
const s = createServer();
|
|
300
|
+
const payload = {
|
|
301
|
+
task_id: "test-task-id",
|
|
302
|
+
run_id: "test-run-id",
|
|
303
|
+
};
|
|
304
|
+
const update = {
|
|
305
|
+
_meta: {
|
|
306
|
+
claudeCode: {
|
|
307
|
+
toolName: "Bash",
|
|
308
|
+
toolResponse: { stdout: "just some output" },
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
(s as unknown as TestableServer).detectAndAttachPrUrl(payload, update);
|
|
314
|
+
expect((s as unknown as TestableServer).detectedPrUrl).toBeNull();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe("buildCloudSystemPrompt", () => {
|
|
319
|
+
it("returns PR-aware prompt when prUrl is provided", () => {
|
|
320
|
+
const s = createServer();
|
|
321
|
+
const prompt = (s as unknown as TestableServer).buildCloudSystemPrompt(
|
|
322
|
+
"https://github.com/org/repo/pull/1",
|
|
323
|
+
);
|
|
324
|
+
expect(prompt).toContain("Do NOT create a new branch");
|
|
325
|
+
expect(prompt).toContain("https://github.com/org/repo/pull/1");
|
|
326
|
+
expect(prompt).toContain("gh pr checkout");
|
|
327
|
+
expect(prompt).not.toContain("Create a pull request");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("returns default prompt when no prUrl", () => {
|
|
331
|
+
const s = createServer();
|
|
332
|
+
const prompt = (s as unknown as TestableServer).buildCloudSystemPrompt();
|
|
333
|
+
expect(prompt).toContain("Create a new branch");
|
|
334
|
+
expect(prompt).toContain("Create a pull request");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("returns default prompt when prUrl is null", () => {
|
|
338
|
+
const s = createServer();
|
|
339
|
+
const prompt = (s as unknown as TestableServer).buildCloudSystemPrompt(
|
|
340
|
+
null,
|
|
341
|
+
);
|
|
342
|
+
expect(prompt).toContain("Create a new branch");
|
|
343
|
+
expect(prompt).toContain("Create a pull request");
|
|
344
|
+
});
|
|
345
|
+
});
|
|
220
346
|
});
|
|
@@ -14,7 +14,12 @@ import {
|
|
|
14
14
|
import { PostHogAPIClient } from "../posthog-api.js";
|
|
15
15
|
import { SessionLogWriter } from "../session-log-writer.js";
|
|
16
16
|
import { TreeTracker } from "../tree-tracker.js";
|
|
17
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
AgentMode,
|
|
19
|
+
DeviceInfo,
|
|
20
|
+
TaskRun,
|
|
21
|
+
TreeSnapshotEvent,
|
|
22
|
+
} from "../types.js";
|
|
18
23
|
import { AsyncMutex } from "../utils/async-mutex.js";
|
|
19
24
|
import { getLlmGatewayUrl } from "../utils/gateway.js";
|
|
20
25
|
import { Logger } from "../utils/logger.js";
|
|
@@ -147,6 +152,8 @@ export class AgentServer {
|
|
|
147
152
|
private session: ActiveSession | null = null;
|
|
148
153
|
private app: Hono;
|
|
149
154
|
private posthogAPI: PostHogAPIClient;
|
|
155
|
+
private questionRelayedToSlack = false;
|
|
156
|
+
private detectedPrUrl: string | null = null;
|
|
150
157
|
|
|
151
158
|
constructor(config: AgentServerConfig) {
|
|
152
159
|
this.config = config;
|
|
@@ -408,12 +415,23 @@ export class AgentServer {
|
|
|
408
415
|
const content = params.content as string;
|
|
409
416
|
|
|
410
417
|
this.logger.info(
|
|
411
|
-
`Processing user message: ${content.substring(0, 100)}...`,
|
|
418
|
+
`Processing user message (detectedPrUrl=${this.detectedPrUrl ?? "none"}): ${content.substring(0, 100)}...`,
|
|
412
419
|
);
|
|
413
420
|
|
|
414
421
|
const result = await this.session.clientConnection.prompt({
|
|
415
422
|
sessionId: this.session.acpSessionId,
|
|
416
423
|
prompt: [{ type: "text", text: content }],
|
|
424
|
+
...(this.detectedPrUrl && {
|
|
425
|
+
_meta: {
|
|
426
|
+
prContext:
|
|
427
|
+
`IMPORTANT — OVERRIDE PREVIOUS INSTRUCTIONS ABOUT CREATING BRANCHES/PRs.\n` +
|
|
428
|
+
`You already have an open pull request: ${this.detectedPrUrl}\n` +
|
|
429
|
+
`You MUST:\n` +
|
|
430
|
+
`1. Check out the existing PR branch with \`gh pr checkout ${this.detectedPrUrl}\`\n` +
|
|
431
|
+
`2. Make changes, commit, and push to that branch\n` +
|
|
432
|
+
`You MUST NOT create a new branch, close the existing PR, or create a new PR.`,
|
|
433
|
+
},
|
|
434
|
+
}),
|
|
417
435
|
});
|
|
418
436
|
|
|
419
437
|
return { stopReason: result.stopReason };
|
|
@@ -521,13 +539,37 @@ export class AgentServer {
|
|
|
521
539
|
clientCapabilities: {},
|
|
522
540
|
});
|
|
523
541
|
|
|
542
|
+
let preTaskRun: TaskRun | null = null;
|
|
543
|
+
try {
|
|
544
|
+
preTaskRun = await this.posthogAPI.getTaskRun(
|
|
545
|
+
payload.task_id,
|
|
546
|
+
payload.run_id,
|
|
547
|
+
);
|
|
548
|
+
} catch {
|
|
549
|
+
this.logger.warn("Failed to fetch task run for session context", {
|
|
550
|
+
taskId: payload.task_id,
|
|
551
|
+
runId: payload.run_id,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const prUrl =
|
|
556
|
+
typeof (preTaskRun?.state as Record<string, unknown>)
|
|
557
|
+
?.slack_notified_pr_url === "string"
|
|
558
|
+
? ((preTaskRun?.state as Record<string, unknown>)
|
|
559
|
+
.slack_notified_pr_url as string)
|
|
560
|
+
: null;
|
|
561
|
+
|
|
562
|
+
if (prUrl) {
|
|
563
|
+
this.detectedPrUrl = prUrl;
|
|
564
|
+
}
|
|
565
|
+
|
|
524
566
|
const sessionResponse = await clientConnection.newSession({
|
|
525
567
|
cwd: this.config.repositoryPath,
|
|
526
568
|
mcpServers: [],
|
|
527
569
|
_meta: {
|
|
528
570
|
sessionId: payload.run_id,
|
|
529
571
|
taskRunId: payload.run_id,
|
|
530
|
-
systemPrompt: { append: this.buildCloudSystemPrompt() },
|
|
572
|
+
systemPrompt: { append: this.buildCloudSystemPrompt(prUrl) },
|
|
531
573
|
},
|
|
532
574
|
});
|
|
533
575
|
|
|
@@ -559,34 +601,65 @@ export class AgentServer {
|
|
|
559
601
|
this.logger.warn("Failed to set task run to in_progress", err),
|
|
560
602
|
);
|
|
561
603
|
|
|
562
|
-
await this.sendInitialTaskMessage(payload);
|
|
604
|
+
await this.sendInitialTaskMessage(payload, preTaskRun);
|
|
563
605
|
}
|
|
564
606
|
|
|
565
|
-
private async sendInitialTaskMessage(
|
|
607
|
+
private async sendInitialTaskMessage(
|
|
608
|
+
payload: JwtPayload,
|
|
609
|
+
prefetchedRun?: TaskRun | null,
|
|
610
|
+
): Promise<void> {
|
|
566
611
|
if (!this.session) return;
|
|
567
612
|
|
|
568
613
|
try {
|
|
569
|
-
this.logger.info("Fetching task details", { taskId: payload.task_id });
|
|
570
614
|
const task = await this.posthogAPI.getTask(payload.task_id);
|
|
571
615
|
|
|
572
|
-
|
|
616
|
+
let taskRun = prefetchedRun ?? null;
|
|
617
|
+
if (!taskRun) {
|
|
618
|
+
try {
|
|
619
|
+
taskRun = await this.posthogAPI.getTaskRun(
|
|
620
|
+
payload.task_id,
|
|
621
|
+
payload.run_id,
|
|
622
|
+
);
|
|
623
|
+
} catch (error) {
|
|
624
|
+
this.logger.warn(
|
|
625
|
+
"Failed to fetch task run for initial prompt override",
|
|
626
|
+
{
|
|
627
|
+
taskId: payload.task_id,
|
|
628
|
+
runId: payload.run_id,
|
|
629
|
+
error,
|
|
630
|
+
},
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const initialPromptOverride = taskRun
|
|
636
|
+
? this.getInitialPromptOverride(taskRun)
|
|
637
|
+
: null;
|
|
638
|
+
const initialPrompt = initialPromptOverride ?? task.description;
|
|
639
|
+
|
|
640
|
+
if (!initialPrompt) {
|
|
573
641
|
this.logger.warn("Task has no description, skipping initial message");
|
|
574
642
|
return;
|
|
575
643
|
}
|
|
576
644
|
|
|
577
645
|
this.logger.info("Sending initial task message", {
|
|
578
646
|
taskId: payload.task_id,
|
|
579
|
-
descriptionLength:
|
|
647
|
+
descriptionLength: initialPrompt.length,
|
|
648
|
+
usedInitialPromptOverride: !!initialPromptOverride,
|
|
580
649
|
});
|
|
581
650
|
|
|
582
651
|
const result = await this.session.clientConnection.prompt({
|
|
583
652
|
sessionId: this.session.acpSessionId,
|
|
584
|
-
prompt: [{ type: "text", text:
|
|
653
|
+
prompt: [{ type: "text", text: initialPrompt }],
|
|
585
654
|
});
|
|
586
655
|
|
|
587
656
|
this.logger.info("Initial task message completed", {
|
|
588
657
|
stopReason: result.stopReason,
|
|
589
658
|
});
|
|
659
|
+
|
|
660
|
+
if (result.stopReason === "end_turn") {
|
|
661
|
+
await this.relayAgentResponse(payload);
|
|
662
|
+
}
|
|
590
663
|
} catch (error) {
|
|
591
664
|
this.logger.error("Failed to send initial task message", error);
|
|
592
665
|
if (this.session) {
|
|
@@ -596,7 +669,36 @@ export class AgentServer {
|
|
|
596
669
|
}
|
|
597
670
|
}
|
|
598
671
|
|
|
599
|
-
private
|
|
672
|
+
private getInitialPromptOverride(taskRun: TaskRun): string | null {
|
|
673
|
+
const state = taskRun.state as Record<string, unknown> | undefined;
|
|
674
|
+
const override = state?.initial_prompt_override;
|
|
675
|
+
if (typeof override !== "string") {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const trimmed = override.trim();
|
|
680
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private buildCloudSystemPrompt(prUrl?: string | null): string {
|
|
684
|
+
if (prUrl) {
|
|
685
|
+
return `
|
|
686
|
+
# Cloud Task Execution
|
|
687
|
+
|
|
688
|
+
This task already has an open pull request: ${prUrl}
|
|
689
|
+
|
|
690
|
+
After completing the requested changes:
|
|
691
|
+
1. Check out the existing PR branch with \`gh pr checkout ${prUrl}\`
|
|
692
|
+
2. Stage and commit all changes with a clear commit message
|
|
693
|
+
3. Push to the existing PR branch
|
|
694
|
+
|
|
695
|
+
Important:
|
|
696
|
+
- Do NOT create a new branch or a new pull request.
|
|
697
|
+
- Do NOT add "Co-Authored-By" trailers to commit messages.
|
|
698
|
+
- Do NOT add "Generated with [Claude Code]" or similar attribution lines to PR descriptions.
|
|
699
|
+
`;
|
|
700
|
+
}
|
|
701
|
+
|
|
600
702
|
return `
|
|
601
703
|
# Cloud Task Execution
|
|
602
704
|
|
|
@@ -680,26 +782,50 @@ Important:
|
|
|
680
782
|
|
|
681
783
|
private createCloudClient(payload: JwtPayload) {
|
|
682
784
|
const mode = this.getEffectiveMode(payload);
|
|
785
|
+
const interactionOrigin = process.env.TWIG_INTERACTION_ORIGIN;
|
|
683
786
|
|
|
684
787
|
return {
|
|
685
788
|
requestPermission: async (params: {
|
|
686
|
-
options: Array<{ kind: string; optionId: string }>;
|
|
789
|
+
options: Array<{ kind: string; optionId: string; name?: string }>;
|
|
790
|
+
toolCall?: {
|
|
791
|
+
_meta?: Record<string, unknown> | null;
|
|
792
|
+
};
|
|
687
793
|
}) => {
|
|
688
794
|
// Background mode: always auto-approve permissions
|
|
689
795
|
// Interactive mode: also auto-approve for now (user can monitor via SSE)
|
|
690
796
|
// Future: interactive mode could pause and wait for user approval via SSE
|
|
691
797
|
this.logger.debug("Permission request", {
|
|
692
798
|
mode,
|
|
799
|
+
interactionOrigin,
|
|
693
800
|
options: params.options,
|
|
694
801
|
});
|
|
695
802
|
|
|
696
803
|
const allowOption = params.options.find(
|
|
697
804
|
(o) => o.kind === "allow_once" || o.kind === "allow_always",
|
|
698
805
|
);
|
|
806
|
+
const selectedOptionId =
|
|
807
|
+
allowOption?.optionId ?? params.options[0].optionId;
|
|
808
|
+
|
|
809
|
+
if (interactionOrigin === "slack") {
|
|
810
|
+
const twigToolKind = params.toolCall?._meta?.twigToolKind;
|
|
811
|
+
if (twigToolKind === "question") {
|
|
812
|
+
this.relaySlackQuestion(payload, params.toolCall?._meta);
|
|
813
|
+
return {
|
|
814
|
+
outcome: { outcome: "cancelled" as const },
|
|
815
|
+
_meta: {
|
|
816
|
+
message:
|
|
817
|
+
"This question has been relayed to the Slack thread where this task originated. " +
|
|
818
|
+
"The user will reply there. Do NOT re-ask the question or pick an answer yourself. " +
|
|
819
|
+
"Simply let the user know you are waiting for their reply.",
|
|
820
|
+
},
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
699
825
|
return {
|
|
700
826
|
outcome: {
|
|
701
827
|
outcome: "selected" as const,
|
|
702
|
-
optionId:
|
|
828
|
+
optionId: selectedOptionId,
|
|
703
829
|
},
|
|
704
830
|
};
|
|
705
831
|
},
|
|
@@ -735,6 +861,128 @@ Important:
|
|
|
735
861
|
};
|
|
736
862
|
}
|
|
737
863
|
|
|
864
|
+
private async relayAgentResponse(payload: JwtPayload): Promise<void> {
|
|
865
|
+
if (!this.session) {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (this.questionRelayedToSlack) {
|
|
870
|
+
this.questionRelayedToSlack = false;
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
try {
|
|
875
|
+
await this.session.logWriter.flush(payload.run_id);
|
|
876
|
+
} catch (error) {
|
|
877
|
+
this.logger.warn("Failed to flush logs before Slack relay", {
|
|
878
|
+
taskId: payload.task_id,
|
|
879
|
+
runId: payload.run_id,
|
|
880
|
+
error,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const message = this.session.logWriter.getLastAgentMessage(payload.run_id);
|
|
885
|
+
if (!message) {
|
|
886
|
+
this.logger.warn("No agent message found for Slack relay", {
|
|
887
|
+
taskId: payload.task_id,
|
|
888
|
+
runId: payload.run_id,
|
|
889
|
+
sessionRegistered: this.session.logWriter.isRegistered(payload.run_id),
|
|
890
|
+
});
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
try {
|
|
895
|
+
await this.posthogAPI.relayMessage(
|
|
896
|
+
payload.task_id,
|
|
897
|
+
payload.run_id,
|
|
898
|
+
message,
|
|
899
|
+
);
|
|
900
|
+
} catch (error) {
|
|
901
|
+
this.logger.warn("Failed to relay initial agent response to Slack", {
|
|
902
|
+
taskId: payload.task_id,
|
|
903
|
+
runId: payload.run_id,
|
|
904
|
+
error,
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
private relaySlackQuestion(
|
|
910
|
+
payload: JwtPayload,
|
|
911
|
+
toolMeta: Record<string, unknown> | null | undefined,
|
|
912
|
+
): void {
|
|
913
|
+
const firstQuestion = this.getFirstQuestionMeta(toolMeta);
|
|
914
|
+
if (!this.isQuestionMeta(firstQuestion)) {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
let message = `*${firstQuestion.question}*\n\n`;
|
|
919
|
+
if (firstQuestion.options?.length) {
|
|
920
|
+
firstQuestion.options.forEach(
|
|
921
|
+
(opt: { label: string; description?: string }, i: number) => {
|
|
922
|
+
message += `${i + 1}. *${opt.label}*`;
|
|
923
|
+
if (opt.description) message += ` — ${opt.description}`;
|
|
924
|
+
message += "\n";
|
|
925
|
+
},
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
message += "\nReply in this thread with your choice.";
|
|
929
|
+
|
|
930
|
+
this.questionRelayedToSlack = true;
|
|
931
|
+
this.posthogAPI
|
|
932
|
+
.relayMessage(payload.task_id, payload.run_id, message)
|
|
933
|
+
.catch((err) =>
|
|
934
|
+
this.logger.warn("Failed to relay question to Slack", { err }),
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
private getFirstQuestionMeta(
|
|
939
|
+
toolMeta: Record<string, unknown> | null | undefined,
|
|
940
|
+
): unknown {
|
|
941
|
+
if (!toolMeta) {
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const questionsValue = toolMeta.questions;
|
|
946
|
+
if (!Array.isArray(questionsValue) || questionsValue.length === 0) {
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return questionsValue[0];
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
private isQuestionMeta(value: unknown): value is {
|
|
954
|
+
question: string;
|
|
955
|
+
options?: Array<{ label: string; description?: string }>;
|
|
956
|
+
} {
|
|
957
|
+
if (!value || typeof value !== "object") {
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const candidate = value as {
|
|
962
|
+
question?: unknown;
|
|
963
|
+
options?: unknown;
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
if (typeof candidate.question !== "string") {
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (candidate.options === undefined) {
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (!Array.isArray(candidate.options)) {
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return candidate.options.every(
|
|
979
|
+
(option) =>
|
|
980
|
+
!!option &&
|
|
981
|
+
typeof option === "object" &&
|
|
982
|
+
typeof (option as { label?: unknown }).label === "string",
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
|
|
738
986
|
private detectAndAttachPrUrl(
|
|
739
987
|
payload: JwtPayload,
|
|
740
988
|
update: Record<string, unknown>,
|
|
@@ -780,6 +1028,7 @@ Important:
|
|
|
780
1028
|
if (!prUrlMatch) return;
|
|
781
1029
|
|
|
782
1030
|
const prUrl = prUrlMatch[0];
|
|
1031
|
+
this.detectedPrUrl = prUrl;
|
|
783
1032
|
this.logger.info("Detected PR URL in bash output", {
|
|
784
1033
|
runId: payload.run_id,
|
|
785
1034
|
prUrl,
|