@hachej/boring-agent 0.1.23 → 0.1.26

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.
@@ -1,5 +1,12 @@
1
+ interface WorkspaceRuntimeContext {
2
+ /** Agent-visible working directory shared by file-tree and shell execution. */
3
+ readonly runtimeCwd: string;
4
+ }
5
+
1
6
  interface Workspace {
7
+ /** Agent-visible workspace root; must match runtimeContext.runtimeCwd. */
2
8
  readonly root: string;
9
+ readonly runtimeContext: WorkspaceRuntimeContext;
3
10
  readFile(relPath: string): Promise<string>;
4
11
  /** Optional binary read operation for media/document previews. */
5
12
  readBinaryFile?(relPath: string): Promise<Uint8Array>;
@@ -137,6 +144,8 @@ interface Sandbox {
137
144
  readonly provider: string;
138
145
  /** Capabilities this sandbox advertises (used for tool gating). */
139
146
  readonly capabilities: readonly SandboxCapability[];
147
+ /** Agent-visible runtime cwd; must match the paired Workspace root. */
148
+ readonly runtimeContext: WorkspaceRuntimeContext;
140
149
  /**
141
150
  * Optional initialization hook. Some backends bind to a workspace +
142
151
  * session at construction (Vercel: snapshot warm-up; Bwrap: bind
@@ -171,6 +180,7 @@ interface Sandbox {
171
180
  dispose?(): Promise<void>;
172
181
  }
173
182
  interface ExecOptions {
183
+ /** Runtime-visible cwd. Relative paths resolve from `runtimeContext.runtimeCwd`. */
174
184
  cwd?: string;
175
185
  env?: Record<string, string>;
176
186
  signal?: AbortSignal;
@@ -287,4 +297,4 @@ interface PluginRestartWarning {
287
297
  message: string;
288
298
  }
289
299
 
290
- export { type Entry as E, type FileSearch as F, type IsolatedCodeInput as I, type PluginRestartWarning as P, type Sandbox as S, type Workspace as W, type ExecOptions as a, type ExecResult as b, type IsolatedCodeOutput as c, type SandboxCapability as d, type SandboxHandleRecord as e, type SandboxHandleStore as f, type Stat as g, WORKSPACE_AGENT_PLUGINS_RELOADED_EVENT as h };
300
+ export { type Entry as E, type FileSearch as F, type IsolatedCodeInput as I, type PluginRestartWarning as P, type Sandbox as S, type Workspace as W, type ExecOptions as a, type ExecResult as b, type IsolatedCodeOutput as c, type SandboxCapability as d, type SandboxHandleRecord as e, type SandboxHandleStore as f, type Stat as g, WORKSPACE_AGENT_PLUGINS_RELOADED_EVENT as h, type WorkspaceRuntimeContext as i };
@@ -0,0 +1,29 @@
1
+ // src/shared/telemetry.ts
2
+ var noopTelemetry = {
3
+ capture() {
4
+ }
5
+ };
6
+ function safeCapture(telemetry, event) {
7
+ try {
8
+ void Promise.resolve(telemetry.capture(event)).catch(() => {
9
+ });
10
+ } catch {
11
+ }
12
+ }
13
+
14
+ // src/shared/validateTool.ts
15
+ function validateTool(tool) {
16
+ if (typeof tool !== "object" || tool === null) return null;
17
+ const t = tool;
18
+ if (typeof t.name !== "string" || t.name.length === 0) return null;
19
+ if (typeof t.description !== "string") return null;
20
+ if (typeof t.parameters !== "object" || t.parameters === null) return null;
21
+ if (typeof t.execute !== "function") return null;
22
+ return t;
23
+ }
24
+
25
+ export {
26
+ noopTelemetry,
27
+ safeCapture,
28
+ validateTool
29
+ };
@@ -1,16 +1,3 @@
1
- // src/shared/telemetry.ts
2
- var noopTelemetry = {
3
- capture() {
4
- }
5
- };
6
- function safeCapture(telemetry, event) {
7
- try {
8
- void Promise.resolve(telemetry.capture(event)).catch(() => {
9
- });
10
- } catch {
11
- }
12
- }
13
-
14
1
  // src/shared/error-codes.ts
15
2
  import { z } from "zod";
16
3
  var ErrorCode = z.enum([
@@ -28,6 +15,11 @@ var ErrorCode = z.enum([
28
15
  "PATH_NOT_FOUND",
29
16
  "PATH_NOT_WRITABLE",
30
17
  "WORKSPACE_UNINITIALIZED",
18
+ "WORKSPACE_NOT_READY",
19
+ // Agent runtime / provisioning
20
+ "AGENT_RUNTIME_NOT_READY",
21
+ "RUNTIME_PROVISIONING_FAILED",
22
+ "RUNTIME_PROVISIONING_LOCKED",
31
23
  // Sandbox / exec
32
24
  "BWRAP_UNAVAILABLE",
33
25
  "BWRAP_TIMEOUT",
@@ -50,6 +42,10 @@ var ErrorCode = z.enum([
50
42
  // Plugin
51
43
  "PLUGIN_LOAD_FAILED",
52
44
  "PLUGIN_NAME_COLLISION",
45
+ "PLUGIN_RUNTIME_REVISION_MISMATCH",
46
+ "PLUGIN_RUNTIME_PRIVATE_FILE",
47
+ "PLUGIN_RUNTIME_UNSAFE_IMPORT",
48
+ "PLUGIN_RUNTIME_TRANSFORM_FAILED",
53
49
  // Runtime provisioning
54
50
  "PROVISIONING_LAYOUT_FAILED",
55
51
  "PROVISIONING_SKILLS_FAILED",
@@ -78,24 +74,10 @@ var ErrorLogFieldsSchema = z.object({
78
74
  msg: z.string().min(1)
79
75
  }).catchall(z.unknown());
80
76
 
81
- // src/shared/validateTool.ts
82
- function validateTool(tool) {
83
- if (typeof tool !== "object" || tool === null) return null;
84
- const t = tool;
85
- if (typeof t.name !== "string" || t.name.length === 0) return null;
86
- if (typeof t.description !== "string") return null;
87
- if (typeof t.parameters !== "object" || t.parameters === null) return null;
88
- if (typeof t.execute !== "function") return null;
89
- return t;
90
- }
91
-
92
77
  export {
93
- noopTelemetry,
94
- safeCapture,
95
78
  ErrorCode,
96
79
  ERROR_CODES,
97
80
  ApiErrorPayloadSchema,
98
81
  ApiErrorResponseSchema,
99
- ErrorLogFieldsSchema,
100
- validateTool
82
+ ErrorLogFieldsSchema
101
83
  };
@@ -90,6 +90,10 @@ interface EvalPromptOptions {
90
90
  systemPrompt?: string;
91
91
  /** Override the chat session id (defaults to a fresh uuid). */
92
92
  sessionId?: string;
93
+ /** Optional headers sent to session/chat requests (e.g. workspace scoping). */
94
+ headers?: Record<string, string>;
95
+ /** Optional query params appended to session/chat requests (e.g. workspaceId). */
96
+ query?: Record<string, string | number | boolean | undefined>;
93
97
  /** Per-call timeout in ms. Defaults to 30_000. */
94
98
  timeoutMs?: number;
95
99
  /** Per-prompt retry count. Default: 0. */
@@ -206,7 +206,9 @@ async function evalAgentPrompt(opts) {
206
206
  prompt: opts.prompt,
207
207
  systemPrompt: opts.systemPrompt,
208
208
  model,
209
- timeoutMs
209
+ timeoutMs,
210
+ headers: opts.headers,
211
+ query: opts.query
210
212
  });
211
213
  } catch (err) {
212
214
  lastResult = {
@@ -219,6 +221,18 @@ async function evalAgentPrompt(opts) {
219
221
  if (attempts > maxRetries) return lastResult;
220
222
  continue;
221
223
  }
224
+ if (captured.errorText) {
225
+ lastResult = {
226
+ ok: false,
227
+ actual: captured.toolCalls,
228
+ text: captured.text,
229
+ usage: captured.usage,
230
+ attempts,
231
+ reason: `stream: ${captured.errorText}`
232
+ };
233
+ if (attempts > maxRetries) return lastResult;
234
+ continue;
235
+ }
222
236
  const matchResult = matchExpectations(opts, captured.toolCalls);
223
237
  lastResult = {
224
238
  ok: matchResult.ok,
@@ -265,9 +279,11 @@ function matchExpectations(opts, actual) {
265
279
  return someCallMatches(opts.expect, actual);
266
280
  }
267
281
  async function runOnce(app, opts) {
282
+ const querySuffix = formatQuerySuffix(opts.query);
268
283
  await app.inject({
269
284
  method: "POST",
270
- url: "/api/v1/agent/sessions",
285
+ url: `/api/v1/agent/sessions${querySuffix}`,
286
+ headers: opts.headers,
271
287
  payload: { id: opts.sessionId }
272
288
  });
273
289
  const userMessage = opts.systemPrompt ? `[SYSTEM]
@@ -280,7 +296,8 @@ ${opts.prompt}` : opts.prompt;
280
296
  res = await withTimeout(
281
297
  app.inject({
282
298
  method: "POST",
283
- url: "/api/v1/agent/chat",
299
+ url: `/api/v1/agent/chat${querySuffix}`,
300
+ headers: opts.headers,
284
301
  payload: {
285
302
  sessionId: opts.sessionId,
286
303
  message: userMessage,
@@ -293,7 +310,8 @@ ${opts.prompt}` : opts.prompt;
293
310
  } finally {
294
311
  void app.inject({
295
312
  method: "DELETE",
296
- url: `/api/v1/agent/sessions/${encodeURIComponent(opts.sessionId)}`
313
+ url: `/api/v1/agent/sessions/${encodeURIComponent(opts.sessionId)}${querySuffix}`,
314
+ headers: opts.headers
297
315
  }).catch(() => {
298
316
  });
299
317
  }
@@ -304,9 +322,20 @@ ${opts.prompt}` : opts.prompt;
304
322
  }
305
323
  return parseSseStream(res.body);
306
324
  }
325
+ function formatQuerySuffix(query) {
326
+ if (!query) return "";
327
+ const params = new URLSearchParams();
328
+ for (const [key, value] of Object.entries(query)) {
329
+ if (value === void 0) continue;
330
+ params.set(key, String(value));
331
+ }
332
+ const encoded = params.toString();
333
+ return encoded ? `?${encoded}` : "";
334
+ }
307
335
  function parseSseStream(body) {
308
336
  const toolCalls = [];
309
337
  const textParts = [];
338
+ const errorParts = [];
310
339
  let usage;
311
340
  for (const line of body.split("\n")) {
312
341
  const trimmed = line.trim();
@@ -337,9 +366,28 @@ function parseSseStream(body) {
337
366
  usage = { input: obj.input, output: obj.output };
338
367
  }
339
368
  }
369
+ } else if (type === "error") {
370
+ const text = stringifyStreamError(chunk);
371
+ if (text) errorParts.push(text);
372
+ }
373
+ }
374
+ return {
375
+ toolCalls,
376
+ text: textParts.join(""),
377
+ usage,
378
+ errorText: errorParts.length ? errorParts.join("\n") : void 0
379
+ };
380
+ }
381
+ function stringifyStreamError(chunk) {
382
+ for (const key of ["errorText", "message", "error"]) {
383
+ const value = chunk[key];
384
+ if (typeof value === "string" && value.trim()) return value;
385
+ if (value && typeof value === "object") {
386
+ const message = value.message;
387
+ if (typeof message === "string" && message.trim()) return message;
340
388
  }
341
389
  }
342
- return { toolCalls, text: textParts.join(""), usage };
390
+ return "unknown stream error";
343
391
  }
344
392
  function withTimeout(p, ms, label) {
345
393
  return new Promise((resolve, reject) => {
@@ -1,11 +1,11 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as ai from 'ai';
3
+ import { FileUIPart, UIMessage, ChatStatus } from 'ai';
2
4
  import * as react from 'react';
3
5
  import { ReactNode, ComponentType, HTMLAttributes, ComponentProps, FormEvent } from 'react';
4
6
  import { T as ToolUiMetadata } from '../tool-ui-DIFNGwYd.js';
5
- import * as ai from 'ai';
6
- import { UIMessage, FileUIPart, ChatStatus } from 'ai';
7
7
  import * as _ai_sdk_react from '@ai-sdk/react';
8
- import { S as SendMessageInput, e as SessionSummary } from '../harness-BCit36Ha.js';
8
+ import { S as SendMessageInput, e as SessionSummary } from '../harness-CQ0uw6xW.js';
9
9
  import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger, InputGroupAddon, InputGroupButton, InputGroupTextarea } from '@hachej/boring-ui-kit';
10
10
  import { Streamdown } from 'streamdown';
11
11
  import { StickToBottom } from 'use-stick-to-bottom';
@@ -165,6 +165,23 @@ type ComposerBlocker = {
165
165
  sessionId?: string;
166
166
  actions?: ComposerBlockerAction[];
167
167
  };
168
+ type ChatPanelWorkspaceWarmupStatus = {
169
+ status: 'preparing';
170
+ requirement?: 'workspace-fs' | 'sandbox-exec' | 'ui-bridge';
171
+ message?: string;
172
+ } | {
173
+ status: 'ready';
174
+ } | {
175
+ status: 'failed';
176
+ requirement?: 'workspace-fs' | 'sandbox-exec' | 'ui-bridge';
177
+ message?: string;
178
+ };
179
+ type ChatSubmitSource = 'composer' | 'suggestion';
180
+ interface ChatSubmitContext {
181
+ files: FileUIPart[];
182
+ sessionId: string;
183
+ source: ChatSubmitSource;
184
+ }
168
185
  interface ChatPanelProps {
169
186
  sessionId: string;
170
187
  toolRenderers?: ToolRendererOverrides;
@@ -222,6 +239,8 @@ interface ChatPanelProps {
222
239
  onData?: (part: unknown) => void;
223
240
  /** Headers sent with chat and chat-history requests. */
224
241
  requestHeaders?: Record<string, string>;
242
+ /** When false, skip initial chat history/stream resume requests until the user sends. */
243
+ hydrateMessages?: boolean;
225
244
  /**
226
245
  * Called with a file path when the user clicks the path label inside
227
246
  * a read / write / edit tool card. Hosts (e.g. @hachej/boring-workspace)
@@ -237,6 +256,29 @@ interface ChatPanelProps {
237
256
  * is zero-cost when off.
238
257
  */
239
258
  debug?: boolean;
259
+ /** Draft text restored into the composer on mount/update. */
260
+ initialDraft?: string;
261
+ /** Auto-submit initialDraft once after it is restored. Defaults to false. */
262
+ autoSubmitInitialDraft?: boolean;
263
+ /** Called after initialDraft is applied to the composer. */
264
+ onDraftRestored?: () => void;
265
+ /** Called after an auto-submitted initial draft is accepted for sending. */
266
+ onAutoSubmitInitialDraftAccepted?: () => void;
267
+ /** Called after an auto-submitted initial draft finishes its turn. */
268
+ onAutoSubmitInitialDraftSettled?: () => void;
269
+ /**
270
+ * Runs before any agent/model/session-history network send. Return false to
271
+ * cancel submission and keep the draft in the composer.
272
+ */
273
+ onBeforeSubmit?: (draft: string, ctx: ChatSubmitContext) => false | void | Promise<false | void>;
274
+ /** Disable server-backed model/skill discovery for public/local-only shells. */
275
+ serverResourcesEnabled?: boolean;
276
+ /** Center the empty-state prompt/composer for public landing experiences. */
277
+ emptyPlacement?: 'default' | 'hero';
278
+ /** Placeholder shown in the composer textarea. */
279
+ composerPlaceholder?: string;
280
+ /** Current workspace warmup state. Preparing/failed states keep submits local so drafts are not lost. */
281
+ workspaceWarmupStatus?: ChatPanelWorkspaceWarmupStatus;
240
282
  /** Generic host-provided blockers that prevent starting a new user turn. */
241
283
  composerBlockers?: ComposerBlocker[];
242
284
  /** Called when the user presses Stop in the composer. */
@@ -276,12 +318,15 @@ type UseAgentChatOptions = Pick<SendMessageInput, 'sessionId' | 'model' | 'think
276
318
  onData?: (part: unknown) => void;
277
319
  requestHeaders?: Record<string, string>;
278
320
  persistMessages?: boolean;
321
+ hydrateMessages?: boolean;
279
322
  };
280
323
  declare function useAgentChat(opts: UseAgentChatOptions): _ai_sdk_react.UseChatHelpers<UIMessage<unknown, ai.UIDataTypes, ai.UITools>>;
281
324
 
282
325
  interface UseSessionsOptions {
283
326
  requestHeaders?: Record<string, string>;
284
327
  storageKey?: string;
328
+ enabled?: boolean;
329
+ refreshKey?: unknown;
285
330
  }
286
331
  interface UseSessionsResult {
287
332
  sessions: SessionSummary[];
@@ -384,7 +429,7 @@ type PromptInputProps = Omit<HTMLAttributes<HTMLFormElement>, "onSubmit" | "onEr
384
429
  code: "max_files" | "max_file_size" | "accept";
385
430
  message: string;
386
431
  }) => void;
387
- onSubmit: (message: PromptInputMessage, event: FormEvent<HTMLFormElement>) => void | Promise<void>;
432
+ onSubmit: (message: PromptInputMessage, event: FormEvent<HTMLFormElement>) => false | void | Promise<false | void>;
388
433
  /** When provided, files are uploaded to the server immediately on add and the
389
434
  * attachment URL is replaced with the stable server path before submit. */
390
435
  onUploadFile?: (file: File) => Promise<{