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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<p align="center">
<img src="https://img.shields.io/badge/node-%3E%3D22-brightgreen" alt="Node.js >= 22">
<img src="https://img.shields.io/badge/license-MIT-blue" alt="License: MIT">
<img src="https://img.shields.io/badge/100%25-KERN-orange" alt="100% KERN">
<img src="https://img.shields.io/badge/rating-Glicko--2-red" alt="Glicko-2 rated">
</p>

**The competitive multi-AI orchestration CLI.**
Expand Down
20 changes: 6 additions & 14 deletions assets/agon-hero-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 4 additions & 10 deletions assets/agon-hero.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions packages/cli/src/generated/blocks/engine.entry.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node
// @generated by kern v3.5.7 — DO NOT EDIT. Source: src/kern/blocks/engine.kern
// @generated by kern v3.5.8 — DO NOT EDIT. Source: src/kern/blocks/engine.kern

// @kern-source: engine:1175
// @kern-source: engine:1183

import React from 'react';
import { render } from 'ink';
Expand Down
30 changes: 18 additions & 12 deletions packages/cli/src/generated/blocks/engine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function EngineProgressView({ engines, mode }: { engines:EngineProgress[]
}

// @kern-source: engine:115
const EngineBlock = React.memo(function EngineBlock({ engineId, color, content, actingNote }: { engineId:string; color:number; content:string; actingNote?:string }) {
const EngineBlock = React.memo(function EngineBlock({ engineId, color, content, actingNote, foldedSteps }: { engineId:string; color:number; content:string; actingNote?:string; foldedSteps?:number }) {
const wrapWidth = contentWidth(8);
const cleaned = cleanEngineOutput(content);
const hexColor = color256toHex(color);
Expand All @@ -106,6 +106,9 @@ const EngineBlock = React.memo(function EngineBlock({ engineId, color, content,
return (
<Box flexDirection="column" marginY={1} paddingLeft={2}>
<Text color={hexColor}>{'\u250c\u2500\u2500 '}<Text bold color={hexColor}>{engineId}</Text>{actingNote ? <Text dimColor>{' ('}{actingNote}{')'}</Text> : null}</Text>
{foldedSteps && foldedSteps > 0 ? (
<Text color={hexColor}>{'\u2502 '}<Text dimColor>{'\u25b8 '}{String(foldedSteps)}{' reasoning steps folded \u00b7 /raw to inspect'}</Text></Text>
) : null}
<Text color={hexColor}>{'\u2502'}</Text>
<RenderedSegments segments={segments} borderColor={hexColor} wrapWidth={wrapWidth} />
<Text color={hexColor}>{'\u2514\u2500\u2500'}</Text>
Expand All @@ -114,8 +117,8 @@ const EngineBlock = React.memo(function EngineBlock({ engineId, color, content,
});
export { EngineBlock };

// @kern-source: engine:150
const ConversationalResponse = React.memo(function ConversationalResponse({ engineId, content, actingNote }: { engineId:string; content:string; actingNote?:string }) {
// @kern-source: engine:154
const ConversationalResponse = React.memo(function ConversationalResponse({ engineId, content, actingNote, foldedSteps }: { engineId:string; content:string; actingNote?:string; foldedSteps?:number }) {
const wrapWidth = contentWidth(2);
const cleaned = cleanEngineOutput(content);
if (!cleaned.trim()) return null;
Expand All @@ -124,14 +127,17 @@ const ConversationalResponse = React.memo(function ConversationalResponse({ engi
return (
<Box flexDirection="column" marginTop={1} marginBottom={0} paddingLeft={1}>
<Text><Text color={accentColor} bold>{icons().dotOn + ' '}{engineId}</Text>{actingNote ? <Text dimColor>{' ('}{actingNote}{')'}</Text> : null}</Text>
{foldedSteps && foldedSteps > 0 ? (
<Text dimColor>{'▸ '}{String(foldedSteps)}{' reasoning steps folded · /raw to inspect'}</Text>
) : null}
<Text>{' '}</Text>
<RenderedSegments segments={segments} borderColor={''} wrapWidth={wrapWidth} />
</Box>
);
});
export { ConversationalResponse };

// @kern-source: engine:172
// @kern-source: engine:180
const CesarRecapBlock = React.memo(function CesarRecapBlock({ event }: { event:OutputEvent & { type: 'cesar-recap' } }) {
const files = Array.isArray((event as any).files) ? (event as any).files : [];
const commands = Array.isArray((event as any).commands) ? (event as any).commands : [];
Expand Down Expand Up @@ -233,7 +239,7 @@ const CesarRecapBlock = React.memo(function CesarRecapBlock({ event }: { event:O
});
export { CesarRecapBlock };

// @kern-source: engine:277
// @kern-source: engine:285
export function DashboardView({ event }: { event:OutputEvent & { type: 'dashboard' } }) {
return (
<Box flexDirection="column" paddingX={1} paddingY={1}>
Expand Down Expand Up @@ -301,7 +307,7 @@ export function DashboardView({ event }: { event:OutputEvent & { type: 'dashboar
);
}

// @kern-source: engine:349
// @kern-source: engine:357
function TableView({ headers, rows }: { headers:string[]; rows:string[][] }) {
const widths = headers.map((h: string, i: number) =>
Math.max(h.length, ...rows.map((r: string[]) => (r[i] ?? '').length)) + 2,
Expand All @@ -325,7 +331,7 @@ function TableView({ headers, rows }: { headers:string[]; rows:string[][] }) {
);
}

// @kern-source: engine:378
// @kern-source: engine:386
const OutputBlockView = React.memo(function OutputBlockView({ event, mode, toolOutputExpanded, thinkingExpanded }: { event:OutputEvent; mode:string; toolOutputExpanded?:boolean; thinkingExpanded?:boolean }) {
switch (event.type) {
case 'text': {
Expand All @@ -344,8 +350,8 @@ const OutputBlockView = React.memo(function OutputBlockView({ event, mode, toolO
</Box>
);
case 'engine-block': return mode === 'chat'
? <ConversationalResponse engineId={event.engineId} content={event.content} actingNote={event.actingNote} />
: <EngineBlock engineId={event.engineId} color={event.color} content={event.content} actingNote={event.actingNote} />;
? <ConversationalResponse engineId={event.engineId} content={event.content} actingNote={event.actingNote} foldedSteps={event.foldedSteps} />
: <EngineBlock engineId={event.engineId} color={event.color} content={event.content} actingNote={event.actingNote} foldedSteps={event.foldedSteps} />;
case 'separator': return <Text>{' '}</Text>;
case 'header': return <Box flexDirection="column"><Text>{' '}</Text><Text bold color="cyan">{' ' + icons().header + ' '}{event.title}</Text></Box>;
case 'success': return <Text>{' '}<Text color="#4ade80">{icons().success}</Text>{' '}{event.message}</Text>;
Expand Down Expand Up @@ -936,7 +942,7 @@ const OutputBlockView = React.memo(function OutputBlockView({ event, mode, toolO
});
export { OutputBlockView };

// @kern-source: engine:995
// @kern-source: engine:1003
const ToolCallGroup = React.memo(function ToolCallGroup({ blocks }: { blocks:OutputBlock[] }) {
const labelForTool = (raw: unknown) => {
const toolKey = String(raw ?? '').toLowerCase();
Expand Down Expand Up @@ -1081,7 +1087,7 @@ const ToolCallGroup = React.memo(function ToolCallGroup({ blocks }: { blocks:Out
});
export { ToolCallGroup };

// @kern-source: engine:1146
// @kern-source: engine:1154
const DebateGroup = React.memo(function DebateGroup({ blocks }: { blocks:OutputBlock[] }) {
const round = (blocks[0]?.event as any)?.round ?? '?';
const w = contentWidth(6);
Expand All @@ -1107,7 +1113,7 @@ const DebateGroup = React.memo(function DebateGroup({ blocks }: { blocks:OutputB
});
export { DebateGroup };

// @kern-source: engine:1175
// @kern-source: engine:1183
const BidGroup = React.memo(function BidGroup({ blocks }: { blocks:OutputBlock[] }) {
const w = contentWidth(6);
return (
Expand Down
200 changes: 200 additions & 0 deletions packages/cli/src/generated/blocks/narration-fold.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// @generated by kern v3.5.8 — DO NOT EDIT. Source: src/kern/blocks/narration-fold.kern

/**
* Result of foldNarration. visible is what renders to scrollback; raw is the untouched original kept (ring) for /raw + copy/search; foldedSteps counts the narration sentences collapsed into the placeholder.
*/
// @kern-source: narration-fold:28
export interface NarrationFoldResult {
raw: string;
visible: string;
foldedSteps: number;
didFold: boolean;
}

/**
* Result of foldNarrationLines (live streaming). visible is the substance to render; foldedSteps is the narration line count collapsed in the current window; lastStep is the most recent narration line, shown as a transient working indicator.
*/
// @kern-source: narration-fold:35
export interface LiveNarrationFold {
visible: string;
foldedSteps: number;
lastStep: string;
}

// @kern-source: narration-fold:44
export const _NARR_STARTERS: string = "I will|I'll|I am going to|I'm going to|Let me|Let's|Now I|Now let|Next,? I|Next,? let|I need to|I should|I want to|Checking|Searching|Reading|Viewing|Opening|Running|Looking at|First,? I";

// @kern-source: narration-fold:45
export const _NARR_INTENT_RE = new RegExp(`^(?:${_NARR_STARTERS})\\b`, 'i');

// @kern-source: narration-fold:47
export const _NARR_TOOL_VERB_RE = /\b(?:read|re-?read|view|inspect|examine|search(?:ing)?|grep|list|open(?:ing)?|check(?:ing)?|look(?:ing)?\s+at|see\s+(?:if|how|what|where)|locate|find(?:\s+(?:the|where|references|out))?|run(?:ning)?\s+(?:the\s+)?tests?|scan(?:ning)?|explore|navigate|understand\s+how|verify|map\s+out|trace)\b/i;

// @kern-source: narration-fold:49
export const _NARR_SUBSTANCE_RE = /\b(?:recommend|the\s+(?:fix|issue|bug|root\s+cause|answer|problem|solution|reason)|because|therefore|so\s+that|found\s+that|in\s+(?:short|summary|conclusion)|here(?:'s|\s+is)|refers?\s+to|means?\b|works?\s+by|consists?\s+of|note\s+that|important(?:ly)?|warning|trade-?off|risk|failure\s+mode|you\s+(?:should|can|need)|we\s+(?:should|can|need)|confidence)\b/i;

// @kern-source: narration-fold:52
export const _NARR_PROTECTED_RE = /^(?:#{1,6}\s|[-*+]\s|\d+[.)]\s|>\s|\||@@FENCE)/;

/**
* Precision-biased narration predicate: a segment is narration only if it opens with a research-intent starter AND carries a tool verb AND no answer/structure/question signal. Anything ambiguous is substance.
*/
// @kern-source: narration-fold:54
function _isNarrationSegment(s: string): boolean {
const t = String(s ?? '').trim();
if (!t) return false;
if (_NARR_PROTECTED_RE.test(t)) return false;
if (t.endsWith('?')) return false;
if (!_NARR_INTENT_RE.test(t)) return false;
if (_NARR_SUBSTANCE_RE.test(t)) return false;
if (!_NARR_TOOL_VERB_RE.test(t)) return false;
if (t.length > 240) return false;
return true;
}

/**
* Collapse contiguous research-narration runs in an engine's final text, keeping code, structure, and the answer tail visible. policy: 'off' (no-op) | 'safe' (default, min run 2) | 'aggressive' (min run 1). Pure + deterministic -- safe to run on every engine-block; clean answers fold nothing.
*/
// @kern-source: narration-fold:69
export function foldNarration(text: string, policy?: string): NarrationFoldResult {
const raw = String(text ?? '');
const mode = policy ?? 'safe';
const noFold: NarrationFoldResult = { raw, visible: raw, foldedSteps: 0, didFold: false };
if (mode === 'off') return noFold;
if (!raw.trim()) return noFold;

// 1) Protect fenced code blocks. Use an @@FENCE@@ sentinel (non-
// whitespace) so the sentence trimming/segmentation below can't
// strip the guard and leak the token into visible text.
const fences: string[] = [];
const guarded = raw.replace(/```[\s\S]*?```/g, (m) => {
fences.push(m);
return `@@FENCE${fences.length - 1}@@`;
});

// 2) Segment into sentences. Black-box agents glue narration when tool
// markup is stripped ("...in the REPL.In Agon AI, open files..."), so
// split (a) on newlines, (b) after sentence punctuation before a
// Capitalized word, and (c) before a narration starter.
const seg = guarded
.replace(/\r/g, '')
.replace(/\n+/g, '\n')
.replace(/([.!?])(?=[A-Z][a-z])/g, '$1\n')
.replace(new RegExp(`([\\s.;:!?)\\]])(?=(?:${_NARR_STARTERS})\\b)`, 'g'), '$1\n');
const sentences = seg.split('\n').map((s) => s.trim()).filter((s) => s.length > 0);

// 3) Classify.
const flags = sentences.map((s) => _isNarrationSegment(s));

// 4) Answer tail: the trailing run of NON-narration sentences. Always
// shown, and required -- a pure-narration turn (no answer) is never
// folded to nothing.
let tailStart = sentences.length;
for (let i = sentences.length - 1; i >= 0; i--) {
if (flags[i]) break;
tailStart = i;
}
if (tailStart >= sentences.length) return noFold;

// 5) Fold contiguous narration runs (>= min length) before the tail.
const FOLD_MIN_RUN = mode === 'aggressive' ? 1 : 2;
const out: string[] = [];
let folded = 0;
let i = 0;
while (i < tailStart) {
if (flags[i]) {
let j = i;
while (j < tailStart && flags[j]) j++;
const runLen = j - i;
if (runLen >= FOLD_MIN_RUN) {
folded += runLen;
} else {
for (let k = i; k < j; k++) out.push(sentences[k]);
}
i = j;
} else {
out.push(sentences[i]);
i++;
}
}
for (let k = tailStart; k < sentences.length; k++) out.push(sentences[k]);

if (folded === 0) return noFold;

// 6) Restore fences (sentinel matches regardless of surrounding space),
// tidy blank runs.
const visible = out
.join('\n')
.replace(/@@FENCE(\d+)@@/g, (_m, n) => fences[Number(n)] ?? '')
.replace(/\n{3,}/g, '\n\n')
.trim();

return { raw, visible, foldedSteps: folded, didFold: true };
}

/**
* Live/streaming variant. Collapses narration LINES (one per source line) into a count + the most recent step, WITHOUT requiring a substance tail -- live content is transient and uncommitted, so folding leading/all narration is correct. StreamingView runs this over its already-bounded visible window so a verbose engine's wall never appears even mid-stream. Fences are protected. 'off' is a no-op.
*/
// @kern-source: narration-fold:148
export function foldNarrationLines(text: string, policy?: string): LiveNarrationFold {
const raw = String(text ?? '');
const mode = policy ?? 'safe';
const noFold: LiveNarrationFold = { visible: raw, foldedSteps: 0, lastStep: '' };
if (mode === 'off' || !raw.trim()) return noFold;
const lines = raw.split('\n');
let inFence = false;
const out: string[] = [];
let folded = 0;
let lastStep = '';
for (const ln of lines) {
if (/^\s*```/.test(ln)) { inFence = !inFence; out.push(ln); continue; }
if (!inFence && _isNarrationSegment(ln)) { folded++; lastStep = ln.trim(); continue; }
out.push(ln);
}
if (folded === 0) return noFold;
const visible = out.join('\n').replace(/\n{3,}/g, '\n\n').trim();
return { visible, foldedSteps: folded, lastStep };
}

// @kern-source: narration-fold:176
export const _FOLD_RING_MAX: number = 20;

// @kern-source: narration-fold:177
export const _foldedRaws: string[] = [] as string[];

/**
* Record the raw (unfolded) text of a just-folded engine-block, capped at the last 20.
*/
// @kern-source: narration-fold:179
export function setLastFoldedRaw(raw: string): void {
const r = String(raw ?? '');
if (!r) return;
_foldedRaws.push(r);
while (_foldedRaws.length > _FOLD_RING_MAX) _foldedRaws.shift();
}

/**
* Number of recent folded raws currently retained (0..20).
*/
// @kern-source: narration-fold:188
export function getFoldedRawCount(): number {
return _foldedRaws.length;
}

/**
* Return the Nth-most-recent folded raw (1 = most recent). '' if out of range.
*/
// @kern-source: narration-fold:194
export function getFoldedRaw(fromEnd: number): string {
const n = Math.max(1, Math.floor(Number(fromEnd) || 1));
const idx = _foldedRaws.length - n;
return idx >= 0 ? _foldedRaws[idx] : '';
}

/**
* Return the most recently folded raw, or '' if nothing has been folded this session.
*/
// @kern-source: narration-fold:202
export function getLastFoldedRaw(): string {
return _foldedRaws.length ? _foldedRaws[_foldedRaws.length - 1] : '';
}
2 changes: 1 addition & 1 deletion packages/cli/src/generated/blocks/rendering.entry.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node
// @generated by kern v3.5.8 — DO NOT EDIT. Source: src/kern/blocks/rendering.kern

// @kern-source: rendering:447
// @kern-source: rendering:494

import React from 'react';
import { render } from 'ink';
Expand Down
Loading