@posthog/agent 2.3.261 → 2.3.267

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.
@@ -18,6 +18,8 @@ import {
18
18
  type InProcessAcpConnection,
19
19
  } from "../adapters/acp-connection";
20
20
  import { selectRecentTurns } from "../adapters/claude/session/jsonl-hydration";
21
+ import type { CodeExecutionMode } from "../execution-mode";
22
+ import { DEFAULT_CODEX_MODEL } from "../gateway-models";
21
23
  import { PostHogAPIClient } from "../posthog-api";
22
24
  import {
23
25
  type ConversationTurn,
@@ -161,6 +163,24 @@ interface ActiveSession {
161
163
  sseController: SseController | null;
162
164
  deviceInfo: DeviceInfo;
163
165
  logWriter: SessionLogWriter;
166
+ /** Current permission mode, tracked for relay decisions */
167
+ permissionMode: CodeExecutionMode;
168
+ /** Whether a desktop client has ever connected via SSE during this session */
169
+ hasDesktopConnected: boolean;
170
+ }
171
+
172
+ function getTaskRunStateString(
173
+ taskRun: TaskRun | null,
174
+ key: string,
175
+ ): string | null {
176
+ const state = taskRun?.state;
177
+
178
+ if (!state || typeof state !== "object") {
179
+ return null;
180
+ }
181
+
182
+ const value = (state as Record<string, unknown>)[key];
183
+ return typeof value === "string" ? value : null;
164
184
  }
165
185
 
166
186
  export class AgentServer {
@@ -180,6 +200,15 @@ export class AgentServer {
180
200
  // causing a second session to be created and duplicate Slack messages to be sent.
181
201
  private initializationPromise: Promise<void> | null = null;
182
202
  private pendingEvents: Record<string, unknown>[] = [];
203
+ private pendingPermissions = new Map<
204
+ string,
205
+ {
206
+ resolve: (response: {
207
+ outcome: { outcome: "selected"; optionId: string };
208
+ _meta?: Record<string, unknown>;
209
+ }) => void;
210
+ }
211
+ >();
183
212
 
184
213
  private detachSseController(controller: SseController): void {
185
214
  if (this.session?.sseController === controller) {
@@ -228,10 +257,18 @@ export class AgentServer {
228
257
  this.app = this.createApp();
229
258
  }
230
259
 
260
+ private getRuntimeAdapter(): "claude" | "codex" {
261
+ return this.config.runtimeAdapter ?? "claude";
262
+ }
263
+
231
264
  private getEffectiveMode(payload: JwtPayload): AgentMode {
232
265
  return payload.mode ?? this.config.mode;
233
266
  }
234
267
 
268
+ private getSessionPermissionMode(): CodeExecutionMode {
269
+ return this.session?.permissionMode ?? "default";
270
+ }
271
+
235
272
  private createApp(): Hono {
236
273
  const app = new Hono();
237
274
 
@@ -285,6 +322,7 @@ export class AgentServer {
285
322
  await this.initializeSession(payload, sseController);
286
323
  } else {
287
324
  this.session.sseController = sseController;
325
+ this.session.hasDesktopConnected = true;
288
326
  this.replayPendingEvents();
289
327
  }
290
328
 
@@ -579,6 +617,51 @@ export class AgentServer {
579
617
  return { closed: true };
580
618
  }
581
619
 
620
+ case "posthog/set_config_option":
621
+ case "set_config_option": {
622
+ const configId = params.configId as string;
623
+ const value = params.value as string;
624
+
625
+ this.logger.info("Set config option requested", { configId, value });
626
+
627
+ const result =
628
+ await this.session.clientConnection.setSessionConfigOption({
629
+ sessionId: this.session.acpSessionId,
630
+ configId,
631
+ value,
632
+ });
633
+
634
+ return {
635
+ configOptions: result.configOptions,
636
+ };
637
+ }
638
+
639
+ case POSTHOG_NOTIFICATIONS.PERMISSION_RESPONSE:
640
+ case "permission_response": {
641
+ const requestId = params.requestId as string;
642
+ const optionId = params.optionId as string;
643
+ const customInput = params.customInput as string | undefined;
644
+ const answers = params.answers as Record<string, string> | undefined;
645
+
646
+ this.logger.info("Permission response received", {
647
+ requestId,
648
+ optionId,
649
+ });
650
+
651
+ const resolved = this.resolvePermission(
652
+ requestId,
653
+ optionId,
654
+ customInput,
655
+ answers,
656
+ );
657
+ if (!resolved) {
658
+ throw new Error(
659
+ `No pending permission request found for id: ${requestId}`,
660
+ );
661
+ }
662
+ return { resolved: true };
663
+ }
664
+
582
665
  default:
583
666
  throw new Error(`Unknown method: ${method}`);
584
667
  }
@@ -638,6 +721,39 @@ export class AgentServer {
638
721
 
639
722
  this.configureEnvironment();
640
723
 
724
+ const [preTaskRun, preTask] = await Promise.all([
725
+ this.posthogAPI
726
+ .getTaskRun(payload.task_id, payload.run_id)
727
+ .catch((err) => {
728
+ this.logger.warn("Failed to fetch task run for session context", {
729
+ taskId: payload.task_id,
730
+ runId: payload.run_id,
731
+ error: err,
732
+ });
733
+ return null;
734
+ }),
735
+ this.posthogAPI.getTask(payload.task_id).catch((err) => {
736
+ this.logger.warn("Failed to fetch task for session context", {
737
+ taskId: payload.task_id,
738
+ error: err,
739
+ });
740
+ return null;
741
+ }),
742
+ ]);
743
+
744
+ const prUrl = getTaskRunStateString(preTaskRun, "slack_notified_pr_url");
745
+
746
+ if (prUrl) {
747
+ this.detectedPrUrl = prUrl;
748
+ }
749
+
750
+ const runtimeAdapter = this.getRuntimeAdapter();
751
+ const sessionSystemPrompt = this.buildSessionSystemPrompt(prUrl);
752
+ const codexInstructions =
753
+ runtimeAdapter === "codex"
754
+ ? this.buildCodexInstructions(sessionSystemPrompt)
755
+ : undefined;
756
+
641
757
  const posthogAPI = new PostHogAPIClient({
642
758
  apiUrl: this.config.apiUrl,
643
759
  projectId: this.config.projectId,
@@ -661,10 +777,23 @@ export class AgentServer {
661
777
  });
662
778
 
663
779
  const acpConnection = createAcpConnection({
780
+ adapter: runtimeAdapter,
664
781
  taskRunId: payload.run_id,
665
782
  taskId: payload.task_id,
666
783
  deviceType: deviceInfo.type,
667
784
  logWriter,
785
+ logger: this.logger,
786
+ codexOptions:
787
+ runtimeAdapter === "codex"
788
+ ? {
789
+ cwd: this.config.repositoryPath ?? "/tmp/workspace",
790
+ apiBaseUrl: process.env.OPENAI_BASE_URL,
791
+ apiKey: this.config.apiKey,
792
+ model: this.config.model ?? DEFAULT_CODEX_MODEL,
793
+ reasoningEffort: this.config.reasoningEffort,
794
+ instructions: codexInstructions,
795
+ }
796
+ : undefined,
668
797
  onStructuredOutput: async (output) => {
669
798
  await this.posthogAPI.setTaskRunOutput(
670
799
  payload.task_id,
@@ -709,50 +838,34 @@ export class AgentServer {
709
838
  clientCapabilities: {},
710
839
  });
711
840
 
712
- const [preTaskRun, preTask] = await Promise.all([
713
- this.posthogAPI
714
- .getTaskRun(payload.task_id, payload.run_id)
715
- .catch((err) => {
716
- this.logger.warn("Failed to fetch task run for session context", {
717
- taskId: payload.task_id,
718
- runId: payload.run_id,
719
- error: err,
720
- });
721
- return null;
722
- }),
723
- this.posthogAPI.getTask(payload.task_id).catch((err) => {
724
- this.logger.warn("Failed to fetch task for session context", {
725
- taskId: payload.task_id,
726
- error: err,
727
- });
728
- return null;
729
- }),
730
- ]);
731
-
732
- const prUrl =
733
- typeof (preTaskRun?.state as Record<string, unknown>)
734
- ?.slack_notified_pr_url === "string"
735
- ? ((preTaskRun?.state as Record<string, unknown>)
736
- .slack_notified_pr_url as string)
737
- : null;
738
-
739
- if (prUrl) {
740
- this.detectedPrUrl = prUrl;
741
- }
742
-
841
+ const runState = preTaskRun?.state as Record<string, unknown> | undefined;
842
+ // Cloud runs default to bypassPermissions (auto-approve everything).
843
+ // Only PostHog Code sets initial_permission_mode explicitly (e.g., "plan").
844
+ const initialPermissionMode: CodeExecutionMode =
845
+ typeof runState?.initial_permission_mode === "string"
846
+ ? (runState.initial_permission_mode as CodeExecutionMode)
847
+ : "bypassPermissions";
743
848
  const sessionResponse = await clientConnection.newSession({
744
849
  cwd: this.config.repositoryPath ?? "/tmp/workspace",
745
850
  mcpServers: this.config.mcpServers ?? [],
746
851
  _meta: {
747
852
  sessionId: payload.run_id,
748
853
  taskRunId: payload.run_id,
749
- systemPrompt: this.buildSessionSystemPrompt(prUrl),
854
+ systemPrompt: sessionSystemPrompt,
855
+ ...(this.config.model && { model: this.config.model }),
750
856
  allowedDomains: this.config.allowedDomains,
751
857
  jsonSchema: preTask?.json_schema ?? null,
858
+ permissionMode: initialPermissionMode,
752
859
  ...(this.config.claudeCode?.plugins?.length && {
753
860
  claudeCode: {
754
861
  options: {
755
- plugins: this.config.claudeCode.plugins,
862
+ ...(this.config.claudeCode?.plugins?.length && {
863
+ plugins: this.config.claudeCode.plugins,
864
+ }),
865
+ ...(runtimeAdapter === "claude" &&
866
+ this.config.reasoningEffort && {
867
+ effort: this.config.reasoningEffort,
868
+ }),
756
869
  },
757
870
  },
758
871
  }),
@@ -774,6 +887,8 @@ export class AgentServer {
774
887
  sseController,
775
888
  deviceInfo,
776
889
  logWriter,
890
+ permissionMode: initialPermissionMode,
891
+ hasDesktopConnected: sseController !== null,
777
892
  };
778
893
 
779
894
  this.logger = new Logger({
@@ -791,6 +906,7 @@ export class AgentServer {
791
906
  this.logger.info(
792
907
  `Agent version: ${this.config.version ?? packageJson.version}`,
793
908
  );
909
+ this.logger.info(`Initial permission mode: ${initialPermissionMode}`);
794
910
 
795
911
  // Signal in_progress so the UI can start polling for updates
796
912
  this.posthogAPI
@@ -1121,6 +1237,14 @@ export class AgentServer {
1121
1237
  return { append: cloudAppend };
1122
1238
  }
1123
1239
 
1240
+ private buildCodexInstructions(
1241
+ systemPrompt: string | { append: string },
1242
+ ): string {
1243
+ return typeof systemPrompt === "string"
1244
+ ? systemPrompt
1245
+ : systemPrompt.append;
1246
+ }
1247
+
1124
1248
  private getCloudInteractionOrigin(): string | undefined {
1125
1249
  return (
1126
1250
  process.env.POSTHOG_CODE_INTERACTION_ORIGIN ??
@@ -1429,12 +1553,10 @@ ${attributionInstructions}
1429
1553
  requestPermission: async (
1430
1554
  params: RequestPermissionRequest,
1431
1555
  ): Promise<RequestPermissionResponse> => {
1432
- // Background mode: always auto-approve permissions
1433
- // Interactive mode: also auto-approve for now (user can monitor via SSE)
1434
- // Future: interactive mode could pause and wait for user approval via SSE
1435
1556
  this.logger.debug("Permission request", {
1436
1557
  mode,
1437
1558
  interactionOrigin,
1559
+ kind: params.toolCall?.kind,
1438
1560
  options: params.options,
1439
1561
  });
1440
1562
 
@@ -1444,8 +1566,11 @@ ${attributionInstructions}
1444
1566
  const selectedOptionId =
1445
1567
  allowOption?.optionId ?? params.options[0].optionId;
1446
1568
 
1569
+ const codeToolKind = params.toolCall?._meta?.codeToolKind;
1570
+ const isPlanApproval = params.toolCall?.kind === "switch_mode";
1571
+
1572
+ // Relay questions to Slack when interaction originated there
1447
1573
  if (interactionOrigin === "slack") {
1448
- const codeToolKind = params.toolCall?._meta?.codeToolKind;
1449
1574
  if (codeToolKind === "question") {
1450
1575
  return this.buildSlackQuestionRelayResponse(
1451
1576
  payload,
@@ -1454,6 +1579,27 @@ ${attributionInstructions}
1454
1579
  }
1455
1580
  }
1456
1581
 
1582
+ // Relay permission requests to the desktop app when:
1583
+ // - Questions: always relay (need human answers regardless of mode)
1584
+ // - Plan approvals: always relay
1585
+ // - Edit/bash in "default" mode: relay for manual approval
1586
+ // Other modes auto-approve. No client connected → auto-approve.
1587
+ {
1588
+ const isQuestion = codeToolKind === "question";
1589
+ const sessionPermissionMode = this.getSessionPermissionMode();
1590
+ const needsRelay =
1591
+ isQuestion || isPlanApproval || sessionPermissionMode === "default";
1592
+
1593
+ if (needsRelay && this.session?.hasDesktopConnected) {
1594
+ this.logger.info("Relaying permission to connected client", {
1595
+ kind: params.toolCall?.kind,
1596
+ isQuestion,
1597
+ sessionPermissionMode,
1598
+ });
1599
+ return this.relayPermissionToClient(params);
1600
+ }
1601
+ }
1602
+
1457
1603
  if (this.shouldBlockPublishPermission(params)) {
1458
1604
  return {
1459
1605
  outcome: { outcome: "cancelled" },
@@ -1481,6 +1627,19 @@ ${attributionInstructions}
1481
1627
  sessionId: string;
1482
1628
  update?: Record<string, unknown>;
1483
1629
  }) => {
1630
+ // Track permission mode changes for relay decisions
1631
+ if (
1632
+ params.update?.sessionUpdate === "current_mode_update" &&
1633
+ typeof params.update?.currentModeId === "string" &&
1634
+ this.session
1635
+ ) {
1636
+ this.session.permissionMode = params.update
1637
+ .currentModeId as CodeExecutionMode;
1638
+ this.logger.info("Permission mode updated", {
1639
+ mode: params.update.currentModeId,
1640
+ });
1641
+ }
1642
+
1484
1643
  // session/update notifications flow through the tapped stream (like local transport)
1485
1644
  // Only handle tree state capture for file changes here
1486
1645
  if (params.update?.sessionUpdate === "tool_call_update") {
@@ -1730,6 +1889,16 @@ ${attributionInstructions}
1730
1889
  this.logger.error("Failed to flush session logs", error);
1731
1890
  }
1732
1891
 
1892
+ // Drain pending permissions before ACP cleanup to avoid deadlocks —
1893
+ // cleanup may await operations that are blocked on a permission response.
1894
+ for (const [, pending] of this.pendingPermissions) {
1895
+ pending.resolve({
1896
+ outcome: { outcome: "selected", optionId: "reject" },
1897
+ _meta: { customInput: "Session is shutting down." },
1898
+ });
1899
+ }
1900
+ this.pendingPermissions.clear();
1901
+
1733
1902
  try {
1734
1903
  await this.session.acpConnection.cleanup();
1735
1904
  } catch (error) {
@@ -1823,4 +1992,55 @@ ${attributionInstructions}
1823
1992
  this.detachSseController(controller);
1824
1993
  }
1825
1994
  }
1995
+
1996
+ /**
1997
+ * Relay a permission request (e.g., plan approval) to the connected desktop
1998
+ * app via SSE and wait for a response via the `/command` endpoint.
1999
+ *
2000
+ * The promise waits indefinitely — if SSE is disconnected, the event is
2001
+ * buffered by broadcastEvent and replayed when the client reconnects. Session
2002
+ * cleanup force-resolves all pending permissions, so there is no leak.
2003
+ */
2004
+ private relayPermissionToClient(params: {
2005
+ options: Array<{ kind: string; optionId: string; name?: string }>;
2006
+ toolCall?: Record<string, unknown> | null;
2007
+ }): Promise<{
2008
+ outcome: { outcome: "selected"; optionId: string };
2009
+ _meta?: Record<string, unknown>;
2010
+ }> {
2011
+ const requestId = crypto.randomUUID();
2012
+
2013
+ this.broadcastEvent({
2014
+ type: "permission_request",
2015
+ requestId,
2016
+ options: params.options,
2017
+ toolCall: params.toolCall,
2018
+ });
2019
+
2020
+ return new Promise((resolve) => {
2021
+ this.pendingPermissions.set(requestId, { resolve });
2022
+ });
2023
+ }
2024
+
2025
+ private resolvePermission(
2026
+ requestId: string,
2027
+ optionId: string,
2028
+ customInput?: string,
2029
+ answers?: Record<string, string>,
2030
+ ): boolean {
2031
+ const pending = this.pendingPermissions.get(requestId);
2032
+ if (!pending) return false;
2033
+
2034
+ this.pendingPermissions.delete(requestId);
2035
+
2036
+ const meta: Record<string, unknown> = {};
2037
+ if (customInput) meta.customInput = customInput;
2038
+ if (answers) meta.answers = answers;
2039
+
2040
+ pending.resolve({
2041
+ outcome: { outcome: "selected" as const, optionId },
2042
+ ...(Object.keys(meta).length > 0 ? { _meta: meta } : {}),
2043
+ });
2044
+ return true;
2045
+ }
1826
2046
  }
package/src/server/bin.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import { z } from "zod/v4";
4
+ import { isSupportedReasoningEffort } from "../adapters/reasoning-effort";
4
5
  import { AgentServer } from "./agent-server";
5
6
  import { claudeCodeConfigSchema, mcpServersSchema } from "./schemas";
6
7
 
@@ -26,6 +27,11 @@ const envSchema = z.object({
26
27
  })
27
28
  .regex(/^\d+$/, "POSTHOG_PROJECT_ID must be a numeric string")
28
29
  .transform((val) => parseInt(val, 10)),
30
+ POSTHOG_CODE_RUNTIME_ADAPTER: z.enum(["claude", "codex"]).optional(),
31
+ POSTHOG_CODE_MODEL: z.string().optional(),
32
+ POSTHOG_CODE_REASONING_EFFORT: z
33
+ .enum(["low", "medium", "high", "max"])
34
+ .optional(),
29
35
  });
30
36
 
31
37
  const program = new Command();
@@ -124,6 +130,21 @@ program
124
130
  .filter(Boolean)
125
131
  : undefined;
126
132
 
133
+ if (
134
+ env.POSTHOG_CODE_RUNTIME_ADAPTER &&
135
+ env.POSTHOG_CODE_MODEL &&
136
+ env.POSTHOG_CODE_REASONING_EFFORT &&
137
+ !isSupportedReasoningEffort(
138
+ env.POSTHOG_CODE_RUNTIME_ADAPTER,
139
+ env.POSTHOG_CODE_MODEL,
140
+ env.POSTHOG_CODE_REASONING_EFFORT,
141
+ )
142
+ ) {
143
+ program.error(
144
+ `POSTHOG_CODE_REASONING_EFFORT '${env.POSTHOG_CODE_REASONING_EFFORT}' is not supported for ${env.POSTHOG_CODE_RUNTIME_ADAPTER} model '${env.POSTHOG_CODE_MODEL}'.`,
145
+ );
146
+ }
147
+
127
148
  const server = new AgentServer({
128
149
  port: parseInt(options.port, 10),
129
150
  jwtPublicKey: env.JWT_PUBLIC_KEY,
@@ -139,6 +160,9 @@ program
139
160
  baseBranch: options.baseBranch,
140
161
  claudeCode,
141
162
  allowedDomains,
163
+ runtimeAdapter: env.POSTHOG_CODE_RUNTIME_ADAPTER,
164
+ model: env.POSTHOG_CODE_MODEL,
165
+ reasoningEffort: env.POSTHOG_CODE_REASONING_EFFORT,
142
166
  });
143
167
 
144
168
  process.on("SIGINT", async () => {
@@ -132,4 +132,56 @@ describe("validateCommandParams", () => {
132
132
 
133
133
  expect(result.success).toBe(false);
134
134
  });
135
+
136
+ it("accepts valid permission_response", () => {
137
+ const result = validateCommandParams("permission_response", {
138
+ requestId: "abc-123",
139
+ optionId: "acceptEdits",
140
+ });
141
+
142
+ expect(result.success).toBe(true);
143
+ });
144
+
145
+ it("accepts permission_response with customInput", () => {
146
+ const result = validateCommandParams("permission_response", {
147
+ requestId: "abc-123",
148
+ optionId: "reject_with_feedback",
149
+ customInput: "Please change the approach",
150
+ });
151
+
152
+ expect(result.success).toBe(true);
153
+ });
154
+
155
+ it("rejects permission_response without requestId", () => {
156
+ const result = validateCommandParams("permission_response", {
157
+ optionId: "acceptEdits",
158
+ });
159
+
160
+ expect(result.success).toBe(false);
161
+ });
162
+
163
+ it("rejects permission_response without optionId", () => {
164
+ const result = validateCommandParams("permission_response", {
165
+ requestId: "abc-123",
166
+ });
167
+
168
+ expect(result.success).toBe(false);
169
+ });
170
+
171
+ it("accepts valid set_config_option", () => {
172
+ const result = validateCommandParams("set_config_option", {
173
+ configId: "mode",
174
+ value: "plan",
175
+ });
176
+
177
+ expect(result.success).toBe(true);
178
+ });
179
+
180
+ it("rejects set_config_option without configId", () => {
181
+ const result = validateCommandParams("set_config_option", {
182
+ value: "plan",
183
+ });
184
+
185
+ expect(result.success).toBe(false);
186
+ });
135
187
  });
@@ -48,6 +48,18 @@ export const userMessageParamsSchema = z.object({
48
48
  ]),
49
49
  });
50
50
 
51
+ export const permissionResponseParamsSchema = z.object({
52
+ requestId: z.string().min(1, "requestId is required"),
53
+ optionId: z.string().min(1, "optionId is required"),
54
+ customInput: z.string().optional(),
55
+ answers: z.record(z.string(), z.string()).optional(),
56
+ });
57
+
58
+ export const setConfigOptionParamsSchema = z.object({
59
+ configId: z.string().min(1, "configId is required"),
60
+ value: z.string().min(1, "value is required"),
61
+ });
62
+
51
63
  export const commandParamsSchemas = {
52
64
  user_message: userMessageParamsSchema,
53
65
  "posthog/user_message": userMessageParamsSchema,
@@ -55,6 +67,10 @@ export const commandParamsSchemas = {
55
67
  "posthog/cancel": z.object({}).optional(),
56
68
  close: z.object({}).optional(),
57
69
  "posthog/close": z.object({}).optional(),
70
+ permission_response: permissionResponseParamsSchema,
71
+ "posthog/permission_response": permissionResponseParamsSchema,
72
+ set_config_option: setConfigOptionParamsSchema,
73
+ "posthog/set_config_option": setConfigOptionParamsSchema,
58
74
  } as const;
59
75
 
60
76
  export type CommandMethod = keyof typeof commandParamsSchemas;
@@ -24,4 +24,7 @@ export interface AgentServerConfig {
24
24
  baseBranch?: string;
25
25
  claudeCode?: ClaudeCodeConfig;
26
26
  allowedDomains?: string[];
27
+ runtimeAdapter?: "claude" | "codex";
28
+ model?: string;
29
+ reasoningEffort?: "low" | "medium" | "high" | "max";
27
30
  }
@@ -20,6 +20,30 @@ export function createPostHogHandlers(options: PostHogHandlersOptions = {}) {
20
20
  } = options;
21
21
 
22
22
  return [
23
+ // GET local LLM gateway models - session initialization fetches these in the
24
+ // background for command/model metadata.
25
+ http.get("http://localhost:3308/:product/v1/models", () => {
26
+ return HttpResponse.json({
27
+ object: "list",
28
+ data: [
29
+ {
30
+ id: "claude-opus-4-6",
31
+ owned_by: "anthropic",
32
+ context_window: 200000,
33
+ supports_streaming: true,
34
+ supports_vision: true,
35
+ },
36
+ {
37
+ id: "gpt-5.4",
38
+ owned_by: "openai",
39
+ context_window: 200000,
40
+ supports_streaming: true,
41
+ supports_vision: true,
42
+ },
43
+ ],
44
+ });
45
+ }),
46
+
23
47
  // POST /append_log/ - Agent log entries
24
48
  http.post(
25
49
  `${baseUrl}/api/projects/:projectId/tasks/:taskId/runs/:runId/append_log/`,