@hachej/boring-agent 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 boringdata
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # @boring/agent
2
+
3
+ Agent runtime and chat UI for boring-ui apps.
4
+
5
+ ```bash
6
+ pnpm add @boring/agent
7
+ ```
8
+
9
+ ---
10
+
11
+ ## What it provides
12
+
13
+ - **Agent runtime** — LLM conversation loop with streaming, tool calling, and three execution modes
14
+ - **Tool catalog** — `bash`, `read`, `write`, `edit`, `grep`, `find`, `ls` and more
15
+ - **Chat UI** — embeddable `ChatPanel` React component
16
+
17
+ ---
18
+
19
+ ## Execution modes
20
+
21
+ | Mode | Isolation | Typical use |
22
+ |---|---|---|
23
+ | `direct` | None | Local dev |
24
+ | `local` | `bwrap` process isolation | Safer Linux deployments |
25
+ | `vercel-sandbox` | Firecracker microVM | Multi-tenant / remote |
26
+
27
+ ---
28
+
29
+ ## Quickstart
30
+
31
+ ```tsx
32
+ import { ChatPanel } from "@boring/agent"
33
+ import { WorkspaceProvider, IdeLayout } from "@boring/workspace"
34
+
35
+ export function App() {
36
+ return (
37
+ <WorkspaceProvider chatPanel={ChatPanel}>
38
+ <IdeLayout />
39
+ </WorkspaceProvider>
40
+ )
41
+ }
42
+ ```
43
+
44
+ Server:
45
+
46
+ ```ts
47
+ import { createAgentApp } from "@boring/agent/server"
48
+
49
+ const app = await createAgentApp({ mode: "local", workspaceRoot: process.cwd() })
50
+ await app.listen({ port: 3001 })
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Model config
56
+
57
+ ```bash
58
+ ANTHROPIC_API_KEY=sk-ant-...
59
+ BORING_AGENT_DEFAULT_MODEL_PROVIDER=anthropic
60
+ BORING_AGENT_DEFAULT_MODEL_ID=claude-sonnet-4-6
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Package surfaces
66
+
67
+ ```ts
68
+ import { ChatPanel } from "@boring/agent" // React chat UI
69
+ import { ... } from "@boring/agent/server" // Node/server entry
70
+ import { ... } from "@boring/agent/front" // Frontend-only
71
+ import { ... } from "@boring/agent/shared" // Platform-agnostic contracts
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Part of [boring-ui](https://github.com/hachej/boring-ui)
77
+
78
+ | Package | Role |
79
+ |---|---|
80
+ | `@boring/core` | DB, auth, app factory |
81
+ | `@boring/workspace` | Plugin system, layouts |
82
+ | `@boring/agent` | Agent runtime + tools |
@@ -0,0 +1,77 @@
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
+ // Sandbox / exec
19
+ "BWRAP_UNAVAILABLE",
20
+ "BWRAP_TIMEOUT",
21
+ "OUTPUT_TRUNCATED",
22
+ "SANDBOX_NOT_READY",
23
+ "SANDBOX_EXPIRED",
24
+ "VERCEL_API_ERROR",
25
+ "CIRCUIT_OPEN",
26
+ "ABORTED",
27
+ // Session / bridge
28
+ "SESSION_NOT_FOUND",
29
+ "SESSION_LOCKED",
30
+ "STREAM_BUFFER_EVICTED",
31
+ "CURSOR_OUT_OF_RANGE",
32
+ "BRIDGE_COMMAND_INVALID",
33
+ // Tool
34
+ "TOOL_NOT_FOUND",
35
+ "TOOL_INVALID_INPUT",
36
+ "TOOL_EXECUTION_ERROR",
37
+ // Plugin
38
+ "PLUGIN_LOAD_FAILED",
39
+ "PLUGIN_NAME_COLLISION",
40
+ // Internal
41
+ "INTERNAL_ERROR"
42
+ ]);
43
+ var ERROR_CODES = ErrorCode.options;
44
+ var ApiErrorPayloadSchema = z.object({
45
+ code: ErrorCode,
46
+ message: z.string().min(1),
47
+ details: z.record(z.unknown()).optional()
48
+ });
49
+ var ApiErrorResponseSchema = z.object({
50
+ error: ApiErrorPayloadSchema
51
+ });
52
+ var ErrorLogFieldsSchema = z.object({
53
+ level: z.enum(["warn", "error"]),
54
+ code: ErrorCode,
55
+ prefix: z.string().min(1),
56
+ msg: z.string().min(1)
57
+ }).catchall(z.unknown());
58
+
59
+ // src/shared/validateTool.ts
60
+ function validateTool(tool) {
61
+ if (typeof tool !== "object" || tool === null) return null;
62
+ const t = tool;
63
+ if (typeof t.name !== "string" || t.name.length === 0) return null;
64
+ if (typeof t.description !== "string") return null;
65
+ if (typeof t.parameters !== "object" || t.parameters === null) return null;
66
+ if (typeof t.execute !== "function") return null;
67
+ return t;
68
+ }
69
+
70
+ export {
71
+ ErrorCode,
72
+ ERROR_CODES,
73
+ ApiErrorPayloadSchema,
74
+ ApiErrorResponseSchema,
75
+ ErrorLogFieldsSchema,
76
+ validateTool
77
+ };
@@ -0,0 +1,241 @@
1
+ import { FastifyInstance } from 'fastify';
2
+
3
+ /**
4
+ * Public types for @hachej/boring-agent/eval — the eval framework.
5
+ *
6
+ * See packages/agent/docs/plans/AGENT_EVAL_FRAMEWORK.md for the design
7
+ * rationale and approved contract.
8
+ */
9
+
10
+ /**
11
+ * A captured tool call from an LLM response. Anthropic / OpenAI both
12
+ * support emitting multiple tool calls in a single response (parallel)
13
+ * or across turns (sequential). The framework collects ALL of them; the
14
+ * matcher then checks for existence (default) or ordering (`expectFirst`).
15
+ */
16
+ interface ToolCall {
17
+ tool: string;
18
+ params: Record<string, unknown>;
19
+ }
20
+ /**
21
+ * Sentinel for "must be present at this key, any value". Use in
22
+ * `expect.params` for fields the LLM generates (uuids, timestamps).
23
+ *
24
+ * In YAML fixtures: `id: !EvalAny`.
25
+ */
26
+ declare const EvalAny: unique symbol;
27
+ type EvalAnyType = typeof EvalAny;
28
+ /**
29
+ * Tagged regex matcher for string params. Use when the value is partially
30
+ * structured (e.g. an id that must start with `chart:`) but the suffix
31
+ * varies between runs.
32
+ *
33
+ * In YAML fixtures: `component: !EvalRegex "^chart:"`.
34
+ */
35
+ interface EvalRegexMatcher {
36
+ __evalRegex: RegExp;
37
+ }
38
+ declare function EvalRegex(re: RegExp | string): EvalRegexMatcher;
39
+ declare function isEvalRegex(value: unknown): value is EvalRegexMatcher;
40
+ /**
41
+ * Expected shape of a tool call. The matcher (matcher.ts) decides if an
42
+ * actual ToolCall satisfies it.
43
+ *
44
+ * - `tool`: exact name match (required).
45
+ * - `params`: partial match by default — every key in `expect.params` must
46
+ * appear in the actual call's params with a matching value. Extra keys
47
+ * in the actual call are allowed unless `strict: true`.
48
+ * - Values inside `params` may be the `EvalAny` sentinel or an
49
+ * `EvalRegexMatcher` wildcard.
50
+ */
51
+ interface ExpectedCall {
52
+ tool: string;
53
+ params?: Record<string, unknown>;
54
+ strict?: boolean;
55
+ }
56
+ /**
57
+ * Per-prompt eval options. See `evalAgentPrompt(opts)` in evalPrompt.ts.
58
+ */
59
+ type EvalModelSelection = string | {
60
+ provider: string;
61
+ id: string;
62
+ };
63
+ interface EvalPromptOptions {
64
+ /** A FastifyInstance from createAgentApp / createWorkspaceAgentApp / etc. */
65
+ app: FastifyInstance;
66
+ /** User prompt sent to the agent. */
67
+ prompt: string;
68
+ /**
69
+ * Expected tool calls. Default: assert each ExpectedCall here matches
70
+ * AT LEAST ONE call in the LLM's response (any order). Mutually
71
+ * exclusive with `expectFirst` and `expectNoToolCall`.
72
+ */
73
+ expect?: ExpectedCall | ExpectedCall[];
74
+ /**
75
+ * Stricter alternative: assert the FIRST tool call matches.
76
+ * Mutually exclusive with `expect` and `expectNoToolCall`.
77
+ */
78
+ expectFirst?: ExpectedCall;
79
+ /**
80
+ * Negative assertion: NO tool was called (LLM answered in plain text).
81
+ * Mutually exclusive with `expect` and `expectFirst`.
82
+ */
83
+ expectNoToolCall?: boolean;
84
+ /**
85
+ * Model id. Defaults to the agent's pinned model (see evalConfig.ts).
86
+ * Suites typically override at the suite level (in YAML).
87
+ */
88
+ model?: EvalModelSelection;
89
+ /** Optional system prompt. */
90
+ systemPrompt?: string;
91
+ /** Override the chat session id (defaults to a fresh uuid). */
92
+ sessionId?: string;
93
+ /** Per-call timeout in ms. Defaults to 30_000. */
94
+ timeoutMs?: number;
95
+ /** Per-prompt retry count. Default: 0. */
96
+ retries?: number;
97
+ }
98
+ interface EvalResult {
99
+ ok: boolean;
100
+ /** All tool calls the LLM made, in order. */
101
+ actual: ToolCall[];
102
+ /** Plain-text response from the LLM. */
103
+ text: string;
104
+ /** Human-readable reason on failure. */
105
+ reason?: string;
106
+ /** Tokens consumed. */
107
+ usage?: {
108
+ input: number;
109
+ output: number;
110
+ };
111
+ /** Number of attempts used (1 + retries on failure). */
112
+ attempts: number;
113
+ }
114
+ /**
115
+ * Suite runner options. See `runEvalSuite(opts)` in runSuite.ts.
116
+ */
117
+ interface SuiteOptions {
118
+ app: FastifyInstance;
119
+ /** Path to a YAML fixture file. See AGENT_EVAL_FRAMEWORK.md for format. */
120
+ fixturesPath: string;
121
+ /** Stop on first failure. Default: false. */
122
+ bail?: boolean;
123
+ /** Concurrency. Default: 4. */
124
+ concurrency?: number;
125
+ /** Suite-level timeout in ms. Default: 5 * 60_000 (5 minutes). */
126
+ suiteTimeoutMs?: number;
127
+ /** Override per-prompt model. Useful for ad-hoc model comparison runs. */
128
+ model?: EvalModelSelection;
129
+ }
130
+ interface SuiteFixturePrompt {
131
+ prompt: string;
132
+ expect?: ExpectedCall | ExpectedCall[];
133
+ expectFirst?: ExpectedCall;
134
+ expectNoToolCall?: boolean;
135
+ retries?: number;
136
+ timeoutMs?: number;
137
+ model?: EvalModelSelection;
138
+ }
139
+ interface SuiteFixture {
140
+ /** Pinned model for the whole suite (each prompt may override). */
141
+ model?: EvalModelSelection;
142
+ /** System prompt prepended to every prompt in this suite. */
143
+ systemPrompt?: string;
144
+ /** Defaults applied to each prompt unless inline-overridden. */
145
+ defaults?: Pick<SuiteFixturePrompt, "retries" | "timeoutMs" | "model">;
146
+ prompts: SuiteFixturePrompt[];
147
+ }
148
+ interface SuiteReport {
149
+ total: number;
150
+ passed: number;
151
+ failed: number;
152
+ passRate: number;
153
+ results: Array<EvalResult & {
154
+ prompt: string;
155
+ expected: SuiteFixturePrompt;
156
+ }>;
157
+ totalUsage: {
158
+ input: number;
159
+ output: number;
160
+ };
161
+ totalDurationMs: number;
162
+ /** True iff every result is ok: true. */
163
+ allPassed: boolean;
164
+ }
165
+
166
+ declare function evalAgentPrompt(opts: EvalPromptOptions): Promise<EvalResult>;
167
+
168
+ declare function runEvalSuite(opts: SuiteOptions): Promise<SuiteReport>;
169
+
170
+ /**
171
+ * Parse a YAML fixture file (text contents) into a `SuiteFixture`.
172
+ *
173
+ * Delegates to the `yaml` package with the custom-tag schema so the
174
+ * matcher wildcards (`!EvalAny`, `!EvalRegex`) resolve into runtime
175
+ * values matcher.ts can consume.
176
+ *
177
+ * Throws on invalid YAML syntax. Returns whatever shape parses — minimal
178
+ * runtime validation here (caller asserts with the SuiteFixture type).
179
+ */
180
+ declare function parseFixtureYaml(text: string): SuiteFixture;
181
+
182
+ /**
183
+ * Pure matcher logic for the eval framework. No LLM, no fetch, no
184
+ * Fastify — exercised in unit tests against fixed ToolCall[] inputs.
185
+ *
186
+ * The matcher implements three modes:
187
+ * 1. someCallMatches (default `expect`) — every ExpectedCall must match
188
+ * at least one actual ToolCall, any order. Captures parallel /
189
+ * sequential tool calling without assuming an order the LLM didn't
190
+ * actually commit to.
191
+ * 2. firstCallMatches (`expectFirst`) — actual[0] must match the
192
+ * ExpectedCall. Use only when ordering is part of the contract.
193
+ * 3. noToolCall (`expectNoToolCall`) — actual must be empty. The LLM
194
+ * answered in plain text without tooling.
195
+ *
196
+ * Param matching is partial by default: every key in expect.params must
197
+ * appear in actual.params with a matching value, but extra keys in
198
+ * actual are ignored. Set `strict: true` to require exact equality (no
199
+ * extra keys allowed).
200
+ */
201
+
202
+ interface MatchOutcome {
203
+ ok: boolean;
204
+ /** Index into `actual[]` that satisfied the expected call. -1 on failure. */
205
+ matchedIndex: number;
206
+ /** Diagnostic when ok is false. */
207
+ reason?: string;
208
+ }
209
+ /**
210
+ * Does ONE actual call satisfy ONE expected call? Used as the inner
211
+ * predicate by all three modes.
212
+ */
213
+ declare function callSatisfies(expected: ExpectedCall, actual: ToolCall): MatchOutcome;
214
+ /**
215
+ * "Does at least one of `actual` satisfy each entry in `expected`?"
216
+ * Returns the index of the first match for each expected entry.
217
+ */
218
+ declare function someCallMatches(expected: ExpectedCall | ExpectedCall[], actual: ToolCall[]): MatchOutcome;
219
+ declare function firstCallMatches(expected: ExpectedCall, actual: ToolCall[]): MatchOutcome;
220
+ declare function noToolCallMatches(actual: ToolCall[]): MatchOutcome;
221
+
222
+ /**
223
+ * Default model for the eval framework.
224
+ *
225
+ * Pinned deliberately so model deprecation is a visible PR (changelog
226
+ * entry + intentional bump), not a transparent update. Per-suite YAML
227
+ * fixtures override this at the suite level so consumers upgrade at
228
+ * their own pace.
229
+ */
230
+ declare const DEFAULT_EVAL_MODEL: {
231
+ readonly provider: "openrouter";
232
+ readonly id: "qwen/qwen3.6-plus";
233
+ };
234
+ /** Default per-call timeout in ms. */
235
+ declare const DEFAULT_TIMEOUT_MS = 30000;
236
+ /** Default suite-level timeout in ms (5 minutes). */
237
+ declare const DEFAULT_SUITE_TIMEOUT_MS: number;
238
+ /** Default concurrency for runEvalSuite. */
239
+ declare const DEFAULT_CONCURRENCY = 4;
240
+
241
+ export { DEFAULT_CONCURRENCY, DEFAULT_EVAL_MODEL, DEFAULT_SUITE_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, EvalAny, type EvalAnyType, type EvalModelSelection, type EvalPromptOptions, EvalRegex, type EvalRegexMatcher, type EvalResult, type ExpectedCall, type MatchOutcome, type SuiteFixture, type SuiteFixturePrompt, type SuiteOptions, type SuiteReport, type ToolCall, callSatisfies, evalAgentPrompt, firstCallMatches, isEvalRegex, noToolCallMatches, parseFixtureYaml, runEvalSuite, someCallMatches };