@punktechnologies/sdk 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 +162 -0
- package/README.md +169 -0
- package/dist/index.d.ts +334 -0
- package/dist/index.js +1058 -0
- package/dist/types.d.ts +346 -0
- package/package.json +64 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var PUNK_CHORUS_MODEL = "punk/chorus";
|
|
3
|
+
var PUNK_MODEL = PUNK_CHORUS_MODEL;
|
|
4
|
+
var DEFAULT_BASE_URL = "http://localhost:4100";
|
|
5
|
+
var MAX_TOOL_CACHE_TTL_SECONDS = 24 * 60 * 60;
|
|
6
|
+
function stringifyToolArguments(value) {
|
|
7
|
+
if (typeof value === "string")
|
|
8
|
+
return value;
|
|
9
|
+
try {
|
|
10
|
+
return JSON.stringify(value ?? {}) ?? "{}";
|
|
11
|
+
} catch {
|
|
12
|
+
return "{}";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function normalizeChatToolCalls(value) {
|
|
16
|
+
if (!Array.isArray(value))
|
|
17
|
+
return [];
|
|
18
|
+
return value.map((tc, index) => {
|
|
19
|
+
const name = tc?.function?.name;
|
|
20
|
+
if (typeof name !== "string" || name.length === 0)
|
|
21
|
+
return null;
|
|
22
|
+
return {
|
|
23
|
+
id: typeof tc?.id === "string" && tc.id.length > 0 ? tc.id : `call_${index}`,
|
|
24
|
+
type: "function",
|
|
25
|
+
function: {
|
|
26
|
+
name,
|
|
27
|
+
arguments: stringifyToolArguments(tc.function.arguments)
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}).filter((tc) => tc !== null);
|
|
31
|
+
}
|
|
32
|
+
function malformedChatResponse(method, path, reason) {
|
|
33
|
+
return new Error(`Punk API ${method} ${path} returned malformed chat response: ${reason}`);
|
|
34
|
+
}
|
|
35
|
+
function malformedApiResponse(method, path, reason) {
|
|
36
|
+
return new Error(`Punk API ${method} ${path} returned malformed response: ${reason}`);
|
|
37
|
+
}
|
|
38
|
+
function isRecord(value) {
|
|
39
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
function requireArrayProperty(value, key, method, path) {
|
|
42
|
+
if (!isRecord(value) || !Array.isArray(value[key])) {
|
|
43
|
+
throw malformedApiResponse(method, path, `missing ${key} array`);
|
|
44
|
+
}
|
|
45
|
+
return value[key];
|
|
46
|
+
}
|
|
47
|
+
function requireObjectProperty(value, key, method, path) {
|
|
48
|
+
if (!isRecord(value) || !isRecord(value[key])) {
|
|
49
|
+
throw malformedApiResponse(method, path, `missing ${key} object`);
|
|
50
|
+
}
|
|
51
|
+
return value[key];
|
|
52
|
+
}
|
|
53
|
+
function requireWebFetchResult(value, method, path) {
|
|
54
|
+
if (!isRecord(value))
|
|
55
|
+
throw malformedApiResponse(method, path, "missing response object");
|
|
56
|
+
if (!isRecord(value.som))
|
|
57
|
+
throw malformedApiResponse(method, path, "missing som object");
|
|
58
|
+
if (typeof value.source !== "string" || value.source.length === 0) {
|
|
59
|
+
throw malformedApiResponse(method, path, "missing source string");
|
|
60
|
+
}
|
|
61
|
+
if (typeof value.url !== "string" || value.url.length === 0) {
|
|
62
|
+
throw malformedApiResponse(method, path, "missing url string");
|
|
63
|
+
}
|
|
64
|
+
if (typeof value.requestedUrl !== "string" || value.requestedUrl.length === 0) {
|
|
65
|
+
throw malformedApiResponse(method, path, "missing requestedUrl string");
|
|
66
|
+
}
|
|
67
|
+
if (typeof value.cached !== "boolean") {
|
|
68
|
+
throw malformedApiResponse(method, path, "missing cached boolean");
|
|
69
|
+
}
|
|
70
|
+
if (typeof value.context !== "string") {
|
|
71
|
+
throw malformedApiResponse(method, path, "missing context string");
|
|
72
|
+
}
|
|
73
|
+
for (const key of ["htmlBytes", "somBytes", "tokensSavedEstimate"]) {
|
|
74
|
+
if (typeof value[key] !== "number" || !Number.isFinite(value[key])) {
|
|
75
|
+
throw malformedApiResponse(method, path, `missing finite ${key}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function requireWebSessionOpenResult(value, method, path) {
|
|
81
|
+
if (!isRecord(value))
|
|
82
|
+
throw malformedApiResponse(method, path, "missing response object");
|
|
83
|
+
if (typeof value.sessionId !== "string" || value.sessionId.length === 0) {
|
|
84
|
+
throw malformedApiResponse(method, path, "missing sessionId string");
|
|
85
|
+
}
|
|
86
|
+
if (!isRecord(value.som))
|
|
87
|
+
throw malformedApiResponse(method, path, "missing som object");
|
|
88
|
+
if (typeof value.source !== "string" || value.source.length === 0) {
|
|
89
|
+
throw malformedApiResponse(method, path, "missing source string");
|
|
90
|
+
}
|
|
91
|
+
if (typeof value.context !== "string") {
|
|
92
|
+
throw malformedApiResponse(method, path, "missing context string");
|
|
93
|
+
}
|
|
94
|
+
const session = requireWebSessionSummary(value.session, method, path, "session");
|
|
95
|
+
if (session.id !== value.sessionId) {
|
|
96
|
+
throw malformedApiResponse(method, path, "session.id does not match sessionId");
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
function requireWebActResult(value, method, path, expectedSessionId) {
|
|
101
|
+
if (!isRecord(value))
|
|
102
|
+
throw malformedApiResponse(method, path, "missing response object");
|
|
103
|
+
if (!isRecord(value.result))
|
|
104
|
+
throw malformedApiResponse(method, path, "missing result object");
|
|
105
|
+
requireWebActionResult(value.result, method, path);
|
|
106
|
+
if (!isRecord(value.som))
|
|
107
|
+
throw malformedApiResponse(method, path, "missing som object");
|
|
108
|
+
if (value.diff !== undefined && !isRecord(value.diff)) {
|
|
109
|
+
throw malformedApiResponse(method, path, "diff must be an object when present");
|
|
110
|
+
}
|
|
111
|
+
const session = requireWebSessionSummary(value.session, method, path, "session");
|
|
112
|
+
if (typeof value.context !== "string") {
|
|
113
|
+
throw malformedApiResponse(method, path, "missing context string");
|
|
114
|
+
}
|
|
115
|
+
if (expectedSessionId !== undefined && session.id !== expectedSessionId) {
|
|
116
|
+
throw malformedApiResponse(method, path, "session.id does not match requested session");
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
function requireWebActionResult(value, method, path) {
|
|
121
|
+
if (typeof value.ok !== "boolean") {
|
|
122
|
+
throw malformedApiResponse(method, path, "result.ok must be boolean");
|
|
123
|
+
}
|
|
124
|
+
if (typeof value.action !== "string" || !["click", "type", "select", "submit"].includes(value.action)) {
|
|
125
|
+
throw malformedApiResponse(method, path, "result.action must be click|type|select|submit");
|
|
126
|
+
}
|
|
127
|
+
if (typeof value.target !== "string" || value.target.length === 0) {
|
|
128
|
+
throw malformedApiResponse(method, path, "result.target must be a non-empty string");
|
|
129
|
+
}
|
|
130
|
+
if (typeof value.url !== "string" || value.url.length === 0) {
|
|
131
|
+
throw malformedApiResponse(method, path, "result.url must be a non-empty string");
|
|
132
|
+
}
|
|
133
|
+
if (value.resolved !== undefined && typeof value.resolved !== "string") {
|
|
134
|
+
throw malformedApiResponse(method, path, "result.resolved must be a string when present");
|
|
135
|
+
}
|
|
136
|
+
if (value.navigated !== undefined && typeof value.navigated !== "boolean") {
|
|
137
|
+
throw malformedApiResponse(method, path, "result.navigated must be boolean when present");
|
|
138
|
+
}
|
|
139
|
+
if (value.requestedUrl !== undefined && typeof value.requestedUrl !== "string") {
|
|
140
|
+
throw malformedApiResponse(method, path, "result.requestedUrl must be a string when present");
|
|
141
|
+
}
|
|
142
|
+
if (value.error !== undefined && typeof value.error !== "string") {
|
|
143
|
+
throw malformedApiResponse(method, path, "result.error must be a string when present");
|
|
144
|
+
}
|
|
145
|
+
if (value.posted !== undefined) {
|
|
146
|
+
if (!isRecord(value.posted)) {
|
|
147
|
+
throw malformedApiResponse(method, path, "result.posted must be an object when present");
|
|
148
|
+
}
|
|
149
|
+
for (const [key, postedValue] of Object.entries(value.posted)) {
|
|
150
|
+
if (typeof postedValue !== "string") {
|
|
151
|
+
throw malformedApiResponse(method, path, `result.posted.${key} must be a string`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function requireWebSessionSummary(value, method, path, label) {
|
|
157
|
+
if (!isRecord(value))
|
|
158
|
+
throw malformedApiResponse(method, path, `missing ${label} object`);
|
|
159
|
+
if (typeof value.id !== "string" || value.id.length === 0) {
|
|
160
|
+
throw malformedApiResponse(method, path, `${label}.id must be a non-empty string`);
|
|
161
|
+
}
|
|
162
|
+
if (typeof value.url !== "string" || value.url.length === 0) {
|
|
163
|
+
throw malformedApiResponse(method, path, `${label}.url must be a non-empty string`);
|
|
164
|
+
}
|
|
165
|
+
if (typeof value.source !== "string" || value.source.length === 0) {
|
|
166
|
+
throw malformedApiResponse(method, path, `${label}.source must be a non-empty string`);
|
|
167
|
+
}
|
|
168
|
+
if (typeof value.openedAt !== "number" || !Number.isFinite(value.openedAt)) {
|
|
169
|
+
throw malformedApiResponse(method, path, `${label}.openedAt must be finite`);
|
|
170
|
+
}
|
|
171
|
+
if (typeof value.ageMs !== "number" || !Number.isFinite(value.ageMs) || value.ageMs < 0) {
|
|
172
|
+
throw malformedApiResponse(method, path, `${label}.ageMs must be a non-negative finite number`);
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
function requireWebSessionsListResult(value, method, path) {
|
|
177
|
+
const sessions = requireArrayProperty(value, "sessions", method, path);
|
|
178
|
+
for (let i = 0;i < sessions.length; i++) {
|
|
179
|
+
requireWebSessionSummary(sessions[i], method, path, `sessions[${i}]`);
|
|
180
|
+
}
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
function requireOkResult(value, method, path) {
|
|
184
|
+
if (!isRecord(value) || value.ok !== true) {
|
|
185
|
+
throw malformedApiResponse(method, path, "missing ok true");
|
|
186
|
+
}
|
|
187
|
+
return { ok: true };
|
|
188
|
+
}
|
|
189
|
+
function requireNonNegativeIntegerProperty(value, key, method, path, label = key) {
|
|
190
|
+
const raw = value[key];
|
|
191
|
+
if (typeof raw !== "number" || !Number.isSafeInteger(raw) || raw < 0) {
|
|
192
|
+
throw malformedApiResponse(method, path, `${label} must be a non-negative integer`);
|
|
193
|
+
}
|
|
194
|
+
return raw;
|
|
195
|
+
}
|
|
196
|
+
function requireNonEmptyStringProperty(value, key, method, path, label = key) {
|
|
197
|
+
const raw = value[key];
|
|
198
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
199
|
+
throw malformedApiResponse(method, path, `${label} must be a non-empty string`);
|
|
200
|
+
}
|
|
201
|
+
return raw;
|
|
202
|
+
}
|
|
203
|
+
function requireFiniteNonNegativeNumberProperty(value, key, method, path, label = key) {
|
|
204
|
+
const raw = value[key];
|
|
205
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) {
|
|
206
|
+
throw malformedApiResponse(method, path, `${label} must be a non-negative finite number`);
|
|
207
|
+
}
|
|
208
|
+
return raw;
|
|
209
|
+
}
|
|
210
|
+
function requireOptionalFiniteNonNegativeNumberProperty(value, key, method, path, label = key) {
|
|
211
|
+
if (value[key] === undefined)
|
|
212
|
+
return;
|
|
213
|
+
requireFiniteNonNegativeNumberProperty(value, key, method, path, label);
|
|
214
|
+
}
|
|
215
|
+
function requireUnitRateProperty(value, key, method, path, label = key) {
|
|
216
|
+
const raw = requireFiniteNonNegativeNumberProperty(value, key, method, path, label);
|
|
217
|
+
if (raw > 1) {
|
|
218
|
+
throw malformedApiResponse(method, path, `${label} must be between 0 and 1`);
|
|
219
|
+
}
|
|
220
|
+
return raw;
|
|
221
|
+
}
|
|
222
|
+
function requireSavingsSummary(value, method, path) {
|
|
223
|
+
if (!isRecord(value))
|
|
224
|
+
throw malformedApiResponse(method, path, "missing response object");
|
|
225
|
+
requireNonEmptyStringProperty(value, "tenantId", method, path);
|
|
226
|
+
for (const key of ["totalRuns", "liveRuns", "optimizedRuns", "blockedRuns", "cacheHits", "artifactHits", "somTokensSaved"]) {
|
|
227
|
+
requireNonNegativeIntegerProperty(value, key, method, path);
|
|
228
|
+
}
|
|
229
|
+
for (const key of ["totalCostUsd", "totalSavedUsd", "ghostSavedUsd", "totalSavedMs"]) {
|
|
230
|
+
requireFiniteNonNegativeNumberProperty(value, key, method, path);
|
|
231
|
+
}
|
|
232
|
+
requireUnitRateProperty(value, "cacheHitRate", method, path);
|
|
233
|
+
requireUnitRateProperty(value, "artifactHitRate", method, path);
|
|
234
|
+
const totalRuns = value.totalRuns;
|
|
235
|
+
const accountedRuns = value.liveRuns + value.optimizedRuns + value.blockedRuns;
|
|
236
|
+
if (accountedRuns !== totalRuns) {
|
|
237
|
+
throw malformedApiResponse(method, path, "run counters must add up to totalRuns");
|
|
238
|
+
}
|
|
239
|
+
if (value.cacheHits > value.optimizedRuns) {
|
|
240
|
+
throw malformedApiResponse(method, path, "cacheHits cannot exceed optimizedRuns");
|
|
241
|
+
}
|
|
242
|
+
if (value.artifactHits > value.optimizedRuns) {
|
|
243
|
+
throw malformedApiResponse(method, path, "artifactHits cannot exceed optimizedRuns");
|
|
244
|
+
}
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
247
|
+
var RUN_STATUSES = new Set(["running", "completed", "failed", "blocked"]);
|
|
248
|
+
var ARTIFACT_STATES = new Set(["candidate", "replay_failed", "replay_passed", "shadowing", "shadow_failed", "approved", "promoted", "canary", "stable", "degraded", "quarantined", "retired"]);
|
|
249
|
+
var ARTIFACT_TYPES = new Set(["static_response", "template", "deterministic_transform", "ruleset_classifier", "tool_plan", "web_extraction", "hybrid"]);
|
|
250
|
+
function requireRunRecord(value, method, path, label = "run") {
|
|
251
|
+
if (!isRecord(value))
|
|
252
|
+
throw malformedApiResponse(method, path, `missing ${label} object`);
|
|
253
|
+
for (const key of ["id", "tenantId", "appId", "requestHash"]) {
|
|
254
|
+
requireNonEmptyStringProperty(value, key, method, path, `${label}.${key}`);
|
|
255
|
+
}
|
|
256
|
+
const status = requireNonEmptyStringProperty(value, "status", method, path, `${label}.status`);
|
|
257
|
+
if (!RUN_STATUSES.has(status)) {
|
|
258
|
+
throw malformedApiResponse(method, path, `${label}.status must be running|completed|failed|blocked`);
|
|
259
|
+
}
|
|
260
|
+
for (const key of ["inputTokens", "outputTokens"]) {
|
|
261
|
+
requireNonNegativeIntegerProperty(value, key, method, path, `${label}.${key}`);
|
|
262
|
+
}
|
|
263
|
+
for (const key of ["costUsd", "savedUsd", "latencyMs", "startedAt"]) {
|
|
264
|
+
requireFiniteNonNegativeNumberProperty(value, key, method, path, `${label}.${key}`);
|
|
265
|
+
}
|
|
266
|
+
requireOptionalFiniteNonNegativeNumberProperty(value, "ghostSavedUsd", method, path, `${label}.ghostSavedUsd`);
|
|
267
|
+
requireOptionalFiniteNonNegativeNumberProperty(value, "completedAt", method, path, `${label}.completedAt`);
|
|
268
|
+
if (value.route !== undefined && (typeof value.route !== "string" || value.route.trim().length === 0)) {
|
|
269
|
+
throw malformedApiResponse(method, path, `${label}.route must be a non-empty string when present`);
|
|
270
|
+
}
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
function requireTraceEventRecord(value, method, path, label) {
|
|
274
|
+
if (!isRecord(value))
|
|
275
|
+
throw malformedApiResponse(method, path, `${label} must be an object`);
|
|
276
|
+
for (const key of ["id", "runId", "tenantId", "type"]) {
|
|
277
|
+
requireNonEmptyStringProperty(value, key, method, path, `${label}.${key}`);
|
|
278
|
+
}
|
|
279
|
+
requireFiniteNonNegativeNumberProperty(value, "ts", method, path, `${label}.ts`);
|
|
280
|
+
if (!isRecord(value.payload)) {
|
|
281
|
+
throw malformedApiResponse(method, path, `${label}.payload must be an object`);
|
|
282
|
+
}
|
|
283
|
+
return value;
|
|
284
|
+
}
|
|
285
|
+
function requireSideEffectRecord(value, method, path, label) {
|
|
286
|
+
if (!isRecord(value))
|
|
287
|
+
throw malformedApiResponse(method, path, `${label} must be an object`);
|
|
288
|
+
for (const key of ["id", "runId", "tenantId", "toolName", "status"]) {
|
|
289
|
+
requireNonEmptyStringProperty(value, key, method, path, `${label}.${key}`);
|
|
290
|
+
}
|
|
291
|
+
const level = requireNonNegativeIntegerProperty(value, "level", method, path, `${label}.level`);
|
|
292
|
+
if (level > 4)
|
|
293
|
+
throw malformedApiResponse(method, path, `${label}.level must be between 0 and 4`);
|
|
294
|
+
requireFiniteNonNegativeNumberProperty(value, "createdAt", method, path, `${label}.createdAt`);
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
297
|
+
function requireRunDetailResult(value, method, path) {
|
|
298
|
+
if (!isRecord(value))
|
|
299
|
+
throw malformedApiResponse(method, path, "missing response object");
|
|
300
|
+
requireRunRecord(value.run, method, path);
|
|
301
|
+
const events = requireArrayProperty(value, "events", method, path);
|
|
302
|
+
events.forEach((event, index) => requireTraceEventRecord(event, method, path, `events[${index}]`));
|
|
303
|
+
const sideEffects = requireArrayProperty(value, "sideEffects", method, path);
|
|
304
|
+
sideEffects.forEach((sideEffect, index) => requireSideEffectRecord(sideEffect, method, path, `sideEffects[${index}]`));
|
|
305
|
+
return value;
|
|
306
|
+
}
|
|
307
|
+
function requireArtifactRecord(value, method, path, label = "artifact") {
|
|
308
|
+
if (!isRecord(value))
|
|
309
|
+
throw malformedApiResponse(method, path, `missing ${label} object`);
|
|
310
|
+
for (const key of ["id", "tenantId", "patternId", "state", "type"]) {
|
|
311
|
+
requireNonEmptyStringProperty(value, key, method, path, `${label}.${key}`);
|
|
312
|
+
}
|
|
313
|
+
if (!ARTIFACT_STATES.has(String(value.state))) {
|
|
314
|
+
throw malformedApiResponse(method, path, `${label}.state is not a known artifact state`);
|
|
315
|
+
}
|
|
316
|
+
if (!ARTIFACT_TYPES.has(String(value.type))) {
|
|
317
|
+
throw malformedApiResponse(method, path, `${label}.type is not a known artifact type`);
|
|
318
|
+
}
|
|
319
|
+
if (!isRecord(value.representation))
|
|
320
|
+
throw malformedApiResponse(method, path, `${label}.representation must be an object`);
|
|
321
|
+
if (!isRecord(value.provenance))
|
|
322
|
+
throw malformedApiResponse(method, path, `${label}.provenance must be an object`);
|
|
323
|
+
requireNonNegativeIntegerProperty(value, "version", method, path, `${label}.version`);
|
|
324
|
+
const sideEffectLevel = requireNonNegativeIntegerProperty(value, "sideEffectLevel", method, path, `${label}.sideEffectLevel`);
|
|
325
|
+
if (sideEffectLevel > 4)
|
|
326
|
+
throw malformedApiResponse(method, path, `${label}.sideEffectLevel must be between 0 and 4`);
|
|
327
|
+
for (const key of ["replayPasses", "replayFails", "shadowPasses", "shadowFails", "livePasses", "liveFails"]) {
|
|
328
|
+
requireNonNegativeIntegerProperty(value, key, method, path, `${label}.${key}`);
|
|
329
|
+
}
|
|
330
|
+
for (const key of ["alpha", "beta", "confidence", "estCostUsd", "estLatencyMs", "createdAt"]) {
|
|
331
|
+
requireFiniteNonNegativeNumberProperty(value, key, method, path, `${label}.${key}`);
|
|
332
|
+
}
|
|
333
|
+
if (value.confidence > 1)
|
|
334
|
+
throw malformedApiResponse(method, path, `${label}.confidence must be between 0 and 1`);
|
|
335
|
+
requireOptionalFiniteNonNegativeNumberProperty(value, "promotedAt", method, path, `${label}.promotedAt`);
|
|
336
|
+
requireOptionalFiniteNonNegativeNumberProperty(value, "lastEvaluatedAt", method, path, `${label}.lastEvaluatedAt`);
|
|
337
|
+
return value;
|
|
338
|
+
}
|
|
339
|
+
function requireArtifactEvaluationRecord(value, method, path, label) {
|
|
340
|
+
if (!isRecord(value))
|
|
341
|
+
throw malformedApiResponse(method, path, `${label} must be an object`);
|
|
342
|
+
for (const key of ["id", "artifactId", "tenantId", "kind"]) {
|
|
343
|
+
requireNonEmptyStringProperty(value, key, method, path, `${label}.${key}`);
|
|
344
|
+
}
|
|
345
|
+
if (!["replay", "shadow", "live"].includes(String(value.kind))) {
|
|
346
|
+
throw malformedApiResponse(method, path, `${label}.kind must be replay|shadow|live`);
|
|
347
|
+
}
|
|
348
|
+
for (const key of ["pass", "structuralPass", "sideEffectPass"]) {
|
|
349
|
+
if (typeof value[key] !== "boolean")
|
|
350
|
+
throw malformedApiResponse(method, path, `${label}.${key} must be boolean`);
|
|
351
|
+
}
|
|
352
|
+
requireUnitRateProperty(value, "semanticScore", method, path, `${label}.semanticScore`);
|
|
353
|
+
requireFiniteNonNegativeNumberProperty(value, "createdAt", method, path, `${label}.createdAt`);
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
function requirePromotionGateStatus(value, method, path) {
|
|
357
|
+
if (!isRecord(value))
|
|
358
|
+
throw malformedApiResponse(method, path, "promotionGate must be an object");
|
|
359
|
+
if (typeof value.eligible !== "boolean")
|
|
360
|
+
throw malformedApiResponse(method, path, "promotionGate.eligible must be boolean");
|
|
361
|
+
if (typeof value.autoPromotable !== "boolean")
|
|
362
|
+
throw malformedApiResponse(method, path, "promotionGate.autoPromotable must be boolean");
|
|
363
|
+
if (!Array.isArray(value.reasons) || value.reasons.some((reason) => typeof reason !== "string")) {
|
|
364
|
+
throw malformedApiResponse(method, path, "promotionGate.reasons must be a string array");
|
|
365
|
+
}
|
|
366
|
+
return value;
|
|
367
|
+
}
|
|
368
|
+
function requireArtifactDetailResult(value, method, path) {
|
|
369
|
+
if (!isRecord(value))
|
|
370
|
+
throw malformedApiResponse(method, path, "missing response object");
|
|
371
|
+
requireArtifactRecord(value.artifact, method, path);
|
|
372
|
+
const evaluations = requireArrayProperty(value, "evaluations", method, path);
|
|
373
|
+
evaluations.forEach((evaluation, index) => requireArtifactEvaluationRecord(evaluation, method, path, `evaluations[${index}]`));
|
|
374
|
+
if (value.pattern !== null && value.pattern !== undefined && !isRecord(value.pattern)) {
|
|
375
|
+
throw malformedApiResponse(method, path, "pattern must be an object or null");
|
|
376
|
+
}
|
|
377
|
+
if (value.promotionGate !== undefined)
|
|
378
|
+
requirePromotionGateStatus(value.promotionGate, method, path);
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
function requireReceiptResult(value, method, path) {
|
|
382
|
+
if (!isRecord(value))
|
|
383
|
+
throw malformedApiResponse(method, path, "missing response object");
|
|
384
|
+
if (value.punkReceipt !== 1)
|
|
385
|
+
throw malformedApiResponse(method, path, "missing punkReceipt marker");
|
|
386
|
+
requireNonEmptyStringProperty(value, "runId", method, path);
|
|
387
|
+
if (!isRecord(value.receipt))
|
|
388
|
+
throw malformedApiResponse(method, path, "missing receipt object");
|
|
389
|
+
if (!Array.isArray(value.ledger))
|
|
390
|
+
throw malformedApiResponse(method, path, "missing ledger array");
|
|
391
|
+
return value;
|
|
392
|
+
}
|
|
393
|
+
function requireEvidencePacketResult(value, method, path) {
|
|
394
|
+
if (!isRecord(value))
|
|
395
|
+
throw malformedApiResponse(method, path, "missing response object");
|
|
396
|
+
if (value.punkEvidencePacket !== 1)
|
|
397
|
+
throw malformedApiResponse(method, path, "missing punkEvidencePacket marker");
|
|
398
|
+
if (!isRecord(value.summary))
|
|
399
|
+
throw malformedApiResponse(method, path, "missing summary object");
|
|
400
|
+
requireRunRecord(value.run, method, path);
|
|
401
|
+
for (const key of ["events", "sideEffects", "auditEvents"]) {
|
|
402
|
+
if (!Array.isArray(value[key]))
|
|
403
|
+
throw malformedApiResponse(method, path, `missing ${key} array`);
|
|
404
|
+
}
|
|
405
|
+
if (!isRecord(value.integrity))
|
|
406
|
+
throw malformedApiResponse(method, path, "missing integrity object");
|
|
407
|
+
if (!isRecord(value.replay))
|
|
408
|
+
throw malformedApiResponse(method, path, "missing replay object");
|
|
409
|
+
return value;
|
|
410
|
+
}
|
|
411
|
+
function requireLearningReport(value, method, path) {
|
|
412
|
+
if (!isRecord(value))
|
|
413
|
+
throw malformedApiResponse(method, path, "missing response object");
|
|
414
|
+
requireNonNegativeIntegerProperty(value, "artifactsSynthesized", method, path);
|
|
415
|
+
if (!Array.isArray(value.promotionsEligible)) {
|
|
416
|
+
throw malformedApiResponse(method, path, "missing promotionsEligible array");
|
|
417
|
+
}
|
|
418
|
+
if (!Array.isArray(value.autoPromoted)) {
|
|
419
|
+
throw malformedApiResponse(method, path, "missing autoPromoted array");
|
|
420
|
+
}
|
|
421
|
+
return value;
|
|
422
|
+
}
|
|
423
|
+
function requireCacheStatsResult(value, method, path) {
|
|
424
|
+
const stats = requireArrayProperty(value, "stats", method, path);
|
|
425
|
+
for (let i = 0;i < stats.length; i++) {
|
|
426
|
+
const stat = stats[i];
|
|
427
|
+
if (!isRecord(stat)) {
|
|
428
|
+
throw malformedApiResponse(method, path, `stats[${i}] must be an object`);
|
|
429
|
+
}
|
|
430
|
+
if (typeof stat.cacheType !== "string" || stat.cacheType.trim().length === 0) {
|
|
431
|
+
throw malformedApiResponse(method, path, `stats[${i}].cacheType must be a non-empty string`);
|
|
432
|
+
}
|
|
433
|
+
requireNonNegativeIntegerProperty(stat, "entries", method, path, `stats[${i}].entries`);
|
|
434
|
+
requireNonNegativeIntegerProperty(stat, "hits", method, path, `stats[${i}].hits`);
|
|
435
|
+
}
|
|
436
|
+
return value;
|
|
437
|
+
}
|
|
438
|
+
function requireMcpServerRecord(value, method, path, label) {
|
|
439
|
+
if (!isRecord(value))
|
|
440
|
+
throw malformedApiResponse(method, path, `${label} must be an object`);
|
|
441
|
+
if (typeof value.id !== "string" || value.id.length === 0) {
|
|
442
|
+
throw malformedApiResponse(method, path, `${label}.id must be a non-empty string`);
|
|
443
|
+
}
|
|
444
|
+
if (typeof value.name !== "string" || value.name.length === 0) {
|
|
445
|
+
throw malformedApiResponse(method, path, `${label}.name must be a non-empty string`);
|
|
446
|
+
}
|
|
447
|
+
if (typeof value.transport !== "string" || !["stdio", "http"].includes(value.transport)) {
|
|
448
|
+
throw malformedApiResponse(method, path, `${label}.transport must be stdio|http`);
|
|
449
|
+
}
|
|
450
|
+
return value;
|
|
451
|
+
}
|
|
452
|
+
function requireMcpServerList(value, method, path) {
|
|
453
|
+
const servers = requireArrayProperty(value, "servers", method, path);
|
|
454
|
+
return servers.map((server, index) => requireMcpServerRecord(server, method, path, `servers[${index}]`));
|
|
455
|
+
}
|
|
456
|
+
function requireMcpServerCreateResult(value, method, path) {
|
|
457
|
+
return requireMcpServerRecord(requireObjectProperty(value, "server", method, path), method, path, "server");
|
|
458
|
+
}
|
|
459
|
+
function cleanPathId(label, value) {
|
|
460
|
+
if (typeof value !== "string") {
|
|
461
|
+
throw new Error(`Punk SDK requires a string ${label}`);
|
|
462
|
+
}
|
|
463
|
+
const trimmed = value.trim();
|
|
464
|
+
if (!trimmed) {
|
|
465
|
+
throw new Error(`Punk SDK requires a non-empty ${label}`);
|
|
466
|
+
}
|
|
467
|
+
return trimmed;
|
|
468
|
+
}
|
|
469
|
+
function normalizeBaseUrl(value) {
|
|
470
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
471
|
+
return (trimmed || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
472
|
+
}
|
|
473
|
+
function firstChatMessage(raw, method, path) {
|
|
474
|
+
if (raw === null || typeof raw !== "object" || !Array.isArray(raw.choices)) {
|
|
475
|
+
throw malformedChatResponse(method, path, "missing choices array");
|
|
476
|
+
}
|
|
477
|
+
const choice = raw.choices[0];
|
|
478
|
+
if (choice === null || typeof choice !== "object" || Array.isArray(choice)) {
|
|
479
|
+
throw malformedChatResponse(method, path, "missing choices[0]");
|
|
480
|
+
}
|
|
481
|
+
const message = choice.message;
|
|
482
|
+
if (message === null || typeof message !== "object" || Array.isArray(message)) {
|
|
483
|
+
throw malformedChatResponse(method, path, "missing choices[0].message");
|
|
484
|
+
}
|
|
485
|
+
const content = message.content;
|
|
486
|
+
const hasToolCalls = normalizeChatToolCalls(message.tool_calls).length > 0;
|
|
487
|
+
const hasRefusal = typeof message.refusal === "string";
|
|
488
|
+
if ((content === undefined || content === null) && !hasToolCalls && !hasRefusal) {
|
|
489
|
+
throw malformedChatResponse(method, path, "choices[0].message has no content or tool_calls");
|
|
490
|
+
}
|
|
491
|
+
return message;
|
|
492
|
+
}
|
|
493
|
+
function chatMessageContentToString(value) {
|
|
494
|
+
if (value === null || value === undefined)
|
|
495
|
+
return "";
|
|
496
|
+
if (typeof value === "string")
|
|
497
|
+
return value;
|
|
498
|
+
if (!Array.isArray(value)) {
|
|
499
|
+
throw malformedChatResponse("POST", "/v1/chat/completions", "message.content was not a string, null, or text parts array");
|
|
500
|
+
}
|
|
501
|
+
return value.map((part) => {
|
|
502
|
+
if (typeof part === "string")
|
|
503
|
+
return part;
|
|
504
|
+
if (typeof part?.text === "string")
|
|
505
|
+
return part.text;
|
|
506
|
+
return "";
|
|
507
|
+
}).join("");
|
|
508
|
+
}
|
|
509
|
+
function chatMessageText(message) {
|
|
510
|
+
if ((message?.content === null || message?.content === undefined) && typeof message?.refusal === "string") {
|
|
511
|
+
return message.refusal;
|
|
512
|
+
}
|
|
513
|
+
return chatMessageContentToString(message?.content);
|
|
514
|
+
}
|
|
515
|
+
var TOOL_LEVEL_4 = new Set([
|
|
516
|
+
"delete",
|
|
517
|
+
"remove",
|
|
518
|
+
"drop",
|
|
519
|
+
"pay",
|
|
520
|
+
"charge",
|
|
521
|
+
"refund",
|
|
522
|
+
"purchase",
|
|
523
|
+
"subscribe",
|
|
524
|
+
"unsubscribe",
|
|
525
|
+
"transfer",
|
|
526
|
+
"withdraw",
|
|
527
|
+
"deposit",
|
|
528
|
+
"revoke",
|
|
529
|
+
"grant",
|
|
530
|
+
"invite",
|
|
531
|
+
"promote",
|
|
532
|
+
"demote",
|
|
533
|
+
"admin",
|
|
534
|
+
"owner",
|
|
535
|
+
"role",
|
|
536
|
+
"permission",
|
|
537
|
+
"permissions",
|
|
538
|
+
"password",
|
|
539
|
+
"secret",
|
|
540
|
+
"token",
|
|
541
|
+
"credential",
|
|
542
|
+
"credentials",
|
|
543
|
+
"terminate",
|
|
544
|
+
"destroy",
|
|
545
|
+
"disable",
|
|
546
|
+
"deactivate",
|
|
547
|
+
"suspend",
|
|
548
|
+
"ban",
|
|
549
|
+
"cancel",
|
|
550
|
+
"deploy",
|
|
551
|
+
"release",
|
|
552
|
+
"rollback"
|
|
553
|
+
]);
|
|
554
|
+
var TOOL_LEVEL_3 = new Set([
|
|
555
|
+
"send",
|
|
556
|
+
"post",
|
|
557
|
+
"create",
|
|
558
|
+
"write",
|
|
559
|
+
"notify",
|
|
560
|
+
"email",
|
|
561
|
+
"slack",
|
|
562
|
+
"ticket",
|
|
563
|
+
"message",
|
|
564
|
+
"sms",
|
|
565
|
+
"comment",
|
|
566
|
+
"reply",
|
|
567
|
+
"publish",
|
|
568
|
+
"submit",
|
|
569
|
+
"approve",
|
|
570
|
+
"merge",
|
|
571
|
+
"webhook",
|
|
572
|
+
"alert"
|
|
573
|
+
]);
|
|
574
|
+
var TOOL_LEVEL_2 = new Set(["update", "upsert", "tag", "label"]);
|
|
575
|
+
var TOOL_LEVEL_1 = new Set([
|
|
576
|
+
"get",
|
|
577
|
+
"list",
|
|
578
|
+
"read",
|
|
579
|
+
"search",
|
|
580
|
+
"fetch",
|
|
581
|
+
"lookup",
|
|
582
|
+
"query"
|
|
583
|
+
]);
|
|
584
|
+
var TOOL_LEVEL_0 = new Set(["format", "parse", "transform", "calc", "calculate"]);
|
|
585
|
+
function toolNameTokens(toolName) {
|
|
586
|
+
return toolName.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[\s._\-:/]+/).map((token) => token.toLowerCase()).filter(Boolean);
|
|
587
|
+
}
|
|
588
|
+
function normalizeSideEffectLevelValue(value) {
|
|
589
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4 ? value : undefined;
|
|
590
|
+
}
|
|
591
|
+
function inferSideEffectLevel(toolName) {
|
|
592
|
+
const tokens = toolNameTokens(toolName);
|
|
593
|
+
if (tokens.includes("key") && tokens.some((token) => token === "api" || token === "auth" || token === "secret" || token === "credential")) {
|
|
594
|
+
return { level: 4, matched: true };
|
|
595
|
+
}
|
|
596
|
+
if (tokens.some((token) => TOOL_LEVEL_4.has(token)))
|
|
597
|
+
return { level: 4, matched: true };
|
|
598
|
+
if (tokens.some((token) => TOOL_LEVEL_3.has(token)))
|
|
599
|
+
return { level: 3, matched: true };
|
|
600
|
+
if (tokens.some((token) => TOOL_LEVEL_2.has(token)))
|
|
601
|
+
return { level: 2, matched: true };
|
|
602
|
+
if (tokens.some((token) => TOOL_LEVEL_1.has(token)))
|
|
603
|
+
return { level: 1, matched: true };
|
|
604
|
+
if (tokens.some((token) => TOOL_LEVEL_0.has(token)))
|
|
605
|
+
return { level: 0, matched: true };
|
|
606
|
+
return { level: 3, matched: false };
|
|
607
|
+
}
|
|
608
|
+
function classifySdkToolSideEffect(toolName, declared) {
|
|
609
|
+
const inferred = inferSideEffectLevel(toolName);
|
|
610
|
+
const validDeclared = normalizeSideEffectLevelValue(declared);
|
|
611
|
+
if (validDeclared === undefined)
|
|
612
|
+
return Math.max(3, inferred.level);
|
|
613
|
+
return inferred.matched ? Math.max(validDeclared, inferred.level) : validDeclared;
|
|
614
|
+
}
|
|
615
|
+
function normalizeTtlSeconds(value) {
|
|
616
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0)
|
|
617
|
+
return;
|
|
618
|
+
return Math.min(value, MAX_TOOL_CACHE_TTL_SECONDS);
|
|
619
|
+
}
|
|
620
|
+
function cleanIdentityValue(value) {
|
|
621
|
+
if (typeof value !== "string")
|
|
622
|
+
return;
|
|
623
|
+
const trimmed = value.trim();
|
|
624
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
625
|
+
}
|
|
626
|
+
function cleanOptionalCacheDimension(label, value) {
|
|
627
|
+
if (typeof value !== "string")
|
|
628
|
+
return;
|
|
629
|
+
const trimmed = value.trim();
|
|
630
|
+
if (trimmed.length === 0 || trimmed !== value || trimmed.length > 120) {
|
|
631
|
+
throw new Error(`Punk traceTool requires '${label}' to be a non-empty trimmed string 120 characters or fewer`);
|
|
632
|
+
}
|
|
633
|
+
return trimmed;
|
|
634
|
+
}
|
|
635
|
+
function requireCleanToolName(value) {
|
|
636
|
+
if (typeof value !== "string") {
|
|
637
|
+
throw new Error("Punk traceTool requires a string tool name");
|
|
638
|
+
}
|
|
639
|
+
const trimmed = value.trim();
|
|
640
|
+
if (!trimmed) {
|
|
641
|
+
throw new Error("Punk traceTool requires a non-empty tool name");
|
|
642
|
+
}
|
|
643
|
+
return trimmed;
|
|
644
|
+
}
|
|
645
|
+
function toolErrorPayload(err) {
|
|
646
|
+
if (err instanceof Error) {
|
|
647
|
+
return {
|
|
648
|
+
name: err.name,
|
|
649
|
+
message: err.message
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
return { message: String(err) };
|
|
653
|
+
}
|
|
654
|
+
function toolCacheHitHasResult(hit) {
|
|
655
|
+
return hit.hit === true && Object.prototype.hasOwnProperty.call(hit, "result") && hit.result !== undefined;
|
|
656
|
+
}
|
|
657
|
+
function normalizeTraceDecision(value) {
|
|
658
|
+
if (!isRecord(value))
|
|
659
|
+
return null;
|
|
660
|
+
const decision = value.decision;
|
|
661
|
+
if (decision !== "allow" && decision !== "deny" && decision !== "approval_required")
|
|
662
|
+
return null;
|
|
663
|
+
return {
|
|
664
|
+
...value,
|
|
665
|
+
decision,
|
|
666
|
+
reason: typeof value.reason === "string" ? value.reason : ""
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function punkChorusChat(params) {
|
|
670
|
+
return { ...params, model: PUNK_CHORUS_MODEL };
|
|
671
|
+
}
|
|
672
|
+
var punkModelChat = punkChorusChat;
|
|
673
|
+
|
|
674
|
+
class Punk {
|
|
675
|
+
baseUrl;
|
|
676
|
+
app;
|
|
677
|
+
agent;
|
|
678
|
+
subject;
|
|
679
|
+
apiKey;
|
|
680
|
+
constructor(opts = {}) {
|
|
681
|
+
this.baseUrl = normalizeBaseUrl(opts.baseUrl);
|
|
682
|
+
this.apiKey = cleanIdentityValue(opts.apiKey);
|
|
683
|
+
this.app = cleanIdentityValue(opts.app) ?? "default-app";
|
|
684
|
+
this.agent = cleanIdentityValue(opts.agent);
|
|
685
|
+
this.subject = cleanIdentityValue(opts.subject);
|
|
686
|
+
}
|
|
687
|
+
async chat(params) {
|
|
688
|
+
let res;
|
|
689
|
+
try {
|
|
690
|
+
res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
691
|
+
method: "POST",
|
|
692
|
+
headers: this.headers({
|
|
693
|
+
"X-Punk-App": this.app,
|
|
694
|
+
"X-Punk-Agent": this.agent,
|
|
695
|
+
"X-Punk-Subject": this.subject
|
|
696
|
+
}),
|
|
697
|
+
body: JSON.stringify({ ...params, stream: false })
|
|
698
|
+
});
|
|
699
|
+
} catch (err) {
|
|
700
|
+
throw this.toTransportError("POST", "/v1/chat/completions", err);
|
|
701
|
+
}
|
|
702
|
+
const path = "/v1/chat/completions";
|
|
703
|
+
const method = "POST";
|
|
704
|
+
const raw = await this.parseJsonResponse(method, path, res);
|
|
705
|
+
const message = firstChatMessage(raw, method, path);
|
|
706
|
+
const toolCalls = normalizeChatToolCalls(message?.tool_calls);
|
|
707
|
+
return {
|
|
708
|
+
content: chatMessageText(message),
|
|
709
|
+
toolCalls,
|
|
710
|
+
runId: res.headers.get("x-punk-run-id") ?? "",
|
|
711
|
+
route: res.headers.get("x-punk-route") ?? "unknown",
|
|
712
|
+
raw
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
traceTool(def) {
|
|
716
|
+
const toolName = requireCleanToolName(def.name);
|
|
717
|
+
const level = classifySdkToolSideEffect(toolName, def.sideEffectLevel);
|
|
718
|
+
const ttlSeconds = normalizeTtlSeconds(def.ttlSeconds);
|
|
719
|
+
const schemaFp = cleanOptionalCacheDimension("schemaFp", def.schemaFp);
|
|
720
|
+
return async (args, ctx) => {
|
|
721
|
+
const runId = cleanIdentityValue(ctx?.runId);
|
|
722
|
+
const cacheable = level <= 1 && ttlSeconds !== undefined && runId !== undefined && this.subject !== undefined;
|
|
723
|
+
if (runId) {
|
|
724
|
+
const decision = await this.tryTraceDecision(runId, "tool.called", {
|
|
725
|
+
name: toolName,
|
|
726
|
+
args,
|
|
727
|
+
sideEffectLevel: level,
|
|
728
|
+
...schemaFp ? { schemaFp } : {}
|
|
729
|
+
});
|
|
730
|
+
if (decision && decision.decision !== "allow") {
|
|
731
|
+
const message = `Punk policy ${decision.decision}: ${decision.reason || "tool execution is not allowed"}`;
|
|
732
|
+
if (level >= 2) {
|
|
733
|
+
await this.tryTrace(runId, "side_effect.suppressed", {
|
|
734
|
+
toolName,
|
|
735
|
+
level,
|
|
736
|
+
payload: args,
|
|
737
|
+
decision: decision.decision,
|
|
738
|
+
reason: decision.reason
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
await this.tryTrace(runId, "tool.completed", {
|
|
742
|
+
name: toolName,
|
|
743
|
+
sideEffectLevel: level,
|
|
744
|
+
error: {
|
|
745
|
+
name: "PunkPolicyError",
|
|
746
|
+
message,
|
|
747
|
+
decision: decision.decision,
|
|
748
|
+
reason: decision.reason
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
throw new Error(message);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (cacheable) {
|
|
755
|
+
const cached = await this.toolCacheCheck(toolName, args, level, schemaFp);
|
|
756
|
+
if (toolCacheHitHasResult(cached)) {
|
|
757
|
+
if (runId) {
|
|
758
|
+
await this.tryTrace(runId, "tool.completed", {
|
|
759
|
+
name: toolName,
|
|
760
|
+
sideEffectLevel: level,
|
|
761
|
+
result: cached.result,
|
|
762
|
+
cached: true,
|
|
763
|
+
...schemaFp ? { schemaFp } : {}
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
return cached.result;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (runId) {
|
|
770
|
+
if (level >= 2) {
|
|
771
|
+
await this.tryTrace(runId, "side_effect.planned", {
|
|
772
|
+
toolName,
|
|
773
|
+
level,
|
|
774
|
+
payload: args
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
let result;
|
|
779
|
+
try {
|
|
780
|
+
result = await def.execute(args);
|
|
781
|
+
} catch (err) {
|
|
782
|
+
if (runId) {
|
|
783
|
+
await this.tryTrace(runId, "tool.completed", {
|
|
784
|
+
name: toolName,
|
|
785
|
+
sideEffectLevel: level,
|
|
786
|
+
error: toolErrorPayload(err),
|
|
787
|
+
...schemaFp ? { schemaFp } : {}
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
throw err;
|
|
791
|
+
}
|
|
792
|
+
if (runId) {
|
|
793
|
+
if (level >= 2) {
|
|
794
|
+
await this.tryTrace(runId, "side_effect.executed", {
|
|
795
|
+
toolName,
|
|
796
|
+
level,
|
|
797
|
+
payload: args
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
await this.tryTrace(runId, "tool.completed", {
|
|
801
|
+
name: toolName,
|
|
802
|
+
sideEffectLevel: level,
|
|
803
|
+
result,
|
|
804
|
+
...schemaFp ? { schemaFp } : {}
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
if (cacheable) {
|
|
808
|
+
await this.toolCacheStore(toolName, args, result, ttlSeconds, level, schemaFp);
|
|
809
|
+
}
|
|
810
|
+
return result;
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
async trace(runId, type, payload) {
|
|
814
|
+
const id = cleanPathId("run id", runId);
|
|
815
|
+
await this.request("POST", "/api/v1/trace", {
|
|
816
|
+
runId: id,
|
|
817
|
+
type,
|
|
818
|
+
payload
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
async feedback(runId, rating, correction) {
|
|
822
|
+
const id = cleanPathId("run id", runId);
|
|
823
|
+
const path = `/api/v1/runs/${encodeURIComponent(id)}/feedback`;
|
|
824
|
+
const trimmedCorrection = typeof correction === "string" ? correction.trim() : "";
|
|
825
|
+
const body = trimmedCorrection ? { type: "correction", correction: trimmedCorrection } : { type: "rating", rating };
|
|
826
|
+
requireOkResult(await this.request("POST", path, body), "POST", path);
|
|
827
|
+
}
|
|
828
|
+
async fetchSom(url, opts) {
|
|
829
|
+
const path = "/api/v1/web/fetch";
|
|
830
|
+
return requireWebFetchResult(await this.request("POST", path, {
|
|
831
|
+
url,
|
|
832
|
+
...opts?.bypassCache ? { bypassCache: true } : {}
|
|
833
|
+
}), "POST", path);
|
|
834
|
+
}
|
|
835
|
+
web = {
|
|
836
|
+
openSession: async (url) => {
|
|
837
|
+
const path = "/api/v1/web/sessions";
|
|
838
|
+
return requireWebSessionOpenResult(await this.request("POST", path, { url }), "POST", path);
|
|
839
|
+
},
|
|
840
|
+
act: async (sessionId, intent) => {
|
|
841
|
+
const id = cleanPathId("web session id", sessionId);
|
|
842
|
+
const path = `/api/v1/web/sessions/${encodeURIComponent(id)}/act`;
|
|
843
|
+
return requireWebActResult(await this.request("POST", path, { intent }), "POST", path, id);
|
|
844
|
+
},
|
|
845
|
+
closeSession: async (sessionId) => {
|
|
846
|
+
const id = cleanPathId("web session id", sessionId);
|
|
847
|
+
const path = `/api/v1/web/sessions/${encodeURIComponent(id)}`;
|
|
848
|
+
return requireOkResult(await this.request("DELETE", path), "DELETE", path);
|
|
849
|
+
},
|
|
850
|
+
listSessions: async () => {
|
|
851
|
+
const path = "/api/v1/web/sessions";
|
|
852
|
+
return requireWebSessionsListResult(await this.request("GET", path), "GET", path);
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
mcp = {
|
|
856
|
+
listServers: async () => {
|
|
857
|
+
const path = "/api/v1/mcp/servers";
|
|
858
|
+
return requireMcpServerList(await this.request("GET", path), "GET", path);
|
|
859
|
+
},
|
|
860
|
+
createServer: async (server) => {
|
|
861
|
+
const path = "/api/v1/mcp/servers";
|
|
862
|
+
return requireMcpServerCreateResult(await this.request("POST", path, server), "POST", path);
|
|
863
|
+
},
|
|
864
|
+
testServer: (id) => this.request("POST", `/api/v1/mcp/servers/${encodeURIComponent(cleanPathId("MCP server id", id))}/test`)
|
|
865
|
+
};
|
|
866
|
+
memory = {
|
|
867
|
+
recordInfluence: (runId, influence) => this.request("POST", `/api/v1/runs/${encodeURIComponent(cleanPathId("run id", runId))}/memory`, influence)
|
|
868
|
+
};
|
|
869
|
+
async ingestPrompt(source, prompt, opts) {
|
|
870
|
+
return this.request("POST", "/api/v1/ingest/prompt", {
|
|
871
|
+
source,
|
|
872
|
+
prompt,
|
|
873
|
+
...opts?.sessionId ? { sessionId: opts.sessionId } : {},
|
|
874
|
+
...opts?.metadata ? { metadata: opts.metadata } : {}
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
async savings() {
|
|
878
|
+
const path = "/api/v1/savings";
|
|
879
|
+
return requireSavingsSummary(await this.request("GET", path), "GET", path);
|
|
880
|
+
}
|
|
881
|
+
async patterns() {
|
|
882
|
+
const path = "/api/v1/patterns";
|
|
883
|
+
const body = await this.request("GET", path);
|
|
884
|
+
return requireArrayProperty(body, "patterns", "GET", path);
|
|
885
|
+
}
|
|
886
|
+
async artifacts() {
|
|
887
|
+
const path = "/api/v1/artifacts";
|
|
888
|
+
const body = await this.request("GET", path);
|
|
889
|
+
return requireArrayProperty(body, "artifacts", "GET", path);
|
|
890
|
+
}
|
|
891
|
+
async artifactDetail(id) {
|
|
892
|
+
const path = `/api/v1/artifacts/${encodeURIComponent(cleanPathId("artifact id", id))}`;
|
|
893
|
+
return requireArtifactDetailResult(await this.request("GET", path), "GET", path);
|
|
894
|
+
}
|
|
895
|
+
async runDetail(id) {
|
|
896
|
+
const path = `/api/v1/runs/${encodeURIComponent(cleanPathId("run id", id))}`;
|
|
897
|
+
return requireRunDetailResult(await this.request("GET", path), "GET", path);
|
|
898
|
+
}
|
|
899
|
+
async receipt(id) {
|
|
900
|
+
const path = `/api/v1/receipts/${encodeURIComponent(cleanPathId("receipt id", id))}`;
|
|
901
|
+
return requireReceiptResult(await this.request("GET", path), "GET", path);
|
|
902
|
+
}
|
|
903
|
+
async evidencePacket(runId) {
|
|
904
|
+
const path = `/api/v1/runs/${encodeURIComponent(cleanPathId("run id", runId))}/evidence-packet`;
|
|
905
|
+
return requireEvidencePacketResult(await this.request("GET", path), "GET", path);
|
|
906
|
+
}
|
|
907
|
+
async cacheStats() {
|
|
908
|
+
const path = "/api/v1/cache/stats";
|
|
909
|
+
const body = await this.request("GET", path);
|
|
910
|
+
return requireCacheStatsResult(body, "GET", path);
|
|
911
|
+
}
|
|
912
|
+
async learningTick() {
|
|
913
|
+
const path = "/api/v1/learning/tick";
|
|
914
|
+
return requireLearningReport(await this.request("POST", path), "POST", path);
|
|
915
|
+
}
|
|
916
|
+
async promoteArtifact(id) {
|
|
917
|
+
const path = `/api/v1/artifacts/${encodeURIComponent(cleanPathId("artifact id", id))}/promote`;
|
|
918
|
+
const body = await this.request("POST", path);
|
|
919
|
+
return requireObjectProperty(body, "artifact", "POST", path);
|
|
920
|
+
}
|
|
921
|
+
async toolCacheCheck(toolName, args, sideEffectLevel, schemaFp) {
|
|
922
|
+
try {
|
|
923
|
+
return await this.request("POST", "/api/v1/tool-cache/check", { toolName, appId: this.app, subject: this.subject, args, sideEffectLevel, schemaFp });
|
|
924
|
+
} catch {
|
|
925
|
+
return { hit: false };
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
async toolCacheStore(toolName, args, result, ttlSeconds, sideEffectLevel, schemaFp) {
|
|
929
|
+
try {
|
|
930
|
+
await this.request("POST", "/api/v1/tool-cache/store", {
|
|
931
|
+
toolName,
|
|
932
|
+
appId: this.app,
|
|
933
|
+
subject: this.subject,
|
|
934
|
+
args,
|
|
935
|
+
result,
|
|
936
|
+
ttlSeconds,
|
|
937
|
+
sideEffectLevel,
|
|
938
|
+
schemaFp
|
|
939
|
+
});
|
|
940
|
+
} catch {}
|
|
941
|
+
}
|
|
942
|
+
headers(extra) {
|
|
943
|
+
const headers = {
|
|
944
|
+
"Content-Type": "application/json"
|
|
945
|
+
};
|
|
946
|
+
if (this.apiKey)
|
|
947
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
948
|
+
if (extra) {
|
|
949
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
950
|
+
if (value !== undefined && value !== "")
|
|
951
|
+
headers[key] = value;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return headers;
|
|
955
|
+
}
|
|
956
|
+
async request(method, path, body) {
|
|
957
|
+
let res;
|
|
958
|
+
try {
|
|
959
|
+
res = await fetch(`${this.baseUrl}${path}`, {
|
|
960
|
+
method,
|
|
961
|
+
headers: this.headers(),
|
|
962
|
+
...body !== undefined ? { body: JSON.stringify(body) } : {}
|
|
963
|
+
});
|
|
964
|
+
} catch (err) {
|
|
965
|
+
throw this.toTransportError(method, path, err);
|
|
966
|
+
}
|
|
967
|
+
return this.parseJsonResponse(method, path, res);
|
|
968
|
+
}
|
|
969
|
+
async parseJsonResponse(method, path, res) {
|
|
970
|
+
const text = await res.text();
|
|
971
|
+
if (!res.ok) {
|
|
972
|
+
throw this.toError(method, path, res, text);
|
|
973
|
+
}
|
|
974
|
+
try {
|
|
975
|
+
return JSON.parse(text);
|
|
976
|
+
} catch {
|
|
977
|
+
throw new Error(`Punk API ${method} ${path} returned non-JSON response: ${res.status} ${res.statusText}` + (text ? ` — ${text.slice(0, 500)}` : ""));
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
toError(method, path, res, text) {
|
|
981
|
+
const message = this.responseErrorMessage(text);
|
|
982
|
+
const runId = res.headers.get("x-punk-run-id");
|
|
983
|
+
const route = res.headers.get("x-punk-route");
|
|
984
|
+
const evidence = [
|
|
985
|
+
runId ? `run ${runId}` : "",
|
|
986
|
+
route ? `route ${route}` : ""
|
|
987
|
+
].filter(Boolean).join(", ");
|
|
988
|
+
return new Error(`Punk API ${method} ${path} failed: ${res.status} ${res.statusText}` + (evidence ? ` (${evidence})` : "") + (message ? ` — ${message}` : ""));
|
|
989
|
+
}
|
|
990
|
+
toTransportError(method, path, err) {
|
|
991
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
992
|
+
return new Error(`Punk API ${method} ${path} could not reach ${this.baseUrl}: ${reason}`);
|
|
993
|
+
}
|
|
994
|
+
responseErrorMessage(text) {
|
|
995
|
+
if (!text)
|
|
996
|
+
return "";
|
|
997
|
+
let data = null;
|
|
998
|
+
try {
|
|
999
|
+
data = JSON.parse(text);
|
|
1000
|
+
} catch {
|
|
1001
|
+
return text.slice(0, 500);
|
|
1002
|
+
}
|
|
1003
|
+
const error = data?.error;
|
|
1004
|
+
let message = "";
|
|
1005
|
+
if (error && typeof error === "object") {
|
|
1006
|
+
if (typeof error.message === "string" && error.message.trim())
|
|
1007
|
+
message = error.message.trim();
|
|
1008
|
+
else if (typeof error.code === "string" && error.code.trim())
|
|
1009
|
+
message = error.code.trim();
|
|
1010
|
+
else {
|
|
1011
|
+
try {
|
|
1012
|
+
message = JSON.stringify(error);
|
|
1013
|
+
} catch {
|
|
1014
|
+
message = String(error);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
} else if (typeof data?.message === "string" && data.message.trim()) {
|
|
1018
|
+
message = data.message.trim();
|
|
1019
|
+
if (typeof error === "string" && error.trim() && error.trim() !== message) {
|
|
1020
|
+
message += ` (${error.trim()})`;
|
|
1021
|
+
}
|
|
1022
|
+
} else if (typeof error === "string" && error.trim()) {
|
|
1023
|
+
message = error.trim();
|
|
1024
|
+
} else {
|
|
1025
|
+
message = text.slice(0, 500);
|
|
1026
|
+
}
|
|
1027
|
+
if (Array.isArray(data?.validation) && data.validation.length > 0) {
|
|
1028
|
+
message += `: ${data.validation.slice(0, 3).map((v) => String(v)).join("; ")}`;
|
|
1029
|
+
}
|
|
1030
|
+
return message.slice(0, 500);
|
|
1031
|
+
}
|
|
1032
|
+
async tryTrace(runId, type, payload) {
|
|
1033
|
+
try {
|
|
1034
|
+
await this.trace(runId, type, payload);
|
|
1035
|
+
} catch {}
|
|
1036
|
+
}
|
|
1037
|
+
async tryTraceDecision(runId, type, payload) {
|
|
1038
|
+
try {
|
|
1039
|
+
const res = await this.request("POST", "/api/v1/trace", {
|
|
1040
|
+
runId,
|
|
1041
|
+
type,
|
|
1042
|
+
payload
|
|
1043
|
+
});
|
|
1044
|
+
return normalizeTraceDecision(res.decision);
|
|
1045
|
+
} catch {
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
var src_default = Punk;
|
|
1051
|
+
export {
|
|
1052
|
+
punkModelChat,
|
|
1053
|
+
punkChorusChat,
|
|
1054
|
+
src_default as default,
|
|
1055
|
+
Punk,
|
|
1056
|
+
PUNK_MODEL,
|
|
1057
|
+
PUNK_CHORUS_MODEL
|
|
1058
|
+
};
|