@hachej/boring-agent 0.1.10 → 0.1.13

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.
@@ -4,7 +4,7 @@ import { ReactNode, ComponentType, HTMLAttributes, ComponentProps, FormEvent, El
4
4
  import * as ai from 'ai';
5
5
  import { UIMessage, FileUIPart, ChatStatus } from 'ai';
6
6
  import * as _ai_sdk_react from '@ai-sdk/react';
7
- import { S as SendMessageInput, d as SessionSummary } from '../harness-DpMCKbzh.js';
7
+ import { S as SendMessageInput, d as SessionSummary } from '../harness-DT3ZzdAN.js';
8
8
  import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger, InputGroupButton, TooltipContent, InputGroupAddon, InputGroupTextarea } from '@hachej/boring-ui-kit';
9
9
  import { Streamdown } from 'streamdown';
10
10
  import { StickToBottom } from 'use-stick-to-bottom';
@@ -344,32 +344,61 @@ function useAgentChat(opts) {
344
344
 
345
345
  // src/front/splitFollowUp.ts
346
346
  function splitFollowUp(msgs, consumed, genId) {
347
- const targetIdx = msgs.findIndex(
347
+ let targetIdx = msgs.findIndex(
348
348
  (m) => m.role === "assistant" && m.parts?.some((p) => {
349
349
  const part = p;
350
350
  return part.type === "data-followup-consumed" || part.id?.startsWith("turn-");
351
351
  })
352
352
  );
353
+ if (targetIdx < 0) {
354
+ for (let i = msgs.length - 1; i >= 0; i--) {
355
+ if (msgs[i]?.role === "assistant") {
356
+ targetIdx = i;
357
+ break;
358
+ }
359
+ }
360
+ }
361
+ const displayTextFromMarker = msgs.flatMap((m) => m.parts ?? []).map((p) => p).find((p) => p.type === "data-followup-consumed" && typeof p.data?.text === "string")?.data?.text;
353
362
  const userMsg = {
354
363
  id: genId(),
355
364
  role: "user",
356
- parts: [...consumed.files, { type: "text", text: consumed.text }]
365
+ parts: [...consumed.files, { type: "text", text: displayTextFromMarker ?? consumed.text }]
357
366
  };
358
367
  if (targetIdx < 0) {
359
368
  return [...msgs, userMsg];
360
369
  }
361
370
  const target = msgs[targetIdx];
362
- const markerIdx = target.parts?.findIndex((p) => {
371
+ const parts = target.parts ?? [];
372
+ let markerIdx = parts.findIndex((p) => {
363
373
  const part = p;
364
374
  return part.type === "data-followup-consumed" || part.id?.startsWith("turn-");
365
- }) ?? -1;
366
- const markerIsPart = markerIdx >= 0 && (target.parts?.[markerIdx]).type === "data-followup-consumed";
367
- const turn1Parts = markerIdx >= 0 ? target.parts?.slice(0, markerIdx) ?? [] : target.parts ?? [];
368
- const turn2Parts = markerIdx >= 0 ? target.parts?.slice(markerIsPart ? markerIdx + 1 : markerIdx) ?? [] : [];
375
+ });
376
+ let markerIsPart = markerIdx >= 0 && parts[markerIdx].type === "data-followup-consumed";
377
+ if (markerIdx < 0) {
378
+ const textPartIndexes = parts.map((p, index) => p.type === "text" ? index : -1).filter((index) => index >= 0);
379
+ if (textPartIndexes.length > 1) {
380
+ markerIdx = textPartIndexes[textPartIndexes.length - 1];
381
+ markerIsPart = false;
382
+ } else {
383
+ return [...msgs.slice(0, targetIdx), userMsg, target, ...msgs.slice(targetIdx + 1)];
384
+ }
385
+ }
386
+ const turn1Parts = markerIdx >= 0 ? parts.slice(0, markerIdx) : parts;
387
+ const turn2Parts = markerIdx >= 0 ? parts.slice(markerIsPart ? markerIdx + 1 : markerIdx) : [];
369
388
  const asst1 = { ...target, parts: turn1Parts };
370
389
  const asst2 = { ...target, id: genId(), parts: turn2Parts };
371
390
  return [...msgs.slice(0, targetIdx), asst1, userMsg, asst2, ...msgs.slice(targetIdx + 1)];
372
391
  }
392
+ function splitFollowUpForDisplay(msgs, consumed, pending, ids) {
393
+ const draft = consumed ?? pending;
394
+ if (!draft || !ids) return msgs;
395
+ const hasFollowUpBoundary = msgs.some(
396
+ (m) => m.role === "assistant" && (m.parts?.some((p) => p.id?.startsWith("turn-") || p.type === "data-followup-consumed") || (m.parts?.filter((p) => p.type === "text").length ?? 0) > 1)
397
+ );
398
+ if (!hasFollowUpBoundary) return msgs;
399
+ let n = 0;
400
+ return splitFollowUp(msgs, draft, () => n++ === 0 ? ids.userId : ids.assistantId);
401
+ }
373
402
 
374
403
  // src/front/DebugDrawer.tsx
375
404
  import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "react";
@@ -3778,6 +3807,8 @@ function ChatPanel(props) {
3778
3807
  const [debugWidth, setDebugWidth] = useState13(440);
3779
3808
  const [pendingMessage, setPendingMessage] = useState13(null);
3780
3809
  const pendingMessageRef = useRef10(null);
3810
+ const followUpPostedRef = useRef10(false);
3811
+ const followUpPostTimerRef = useRef10(null);
3781
3812
  const consumedFollowUpRef = useRef10(null);
3782
3813
  const followUpSplitIdsRef = useRef10(null);
3783
3814
  const {
@@ -3793,7 +3824,8 @@ function ChatPanel(props) {
3793
3824
  onData: (part) => {
3794
3825
  if (part?.type === "data-followup-consumed") {
3795
3826
  const pending = pendingMessageRef.current;
3796
- consumedFollowUpRef.current = pending ? { text: pending.text, files: pending.files } : null;
3827
+ const serverText = part?.data?.text;
3828
+ consumedFollowUpRef.current = pending ? { text: pending.text, files: pending.files } : typeof serverText === "string" ? { text: serverText, files: [] } : null;
3797
3829
  followUpSplitIdsRef.current = {
3798
3830
  userId: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-followup-user`,
3799
3831
  assistantId: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-followup-assistant`
@@ -3923,17 +3955,35 @@ function ChatPanel(props) {
3923
3955
  return () => globalThis.removeEventListener?.("boring:model-change", onChange);
3924
3956
  }, []);
3925
3957
  const isStreaming = status === "submitted" || status === "streaming";
3926
- const displayMessages = useMemo10(() => {
3927
- const consumed = consumedFollowUpRef.current ?? pendingMessageRef.current;
3928
- const ids = followUpSplitIdsRef.current;
3929
- if (!consumed || !ids) return messages;
3930
- const hasFollowUpPart = messages.some(
3931
- (m) => m.role === "assistant" && m.parts?.some((p) => p.id?.startsWith("turn-"))
3932
- );
3933
- if (!hasFollowUpPart) return messages;
3934
- let n = 0;
3935
- return splitFollowUp(messages, consumed, () => n++ === 0 ? ids.userId : ids.assistantId);
3936
- }, [messages]);
3958
+ const displayMessages = useMemo10(
3959
+ () => splitFollowUpForDisplay(
3960
+ messages,
3961
+ consumedFollowUpRef.current,
3962
+ pendingMessageRef.current,
3963
+ followUpSplitIdsRef.current
3964
+ ),
3965
+ [messages]
3966
+ );
3967
+ const postPendingFollowUp = useCallback12((pending) => {
3968
+ if (followUpPostedRef.current) return;
3969
+ followUpPostedRef.current = true;
3970
+ fetch(`/api/v1/agent/chat/${encodeURIComponent(sessionId)}/followup`, {
3971
+ method: "POST",
3972
+ headers: {
3973
+ "Content-Type": "application/json",
3974
+ ...requestHeaders
3975
+ },
3976
+ body: JSON.stringify({ message: pending.serverMessage, displayText: pending.text, attachments: pending.attachments })
3977
+ }).catch(() => {
3978
+ followUpPostedRef.current = false;
3979
+ });
3980
+ }, [sessionId, requestHeaders]);
3981
+ useEffect11(() => {
3982
+ if (status !== "streaming") return;
3983
+ const pending = pendingMessageRef.current;
3984
+ if (!pending) return;
3985
+ postPendingFollowUp(pending);
3986
+ }, [status, postPendingFollowUp]);
3937
3987
  const prevStatusForQueue = useRef10(status);
3938
3988
  useEffect11(() => {
3939
3989
  const prev = prevStatusForQueue.current;
@@ -3946,6 +3996,9 @@ function ChatPanel(props) {
3946
3996
  consumedFollowUpRef.current = null;
3947
3997
  followUpSplitIdsRef.current = null;
3948
3998
  pendingMessageRef.current = null;
3999
+ followUpPostedRef.current = false;
4000
+ if (followUpPostTimerRef.current) clearTimeout(followUpPostTimerRef.current);
4001
+ followUpPostTimerRef.current = null;
3949
4002
  setPendingMessage(null);
3950
4003
  let n = 0;
3951
4004
  const genId = () => ids ? n++ === 0 ? ids.userId : ids.assistantId : globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
@@ -3966,6 +4019,9 @@ function ChatPanel(props) {
3966
4019
  consumedFollowUpRef.current = null;
3967
4020
  followUpSplitIdsRef.current = null;
3968
4021
  pendingMessageRef.current = null;
4022
+ followUpPostedRef.current = false;
4023
+ if (followUpPostTimerRef.current) clearTimeout(followUpPostTimerRef.current);
4024
+ followUpPostTimerRef.current = null;
3969
4025
  setPendingMessage(null);
3970
4026
  fetch(`/api/v1/agent/chat/${encodeURIComponent(sessionId)}/followup`, {
3971
4027
  method: "DELETE",
@@ -4155,16 +4211,17 @@ ${content}
4155
4211
  attachments: resolvedAttachments
4156
4212
  };
4157
4213
  pendingMessageRef.current = nextPending;
4214
+ followUpPostedRef.current = false;
4158
4215
  setPendingMessage(nextPending);
4159
- fetch(`/api/v1/agent/chat/${encodeURIComponent(sessionId)}/followup`, {
4160
- method: "POST",
4161
- headers: {
4162
- "Content-Type": "application/json",
4163
- ...requestHeaders
4164
- },
4165
- body: JSON.stringify({ message: serverMessage, attachments: resolvedAttachments })
4166
- }).catch(() => {
4167
- });
4216
+ if (status === "streaming") {
4217
+ postPendingFollowUp(nextPending);
4218
+ } else {
4219
+ if (followUpPostTimerRef.current) clearTimeout(followUpPostTimerRef.current);
4220
+ followUpPostTimerRef.current = setTimeout(() => {
4221
+ const pending = pendingMessageRef.current;
4222
+ if (pending) postPendingFollowUp(pending);
4223
+ }, 1e3);
4224
+ }
4168
4225
  }
4169
4226
  return;
4170
4227
  }
@@ -51,7 +51,7 @@ interface AgentHarness {
51
51
  * A `data-followup-consumed` chunk is emitted before the follow-up turn so
52
52
  * the client can clear its pending-message bubble immediately.
53
53
  */
54
- followUp?(sessionId: string, text: string, attachments?: MessageAttachment[]): void;
54
+ followUp?(sessionId: string, text: string, attachments?: MessageAttachment[], displayText?: string): void | Promise<void>;
55
55
  /**
56
56
  * Discard any queued follow-up for this session (called by the Stop button).
57
57
  */
@@ -215,9 +215,16 @@ interface VercelSandboxWorkspaceOptions {
215
215
  }
216
216
  declare function createVercelSandboxWorkspace(sandbox: Sandbox$1, workspaceOpts?: VercelSandboxWorkspaceOptions): VercelSandboxWorkspace;
217
217
 
218
- type RuntimeModeId = 'direct' | 'local' | 'vercel-sandbox';
218
+ type BuiltinRuntimeModeId = 'direct' | 'local' | 'vercel-sandbox';
219
+ type RuntimeModeId = BuiltinRuntimeModeId | (string & {});
219
220
  interface RuntimeModeAdapter {
220
221
  readonly id: RuntimeModeId;
222
+ /**
223
+ * Declares whether the workspace files are strongly available on the host
224
+ * path before create() runs. Composition layers use this to decide whether
225
+ * host-side fs checks/prompts are safe without hard-coding sandbox IDs.
226
+ */
227
+ readonly workspaceFsCapability?: Workspace['fsCapability'];
221
228
  create(ctx: ModeContext): Promise<RuntimeBundle>;
222
229
  dispose?(): Promise<void>;
223
230
  }
@@ -260,6 +267,8 @@ interface CreateAgentAppOptions {
260
267
  sessionId?: string;
261
268
  templatePath?: string;
262
269
  mode?: RuntimeModeId;
270
+ /** Supply a custom runtime adapter to plug in non-built-in sandbox/workspace modes. */
271
+ runtimeModeAdapter?: RuntimeModeAdapter;
263
272
  authToken?: string;
264
273
  version?: string;
265
274
  logger?: boolean;
@@ -295,12 +304,15 @@ interface RegisterAgentRoutesOptions {
295
304
  request?: FastifyRequest;
296
305
  }) => string | undefined | Promise<string | undefined>;
297
306
  mode?: RuntimeModeId;
307
+ /** Supply a custom runtime adapter to plug in non-built-in sandbox/workspace modes. */
308
+ runtimeModeAdapter?: RuntimeModeAdapter;
298
309
  version?: string;
299
310
  extraTools?: AgentTool[];
300
311
  getExtraTools?: (ctx: {
301
312
  workspaceId: string;
302
313
  workspaceRoot: string;
303
314
  runtimeMode: RuntimeModeId;
315
+ workspaceFsCapability?: Workspace['fsCapability'];
304
316
  }) => AgentTool[] | Promise<AgentTool[]>;
305
317
  systemPromptAppend?: string;
306
318
  resourceLoaderOptions?: PiResourceLoaderOptions;
@@ -329,4 +341,4 @@ interface Logger {
329
341
  }
330
342
  declare function createLogger(prefix: string): Logger;
331
343
 
332
- export { type CreateAgentAppOptions, type DeploymentSnapshotProvider, type DeploymentSnapshotRecipe, type DeploymentSnapshotResult, type DeploymentSnapshotStatus, FileHandleStore, type LogFields, type Logger, type ModeContext, PI_PACKAGE_RESOURCE_FILTERS, type PiPackageSource, type PiResourceLoaderOptions, type ProvisionRuntimeWorkspaceOptions, type RegisterAgentRoutesOptions, type RuntimeBundle, type RuntimeModeAdapter, type RuntimeModeId, type RuntimeProvisioningContribution, type RuntimePythonSpec, type RuntimeTemplateContribution, type RuntimeWorkspaceProvisioningResult, type SnapshotBakeOptions, type SnapshotBakeResult, UV_SETUP_COMMANDS, UV_SETUP_COMMANDS as VERCEL_UV_SETUP_COMMANDS, type VercelBakeClient, type VercelBakeSandbox, type VercelDeploymentSnapshotOptions, applyCspHeaders, autoDetectMode, bakeSnapshotIfNeeded, buildDeploymentSnapshotRecipe, buildPackageHash, buildSnapshotRecipeHash, compactPiPackages, createAgentApp, createBwrapSandbox, createDirectSandbox, createLogger, createNodeWorkspace, createVercelDeploymentSnapshotProvider, createVercelSandboxWorkspace, hasBwrap, mergePiPackageSources, piPackageSourceKey, prepareDeploymentSnapshot, prepareVercelDeploymentSnapshot, provisionRuntimeWorkspace, registerAgentRoutes, resolveMode, resolveSandboxHandle };
344
+ export { type BuiltinRuntimeModeId, type CreateAgentAppOptions, type DeploymentSnapshotProvider, type DeploymentSnapshotRecipe, type DeploymentSnapshotResult, type DeploymentSnapshotStatus, FileHandleStore, type LogFields, type Logger, type ModeContext, PI_PACKAGE_RESOURCE_FILTERS, type PiPackageSource, type PiResourceLoaderOptions, type ProvisionRuntimeWorkspaceOptions, type RegisterAgentRoutesOptions, type RuntimeBundle, type RuntimeModeAdapter, type RuntimeModeId, type RuntimeProvisioningContribution, type RuntimePythonSpec, type RuntimeTemplateContribution, type RuntimeWorkspaceProvisioningResult, type SnapshotBakeOptions, type SnapshotBakeResult, UV_SETUP_COMMANDS, UV_SETUP_COMMANDS as VERCEL_UV_SETUP_COMMANDS, type VercelBakeClient, type VercelBakeSandbox, type VercelDeploymentSnapshotOptions, applyCspHeaders, autoDetectMode, bakeSnapshotIfNeeded, buildDeploymentSnapshotRecipe, buildPackageHash, buildSnapshotRecipeHash, compactPiPackages, createAgentApp, createBwrapSandbox, createDirectSandbox, createLogger, createNodeWorkspace, createVercelDeploymentSnapshotProvider, createVercelSandboxWorkspace, hasBwrap, mergePiPackageSources, piPackageSourceKey, prepareDeploymentSnapshot, prepareVercelDeploymentSnapshot, provisionRuntimeWorkspace, registerAgentRoutes, resolveMode, resolveSandboxHandle };
@@ -2164,6 +2164,7 @@ async function copyTemplate(templatePath, workspaceRoot) {
2164
2164
  // src/server/runtime/modes/direct.ts
2165
2165
  var directModeAdapter = {
2166
2166
  id: "direct",
2167
+ workspaceFsCapability: "strong",
2167
2168
  async create(ctx) {
2168
2169
  await mkdir6(ctx.workspaceRoot, { recursive: true });
2169
2170
  await copyTemplate(ctx.templatePath, ctx.workspaceRoot);
@@ -2182,6 +2183,7 @@ var directModeAdapter = {
2182
2183
  import { mkdir as mkdir7 } from "fs/promises";
2183
2184
  var localModeAdapter = {
2184
2185
  id: "local",
2186
+ workspaceFsCapability: "strong",
2185
2187
  async create(ctx) {
2186
2188
  if (process.platform !== "linux") {
2187
2189
  throw new Error("local mode requires Linux with bubblewrap");
@@ -2242,13 +2244,40 @@ function timeoutResult(durationMs) {
2242
2244
  stderrEncoding: "utf-8"
2243
2245
  };
2244
2246
  }
2247
+ function toRemotePath(value) {
2248
+ if (value === VERCEL_SANDBOX_WORKSPACE_ROOT) return VERCEL_SANDBOX_REMOTE_ROOT;
2249
+ if (value.startsWith(`${VERCEL_SANDBOX_WORKSPACE_ROOT}/`)) {
2250
+ return `${VERCEL_SANDBOX_REMOTE_ROOT}${value.slice(VERCEL_SANDBOX_WORKSPACE_ROOT.length)}`;
2251
+ }
2252
+ return value;
2253
+ }
2254
+ function escapeRegExp(value) {
2255
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2256
+ }
2257
+ var WORKSPACE_ALIAS_PREFIX_BOUNDARY = String.raw`(^|[\s"'=:;,\[({<>&|])`;
2258
+ var WORKSPACE_ALIAS_SUFFIX_BOUNDARY = String.raw`(?=/|$|[\s"'=:;,)\]}> &|])`;
2259
+ var WORKSPACE_ALIAS_PATTERN = new RegExp(
2260
+ `${WORKSPACE_ALIAS_PREFIX_BOUNDARY}${escapeRegExp(VERCEL_SANDBOX_WORKSPACE_ROOT)}${WORKSPACE_ALIAS_SUFFIX_BOUNDARY}`,
2261
+ "g"
2262
+ );
2263
+ function replaceWorkspaceAliases(value) {
2264
+ return value.replace(
2265
+ WORKSPACE_ALIAS_PATTERN,
2266
+ (_match, prefix) => `${prefix}${VERCEL_SANDBOX_REMOTE_ROOT}`
2267
+ );
2268
+ }
2245
2269
  function toRemoteCwd(cwd) {
2246
2270
  if (!cwd) return cwd;
2247
- if (cwd === VERCEL_SANDBOX_WORKSPACE_ROOT) return VERCEL_SANDBOX_REMOTE_ROOT;
2248
- if (cwd.startsWith(`${VERCEL_SANDBOX_WORKSPACE_ROOT}/`)) {
2249
- return `${VERCEL_SANDBOX_REMOTE_ROOT}${cwd.slice(VERCEL_SANDBOX_WORKSPACE_ROOT.length)}`;
2250
- }
2251
- return cwd;
2271
+ return toRemotePath(cwd);
2272
+ }
2273
+ function toRemoteCommand(command) {
2274
+ return replaceWorkspaceAliases(command);
2275
+ }
2276
+ function toRemoteEnv(env) {
2277
+ if (!env) return void 0;
2278
+ return Object.fromEntries(
2279
+ Object.entries(env).map(([key, value]) => [key, replaceWorkspaceAliases(toRemotePath(value))])
2280
+ );
2252
2281
  }
2253
2282
  function createVercelSandboxExec(sandbox, execOpts = {}) {
2254
2283
  return {
@@ -2294,9 +2323,9 @@ function createVercelSandboxExec(sandbox, execOpts = {}) {
2294
2323
  }
2295
2324
  const result = await sandbox.runCommand({
2296
2325
  cmd: "sh",
2297
- args: ["-c", cmd],
2326
+ args: ["-c", toRemoteCommand(cmd)],
2298
2327
  cwd: toRemoteCwd(opts?.cwd),
2299
- env: opts?.env,
2328
+ env: toRemoteEnv(opts?.env),
2300
2329
  signal: controller.signal,
2301
2330
  stdout: createStreamWritable(stdoutCollector, captureState, opts?.onStdout),
2302
2331
  stderr: createStreamWritable(stderrCollector, captureState, opts?.onStderr)
@@ -2578,6 +2607,7 @@ function createVercelSandboxModeAdapter(opts = {}) {
2578
2607
  const snapshotScheduler = opts.snapshotScheduler ?? null;
2579
2608
  return {
2580
2609
  id: "vercel-sandbox",
2610
+ workspaceFsCapability: "best-effort",
2581
2611
  async dispose() {
2582
2612
  await snapshotScheduler?.shutdown();
2583
2613
  },
@@ -2674,7 +2704,7 @@ var MODE_ADAPTERS = {
2674
2704
  local: localModeAdapter,
2675
2705
  "vercel-sandbox": vercelSandboxModeAdapter
2676
2706
  };
2677
- function isRuntimeModeId(value) {
2707
+ function isBuiltinRuntimeModeId(value) {
2678
2708
  return value === "direct" || value === "local" || value === "vercel-sandbox";
2679
2709
  }
2680
2710
  function hasBwrap() {
@@ -2684,7 +2714,7 @@ function hasBwrap() {
2684
2714
  function autoDetectMode() {
2685
2715
  const explicitMode = getEnv("BORING_AGENT_MODE");
2686
2716
  if (explicitMode) {
2687
- if (!isRuntimeModeId(explicitMode)) {
2717
+ if (!isBuiltinRuntimeModeId(explicitMode)) {
2688
2718
  throw new Error(
2689
2719
  `Invalid BORING_AGENT_MODE "${explicitMode}". Expected direct, local, or vercel-sandbox.`
2690
2720
  );
@@ -2697,7 +2727,8 @@ function autoDetectMode() {
2697
2727
  return "direct";
2698
2728
  }
2699
2729
  function resolveMode(mode = autoDetectMode()) {
2700
- return MODE_ADAPTERS[mode];
2730
+ if (isBuiltinRuntimeModeId(mode)) return MODE_ADAPTERS[mode];
2731
+ throw new Error(`Runtime mode "${mode}" has no built-in adapter. Pass runtimeModeAdapter to use a custom sandbox mode.`);
2701
2732
  }
2702
2733
 
2703
2734
  // src/server/createAgentApp.ts
@@ -3689,6 +3720,16 @@ function mergePiPackageSources(base = [], additional = []) {
3689
3720
  }
3690
3721
 
3691
3722
  // src/server/harness/pi-coding-agent/createHarness.ts
3723
+ function extractUserMessageText(message) {
3724
+ const record = message;
3725
+ if (record?.role !== "user") return "";
3726
+ if (typeof record.content === "string") return record.content;
3727
+ if (!Array.isArray(record.content)) return "";
3728
+ return record.content.map((part) => {
3729
+ const p = part;
3730
+ return p.type === "text" && typeof p.text === "string" ? p.text : "";
3731
+ }).join("");
3732
+ }
3692
3733
  function extractAssistantMessageText(message) {
3693
3734
  const record = message;
3694
3735
  const role = typeof record?.role === "string" ? record.role : void 0;
@@ -3874,16 +3915,18 @@ function createPiCodingAgentHarness(opts) {
3874
3915
  await originalDelete(ctx, sessionId);
3875
3916
  disposePiSession(sessionId);
3876
3917
  };
3877
- const harnessFollowUpQueues = /* @__PURE__ */ new Map();
3918
+ const nativeFollowUpPending = /* @__PURE__ */ new Set();
3878
3919
  return {
3879
3920
  id: "pi-coding-agent",
3880
3921
  placement: "server",
3881
3922
  sessions: sessionStore,
3882
- followUp(sessionId, text, attachments) {
3883
- harnessFollowUpQueues.set(sessionId, { message: text, attachments });
3923
+ async followUp(sessionId, text, _attachments, _displayText = text) {
3924
+ const handle = piSessions.get(sessionId);
3925
+ if (!handle) return;
3926
+ nativeFollowUpPending.add(sessionId);
3927
+ await handle.piSession.followUp(text);
3884
3928
  },
3885
- clearFollowUp(sessionId) {
3886
- harnessFollowUpQueues.delete(sessionId);
3929
+ clearFollowUp(_sessionId) {
3887
3930
  },
3888
3931
  /**
3889
3932
  * Pi exposes the resolved system prompt as a getter on AgentSession.
@@ -3900,7 +3943,6 @@ function createPiCodingAgentHarness(opts) {
3900
3943
  input,
3901
3944
  ctx
3902
3945
  );
3903
- harnessFollowUpQueues.delete(input.sessionId);
3904
3946
  const chunks = [];
3905
3947
  let done = false;
3906
3948
  let streamError = null;
@@ -3960,7 +4002,6 @@ function createPiCodingAgentHarness(opts) {
3960
4002
  return chunk2;
3961
4003
  });
3962
4004
  }
3963
- let pendingFollowUp;
3964
4005
  let promptSettled = false;
3965
4006
  let promptPromise = Promise.resolve();
3966
4007
  const unsubscribe = piSession.subscribe((event) => {
@@ -3982,7 +4023,19 @@ function createPiCodingAgentHarness(opts) {
3982
4023
  activeTools.delete(event.toolCallId);
3983
4024
  if (activeTools.size === 0) stopHeartbeat();
3984
4025
  }
3985
- let converted = namespaceInlinePartIds(dedupStartChunks(piEventToChunks(event)));
4026
+ let converted;
4027
+ if (event.type === "message_start" && event.message?.role === "user") {
4028
+ const text = extractUserMessageText(event.message);
4029
+ converted = text && text !== input.message ? [{ type: "data-followup-consumed", data: { text } }] : [];
4030
+ if (text && text !== input.message) {
4031
+ inlineTurnIndex += 1;
4032
+ nativeFollowUpPending.delete(input.sessionId);
4033
+ }
4034
+ } else if (event.type === "message_end" && event.message?.role === "user") {
4035
+ converted = [];
4036
+ } else {
4037
+ converted = namespaceInlinePartIds(dedupStartChunks(piEventToChunks(event)));
4038
+ }
3986
4039
  if (event.type === "message_update") {
3987
4040
  const ame = event.assistantMessageEvent;
3988
4041
  if (ame.type === "text_end" && typeof ame.content === "string" && ame.content.length > 0 && !textDeltaSeen.has(ame.contentIndex)) {
@@ -4042,16 +4095,7 @@ function createPiCodingAgentHarness(opts) {
4042
4095
  assistantText += text;
4043
4096
  }
4044
4097
  }
4045
- const followUp = harnessFollowUpQueues.get(input.sessionId);
4046
- if (followUp !== void 0 && !ctx.abortSignal.aborted) {
4047
- harnessFollowUpQueues.delete(input.sessionId);
4048
- chunks.push({ type: "data-followup-consumed" });
4049
- inlineTurnIndex += 1;
4050
- sawTextChunk = false;
4051
- textStartSeen.clear();
4052
- textDeltaSeen.clear();
4053
- assistantText = "";
4054
- pendingFollowUp = followUp;
4098
+ if (nativeFollowUpPending.has(input.sessionId) && !ctx.abortSignal.aborted) {
4055
4099
  } else {
4056
4100
  done = true;
4057
4101
  stopHeartbeat();
@@ -4107,11 +4151,6 @@ ${attachmentNotes.join("\n")}` : message,
4107
4151
  const prepared = await prepareTurn(message, attachments);
4108
4152
  return piSession.prompt(prepared.message, prepared.promptOpts).then(() => {
4109
4153
  promptSettled = true;
4110
- if (pendingFollowUp !== void 0 && !ctx.abortSignal.aborted) {
4111
- const next = pendingFollowUp;
4112
- pendingFollowUp = void 0;
4113
- promptPromise = startTurn(next.message, next.attachments);
4114
- }
4115
4154
  }).catch((err) => {
4116
4155
  promptSettled = true;
4117
4156
  streamError = err;
@@ -4534,13 +4573,33 @@ function boundFs(workspaceRoot) {
4534
4573
  }
4535
4574
 
4536
4575
  // src/server/tools/operations/vercel.ts
4537
- import { relative as relative5 } from "path";
4576
+ import { isAbsolute as isAbsolute5, relative as relative5 } from "path";
4577
+ function rootAliases(workspace) {
4578
+ const aliases = [workspace.root];
4579
+ if (workspace.root === VERCEL_SANDBOX_WORKSPACE_ROOT) aliases.push(VERCEL_SANDBOX_REMOTE_ROOT);
4580
+ if (workspace.root === VERCEL_SANDBOX_REMOTE_ROOT) aliases.push(VERCEL_SANDBOX_WORKSPACE_ROOT);
4581
+ return aliases;
4582
+ }
4583
+ function isOutsideWorkspaceRel(rel) {
4584
+ return rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute5(rel);
4585
+ }
4538
4586
  function toRelPath(workspace, absolutePath) {
4539
- const rel = relative5(workspace.root, absolutePath);
4540
- if (rel.startsWith("..") || rel.startsWith("/")) {
4541
- throw new Error(`path "${absolutePath}" is outside workspace`);
4587
+ for (const root of rootAliases(workspace)) {
4588
+ const rel = relative5(root, absolutePath);
4589
+ if (!isOutsideWorkspaceRel(rel)) return rel;
4590
+ }
4591
+ const skillMarker = "/.agents/skills/";
4592
+ const skillIndex = absolutePath.indexOf(skillMarker);
4593
+ if (skillIndex >= 0) {
4594
+ const skillPath = absolutePath.slice(skillIndex + skillMarker.length);
4595
+ if (skillPath.includes("\0") || skillPath.split(/[\\/]+/).includes("..")) {
4596
+ throw new Error(`path "${absolutePath}" escapes the workspace skills directory`);
4597
+ }
4598
+ return `.agents/skills/${skillPath}`;
4542
4599
  }
4543
- return rel;
4600
+ throw new Error(
4601
+ `path "${absolutePath}" is outside workspace; use a path relative to the workspace root or under ${rootAliases(workspace).join(" / ")}`
4602
+ );
4544
4603
  }
4545
4604
  function vercelBashOps(sandbox) {
4546
4605
  return {
@@ -4599,6 +4658,39 @@ function vercelEditOps(workspace) {
4599
4658
  }
4600
4659
  };
4601
4660
  }
4661
+ function toRemotePath2(value) {
4662
+ if (value === VERCEL_SANDBOX_WORKSPACE_ROOT) return VERCEL_SANDBOX_REMOTE_ROOT;
4663
+ if (value.startsWith(`${VERCEL_SANDBOX_WORKSPACE_ROOT}/`)) {
4664
+ return `${VERCEL_SANDBOX_REMOTE_ROOT}${value.slice(VERCEL_SANDBOX_WORKSPACE_ROOT.length)}`;
4665
+ }
4666
+ return value;
4667
+ }
4668
+ function findPredicate(pattern) {
4669
+ const isPathShaped = pattern.includes("/") || pattern.includes("**");
4670
+ if (!isPathShaped) return `-name ${shellEscape(pattern)}`;
4671
+ let translated = pattern.replaceAll("**", "*").replace(/^\/+/, "");
4672
+ if (!translated.startsWith("*")) translated = `*${translated}`;
4673
+ return `-path ${shellEscape(translated)}`;
4674
+ }
4675
+ function findIgnoreArgs(cwd, ignore) {
4676
+ return ignore.map((pattern) => `! -path ${shellEscape(`${cwd}/${pattern.replace(/^\/+/, "")}/*`)}`).join(" ");
4677
+ }
4678
+ function fallbackFindCommand(pattern, cwd, options) {
4679
+ return [
4680
+ "find",
4681
+ shellEscape(cwd),
4682
+ "-maxdepth 20",
4683
+ "-type f",
4684
+ findIgnoreArgs(cwd, options.ignore),
4685
+ findPredicate(pattern),
4686
+ `| head -n ${Math.max(1, Math.trunc(options.limit))}`
4687
+ ].filter(Boolean).join(" ");
4688
+ }
4689
+ function isFdMissing(result) {
4690
+ if (result.exitCode !== 127) return false;
4691
+ const stderr = Buffer.from(result.stderr).toString("utf-8");
4692
+ return /\bfd: (?:not found|command not found)\b/i.test(stderr);
4693
+ }
4602
4694
  function vercelFindOps(sandbox, workspace) {
4603
4695
  return {
4604
4696
  async exists(absolutePath) {
@@ -4617,18 +4709,25 @@ function vercelFindOps(sandbox, workspace) {
4617
4709
  return result.exitCode === 0;
4618
4710
  },
4619
4711
  async glob(pattern, cwd, options) {
4712
+ const remoteCwd = toRemotePath2(cwd);
4620
4713
  const args = ["fd", "--glob", "--no-require-git", "--max-results", String(options.limit)];
4621
4714
  for (const ig of options.ignore) {
4622
4715
  args.push("--exclude", ig);
4623
4716
  }
4624
- args.push(pattern, cwd);
4625
- const result = await sandbox.exec(args.map(shellEscape).join(" "), {
4717
+ args.push(pattern, remoteCwd);
4718
+ let result = await sandbox.exec(args.map(shellEscape).join(" "), {
4626
4719
  timeoutMs: 3e4,
4627
4720
  maxOutputBytes: 1048576
4628
4721
  });
4722
+ if (isFdMissing(result)) {
4723
+ result = await sandbox.exec(fallbackFindCommand(pattern, remoteCwd, options), {
4724
+ timeoutMs: 3e4,
4725
+ maxOutputBytes: 1048576
4726
+ });
4727
+ }
4629
4728
  if (result.exitCode !== 0 && result.exitCode !== 1) {
4630
4729
  const stderr = Buffer.from(result.stderr).toString("utf-8").trim();
4631
- throw new Error(`fd failed (exit ${result.exitCode}): ${stderr}`);
4730
+ throw new Error(`file search failed (exit ${result.exitCode}): ${stderr}`);
4632
4731
  }
4633
4732
  const stdout = Buffer.from(result.stdout).toString("utf-8");
4634
4733
  return stdout.split("\n").filter(Boolean);
@@ -6158,7 +6257,12 @@ function chatRoutes(app, opts, done) {
6158
6257
  });
6159
6258
  }
6160
6259
  const runtime = await resolveRuntime(request);
6161
- runtime.harness.followUp?.(sessionId, body.message, parsedAttachments.data);
6260
+ await runtime.harness.followUp?.(
6261
+ sessionId,
6262
+ body.message,
6263
+ parsedAttachments.data,
6264
+ typeof body.displayText === "string" && body.displayText.length > 0 ? body.displayText : body.message
6265
+ );
6162
6266
  return reply.code(202).send({ queued: true });
6163
6267
  }
6164
6268
  );
@@ -6281,9 +6385,16 @@ import { randomUUID as randomUUID4 } from "crypto";
6281
6385
  import { z as z2 } from "zod";
6282
6386
  var DEFAULT_SESSION_TITLE2 = "New session";
6283
6387
  var DEFAULT_WORKSPACE_ID2 = "default";
6388
+ var MAX_ANALYSIS_TRANSCRIPT_CHARS = 12e4;
6284
6389
  var createSessionBodySchema = z2.object({
6285
6390
  title: z2.string().min(1).max(200).optional()
6286
6391
  }).optional();
6392
+ var analyzeSessionBodySchema = z2.object({
6393
+ instructions: z2.string().max(4e3).optional(),
6394
+ title: z2.string().min(1).max(200).optional(),
6395
+ run: z2.boolean().optional(),
6396
+ includeTranscript: z2.boolean().optional()
6397
+ }).optional();
6287
6398
  var SessionNotFoundError = class extends Error {
6288
6399
  constructor(sessionId) {
6289
6400
  super(`Session not found: ${sessionId}`);
@@ -6380,13 +6491,141 @@ function classifySessionError(err, reply) {
6380
6491
  }
6381
6492
  });
6382
6493
  }
6494
+ function stableStringify(value) {
6495
+ if (typeof value === "string") return value;
6496
+ try {
6497
+ return JSON.stringify(value, null, 2);
6498
+ } catch {
6499
+ return String(value);
6500
+ }
6501
+ }
6502
+ function partText(part) {
6503
+ const value = part;
6504
+ if (typeof value.text === "string") return value.text;
6505
+ if (typeof value.delta === "string") return value.delta;
6506
+ if (typeof value.content === "string") return value.content;
6507
+ return "";
6508
+ }
6509
+ function toolName(part) {
6510
+ if (typeof part.toolName === "string") return part.toolName;
6511
+ if (typeof part.type === "string" && part.type.startsWith("tool-")) {
6512
+ return part.type.slice("tool-".length);
6513
+ }
6514
+ return "tool";
6515
+ }
6516
+ function formatToolPart(part) {
6517
+ const name = toolName(part);
6518
+ const lines = [`[tool:${name}]`];
6519
+ if ("input" in part) {
6520
+ lines.push("input:", stableStringify(part.input));
6521
+ }
6522
+ if ("output" in part) {
6523
+ lines.push("output:", stableStringify(part.output));
6524
+ }
6525
+ if (typeof part.errorText === "string" && part.errorText.length > 0) {
6526
+ lines.push("error:", part.errorText);
6527
+ }
6528
+ return lines.join("\n");
6529
+ }
6530
+ function formatMessageForTranscript(message, index) {
6531
+ const msg = message;
6532
+ const role = String(msg.role ?? "unknown").toUpperCase();
6533
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
6534
+ const lines = [`## ${index + 1}. ${role}`];
6535
+ if (typeof msg.content === "string" && msg.content.trim()) {
6536
+ lines.push(msg.content.trim());
6537
+ }
6538
+ for (const part of parts) {
6539
+ const record = part;
6540
+ const type = typeof record.type === "string" ? record.type : "";
6541
+ if (type === "text") {
6542
+ const text2 = partText(record).trim();
6543
+ if (text2) lines.push(text2);
6544
+ continue;
6545
+ }
6546
+ if (type === "reasoning") {
6547
+ const text2 = partText(record).trim();
6548
+ if (text2) lines.push(`[reasoning]
6549
+ ${text2}`);
6550
+ continue;
6551
+ }
6552
+ if (type.startsWith("tool-")) {
6553
+ lines.push(formatToolPart(record));
6554
+ continue;
6555
+ }
6556
+ const text = partText(record).trim();
6557
+ if (text) lines.push(`[${type || "part"}]
6558
+ ${text}`);
6559
+ }
6560
+ if (lines.length === 1) lines.push("(no visible content)");
6561
+ return lines.join("\n\n");
6562
+ }
6563
+ function formatSessionTranscript(session) {
6564
+ const header = [
6565
+ `# Agent session transcript: ${session.title}`,
6566
+ "",
6567
+ `- Session: ${session.id}`,
6568
+ `- Created: ${session.createdAt}`,
6569
+ `- Updated: ${session.updatedAt}`,
6570
+ `- User turns: ${session.turnCount}`
6571
+ ];
6572
+ const body = session.messages.map(formatMessageForTranscript);
6573
+ return [...header, "", ...body].join("\n");
6574
+ }
6575
+ function truncateMiddle(value, maxChars) {
6576
+ if (value.length <= maxChars) return value;
6577
+ const keep = Math.floor((maxChars - 200) / 2);
6578
+ return `${value.slice(0, keep)}
6579
+
6580
+ [... transcript truncated: ${value.length - keep * 2} chars omitted ...]
6581
+
6582
+ ${value.slice(-keep)}`;
6583
+ }
6584
+ function buildAnalysisPrompt(session, transcript, instructions) {
6585
+ const boundedTranscript = truncateMiddle(transcript, MAX_ANALYSIS_TRANSCRIPT_CHARS);
6586
+ return [
6587
+ "You are an agent-session analyst. Analyze the transcript below and explain what is going on.",
6588
+ "",
6589
+ "Return a concise, evidence-backed report with these sections:",
6590
+ "1. User goal",
6591
+ "2. Current state",
6592
+ "3. What the agent did",
6593
+ "4. Failures, confusing behavior, or likely root causes",
6594
+ "5. Files/tools/commands that matter",
6595
+ "6. Risks and recommended next actions",
6596
+ "",
6597
+ "Rules:",
6598
+ "- Cite concrete transcript evidence when possible.",
6599
+ '- Say "unknown" when the transcript does not prove something.',
6600
+ "- Do not modify files unless explicitly asked in follow-up.",
6601
+ instructions ? `- Extra user instructions: ${instructions}` : "",
6602
+ "",
6603
+ `Source session: ${session.id} (${session.title})`,
6604
+ "",
6605
+ "<transcript>",
6606
+ boundedTranscript,
6607
+ "</transcript>"
6608
+ ].filter(Boolean).join("\n");
6609
+ }
6610
+ function analysisTextFromChunks(chunks) {
6611
+ return chunks.map((chunk2) => {
6612
+ const record = chunk2;
6613
+ return record.type === "text-delta" && typeof record.delta === "string" ? record.delta : "";
6614
+ }).join("").trim();
6615
+ }
6383
6616
  function sessionRoutes(app, opts, done) {
6384
6617
  const sessionStore = opts.sessionStore ?? new InMemorySessionStore();
6385
6618
  const validateCreateBody = createBodyValidator(createSessionBodySchema);
6619
+ const validateAnalyzeBody = createBodyValidator(analyzeSessionBodySchema);
6386
6620
  async function resolveSessionStore(request) {
6387
6621
  if (opts.getSessionStore) return await opts.getSessionStore(request);
6388
6622
  return sessionStore;
6389
6623
  }
6624
+ async function resolveRuntime(request) {
6625
+ if (opts.getRuntime) return await opts.getRuntime(request);
6626
+ if (opts.harness && opts.workdir) return { harness: opts.harness, workdir: opts.workdir };
6627
+ throw new Error("session analysis requires harness/workdir or getRuntime");
6628
+ }
6390
6629
  app.get("/api/v1/agent/sessions", async (request, reply) => {
6391
6630
  try {
6392
6631
  const store = await resolveSessionStore(request);
@@ -6421,6 +6660,113 @@ function sessionRoutes(app, opts, done) {
6421
6660
  return classifySessionError(err, reply);
6422
6661
  }
6423
6662
  });
6663
+ app.get("/api/v1/agent/sessions/:id/transcript", async (request, reply) => {
6664
+ const params = request.params;
6665
+ const sessionId = requireSessionId(params.id, reply);
6666
+ if (sessionId === null) return;
6667
+ const query = request.query;
6668
+ const format = query.format === "json" ? "json" : "markdown";
6669
+ try {
6670
+ const store = await resolveSessionStore(request);
6671
+ const session = await store.load(getSessionCtx(request), sessionId);
6672
+ const transcript = formatSessionTranscript(session);
6673
+ if (format === "json") {
6674
+ return {
6675
+ session: {
6676
+ id: session.id,
6677
+ title: session.title,
6678
+ createdAt: session.createdAt,
6679
+ updatedAt: session.updatedAt,
6680
+ turnCount: session.turnCount
6681
+ },
6682
+ transcript,
6683
+ messages: session.messages
6684
+ };
6685
+ }
6686
+ return reply.header("Content-Type", "text/markdown; charset=utf-8").send(transcript);
6687
+ } catch (err) {
6688
+ return classifySessionError(err, reply);
6689
+ }
6690
+ });
6691
+ app.post(
6692
+ "/api/v1/agent/sessions/:id/analysis",
6693
+ { preHandler: validateAnalyzeBody },
6694
+ async (request, reply) => {
6695
+ const params = request.params;
6696
+ const sourceSessionId = requireSessionId(params.id, reply);
6697
+ if (sourceSessionId === null) return;
6698
+ const body = request.body;
6699
+ const run2 = body?.run === true;
6700
+ const sessionCtx = getSessionCtx(request);
6701
+ try {
6702
+ const store = await resolveSessionStore(request);
6703
+ const sourceSession = await store.load(sessionCtx, sourceSessionId);
6704
+ const transcript = formatSessionTranscript(sourceSession);
6705
+ const prompt = buildAnalysisPrompt(sourceSession, transcript, body?.instructions);
6706
+ let runtime = null;
6707
+ if (run2) {
6708
+ try {
6709
+ runtime = await resolveRuntime(request);
6710
+ } catch {
6711
+ return reply.code(501).send({
6712
+ error: {
6713
+ code: ERROR_CODE_NOT_IMPLEMENTED,
6714
+ message: "session analysis runtime is not configured"
6715
+ }
6716
+ });
6717
+ }
6718
+ }
6719
+ const analysisSession = await store.create(sessionCtx, {
6720
+ title: body?.title ?? `Analysis: ${sourceSession.title}`.slice(0, 200)
6721
+ });
6722
+ if (!run2) {
6723
+ return {
6724
+ sourceSession: {
6725
+ id: sourceSession.id,
6726
+ title: sourceSession.title,
6727
+ createdAt: sourceSession.createdAt,
6728
+ updatedAt: sourceSession.updatedAt,
6729
+ turnCount: sourceSession.turnCount
6730
+ },
6731
+ analysisSession,
6732
+ prompt,
6733
+ transcriptUrl: `/api/v1/agent/sessions/${encodeURIComponent(sourceSession.id)}/transcript`,
6734
+ ...body?.includeTranscript ? { transcript } : {}
6735
+ };
6736
+ }
6737
+ if (!runtime) {
6738
+ throw new Error("session analysis runtime is not configured");
6739
+ }
6740
+ const abortController = new AbortController();
6741
+ const chunks = [];
6742
+ for await (const chunk2 of runtime.harness.sendMessage(
6743
+ { sessionId: analysisSession.id, message: prompt },
6744
+ {
6745
+ abortSignal: abortController.signal,
6746
+ workdir: runtime.workdir
6747
+ }
6748
+ )) {
6749
+ chunks.push(chunk2);
6750
+ }
6751
+ return {
6752
+ sourceSession: {
6753
+ id: sourceSession.id,
6754
+ title: sourceSession.title,
6755
+ createdAt: sourceSession.createdAt,
6756
+ updatedAt: sourceSession.updatedAt,
6757
+ turnCount: sourceSession.turnCount
6758
+ },
6759
+ analysisSession,
6760
+ prompt,
6761
+ transcriptUrl: `/api/v1/agent/sessions/${encodeURIComponent(sourceSession.id)}/transcript`,
6762
+ analysisText: analysisTextFromChunks(chunks),
6763
+ ...body?.includeTranscript ? { transcript } : {}
6764
+ };
6765
+ } catch (err) {
6766
+ return classifySessionError(err, reply);
6767
+ }
6768
+ }
6769
+ );
6424
6770
  app.delete("/api/v1/agent/sessions/:id", async (request, reply) => {
6425
6771
  const params = request.params;
6426
6772
  const sessionId = requireSessionId(params.id, reply);
@@ -6678,14 +7024,15 @@ async function createAgentApp(opts = {}) {
6678
7024
  const sessionId = opts.sessionId ?? DEFAULT_SESSION_ID;
6679
7025
  const templatePath = opts.templatePath ?? getEnv("BORING_AGENT_TEMPLATE_PATH");
6680
7026
  const app = Fastify({ logger: opts.logger ?? true, bodyLimit: 16 * 1024 * 1024 });
6681
- const resolvedMode = opts.mode ?? autoDetectMode();
6682
- const runtimeBundle = await resolveMode(resolvedMode).create({
7027
+ const resolvedMode = opts.runtimeModeAdapter?.id ?? opts.mode ?? autoDetectMode();
7028
+ const modeAdapter = opts.runtimeModeAdapter ?? resolveMode(resolvedMode);
7029
+ const runtimeBundle = await modeAdapter.create({
6683
7030
  workspaceRoot,
6684
7031
  sessionId,
6685
7032
  templatePath
6686
7033
  });
6687
7034
  const pluginTools = [];
6688
- if (resolvedMode !== "vercel-sandbox") {
7035
+ if (modeAdapter.workspaceFsCapability === "strong") {
6689
7036
  const pluginResult = await loadPlugins({ cwd: workspaceRoot });
6690
7037
  if (pluginResult.errors.length > 0) {
6691
7038
  for (const e of pluginResult.errors) {
@@ -6741,7 +7088,9 @@ async function createAgentApp(opts = {}) {
6741
7088
  sessionChangesTracker
6742
7089
  });
6743
7090
  await app.register(sessionRoutes, {
6744
- sessionStore: harness.sessions
7091
+ sessionStore: harness.sessions,
7092
+ harness,
7093
+ workdir: runtimeBundle.workspace.root
6745
7094
  });
6746
7095
  await app.register(systemPromptRoutes, { harness });
6747
7096
  await app.register(modelsRoutes);
@@ -6971,8 +7320,8 @@ var registerAgentRoutes = async (app, opts) => {
6971
7320
  const workspaceRoot = opts.workspaceRoot ?? process.cwd();
6972
7321
  const sessionId = opts.sessionId ?? DEFAULT_WORKSPACE_ID3;
6973
7322
  const templatePath = opts.templatePath ?? getEnv("BORING_AGENT_TEMPLATE_PATH");
6974
- const resolvedMode = opts.mode ?? autoDetectMode();
6975
- const modeAdapter = selectRuntimeModeAdapter(resolvedMode, opts.sandboxHandleStore);
7323
+ const resolvedMode = opts.runtimeModeAdapter?.id ?? opts.mode ?? autoDetectMode();
7324
+ const modeAdapter = opts.runtimeModeAdapter ?? selectRuntimeModeAdapter(resolvedMode, opts.sandboxHandleStore);
6976
7325
  app.addHook("onClose", async () => {
6977
7326
  await modeAdapter.dispose?.();
6978
7327
  });
@@ -7000,7 +7349,7 @@ var registerAgentRoutes = async (app, opts) => {
7000
7349
  ...buildUploadAgentTools(runtimeBundle)
7001
7350
  ];
7002
7351
  const pluginTools = [];
7003
- if (resolvedMode !== "vercel-sandbox") {
7352
+ if (modeAdapter.workspaceFsCapability === "strong") {
7004
7353
  const pluginResult = await loadPlugins({ cwd: root });
7005
7354
  if (pluginResult.errors.length > 0) {
7006
7355
  for (const e of pluginResult.errors) {
@@ -7017,7 +7366,8 @@ var registerAgentRoutes = async (app, opts) => {
7017
7366
  const scopedExtraTools = opts.getExtraTools ? await opts.getExtraTools({
7018
7367
  workspaceId,
7019
7368
  workspaceRoot: root,
7020
- runtimeMode: resolvedMode
7369
+ runtimeMode: resolvedMode,
7370
+ workspaceFsCapability: runtimeBundle.workspace.fsCapability
7021
7371
  }) : [];
7022
7372
  const tools = mergeTools({
7023
7373
  standardTools,
@@ -7193,6 +7543,13 @@ var registerAgentRoutes = async (app, opts) => {
7193
7543
  getSessionStore: async (request) => {
7194
7544
  const binding = await getBindingForRequest(request);
7195
7545
  return binding.harness.sessions;
7546
+ },
7547
+ getRuntime: async (request) => {
7548
+ const binding = await getBindingForRequest(request);
7549
+ return {
7550
+ harness: binding.harness,
7551
+ workdir: binding.runtimeBundle.workspace.root
7552
+ };
7196
7553
  }
7197
7554
  });
7198
7555
  await app.register(systemPromptRoutes, {
@@ -1,4 +1,4 @@
1
- export { A as AgentHarness, R as RunContext, S as SendMessageInput, a as SessionCtx, b as SessionDetail, c as SessionStore, d as SessionSummary } from '../harness-DpMCKbzh.js';
1
+ export { A as AgentHarness, R as RunContext, S as SendMessageInput, a as SessionCtx, b as SessionDetail, c as SessionStore, d as SessionSummary } from '../harness-DT3ZzdAN.js';
2
2
  import { W as Workspace, S as Sandbox, F as FileSearch, A as AgentTool } from '../sandbox-handle-store-DCNEJJ6b.js';
3
3
  export { E as Entry, a as ExecOptions, b as ExecResult, I as IsolatedCodeInput, c as IsolatedCodeOutput, J as JSONSchema, d as SandboxCapability, e as SandboxHandleRecord, f as SandboxHandleStore, g as Stat, T as ToolExecContext, h as ToolResult } from '../sandbox-handle-store-DCNEJJ6b.js';
4
4
  export { UIMessage, UIMessageChunk } from 'ai';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-agent",
3
- "version": "0.1.10",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Pane-embeddable coding agent. Ships direct/local/vercel-sandbox execution modes behind one interface.",
@@ -74,7 +74,7 @@
74
74
  "use-stick-to-bottom": "^1.1.3",
75
75
  "yaml": "^2.8.3",
76
76
  "zod": "^3.25.76",
77
- "@hachej/boring-ui-kit": "0.1.10"
77
+ "@hachej/boring-ui-kit": "0.1.13"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@opentelemetry/api": "^1.9.1",