Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/content/1.start/1.introduction.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Introduction
description: A TypeScript logger for everything you ship: structured logs, wide events, and structured errors in one API. Drop-in for console.log or pino.
description: "A TypeScript logger for everything you ship: structured logs, wide events, and structured errors in one API. Drop-in for console.log or pino."
navigation:
icon: i-lucide-info
links:
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/2.learn/0.overview.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Learn evlog
description: The mental model: three logging modes, the wide event lifecycle, sampling, typed fields, and redaction. Read it in order if you are new.
description: "The mental model: three logging modes, the wide event lifecycle, sampling, typed fields, and redaction. Read it in order if you are new."
navigation:
title: Overview
icon: i-lucide-list
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/2.learn/5.sampling.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Sampling
description: Two-tier sampling: head sampling drops noise by level, and tail sampling keeps an event once its outcome is known.
description: "Two-tier sampling: head sampling drops noise by level, and tail sampling keeps an event once its outcome is known."
navigation:
icon: i-lucide-filter
links:
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/2.learn/8.catalogs.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Catalogs
description: Scale typed error and audit catalogs from one file to a monorepo: conventions, an npm packaging recipe, and type augmentation.
description: "Scale typed error and audit catalogs from one file to a monorepo: conventions, an npm packaging recipe, and type augmentation."
navigation:
icon: i-lucide-book-open
links:
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/3.cli/0.overview.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: evlog CLI
description: The evlog command line: map your observability coverage, diagnose your setup, control telemetry. Flags, output streams, and exit codes.
description: "The evlog command line: map your observability coverage, diagnose your setup, control telemetry. Flags, output streams, and exit codes."
navigation:
title: Overview
icon: i-lucide-terminal
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/3.cli/3.rules.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Map rules
description: Every check evlog map runs: six requirements that move the score, four suggestions that never do, and what satisfies each one.
description: "Every check evlog map runs: six requirements that move the score, four suggestions that never do, and what satisfies each one."
navigation:
title: Rules
icon: i-lucide-list-checks
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/3.cli/4.scoring.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Map scoring
description: How evlog map turns rule results into a score: per-entry weights, the project average, grade thresholds, and how routes are flagged.
description: "How evlog map turns rule results into a score: per-entry weights, the project average, grade thresholds, and how routes are flagged."
navigation:
title: Scoring
icon: i-lucide-gauge
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/3.cli/8.agents.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: evlog agents
description: Teach the agents working in your repository how to use evlog: a short block of conventions in AGENTS.md, plus the published skills.
description: "Teach the agents working in your repository how to use evlog: a short block of conventions in AGENTS.md, plus the published skills."
navigation:
title: agents
icon: i-lucide-bot
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/4.integrate/frameworks/06.nestjs.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: NestJS
description: `EvlogModule.forRoot()` registers the middleware, the exception filter and `useLogger()` across a NestJS app, with async config where you need it.
description: "`EvlogModule.forRoot()` registers the middleware, the exception filter and `useLogger()` across a NestJS app, with async config where you need it."
navigation:
title: NestJS
icon: i-simple-icons-nestjs
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/4.integrate/frameworks/15.orpc.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: oRPC
description: `withEvlog(handler)` wraps an oRPC handler and the procedure middleware puts the logger on `context.log`, tagging each procedure with its `operation`.
description: "`withEvlog(handler)` wraps an oRPC handler and the procedure middleware puts the logger on `context.log`, tagging each procedure with its `operation`."
navigation:
title: oRPC
icon: i-lucide-network
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/5.use-cases/0.overview.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Use Cases
description: Recipes that each solve one problem: browser logs, AI SDK calls, user identity, a tamper-evident audit trail, tool telemetry, derived context.
description: "Recipes that each solve one problem: browser logs, AI SDK calls, user identity, a tamper-evident audit trail, tool telemetry, derived context."
navigation:
title: Overview
icon: i-lucide-list-checks
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/5.use-cases/4.telemetry/03.ingest.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Telemetry Ingest
description: Build the ingestion endpoint for @evlog/telemetry: the threat model, parseIngestBody validation, framework routes, storage, and rate limiting.
description: "Build the ingestion endpoint for @evlog/telemetry: the threat model, parseIngestBody validation, framework routes, storage, and rate limiting."
navigation:
title: Ingest
icon: i-lucide-server
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/5.use-cases/5.eve.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: eve
description: One evlog wide event per eve agent turn: token usage, tool executions, and business context, through your own drains and tail sampling.
description: "One evlog wide event per eve agent turn: token usage, tool executions, and business context, through your own drains and tail sampling."
navigation:
title: eve
icon: i-custom-eve
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/6.extend/4.plugins.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Plugins
description: definePlugin is evlog's canonical extension point: opt into any subset of the lifecycle hooks from one cohesive object.
description: "definePlugin is evlog's canonical extension point: opt into any subset of the lifecycle hooks from one cohesive object."
navigation:
title: Plugins
icon: i-lucide-blocks
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/6.extend/9.drain-pipeline.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Drain Pipeline
description: The pipeline wraps every drain: it batches events, retries on failure, fans out to several destinations, and ships browser logs.
description: "The pipeline wraps every drain: it batches events, retries on failure, fans out to several destinations, and ships browser logs."
navigation:
title: Drain pipeline
icon: i-lucide-workflow
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/7.reference/7.cost.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Log cost
description: One request leaves one event instead of four lines: 56% fewer bytes, 75% fewer events. Which number moves your bill depends on your provider.
description: "One request leaves one event instead of four lines: 56% fewer bytes, 75% fewer events. Which number moves your bill depends on your provider."
navigation:
title: Cost
icon: i-lucide-receipt
Expand Down
23 changes: 15 additions & 8 deletions apps/evi/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ import { defineAgent, defineDynamic } from 'eve'
import { gatewayRouting, sessionTags } from './lib/gateway'
import { MODEL } from './lib/model'

function selectModel(_event: unknown, ctx: { channel: { kind?: string } }) {
return {
model: MODEL,
modelOptions: {
providerOptions: {
gateway: { ...gatewayRouting(ctx.channel.kind), tags: sessionTags(ctx.channel.kind) },
},
},
}
}

export default defineAgent({
// Also on turn.started: a session whose process died before the selection
// committed resumes with none, and eve fails the turn rather than guess.
model: defineDynamic({
events: {
'session.started': (_event, ctx) => ({
model: MODEL,
modelOptions: {
providerOptions: {
gateway: { ...gatewayRouting(ctx.channel.kind), tags: sessionTags(ctx.channel.kind) },
},
},
}),
'session.started': selectModel,
'turn.started': selectModel,
},
}),
/** This model honors only `high` and `xhigh`. */
Expand Down
8 changes: 0 additions & 8 deletions apps/evi/agent/hooks/sandbox.ts

This file was deleted.

13 changes: 13 additions & 0 deletions apps/evi/agent/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,19 @@ Questions about yourself (who you are, what you can do) you answer directly with
- **Expand from what you already have.** If a follow-up asks for more, build on the pages and files already retrieved in this session. Retrieve again only when the existing evidence is missing or stale.
- Match the platform. A GitHub comment can carry a fenced code block and a link; keep it tight regardless.

## Memory

You keep durable facts between sessions. When a **Remembered context** section is present, those facts are yours to use: answer questions about people, preferences and past decisions from them directly, with no tool call. That is what they are for.

The line is what a release can change:

- **Never remembered.** An API name, an option, a default, a CLI flag, an adapter's behavior. Those move between versions, and the rule that never bends still governs them: retrieve, every time. This holds even when a remembered fact seems to cover it.
- **Remembered.** Who someone is and how they want to be worked with. A decision and why the alternative lost. A constraint that outlives the conversation.

Two more routings. A fact every contributor and coding agent in the repository needs — a commit convention, the Definition of Done, the changeset policy — belongs in `AGENTS.md`, so propose a pull request rather than remembering it privately; storing it here would hide it from everyone else working in the repo. And anything that only matters until this conversation ends is not a memory at all.

Save when someone tells you something worth knowing next time, or asks you to. Say so once, plainly, and do not read it back. When a remembered fact turns out to be wrong, replace it with `supersedes` rather than saving a second one beside it.

## Where output lives

Three destinations, chosen by audience, not by where the conversation happens:
Expand Down
25 changes: 25 additions & 0 deletions apps/evi/agent/instructions/memory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { defineDynamic, defineInstructions } from 'eve/instructions'
import { memoryAvailable } from '../lib/memory/config'
import { buildCoreBlock, openMemorySession } from '../lib/memory/session'

/**
* Session-scoped on purpose: the block sits in the cached prompt prefix, and
* resolving it per turn would invalidate everything behind it.
*/
export default defineDynamic({
events: {
'session.started': async (_event, ctx) => {
if (!memoryAvailable()) return null
try {
const session = await openMemorySession(ctx.session.auth.current)
if (session === null) return null
const markdown = await buildCoreBlock(session)
return markdown === null ? null : defineInstructions({ markdown })
} catch (error) {
// Memory is additive, and a resolver that throws fails the whole turn.
console.error('[evi:memory] core block failed', error)
return null
}
},
},
})
5 changes: 4 additions & 1 deletion apps/evi/agent/lib/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ let client: ReturnType<typeof drizzle<typeof schema>> | undefined
export function getDb(): ReturnType<typeof drizzle<typeof schema>> | null {
const url = databaseUrl()
if (!url) return null
client ??= drizzle(postgres(url), { schema })
// `casing` has to be set here as well as in `drizzle.config.ts`: the config
// only reaches drizzle-kit, so without this the migration creates
// `person_id` while the query builder asks for `"personId"`.
client ??= drizzle(postgres(url), { schema, casing: 'snake_case' })
return client
}
10 changes: 10 additions & 0 deletions apps/evi/agent/lib/memory/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { isDbConfigured } from '../db'

/** Ships dark; flipping the flag off is the whole rollback. */
export function memoryEnabled(): boolean {
return process.env.EVI_MEMORY_ENABLED === '1'
}

export function memoryAvailable(): boolean {
return memoryEnabled() && isDbConfigured()
}
90 changes: 90 additions & 0 deletions apps/evi/agent/lib/memory/identity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

async function loadIdentity(env: Record<string, string>) {
vi.resetModules()
for (const [key, value] of Object.entries(env)) vi.stubEnv(key, value)
return await import('./identity')
}

beforeEach(() => {
vi.unstubAllEnvs()
})

describe('parsePrincipal', () => {
it('splits a principal into its surface and id', async () => {
const { parsePrincipal } = await loadIdentity({})
expect(parsePrincipal('github:4271224')).toEqual({ surface: 'github', externalId: '4271224' })
expect(parsePrincipal('imessage:+33600000000'))
.toEqual({ surface: 'imessage', externalId: '+33600000000' })
})

it('splits on the first colon only, so an id may contain one', async () => {
const { parsePrincipal } = await loadIdentity({})
expect(parsePrincipal('mcp:hugo:laptop'))
.toEqual({ surface: 'mcp', externalId: 'hugo:laptop' })
})

it('refuses an unknown surface rather than inventing one', async () => {
const { parsePrincipal } = await loadIdentity({})
// A wrong identity row is a join key that merges two people.
expect(parsePrincipal('slack:U123')).toBeNull()
})

it.each([
['no separator', 'github'],
['an empty id', 'github:'],
['an empty surface', ':4271224'],
['nothing at all', undefined],
])('refuses %s', async (_label, principal) => {
const { parsePrincipal } = await loadIdentity({})
expect(parsePrincipal(principal)).toBeNull()
})
})

describe('surfaceOf', () => {
it('maps the photon channel to imessage, matching the seeded principals', async () => {
const { surfaceOf } = await loadIdentity({})
// `trust.ts` mints `imessage:<phone>`; recording `photon` would not line up.
expect(surfaceOf('photon')).toBe('imessage')
})

it('passes the channels that already share their surface name', async () => {
const { surfaceOf } = await loadIdentity({})
expect(surfaceOf('github')).toBe('github')
expect(surfaceOf('linear')).toBe('linear')
expect(surfaceOf('mcp')).toBe('mcp')
})

it('falls back to local for the framework channels', async () => {
const { surfaceOf } = await loadIdentity({})
expect(surfaceOf('http')).toBe('local')
expect(surfaceOf('schedule')).toBe('local')
})
})

describe('maintainerIdentities', () => {
it('turns every configured principal into an identity row', async () => {
const { maintainerIdentities } = await loadIdentity({
MAINTAINER_GITHUB_ID: '4271224',
MAINTAINER_LINEAR_ID: 'lin-1',
MAINTAINER_PHONE: '+33600000000',
EVI_MCP_TOKEN: 'token',
})
expect(maintainerIdentities()).toEqual(expect.arrayContaining([
{ surface: 'github', externalId: '4271224' },
{ surface: 'linear', externalId: 'lin-1' },
{ surface: 'imessage', externalId: '+33600000000' },
{ surface: 'mcp', externalId: 'hugo' },
]))
})

it('omits a channel with no configured principal', async () => {
const { maintainerIdentities } = await loadIdentity({ MAINTAINER_GITHUB_ID: '4271224' })
expect(maintainerIdentities()).toEqual([{ surface: 'github', externalId: '4271224' }])
})

it('is empty when nothing is configured, so no person is seeded', async () => {
const { maintainerIdentities } = await loadIdentity({})
expect(maintainerIdentities()).toEqual([])
})
})
Loading
Loading