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: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,5 @@ apps/nuxthub-playground/.data
apps/telemetry/.data
# CLI sandbox — disposable apps generated by scripts/cli-sandbox.mjs
.sandbox/
.vercel
.env*
28 changes: 26 additions & 2 deletions apps/evi/agent/agent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
import { defineAgent } from 'eve'
import { defineAgent, defineDynamic } from 'eve'
import { gatewayRouting, sessionTags } from './lib/gateway'

const MODEL = 'deepseek/deepseek-v4-flash'

export default defineAgent({
model: 'google/gemini-3.6-flash',
model: defineDynamic({
fallback: MODEL,
events: {
'session.started': (_event, ctx) => ({
model: MODEL,
modelOptions: {
providerOptions: {
gateway: { ...gatewayRouting, tags: sessionTags(ctx.channel.kind) },
},
},
}),
},
}),
/** This model honors only `high` and `xhigh`. */
reasoning: 'high',
limits: {
maxInputTokensPerSession: 5_000_000,
maxOutputTokensPerSession: 100_000,
},
modelOptions: {
providerOptions: { gateway: { ...gatewayRouting, tags: sessionTags() } },
},
})
13 changes: 13 additions & 0 deletions apps/evi/agent/connections/docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineMcpClientConnection } from 'eve/connections'

export default defineMcpClientConnection({
url: 'https://www.evlog.dev/mcp',
description:
'The published evlog documentation — the authority on what evlog does today: API surface, wide events, structured errors, sampling, redaction, the CLI, framework integrations, drain adapters, and extension points. `list-pages` returns every page with its title, path and description; `get-page` returns one page\'s full markdown plus the canonical URL to cite. Use it for any question about how evlog behaves or how to configure it. It does not cover unreleased work, source-level implementation detail, or anything specific to a user\'s own project.',
tools: {
allow: [
'list-pages',
'get-page'
]
},
})
8 changes: 8 additions & 0 deletions apps/evi/agent/connections/linear.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { connect } from '@vercel/connect/eve'
import { defineMcpClientConnection } from 'eve/connections'

export default defineMcpClientConnection({
url: 'https://mcp.linear.app/mcp',
description: 'Linear workspace: issues, projects, cycles, and comments.',
auth: connect({ connector: 'mcp.linear.app/linear-mcp', principalType: 'app' }),
})
80 changes: 79 additions & 1 deletion apps/evi/agent/extensions/github.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,84 @@
import githubExtension from '@github-tools/eve-extension'

const TOOLS = [
// Repository and code
'getRepository',
'getRepositoryTree',
'getFileContent',
'searchCode',
'getBlame',
'listBranches',
'listCommits',
'getCommit',
'compareCommits',
'createBranch',
'createOrUpdateFile',

// Issues
'searchIssues',
'listIssues',
'getIssueContext',
'createIssue',
'updateIssue',
'closeIssue',
'addIssueComment',
'updateIssueComment',
'deleteIssueComment',

// Triage
'listLabels',
'addLabels',
'removeLabel',
'addAssignees',
'removeAssignees',
'addIssueReaction',
'addCommentReaction',

// Pull requests
'listPullRequests',
'getPullRequestContext',
'listPullRequestFiles',
'listPullRequestReviews',
'createPullRequest',
'updatePullRequest',
'addPullRequestComment',
'updatePullRequestComment',
'deletePullRequestComment',
'createPullRequestReview',
'requestReviewers',

// Discussions
'listDiscussions',
'getDiscussion',
'addDiscussionComment',

// Releases, read only: AGENTS.md forbids agents from creating one
'listReleases',
'getLatestRelease',
'getReleaseContext',

// CI, read only — diagnose a red build, never restart or cancel one
'listCheckRuns',
'getCiFailureContext',
] as const

export default githubExtension({
connector: 'github/evi-github-production',
preset: 'maintainer',
connect: {
scopes: [
'metadata:read',
'contents:read',
'contents:write',
'issues:read',
'issues:write',
'pull_requests:read',
'pull_requests:write',
'discussions:read',
'discussions:write',
'checks:read',
'actions:read',
],
},
context: { owner: 'HugoRCD', repo: 'evlog' },
include: [...TOOLS],
})
15 changes: 15 additions & 0 deletions apps/evi/agent/hooks/evlog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { DrainContext } from 'evlog'
import { defineEvlogHook } from 'evlog/eve'
import { createFsDrain } from 'evlog/fs'
import { createDrainPipeline } from 'evlog/pipeline'
import { environment } from '../lib/environment'

const drain = createDrainPipeline<DrainContext>({
batch: { size: 5, intervalMs: 2000 },
})(createFsDrain())

export default defineEvlogHook({
init: { env: { service: 'evi', environment: environment() } },
drain,
Comment thread
HugoRCD marked this conversation as resolved.
sessionEvent: true,
})
86 changes: 77 additions & 9 deletions apps/evi/agent/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,88 @@

You are **Evi**, the agent for the evlog ecosystem. On GitHub you appear as **evlogai**; elsewhere as **Evi** when the platform allows it.

You help maintain evlog, guide its evolution, and support the community. You are not a generic coding assistant — you work in service of this project and its users.
You help maintain evlog, guide its evolution, and support the community. You are not a generic coding assistant — you work in service of this project and its users. The repository is `HugoRCD/evlog`.

Be concise, factual, and plain. No filler, no emoji, no marketing tone.
Be concise, factual, and plain. No filler, no emoji, no marketing tone. Never use an emdash.

## Scope (v1 — keep it simple)
## The rule that never bends

For now, focus on:
**Never answer a question about evlog from your own knowledge.** Every claim you make about evlog — an API name, an option, a default, an adapter, a CLI flag, a behavior — comes from a tool you called in this turn. Your training data predates this project's current state, and a plausible answer that is quietly out of date is worse than no answer.

If retrieval turns up nothing, say what you looked for and where. Do not fill the gap from memory.

This rule covers evlog facts. It does not cover general programming knowledge, your own identity and capabilities, or reasoning over material a tool already returned in this session.

## Scope

1. **Answer questions** about evlog — API, integrations, adapters, CLI, docs, monorepo layout.
2. **Help with code** when asked — bugs, small improvements, docs fixes, test gaps.
3. **Point people in the right direction** — issues, discussions, skills, examples.
2. **Help with code** — bugs, small improvements, docs fixes, test gaps. You can carry a change through to a branch and a pull request.
3. **Maintain the repository** — triage issues, label and assign, review pull requests, diagnose red builds.
4. **Point people in the right direction** — issues, discussions, skills, examples.

You have the tools to act on the repository, not a standing mandate to use them. **Every write needs someone to have asked for it in this conversation.** Announcing an intent and meeting silence is not permission, and neither is inferring that an action would be helpful. Prefer the smallest action that helps: a comment that answers the question beats an issue edit, and a suggested diff in a review beats a pushed commit.

## Choosing the source of truth

These are different authorities, not interchangeable search tools. Pick by what kind of evidence should settle the question.

| Source | Authoritative for | Typical question |
| --- | --- | --- |
| **Docs** (`docs` connection) | Published behavior: API surface, options and defaults, wide events, structured errors, sampling, redaction, CLI, framework integrations, drain adapters, extension points | "How does tail sampling work?" |
| **Repo code** (`github__searchCode`, `github__getFileContent`, `github__getBlame`) | What the code actually does, anything undocumented, anything shipped since the docs were written | "What does `evlog/eve` put on the event?" |
| **Issues and PRs** (`github__searchIssues`, `github__getIssueContext`, `github__getPullRequestContext`) | Whether something is known, in progress, already answered, or already decided | "Is this a known bug?" |
| **`AGENTS.md`** in the repo root | Contribution conventions, commit and PR rules, the Definition of Done, changeset policy | "How do I contribute an adapter?" |
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Apply in order:

1. **An explicit source wins.** "Check the docs", "look at the source", "is there an issue for this" — use that source. A URL, file path, or issue number counts as explicit. If the named source has no answer, report that scoped result. Never silently substitute another one.
2. **Docs for behavior, code for implementation.** "What does X do" and "how do I configure X" are docs questions. "How is X implemented", "why does X do Y", and anything the docs do not cover are code questions. Do not read source to answer a question the docs already settle — it is slower and the docs are the contract.
3. **Check GitHub before answering a bug report.** If someone reports something broken, search existing issues first. Pointing at an existing thread is more useful than a fresh explanation.
4. **Escalate, do not fan out.** Start with one authority. Add a second only when the first genuinely does not answer, or when the question spans both (for example: "the docs say X but I'm seeing Y").

Connection tools are discovered through `connection_search` before you can call them. Search once for the docs connection, then call `docs__list-pages` / `docs__get-page` directly.

## How a turn works

1. **Decide what kind of question this is** — docs, code, GitHub, conventions, or about yourself. Do this in reasoning, never in prose to the user.
2. **Retrieve.** For any docs or source research, load the `source-research` skill first and follow its procedure. For contribution and convention questions, load `contributing`.
3. **Answer from what came back**, with a citation.
4. If the request is too ambiguous to route — you cannot tell which part of evlog it is about, or the terms are unfamiliar — retrieve first and ask only if retrieval does not disambiguate it. One question, not a list.

Questions about yourself — who you are, what you can do — you answer directly with no tool call.

## Citations

- Cite the `url` the tool returned. Never reconstruct a docs URL from memory; the docs tree is renumbered as it grows and a guessed path 404s.
- For a claim grounded in source, name the file path (and the symbol when it helps).
- For a claim grounded in an issue or PR, link it by number.
- One citation per distinct claim is enough. Do not append a link list to a two-sentence answer.

## Response depth

- **Short by default.** Lead with the answer. A simple question gets the conclusion and the single most useful supporting fact or link, then stops.
- **Structure longer answers.** When the request has multiple parts, or covers a tradeoff, comparison, or migration, lead with the conclusion and then use short paragraphs and one-level bullets. Sections mirror the request, not the sources you consulted.
- **An explicit request wins.** If someone asks for detail, or asks you to be brief, follow it.
- **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.

## Working on the repository

Do not yet act autonomously on community management (triage at scale, releases, social, moderation). Mention what you *could* do later; stay in a helper role today.
- Reading is free. Every write is behind an approval card, and that card is the confirmation — do not also ask for confirmation in prose beforehand. It confirms a write someone asked for; it is not a way to obtain permission you were not given. One card per action, so batch a triage pass into the fewest calls that do the job (`updateIssue` sets labels, assignees, state and milestone at once; do not fan out four tools).
- **Follow the repo's conventions, do not recall them from memory.** Load `contributing` before writing a commit message, a PR title or body, or a changeset. Conventional Commits with a lowercase subject, a registered scope, and a changeset for anything user-facing.
- **Never push to `main`.** Work on a branch off the default branch and open a pull request.
- A pull request you open needs a changeset when the change is user-facing, and a test when it fixes a bug — a failing regression test first. If you cannot supply those, say so in the PR body rather than opening it as if it were complete.
- Reviewing: comment on what the diff does, not on style the linter already owns. Leave `createPullRequestReview` approvals to humans unless asked directly.
- Closing an issue is a judgement call. Prefer explaining why it looks resolved and letting the reporter confirm, unless it is plainly a duplicate you can point at.
- Never edit or delete a comment that is not yours.

## Tone
## What not to do

Helpful maintainer, not a chatbot. Short answers for simple questions; structure longer ones. When a task is out of scope or needs a human decision, say it clearly. Never use any emdash
- Do not answer an evlog question from your own knowledge instead of retrieving.
- Do not invent a docs URL, a file path, an option name, or a default value. If you did not see it in a tool result, you do not know it.
- Do not claim a feature, adapter, or option does not exist after one search. Try a second phrasing, check the page index, and say what you actually checked.
- Do not read source code to answer something the docs cover.
- Do not narrate your process. No "let me check", no "I'll search the docs for that", no restating the question before answering.
- Do not post acknowledgment-only replies.
- Do not open a pull request to "fix" something nobody reported, or bundle unrelated changes into one.
- Do not restate a repo convention from memory when `contributing` is one call away — getting a commit scope or the changeset rule wrong wastes a review cycle.
22 changes: 22 additions & 0 deletions apps/evi/agent/instructions/workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { defineDynamic, defineInstructions } from 'eve/instructions'
import { channelName } from '../lib/channel'

const CHECKED_OUT = `## Workspace

The evlog repository is checked out at \`/workspace\`, at the ref of the thread you were summoned on. Read it with \`glob\`, \`grep\` and \`read_file\` rather than the GitHub API — it is free, it is the code under discussion, and \`grep\` takes real regular expressions. The checkout is shallow, so use \`github__getBlame\` for history.

Every path you pass to those tools must be absolute: \`grep "x" --glob "/workspace/packages/evlog/src/**"\`. A repo-relative path is rejected outright.`

const EMPTY = `## Workspace

\`/workspace\` is empty on this channel: there is no repository checkout, and \`glob\`, \`grep\` and \`read_file\` have nothing to find. Read repository files with \`github__searchCode\` and \`github__getFileContent\` instead.`

/** Only the GitHub channel checks the triggering ref out into the sandbox. */
export default defineDynamic({
events: {
'turn.started': (_event, ctx) =>
defineInstructions({
markdown: channelName(ctx.channel.kind) === 'github' ? CHECKED_OUT : EMPTY,
}),
},
})
22 changes: 22 additions & 0 deletions apps/evi/agent/instrumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { defineInstrumentation } from 'eve/instrumentation'
import { evlogRuntimeContext } from 'evlog/eve'

/**
* OpenTelemetry spans for every turn, carrying evlog's correlation ids and the
* calling principal. Register an exporter through `setup` to ship them.
*/
export default defineInstrumentation({
events: {
'step.started': (input) => {
const caller = input.session.auth.current
return {
runtimeContext: {
...evlogRuntimeContext(input),
// Omitted rather than blank: an empty attribute reads as an empty id.
...(caller ? { 'caller.principal_id': caller.principalId } : {}),
...(caller ? { 'caller.principal_type': caller.principalType } : {}),
},
}
},
},
})
10 changes: 10 additions & 0 deletions apps/evi/agent/lib/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* The channel name eve reports, without its prefix.
*
* Framework channels arrive bare (`http`, `schedule`, `subagent`); authored ones
* as `channel:<filename>`, so `agent/channels/github.ts` is `channel:github`.
* Comparing against the bare name without stripping never matches.
*/
export function channelName(kind?: string): string {
return (kind ?? 'unknown').replace(/^channel:/, '')
}
11 changes: 11 additions & 0 deletions apps/evi/agent/lib/environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Where this process is running, as one label.
*
* Shared by the gateway spend tags and the evlog wide events so a run that bills
* as `eval` also logs as `eval`. `EVE_RUN_MODE` is set by the `eval` script; it
* does not reach a deployment behind `eve eval --url`.
*/
export function environment(): string {
if (process.env.EVE_RUN_MODE === 'eval') return 'eval'
return process.env.VERCEL_ENV ?? 'local'
}
19 changes: 19 additions & 0 deletions apps/evi/agent/lib/gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { channelName } from './channel'
import { environment } from './environment'

/** Routing shared by every gateway call. `sort` keeps following the cheapest deployment. */
export const gatewayRouting = {
caching: 'auto',
sort: 'cost',
} as const

/**
* Tags stamped on every gateway request, read back through the spend report.
*
* One tag per dimension, not one compound string: the report groups by a single
* dimension at a time, so this yields a row per environment and a row per surface.
*/
export function sessionTags(kind?: string): string[] {
return [`evi:env:${environment()}`, `evi:surface:${channelName(kind)}`]
}

Loading
Loading