@hachej/boring-agent 0.1.34 → 0.1.35

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,19 +4,41 @@ import {
4
4
  validateTool
5
5
  } from "../chunk-HDOSWEXG.js";
6
6
  import {
7
- DEFAULT_AGENT_RUNTIME_CAPABILITIES,
8
- PI_AGENT_RUNTIME_CAPABILITIES,
9
- WORKSPACE_AGENT_PLUGINS_RELOADED_EVENT,
10
- extractToolUiMetadata,
11
- isToolUiMetadata
12
- } from "../chunk-B5JECXMG.js";
7
+ WORKSPACE_AGENT_PLUGINS_RELOADED_EVENT
8
+ } from "../chunk-NZN2PRET.js";
13
9
  import {
14
10
  ApiErrorPayloadSchema,
15
11
  ApiErrorResponseSchema,
12
+ BoringChatMessageSchema,
13
+ BoringChatPartSchema,
14
+ ChatAttachmentPayloadSchema,
15
+ ChatErrorSchema,
16
+ ChatModelSelectionSchema,
17
+ CommandReceiptSchema,
16
18
  ERROR_CODES,
17
19
  ErrorCode,
18
- ErrorLogFieldsSchema
19
- } from "../chunk-Q7ZLUJQW.js";
20
+ ErrorLogFieldsSchema,
21
+ FollowUpPayloadSchema,
22
+ FollowUpReceiptSchema,
23
+ InterruptPayloadSchema,
24
+ PiChatEventSchema,
25
+ PiChatHeartbeatFrameSchema,
26
+ PiChatSnapshotSchema,
27
+ PiChatStatusSchema,
28
+ PiChatStreamFrameSchema,
29
+ PromptPayloadSchema,
30
+ PromptReceiptSchema,
31
+ QueueClearPayloadSchema,
32
+ QueueClearReceiptSchema,
33
+ QueuedUserMessageSchema,
34
+ StopPayloadSchema,
35
+ StopReceiptSchema,
36
+ ThinkingLevelSchema,
37
+ ToolUiMetadataSchema,
38
+ extractToolUiMetadata,
39
+ isToolUiMetadata,
40
+ sanitizeToolUiMetadata2 as sanitizeToolUiMetadata
41
+ } from "../chunk-VQ7VSSSX.js";
20
42
 
21
43
  // src/shared/config-schema.ts
22
44
  import { z } from "zod";
@@ -71,22 +93,56 @@ var EnvSchema = z.object({
71
93
  function validateConfig(config) {
72
94
  return ConfigSchema.parse(config);
73
95
  }
96
+
97
+ // src/shared/capabilities.ts
98
+ var DEFAULT_AGENT_RUNTIME_CAPABILITIES = {
99
+ nativeFollowUp: false,
100
+ aiSdkOwnsHistory: true
101
+ };
102
+ var PI_AGENT_RUNTIME_CAPABILITIES = {
103
+ nativeFollowUp: true,
104
+ aiSdkOwnsHistory: false
105
+ };
74
106
  export {
75
107
  ApiErrorPayloadSchema,
76
108
  ApiErrorResponseSchema,
109
+ BoringChatMessageSchema,
110
+ BoringChatPartSchema,
111
+ ChatAttachmentPayloadSchema,
112
+ ChatErrorSchema,
113
+ ChatModelSelectionSchema,
114
+ CommandReceiptSchema,
77
115
  ConfigSchema,
78
116
  DEFAULT_AGENT_RUNTIME_CAPABILITIES,
79
117
  ERROR_CODES,
80
118
  EnvSchema,
81
119
  ErrorCode,
82
120
  ErrorLogFieldsSchema,
121
+ FollowUpPayloadSchema,
122
+ FollowUpReceiptSchema,
123
+ InterruptPayloadSchema,
83
124
  PI_AGENT_RUNTIME_CAPABILITIES,
125
+ PiChatEventSchema,
126
+ PiChatHeartbeatFrameSchema,
127
+ PiChatSnapshotSchema,
128
+ PiChatStatusSchema,
129
+ PiChatStreamFrameSchema,
130
+ PromptPayloadSchema,
131
+ PromptReceiptSchema,
132
+ QueueClearPayloadSchema,
133
+ QueueClearReceiptSchema,
134
+ QueuedUserMessageSchema,
84
135
  RuntimeModeSchema,
136
+ StopPayloadSchema,
137
+ StopReceiptSchema,
138
+ ThinkingLevelSchema,
139
+ ToolUiMetadataSchema,
85
140
  WORKSPACE_AGENT_PLUGINS_RELOADED_EVENT,
86
141
  extractToolUiMetadata,
87
142
  isToolUiMetadata,
88
143
  noopTelemetry,
89
144
  safeCapture,
145
+ sanitizeToolUiMetadata,
90
146
  validateConfig,
91
147
  validateTool
92
148
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-agent",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
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.",
@@ -41,7 +41,6 @@
41
41
  "docs"
42
42
  ],
43
43
  "dependencies": {
44
- "@ai-sdk/react": "^3.0.170",
45
44
  "@mariozechner/pi-coding-agent": "npm:@earendil-works/pi-coding-agent@0.75.5",
46
45
  "@radix-ui/react-collapsible": "^1.1.12",
47
46
  "@radix-ui/react-dialog": "^1.1.15",
@@ -75,9 +74,10 @@
75
74
  "use-stick-to-bottom": "^1.1.3",
76
75
  "yaml": "^2.8.3",
77
76
  "zod": "^3.25.76",
78
- "@hachej/boring-ui-kit": "0.1.34"
77
+ "@hachej/boring-ui-kit": "0.1.35"
79
78
  },
80
79
  "devDependencies": {
80
+ "@antithesishq/bombadil": "0.5.0",
81
81
  "@opentelemetry/api": "^1.9.1",
82
82
  "@tailwindcss/postcss": "^4.2.4",
83
83
  "@types/node": "^22.15.3",
@@ -92,7 +92,7 @@
92
92
  "tsx": "^4.19.4",
93
93
  "typescript": "^5.8.3",
94
94
  "vite": "^6.3.3",
95
- "vitest": "^3.1.3"
95
+ "vitest": "^3.2.6"
96
96
  },
97
97
  "peerDependencies": {
98
98
  "react": "^18.0.0 || ^19.0.0",
@@ -117,6 +117,9 @@
117
117
  "dev": "tsx src/server/dev.ts",
118
118
  "test": "vitest run",
119
119
  "test:e2e": "playwright test -c e2e/playwright.config.ts",
120
+ "test:chat-baseline": "node ./scripts/run-chat-baseline.mjs",
121
+ "test:bombadil:chat": "node ./scripts/run-bombadil-chat.mjs",
122
+ "test:bombadil:chat:nightly": "BOMBADIL_TIME_LIMIT=5m node ./scripts/run-bombadil-chat.mjs",
120
123
  "test:regression": "vitest run src/server/__tests__/system-prompt-size.regression.test.ts",
121
124
  "test:watch": "vitest",
122
125
  "eval": "tsx ./scripts/eval.ts",
@@ -1,36 +0,0 @@
1
- // src/shared/tool-ui.ts
2
- function isRecord(value) {
3
- return typeof value === "object" && value !== null;
4
- }
5
- function isToolUiMetadata(value) {
6
- if (!isRecord(value)) return false;
7
- return (value.rendererId === void 0 || typeof value.rendererId === "string") && (value.displayGroup === void 0 || typeof value.displayGroup === "string") && (value.icon === void 0 || typeof value.icon === "string");
8
- }
9
- function extractToolUiMetadata(output) {
10
- if (!isRecord(output)) return void 0;
11
- const details = output.details;
12
- if (!isRecord(details)) return void 0;
13
- if (isToolUiMetadata(details.ui)) return details.ui;
14
- return void 0;
15
- }
16
-
17
- // src/shared/capabilities.ts
18
- var DEFAULT_AGENT_RUNTIME_CAPABILITIES = {
19
- nativeFollowUp: false,
20
- aiSdkOwnsHistory: true
21
- };
22
- var PI_AGENT_RUNTIME_CAPABILITIES = {
23
- nativeFollowUp: true,
24
- aiSdkOwnsHistory: false
25
- };
26
-
27
- // src/shared/agentPluginEvents.ts
28
- var WORKSPACE_AGENT_PLUGINS_RELOADED_EVENT = "boring-ui:agent-plugins-reloaded";
29
-
30
- export {
31
- isToolUiMetadata,
32
- extractToolUiMetadata,
33
- DEFAULT_AGENT_RUNTIME_CAPABILITIES,
34
- PI_AGENT_RUNTIME_CAPABILITIES,
35
- WORKSPACE_AGENT_PLUGINS_RELOADED_EVENT
36
- };
@@ -1,85 +0,0 @@
1
- // src/shared/message-sanitizer.ts
2
- function collapseExactRepeatedText(text) {
3
- const maxCopies = Math.min(20, Math.floor(text.length / 4));
4
- for (let copies = maxCopies; copies >= 2; copies -= 1) {
5
- if (text.length % copies !== 0) continue;
6
- const unit = text.slice(0, text.length / copies);
7
- if (unit.length < 4) continue;
8
- if (unit.repeat(copies) === text) return unit;
9
- }
10
- return text;
11
- }
12
- function textFromMessage(message, role) {
13
- if (message.role !== role) return null;
14
- const text = (message.parts ?? []).map((part) => {
15
- const candidate = part;
16
- return candidate.type === "text" && typeof candidate.text === "string" ? candidate.text : "";
17
- }).join("").trim();
18
- return text || null;
19
- }
20
- function assistantVisibleText(message) {
21
- return textFromMessage(message, "assistant");
22
- }
23
- function userVisibleText(message) {
24
- return textFromMessage(message, "user");
25
- }
26
- function isTransientUserId(id) {
27
- return /^user-\d+$/.test(id);
28
- }
29
- function isEmptyAssistantMessage(message) {
30
- return message.role === "assistant" && (!message.parts || message.parts.length === 0);
31
- }
32
- function sanitizeUiMessage(message) {
33
- if (message.role !== "assistant") return message;
34
- let changed = false;
35
- const parts = [];
36
- for (const part of message.parts ?? []) {
37
- const candidate = part;
38
- if (candidate.type !== "text" || typeof candidate.text !== "string") {
39
- parts.push(part);
40
- continue;
41
- }
42
- const text = collapseExactRepeatedText(candidate.text);
43
- const previous = parts[parts.length - 1];
44
- if (previous?.type === "text" && previous.text === text) {
45
- changed = true;
46
- continue;
47
- }
48
- if (text !== candidate.text) changed = true;
49
- parts.push(text === candidate.text ? part : { ...candidate, text });
50
- }
51
- return changed ? { ...message, parts } : message;
52
- }
53
- function uiMessageContentKey(message) {
54
- const sanitized = sanitizeUiMessage(message);
55
- return `${sanitized.role}:${JSON.stringify(sanitized.parts ?? [])}`;
56
- }
57
- function dropEmptyAssistantUiMessages(messages) {
58
- return messages.filter((message) => !isEmptyAssistantMessage(message));
59
- }
60
- function sanitizeUiMessages(messages, options = {}) {
61
- const deduped = [];
62
- const seenUserText = /* @__PURE__ */ new Set();
63
- for (const rawMessage of messages) {
64
- if (options.dropEmptyAssistantMessages && isEmptyAssistantMessage(rawMessage)) continue;
65
- const message = sanitizeUiMessage(rawMessage);
66
- const userText = userVisibleText(message);
67
- const id = typeof message.id === "string" ? message.id : "";
68
- if (userText) {
69
- if (seenUserText.has(userText) && isTransientUserId(id)) continue;
70
- if (!id.startsWith("pending-user:")) seenUserText.add(userText);
71
- }
72
- const previous = deduped[deduped.length - 1];
73
- const assistantText = assistantVisibleText(message);
74
- if (assistantText && previous && assistantVisibleText(previous) === assistantText) continue;
75
- deduped.push(message);
76
- }
77
- return deduped;
78
- }
79
-
80
- export {
81
- sanitizeUiMessage,
82
- uiMessageContentKey,
83
- dropEmptyAssistantUiMessages,
84
- sanitizeUiMessages
85
- };
@@ -1,88 +0,0 @@
1
- // src/shared/error-codes.ts
2
- import { z } from "zod";
3
- var ErrorCode = z.enum([
4
- // Auth / config
5
- "MISSING_API_KEY",
6
- "INVALID_API_KEY",
7
- "OIDC_REFRESH_FAILED",
8
- "VERCEL_AUTH_FAILED",
9
- "CONFIG_INVALID",
10
- // Workspace / path
11
- "PATH_ESCAPE",
12
- "PATH_ABSOLUTE",
13
- "PATH_NULL_BYTE",
14
- "PATH_SYMLINK_ESCAPE",
15
- "PATH_NOT_FOUND",
16
- "PATH_NOT_WRITABLE",
17
- "WORKSPACE_UNINITIALIZED",
18
- "WORKSPACE_NOT_READY",
19
- // Agent runtime / provisioning
20
- "AGENT_RUNTIME_NOT_READY",
21
- "RUNTIME_PROVISIONING_FAILED",
22
- "RUNTIME_PROVISIONING_LOCKED",
23
- // Sandbox / exec
24
- "BWRAP_UNAVAILABLE",
25
- "BWRAP_TIMEOUT",
26
- "OUTPUT_TRUNCATED",
27
- "SANDBOX_NOT_READY",
28
- "SANDBOX_EXPIRED",
29
- "VERCEL_API_ERROR",
30
- "CIRCUIT_OPEN",
31
- "ABORTED",
32
- // Session / bridge
33
- "SESSION_NOT_FOUND",
34
- "SESSION_LOCKED",
35
- "STREAM_BUFFER_EVICTED",
36
- "CURSOR_OUT_OF_RANGE",
37
- "BRIDGE_COMMAND_INVALID",
38
- // Tool
39
- "TOOL_NOT_FOUND",
40
- "TOOL_INVALID_INPUT",
41
- "TOOL_EXECUTION_ERROR",
42
- // Plugin
43
- "PLUGIN_LOAD_FAILED",
44
- "PLUGIN_NAME_COLLISION",
45
- "PLUGIN_RUNTIME_REVISION_MISMATCH",
46
- "PLUGIN_RUNTIME_PRIVATE_FILE",
47
- "PLUGIN_RUNTIME_UNSAFE_IMPORT",
48
- "PLUGIN_RUNTIME_TRANSFORM_FAILED",
49
- "RUNTIME_PLUGIN_NOT_FOUND",
50
- "RUNTIME_PLUGIN_ROUTE_NOT_FOUND",
51
- "RUNTIME_PLUGIN_HANDLER_FAILED",
52
- "RUNTIME_PLUGIN_LOAD_FAILED",
53
- "RUNTIME_PLUGIN_RESPONSE_UNSUPPORTED",
54
- // Runtime provisioning
55
- "PROVISIONING_LAYOUT_FAILED",
56
- "PROVISIONING_SKILLS_FAILED",
57
- "PROVISIONING_TEMPLATES_FAILED",
58
- "PROVISIONING_NODE_PREFLIGHT_FAILED",
59
- "PROVISIONING_NPM_INSTALL_FAILED",
60
- "PROVISIONING_UV_BOOTSTRAP_FAILED",
61
- "PROVISIONING_UV_INSTALL_FAILED",
62
- "PROVISIONING_ARTIFACT_FAILED",
63
- // Internal
64
- "INTERNAL_ERROR"
65
- ]);
66
- var ERROR_CODES = ErrorCode.options;
67
- var ApiErrorPayloadSchema = z.object({
68
- code: ErrorCode,
69
- message: z.string().min(1),
70
- details: z.record(z.unknown()).optional()
71
- });
72
- var ApiErrorResponseSchema = z.object({
73
- error: ApiErrorPayloadSchema
74
- });
75
- var ErrorLogFieldsSchema = z.object({
76
- level: z.enum(["warn", "error"]),
77
- code: ErrorCode,
78
- prefix: z.string().min(1),
79
- msg: z.string().min(1)
80
- }).catchall(z.unknown());
81
-
82
- export {
83
- ErrorCode,
84
- ERROR_CODES,
85
- ApiErrorPayloadSchema,
86
- ApiErrorResponseSchema,
87
- ErrorLogFieldsSchema
88
- };
@@ -1,153 +0,0 @@
1
- import { UIMessage, UIMessageChunk } from 'ai';
2
-
3
- interface SessionStore {
4
- list(ctx: SessionCtx, options?: SessionListOptions): Promise<SessionSummary[]>;
5
- create(ctx: SessionCtx, init?: {
6
- title?: string;
7
- }): Promise<SessionSummary>;
8
- load(ctx: SessionCtx, sessionId: string): Promise<SessionDetail>;
9
- delete(ctx: SessionCtx, sessionId: string): Promise<void>;
10
- /**
11
- * Persist a snapshot of UI-layer messages so they survive server restarts.
12
- * Called by the client after each completed turn. Optional — stores that
13
- * don't implement it simply don't persist UI messages server-side.
14
- */
15
- saveMessages?(ctx: SessionCtx, sessionId: string, messages: UIMessage[]): Promise<void>;
16
- }
17
- interface SessionCtx {
18
- workspaceId: string;
19
- userId?: string;
20
- }
21
- interface SessionListOptions {
22
- limit?: number;
23
- offset?: number;
24
- }
25
- interface SessionSummary {
26
- id: string;
27
- title: string;
28
- createdAt: string;
29
- updatedAt: string;
30
- turnCount: number;
31
- }
32
- interface SessionDetail extends SessionSummary {
33
- messages: UIMessage[];
34
- }
35
-
36
- interface TelemetrySink {
37
- capture(event: TelemetryEvent): void | Promise<void>;
38
- flush?(): void | Promise<void>;
39
- }
40
- interface TelemetryEvent {
41
- name: string;
42
- distinctId?: string;
43
- properties?: Record<string, unknown>;
44
- }
45
- declare const noopTelemetry: TelemetrySink;
46
- declare function safeCapture(telemetry: TelemetrySink, event: TelemetryEvent): void;
47
-
48
- type JSONSchema = Record<string, unknown>;
49
- type ToolReadinessRequirement = 'workspace-fs' | 'sandbox-exec' | 'ui-bridge' | 'runtime-dependencies' | `runtime:${string}`;
50
- interface AgentTool {
51
- name: string;
52
- description: string;
53
- /** Optional one-line prompt entry. Pi-built tools should preserve pi's snippet verbatim. */
54
- promptSnippet?: string;
55
- readinessRequirements?: ToolReadinessRequirement[];
56
- parameters: JSONSchema;
57
- execute(params: Record<string, unknown>, ctx: ToolExecContext): Promise<ToolResult>;
58
- }
59
- interface ToolExecContext {
60
- abortSignal: AbortSignal;
61
- toolCallId: string;
62
- onUpdate?: (partial: string) => void;
63
- /** Agent chat/session id executing this tool, when known. */
64
- sessionId?: string;
65
- }
66
- interface ToolResult {
67
- content: Array<{
68
- type: 'text';
69
- text: string;
70
- }>;
71
- isError?: boolean;
72
- details?: unknown;
73
- }
74
-
75
- interface AgentHarnessFactoryInput {
76
- tools: AgentTool[];
77
- /** Host/storage cwd used for harness-owned filesystem resources. */
78
- cwd: string;
79
- /** Agent-visible cwd used by Pi/system prompt/session metadata. */
80
- runtimeCwd?: string;
81
- systemPromptAppend?: string;
82
- sessionNamespace?: string;
83
- sessionDir?: string;
84
- /**
85
- * Optional dynamic system-prompt source. Harness calls it whenever it
86
- * builds or rebuilds a session prompt and appends the returned string.
87
- * Workspace plugin layer wires this so live-reloaded plugins can contribute
88
- * prompt context without a workspace-injected harness extension.
89
- */
90
- systemPromptDynamic?: () => string | undefined | Promise<string | undefined>;
91
- /** Host-provided telemetry sink. Optional and best-effort; harnesses may ignore it. */
92
- telemetry?: TelemetrySink;
93
- }
94
- type AgentHarnessFactory = (input: AgentHarnessFactoryInput) => AgentHarness | Promise<AgentHarness>;
95
- interface AgentHarness {
96
- readonly id: string;
97
- readonly placement: 'server' | 'browser';
98
- /** Send a user message. Yields AI SDK UIMessage stream chunks. */
99
- sendMessage(input: SendMessageInput, ctx: RunContext): AsyncIterable<UIMessageChunk>;
100
- /** Session lifecycle; may delegate to an underlying runtime (e.g. pi's JSONL). */
101
- sessions: SessionStore;
102
- /**
103
- * Resolved system prompt currently in effect for `sessionId`. Returns
104
- * `undefined` when the underlying runtime hasn't yet instantiated a
105
- * session (typical pre-first-turn state — pi creates lazily on first
106
- * `sendMessage`). Optional so non-pi harnesses can opt out cleanly.
107
- */
108
- getSystemPrompt?: (sessionId: string) => string | undefined;
109
- /**
110
- * Queue a follow-up message for delivery after the current streaming turn.
111
- * When called while a `sendMessage` stream is active, the harness keeps
112
- * the HTTP stream open after `agent_end` and processes the follow-up as a
113
- * second turn in the same response — no extra round-trip needed.
114
- * A `data-followup-consumed` chunk is emitted before the follow-up turn so
115
- * the client can clear its pending-message bubble immediately.
116
- */
117
- followUp?(sessionId: string, text: string, attachments?: MessageAttachment[], displayText?: string, options?: FollowUpOptions): void | Promise<void>;
118
- /**
119
- * Discard queued follow-up(s) for this session (called by the Stop button or
120
- * by a queued-message delete action). When `options` identifies a single
121
- * client message, implementations should remove only that item if possible.
122
- */
123
- clearFollowUp?(sessionId: string, options?: FollowUpOptions): void;
124
- /** Reload native agent resources/extensions for an existing session. */
125
- reloadSession?: (sessionId: string) => Promise<boolean>;
126
- }
127
- interface FollowUpOptions {
128
- clientNonce?: string;
129
- clientSeq?: number;
130
- }
131
- interface MessageAttachment {
132
- filename?: string;
133
- mediaType?: string;
134
- /** data: URL (base64) or remote URL */
135
- url: string;
136
- }
137
- interface SendMessageInput {
138
- sessionId: string;
139
- message: string;
140
- thinkingLevel?: 'off' | 'low' | 'medium' | 'high';
141
- model?: {
142
- provider: string;
143
- id: string;
144
- };
145
- attachments?: MessageAttachment[];
146
- }
147
- interface RunContext {
148
- abortSignal: AbortSignal;
149
- workdir: string;
150
- userId?: string;
151
- }
152
-
153
- export { type AgentTool as A, type JSONSchema as J, type RunContext as R, type SendMessageInput as S, type TelemetryEvent as T, type AgentHarness as a, type SessionCtx as b, type SessionDetail as c, type SessionStore as d, type SessionSummary as e, type TelemetrySink as f, type ToolExecContext as g, type ToolResult as h, type AgentHarnessFactory as i, type AgentHarnessFactoryInput as j, noopTelemetry as n, safeCapture as s };
@@ -1,20 +0,0 @@
1
- interface ToolUiMetadata {
2
- /** Renderer key registered by the app shell, e.g. `pi-subagent`. */
3
- rendererId?: string;
4
- /** Optional grouping label for future tool timelines/summaries. */
5
- displayGroup?: string;
6
- /** Optional icon hint. Frontends decide whether/how to honor it. */
7
- icon?: string;
8
- /** Renderer-specific structured data. */
9
- details?: unknown;
10
- }
11
- declare function isToolUiMetadata(value: unknown): value is ToolUiMetadata;
12
- /**
13
- * Extract structured UI metadata from a ToolResult-like output.
14
- *
15
- * Shape:
16
- * output.details.ui = { rendererId, displayGroup, icon, details }
17
- */
18
- declare function extractToolUiMetadata(output: unknown): ToolUiMetadata | undefined;
19
-
20
- export { type ToolUiMetadata as T, extractToolUiMetadata as e, isToolUiMetadata as i };