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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
with:
node-version: 22
- run: npm install
- run: npm test
- run: npm run typecheck
- run: npm run build

Expand Down
58 changes: 58 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# AGENTS.md

How a coding agent works on AgentLog. Humans start at [README.md](./README.md).

**Package 0.3.0** · spec **0.2.0** · last product freeze 2026-03-30 ·
upgraded 2026-08-17 against Watchtower 5.x.

## What this repo is

The **interchange format**. Watchtower captures. AgentLog validates and
converts. Do not add a second capture engine here.

| Lives here | Does not live here |
|---|---|
| Spec, JSON Schema, Zod, types | Floor, board, Fly indexer |
| Converters *to* AgentLog JSON | Disk discovery of ~/.codex, ~/.cursor |
| `AGENT_SOURCES` | Ora `ActorContext` (that stays in ora-ai `packages/agentlog` 0.1.0) |

The ora-ai workspace copy is still **0.1.0** plus ActorContext. Do not
overwrite it from this tree. Do not copy ActorContext into this Apache
repo.

## Do this first

1. `npm install && npm test && npm run typecheck && npm run build`
2. A new converter must emit a document `validateAgentLog` accepts.
3. New Watchtower sources go in `src/sources.ts` and the watchtower
name/provider maps. Do not invent a parallel enum.
4. Capture on disk stays in `@braintied/watchtower` /
`github.com/braintied/watchtower`. This package consumes
`SessionPayload`.

## Convert paths

```ts
import { exportFromPayload } from '@braintied/agentlog/convert/payload';
import { exportWatchtowerSession } from '@braintied/agentlog/convert/watchtower';
import { convertGrokHistory } from '@braintied/agentlog/convert/grok';
```

`exportWatchtowerSession(..., { messages })` uses live
`session_messages`. Without that it falls back to
`metadata.raw_content` (`[user] ` / `[assistant] `), which is the
March 2026 reconstruction and loses tool rows.

Session keys are `source:<uuid>` (`grok:…`, `claude:…`). The converter
splits on the first colon.

## Do not

- Bump `specVersion` for a converter-only change. Spec 0.2.0 stays
until the schema grows.
- Publish `@braintied/agentlog` from ora-ai. This repo is the public
package.
- Point strangers at `ora-watchtower.fly.dev`.
- Add Aider/Codex disk parsers that duplicate Watchtower adapters
unless Watchtower itself grew a new on-disk format this package
must read without Watchtower installed.
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Changelog

## 0.3.0 — 2026-08-17

Five months after 0.2.1 (2026-03-30). Spec stays **0.2.0**. The package
caught up to Watchtower 5.x.

- `AGENT_SOURCES`: `claude_code`, `grok`, `codex`, `cursor`,
`opencode`, `gemini`, `kulti_meet`
- `convert/payload`: Watchtower `SessionPayload` (the live webhook)
- `convert/watchtower`: prefers `session_messages` over
`metadata.raw_content`; maps grok / opencode / kulti_meet
- `convert/grok`: `chat_history.jsonl`
- Tests: `npm test`
- AGENTS.md for implementers

Did not publish to npm from this change. Did not merge Ora
`ActorContext` into this Apache tree.

## 0.2.1 — 2026-03-30

JSON Schema, spec, and examples aligned to 0.2.0.

## 0.2.0 — 2026-03-30

Twelve event types, OTel fields, multi-agent team object.

## 0.1.1 — 2026-03-30

JSON Schema and debugging example.

## 0.1.0 — 2026-03-30

First public release.
52 changes: 44 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ AgentLog defines a portable JSON format for recording what happens during AI age

## The Problem

Every AI coding tool (Claude Code, Cursor, Codex, Aider) produces session logs in its own proprietary format. There's no way to:
Every AI coding tool (Claude Code, Grok, Codex, Cursor, OpenCode, Kulti) produces session logs in its own proprietary format. There's no way to:

- **Search** across sessions from different tools
- **Measure** AI coding impact across your engineering org
Expand Down Expand Up @@ -79,12 +79,37 @@ const session = await convertClaudeCodeSession('~/.claude/projects/myproject/ses
console.log(session.events.length, 'events captured');
```

### Export from Watchtower
### Export from Watchtower 5.x

Watchtower is the capture product. AgentLog is the interchange format.
Hooks and disk adapters already emit a `SessionPayload`. Convert that:

```typescript
import { exportFromPayload } from '@braintied/agentlog/convert/payload';

const agentLog = exportFromPayload(webhookBody);
```

Or a `coding_sessions` row, with live `session_messages` when you have them
(they win over the old `metadata.raw_content` reconstruction):

```typescript
import { exportWatchtowerSession } from '@braintied/agentlog/convert/watchtower';

const agentLog = exportWatchtowerSession(dbRow, { projectName: 'my-app' });
const agentLog = exportWatchtowerSession(dbRow, {
projectName: 'my-app',
messages: sessionMessages,
});
```

### Convert a Grok history file

```typescript
import { convertGrokHistory } from '@braintied/agentlog/convert/grok';

const session = await convertGrokHistory(
'~/.grok/sessions/.../chat_history.jsonl',
);
```

## Schema
Expand All @@ -108,7 +133,8 @@ The required context: who, when, where, what tool.

### Layer 2 — Event Timeline

Seven event types capture everything that happened:
Twelve event types (spec 0.2.0). Capture produces `message` and
`toolCall` first; the rest are for producers that have them.

| Type | What it captures | Key fields |
|------|-----------------|------------|
Expand All @@ -119,6 +145,11 @@ Seven event types capture everything that happened:
| **`search`** | Code/web search | `tool`, `query`, `resultCount` |
| **`reasoning`** | AI decision-making | `intent`, `alternatives`, `rationale` |
| **`error`** | Errors + recovery | `message`, `code`, `recovery`, `resolved` |
| **`handoff`** | Agent-to-agent | `fromAgent`, `toAgent`, `reason` |
| **`approval`** | Human gates | `action`, `approver`, `decision` |
| **`plan`** | Planned steps | `title`, `steps` |
| **`checkpoint`** | Save points | `checkpointType`, `label` |
| **`contextLoad`** | Injected context | `source`, `query` |

All events share: `id`, `timestamp`, `parentId` (for nesting), `durationMs`, `properties` (extensibility).

Expand Down Expand Up @@ -160,11 +191,16 @@ Every object has a `properties` bag for vendor-specific data ([SARIF pattern](ht

| Converter | Status |
|-----------|--------|
| Watchtower `SessionPayload` (5.x wire) | Available (`convert/payload`) |
| Watchtower `coding_sessions` + `session_messages` | Available (`convert/watchtower`) |
| Claude Code JSONL | Available |
| Watchtower DB | Available |
| Aider | Planned |
| OpenAI Codex | Planned |
| Cursor | Planned |
| Grok `chat_history.jsonl` | Available (`convert/grok`) |
| Codex / Cursor / OpenCode disk | Capture is Watchtower's job. Feed the payload. |
| Aider | Not started |

Sources named in `AGENT_SOURCES`: `claude_code`, `grok`, `codex`,
`cursor`, `opencode`, `gemini`, `kulti_meet`. A missing source is not
silently remapped.

## Examples

Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 12 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@braintied/agentlog",
"version": "0.2.1",
"description": "AgentLog — Open standard for AI agent session interchange. Schema, types, validation, and converters.",
"version": "0.3.0",
"description": "Portable JSON for AI agent sessions. Spec 0.2.0. Converts Claude Code, Grok, Codex, Cursor, OpenCode, and Watchtower 5.x.",
"homepage": "https://github.com/braintied/agentlog",
"repository": {
"type": "git",
Expand All @@ -22,6 +22,14 @@
"types": "./dist/convert/watchtower.d.ts",
"import": "./dist/convert/watchtower.js"
},
"./convert/payload": {
"types": "./dist/convert/payload.d.ts",
"import": "./dist/convert/payload.js"
},
"./convert/grok": {
"types": "./dist/convert/grok.d.ts",
"import": "./dist/convert/grok.js"
},
"./schemas/agentlog.schema.json": "./schemas/agentlog.schema.json"
},
"files": [
Expand All @@ -32,7 +40,8 @@
],
"scripts": {
"build": "tsup",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"test": "npm run build && node --test test/*.test.mjs"
},
"dependencies": {
"zod": "^4.3.6"
Expand Down
1 change: 1 addition & 0 deletions spec/agentlog-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
**Version**: 0.2.0
**Status**: Draft
**Date**: 2026-03-30
**Updated**: 2026-08-17 (implementations: package 0.3.0; schema unchanged)
**License**: Apache-2.0

## 1. Introduction
Expand Down
84 changes: 84 additions & 0 deletions src/convert/grok.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Grok `chat_history.jsonl` → AgentLog.
*
* Path: ~/.grok/sessions/<cwdEnc>/<sessionId>/chat_history.jsonl
* Capture still belongs to Watchtower. This converter is for a file
* you already have.
*
* @license Apache-2.0
*/

import { readFile } from 'node:fs/promises';
import { basename, dirname } from 'node:path';
import type { AgentLog } from '../schema.js';
import { exportFromPayload, type CapturedMessage, type PayloadConvertOptions } from './payload.js';

export interface GrokConvertOptions extends PayloadConvertOptions {
sessionId?: string;
}

export async function convertGrokHistory(
historyPath: string,
options: GrokConvertOptions = {},
): Promise<AgentLog> {
const text = await readFile(historyPath, 'utf8');
const messages: CapturedMessage[] = [];
for (const line of text.split('\n')) {
const trimmed = line.trim();
if (trimmed === '') continue;
let entry: Record<string, unknown>;
try {
entry = JSON.parse(trimmed) as Record<string, unknown>;
} catch {
continue;
}
const kind = typeof entry.type === 'string' ? entry.type : '';
if (kind === 'system' || kind === 'reasoning' || kind === 'tool_result') continue;
let role = '';
let content = '';
if (kind === 'user') {
const raw = extractText(entry.content).trim();
if (raw.includes('<user_info>') && !raw.includes('<user_query>')) continue;
if (raw.startsWith('<system-reminder>') && !raw.includes('<user_query>')) continue;
const query = raw.match(/<user_query>\s*([\s\S]*?)\s*<\/user_query>/);
content = query !== null && query[1].trim() !== '' ? query[1].trim() : raw;
role = 'user';
} else if (kind === 'assistant') {
role = 'assistant';
content = extractText(entry.content).trim();
} else {
continue;
}
if (content === '') continue;
const timestamp = typeof entry.timestamp === 'string'
? entry.timestamp
: new Date(0).toISOString();
messages.push({ role, content, timestamp });
}

const sessionDir = dirname(historyPath);
const sessionId = options.sessionId !== undefined ? options.sessionId : basename(sessionDir);
return exportFromPayload({
session_key: `grok:${sessionId}`,
source: 'grok',
messages,
message_count: messages.length,
session_started_at: messages.length > 0 ? messages[0].timestamp : undefined,
session_ended_at: messages.length > 0 ? messages[messages.length - 1].timestamp : undefined,
}, options);
}

function extractText(content: unknown): string {
if (typeof content === 'string') return content;
if (!Array.isArray(content)) return '';
return content
.map((part) => {
if (typeof part === 'string') return part;
if (part !== null && typeof part === 'object' && typeof (part as { text?: unknown }).text === 'string') {
return (part as { text: string }).text;
}
return '';
})
.filter((part) => part !== '')
.join('\n');
}
Loading
Loading