thumbgate 1.28.4 → 1.29.2
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/.claude/commands/dashboard.md +11 -1
- package/.claude/commands/thumbgate-dashboard.md +23 -8
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +18 -10
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +66 -3
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +88 -2
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +8 -8
- package/bin/postinstall.js +4 -13
- package/commands/dashboard.md +11 -1
- package/commands/thumbgate-dashboard.md +23 -8
- package/config/agent-outcome-monitor-thresholds.json +63 -0
- package/config/evals/agent-outcomes-baseline.json +17 -0
- package/config/evals/agent-outcomes-golden.json +412 -0
- package/config/evals/prompt-eval-baseline.json +23 -0
- package/config/github-about.json +5 -4
- package/config/post-deploy-marketing-pages.json +6 -6
- package/config/schemas/task-outcome-receipt.schema.json +296 -0
- package/docs/integrations/grafana/README.md +109 -0
- package/docs/integrations/grafana/thumbgate-revenue-evidence-dashboard.json +1930 -0
- package/openapi/openapi.yaml +475 -5
- package/package.json +75 -22
- package/public/agent-manager.html +10 -11
- package/public/agents-cost-savings.html +2 -2
- package/public/assets/brand/thumbgate-logo-transparent.svg +6 -11
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +11 -10
- package/public/assets/brand/thumbgate-mark.svg +10 -11
- package/public/blog/inside-your-boundary.html +114 -0
- package/public/blog/process-over-outcome-gates.html +119 -0
- package/public/blog.html +296 -402
- package/public/brand/thumbgate-mark.svg +5 -9
- package/public/codex-enterprise.html +2 -2
- package/public/compare.html +12 -3
- package/public/diagnostic.html +79 -29
- package/public/guide.html +4 -4
- package/public/index.html +1090 -2098
- package/public/install.html +3 -3
- package/public/js/buyer-intent.js +33 -18
- package/public/numbers.html +2 -2
- package/public/pricing.html +268 -408
- package/public/pro.html +4 -4
- package/scripts/agent-outcome-eval.js +130 -0
- package/scripts/agent-outcome-monitor.js +261 -0
- package/scripts/agent-reasoning-traces.js +8 -9
- package/scripts/async-job-runner.js +107 -13
- package/scripts/billing.js +456 -126
- package/scripts/buyer-paths.js +102 -0
- package/scripts/cli-feedback.js +2 -2
- package/scripts/commercial-offer.js +18 -10
- package/scripts/durability/step.js +121 -12
- package/scripts/external-customer-audit.js +881 -0
- package/scripts/feedback-loop.js +26 -0
- package/scripts/gates-engine.js +554 -19
- package/scripts/grafana-revenue-evidence.js +856 -0
- package/scripts/human-escalation.js +265 -0
- package/scripts/hybrid-feedback-context.js +93 -50
- package/scripts/jsonl-window.js +89 -0
- package/scripts/judge-reward-function.js +30 -18
- package/scripts/lesson-embedding-index.js +3 -7
- package/scripts/meta-agent-loop.js +20 -2
- package/scripts/observability-env.js +139 -0
- package/scripts/observability-setup.js +55 -0
- package/scripts/plausible-domain-config.js +4 -0
- package/scripts/prompt-eval.js +81 -4
- package/scripts/provider-live-evidence.js +1290 -0
- package/scripts/provider-payment-reconciler.js +442 -0
- package/scripts/provider-revenue-evidence.js +249 -0
- package/scripts/rate-limiter.js +1 -5
- package/scripts/revenue-action-eligibility.js +414 -0
- package/scripts/revenue-evidence-remediation.js +694 -0
- package/scripts/revenue-offer-system.js +709 -0
- package/scripts/sales-pipeline.js +1117 -0
- package/scripts/schedule-manager.js +249 -0
- package/scripts/seo-gsd.js +8 -4
- package/scripts/stripe-credentials.js +37 -0
- package/scripts/stripe-revenue-catalog-audit.js +363 -0
- package/scripts/stripe-revenue-catalog.js +164 -0
- package/scripts/task-outcomes.js +425 -0
- package/scripts/telemetry-analytics.js +23 -3
- package/scripts/tool-contract-validator.js +287 -59
- package/scripts/tool-registry.js +143 -0
- package/scripts/vector-store.js +83 -7
- package/scripts/workflow-intake-queue.js +483 -0
- package/src/api/server.js +647 -118
|
@@ -2,75 +2,303 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Tool Contract Validator
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* A dependency-free JSON Schema subset for tool arguments and structured
|
|
7
|
+
* outputs. The validator intentionally fails closed for unsupported schema
|
|
8
|
+
* shapes used at an agent boundary instead of silently accepting them.
|
|
6
9
|
*/
|
|
10
|
+
|
|
11
|
+
const FORMAT_CHECKS = Object.freeze({
|
|
12
|
+
'date-time': (value) => !Number.isNaN(Date.parse(value)) && /T/.test(value),
|
|
13
|
+
email: isEmail,
|
|
14
|
+
uri: (value) => {
|
|
15
|
+
try {
|
|
16
|
+
new URL(value);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
7
24
|
function validateToolContract(schema, args) {
|
|
25
|
+
if (!schema) return { valid: true, errors: [] };
|
|
8
26
|
const errors = [];
|
|
9
|
-
|
|
27
|
+
validateValue(schema, args, '', errors);
|
|
28
|
+
return { valid: errors.length === 0, errors };
|
|
29
|
+
}
|
|
10
30
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
31
|
+
function validateStructuredOutput(output, schema) {
|
|
32
|
+
let value = output;
|
|
33
|
+
if (typeof output === 'string') {
|
|
34
|
+
try {
|
|
35
|
+
value = JSON.parse(output);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return {
|
|
38
|
+
valid: false,
|
|
39
|
+
errors: [`Structured output must be valid JSON: ${err.message}`],
|
|
40
|
+
value: null,
|
|
41
|
+
};
|
|
15
42
|
}
|
|
43
|
+
}
|
|
44
|
+
const result = validateToolContract(schema, value);
|
|
45
|
+
return { ...result, value };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function validateConstant(schema, value, path, errors) {
|
|
49
|
+
if (schema.const !== undefined && !deepEqual(value, schema.const)) {
|
|
50
|
+
errors.push(`${label(path)} must equal ${display(schema.const)} (got ${display(value)})`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validateEnum(schema, value, path, errors) {
|
|
55
|
+
if (!Array.isArray(schema.enum) || schema.enum.some((entry) => deepEqual(entry, value))) return;
|
|
56
|
+
if (path && schema.type === 'string') {
|
|
57
|
+
errors.push(`Parameter '${path}' must be one of [${schema.enum.join(', ')}] (got '${value}')`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
errors.push(`${label(path)} must be one of [${schema.enum.map(display).join(', ')}] (got ${display(value)})`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function validateValue(schema, value, path, errors) {
|
|
64
|
+
if (schema === true || !schema) return;
|
|
65
|
+
if (schema === false) {
|
|
66
|
+
errors.push(`${label(path)} is disallowed by schema`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
validateConstant(schema, value, path, errors);
|
|
71
|
+
validateEnum(schema, value, path, errors);
|
|
72
|
+
|
|
73
|
+
validateCombinators(schema, value, path, errors);
|
|
74
|
+
|
|
75
|
+
if (value === undefined) return;
|
|
76
|
+
const allowedTypes = normalizeTypes(schema.type);
|
|
77
|
+
if (allowedTypes.length > 0 && !allowedTypes.some((type) => matchesType(type, value))) {
|
|
78
|
+
errors.push(typeError(path, allowedTypes, value));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (value === null) return;
|
|
83
|
+
const actualType = jsonType(value);
|
|
84
|
+
if (actualType === 'object') validateObject(schema, value, path, errors);
|
|
85
|
+
if (actualType === 'array') validateArray(schema, value, path, errors);
|
|
86
|
+
if (actualType === 'string') validateString(schema, value, path, errors);
|
|
87
|
+
if (actualType === 'number' || actualType === 'integer') validateNumber(schema, value, path, errors);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function validateCombinators(schema, value, path, errors) {
|
|
91
|
+
if (Array.isArray(schema.allOf)) {
|
|
92
|
+
for (const child of schema.allOf) validateValue(child, value, path, errors);
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(schema.anyOf)) {
|
|
95
|
+
const matches = schema.anyOf.filter((child) => validateChild(child, value).valid).length;
|
|
96
|
+
if (matches === 0) errors.push(`${label(path)} must match at least one anyOf schema`);
|
|
97
|
+
}
|
|
98
|
+
if (Array.isArray(schema.oneOf)) {
|
|
99
|
+
const matches = schema.oneOf.filter((child) => validateChild(child, value).valid).length;
|
|
100
|
+
if (matches !== 1) errors.push(`${label(path)} must match exactly one oneOf schema (matched ${matches})`);
|
|
101
|
+
}
|
|
102
|
+
if (schema.not && validateChild(schema.not, value).valid) {
|
|
103
|
+
errors.push(`${label(path)} matches a forbidden schema`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validateChild(schema, value) {
|
|
108
|
+
const errors = [];
|
|
109
|
+
validateValue(schema, value, '', errors);
|
|
110
|
+
return { valid: errors.length === 0, errors };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function validatePropertyCount(schema, keys, path, errors) {
|
|
114
|
+
if (Number.isFinite(schema.minProperties) && keys.length < schema.minProperties) {
|
|
115
|
+
errors.push(`${label(path)} must have at least ${schema.minProperties} properties`);
|
|
116
|
+
}
|
|
117
|
+
if (Number.isFinite(schema.maxProperties) && keys.length > schema.maxProperties) {
|
|
118
|
+
errors.push(`${label(path)} must have at most ${schema.maxProperties} properties`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function validateRequiredProperties(schema, value, path, errors) {
|
|
123
|
+
for (const required of schema.required || []) {
|
|
124
|
+
if (value[required] === undefined || value[required] === null || value[required] === '') {
|
|
125
|
+
const requiredPath = joinPath(path, required);
|
|
126
|
+
errors.push(path
|
|
127
|
+
? `Parameter '${path}': Missing required parameter: '${required}'`
|
|
128
|
+
: `Missing required parameter: '${required}'`);
|
|
129
|
+
if (path && requiredPath === path) break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validateKnownProperties(schema, value, path, errors) {
|
|
135
|
+
const properties = schema.properties || {};
|
|
136
|
+
for (const [key, childSchema] of Object.entries(properties)) {
|
|
137
|
+
if (value[key] === undefined) continue;
|
|
138
|
+
validateValue(childSchema, value[key], joinPath(path, key), errors);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function validateAdditionalProperties(schema, value, keys, path, errors) {
|
|
143
|
+
const properties = schema.properties || {};
|
|
144
|
+
for (const key of keys) {
|
|
145
|
+
if (Object.hasOwn(properties, key)) continue;
|
|
146
|
+
if (schema.additionalProperties === false) {
|
|
147
|
+
errors.push(`Unexpected parameter: '${joinPath(path, key)}'`);
|
|
148
|
+
} else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
149
|
+
validateValue(schema.additionalProperties, value[key], joinPath(path, key), errors);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function validateObject(schema, value, path, errors) {
|
|
155
|
+
const keys = Object.keys(value);
|
|
156
|
+
validatePropertyCount(schema, keys, path, errors);
|
|
157
|
+
validateRequiredProperties(schema, value, path, errors);
|
|
158
|
+
validateKnownProperties(schema, value, path, errors);
|
|
159
|
+
validateAdditionalProperties(schema, value, keys, path, errors);
|
|
160
|
+
}
|
|
16
161
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
162
|
+
function validateArray(schema, value, path, errors) {
|
|
163
|
+
if (Number.isFinite(schema.minItems) && value.length < schema.minItems) {
|
|
164
|
+
errors.push(`${label(path)} must contain at least ${schema.minItems} items`);
|
|
165
|
+
}
|
|
166
|
+
if (Number.isFinite(schema.maxItems) && value.length > schema.maxItems) {
|
|
167
|
+
errors.push(`${label(path)} must contain at most ${schema.maxItems} items`);
|
|
168
|
+
}
|
|
169
|
+
if (schema.uniqueItems === true) {
|
|
170
|
+
const serialized = value.map(stableStringify);
|
|
171
|
+
if (new Set(serialized).size !== serialized.length) {
|
|
172
|
+
errors.push(`${label(path)} must contain unique items`);
|
|
24
173
|
}
|
|
174
|
+
}
|
|
175
|
+
if (schema.items) {
|
|
176
|
+
value.forEach((entry, index) => validateValue(schema.items, entry, `${path}[${index}]`, errors));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
25
179
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
} else if (propSchema.type === 'number') {
|
|
42
|
-
if (valType !== 'number' || isNaN(value)) {
|
|
43
|
-
errors.push(`Parameter '${key}' must be a number (got ${valType})`);
|
|
44
|
-
}
|
|
45
|
-
} else if (propSchema.type === 'boolean') {
|
|
46
|
-
if (valType !== 'boolean') {
|
|
47
|
-
errors.push(`Parameter '${key}' must be a boolean (got ${valType})`);
|
|
48
|
-
}
|
|
49
|
-
} else if (propSchema.type === 'array') {
|
|
50
|
-
if (!Array.isArray(value)) {
|
|
51
|
-
errors.push(`Parameter '${key}' must be an array (got ${valType})`);
|
|
52
|
-
}
|
|
53
|
-
} else if (propSchema.type === 'object') {
|
|
54
|
-
if (valType !== 'object' || value === null || Array.isArray(value)) {
|
|
55
|
-
errors.push(`Parameter '${key}' must be an object (got ${valType})`);
|
|
56
|
-
} else {
|
|
57
|
-
// Recurse for nested objects
|
|
58
|
-
const subRes = validateToolContract(propSchema, value);
|
|
59
|
-
if (!subRes.valid) {
|
|
60
|
-
for (const err of subRes.errors) {
|
|
61
|
-
errors.push(`Parameter '${key}': ${err}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
180
|
+
function validateString(schema, value, path, errors) {
|
|
181
|
+
if (Number.isFinite(schema.minLength) && value.length < schema.minLength) {
|
|
182
|
+
errors.push(`${label(path)} must contain at least ${schema.minLength} characters`);
|
|
183
|
+
}
|
|
184
|
+
if (Number.isFinite(schema.maxLength) && value.length > schema.maxLength) {
|
|
185
|
+
errors.push(`${label(path)} must contain at most ${schema.maxLength} characters`);
|
|
186
|
+
}
|
|
187
|
+
if (schema.pattern) {
|
|
188
|
+
let regex;
|
|
189
|
+
try {
|
|
190
|
+
regex = new RegExp(schema.pattern);
|
|
191
|
+
} catch {
|
|
192
|
+
errors.push(`${label(path)} uses invalid schema pattern '${schema.pattern}'`);
|
|
193
|
+
return;
|
|
67
194
|
}
|
|
195
|
+
if (!regex.test(value)) errors.push(`${label(path)} must match pattern ${schema.pattern}`);
|
|
196
|
+
}
|
|
197
|
+
if (schema.format && FORMAT_CHECKS[schema.format] && !FORMAT_CHECKS[schema.format](value)) {
|
|
198
|
+
errors.push(`${label(path)} must match format ${schema.format}`);
|
|
68
199
|
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function validateNumber(schema, value, path, errors) {
|
|
203
|
+
if (Number.isFinite(schema.minimum) && value < schema.minimum) {
|
|
204
|
+
errors.push(`${label(path)} must be >= ${schema.minimum}`);
|
|
205
|
+
}
|
|
206
|
+
if (Number.isFinite(schema.maximum) && value > schema.maximum) {
|
|
207
|
+
errors.push(`${label(path)} must be <= ${schema.maximum}`);
|
|
208
|
+
}
|
|
209
|
+
if (Number.isFinite(schema.exclusiveMinimum) && value <= schema.exclusiveMinimum) {
|
|
210
|
+
errors.push(`${label(path)} must be > ${schema.exclusiveMinimum}`);
|
|
211
|
+
}
|
|
212
|
+
if (Number.isFinite(schema.exclusiveMaximum) && value >= schema.exclusiveMaximum) {
|
|
213
|
+
errors.push(`${label(path)} must be < ${schema.exclusiveMaximum}`);
|
|
214
|
+
}
|
|
215
|
+
if (Number.isFinite(schema.multipleOf) && !isMultipleOf(value, schema.multipleOf)) {
|
|
216
|
+
errors.push(`${label(path)} must be a multiple of ${schema.multipleOf}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizeTypes(type) {
|
|
221
|
+
if (!type) return [];
|
|
222
|
+
return Array.isArray(type) ? type : [type];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function matchesType(type, value) {
|
|
226
|
+
if (type === 'null') return value === null;
|
|
227
|
+
if (type === 'array') return Array.isArray(value);
|
|
228
|
+
if (type === 'object') return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
229
|
+
if (type === 'integer') return Number.isInteger(value);
|
|
230
|
+
if (type === 'number') return typeof value === 'number' && Number.isFinite(value);
|
|
231
|
+
return typeof value === type;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isEmail(value) {
|
|
235
|
+
if (typeof value !== 'string' || /\s/.test(value)) return false;
|
|
236
|
+
const atIndex = value.indexOf('@');
|
|
237
|
+
if (atIndex <= 0 || atIndex !== value.lastIndexOf('@')) return false;
|
|
238
|
+
const domain = value.slice(atIndex + 1);
|
|
239
|
+
const dotIndex = domain.lastIndexOf('.');
|
|
240
|
+
return dotIndex > 0 && dotIndex < domain.length - 1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function jsonType(value) {
|
|
244
|
+
if (value === null) return 'null';
|
|
245
|
+
if (Array.isArray(value)) return 'array';
|
|
246
|
+
if (Number.isInteger(value)) return 'integer';
|
|
247
|
+
if (typeof value === 'number') return 'number';
|
|
248
|
+
return typeof value;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function typeError(path, types, value) {
|
|
252
|
+
const expected = types.length === 1 ? types[0] : types.join(' or ');
|
|
253
|
+
const actual = actualTypeName(value);
|
|
254
|
+
if (!path) return `Expected ${expected}, got ${actual}`;
|
|
255
|
+
return `Parameter '${path}' must be ${article(expected)} ${expected} (got ${actual})`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function actualTypeName(value) {
|
|
259
|
+
if (value === null) return 'null';
|
|
260
|
+
if (Array.isArray(value)) return 'array';
|
|
261
|
+
return typeof value;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function article(value) {
|
|
265
|
+
return /^[aeiou]/i.test(value) ? 'an' : 'a';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function label(path) {
|
|
269
|
+
return path ? `Parameter '${path}'` : 'Value';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function joinPath(parent, child) {
|
|
273
|
+
return parent ? `${parent}.${child}` : child;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function display(value) {
|
|
277
|
+
if (typeof value === 'string') return `'${value}'`;
|
|
278
|
+
return stableStringify(value);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function stableStringify(value) {
|
|
282
|
+
if (!value || typeof value !== 'object') return JSON.stringify(value);
|
|
283
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
|
|
284
|
+
const keys = Object.keys(value).sort((left, right) => left.localeCompare(right));
|
|
285
|
+
const properties = keys.map((key) => [
|
|
286
|
+
JSON.stringify(key),
|
|
287
|
+
stableStringify(value[key]),
|
|
288
|
+
].join(':'));
|
|
289
|
+
return ['{', properties.join(','), '}'].join('');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function deepEqual(a, b) {
|
|
293
|
+
return stableStringify(a) === stableStringify(b);
|
|
294
|
+
}
|
|
69
295
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
};
|
|
296
|
+
function isMultipleOf(value, multiple) {
|
|
297
|
+
const quotient = value / multiple;
|
|
298
|
+
return Math.abs(quotient - Math.round(quotient)) < Number.EPSILON * 10;
|
|
74
299
|
}
|
|
75
300
|
|
|
76
|
-
module.exports = {
|
|
301
|
+
module.exports = {
|
|
302
|
+
validateStructuredOutput,
|
|
303
|
+
validateToolContract,
|
|
304
|
+
};
|
package/scripts/tool-registry.js
CHANGED
|
@@ -60,6 +60,79 @@ const GOAL_CONTRACT_SCHEMA = {
|
|
|
60
60
|
},
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
const TASK_OUTCOME_INPUT_SCHEMA = {
|
|
64
|
+
type: 'object',
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
required: ['taskId', 'goal', 'status', 'verification', 'toolCalls', 'policy', 'efficiency'],
|
|
67
|
+
properties: {
|
|
68
|
+
taskId: { type: 'string', minLength: 1 },
|
|
69
|
+
taskType: { type: 'string' },
|
|
70
|
+
goal: { type: 'string', minLength: 1 },
|
|
71
|
+
expectedOutcome: { type: 'string' },
|
|
72
|
+
status: { type: 'string', enum: ['completed', 'failed', 'partial', 'escalated'] },
|
|
73
|
+
verification: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
additionalProperties: false,
|
|
76
|
+
required: ['performed', 'passed', 'evidence'],
|
|
77
|
+
properties: {
|
|
78
|
+
performed: { type: 'boolean' },
|
|
79
|
+
passed: { type: 'boolean' },
|
|
80
|
+
verifier: { type: 'string' },
|
|
81
|
+
method: { type: 'string' },
|
|
82
|
+
evidence: { type: 'array', items: { type: 'string' } },
|
|
83
|
+
unsupportedClaims: { type: 'integer', minimum: 0 },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
toolCalls: {
|
|
87
|
+
type: 'array',
|
|
88
|
+
items: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
additionalProperties: false,
|
|
91
|
+
required: ['name', 'contractValid', 'allowed', 'succeeded', 'attempts'],
|
|
92
|
+
properties: {
|
|
93
|
+
name: { type: 'string' },
|
|
94
|
+
contractValid: { type: 'boolean' },
|
|
95
|
+
allowed: { type: 'boolean' },
|
|
96
|
+
succeeded: { type: 'boolean' },
|
|
97
|
+
attempts: { type: 'integer', minimum: 1 },
|
|
98
|
+
latencyMs: { type: 'number', minimum: 0 },
|
|
99
|
+
costUsd: { type: 'number', minimum: 0 },
|
|
100
|
+
sideEffect: { type: 'boolean' },
|
|
101
|
+
idempotencyKey: { type: 'string' },
|
|
102
|
+
duplicateSideEffect: { type: 'boolean' },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
policy: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
additionalProperties: false,
|
|
109
|
+
required: ['violations', 'unsafeEscapes', 'falseBlocks'],
|
|
110
|
+
properties: {
|
|
111
|
+
violations: { type: 'integer', minimum: 0 },
|
|
112
|
+
unsafeEscapes: { type: 'integer', minimum: 0 },
|
|
113
|
+
falseBlocks: { type: 'integer', minimum: 0 },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
failure: { type: 'object', additionalProperties: true },
|
|
117
|
+
escalation: { type: 'object', additionalProperties: true },
|
|
118
|
+
efficiency: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
additionalProperties: false,
|
|
121
|
+
required: ['latencyMs', 'costUsd'],
|
|
122
|
+
properties: {
|
|
123
|
+
latencyMs: { type: 'number', minimum: 0 },
|
|
124
|
+
costUsd: { type: 'number', minimum: 0 },
|
|
125
|
+
firstAttempt: { type: 'boolean' },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
businessOutcome: { type: 'object', additionalProperties: true },
|
|
129
|
+
traceId: { type: 'string' },
|
|
130
|
+
idempotencyKey: { type: 'string' },
|
|
131
|
+
versions: { type: 'object', additionalProperties: true },
|
|
132
|
+
metadata: { type: 'object', additionalProperties: true },
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
63
136
|
const TOOLS = [
|
|
64
137
|
readOnlyTool({
|
|
65
138
|
name: 'capture_feedback',
|
|
@@ -969,6 +1042,76 @@ const TOOLS = [
|
|
|
969
1042
|
},
|
|
970
1043
|
},
|
|
971
1044
|
}),
|
|
1045
|
+
destructiveTool({
|
|
1046
|
+
name: 'record_task_outcome',
|
|
1047
|
+
title: 'Record Verified Task Outcome',
|
|
1048
|
+
description: 'Record an idempotent task-level outcome with verification evidence, tool correctness, policy behavior, latency, cost, and business KPI movement. A completed response without evidence is recorded as not working.',
|
|
1049
|
+
inputSchema: TASK_OUTCOME_INPUT_SCHEMA,
|
|
1050
|
+
}),
|
|
1051
|
+
readOnlyTool({
|
|
1052
|
+
name: 'get_task_outcomes',
|
|
1053
|
+
title: 'Get Task Outcomes',
|
|
1054
|
+
description: 'Read a task outcome by taskId or the most recent evidence-backed outcome receipts.',
|
|
1055
|
+
inputSchema: {
|
|
1056
|
+
type: 'object',
|
|
1057
|
+
additionalProperties: false,
|
|
1058
|
+
properties: {
|
|
1059
|
+
taskId: { type: 'string' },
|
|
1060
|
+
limit: { type: 'integer', minimum: 1, maximum: 100 },
|
|
1061
|
+
},
|
|
1062
|
+
},
|
|
1063
|
+
}),
|
|
1064
|
+
readOnlyTool({
|
|
1065
|
+
name: 'get_agent_outcome_metrics',
|
|
1066
|
+
title: 'Get Agent Outcome Metrics',
|
|
1067
|
+
description: 'Compute transparent task, tool, safety, escalation, latency, cost, and business metrics from recorded task outcomes. Empty data returns insufficient_evidence.',
|
|
1068
|
+
inputSchema: {
|
|
1069
|
+
type: 'object',
|
|
1070
|
+
additionalProperties: false,
|
|
1071
|
+
properties: {},
|
|
1072
|
+
},
|
|
1073
|
+
}),
|
|
1074
|
+
destructiveTool({
|
|
1075
|
+
name: 'request_human_escalation',
|
|
1076
|
+
title: 'Request Human Escalation',
|
|
1077
|
+
description: 'Create an idempotent, expiring human-review request with requester identity and evidence. Agents cannot approve their own requests.',
|
|
1078
|
+
inputSchema: {
|
|
1079
|
+
type: 'object',
|
|
1080
|
+
additionalProperties: false,
|
|
1081
|
+
required: ['taskId', 'reason', 'requester', 'evidence'],
|
|
1082
|
+
properties: {
|
|
1083
|
+
taskId: { type: 'string', minLength: 1 },
|
|
1084
|
+
reason: { type: 'string', minLength: 1 },
|
|
1085
|
+
severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
|
|
1086
|
+
requester: {
|
|
1087
|
+
type: 'object',
|
|
1088
|
+
additionalProperties: false,
|
|
1089
|
+
required: ['id', 'kind'],
|
|
1090
|
+
properties: {
|
|
1091
|
+
id: { type: 'string', minLength: 1 },
|
|
1092
|
+
kind: { type: 'string', enum: ['agent', 'service', 'human'] },
|
|
1093
|
+
displayName: { type: 'string' },
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
1096
|
+
evidence: { type: 'array', minItems: 1, items: { type: 'string', minLength: 1 } },
|
|
1097
|
+
ttlMs: { type: 'number', minimum: 1 },
|
|
1098
|
+
idempotencyKey: { type: 'string' },
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
}),
|
|
1102
|
+
readOnlyTool({
|
|
1103
|
+
name: 'list_human_escalations',
|
|
1104
|
+
title: 'List Human Escalations',
|
|
1105
|
+
description: 'List auditable human-escalation state. Approval decisions are deliberately absent from the agent tool surface.',
|
|
1106
|
+
inputSchema: {
|
|
1107
|
+
type: 'object',
|
|
1108
|
+
additionalProperties: false,
|
|
1109
|
+
properties: {
|
|
1110
|
+
status: { type: 'string', enum: ['pending', 'approved', 'rejected', 'cancelled', 'expired'] },
|
|
1111
|
+
limit: { type: 'integer', minimum: 1, maximum: 100 },
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
}),
|
|
972
1115
|
readOnlyTool({
|
|
973
1116
|
name: 'verify_claim',
|
|
974
1117
|
description: 'Check whether a claim has enough tracked evidence before the agent asserts it.',
|
package/scripts/vector-store.js
CHANGED
|
@@ -28,6 +28,7 @@ let _lastEmbeddingProfile = null;
|
|
|
28
28
|
let _pipelineLoader = null;
|
|
29
29
|
let _geminiEmbedderForTests = null;
|
|
30
30
|
const TABLE_NAME = 'thumbgate_memories';
|
|
31
|
+
const FEATURE_HASH_DIMENSIONS = 384;
|
|
31
32
|
|
|
32
33
|
async function getLanceDB() {
|
|
33
34
|
if (!_lancedb) {
|
|
@@ -69,6 +70,58 @@ async function loadPipelineForProfile(profile) {
|
|
|
69
70
|
return pipe;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
function hasLocalTransformerProvider() {
|
|
74
|
+
if (_pipelineLoader) return true;
|
|
75
|
+
try {
|
|
76
|
+
require.resolve('@huggingface/transformers');
|
|
77
|
+
return true;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function fnv1a32(value) {
|
|
84
|
+
let hash = 0x811c9dc5;
|
|
85
|
+
const bytes = Buffer.from(String(value), 'utf8');
|
|
86
|
+
for (const byte of bytes) {
|
|
87
|
+
hash ^= byte;
|
|
88
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
89
|
+
}
|
|
90
|
+
return hash;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function addHashedFeature(vector, feature, weight) {
|
|
94
|
+
const hash = fnv1a32(feature);
|
|
95
|
+
const index = hash % vector.length;
|
|
96
|
+
const sign = (hash & 0x80000000) === 0 ? 1 : -1;
|
|
97
|
+
vector[index] += sign * weight;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function embedWithFeatureHash(text) {
|
|
101
|
+
const vector = Array(FEATURE_HASH_DIMENSIONS).fill(0);
|
|
102
|
+
const tokens = String(text || '').toLowerCase().match(/[\p{L}\p{N}_-]+/gu) || [];
|
|
103
|
+
|
|
104
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
105
|
+
const token = tokens[index];
|
|
106
|
+
addHashedFeature(vector, `token:${token}`, 1);
|
|
107
|
+
if (index > 0) {
|
|
108
|
+
addHashedFeature(vector, `bigram:${tokens[index - 1]}:${token}`, 0.6);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const bounded = `^${token.slice(0, 64)}$`;
|
|
112
|
+
for (let offset = 0; offset <= bounded.length - 3; offset += 1) {
|
|
113
|
+
addHashedFeature(vector, `trigram:${bounded.slice(offset, offset + 3)}`, 0.3);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const norm = Math.sqrt(vector.reduce((sum, value) => sum + (value * value), 0));
|
|
118
|
+
if (norm === 0) {
|
|
119
|
+
vector[0] = 1;
|
|
120
|
+
return vector;
|
|
121
|
+
}
|
|
122
|
+
return vector.map((value) => value / norm);
|
|
123
|
+
}
|
|
124
|
+
|
|
72
125
|
async function getEmbeddingPipeline() {
|
|
73
126
|
const resolved = resolveEmbeddingProfile();
|
|
74
127
|
const report = writeModelFitReport(getFeedbackDir(), { resolved }).report;
|
|
@@ -247,12 +300,33 @@ async function embed(text, options = {}) {
|
|
|
247
300
|
console.warn(`Gemini embedding fallback: ${geminiError.message}`);
|
|
248
301
|
}
|
|
249
302
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
303
|
+
if (hasLocalTransformerProvider()) {
|
|
304
|
+
try {
|
|
305
|
+
const { pipe, profile } = await getEmbeddingPipeline();
|
|
306
|
+
const output = await pipe(truncateForEmbedding(text, profile.activeProfile.maxChars), {
|
|
307
|
+
pooling: 'mean',
|
|
308
|
+
normalize: true,
|
|
309
|
+
});
|
|
310
|
+
return Array.from(output.data); // Float32Array -> plain number[] for LanceDB Arrow serialization
|
|
311
|
+
} catch (transformerError) {
|
|
312
|
+
console.warn(`Transformers.js embedding fallback: ${transformerError.message}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const vector = embedWithFeatureHash(text);
|
|
317
|
+
_lastEmbeddingProfile = {
|
|
318
|
+
generatedAt: new Date().toISOString(),
|
|
319
|
+
source: 'built-in',
|
|
320
|
+
activeProfile: {
|
|
321
|
+
id: 'feature-hash-v1',
|
|
322
|
+
model: 'ThumbGate feature hashing',
|
|
323
|
+
outputDimensionality: FEATURE_HASH_DIMENSIONS,
|
|
324
|
+
task: options.task || 'code retrieval',
|
|
325
|
+
rationale: 'Deterministic zero-dependency local text embedding.',
|
|
326
|
+
},
|
|
327
|
+
fallbackUsed: false,
|
|
328
|
+
};
|
|
329
|
+
return vector;
|
|
256
330
|
}
|
|
257
331
|
|
|
258
332
|
async function upsertFeedback(feedbackEvent) {
|
|
@@ -269,7 +343,8 @@ async function upsertFeedback(feedbackEvent) {
|
|
|
269
343
|
feedbackEvent.whatWorked || '',
|
|
270
344
|
].filter(Boolean).join('. ');
|
|
271
345
|
|
|
272
|
-
// Embed is pure CPU/model work (
|
|
346
|
+
// Embed is pure CPU/model work (managed, optional Transformers.js, built-in,
|
|
347
|
+
// or stub) and deterministic for local providers.
|
|
273
348
|
// for a given input, so no retry is needed here. Retry wraps the table
|
|
274
349
|
// write below, which is the actual I/O failure surface.
|
|
275
350
|
const vector = await embed(textForEmbedding, {
|
|
@@ -364,4 +439,5 @@ module.exports = {
|
|
|
364
439
|
setLanceLoaderForTests,
|
|
365
440
|
setGeminiEmbedderForTests,
|
|
366
441
|
truncateForEmbedding,
|
|
442
|
+
embedWithFeatureHash,
|
|
367
443
|
};
|