@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.
package/dist/front/index.d.ts
CHANGED
|
@@ -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-
|
|
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';
|
package/dist/front/index.js
CHANGED
|
@@ -344,32 +344,61 @@ function useAgentChat(opts) {
|
|
|
344
344
|
|
|
345
345
|
// src/front/splitFollowUp.ts
|
|
346
346
|
function splitFollowUp(msgs, consumed, genId) {
|
|
347
|
-
|
|
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
|
|
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
|
-
})
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
)
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
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
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
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
|
*/
|
package/dist/server/index.d.ts
CHANGED
|
@@ -215,9 +215,16 @@ interface VercelSandboxWorkspaceOptions {
|
|
|
215
215
|
}
|
|
216
216
|
declare function createVercelSandboxWorkspace(sandbox: Sandbox$1, workspaceOpts?: VercelSandboxWorkspaceOptions): VercelSandboxWorkspace;
|
|
217
217
|
|
|
218
|
-
type
|
|
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 };
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
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,
|
|
3883
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
4540
|
-
|
|
4541
|
-
|
|
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
|
-
|
|
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,
|
|
4625
|
-
|
|
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(`
|
|
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?.(
|
|
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
|
|
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 (
|
|
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 (
|
|
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, {
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -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-
|
|
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.
|
|
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.
|
|
77
|
+
"@hachej/boring-ui-kit": "0.1.13"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@opentelemetry/api": "^1.9.1",
|