@brutal-ai/sdk 0.0.0-stage → 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/README.md +2 -2
- package/index.d.ts +106 -0
- package/index.js +163 -0
- package/package.json +24 -4
package/README.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @brutal-ai/sdk
|
|
2
2
|
|
|
3
|
-
|
|
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.1.0",
|
|
3
|
+
"license": "MIT",
|
|
2
4
|
"name": "@brutal-ai/sdk",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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.1.0"
|
|
25
|
+
}
|
|
26
|
+
}
|