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.
Files changed (87) hide show
  1. package/.claude/commands/dashboard.md +11 -1
  2. package/.claude/commands/thumbgate-dashboard.md +23 -8
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.well-known/llms.txt +18 -10
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +66 -3
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/forge/forge.yaml +3 -3
  9. package/adapters/mcp/server-stdio.js +88 -2
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bin/cli.js +8 -8
  12. package/bin/postinstall.js +4 -13
  13. package/commands/dashboard.md +11 -1
  14. package/commands/thumbgate-dashboard.md +23 -8
  15. package/config/agent-outcome-monitor-thresholds.json +63 -0
  16. package/config/evals/agent-outcomes-baseline.json +17 -0
  17. package/config/evals/agent-outcomes-golden.json +412 -0
  18. package/config/evals/prompt-eval-baseline.json +23 -0
  19. package/config/github-about.json +5 -4
  20. package/config/post-deploy-marketing-pages.json +6 -6
  21. package/config/schemas/task-outcome-receipt.schema.json +296 -0
  22. package/docs/integrations/grafana/README.md +109 -0
  23. package/docs/integrations/grafana/thumbgate-revenue-evidence-dashboard.json +1930 -0
  24. package/openapi/openapi.yaml +475 -5
  25. package/package.json +75 -22
  26. package/public/agent-manager.html +10 -11
  27. package/public/agents-cost-savings.html +2 -2
  28. package/public/assets/brand/thumbgate-logo-transparent.svg +6 -11
  29. package/public/assets/brand/thumbgate-mark-inline-v3.svg +11 -10
  30. package/public/assets/brand/thumbgate-mark.svg +10 -11
  31. package/public/blog/inside-your-boundary.html +114 -0
  32. package/public/blog/process-over-outcome-gates.html +119 -0
  33. package/public/blog.html +296 -402
  34. package/public/brand/thumbgate-mark.svg +5 -9
  35. package/public/codex-enterprise.html +2 -2
  36. package/public/compare.html +12 -3
  37. package/public/diagnostic.html +79 -29
  38. package/public/guide.html +4 -4
  39. package/public/index.html +1090 -2098
  40. package/public/install.html +3 -3
  41. package/public/js/buyer-intent.js +33 -18
  42. package/public/numbers.html +2 -2
  43. package/public/pricing.html +268 -408
  44. package/public/pro.html +4 -4
  45. package/scripts/agent-outcome-eval.js +130 -0
  46. package/scripts/agent-outcome-monitor.js +261 -0
  47. package/scripts/agent-reasoning-traces.js +8 -9
  48. package/scripts/async-job-runner.js +107 -13
  49. package/scripts/billing.js +456 -126
  50. package/scripts/buyer-paths.js +102 -0
  51. package/scripts/cli-feedback.js +2 -2
  52. package/scripts/commercial-offer.js +18 -10
  53. package/scripts/durability/step.js +121 -12
  54. package/scripts/external-customer-audit.js +881 -0
  55. package/scripts/feedback-loop.js +26 -0
  56. package/scripts/gates-engine.js +554 -19
  57. package/scripts/grafana-revenue-evidence.js +856 -0
  58. package/scripts/human-escalation.js +265 -0
  59. package/scripts/hybrid-feedback-context.js +93 -50
  60. package/scripts/jsonl-window.js +89 -0
  61. package/scripts/judge-reward-function.js +30 -18
  62. package/scripts/lesson-embedding-index.js +3 -7
  63. package/scripts/meta-agent-loop.js +20 -2
  64. package/scripts/observability-env.js +139 -0
  65. package/scripts/observability-setup.js +55 -0
  66. package/scripts/plausible-domain-config.js +4 -0
  67. package/scripts/prompt-eval.js +81 -4
  68. package/scripts/provider-live-evidence.js +1290 -0
  69. package/scripts/provider-payment-reconciler.js +442 -0
  70. package/scripts/provider-revenue-evidence.js +249 -0
  71. package/scripts/rate-limiter.js +1 -5
  72. package/scripts/revenue-action-eligibility.js +414 -0
  73. package/scripts/revenue-evidence-remediation.js +694 -0
  74. package/scripts/revenue-offer-system.js +709 -0
  75. package/scripts/sales-pipeline.js +1117 -0
  76. package/scripts/schedule-manager.js +249 -0
  77. package/scripts/seo-gsd.js +8 -4
  78. package/scripts/stripe-credentials.js +37 -0
  79. package/scripts/stripe-revenue-catalog-audit.js +363 -0
  80. package/scripts/stripe-revenue-catalog.js +164 -0
  81. package/scripts/task-outcomes.js +425 -0
  82. package/scripts/telemetry-analytics.js +23 -3
  83. package/scripts/tool-contract-validator.js +287 -59
  84. package/scripts/tool-registry.js +143 -0
  85. package/scripts/vector-store.js +83 -7
  86. package/scripts/workflow-intake-queue.js +483 -0
  87. package/src/api/server.js +647 -118
@@ -2,75 +2,303 @@
2
2
 
3
3
  /**
4
4
  * Tool Contract Validator
5
- * Validates tool arguments against the tool's inputSchema.
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
- if (!schema) return { valid: true, errors };
27
+ validateValue(schema, args, '', errors);
28
+ return { valid: errors.length === 0, errors };
29
+ }
10
30
 
11
- if (schema.type === 'object') {
12
- if (typeof args !== 'object' || args === null || Array.isArray(args)) {
13
- errors.push(`Expected object, got ${args === null ? 'null' : Array.isArray(args) ? 'array' : typeof args}`);
14
- return { valid: false, errors };
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
- // Check required fields
18
- if (Array.isArray(schema.required)) {
19
- for (const req of schema.required) {
20
- if (args[req] === undefined || args[req] === null || args[req] === '') {
21
- errors.push(`Missing required parameter: '${req}'`);
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
- // Check properties
27
- if (schema.properties) {
28
- for (const [key, propSchema] of Object.entries(schema.properties)) {
29
- const value = args[key];
30
- if (value === undefined || value === null) continue; // Optional field not provided
31
-
32
- const valType = typeof value;
33
- if (propSchema.type === 'string') {
34
- if (valType !== 'string') {
35
- errors.push(`Parameter '${key}' must be a string (got ${valType})`);
36
- } else if (Array.isArray(propSchema.enum)) {
37
- if (!propSchema.enum.includes(value)) {
38
- errors.push(`Parameter '${key}' must be one of [${propSchema.enum.join(', ')}] (got '${value}')`);
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
- return {
71
- valid: errors.length === 0,
72
- errors,
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 = { validateToolContract };
301
+ module.exports = {
302
+ validateStructuredOutput,
303
+ validateToolContract,
304
+ };
@@ -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.',
@@ -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
- const { pipe, profile } = await getEmbeddingPipeline();
251
- const output = await pipe(truncateForEmbedding(text, profile.activeProfile.maxChars), {
252
- pooling: 'mean',
253
- normalize: true,
254
- });
255
- return Array.from(output.data); // Float32Array -> plain number[] for LanceDB Arrow serialization
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 (transformers.js or stub) — deterministic
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
  };