@princetheprogrammerbtw/husk 0.1.1 → 0.3.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/dist/index.js CHANGED
@@ -235,6 +235,138 @@ function sanitize(sessionId) {
235
235
  return sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
236
236
  }
237
237
 
238
+ // src/memory/vector-inmemory.ts
239
+ var InMemoryVectorStore = class {
240
+ items = /* @__PURE__ */ new Map();
241
+ async upsert(item) {
242
+ this.items.set(item.id, item);
243
+ }
244
+ async search(queryEmbedding, topK) {
245
+ if (this.items.size === 0) return [];
246
+ if (topK <= 0) return [];
247
+ const scored = [];
248
+ for (const item of this.items.values()) {
249
+ const score = cosineSimilarity(queryEmbedding, item.embedding);
250
+ scored.push({
251
+ id: item.id,
252
+ content: item.content,
253
+ score,
254
+ ...item.metadata ? { metadata: item.metadata } : {}
255
+ });
256
+ }
257
+ scored.sort((a, b) => b.score - a.score);
258
+ return scored.slice(0, topK);
259
+ }
260
+ async remove(id) {
261
+ this.items.delete(id);
262
+ }
263
+ async list() {
264
+ return [...this.items.keys()];
265
+ }
266
+ async clear() {
267
+ this.items.clear();
268
+ }
269
+ async count() {
270
+ return this.items.size;
271
+ }
272
+ };
273
+ function cosineSimilarity(a, b) {
274
+ if (a.length !== b.length) {
275
+ throw new Error(`Vector dimension mismatch: ${a.length} vs ${b.length}`);
276
+ }
277
+ let dot = 0;
278
+ let normA = 0;
279
+ let normB = 0;
280
+ for (let i = 0; i < a.length; i++) {
281
+ const ai = a[i] ?? 0;
282
+ const bi = b[i] ?? 0;
283
+ dot += ai * bi;
284
+ normA += ai * ai;
285
+ normB += bi * bi;
286
+ }
287
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
288
+ if (denom === 0) return 0;
289
+ return dot / denom;
290
+ }
291
+
292
+ // src/memory/embedder-hash.ts
293
+ var HashEmbedder = class {
294
+ dimensions;
295
+ ngramSize;
296
+ constructor(options = {}) {
297
+ this.dimensions = options.dimensions ?? 256;
298
+ this.ngramSize = options.ngramSize ?? 3;
299
+ }
300
+ async embed(text) {
301
+ const vec = new Array(this.dimensions).fill(0);
302
+ const normalized = text.toLowerCase();
303
+ for (let i = 0; i <= normalized.length - this.ngramSize; i++) {
304
+ const ngram = normalized.slice(i, i + this.ngramSize);
305
+ const hash = simpleHash(ngram);
306
+ const idx = hash % this.dimensions;
307
+ vec[idx] = (vec[idx] ?? 0) + (hash % 2 === 0 ? 1 : -1);
308
+ }
309
+ const norm = Math.sqrt(vec.reduce((sum, v) => sum + v * v, 0));
310
+ if (norm === 0) return vec;
311
+ return vec.map((v) => v / norm);
312
+ }
313
+ };
314
+ function simpleHash(s) {
315
+ let hash = 5381;
316
+ for (let i = 0; i < s.length; i++) {
317
+ hash = hash * 33 ^ s.charCodeAt(i);
318
+ }
319
+ return hash >>> 0;
320
+ }
321
+
322
+ // src/memory/vector.ts
323
+ function defineMemorySearchTool(options) {
324
+ const { store, embedder, defaultTopK = 5 } = options;
325
+ return {
326
+ name: "MemorySearch",
327
+ description: "Search long-term memory for past interactions. Use this when the user references something you might have seen before, or when you need context that is not in the current conversation.",
328
+ inputSchema: makeMemorySearchSchema(),
329
+ execute: async (input) => {
330
+ const embedding = await embedder.embed(input.query);
331
+ const topK = input.topK ?? defaultTopK;
332
+ const results = await store.search(embedding, topK);
333
+ if (results.length === 0) {
334
+ return { output: "No matching memories found." };
335
+ }
336
+ return {
337
+ output: results.map((r) => `[score=${r.score.toFixed(3)}] ${r.content}`).join("\n")
338
+ };
339
+ }
340
+ };
341
+ }
342
+ function defineRememberTool(options) {
343
+ const { store, embedder } = options;
344
+ return {
345
+ name: "Remember",
346
+ description: "Save a piece of information to long-term memory. The next time you (or another agent) need this, call MemorySearch to recall it. Use this for user preferences, important decisions, or any fact that should survive across sessions.",
347
+ inputSchema: makeRememberSchema(),
348
+ execute: async (input) => {
349
+ const embedding = await embedder.embed(input.content);
350
+ await store.upsert({ id: input.id, content: input.content, embedding });
351
+ return { output: `Remembered: ${input.content.slice(0, 80)}` };
352
+ }
353
+ };
354
+ }
355
+ function makeMemorySearchSchema() {
356
+ const properties = {
357
+ query: { type: "string", description: "Natural-language search query." },
358
+ topK: { type: "integer", description: "Number of results to return. Default: 5." }
359
+ };
360
+ return { type: "object", properties, required: ["query"] };
361
+ }
362
+ function makeRememberSchema() {
363
+ const properties = {
364
+ id: { type: "string", description: "Unique identifier for this memory (caller-provided)." },
365
+ content: { type: "string", description: "The text to remember." }
366
+ };
367
+ return { type: "object", properties, required: ["id", "content"] };
368
+ }
369
+
238
370
  // src/core/steering.ts
239
371
  function buildSystemPrompt(steering) {
240
372
  const parts = [];
@@ -821,6 +953,27 @@ function mapStopReason2(reason) {
821
953
  }
822
954
  }
823
955
 
956
+ // src/providers/ollama.ts
957
+ var DEFAULT_BASE_URL = "http://localhost:11434/v1";
958
+ var DEFAULT_MODEL = "llama3.2";
959
+ var PLACEHOLDER_API_KEY = "ollama";
960
+ var OllamaProvider = class {
961
+ name = "ollama";
962
+ model;
963
+ inner;
964
+ constructor(options = {}) {
965
+ this.model = options.model ?? DEFAULT_MODEL;
966
+ this.inner = new OpenAIProvider({
967
+ apiKey: options.apiKey ?? PLACEHOLDER_API_KEY,
968
+ model: this.model,
969
+ baseURL: options.baseURL ?? DEFAULT_BASE_URL
970
+ });
971
+ }
972
+ chat(request) {
973
+ return this.inner.chat(request);
974
+ }
975
+ };
976
+
824
977
  // src/tools/registry.ts
825
978
  function defineTool(tool) {
826
979
  return {
@@ -1103,9 +1256,293 @@ function truncateOutput(output, limit) {
1103
1256
  ... (${lines.length - limit} more matches truncated)`;
1104
1257
  }
1105
1258
 
1259
+ // src/evals/types.ts
1260
+ function equals(expected) {
1261
+ return (result) => {
1262
+ const pass = result.output === expected;
1263
+ return pass ? { name: `equals(${JSON.stringify(expected).slice(0, 40)})`, pass: true } : {
1264
+ name: `equals(${JSON.stringify(expected).slice(0, 40)})`,
1265
+ pass: false,
1266
+ message: `Expected ${JSON.stringify(expected)}, got ${JSON.stringify(result.output).slice(0, 200)}`
1267
+ };
1268
+ };
1269
+ }
1270
+ function contains(needle) {
1271
+ return (result) => {
1272
+ const pass = result.output.includes(needle);
1273
+ return pass ? { name: `contains(${JSON.stringify(needle).slice(0, 40)})`, pass: true } : {
1274
+ name: `contains(${JSON.stringify(needle).slice(0, 40)})`,
1275
+ pass: false,
1276
+ message: `Expected output to contain ${JSON.stringify(needle)}, got ${JSON.stringify(result.output).slice(0, 200)}`
1277
+ };
1278
+ };
1279
+ }
1280
+ function matches(pattern) {
1281
+ return (result) => {
1282
+ const m = pattern.exec(result.output);
1283
+ return {
1284
+ name: `matches(${pattern})`,
1285
+ pass: m !== null,
1286
+ ...m === null ? {
1287
+ message: `Output did not match ${pattern}: ${JSON.stringify(result.output).slice(0, 200)}`
1288
+ } : {}
1289
+ };
1290
+ };
1291
+ }
1292
+ function fn(name, predicate, message) {
1293
+ return (result) => {
1294
+ const pass = predicate(result.output);
1295
+ return {
1296
+ name,
1297
+ pass,
1298
+ ...pass ? {} : { message: message ?? `Predicate ${name} failed` }
1299
+ };
1300
+ };
1301
+ }
1302
+ function notContains(needle) {
1303
+ return (result) => {
1304
+ const pass = !result.output.includes(needle);
1305
+ return pass ? { name: `notContains(${JSON.stringify(needle).slice(0, 40)})`, pass: true } : {
1306
+ name: `notContains(${JSON.stringify(needle).slice(0, 40)})`,
1307
+ pass: false,
1308
+ message: `Output should not contain ${JSON.stringify(needle)} but did: ${JSON.stringify(result.output).slice(0, 200)}`
1309
+ };
1310
+ };
1311
+ }
1312
+ function lengthBetween(min, max) {
1313
+ return (result) => {
1314
+ const len = result.output.length;
1315
+ const pass = len >= min && len <= max;
1316
+ return pass ? { name: `lengthBetween(${min}, ${max})`, pass: true } : {
1317
+ name: `lengthBetween(${min}, ${max})`,
1318
+ pass: false,
1319
+ message: `Output length ${len} not in [${min}, ${max}]`
1320
+ };
1321
+ };
1322
+ }
1323
+
1324
+ // src/evals/runner.ts
1325
+ async function runSuite(suite, factory, options = {}) {
1326
+ const start = Date.now();
1327
+ const results = [];
1328
+ let passed = 0;
1329
+ for (const c of suite.cases) {
1330
+ options.onCaseStart?.(c.name);
1331
+ const caseResult = await runCase(c, factory);
1332
+ results.push(caseResult);
1333
+ if (caseResult.passed) passed += 1;
1334
+ options.onCaseEnd?.(caseResult);
1335
+ if (options.failFast && !caseResult.passed) {
1336
+ break;
1337
+ }
1338
+ }
1339
+ return {
1340
+ suiteName: suite.name,
1341
+ results,
1342
+ passed,
1343
+ total: suite.cases.length,
1344
+ durationMs: Date.now() - start
1345
+ };
1346
+ }
1347
+ async function runCase(c, factory) {
1348
+ const start = Date.now();
1349
+ const agent = await factory();
1350
+ let agentResult;
1351
+ try {
1352
+ agentResult = await agent.run(c.input);
1353
+ } catch (err) {
1354
+ const message = err instanceof Error ? err.message : String(err);
1355
+ const errorAssertionResult = {
1356
+ pass: false,
1357
+ name: "agent.run",
1358
+ message: `agent.run threw: ${message}`
1359
+ };
1360
+ return {
1361
+ caseName: c.name,
1362
+ passed: false,
1363
+ assertionResults: [errorAssertionResult],
1364
+ agentResult: {
1365
+ output: "",
1366
+ messages: [],
1367
+ iterations: 0,
1368
+ usage: { inputTokens: 0, outputTokens: 0 },
1369
+ durationMs: Date.now() - start
1370
+ },
1371
+ durationMs: Date.now() - start
1372
+ };
1373
+ }
1374
+ const assertionResults = [];
1375
+ for (const a of c.assertions) {
1376
+ const r = await a(agentResult);
1377
+ assertionResults.push(r);
1378
+ }
1379
+ const allPassed = assertionResults.every((r) => r.pass);
1380
+ return {
1381
+ caseName: c.name,
1382
+ passed: allPassed,
1383
+ assertionResults,
1384
+ agentResult,
1385
+ durationMs: Date.now() - start
1386
+ };
1387
+ }
1388
+ function defineSuite(suite) {
1389
+ return {
1390
+ name: suite.name,
1391
+ cases: suite.cases
1392
+ };
1393
+ }
1394
+
1395
+ // src/obs/tracer.ts
1396
+ var NoopTracer = class {
1397
+ startSpan(_options, _parent) {
1398
+ const ctx = {
1399
+ traceId: "0",
1400
+ spanId: "0"
1401
+ };
1402
+ return {
1403
+ context: ctx,
1404
+ addEvent: () => {
1405
+ },
1406
+ setAttribute: () => {
1407
+ },
1408
+ recordException: () => {
1409
+ },
1410
+ setStatus: () => {
1411
+ },
1412
+ end: () => {
1413
+ }
1414
+ };
1415
+ }
1416
+ };
1417
+
1418
+ // src/obs/mapper.ts
1419
+ var EventTracer = class {
1420
+ tracer;
1421
+ traceSpan = null;
1422
+ iterationSpan = null;
1423
+ toolSpans = /* @__PURE__ */ new Map();
1424
+ constructor(tracer) {
1425
+ this.tracer = tracer;
1426
+ }
1427
+ /**
1428
+ * Bind as an event handler: `agent.onAny(tracer.onEvent.bind(tracer))`
1429
+ */
1430
+ onEvent = (event) => {
1431
+ switch (event.type) {
1432
+ case "agent:start": {
1433
+ this.traceSpan = this.tracer.startSpan({
1434
+ name: "agent.run",
1435
+ kind: "internal",
1436
+ attributes: {
1437
+ "husk.input": event.input,
1438
+ "husk.session_id": event.sessionId
1439
+ }
1440
+ });
1441
+ break;
1442
+ }
1443
+ case "agent:iteration": {
1444
+ this.iterationSpan?.end();
1445
+ this.iterationSpan = this.tracer.startSpan(
1446
+ {
1447
+ name: `iteration.${event.iteration}`,
1448
+ kind: "internal",
1449
+ attributes: { "husk.iteration": event.iteration }
1450
+ },
1451
+ this.traceSpan?.context
1452
+ );
1453
+ break;
1454
+ }
1455
+ case "provider:request": {
1456
+ this.iterationSpan?.addEvent("provider.request", {
1457
+ "provider.model": event.request.model
1458
+ });
1459
+ break;
1460
+ }
1461
+ case "provider:response": {
1462
+ if (this.iterationSpan) {
1463
+ this.iterationSpan.setAttribute(
1464
+ "provider.input_tokens",
1465
+ event.response.usage.inputTokens
1466
+ );
1467
+ this.iterationSpan.setAttribute(
1468
+ "provider.output_tokens",
1469
+ event.response.usage.outputTokens
1470
+ );
1471
+ this.iterationSpan.setAttribute("provider.stop_reason", event.response.stopReason);
1472
+ this.iterationSpan.setAttribute("provider.duration_ms", event.durationMs);
1473
+ }
1474
+ break;
1475
+ }
1476
+ case "tool:call": {
1477
+ const span = this.tracer.startSpan(
1478
+ {
1479
+ name: `tool.${event.name}`,
1480
+ kind: "internal",
1481
+ attributes: {
1482
+ "tool.name": event.name,
1483
+ "tool.input": JSON.stringify(event.input)
1484
+ }
1485
+ },
1486
+ this.iterationSpan?.context ?? this.traceSpan?.context
1487
+ );
1488
+ this.toolSpans.set(event.id, span);
1489
+ break;
1490
+ }
1491
+ case "tool:result": {
1492
+ const span = this.toolSpans.get(event.id);
1493
+ if (span) {
1494
+ span.setAttribute("tool.is_error", event.result.isError ?? false);
1495
+ span.setAttribute("tool.duration_ms", event.durationMs);
1496
+ if (event.result.isError) {
1497
+ span.setStatus("error", event.result.output);
1498
+ } else {
1499
+ span.setStatus("ok");
1500
+ }
1501
+ span.end();
1502
+ this.toolSpans.delete(event.id);
1503
+ }
1504
+ break;
1505
+ }
1506
+ case "agent:end": {
1507
+ this.iterationSpan?.end();
1508
+ this.iterationSpan = null;
1509
+ if (this.traceSpan) {
1510
+ this.traceSpan.setAttribute("husk.iterations", event.iterations);
1511
+ this.traceSpan.setAttribute("husk.duration_ms", event.durationMs);
1512
+ this.traceSpan.setStatus("ok");
1513
+ this.traceSpan.end();
1514
+ this.traceSpan = null;
1515
+ }
1516
+ break;
1517
+ }
1518
+ case "agent:error": {
1519
+ if (this.traceSpan) {
1520
+ this.traceSpan.recordException(event.error);
1521
+ this.traceSpan.setStatus("error", event.error.message);
1522
+ this.traceSpan.end();
1523
+ this.traceSpan = null;
1524
+ }
1525
+ this.iterationSpan?.end();
1526
+ this.iterationSpan = null;
1527
+ for (const span of this.toolSpans.values()) {
1528
+ span.end();
1529
+ }
1530
+ this.toolSpans.clear();
1531
+ break;
1532
+ }
1533
+ case "agent:message": {
1534
+ this.iterationSpan?.addEvent("message", {
1535
+ "message.role": event.message.role
1536
+ });
1537
+ break;
1538
+ }
1539
+ }
1540
+ };
1541
+ };
1542
+
1106
1543
  // src/index.ts
1107
1544
  var VERSION = "0.1.0";
1108
1545
 
1109
- export { Agent, AgentEventEmitter, AnthropicProvider, Bash, ConsoleLogger, Edit, FileStore, Grep, InMemoryStore, OpenAIProvider, Read, VERSION, Write, arrayField, booleanField, buildExampleMessages, buildSystemPrompt, defineTool, integerField, logEventsTo, numberField, objectField, objectSchema, stringField };
1546
+ export { Agent, AgentEventEmitter, AnthropicProvider, Bash, ConsoleLogger, Edit, EventTracer, FileStore, Grep, HashEmbedder, InMemoryStore, InMemoryVectorStore, NoopTracer, OllamaProvider, OpenAIProvider, Read, VERSION, Write, arrayField, booleanField, buildExampleMessages, buildSystemPrompt, contains, cosineSimilarity, defineMemorySearchTool, defineRememberTool, defineSuite, defineTool, equals, fn, integerField, lengthBetween, logEventsTo, matches, notContains, numberField, objectField, objectSchema, runSuite, stringField };
1110
1547
  //# sourceMappingURL=index.js.map
1111
1548
  //# sourceMappingURL=index.js.map