@princetheprogrammerbtw/husk 0.1.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,702 @@
1
+ /**
2
+ * Husk — core type definitions.
3
+ *
4
+ * This file is the foundation of the public API. Every other module
5
+ * (agent loop, providers, tools, memory, steering) depends on the
6
+ * types defined here. Treat changes to this file as breaking changes
7
+ * unless the change is purely additive (a new optional field).
8
+ *
9
+ * Design principle: model the LARGEST common subset of provider
10
+ * APIs (Anthropic + OpenAI), then let provider adapters translate
11
+ * provider-specific formats into these shapes.
12
+ */
13
+ /** Roles a message can take. `tool` is used for tool execution results. */
14
+ type Role = 'system' | 'user' | 'assistant' | 'tool';
15
+ /**
16
+ * The content of a message. Most simple messages are plain strings.
17
+ * Messages that involve tool use or tool results are arrays of blocks.
18
+ *
19
+ * String content is the common case (user prompts, simple replies).
20
+ * Block content is used when the message contains tool calls (from the
21
+ * assistant) or tool results (in response to a tool call).
22
+ */
23
+ type MessageContent = string | ContentBlock[];
24
+ type ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock;
25
+ interface TextBlock {
26
+ readonly type: 'text';
27
+ readonly text: string;
28
+ }
29
+ /** A request from the assistant to invoke a tool. */
30
+ interface ToolUseBlock {
31
+ readonly type: 'tool_use';
32
+ readonly id: string;
33
+ readonly name: string;
34
+ readonly input: unknown;
35
+ }
36
+ /** The result of a tool invocation, fed back to the assistant. */
37
+ interface ToolResultBlock {
38
+ readonly type: 'tool_result';
39
+ readonly toolUseId: string;
40
+ readonly content: string | ContentBlock[];
41
+ readonly isError?: boolean;
42
+ }
43
+ interface Message {
44
+ readonly role: Role;
45
+ readonly content: MessageContent;
46
+ /** Used for `tool` role messages to identify which tool produced the result. */
47
+ readonly name?: string;
48
+ /** Used for `tool` role messages to link back to the originating ToolUseBlock. */
49
+ readonly toolCallId?: string;
50
+ }
51
+ /**
52
+ * A minimal JSON Schema type. We don't try to model the full spec —
53
+ * just the shape that tools actually need: object with properties,
54
+ * required fields, descriptions. Provider adapters can downcast to
55
+ * their own schema types.
56
+ */
57
+ interface JSONSchema {
58
+ readonly type: 'object';
59
+ readonly properties: Readonly<Record<string, JSONSchemaField>>;
60
+ readonly required?: readonly string[];
61
+ readonly additionalProperties?: boolean;
62
+ }
63
+ interface JSONSchemaField {
64
+ readonly type: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object' | 'null';
65
+ readonly description?: string;
66
+ readonly enum?: readonly unknown[];
67
+ readonly items?: JSONSchemaField;
68
+ readonly properties?: Readonly<Record<string, JSONSchemaField>>;
69
+ readonly required?: readonly string[];
70
+ }
71
+ interface ToolContext {
72
+ /** Abort signal to support cancellation. */
73
+ readonly signal?: AbortSignal;
74
+ /** Structured logger the tool can use. */
75
+ readonly logger?: Logger;
76
+ }
77
+ /**
78
+ * The result of running a tool. `output` is what the LLM sees.
79
+ * `isError` distinguishes a successful "no results found" from
80
+ * an actual exception.
81
+ */
82
+ interface ToolResult {
83
+ readonly output: string;
84
+ readonly isError?: boolean;
85
+ }
86
+ /**
87
+ * A tool the agent can invoke. Providers translate this to their
88
+ * native tool format (Anthropic's `tools` array, OpenAI's
89
+ * `functions` array, etc.).
90
+ */
91
+ interface ToolDefinition<TInput = unknown> {
92
+ readonly name: string;
93
+ readonly description: string;
94
+ readonly inputSchema: JSONSchema;
95
+ readonly execute: (input: TInput, ctx: ToolContext) => Promise<ToolResult>;
96
+ }
97
+ /** Why the model stopped generating. */
98
+ type StopReason = 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence' | 'error';
99
+ interface TokenUsage {
100
+ readonly inputTokens: number;
101
+ readonly outputTokens: number;
102
+ }
103
+ interface ChatRequest {
104
+ readonly model: string;
105
+ readonly messages: readonly Message[];
106
+ readonly tools?: readonly ToolDefinition[];
107
+ readonly system?: string;
108
+ readonly temperature?: number;
109
+ readonly maxTokens?: number;
110
+ readonly stopSequences?: readonly string[];
111
+ }
112
+ interface ChatResponse {
113
+ readonly message: Message;
114
+ readonly usage: TokenUsage;
115
+ readonly stopReason: StopReason;
116
+ readonly model: string;
117
+ }
118
+ interface ChatChunk {
119
+ readonly type: 'text' | 'tool_use_start' | 'tool_use_delta' | 'message_end';
120
+ readonly text?: string;
121
+ readonly toolUse?: {
122
+ id: string;
123
+ name: string;
124
+ inputDelta?: string;
125
+ };
126
+ readonly usage?: TokenUsage;
127
+ readonly stopReason?: StopReason;
128
+ }
129
+ /**
130
+ * A model provider. Implementations translate the provider-agnostic
131
+ * `ChatRequest` to the provider's wire format and back.
132
+ *
133
+ * `name` is the provider family ("anthropic", "openai", "ollama").
134
+ * `model` is the specific model id the provider is configured for
135
+ * (e.g. "claude-opus-4-6"). The agent loop reads `model` when building
136
+ * requests, so providers should be configured with a model at
137
+ * construction time.
138
+ */
139
+ interface Provider {
140
+ readonly name: string;
141
+ readonly model: string;
142
+ chat(request: ChatRequest): Promise<ChatResponse>;
143
+ stream?(request: ChatRequest): AsyncIterable<ChatChunk>;
144
+ }
145
+ /**
146
+ * A memory store. Two backends ship in v0.1.0:
147
+ * - InMemoryStore: session-scoped, lost on process exit
148
+ * - FileStore: persistent, JSONL on disk
149
+ */
150
+ interface MemoryStore {
151
+ /** Load all messages for a session, in order. */
152
+ read(sessionId: string): Promise<readonly Message[]>;
153
+ /** Append a message to a session. */
154
+ append(sessionId: string, message: Message): Promise<void>;
155
+ /** Clear all messages for a session. */
156
+ clear(sessionId: string): Promise<void>;
157
+ /** List all session IDs the store knows about. */
158
+ listSessions(): Promise<readonly string[]>;
159
+ }
160
+ interface Example {
161
+ readonly user: string;
162
+ readonly assistant: string;
163
+ }
164
+ interface SteeringConfig {
165
+ /** A system prompt prepended to every conversation. */
166
+ readonly systemPrompt?: string;
167
+ /** Behavioral rules injected into the system prompt as a numbered list. */
168
+ readonly rules?: readonly string[];
169
+ /** Few-shot examples prepended to the conversation as user/assistant pairs. */
170
+ readonly examples?: readonly Example[];
171
+ }
172
+ interface AgentConfig {
173
+ readonly model: Provider;
174
+ readonly tools?: readonly ToolDefinition[];
175
+ readonly memory?: MemoryStore;
176
+ readonly steering?: SteeringConfig;
177
+ /** Hard cap on agent loop iterations. Default: 25. */
178
+ readonly maxIterations?: number;
179
+ /** Sampling temperature. Default: 0 (deterministic). */
180
+ readonly temperature?: number;
181
+ /** Max output tokens per model call. Provider-specific defaults apply. */
182
+ readonly maxTokens?: number;
183
+ /** Abort signal for cancellation. */
184
+ readonly signal?: AbortSignal;
185
+ /** Session ID for memory continuity. Default: 'default'. */
186
+ readonly sessionId?: string;
187
+ }
188
+ interface AgentResult {
189
+ readonly output: string;
190
+ readonly messages: readonly Message[];
191
+ readonly iterations: number;
192
+ readonly usage: TokenUsage;
193
+ readonly durationMs: number;
194
+ }
195
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
196
+ interface Logger {
197
+ debug(message: string, fields?: Record<string, unknown>): void;
198
+ info(message: string, fields?: Record<string, unknown>): void;
199
+ warn(message: string, fields?: Record<string, unknown>): void;
200
+ error(message: string, fields?: Record<string, unknown>): void;
201
+ }
202
+
203
+ /**
204
+ * Husk — typed event emitter for observability.
205
+ *
206
+ * Every interesting thing that happens inside the agent loop fires
207
+ * an event. Downstream consumers (loggers, tracers, dashboards, test
208
+ * assertions) subscribe to these events to observe behavior without
209
+ * having to monkey-patch the agent.
210
+ *
211
+ * Design choice: a discriminated-union event type instead of a generic
212
+ * EventEmitter. The compiler can verify that handlers receive the right
213
+ * payload shape, and tooling can autocomplete event names.
214
+ */
215
+
216
+ type AgentEvent = {
217
+ readonly type: 'agent:start';
218
+ readonly input: string;
219
+ readonly sessionId: string;
220
+ } | {
221
+ readonly type: 'agent:iteration';
222
+ readonly iteration: number;
223
+ } | {
224
+ readonly type: 'agent:message';
225
+ readonly message: Message;
226
+ } | {
227
+ readonly type: 'provider:request';
228
+ readonly request: ChatRequest;
229
+ } | {
230
+ readonly type: 'provider:response';
231
+ readonly response: ChatResponse;
232
+ readonly durationMs: number;
233
+ } | {
234
+ readonly type: 'tool:call';
235
+ readonly id: string;
236
+ readonly name: string;
237
+ readonly input: unknown;
238
+ } | {
239
+ readonly type: 'tool:result';
240
+ readonly id: string;
241
+ readonly name: string;
242
+ readonly result: ToolResult;
243
+ readonly durationMs: number;
244
+ } | {
245
+ readonly type: 'agent:end';
246
+ readonly output: string;
247
+ readonly iterations: number;
248
+ readonly durationMs: number;
249
+ } | {
250
+ readonly type: 'agent:error';
251
+ readonly error: Error;
252
+ };
253
+ /** A handler for a specific event type. */
254
+ type AgentEventHandler<E extends AgentEvent = AgentEvent> = (event: E) => void | Promise<void>;
255
+ /**
256
+ * A minimal, type-safe event bus. We could use Node's EventEmitter,
257
+ * but the untyped `on('event', handler)` API loses the discriminated-
258
+ * union narrowing we get from per-type handlers.
259
+ */
260
+ declare class AgentEventEmitter {
261
+ private readonly handlers;
262
+ private readonly wildcardHandlers;
263
+ /**
264
+ * Subscribe to a specific event type. The handler receives only
265
+ * events of that type with the correct payload shape.
266
+ */
267
+ on<E extends AgentEvent['type']>(type: E, handler: AgentEventHandler<Extract<AgentEvent, {
268
+ type: E;
269
+ }>>): () => void;
270
+ /**
271
+ * Subscribe to all events. Useful for loggers and tracers.
272
+ */
273
+ onAny(handler: AgentEventHandler): () => void;
274
+ off<E extends AgentEvent['type']>(type: E, handler: AgentEventHandler<Extract<AgentEvent, {
275
+ type: E;
276
+ }>>): void;
277
+ /**
278
+ * Emit an event. Handlers are awaited sequentially; an async handler
279
+ * that throws is logged but doesn't stop subsequent handlers.
280
+ */
281
+ emit(event: AgentEvent): Promise<void>;
282
+ }
283
+ /**
284
+ * A simple console-based logger. Useful for development and as a
285
+ * reference implementation for custom loggers.
286
+ */
287
+ declare class ConsoleLogger implements Logger {
288
+ debug(message: string, fields?: Record<string, unknown>): void;
289
+ info(message: string, fields?: Record<string, unknown>): void;
290
+ warn(message: string, fields?: Record<string, unknown>): void;
291
+ error(message: string, fields?: Record<string, unknown>): void;
292
+ private format;
293
+ }
294
+ /**
295
+ * Convert an event stream into structured log lines via a Logger.
296
+ * Drop-in for stdout/JSON observability.
297
+ */
298
+ declare function logEventsTo(logger: Logger): AgentEventHandler;
299
+
300
+ /**
301
+ * Husk — memory store implementations.
302
+ *
303
+ * Two stores ship in v0.1.0:
304
+ * - InMemoryStore: session-scoped, fast, lost on process exit
305
+ * - FileStore: persistent across sessions, JSONL on disk
306
+ *
307
+ * Both implement the MemoryStore interface from ./types.js. The agent
308
+ * loop doesn't care which one it gets — it just calls read/append/clear.
309
+ *
310
+ * Design choice: separate stores per file but exported from the same
311
+ * module. Users can import what they need: `import { InMemory, File } from
312
+ * '@princetheprogrammerbtw/husk'`.
313
+ */
314
+
315
+ /**
316
+ * Session-scoped memory. Messages live in a Map in process memory.
317
+ * Fast, zero-dep, but ephemeral — perfect for single-run agents.
318
+ */
319
+ declare class InMemoryStore implements MemoryStore {
320
+ private readonly sessions;
321
+ read(sessionId: string): Promise<readonly Message[]>;
322
+ append(sessionId: string, message: Message): Promise<void>;
323
+ clear(sessionId: string): Promise<void>;
324
+ listSessions(): Promise<readonly string[]>;
325
+ }
326
+ /**
327
+ * Persistent memory backed by a JSONL file. One file per session by
328
+ * default, or a single file with a `__session` field per line if you
329
+ * want a unified log.
330
+ *
331
+ * JSONL is the format because:
332
+ * - Append-only writes are O(1) (no read-modify-write race)
333
+ * - Corruption is line-scoped, not file-scoped
334
+ * - It's grep-friendly for debugging
335
+ */
336
+ interface FileStoreOptions {
337
+ /** Directory where session files live. Default: './.husk/memory'. */
338
+ readonly path?: string;
339
+ /** Use a single file with session markers (default: false, one file per session). */
340
+ readonly unified?: boolean;
341
+ }
342
+ declare class FileStore implements MemoryStore {
343
+ private readonly rootDir;
344
+ private readonly unified;
345
+ private readonly writeLocks;
346
+ constructor(options?: FileStoreOptions);
347
+ read(sessionId: string): Promise<readonly Message[]>;
348
+ append(sessionId: string, message: Message): Promise<void>;
349
+ private doAppend;
350
+ clear(sessionId: string): Promise<void>;
351
+ listSessions(): Promise<readonly string[]>;
352
+ private fileFor;
353
+ }
354
+
355
+ /**
356
+ * Husk — steering prompt builder.
357
+ *
358
+ * "Steering" is the config that shapes agent behavior: system prompt,
359
+ * rules, and few-shot examples. The builder takes a SteeringConfig
360
+ * and produces the artifacts the agent loop needs:
361
+ * - buildSystemPrompt() → the string to send as the system message
362
+ * - buildExamples() → the user/assistant message pairs to seed history
363
+ *
364
+ * Why a separate module? Two reasons:
365
+ * 1. The agent loop stays focused on the loop logic, not prompt assembly.
366
+ * 2. Steering is the most-likely-to-be-customized piece; users can
367
+ * subclass or replace the builder without touching the agent.
368
+ */
369
+
370
+ /**
371
+ * Combine systemPrompt + rules into a single system prompt string.
372
+ * Rules are numbered for explicit citation ("see rule 3") and
373
+ * appended after a header so models parse them as a separate list.
374
+ */
375
+ declare function buildSystemPrompt(steering: SteeringConfig): string | undefined;
376
+ /**
377
+ * Convert few-shot examples into a sequence of user/assistant message
378
+ * pairs that get prepended to the conversation history. The model
379
+ * sees these as if they had happened earlier in the conversation,
380
+ * which is how few-shot prompting works mechanically.
381
+ *
382
+ * Examples are emitted in order; the first user message of an example
383
+ * comes right after the previous example's assistant message (or right
384
+ * after the system prompt for the first example).
385
+ */
386
+ declare function buildExampleMessages(examples: readonly Example[]): readonly Message[];
387
+
388
+ /**
389
+ * Husk — the agent loop.
390
+ *
391
+ * This is the heartbeat of the harness. The loop is small but every
392
+ * line matters:
393
+ *
394
+ * 1. Compose the conversation (examples + memory + new input)
395
+ * 2. Call the model
396
+ * 3. Decide what to do based on stopReason
397
+ * 4. If tool_use, execute tools and feed results back, then loop
398
+ * 5. If end_turn, return the final output
399
+ *
400
+ * Design choices worth knowing:
401
+ *
402
+ * - Tools are executed in parallel within a single iteration. The
403
+ * model can request multiple tools in one turn; we honor that and
404
+ * feed all results back at once. Most agent frameworks get this wrong
405
+ * by serializing tool calls.
406
+ *
407
+ * - A faulty tool does not crash the loop. The error becomes a
408
+ * tool_result with isError=true, the model sees it, and can either
409
+ * retry with corrected input or report back to the user. This is
410
+ * how a real assistant would behave.
411
+ *
412
+ * - The loop is bounded by maxIterations. Default 25 is enough for
413
+ * most agent tasks without running away on infinite loops.
414
+ *
415
+ * - The system prompt is rebuilt on every iteration from the steering
416
+ * config. Cheap, and means hot-reloading rules works.
417
+ */
418
+
419
+ declare class Agent {
420
+ readonly events: AgentEventEmitter;
421
+ readonly provider: AgentConfig['model'];
422
+ readonly tools: readonly ToolDefinition[];
423
+ readonly steering: AgentConfig['steering'];
424
+ readonly maxIterations: number;
425
+ readonly temperature: number;
426
+ readonly maxTokens: number | undefined;
427
+ readonly signal: AbortSignal | undefined;
428
+ readonly sessionId: string;
429
+ readonly memory: AgentConfig['memory'];
430
+ readonly logger: Logger;
431
+ constructor(config: AgentConfig);
432
+ /**
433
+ * Subscribe to a specific event type. Returns an unsubscribe fn.
434
+ */
435
+ on: AgentEventEmitter['on'];
436
+ /**
437
+ * Subscribe to all events. Returns an unsubscribe fn.
438
+ */
439
+ onAny: AgentEventEmitter['onAny'];
440
+ /**
441
+ * Run the agent loop to completion on the given input.
442
+ * Returns the final result with output text, full message history,
443
+ * token usage, and duration.
444
+ */
445
+ run(input: string): Promise<AgentResult>;
446
+ private recordMessage;
447
+ private executeTool;
448
+ }
449
+
450
+ /**
451
+ * Husk — Anthropic Claude provider adapter.
452
+ *
453
+ * Translates Husk's provider-agnostic ChatRequest to the Anthropic
454
+ * Messages API format and back. This is the only file in the project
455
+ * that knows what Anthropic's wire format looks like.
456
+ *
457
+ * Wire-format mapping (Husk → Anthropic):
458
+ * - MessageRole 'assistant' + ToolUseBlock → assistant message with tool_use blocks
459
+ * - MessageRole 'user' + ToolResultBlock[] → user message with tool_result blocks
460
+ * - ToolDefinition (Husk JSON Schema) → Anthropic input_schema (passes through)
461
+ * - StopReason 'end_turn' / 'tool_use' / 'max_tokens' / 'stop_sequence'
462
+ * → returned as-is from stop_reason
463
+ *
464
+ * Defaults:
465
+ * - model: 'claude-opus-4-6' (override via constructor)
466
+ * - max_tokens: 8192 (Anthropic requires this on every request)
467
+ * - apiKey: process.env.ANTHROPIC_API_KEY
468
+ */
469
+
470
+ interface AnthropicProviderOptions {
471
+ /** Override the API key. Default: process.env.ANTHROPIC_API_KEY. */
472
+ readonly apiKey?: string;
473
+ /** Model id. Default: 'claude-opus-4-6'. */
474
+ readonly model?: string;
475
+ /** Override the API base URL (for proxies, self-hosted, etc). */
476
+ readonly baseURL?: string;
477
+ /** Default max_tokens for requests. Anthropic requires this. Default: 8192. */
478
+ readonly maxTokens?: number;
479
+ }
480
+ declare class AnthropicProvider implements Provider {
481
+ readonly name = "anthropic";
482
+ readonly model: string;
483
+ private readonly client;
484
+ private readonly defaultMaxTokens;
485
+ constructor(options?: AnthropicProviderOptions);
486
+ chat(request: ChatRequest): Promise<ChatResponse>;
487
+ }
488
+
489
+ /**
490
+ * Husk — OpenAI provider adapter.
491
+ *
492
+ * Translates Husk's provider-agnostic ChatRequest to the OpenAI
493
+ * Chat Completions API format and back. The shape is similar to
494
+ * Anthropic but with two important differences:
495
+ *
496
+ * 1. Tool results are their own message role ('tool'), not blocks in
497
+ * a user message. The Husk agent loop emits tool results as user-
498
+ * role messages with tool_result content blocks; we split them
499
+ * out into individual tool-role messages here.
500
+ *
501
+ * 2. Assistant tool calls are an array of tool_call objects on the
502
+ * assistant message, not content blocks. We map Husk's tool_use
503
+ * blocks to OpenAI's tool_calls shape.
504
+ *
505
+ * 3. Tools use the legacy 'functions' shape via the 'tools' field with
506
+ * 'function' type. (OpenAI's new 'tools' format is the same; we
507
+ * use it.)
508
+ */
509
+
510
+ interface OpenAIProviderOptions {
511
+ /** Override the API key. Default: process.env.OPENAI_API_KEY. */
512
+ readonly apiKey?: string;
513
+ /** Model id. Default: 'gpt-5'. */
514
+ readonly model?: string;
515
+ /** Override the API base URL (for proxies, Azure OpenAI, etc). */
516
+ readonly baseURL?: string;
517
+ /** Organization id (for OpenAI orgs). */
518
+ readonly organization?: string;
519
+ }
520
+ declare class OpenAIProvider implements Provider {
521
+ readonly name = "openai";
522
+ readonly model: string;
523
+ private readonly client;
524
+ constructor(options?: OpenAIProviderOptions);
525
+ chat(request: ChatRequest): Promise<ChatResponse>;
526
+ }
527
+
528
+ /**
529
+ * Husk — tool registry helpers.
530
+ *
531
+ * Tools in Husk are just objects that implement ToolDefinition. There's
532
+ * no "register" call — you just pass an array to the Agent. These helpers
533
+ * exist to make the common cases (naming, validation, common schemas)
534
+ * less verbose.
535
+ *
536
+ * Why no global registry? Global state makes testing harder, breaks
537
+ * tree-shaking, and prevents running multiple agents with different
538
+ * tool sets in the same process. Explicit arrays are clearer.
539
+ */
540
+
541
+ /**
542
+ * Helper to build a tool definition with less boilerplate. The runtime
543
+ * behavior is identical to a hand-written ToolDefinition object; this
544
+ * just makes the common case (typed name, description, schema, executor)
545
+ * read like a function call.
546
+ */
547
+ declare function defineTool<TInput>(tool: {
548
+ name: string;
549
+ description: string;
550
+ inputSchema: JSONSchema;
551
+ execute: (input: TInput) => Promise<string> | string;
552
+ }): ToolDefinition<TInput>;
553
+ /** String field with optional enum. */
554
+ declare function stringField(description: string, options?: {
555
+ enum?: readonly string[];
556
+ }): JSONSchemaField;
557
+ /** Number field (integer or float). */
558
+ declare function numberField(description: string): JSONSchemaField;
559
+ /** Integer field. */
560
+ declare function integerField(description: string): JSONSchemaField;
561
+ /** Boolean field. */
562
+ declare function booleanField(description: string): JSONSchemaField;
563
+ /** Array field of a given element type. */
564
+ declare function arrayField(description: string, items: JSONSchemaField): JSONSchemaField;
565
+ /** Object field with nested properties. */
566
+ declare function objectField(description: string, properties: Readonly<Record<string, JSONSchemaField>>, required?: readonly string[]): JSONSchemaField;
567
+ /**
568
+ * Build an object schema (the typical top-level shape for tool inputs).
569
+ * Convenience wrapper around JSONSchema that defaults to type 'object'.
570
+ */
571
+ declare function objectSchema(properties: Readonly<Record<string, JSONSchemaField>>, required?: readonly string[]): JSONSchema;
572
+
573
+ /**
574
+ * Husk — built-in Read tool.
575
+ *
576
+ * Reads a file from the local filesystem and returns its contents.
577
+ * Supports offset (line number) and limit (max lines) for paging
578
+ * through large files.
579
+ *
580
+ * Safety: paths are resolved relative to the working directory. We
581
+ * refuse to read paths that escape the workspace (e.g. '../etc/passwd')
582
+ * unless an explicit 'allowOutsideWorkspace' flag is set.
583
+ */
584
+ interface ReadInput {
585
+ /** Path to the file, relative to the working directory. */
586
+ path: string;
587
+ /** Line number to start reading from (1-indexed). Default: 1. */
588
+ offset?: number;
589
+ /** Maximum number of lines to read. Default: 2000. */
590
+ limit?: number;
591
+ }
592
+ declare const Read: ToolDefinition<ReadInput>;
593
+
594
+ /**
595
+ * Husk — built-in Write tool.
596
+ *
597
+ * Writes a file to the local filesystem. Creates parent directories
598
+ * if they don't exist. Overwrites the file if it already exists.
599
+ *
600
+ * Returns the number of bytes written and the absolute path so the
601
+ * model can confirm where the content landed.
602
+ */
603
+ interface WriteInput {
604
+ /** Path to the file, relative to the working directory. */
605
+ path: string;
606
+ /** Content to write. */
607
+ content: string;
608
+ }
609
+ declare const Write: ToolDefinition<WriteInput>;
610
+
611
+ /**
612
+ * Husk — built-in Edit tool.
613
+ *
614
+ * Performs a string replacement in a file. The 'oldText' must match
615
+ * exactly (including whitespace) and must appear exactly once in the
616
+ * file. This single-match requirement is what makes the operation
617
+ * safe: ambiguous replacements fail loudly rather than corrupting
618
+ * unrelated sections.
619
+ *
620
+ * For multi-occurrence replacements, the agent should read the file
621
+ * first to identify the exact context, then call Edit with enough
622
+ * surrounding lines to make the match unique.
623
+ */
624
+ interface EditInput {
625
+ /** Path to the file, relative to the working directory. */
626
+ path: string;
627
+ /** The exact text to replace. Must match exactly one location. */
628
+ oldText: string;
629
+ /** The text to replace it with. */
630
+ newText: string;
631
+ }
632
+ declare const Edit: ToolDefinition<EditInput>;
633
+
634
+ /**
635
+ * Husk — built-in Bash tool.
636
+ *
637
+ * Executes a shell command and returns stdout/stderr/exit code. The
638
+ * harness is the model running with developer-level filesystem
639
+ * access, so a "rm -rf /" mistake is catastrophic. The safety rails
640
+ * here are a first line of defense — they catch the obvious
641
+ * footguns, not all of them.
642
+ *
643
+ * Safety rails (v0.1.0):
644
+ * - Block a denylist of catastrophic command patterns. The denylist
645
+ * is regex-based, scoped to the command string, and intentionally
646
+ * conservative. False positives (a command that looks dangerous
647
+ * but isn't) are acceptable; false negatives are not.
648
+ * - Time out after 60 seconds by default. The model can request a
649
+ * longer timeout (max 10 minutes).
650
+ *
651
+ * Not in scope for v0.1.0 (deferred to v0.2 with config flag):
652
+ * - Per-command confirmation prompts
653
+ * - Network egress filtering
654
+ * - Filesystem sandboxing
655
+ * - Audit logging to a separate file
656
+ */
657
+ interface BashInput {
658
+ /** The shell command to execute. */
659
+ command: string;
660
+ /** Optional description of what the command does (for logging). */
661
+ description?: string;
662
+ /** Timeout in milliseconds. Default: 60000 (1 min). Max: 600000 (10 min). */
663
+ timeout?: number;
664
+ }
665
+ declare const Bash: ToolDefinition<BashInput>;
666
+
667
+ /**
668
+ * Husk — built-in Grep tool.
669
+ *
670
+ * Searches files for a regex pattern. Uses ripgrep ('rg') if available
671
+ * for speed; falls back to grep with --line-numbers --no-heading.
672
+ * Returns matching lines with file:line:content format.
673
+ *
674
+ * Default scope: the current working directory, recursively, respecting
675
+ * .gitignore. The model can scope to a specific path or file.
676
+ */
677
+ interface GrepInput {
678
+ /** Regex pattern to search for. */
679
+ pattern: string;
680
+ /** File or directory to search in. Default: current directory. */
681
+ path?: string;
682
+ /** File glob to filter by (e.g. '*.ts'). Default: all files. */
683
+ glob?: string;
684
+ /** Case-insensitive search. Default: false. */
685
+ ignoreCase?: boolean;
686
+ /** Maximum number of matching lines to return. Default: 100. */
687
+ limit?: number;
688
+ }
689
+ declare const Grep: ToolDefinition<GrepInput>;
690
+
691
+ /**
692
+ * Husk — public API entry point.
693
+ *
694
+ * Single import surface for users:
695
+ * import { Agent, Anthropic, OpenAI, Read, Write, Bash, Edit, Grep,
696
+ * InMemoryStore, FileStore, ConsoleLogger } from '@princetheprogrammerbtw/husk';
697
+ *
698
+ * Re-exports are added incrementally as features land (see commit history).
699
+ */
700
+ declare const VERSION = "0.0.1";
701
+
702
+ export { Agent, type AgentConfig, type AgentEvent, AgentEventEmitter, type AgentEventHandler, type AgentResult, AnthropicProvider, type AnthropicProviderOptions, Bash, type BashInput, type ChatChunk, type ChatRequest, type ChatResponse, ConsoleLogger, type ContentBlock, Edit, type EditInput, type Example, FileStore, type FileStoreOptions, Grep, type GrepInput, InMemoryStore, type JSONSchema, type JSONSchemaField, type LogLevel, type Logger, type MemoryStore, type Message, type MessageContent, OpenAIProvider, type OpenAIProviderOptions, type Provider, Read, type ReadInput, type Role, type SteeringConfig, type StopReason, type TextBlock, type TokenUsage, type ToolContext, type ToolDefinition, type ToolResult, type ToolResultBlock, type ToolUseBlock, VERSION, Write, type WriteInput, arrayField, booleanField, buildExampleMessages, buildSystemPrompt, defineTool, integerField, logEventsTo, numberField, objectField, objectSchema, stringField };