@princetheprogrammerbtw/husk 0.2.0 → 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.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { T as Tracer } from './tracer-y41CTrNG.js';
2
+ export { N as NoopTracer, b as Span, a as SpanContext, c as SpanKind, S as SpanOptions } from './tracer-y41CTrNG.js';
3
+
1
4
  /**
2
5
  * Husk — core type definitions.
3
6
  *
@@ -352,6 +355,169 @@ declare class FileStore implements MemoryStore {
352
355
  private fileFor;
353
356
  }
354
357
 
358
+ /**
359
+ * Husk — vector memory types and interfaces.
360
+ *
361
+ * Long-term memory for agents, separate from the short-term
362
+ * Message[] memory in src/core/memory.ts. Vector stores are queried
363
+ * by semantic similarity: you provide a query, get back the top-K
364
+ * most similar past items.
365
+ *
366
+ * Design choice: the agent accesses vector memory through TOOLS
367
+ * (MemorySearch, Remember) rather than automatic injection. This
368
+ * means:
369
+ * - The model decides when to recall (avoids noisy "here's some
370
+ * vaguely related past conversation" injections)
371
+ * - The same memory store can be used by multiple agents
372
+ * - Vector memory integrates with the existing tool framework, no
373
+ * agent-loop changes
374
+ *
375
+ * The VectorStore interface is intentionally simple so users can
376
+ * plug in their own backend (Chroma, Pinecone, sqlite-vec, etc.).
377
+ * Husk ships one in-memory backend for v0.3.0.
378
+ */
379
+
380
+ /**
381
+ * A single memory item: the text, its embedding, and optional
382
+ * metadata for filtering or display.
383
+ */
384
+ interface MemoryItem {
385
+ /** Unique id (caller-provided, allows updates/deletes). */
386
+ readonly id: string;
387
+ /** The text content. What the model sees when this is recalled. */
388
+ readonly content: string;
389
+ /** Pre-computed embedding vector. */
390
+ readonly embedding: readonly number[];
391
+ /** Optional metadata (timestamp, source, tags, etc.). */
392
+ readonly metadata?: Readonly<Record<string, unknown>>;
393
+ }
394
+ /**
395
+ * The result of a similarity search: the matched item plus its
396
+ * similarity score (higher = more similar). Score is implementation-
397
+ * dependent (cosine similarity for the in-memory backend).
398
+ */
399
+ interface SearchResult {
400
+ readonly id: string;
401
+ readonly content: string;
402
+ readonly score: number;
403
+ readonly metadata?: Readonly<Record<string, unknown>>;
404
+ }
405
+ interface VectorStore {
406
+ /** Add or update a memory item. */
407
+ upsert(item: MemoryItem): Promise<void>;
408
+ /** Search for the top-K most similar items to the query embedding. */
409
+ search(queryEmbedding: readonly number[], topK: number): Promise<readonly SearchResult[]>;
410
+ /** Remove a memory by id. No-op if not present. */
411
+ remove(id: string): Promise<void>;
412
+ /** List all memory ids (for debugging/inspection). */
413
+ list(): Promise<readonly string[]>;
414
+ /** Remove all memories. */
415
+ clear(): Promise<void>;
416
+ /** Total count of memories. */
417
+ count(): Promise<number>;
418
+ }
419
+ interface EmbeddingProvider {
420
+ /** Generate an embedding vector for the given text. */
421
+ embed(text: string): Promise<readonly number[]>;
422
+ /** The dimensionality of the vectors this provider produces. */
423
+ readonly dimensions: number;
424
+ }
425
+ interface MemoryToolOptions {
426
+ /** The vector store to read/write. */
427
+ readonly store: VectorStore;
428
+ /** The embedding provider (used inside the tools). */
429
+ readonly embedder: EmbeddingProvider;
430
+ /**
431
+ * Default top-K for searches when the agent doesn't specify.
432
+ * Default: 5.
433
+ */
434
+ readonly defaultTopK?: number;
435
+ }
436
+ /**
437
+ * Build the MemorySearch tool: agent calls it with a natural-
438
+ * language query, gets back the top-K most similar past items.
439
+ */
440
+ declare function defineMemorySearchTool(options: MemoryToolOptions): ToolDefinition<{
441
+ query: string;
442
+ topK?: number;
443
+ }>;
444
+ /**
445
+ * Build the Remember tool: agent calls it to save a fact/observation
446
+ * to long-term memory for later recall.
447
+ */
448
+ declare function defineRememberTool(options: MemoryToolOptions): ToolDefinition<{
449
+ id: string;
450
+ content: string;
451
+ }>;
452
+
453
+ /**
454
+ * Husk — in-memory vector store.
455
+ *
456
+ * Naive O(n) linear scan with cosine similarity. Fine for thousands
457
+ * of memories; slow for millions. The VectorStore interface is
458
+ * pluggable so users can swap in Chroma, Pinecone, sqlite-vec, or
459
+ * any ANN index for production scale.
460
+ *
461
+ * Why we ship this: zero external dependencies, deterministic
462
+ * behavior for testing, good enough for the common case of
463
+ * "remember user preferences across sessions" (a few hundred items).
464
+ *
465
+ * For very large stores, see:
466
+ * - chroma (separate server, ~3-line adapter)
467
+ * - pinecone (managed, REST API)
468
+ * - sqlite-vec (in-process, single binary)
469
+ * - hnswlib-node (in-process, true ANN)
470
+ */
471
+
472
+ declare class InMemoryVectorStore implements VectorStore {
473
+ private readonly items;
474
+ upsert(item: MemoryItem): Promise<void>;
475
+ search(queryEmbedding: readonly number[], topK: number): Promise<readonly SearchResult[]>;
476
+ remove(id: string): Promise<void>;
477
+ list(): Promise<readonly string[]>;
478
+ clear(): Promise<void>;
479
+ count(): Promise<number>;
480
+ }
481
+ /**
482
+ * Cosine similarity in [-1, 1]. Returns 0 if either vector is zero.
483
+ * (1.0 = identical direction, 0 = orthogonal, -1 = opposite)
484
+ */
485
+ declare function cosineSimilarity(a: readonly number[], b: readonly number[]): number;
486
+
487
+ /**
488
+ * Husk — simple embedding provider for testing and offline use.
489
+ *
490
+ * Produces deterministic pseudo-embeddings from text by hashing
491
+ * character n-grams into a fixed-dimension vector. NOT a real
492
+ * embedding model — semantic quality is poor, but it's:
493
+ *
494
+ * - Deterministic (same text → same vector)
495
+ * - Zero-dependency (no API call, no model file)
496
+ * - Useful for tests, demos, and offline development
497
+ *
498
+ * For real semantic search, use a real EmbeddingProvider:
499
+ * - OpenAIEmbedder (text-embedding-3-small, 1536 dims)
500
+ * - sentence-transformers via a small Python sidecar
501
+ * - CohereEmbedder, VoyageEmbedder, etc.
502
+ *
503
+ * The "similarity" this produces is bag-of-chars similarity, not
504
+ * semantic similarity. Two texts with similar character n-grams
505
+ * will score high even if they mean different things.
506
+ */
507
+
508
+ interface HashEmbedderOptions {
509
+ /** Output vector dimensions. Default: 256. */
510
+ readonly dimensions?: number;
511
+ /** N-gram size for the hashing. Default: 3 (trigrams). */
512
+ readonly ngramSize?: number;
513
+ }
514
+ declare class HashEmbedder implements EmbeddingProvider {
515
+ readonly dimensions: number;
516
+ private readonly ngramSize;
517
+ constructor(options?: HashEmbedderOptions);
518
+ embed(text: string): Promise<readonly number[]>;
519
+ }
520
+
355
521
  /**
356
522
  * Husk — steering prompt builder.
357
523
  *
@@ -865,69 +1031,6 @@ declare function defineSuite(suite: {
865
1031
  cases: readonly EvalCase[];
866
1032
  }): EvalSuite;
867
1033
 
868
- /**
869
- * Husk — observability types (tracer interface).
870
- *
871
- * A minimal, OTel-inspired tracer interface. Husk's events are mapped
872
- * to spans by the mapper in ./tracer.ts. Users can plug in the real
873
- * @opentelemetry/api tracer via the adapter (see ./otel-adapter.ts)
874
- * or any other compatible backend.
875
- *
876
- * Design choice: we don't depend on @opentelemetry/api directly. The
877
- * interface here is a strict subset of OTel's Span interface (just
878
- * what's needed for agent observability). Keeping the dep out of
879
- * Husk's core means users who don't need OTel pay nothing for it.
880
- *
881
- * For users who want full OTel:
882
- * import { trace } from '@opentelemetry/api';
883
- * import { toOtelTracer } from '@princetheprogrammerbtw/husk/otel-adapter';
884
- * agent.onAny(toOtelTracer(trace.getTracer('husk')).onEvent);
885
- */
886
- type SpanKind = 'internal' | 'client' | 'server';
887
- interface SpanContext {
888
- /** Unique trace id (all spans in one agent.run share this). */
889
- readonly traceId: string;
890
- /** Unique span id. */
891
- readonly spanId: string;
892
- /** Parent span id, if any. */
893
- readonly parentSpanId?: string;
894
- }
895
- interface SpanOptions {
896
- readonly name: string;
897
- readonly kind?: SpanKind;
898
- readonly attributes?: Readonly<Record<string, unknown>>;
899
- readonly startTimeNs?: bigint;
900
- }
901
- interface Span {
902
- readonly context: SpanContext;
903
- /** Record an event (timestamped annotation) on the span. */
904
- addEvent(name: string, attributes?: Record<string, unknown>): void;
905
- /** Set or update an attribute on the span. */
906
- setAttribute(key: string, value: string | number | boolean | null): void;
907
- /** Record an exception. */
908
- recordException(err: Error): void;
909
- /** Mark the span as failed. */
910
- setStatus(status: 'ok' | 'error', message?: string): void;
911
- /** End the span. Must be called exactly once. */
912
- end(endTimeNs?: bigint): void;
913
- }
914
- interface Tracer {
915
- /**
916
- * Start a new span. If parent is provided, the new span becomes a
917
- * child of it. Returns the new span; caller is responsible for
918
- * calling .end() on it.
919
- */
920
- startSpan(options: SpanOptions, parent?: SpanContext): Span;
921
- }
922
- /**
923
- * A tracer that does nothing. Used when no real tracer is configured.
924
- * Zero overhead — every method is a no-op, so the cost is one virtual
925
- * call per event.
926
- */
927
- declare class NoopTracer implements Tracer {
928
- startSpan(_options: SpanOptions, _parent?: SpanContext): Span;
929
- }
930
-
931
1034
  /**
932
1035
  * Husk — agent event → tracer mapper.
933
1036
  *
@@ -967,4 +1070,4 @@ declare class EventTracer {
967
1070
  */
968
1071
  declare const VERSION = "0.1.0";
969
1072
 
970
- export { Agent, type AgentConfig, type AgentEvent, AgentEventEmitter, type AgentEventHandler, type AgentFactory, type AgentResult, AnthropicProvider, type AnthropicProviderOptions, type Assertion, type AssertionResult, Bash, type BashInput, type CaseResult, type ChatChunk, type ChatRequest, type ChatResponse, ConsoleLogger, type ContentBlock, Edit, type EditInput, type EvalCase, type EvalSuite, EventTracer, type Example, FileStore, type FileStoreOptions, Grep, type GrepInput, InMemoryStore, type JSONSchema, type JSONSchemaField, type LogLevel, type Logger, type MemoryStore, type Message, type MessageContent, NoopTracer, OllamaProvider, type OllamaProviderOptions, OpenAIProvider, type OpenAIProviderOptions, type Provider, Read, type ReadInput, type Role, type RunSuiteOptions, type Span, type SpanContext, type SpanKind, type SpanOptions, type SteeringConfig, type StopReason, type SuiteResult, type TextBlock, type TokenUsage, type ToolContext, type ToolDefinition, type ToolResult, type ToolResultBlock, type ToolUseBlock, type Tracer, VERSION, Write, type WriteInput, arrayField, booleanField, buildExampleMessages, buildSystemPrompt, contains, defineSuite, defineTool, equals, fn, integerField, lengthBetween, logEventsTo, matches, notContains, numberField, objectField, objectSchema, runSuite, stringField };
1073
+ export { Agent, type AgentConfig, type AgentEvent, AgentEventEmitter, type AgentEventHandler, type AgentFactory, type AgentResult, AnthropicProvider, type AnthropicProviderOptions, type Assertion, type AssertionResult, Bash, type BashInput, type CaseResult, type ChatChunk, type ChatRequest, type ChatResponse, ConsoleLogger, type ContentBlock, Edit, type EditInput, type EmbeddingProvider, type EvalCase, type EvalSuite, EventTracer, type Example, FileStore, type FileStoreOptions, Grep, type GrepInput, HashEmbedder, type HashEmbedderOptions, InMemoryStore, InMemoryVectorStore, type JSONSchema, type JSONSchemaField, type LogLevel, type Logger, type MemoryItem, type MemoryStore, type MemoryToolOptions, type Message, type MessageContent, OllamaProvider, type OllamaProviderOptions, OpenAIProvider, type OpenAIProviderOptions, type Provider, Read, type ReadInput, type Role, type RunSuiteOptions, type SearchResult, type SteeringConfig, type StopReason, type SuiteResult, type TextBlock, type TokenUsage, type ToolContext, type ToolDefinition, type ToolResult, type ToolResultBlock, type ToolUseBlock, Tracer, VERSION, type VectorStore, Write, type WriteInput, arrayField, booleanField, buildExampleMessages, buildSystemPrompt, contains, cosineSimilarity, defineMemorySearchTool, defineRememberTool, defineSuite, defineTool, equals, fn, integerField, lengthBetween, logEventsTo, matches, notContains, numberField, objectField, objectSchema, runSuite, stringField };
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 = [];
@@ -1411,6 +1543,6 @@ var EventTracer = class {
1411
1543
  // src/index.ts
1412
1544
  var VERSION = "0.1.0";
1413
1545
 
1414
- export { Agent, AgentEventEmitter, AnthropicProvider, Bash, ConsoleLogger, Edit, EventTracer, FileStore, Grep, InMemoryStore, NoopTracer, OllamaProvider, OpenAIProvider, Read, VERSION, Write, arrayField, booleanField, buildExampleMessages, buildSystemPrompt, contains, defineSuite, defineTool, equals, fn, integerField, lengthBetween, logEventsTo, matches, notContains, numberField, objectField, objectSchema, runSuite, 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 };
1415
1547
  //# sourceMappingURL=index.js.map
1416
1548
  //# sourceMappingURL=index.js.map