@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.
@@ -0,0 +1,49 @@
1
+ import { Tracer as Tracer$1 } from '@opentelemetry/api';
2
+ import { T as Tracer, S as SpanOptions, a as SpanContext, b as Span } from '../tracer-y41CTrNG.js';
3
+
4
+ /**
5
+ * Husk — OpenTelemetry adapter.
6
+ *
7
+ * Bridges Husk's minimal Tracer interface to the real
8
+ * @opentelemetry/api Tracer. Users who want production observability
9
+ * install @opentelemetry/api alongside Husk, then use this adapter
10
+ * to convert their OTel tracer into a Husk Tracer for use with
11
+ * EventTracer.
12
+ *
13
+ * Subpath import: '@princetheprogrammerbtw/husk/otel'
14
+ *
15
+ * @opentelemetry/api is declared as an *optional peer* dependency.
16
+ * If you try to use this subpath without installing OTel, you'll
17
+ * get a clear import error.
18
+ *
19
+ * Usage:
20
+ *
21
+ * // 1. Set up OTel (your existing code)
22
+ * import { trace } from '@opentelemetry/api';
23
+ * const otelTracer = trace.getTracer('my-app', '1.0.0');
24
+ *
25
+ * // 2. Bridge to Husk
26
+ * import { EventTracer } from '@princetheprogrammerbtw/husk';
27
+ * import { OtelTracerAdapter } from '@princetheprogrammerbtw/husk/otel';
28
+ * const huskTracer = new OtelTracerAdapter(otelTracer);
29
+ *
30
+ * // 3. Wire up the agent
31
+ * const agent = new Agent({ model: ... });
32
+ * agent.onAny(new EventTracer(huskTracer).onEvent);
33
+ *
34
+ * // 4. Configure your OTel exporter as usual (OTLP, Jaeger, etc.)
35
+ * // Husk's events now show up as spans in your backend.
36
+ */
37
+
38
+ interface OtelTracerAdapterOptions {
39
+ /** Optional attribute transformer. Default: pass through. */
40
+ readonly transformAttribute?: (key: string, value: string | number | boolean | null) => string | number | boolean;
41
+ }
42
+ declare class OtelTracerAdapter implements Tracer {
43
+ private readonly otel;
44
+ private readonly options;
45
+ constructor(otel: Tracer$1, options?: OtelTracerAdapterOptions);
46
+ startSpan(options: SpanOptions, _parent?: SpanContext): Span;
47
+ }
48
+
49
+ export { OtelTracerAdapter, type OtelTracerAdapterOptions };
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ // src/otel/index.ts
3
+ var OtelTracerAdapter = class {
4
+ otel;
5
+ options;
6
+ constructor(otel, options = {}) {
7
+ this.otel = otel;
8
+ this.options = options;
9
+ }
10
+ startSpan(options, _parent) {
11
+ const otelSpan = this.otel.startSpan(options.name, {
12
+ kind: mapKind(options.kind),
13
+ attributes: stringifyAttrs(options.attributes)
14
+ });
15
+ return new OtelSpanAdapter(otelSpan, this.options);
16
+ }
17
+ };
18
+ var OtelSpanAdapter = class {
19
+ context;
20
+ otel;
21
+ constructor(otel, _options) {
22
+ this.otel = otel;
23
+ const ctx = otel.spanContext();
24
+ this.context = {
25
+ traceId: ctx.traceId,
26
+ spanId: ctx.spanId
27
+ };
28
+ }
29
+ addEvent(name, attributes) {
30
+ this.otel.addEvent(name, stringifyAttrs(attributes));
31
+ }
32
+ setAttribute(key, value) {
33
+ this.otel.setAttribute(key, value === null ? "" : value);
34
+ }
35
+ recordException(err) {
36
+ this.otel.recordException(err);
37
+ }
38
+ setStatus(status, message) {
39
+ if (status === "ok") {
40
+ this.otel.setStatus({ code: 1 });
41
+ } else {
42
+ this.otel.setStatus({ code: 2, message: message ?? "error" });
43
+ }
44
+ }
45
+ end(_endTimeNs) {
46
+ this.otel.end();
47
+ }
48
+ };
49
+ function mapKind(kind) {
50
+ switch (kind) {
51
+ case "client":
52
+ return 2;
53
+ case "server":
54
+ return 1;
55
+ default:
56
+ return 0;
57
+ }
58
+ }
59
+ function stringifyAttrs(attrs) {
60
+ if (!attrs) return {};
61
+ const out = {};
62
+ for (const [k, v] of Object.entries(attrs)) {
63
+ if (v === null || v === void 0) continue;
64
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
65
+ out[k] = v;
66
+ } else {
67
+ out[k] = JSON.stringify(v);
68
+ }
69
+ }
70
+ return out;
71
+ }
72
+
73
+ export { OtelTracerAdapter };
74
+ //# sourceMappingURL=index.js.map
75
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/otel/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Husk — OpenTelemetry adapter.\n *\n * Bridges Husk's minimal Tracer interface to the real\n * @opentelemetry/api Tracer. Users who want production observability\n * install @opentelemetry/api alongside Husk, then use this adapter\n * to convert their OTel tracer into a Husk Tracer for use with\n * EventTracer.\n *\n * Subpath import: '@princetheprogrammerbtw/husk/otel'\n *\n * @opentelemetry/api is declared as an *optional peer* dependency.\n * If you try to use this subpath without installing OTel, you'll\n * get a clear import error.\n *\n * Usage:\n *\n * // 1. Set up OTel (your existing code)\n * import { trace } from '@opentelemetry/api';\n * const otelTracer = trace.getTracer('my-app', '1.0.0');\n *\n * // 2. Bridge to Husk\n * import { EventTracer } from '@princetheprogrammerbtw/husk';\n * import { OtelTracerAdapter } from '@princetheprogrammerbtw/husk/otel';\n * const huskTracer = new OtelTracerAdapter(otelTracer);\n *\n * // 3. Wire up the agent\n * const agent = new Agent({ model: ... });\n * agent.onAny(new EventTracer(huskTracer).onEvent);\n *\n * // 4. Configure your OTel exporter as usual (OTLP, Jaeger, etc.)\n * // Husk's events now show up as spans in your backend.\n */\n\nimport type { Span as OtelSpan, Tracer as OtelTracer } from '@opentelemetry/api';\nimport type {\n Span as HuskSpan,\n SpanContext,\n SpanKind,\n SpanOptions,\n Tracer,\n} from '../obs/tracer.js';\n\nexport interface OtelTracerAdapterOptions {\n /** Optional attribute transformer. Default: pass through. */\n readonly transformAttribute?: (\n key: string,\n value: string | number | boolean | null,\n ) => string | number | boolean;\n}\n\nexport class OtelTracerAdapter implements Tracer {\n private readonly otel: OtelTracer;\n private readonly options: OtelTracerAdapterOptions;\n\n constructor(otel: OtelTracer, options: OtelTracerAdapterOptions = {}) {\n this.otel = otel;\n this.options = options;\n }\n\n startSpan(options: SpanOptions, _parent?: SpanContext): HuskSpan {\n const otelSpan = this.otel.startSpan(options.name, {\n kind: mapKind(options.kind),\n attributes: stringifyAttrs(options.attributes),\n });\n return new OtelSpanAdapter(otelSpan, this.options);\n }\n}\n\n// ───────────────────────────────────────────────────────────────────\n// Span adapter\n// ───────────────────────────────────────────────────────────────────\n\nclass OtelSpanAdapter implements HuskSpan {\n readonly context: SpanContext;\n private readonly otel: OtelSpan;\n\n constructor(otel: OtelSpan, _options: OtelTracerAdapterOptions) {\n this.otel = otel;\n const ctx = otel.spanContext();\n this.context = {\n traceId: ctx.traceId,\n spanId: ctx.spanId,\n };\n }\n\n addEvent(name: string, attributes?: Record<string, unknown>): void {\n this.otel.addEvent(name, stringifyAttrs(attributes));\n }\n\n setAttribute(key: string, value: string | number | boolean | null): void {\n // OTel attributes can't be null; encode as empty string.\n this.otel.setAttribute(key, value === null ? '' : value);\n }\n\n recordException(err: Error): void {\n this.otel.recordException(err);\n }\n\n setStatus(status: 'ok' | 'error', message?: string): void {\n if (status === 'ok') {\n this.otel.setStatus({ code: 1 }); // OTel SpanStatusCode.OK\n } else {\n this.otel.setStatus({ code: 2, message: message ?? 'error' }); // OTel SpanStatusCode.ERROR\n }\n }\n\n end(_endTimeNs?: bigint): void {\n this.otel.end();\n }\n}\n\n// ───────────────────────────────────────────────────────────────────\n// Helpers\n// ───────────────────────────────────────────────────────────────────\n\nfunction mapKind(kind: SpanKind | undefined): 0 | 1 | 2 {\n // OTel SpanKind: 0=INTERNAL, 1=SERVER, 2=CLIENT\n switch (kind) {\n case 'client':\n return 2;\n case 'server':\n return 1;\n default:\n return 0;\n }\n}\n\nfunction stringifyAttrs(\n attrs: Readonly<Record<string, unknown>> | undefined,\n): Record<string, string | number | boolean> {\n if (!attrs) return {};\n const out: Record<string, string | number | boolean> = {};\n for (const [k, v] of Object.entries(attrs)) {\n if (v === null || v === undefined) continue;\n if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {\n out[k] = v;\n } else {\n out[k] = JSON.stringify(v);\n }\n }\n return out;\n}\n"]}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Husk — observability types (tracer interface).
3
+ *
4
+ * A minimal, OTel-inspired tracer interface. Husk's events are mapped
5
+ * to spans by the mapper in ./tracer.ts. Users can plug in the real
6
+ * @opentelemetry/api tracer via the adapter (see ./otel-adapter.ts)
7
+ * or any other compatible backend.
8
+ *
9
+ * Design choice: we don't depend on @opentelemetry/api directly. The
10
+ * interface here is a strict subset of OTel's Span interface (just
11
+ * what's needed for agent observability). Keeping the dep out of
12
+ * Husk's core means users who don't need OTel pay nothing for it.
13
+ *
14
+ * For users who want full OTel:
15
+ * import { trace } from '@opentelemetry/api';
16
+ * import { toOtelTracer } from '@princetheprogrammerbtw/husk/otel-adapter';
17
+ * agent.onAny(toOtelTracer(trace.getTracer('husk')).onEvent);
18
+ */
19
+ type SpanKind = 'internal' | 'client' | 'server';
20
+ interface SpanContext {
21
+ /** Unique trace id (all spans in one agent.run share this). */
22
+ readonly traceId: string;
23
+ /** Unique span id. */
24
+ readonly spanId: string;
25
+ /** Parent span id, if any. */
26
+ readonly parentSpanId?: string;
27
+ }
28
+ interface SpanOptions {
29
+ readonly name: string;
30
+ readonly kind?: SpanKind;
31
+ readonly attributes?: Readonly<Record<string, unknown>>;
32
+ readonly startTimeNs?: bigint;
33
+ }
34
+ interface Span {
35
+ readonly context: SpanContext;
36
+ /** Record an event (timestamped annotation) on the span. */
37
+ addEvent(name: string, attributes?: Record<string, unknown>): void;
38
+ /** Set or update an attribute on the span. */
39
+ setAttribute(key: string, value: string | number | boolean | null): void;
40
+ /** Record an exception. */
41
+ recordException(err: Error): void;
42
+ /** Mark the span as failed. */
43
+ setStatus(status: 'ok' | 'error', message?: string): void;
44
+ /** End the span. Must be called exactly once. */
45
+ end(endTimeNs?: bigint): void;
46
+ }
47
+ interface Tracer {
48
+ /**
49
+ * Start a new span. If parent is provided, the new span becomes a
50
+ * child of it. Returns the new span; caller is responsible for
51
+ * calling .end() on it.
52
+ */
53
+ startSpan(options: SpanOptions, parent?: SpanContext): Span;
54
+ }
55
+ /**
56
+ * A tracer that does nothing. Used when no real tracer is configured.
57
+ * Zero overhead — every method is a no-op, so the cost is one virtual
58
+ * call per event.
59
+ */
60
+ declare class NoopTracer implements Tracer {
61
+ startSpan(_options: SpanOptions, _parent?: SpanContext): Span;
62
+ }
63
+
64
+ export { NoopTracer as N, type SpanOptions as S, type Tracer as T, type SpanContext as a, type Span as b, type SpanKind as c };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@princetheprogrammerbtw/husk",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Provider-agnostic agent harness — memory, tools, sub-agents, and observability wrapped around any LLM.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -57,6 +57,7 @@
57
57
  },
58
58
  "devDependencies": {
59
59
  "@biomejs/biome": "^1.9.4",
60
+ "@opentelemetry/api": "^1.9.1",
60
61
  "@types/node": "^22.10.0",
61
62
  "tsup": "^8.3.5",
62
63
  "typescript": "^5.7.2"