@posthog/agent 2.1.118 → 2.1.120
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 +109 -0
- package/src/server/agent-server.ts +261 -12
- package/src/server/question-relay.test.ts +343 -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/dist/server/bin.cjs
CHANGED
|
@@ -904,7 +904,7 @@ var import_hono = require("hono");
|
|
|
904
904
|
// package.json
|
|
905
905
|
var package_default = {
|
|
906
906
|
name: "@posthog/agent",
|
|
907
|
-
version: "2.1.
|
|
907
|
+
version: "2.1.120",
|
|
908
908
|
repository: "https://github.com/PostHog/twig",
|
|
909
909
|
description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
|
|
910
910
|
exports: {
|
|
@@ -1520,6 +1520,10 @@ ${chunk.resource.text}
|
|
|
1520
1520
|
function promptToClaude(prompt) {
|
|
1521
1521
|
const content = [];
|
|
1522
1522
|
const context = [];
|
|
1523
|
+
const prContext = prompt._meta?.prContext;
|
|
1524
|
+
if (typeof prContext === "string") {
|
|
1525
|
+
content.push(sdkText(prContext));
|
|
1526
|
+
}
|
|
1523
1527
|
for (const chunk of prompt.prompt) {
|
|
1524
1528
|
processPromptChunk(chunk, content, context);
|
|
1525
1529
|
}
|
|
@@ -2966,9 +2970,10 @@ async function handleAskUserQuestionTool(context) {
|
|
|
2966
2970
|
}
|
|
2967
2971
|
});
|
|
2968
2972
|
if (response.outcome?.outcome !== "selected") {
|
|
2973
|
+
const customMessage = response._meta?.message;
|
|
2969
2974
|
return {
|
|
2970
2975
|
behavior: "deny",
|
|
2971
|
-
message: "User cancelled the questions",
|
|
2976
|
+
message: typeof customMessage === "string" ? customMessage : "User cancelled the questions",
|
|
2972
2977
|
interrupt: true
|
|
2973
2978
|
};
|
|
2974
2979
|
}
|
|
@@ -4338,6 +4343,16 @@ var PostHogAPIClient = class {
|
|
|
4338
4343
|
}
|
|
4339
4344
|
);
|
|
4340
4345
|
}
|
|
4346
|
+
async relayMessage(taskId, runId, text2) {
|
|
4347
|
+
const teamId = this.getTeamId();
|
|
4348
|
+
await this.apiRequest(
|
|
4349
|
+
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/relay_message/`,
|
|
4350
|
+
{
|
|
4351
|
+
method: "POST",
|
|
4352
|
+
body: JSON.stringify({ text: text2 })
|
|
4353
|
+
}
|
|
4354
|
+
);
|
|
4355
|
+
}
|
|
4341
4356
|
async uploadTaskArtifacts(taskId, runId, artifacts) {
|
|
4342
4357
|
if (!artifacts.length) {
|
|
4343
4358
|
return [];
|
|
@@ -4523,6 +4538,10 @@ var SessionLogWriter = class _SessionLogWriter {
|
|
|
4523
4538
|
return;
|
|
4524
4539
|
}
|
|
4525
4540
|
this.emitCoalescedMessage(sessionId, session);
|
|
4541
|
+
const nonChunkAgentText = this.extractAgentMessageText(message);
|
|
4542
|
+
if (nonChunkAgentText) {
|
|
4543
|
+
session.lastAgentMessage = nonChunkAgentText;
|
|
4544
|
+
}
|
|
4526
4545
|
const entry = {
|
|
4527
4546
|
type: "notification",
|
|
4528
4547
|
timestamp,
|
|
@@ -4622,6 +4641,7 @@ var SessionLogWriter = class _SessionLogWriter {
|
|
|
4622
4641
|
if (!session.chunkBuffer) return;
|
|
4623
4642
|
const { text: text2, firstTimestamp } = session.chunkBuffer;
|
|
4624
4643
|
session.chunkBuffer = void 0;
|
|
4644
|
+
session.lastAgentMessage = text2;
|
|
4625
4645
|
const entry = {
|
|
4626
4646
|
type: "notification",
|
|
4627
4647
|
timestamp: firstTimestamp,
|
|
@@ -4644,6 +4664,29 @@ var SessionLogWriter = class _SessionLogWriter {
|
|
|
4644
4664
|
this.scheduleFlush(sessionId);
|
|
4645
4665
|
}
|
|
4646
4666
|
}
|
|
4667
|
+
getLastAgentMessage(sessionId) {
|
|
4668
|
+
return this.sessions.get(sessionId)?.lastAgentMessage;
|
|
4669
|
+
}
|
|
4670
|
+
extractAgentMessageText(message) {
|
|
4671
|
+
if (message.method !== "session/update") {
|
|
4672
|
+
return null;
|
|
4673
|
+
}
|
|
4674
|
+
const params = message.params;
|
|
4675
|
+
const update = params?.update;
|
|
4676
|
+
if (update?.sessionUpdate !== "agent_message") {
|
|
4677
|
+
return null;
|
|
4678
|
+
}
|
|
4679
|
+
const content = update.content;
|
|
4680
|
+
if (content?.type === "text" && typeof content.text === "string") {
|
|
4681
|
+
const trimmed2 = content.text.trim();
|
|
4682
|
+
return trimmed2.length > 0 ? trimmed2 : null;
|
|
4683
|
+
}
|
|
4684
|
+
if (typeof update.message === "string") {
|
|
4685
|
+
const trimmed2 = update.message.trim();
|
|
4686
|
+
return trimmed2.length > 0 ? trimmed2 : null;
|
|
4687
|
+
}
|
|
4688
|
+
return null;
|
|
4689
|
+
}
|
|
4647
4690
|
scheduleFlush(sessionId) {
|
|
4648
4691
|
const existing = this.flushTimeouts.get(sessionId);
|
|
4649
4692
|
if (existing) clearTimeout(existing);
|
|
@@ -10316,6 +10359,8 @@ var AgentServer = class {
|
|
|
10316
10359
|
session = null;
|
|
10317
10360
|
app;
|
|
10318
10361
|
posthogAPI;
|
|
10362
|
+
questionRelayedToSlack = false;
|
|
10363
|
+
detectedPrUrl = null;
|
|
10319
10364
|
constructor(config) {
|
|
10320
10365
|
this.config = config;
|
|
10321
10366
|
this.logger = new Logger({ debug: true, prefix: "[AgentServer]" });
|
|
@@ -10528,11 +10573,21 @@ var AgentServer = class {
|
|
|
10528
10573
|
case "user_message": {
|
|
10529
10574
|
const content = params.content;
|
|
10530
10575
|
this.logger.info(
|
|
10531
|
-
`Processing user message: ${content.substring(0, 100)}...`
|
|
10576
|
+
`Processing user message (detectedPrUrl=${this.detectedPrUrl ?? "none"}): ${content.substring(0, 100)}...`
|
|
10532
10577
|
);
|
|
10533
10578
|
const result = await this.session.clientConnection.prompt({
|
|
10534
10579
|
sessionId: this.session.acpSessionId,
|
|
10535
|
-
prompt: [{ type: "text", text: content }]
|
|
10580
|
+
prompt: [{ type: "text", text: content }],
|
|
10581
|
+
...this.detectedPrUrl && {
|
|
10582
|
+
_meta: {
|
|
10583
|
+
prContext: `IMPORTANT \u2014 OVERRIDE PREVIOUS INSTRUCTIONS ABOUT CREATING BRANCHES/PRs.
|
|
10584
|
+
You already have an open pull request: ${this.detectedPrUrl}
|
|
10585
|
+
You MUST:
|
|
10586
|
+
1. Check out the existing PR branch with \`gh pr checkout ${this.detectedPrUrl}\`
|
|
10587
|
+
2. Make changes, commit, and push to that branch
|
|
10588
|
+
You MUST NOT create a new branch, close the existing PR, or create a new PR.`
|
|
10589
|
+
}
|
|
10590
|
+
}
|
|
10536
10591
|
});
|
|
10537
10592
|
return { stopReason: result.stopReason };
|
|
10538
10593
|
}
|
|
@@ -10617,13 +10672,29 @@ var AgentServer = class {
|
|
|
10617
10672
|
protocolVersion: import_sdk4.PROTOCOL_VERSION,
|
|
10618
10673
|
clientCapabilities: {}
|
|
10619
10674
|
});
|
|
10675
|
+
let preTaskRun = null;
|
|
10676
|
+
try {
|
|
10677
|
+
preTaskRun = await this.posthogAPI.getTaskRun(
|
|
10678
|
+
payload.task_id,
|
|
10679
|
+
payload.run_id
|
|
10680
|
+
);
|
|
10681
|
+
} catch {
|
|
10682
|
+
this.logger.warn("Failed to fetch task run for session context", {
|
|
10683
|
+
taskId: payload.task_id,
|
|
10684
|
+
runId: payload.run_id
|
|
10685
|
+
});
|
|
10686
|
+
}
|
|
10687
|
+
const prUrl = typeof preTaskRun?.state?.slack_notified_pr_url === "string" ? (preTaskRun?.state).slack_notified_pr_url : null;
|
|
10688
|
+
if (prUrl) {
|
|
10689
|
+
this.detectedPrUrl = prUrl;
|
|
10690
|
+
}
|
|
10620
10691
|
const sessionResponse = await clientConnection.newSession({
|
|
10621
10692
|
cwd: this.config.repositoryPath,
|
|
10622
10693
|
mcpServers: [],
|
|
10623
10694
|
_meta: {
|
|
10624
10695
|
sessionId: payload.run_id,
|
|
10625
10696
|
taskRunId: payload.run_id,
|
|
10626
|
-
systemPrompt: { append: this.buildCloudSystemPrompt() }
|
|
10697
|
+
systemPrompt: { append: this.buildCloudSystemPrompt(prUrl) }
|
|
10627
10698
|
}
|
|
10628
10699
|
});
|
|
10629
10700
|
const acpSessionId = sessionResponse.sessionId;
|
|
@@ -10647,28 +10718,51 @@ var AgentServer = class {
|
|
|
10647
10718
|
}).catch(
|
|
10648
10719
|
(err) => this.logger.warn("Failed to set task run to in_progress", err)
|
|
10649
10720
|
);
|
|
10650
|
-
await this.sendInitialTaskMessage(payload);
|
|
10721
|
+
await this.sendInitialTaskMessage(payload, preTaskRun);
|
|
10651
10722
|
}
|
|
10652
|
-
async sendInitialTaskMessage(payload) {
|
|
10723
|
+
async sendInitialTaskMessage(payload, prefetchedRun) {
|
|
10653
10724
|
if (!this.session) return;
|
|
10654
10725
|
try {
|
|
10655
|
-
this.logger.info("Fetching task details", { taskId: payload.task_id });
|
|
10656
10726
|
const task = await this.posthogAPI.getTask(payload.task_id);
|
|
10657
|
-
|
|
10727
|
+
let taskRun = prefetchedRun ?? null;
|
|
10728
|
+
if (!taskRun) {
|
|
10729
|
+
try {
|
|
10730
|
+
taskRun = await this.posthogAPI.getTaskRun(
|
|
10731
|
+
payload.task_id,
|
|
10732
|
+
payload.run_id
|
|
10733
|
+
);
|
|
10734
|
+
} catch (error) {
|
|
10735
|
+
this.logger.warn(
|
|
10736
|
+
"Failed to fetch task run for initial prompt override",
|
|
10737
|
+
{
|
|
10738
|
+
taskId: payload.task_id,
|
|
10739
|
+
runId: payload.run_id,
|
|
10740
|
+
error
|
|
10741
|
+
}
|
|
10742
|
+
);
|
|
10743
|
+
}
|
|
10744
|
+
}
|
|
10745
|
+
const initialPromptOverride = taskRun ? this.getInitialPromptOverride(taskRun) : null;
|
|
10746
|
+
const initialPrompt = initialPromptOverride ?? task.description;
|
|
10747
|
+
if (!initialPrompt) {
|
|
10658
10748
|
this.logger.warn("Task has no description, skipping initial message");
|
|
10659
10749
|
return;
|
|
10660
10750
|
}
|
|
10661
10751
|
this.logger.info("Sending initial task message", {
|
|
10662
10752
|
taskId: payload.task_id,
|
|
10663
|
-
descriptionLength:
|
|
10753
|
+
descriptionLength: initialPrompt.length,
|
|
10754
|
+
usedInitialPromptOverride: !!initialPromptOverride
|
|
10664
10755
|
});
|
|
10665
10756
|
const result = await this.session.clientConnection.prompt({
|
|
10666
10757
|
sessionId: this.session.acpSessionId,
|
|
10667
|
-
prompt: [{ type: "text", text:
|
|
10758
|
+
prompt: [{ type: "text", text: initialPrompt }]
|
|
10668
10759
|
});
|
|
10669
10760
|
this.logger.info("Initial task message completed", {
|
|
10670
10761
|
stopReason: result.stopReason
|
|
10671
10762
|
});
|
|
10763
|
+
if (result.stopReason === "end_turn") {
|
|
10764
|
+
await this.relayAgentResponse(payload);
|
|
10765
|
+
}
|
|
10672
10766
|
} catch (error) {
|
|
10673
10767
|
this.logger.error("Failed to send initial task message", error);
|
|
10674
10768
|
if (this.session) {
|
|
@@ -10677,7 +10771,33 @@ var AgentServer = class {
|
|
|
10677
10771
|
await this.signalTaskComplete(payload, "error");
|
|
10678
10772
|
}
|
|
10679
10773
|
}
|
|
10680
|
-
|
|
10774
|
+
getInitialPromptOverride(taskRun) {
|
|
10775
|
+
const state = taskRun.state;
|
|
10776
|
+
const override = state?.initial_prompt_override;
|
|
10777
|
+
if (typeof override !== "string") {
|
|
10778
|
+
return null;
|
|
10779
|
+
}
|
|
10780
|
+
const trimmed2 = override.trim();
|
|
10781
|
+
return trimmed2.length > 0 ? trimmed2 : null;
|
|
10782
|
+
}
|
|
10783
|
+
buildCloudSystemPrompt(prUrl) {
|
|
10784
|
+
if (prUrl) {
|
|
10785
|
+
return `
|
|
10786
|
+
# Cloud Task Execution
|
|
10787
|
+
|
|
10788
|
+
This task already has an open pull request: ${prUrl}
|
|
10789
|
+
|
|
10790
|
+
After completing the requested changes:
|
|
10791
|
+
1. Check out the existing PR branch with \`gh pr checkout ${prUrl}\`
|
|
10792
|
+
2. Stage and commit all changes with a clear commit message
|
|
10793
|
+
3. Push to the existing PR branch
|
|
10794
|
+
|
|
10795
|
+
Important:
|
|
10796
|
+
- Do NOT create a new branch or a new pull request.
|
|
10797
|
+
- Do NOT add "Co-Authored-By" trailers to commit messages.
|
|
10798
|
+
- Do NOT add "Generated with [Claude Code]" or similar attribution lines to PR descriptions.
|
|
10799
|
+
`;
|
|
10800
|
+
}
|
|
10681
10801
|
return `
|
|
10682
10802
|
# Cloud Task Execution
|
|
10683
10803
|
|
|
@@ -10747,19 +10867,34 @@ Important:
|
|
|
10747
10867
|
}
|
|
10748
10868
|
createCloudClient(payload) {
|
|
10749
10869
|
const mode = this.getEffectiveMode(payload);
|
|
10870
|
+
const interactionOrigin = process.env.TWIG_INTERACTION_ORIGIN;
|
|
10750
10871
|
return {
|
|
10751
10872
|
requestPermission: async (params) => {
|
|
10752
10873
|
this.logger.debug("Permission request", {
|
|
10753
10874
|
mode,
|
|
10875
|
+
interactionOrigin,
|
|
10754
10876
|
options: params.options
|
|
10755
10877
|
});
|
|
10756
10878
|
const allowOption = params.options.find(
|
|
10757
10879
|
(o) => o.kind === "allow_once" || o.kind === "allow_always"
|
|
10758
10880
|
);
|
|
10881
|
+
const selectedOptionId = allowOption?.optionId ?? params.options[0].optionId;
|
|
10882
|
+
if (interactionOrigin === "slack") {
|
|
10883
|
+
const twigToolKind = params.toolCall?._meta?.twigToolKind;
|
|
10884
|
+
if (twigToolKind === "question") {
|
|
10885
|
+
this.relaySlackQuestion(payload, params.toolCall?._meta);
|
|
10886
|
+
return {
|
|
10887
|
+
outcome: { outcome: "cancelled" },
|
|
10888
|
+
_meta: {
|
|
10889
|
+
message: "This question has been relayed to the Slack thread where this task originated. The user will reply there. Do NOT re-ask the question or pick an answer yourself. Simply let the user know you are waiting for their reply."
|
|
10890
|
+
}
|
|
10891
|
+
};
|
|
10892
|
+
}
|
|
10893
|
+
}
|
|
10759
10894
|
return {
|
|
10760
10895
|
outcome: {
|
|
10761
10896
|
outcome: "selected",
|
|
10762
|
-
optionId:
|
|
10897
|
+
optionId: selectedOptionId
|
|
10763
10898
|
}
|
|
10764
10899
|
};
|
|
10765
10900
|
},
|
|
@@ -10778,6 +10913,97 @@ Important:
|
|
|
10778
10913
|
}
|
|
10779
10914
|
};
|
|
10780
10915
|
}
|
|
10916
|
+
async relayAgentResponse(payload) {
|
|
10917
|
+
if (!this.session) {
|
|
10918
|
+
return;
|
|
10919
|
+
}
|
|
10920
|
+
if (this.questionRelayedToSlack) {
|
|
10921
|
+
this.questionRelayedToSlack = false;
|
|
10922
|
+
return;
|
|
10923
|
+
}
|
|
10924
|
+
try {
|
|
10925
|
+
await this.session.logWriter.flush(payload.run_id);
|
|
10926
|
+
} catch (error) {
|
|
10927
|
+
this.logger.warn("Failed to flush logs before Slack relay", {
|
|
10928
|
+
taskId: payload.task_id,
|
|
10929
|
+
runId: payload.run_id,
|
|
10930
|
+
error
|
|
10931
|
+
});
|
|
10932
|
+
}
|
|
10933
|
+
const message = this.session.logWriter.getLastAgentMessage(payload.run_id);
|
|
10934
|
+
if (!message) {
|
|
10935
|
+
this.logger.warn("No agent message found for Slack relay", {
|
|
10936
|
+
taskId: payload.task_id,
|
|
10937
|
+
runId: payload.run_id,
|
|
10938
|
+
sessionRegistered: this.session.logWriter.isRegistered(payload.run_id)
|
|
10939
|
+
});
|
|
10940
|
+
return;
|
|
10941
|
+
}
|
|
10942
|
+
try {
|
|
10943
|
+
await this.posthogAPI.relayMessage(
|
|
10944
|
+
payload.task_id,
|
|
10945
|
+
payload.run_id,
|
|
10946
|
+
message
|
|
10947
|
+
);
|
|
10948
|
+
} catch (error) {
|
|
10949
|
+
this.logger.warn("Failed to relay initial agent response to Slack", {
|
|
10950
|
+
taskId: payload.task_id,
|
|
10951
|
+
runId: payload.run_id,
|
|
10952
|
+
error
|
|
10953
|
+
});
|
|
10954
|
+
}
|
|
10955
|
+
}
|
|
10956
|
+
relaySlackQuestion(payload, toolMeta2) {
|
|
10957
|
+
const firstQuestion = this.getFirstQuestionMeta(toolMeta2);
|
|
10958
|
+
if (!this.isQuestionMeta(firstQuestion)) {
|
|
10959
|
+
return;
|
|
10960
|
+
}
|
|
10961
|
+
let message = `*${firstQuestion.question}*
|
|
10962
|
+
|
|
10963
|
+
`;
|
|
10964
|
+
if (firstQuestion.options?.length) {
|
|
10965
|
+
firstQuestion.options.forEach(
|
|
10966
|
+
(opt, i) => {
|
|
10967
|
+
message += `${i + 1}. *${opt.label}*`;
|
|
10968
|
+
if (opt.description) message += ` \u2014 ${opt.description}`;
|
|
10969
|
+
message += "\n";
|
|
10970
|
+
}
|
|
10971
|
+
);
|
|
10972
|
+
}
|
|
10973
|
+
message += "\nReply in this thread with your choice.";
|
|
10974
|
+
this.questionRelayedToSlack = true;
|
|
10975
|
+
this.posthogAPI.relayMessage(payload.task_id, payload.run_id, message).catch(
|
|
10976
|
+
(err) => this.logger.warn("Failed to relay question to Slack", { err })
|
|
10977
|
+
);
|
|
10978
|
+
}
|
|
10979
|
+
getFirstQuestionMeta(toolMeta2) {
|
|
10980
|
+
if (!toolMeta2) {
|
|
10981
|
+
return null;
|
|
10982
|
+
}
|
|
10983
|
+
const questionsValue = toolMeta2.questions;
|
|
10984
|
+
if (!Array.isArray(questionsValue) || questionsValue.length === 0) {
|
|
10985
|
+
return null;
|
|
10986
|
+
}
|
|
10987
|
+
return questionsValue[0];
|
|
10988
|
+
}
|
|
10989
|
+
isQuestionMeta(value) {
|
|
10990
|
+
if (!value || typeof value !== "object") {
|
|
10991
|
+
return false;
|
|
10992
|
+
}
|
|
10993
|
+
const candidate = value;
|
|
10994
|
+
if (typeof candidate.question !== "string") {
|
|
10995
|
+
return false;
|
|
10996
|
+
}
|
|
10997
|
+
if (candidate.options === void 0) {
|
|
10998
|
+
return true;
|
|
10999
|
+
}
|
|
11000
|
+
if (!Array.isArray(candidate.options)) {
|
|
11001
|
+
return false;
|
|
11002
|
+
}
|
|
11003
|
+
return candidate.options.every(
|
|
11004
|
+
(option) => !!option && typeof option === "object" && typeof option.label === "string"
|
|
11005
|
+
);
|
|
11006
|
+
}
|
|
10781
11007
|
detectAndAttachPrUrl(payload, update) {
|
|
10782
11008
|
try {
|
|
10783
11009
|
const meta = update?._meta?.claudeCode;
|
|
@@ -10808,6 +11034,7 @@ Important:
|
|
|
10808
11034
|
);
|
|
10809
11035
|
if (!prUrlMatch) return;
|
|
10810
11036
|
const prUrl = prUrlMatch[0];
|
|
11037
|
+
this.detectedPrUrl = prUrl;
|
|
10811
11038
|
this.logger.info("Detected PR URL in bash output", {
|
|
10812
11039
|
runId: payload.run_id,
|
|
10813
11040
|
prUrl
|