Files
nanobot-ts/docs/Discoveries.md

3.5 KiB

Discoveries

Empirical learnings from implementation that future sessions should know.

Zod v4 Specifics

.default() on Nested Objects

Zod v4 requires factory functions for nested object defaults, and the factory must return the full output type (not just {}):

// ❌ Wrong - empty object won't match the schema
const Config = z.object({
  nested: NestedSchema.default({}),
});

// ✅ Correct - factory returning full type
const Config = z.object({
  nested: NestedSchema.default(() => ({ field: value, ... })),
});

z.record() Requires Two Arguments

// ❌ Wrong
z.record(z.string())

// ✅ Correct
z.record(z.string(), z.unknown())

AI SDK v6 Changes

v4/v5 v6
LanguageModelV2 LanguageModel
maxTokens maxOutputTokens
maxSteps stopWhen: stepCountIs(n)
usage.promptTokens usage.inputTokens
usage.completionTokens usage.outputTokens

ollama-ai-provider Compatibility

ollama-ai-provider v1.2.0 returns LanguageModelV1, not the expected LanguageModel (v2/v3). Cast at call site:

import { ollama } from 'ollama-ai-provider';
import type { LanguageModel } from 'ai';

const model = ollama('llama3.2') as unknown as LanguageModel;

js-tiktoken API

// ❌ Wrong (Python-style)
import { get_encoding } from 'js-tiktoken';

// ✅ Correct
import { getEncoding } from 'js-tiktoken';

Bun/Node Globals

Document is not available as a global in Bun/Node. For DOM-like operations:

// ❌ Wrong
function makeDocument(): Document { ... }

// ✅ Correct
function makePseudoDocument(): Record<string, unknown> { ... }
// Cast at call site if needed

WebSocket Error Types

WebSocket onerror handler receives an Event, not an Error:

socket.onerror = (err: Event) => {
  console.error(`WebSocket error: ${err.type}`);
};

Template Literals with Unknown Types

When interpolating unknown types in template literals, explicitly convert to string:

// ❌ Risky - may throw
console.log(`Error: ${err}`);

// ✅ Safe
console.log(`Error: ${String(err)}`);

Helper: strArg

For safely extracting string arguments from Record<string, unknown>:

// src/agent/tools/base.ts
export function strArg(args: Record<string, unknown>, key: string, fallback = ''): string {
  const val = args[key];
  return typeof val === 'string' ? val : fallback;
}

Usage:

// ❌ Verbose
const path = String(args['path'] ?? '');

// ✅ Cleaner
const path = strArg(args, 'path');
const timeout = parseInt(strArg(args, 'timeout', '30'), 10);

Mattermost WebSocket

  • Uses raw WebSocket + fetch (no mattermostdriver library)
  • Auth via hello message with token
  • Event types: posted, post_edited, reaction_added, etc.
  • Group channel policy: mention (default), open, allowlist

Session Persistence

  • Format: JSONL (one JSON object per line)
  • Location: ~/.config/nanobot/sessions/{sessionKey}.jsonl
  • Tool results truncated at 16,000 characters
  • Memory consolidation triggered when approaching context window limit

Retry Logic

LLMProvider.chatWithRetry() retries on:

  • HTTP 429 (rate limit)
  • HTTP 5xx (server errors)
  • Timeouts
  • Network errors

Max 3 attempts with exponential backoff.

Config Precedence

  1. CLI flags (-c, -m, -w, -M)
  2. Environment variables (NANOBOT_CONFIG, NANOBOT_MODEL, NANOBOT_WORKSPACE)
  3. Config file (~/.config/nanobot/config.json)
  4. Zod schema defaults