@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.
@@ -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.118",
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
- if (!task.description) {
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: task.description.length
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: task.description }]
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
- buildCloudSystemPrompt() {
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: allowOption?.optionId ?? params.options[0].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