@brutal-ai/sdk 0.0.0-stage → 0.2.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.
Files changed (4) hide show
  1. package/README.md +2 -2
  2. package/index.d.ts +106 -0
  3. package/index.js +163 -0
  4. package/package.json +24 -4
package/README.md CHANGED
@@ -1,3 +1,3 @@
1
- # Temporary Holding Version
1
+ # @brutal-ai/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Brutal's TypeScript SDK: the report_feedback tool and a recorder of recent tool calls. See [Brutal](https://github.com/cldixon/brutal).
package/index.d.ts ADDED
@@ -0,0 +1,106 @@
1
+ import { type Context, type ReportInput, type ToolCall, toolDefinition } from "@brutal-ai/schema";
2
+ /**
3
+ * The TypeScript SDK. Give the agent the `report_feedback` tool, wrap its other tools so their
4
+ * recent calls are recorded, and every report the agent files arrives with that context attached.
5
+ * The agent supplies only its judgment. Delivery never slows or fails the agent's turn.
6
+ *
7
+ * const brutal = createBrutal({ endpoint, ingestKey, context: () => ({ app_version }) });
8
+ * const convo = brutal.conversation(conversationId);
9
+ * const search = convo.wrap("search_parts", searchParts);
10
+ * tools: [brutal.anthropicTool(), …]; on a report_feedback call: await convo.handle(input)
11
+ */
12
+ export interface BrutalOptions {
13
+ /** The Brutal Worker's URL, e.g. https://brutal.example.workers.dev. */
14
+ endpoint: string;
15
+ /** An ingest key: it can only submit reports. Keep it on the server. */
16
+ ingestKey: string;
17
+ /** Context added to every report: app version, branch, environment, model, agent name. */
18
+ context?: () => Partial<Context>;
19
+ /** Last chance to scrub a report before it leaves the app; return null to drop it. */
20
+ redact?: (report: ReportInput) => ReportInput | null;
21
+ /** How many recent tool calls to keep per conversation (default 20). */
22
+ maxRecentCalls?: number;
23
+ /** Characters kept of each recorded call's args and result (default 2000). */
24
+ maxFieldChars?: number;
25
+ /** How long a report may take before it is given up on (default 3000 ms). */
26
+ timeoutMs?: number;
27
+ /** Told when a report could not be delivered. Never thrown to the agent. */
28
+ onError?: (error: unknown) => void;
29
+ fetch?: typeof fetch;
30
+ }
31
+ export declare function createBrutal(options: BrutalOptions): {
32
+ report: (input: ReportInput) => Promise<boolean>;
33
+ conversation: (conversationId: string, extra?: Partial<Context>) => {
34
+ /** Records one tool call by hand. */
35
+ record: (call: ToolCall) => void;
36
+ /** Wraps a tool so each call is recorded with its args, result or error, and latency. */
37
+ wrap<A extends unknown[], R>(name: string, fn: (...args: A) => R | Promise<R>): (...args: A) => Promise<R>;
38
+ /** The recorded calls, oldest first. */
39
+ recent: () => ToolCall[];
40
+ /**
41
+ * Handles the agent's `report_feedback` call: validates its input, attaches the context
42
+ * and recent tool calls, sends it, and returns the line to give back to the agent. A bad
43
+ * input is still sent (as `other`), so the report is not lost to a model's mistake.
44
+ */
45
+ handle(input: unknown): Promise<string>;
46
+ };
47
+ /** The canonical tool: name, description, JSON Schema. */
48
+ definition: typeof toolDefinition;
49
+ /** The tool for the Anthropic Messages API (`tools: [...]`). */
50
+ anthropicTool(): {
51
+ name: string;
52
+ description: string;
53
+ input_schema: import("zod/v4/core").ZodStandardJSONSchemaPayload<import("zod").ZodObject<{
54
+ kind: import("zod").ZodEnum<{
55
+ bug: "bug";
56
+ friction: "friction";
57
+ missing_capability: "missing_capability";
58
+ confusing_output: "confusing_output";
59
+ latency: "latency";
60
+ eval_candidate: "eval_candidate";
61
+ other: "other";
62
+ }>;
63
+ title: import("zod").ZodString;
64
+ summary: import("zod").ZodString;
65
+ details: import("zod").ZodOptional<import("zod").ZodString>;
66
+ tool_name: import("zod").ZodOptional<import("zod").ZodString>;
67
+ impact: import("zod").ZodOptional<import("zod").ZodEnum<{
68
+ blocked: "blocked";
69
+ worked_around: "worked_around";
70
+ minor: "minor";
71
+ }>>;
72
+ user_impact: import("zod").ZodOptional<import("zod").ZodString>;
73
+ }, import("zod/v4/core").$strip>>;
74
+ };
75
+ /**
76
+ * The tool for the Vercel AI SDK (`tools: { report_feedback: … }`): a zod schema, which the
77
+ * AI SDK takes as is, and an `execute` bound to one conversation.
78
+ */
79
+ aiSdkTool(conversationId: string, extra?: Partial<Context>): {
80
+ description: string;
81
+ inputSchema: import("zod").ZodObject<{
82
+ kind: import("zod").ZodEnum<{
83
+ bug: "bug";
84
+ friction: "friction";
85
+ missing_capability: "missing_capability";
86
+ confusing_output: "confusing_output";
87
+ latency: "latency";
88
+ eval_candidate: "eval_candidate";
89
+ other: "other";
90
+ }>;
91
+ title: import("zod").ZodString;
92
+ summary: import("zod").ZodString;
93
+ details: import("zod").ZodOptional<import("zod").ZodString>;
94
+ tool_name: import("zod").ZodOptional<import("zod").ZodString>;
95
+ impact: import("zod").ZodOptional<import("zod").ZodEnum<{
96
+ blocked: "blocked";
97
+ worked_around: "worked_around";
98
+ minor: "minor";
99
+ }>>;
100
+ user_impact: import("zod").ZodOptional<import("zod").ZodString>;
101
+ }, import("zod/v4/core").$strip>;
102
+ execute: (input: unknown) => Promise<string>;
103
+ };
104
+ toolName: string;
105
+ };
106
+ export type Brutal = ReturnType<typeof createBrutal>;
package/index.js ADDED
@@ -0,0 +1,163 @@
1
+ // packages/sdk/src/index.ts
2
+ import {
3
+ AgentInput,
4
+ TOOL_ACK,
5
+ TOOL_DESCRIPTION,
6
+ TOOL_NAME,
7
+ toolDefinition
8
+ } from "@brutal-ai/schema";
9
+ var MAX_CONVERSATIONS = 1000;
10
+ var SDK_VERSION = "0.1.0";
11
+ function createBrutal(options) {
12
+ const maxCalls = options.maxRecentCalls ?? 20;
13
+ const maxChars = options.maxFieldChars ?? 2000;
14
+ const timeoutMs = options.timeoutMs ?? 3000;
15
+ const doFetch = options.fetch ?? fetch;
16
+ const endpoint = options.endpoint.replace(/\/+$/, "");
17
+ const buffers = new Map;
18
+ const clip = (value) => {
19
+ if (value === undefined)
20
+ return;
21
+ const text = typeof value === "string" ? value : safeJson(value);
22
+ return text.length > maxChars ? `${text.slice(0, maxChars)}… (${text.length} chars)` : value;
23
+ };
24
+ function bufferFor(conversationId) {
25
+ let buffer = buffers.get(conversationId);
26
+ if (buffer) {
27
+ buffers.delete(conversationId);
28
+ } else {
29
+ buffer = [];
30
+ if (buffers.size >= MAX_CONVERSATIONS) {
31
+ const oldest = buffers.keys().next().value;
32
+ if (oldest !== undefined)
33
+ buffers.delete(oldest);
34
+ }
35
+ }
36
+ buffers.set(conversationId, buffer);
37
+ return buffer;
38
+ }
39
+ function record(conversationId, call) {
40
+ const buffer = bufferFor(conversationId);
41
+ buffer.push({
42
+ ...call,
43
+ args: clip(call.args),
44
+ result: clip(call.result),
45
+ error: call.error?.slice(0, maxChars),
46
+ at: call.at ?? new Date().toISOString()
47
+ });
48
+ if (buffer.length > maxCalls)
49
+ buffer.splice(0, buffer.length - maxCalls);
50
+ }
51
+ async function report(input) {
52
+ try {
53
+ const shaped = options.redact ? options.redact(input) : input;
54
+ if (!shaped)
55
+ return false;
56
+ const body = JSON.stringify(shaped);
57
+ const deadline = AbortSignal.timeout(timeoutMs);
58
+ for (let attempt = 0;; attempt++) {
59
+ let res;
60
+ try {
61
+ res = await doFetch(`${endpoint}/v1/reports`, {
62
+ method: "POST",
63
+ headers: {
64
+ authorization: `Bearer ${options.ingestKey}`,
65
+ "content-type": "application/json"
66
+ },
67
+ body,
68
+ signal: deadline
69
+ });
70
+ } catch (error) {
71
+ if (attempt >= 1 || deadline.aborted)
72
+ throw error;
73
+ continue;
74
+ }
75
+ if (res.ok)
76
+ return true;
77
+ if (res.status < 500 || attempt >= 1) {
78
+ throw new Error(`brutal: ${res.status} ${await res.text()}`);
79
+ }
80
+ }
81
+ } catch (error) {
82
+ options.onError?.(error);
83
+ return false;
84
+ }
85
+ }
86
+ function conversation(conversationId, extra = {}) {
87
+ return {
88
+ record: (call) => record(conversationId, call),
89
+ wrap(name, fn) {
90
+ return async (...args) => {
91
+ const started = performance.now();
92
+ const shownArgs = args.length === 1 ? args[0] : args;
93
+ try {
94
+ const result = await fn(...args);
95
+ record(conversationId, {
96
+ name,
97
+ args: shownArgs,
98
+ status: "ok",
99
+ latency_ms: Math.round(performance.now() - started),
100
+ result
101
+ });
102
+ return result;
103
+ } catch (error) {
104
+ record(conversationId, {
105
+ name,
106
+ args: shownArgs,
107
+ status: "error",
108
+ latency_ms: Math.round(performance.now() - started),
109
+ error: error instanceof Error ? error.message : String(error)
110
+ });
111
+ throw error;
112
+ }
113
+ };
114
+ },
115
+ recent: () => [...buffers.get(conversationId) ?? []],
116
+ async handle(input) {
117
+ const parsed = AgentInput.safeParse(input);
118
+ const agent = parsed.success ? parsed.data : {
119
+ kind: "other",
120
+ title: "Unparsed report_feedback call",
121
+ summary: "The agent's report did not match the tool's schema.",
122
+ details: safeJson(input).slice(0, 1e4)
123
+ };
124
+ await report({
125
+ ...agent,
126
+ context: {
127
+ ...options.context?.(),
128
+ ...extra,
129
+ conversation_id: conversationId,
130
+ recent_tool_calls: [...buffers.get(conversationId) ?? []],
131
+ sdk: "@brutal-ai/sdk",
132
+ sdk_version: SDK_VERSION
133
+ }
134
+ });
135
+ return TOOL_ACK;
136
+ }
137
+ };
138
+ }
139
+ return {
140
+ report,
141
+ conversation,
142
+ definition: toolDefinition,
143
+ anthropicTool() {
144
+ const { name, description, input_schema } = toolDefinition();
145
+ return { name, description, input_schema };
146
+ },
147
+ aiSdkTool(conversationId, extra = {}) {
148
+ const convo = conversation(conversationId, extra);
149
+ return { description: TOOL_DESCRIPTION, inputSchema: AgentInput, execute: convo.handle };
150
+ },
151
+ toolName: TOOL_NAME
152
+ };
153
+ }
154
+ function safeJson(value) {
155
+ try {
156
+ return JSON.stringify(value) ?? String(value);
157
+ } catch {
158
+ return String(value);
159
+ }
160
+ }
161
+ export {
162
+ createBrutal
163
+ };
package/package.json CHANGED
@@ -1,6 +1,26 @@
1
1
  {
2
+ "version": "0.2.0",
3
+ "license": "MIT",
2
4
  "name": "@brutal-ai/sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
5
+ "description": "Brutal's TypeScript SDK: the report_feedback tool and a recorder of recent tool calls",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/cldixon/brutal.git",
10
+ "directory": "packages/sdk"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./index.d.ts",
15
+ "default": "./index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "index.js",
20
+ "index.d.ts",
21
+ "README.md"
22
+ ],
23
+ "dependencies": {
24
+ "@brutal-ai/schema": "0.2.0"
25
+ }
26
+ }