mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-25 14:02:22 +02:00
[eric] chat: stop filing the answer as tool chrome, only short unstructured narration folds into the group
This commit is contained in:
@@ -78,6 +78,7 @@ import { setCardSidecar, commitDraft, updateWorkflowCard, controlWorkflowRun } f
|
||||
import { shallowEqual } from 'react-redux';
|
||||
import { useClaudeTokens, useThemeAccent, useThemeMode } from '@/shared/styles/ThemeContext';
|
||||
import { parseMcpToolName, getMcpInputSummary } from '@/shared/mcpToolMeta';
|
||||
import { isNarration } from './parsing/isNarration';
|
||||
|
||||
const CONTEXT_WINDOWS: Record<string, number> = {
|
||||
'opus-4-8': 1_000_000,
|
||||
@@ -1116,7 +1117,9 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
let j = i;
|
||||
while (j < activeBranchMessages.length && activeBranchMessages[j].role === 'assistant') j++;
|
||||
const next = activeBranchMessages[j];
|
||||
if (next && (next.role === 'tool_call' || next.role === 'tool_result')) {
|
||||
const absorbable = activeBranchMessages.slice(i, j).every((a) => isNarration(a.content));
|
||||
// A long or structured message is the answer, not narration. Absorbing it hides the whole deliverable in a grey tool row and strips its markdown, which is worse than showing one redundant line.
|
||||
if (next && absorbable && (next.role === 'tool_call' || next.role === 'tool_result')) {
|
||||
for (let k = i; k < j; k++) {
|
||||
if (!activeBranchMessages[k].hidden) noteMarks.push({ afterCall: callsSoFar, msg: activeBranchMessages[k] });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// The answer must never be filed as tool chrome.
|
||||
//
|
||||
// Live case this exists for: an agent wrote a 1,060-word decision memo, then saved a memory. That
|
||||
// one trailing tool call reclassified the memo as narration, so the card showed the user's question,
|
||||
// a grey "3 tool calls" row, and nothing else. 0 of 1,060 words in the DOM. The run was marked done
|
||||
// and charged $0.22. Expanding the row showed the memo as raw markdown in 12px grey, labelled with
|
||||
// the file the agent had read.
|
||||
//
|
||||
// Run: cd frontend && npx tsx --test src/app/pages/AgentChat/parsing/isNarration.test.ts
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { isNarration } from './isNarration.js';
|
||||
|
||||
test('short passing remarks are narration', () => {
|
||||
for (const s of [
|
||||
'Let me check that.',
|
||||
'Found the session log. Setting up the monitor now.',
|
||||
'Good question, let me look at what the workflow actually does rather than answer from memory.',
|
||||
'One moment.',
|
||||
]) {
|
||||
assert.equal(isNarration(s), true, `should absorb: ${s}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('a long answer is never narration', () => {
|
||||
const memo = 'The recommendation is to use a managed platform. '.repeat(12);
|
||||
assert.ok(memo.length > 240);
|
||||
assert.equal(isNarration(memo), false);
|
||||
});
|
||||
|
||||
test('structure means it is a deliverable, however short', () => {
|
||||
for (const s of [
|
||||
'# Decision Memo',
|
||||
'## Bottom line\nManaged, not self-hosted.',
|
||||
'- Docusaurus\n- Astro Starlight',
|
||||
'1. First\n2. Second',
|
||||
'> A quoted conclusion.',
|
||||
'```\nkubectl apply -f .\n```',
|
||||
'| Platform | Cost |\n| --- | --- |',
|
||||
]) {
|
||||
assert.equal(isNarration(s), false, `should stay visible: ${s.slice(0, 24)}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the real memo opening is not narration', () => {
|
||||
const opening = '# Decision Memo: Kubernetes Hosting for a 12-Person Company\n\n'
|
||||
+ '**Recommendation: use a managed platform. Do not self-host.**';
|
||||
assert.equal(isNarration(opening), false);
|
||||
});
|
||||
|
||||
test('a bare sentence mentioning a dash is still narration', () => {
|
||||
// The structure test anchors to line starts, so prose containing a hyphen must not trip it.
|
||||
assert.equal(isNarration('Checking the well-known ports first.'), true);
|
||||
});
|
||||
|
||||
test('empty and non-string content absorb rather than render an empty bubble', () => {
|
||||
assert.equal(isNarration(''), true);
|
||||
assert.equal(isNarration(' '), true);
|
||||
assert.equal(isNarration(null), true);
|
||||
assert.equal(isNarration(undefined), true);
|
||||
assert.equal(isNarration(42), true);
|
||||
});
|
||||
|
||||
test('the asymmetry is respected at the boundary', () => {
|
||||
// Bias is deliberate: a redundant line costs noise, a hidden answer costs the whole run.
|
||||
assert.equal(isNarration('x'.repeat(240)), true);
|
||||
assert.equal(isNarration('x'.repeat(241)), false);
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
// Is this assistant message throwaway narration, or is it the answer?
|
||||
//
|
||||
// The transcript folds narration that sits between two tool runs into the grey tool group, which is
|
||||
// right for "Let me check that" and catastrophic for a 1,060-word memo: the deliverable vanishes
|
||||
// from the card entirely, and expanding the group renders it as raw markdown source in tertiary
|
||||
// grey inside a row labelled with whatever file the agent happened to read. Observed live on a real
|
||||
// run, with the answer streaming in and then disappearing the instant the run completed.
|
||||
//
|
||||
// The two mistakes are not symmetric. Showing one redundant sentence inline costs a line of noise.
|
||||
// Hiding the answer costs the user the entire run, silently, while it is marked done and billed.
|
||||
// So this is deliberately biased toward "that is the answer, show it".
|
||||
|
||||
/** Past this, it is not a passing remark. Real narration in practice is a short sentence or two. */
|
||||
const NARRATION_MAX_CHARS = 240;
|
||||
|
||||
// Structure means someone is presenting, not muttering: a heading, a list, a table, a code fence, a
|
||||
// quote, or a horizontal rule. Any of these makes the message a deliverable regardless of length.
|
||||
const STRUCTURE = /(^|\n)\s*(#{1,6}\s|[-*+]\s|\d+\.\s|>\s|\||```|---)/;
|
||||
|
||||
export function isNarration(content: unknown): boolean {
|
||||
if (typeof content !== 'string') return true;
|
||||
const text = content.trim();
|
||||
if (text.length === 0) return true;
|
||||
if (text.length > NARRATION_MAX_CHARS) return false;
|
||||
return !STRUCTURE.test(text);
|
||||
}
|
||||
Reference in New Issue
Block a user