Skip to content

Commit a489ac7

Browse files
committed
Simplify the action logger
Replace the per-tool formatters (point/dragPath/quote/keypress/etc.) with a generic key=value renderer, keeping only the two unwraps that materially help readability: Anthropic's computer_batch and OpenAI's computer_use_extra. Collapse narration whitespace so each agent> line stays on one line. ~99 → 45 lines, same clean output across all three providers.
1 parent a9bb98e commit a489ac7

3 files changed

Lines changed: 99 additions & 261 deletions

File tree

Lines changed: 33 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,45 @@
11
import type { AgentEvent } from '@onkernel/cua-agent';
22

3-
// Human-readable trace of what the agent is doing, meant for
4-
// `agent.subscribe(logAgentEvent)`: the model's narration between steps
5-
// (`agent>`), then each concrete browser action it takes (`→`). Failed
6-
// actions are marked so retries are visible.
3+
// Logs the agent's narration (`agent>`) and each browser action (`→`) it takes.
4+
// Pass to `agent.subscribe(logAgentEvent)`.
75
export function logAgentEvent(event: AgentEvent): void {
8-
switch (event.type) {
9-
case 'message_end': {
10-
const text = assistantText(event.message);
11-
if (text) console.log(`\nagent> ${text}`);
12-
return;
13-
}
14-
case 'tool_execution_start':
15-
console.log(` → ${formatAction(event.toolName, event.args)}`);
16-
return;
17-
case 'tool_execution_end':
18-
if (event.isError) {
19-
console.log(` ✗ ${event.toolName} failed`);
20-
}
21-
return;
6+
if (event.type === 'tool_execution_start') {
7+
console.log(` → ${describe(event.toolName, event.args)}`);
8+
} else if (event.type === 'tool_execution_end' && event.isError) {
9+
console.log(` ✗ ${event.toolName} failed`);
10+
} else if (event.type === 'message_end') {
11+
const text = narration(event.message);
12+
if (text) console.log(`\nagent> ${text}`);
2213
}
2314
}
2415

25-
// Concatenate the visible text blocks of an assistant message. Non-assistant
26-
// messages and tool-call-only turns have no narration and return ''.
27-
function assistantText(message: unknown): string {
28-
const m = message as { role?: string; content?: unknown };
29-
if (m.role !== 'assistant' || !Array.isArray(m.content)) return '';
30-
return m.content
31-
.filter((b): b is { type: 'text'; text: string } => isTextBlock(b))
32-
.map((b) => b.text.trim())
33-
.filter(Boolean)
34-
.join(' ');
35-
}
36-
37-
function isTextBlock(b: unknown): boolean {
38-
return typeof b === 'object' && b !== null && (b as { type?: string }).type === 'text';
39-
}
40-
41-
// Render one tool call as a short action line. Anthropic batches actions under
42-
// `computer_batch`; OpenAI/Gemini emit `goto`/`click`/... directly. Both flow
43-
// through here — batch sub-actions carry their kind in `type`.
44-
function formatAction(toolName: string, rawArgs: unknown): string {
45-
const a = (rawArgs ?? {}) as Record<string, any>;
46-
switch (toolName) {
47-
case 'computer_batch':
48-
return Array.isArray(a.actions)
49-
? a.actions.map((sub: any) => formatAction(sub?.type, sub)).join('; ')
50-
: 'batch';
51-
case 'computer_use_extra':
52-
// OpenAI's navigation helper wraps the real action under `action`.
53-
return typeof a.action === 'string' ? formatAction(a.action, a) : compact(a);
54-
case 'screenshot':
55-
return 'screenshot';
56-
case 'goto':
57-
return `goto ${a.url ?? ''}`.trim();
58-
case 'click':
59-
case 'left_click':
60-
case 'double_click':
61-
case 'right_click':
62-
return `${toolName} ${point(a)}`;
63-
case 'drag':
64-
return `drag ${dragPath(a.path)}`;
65-
case 'keypress':
66-
return `keypress ${Array.isArray(a.keys) ? a.keys.join('+') : compact(a)}`;
67-
case 'type':
68-
return `type ${quote(a.text)}`;
69-
case 'scroll':
70-
return `scroll ${point(a)}`.trim();
71-
case 'wait':
72-
return `wait ${a.ms ?? ''}ms`;
73-
default:
74-
return `${toolName ?? 'action'} ${compact(a)}`.trim();
16+
// Anthropic nests actions under `computer_batch`, OpenAI under
17+
// `computer_use_extra`; unwrap those, then print the action name and its args.
18+
function describe(name: string, args: any): string {
19+
if (name === 'computer_batch' && Array.isArray(args?.actions)) {
20+
return args.actions.map((a: any) => describe(a.type, a)).join('; ');
7521
}
22+
if (name === 'computer_use_extra' && typeof args?.action === 'string') {
23+
return describe(args.action, args);
24+
}
25+
const params = Object.entries(args ?? {})
26+
.filter(([key]) => key !== 'type' && key !== 'action')
27+
.map(([key, value]) => `${key}=${short(value)}`)
28+
.join(' ');
29+
return params ? `${name} ${params}` : name;
7630
}
7731

78-
function point(a: Record<string, any>): string {
79-
if (typeof a.x === 'number' && typeof a.y === 'number') return `(${a.x}, ${a.y})`;
80-
if (typeof a.x === 'number') return `(${a.x})`;
81-
return '';
82-
}
83-
84-
function dragPath(path: unknown): string {
85-
if (!Array.isArray(path) || path.length === 0) return '';
86-
const first = path[0];
87-
const last = path[path.length - 1];
88-
return `${point(first)}${point(last)}`;
89-
}
90-
91-
function quote(text: unknown): string {
92-
const s = typeof text === 'string' ? text : String(text ?? '');
93-
return `"${s.length > 80 ? `${s.slice(0, 77)}...` : s}"`;
32+
function narration(message: any): string {
33+
if (message?.role !== 'assistant') return '';
34+
return message.content
35+
.filter((block: any) => block.type === 'text')
36+
.map((block: any) => block.text)
37+
.join(' ')
38+
.replace(/\s+/g, ' ')
39+
.trim();
9440
}
9541

96-
function compact(value: unknown): string {
97-
const text = JSON.stringify(value) ?? '';
98-
return text === '{}' ? '' : text.length > 120 ? `${text.slice(0, 117)}...` : text;
42+
function short(value: unknown): string {
43+
const text = typeof value === 'string' ? value : JSON.stringify(value);
44+
return text.length > 80 ? `${text.slice(0, 77)}...` : text;
9945
}
Lines changed: 33 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,45 @@
11
import type { AgentEvent } from '@onkernel/cua-agent';
22

3-
// Human-readable trace of what the agent is doing, meant for
4-
// `agent.subscribe(logAgentEvent)`: the model's narration between steps
5-
// (`agent>`), then each concrete browser action it takes (`→`). Failed
6-
// actions are marked so retries are visible.
3+
// Logs the agent's narration (`agent>`) and each browser action (`→`) it takes.
4+
// Pass to `agent.subscribe(logAgentEvent)`.
75
export function logAgentEvent(event: AgentEvent): void {
8-
switch (event.type) {
9-
case 'message_end': {
10-
const text = assistantText(event.message);
11-
if (text) console.log(`\nagent> ${text}`);
12-
return;
13-
}
14-
case 'tool_execution_start':
15-
console.log(` → ${formatAction(event.toolName, event.args)}`);
16-
return;
17-
case 'tool_execution_end':
18-
if (event.isError) {
19-
console.log(` ✗ ${event.toolName} failed`);
20-
}
21-
return;
6+
if (event.type === 'tool_execution_start') {
7+
console.log(` → ${describe(event.toolName, event.args)}`);
8+
} else if (event.type === 'tool_execution_end' && event.isError) {
9+
console.log(` ✗ ${event.toolName} failed`);
10+
} else if (event.type === 'message_end') {
11+
const text = narration(event.message);
12+
if (text) console.log(`\nagent> ${text}`);
2213
}
2314
}
2415

25-
// Concatenate the visible text blocks of an assistant message. Non-assistant
26-
// messages and tool-call-only turns have no narration and return ''.
27-
function assistantText(message: unknown): string {
28-
const m = message as { role?: string; content?: unknown };
29-
if (m.role !== 'assistant' || !Array.isArray(m.content)) return '';
30-
return m.content
31-
.filter((b): b is { type: 'text'; text: string } => isTextBlock(b))
32-
.map((b) => b.text.trim())
33-
.filter(Boolean)
34-
.join(' ');
35-
}
36-
37-
function isTextBlock(b: unknown): boolean {
38-
return typeof b === 'object' && b !== null && (b as { type?: string }).type === 'text';
39-
}
40-
41-
// Render one tool call as a short action line. Anthropic batches actions under
42-
// `computer_batch`; OpenAI/Gemini emit `goto`/`click`/... directly. Both flow
43-
// through here — batch sub-actions carry their kind in `type`.
44-
function formatAction(toolName: string, rawArgs: unknown): string {
45-
const a = (rawArgs ?? {}) as Record<string, any>;
46-
switch (toolName) {
47-
case 'computer_batch':
48-
return Array.isArray(a.actions)
49-
? a.actions.map((sub: any) => formatAction(sub?.type, sub)).join('; ')
50-
: 'batch';
51-
case 'computer_use_extra':
52-
// OpenAI's navigation helper wraps the real action under `action`.
53-
return typeof a.action === 'string' ? formatAction(a.action, a) : compact(a);
54-
case 'screenshot':
55-
return 'screenshot';
56-
case 'goto':
57-
return `goto ${a.url ?? ''}`.trim();
58-
case 'click':
59-
case 'left_click':
60-
case 'double_click':
61-
case 'right_click':
62-
return `${toolName} ${point(a)}`;
63-
case 'drag':
64-
return `drag ${dragPath(a.path)}`;
65-
case 'keypress':
66-
return `keypress ${Array.isArray(a.keys) ? a.keys.join('+') : compact(a)}`;
67-
case 'type':
68-
return `type ${quote(a.text)}`;
69-
case 'scroll':
70-
return `scroll ${point(a)}`.trim();
71-
case 'wait':
72-
return `wait ${a.ms ?? ''}ms`;
73-
default:
74-
return `${toolName ?? 'action'} ${compact(a)}`.trim();
16+
// Anthropic nests actions under `computer_batch`, OpenAI under
17+
// `computer_use_extra`; unwrap those, then print the action name and its args.
18+
function describe(name: string, args: any): string {
19+
if (name === 'computer_batch' && Array.isArray(args?.actions)) {
20+
return args.actions.map((a: any) => describe(a.type, a)).join('; ');
7521
}
22+
if (name === 'computer_use_extra' && typeof args?.action === 'string') {
23+
return describe(args.action, args);
24+
}
25+
const params = Object.entries(args ?? {})
26+
.filter(([key]) => key !== 'type' && key !== 'action')
27+
.map(([key, value]) => `${key}=${short(value)}`)
28+
.join(' ');
29+
return params ? `${name} ${params}` : name;
7630
}
7731

78-
function point(a: Record<string, any>): string {
79-
if (typeof a.x === 'number' && typeof a.y === 'number') return `(${a.x}, ${a.y})`;
80-
if (typeof a.x === 'number') return `(${a.x})`;
81-
return '';
82-
}
83-
84-
function dragPath(path: unknown): string {
85-
if (!Array.isArray(path) || path.length === 0) return '';
86-
const first = path[0];
87-
const last = path[path.length - 1];
88-
return `${point(first)}${point(last)}`;
89-
}
90-
91-
function quote(text: unknown): string {
92-
const s = typeof text === 'string' ? text : String(text ?? '');
93-
return `"${s.length > 80 ? `${s.slice(0, 77)}...` : s}"`;
32+
function narration(message: any): string {
33+
if (message?.role !== 'assistant') return '';
34+
return message.content
35+
.filter((block: any) => block.type === 'text')
36+
.map((block: any) => block.text)
37+
.join(' ')
38+
.replace(/\s+/g, ' ')
39+
.trim();
9440
}
9541

96-
function compact(value: unknown): string {
97-
const text = JSON.stringify(value) ?? '';
98-
return text === '{}' ? '' : text.length > 120 ? `${text.slice(0, 117)}...` : text;
42+
function short(value: unknown): string {
43+
const text = typeof value === 'string' ? value : JSON.stringify(value);
44+
return text.length > 80 ? `${text.slice(0, 77)}...` : text;
9945
}
Lines changed: 33 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,45 @@
11
import type { AgentEvent } from '@onkernel/cua-agent';
22

3-
// Human-readable trace of what the agent is doing, meant for
4-
// `agent.subscribe(logAgentEvent)`: the model's narration between steps
5-
// (`agent>`), then each concrete browser action it takes (`→`). Failed
6-
// actions are marked so retries are visible.
3+
// Logs the agent's narration (`agent>`) and each browser action (`→`) it takes.
4+
// Pass to `agent.subscribe(logAgentEvent)`.
75
export function logAgentEvent(event: AgentEvent): void {
8-
switch (event.type) {
9-
case 'message_end': {
10-
const text = assistantText(event.message);
11-
if (text) console.log(`\nagent> ${text}`);
12-
return;
13-
}
14-
case 'tool_execution_start':
15-
console.log(` → ${formatAction(event.toolName, event.args)}`);
16-
return;
17-
case 'tool_execution_end':
18-
if (event.isError) {
19-
console.log(` ✗ ${event.toolName} failed`);
20-
}
21-
return;
6+
if (event.type === 'tool_execution_start') {
7+
console.log(` → ${describe(event.toolName, event.args)}`);
8+
} else if (event.type === 'tool_execution_end' && event.isError) {
9+
console.log(` ✗ ${event.toolName} failed`);
10+
} else if (event.type === 'message_end') {
11+
const text = narration(event.message);
12+
if (text) console.log(`\nagent> ${text}`);
2213
}
2314
}
2415

25-
// Concatenate the visible text blocks of an assistant message. Non-assistant
26-
// messages and tool-call-only turns have no narration and return ''.
27-
function assistantText(message: unknown): string {
28-
const m = message as { role?: string; content?: unknown };
29-
if (m.role !== 'assistant' || !Array.isArray(m.content)) return '';
30-
return m.content
31-
.filter((b): b is { type: 'text'; text: string } => isTextBlock(b))
32-
.map((b) => b.text.trim())
33-
.filter(Boolean)
34-
.join(' ');
35-
}
36-
37-
function isTextBlock(b: unknown): boolean {
38-
return typeof b === 'object' && b !== null && (b as { type?: string }).type === 'text';
39-
}
40-
41-
// Render one tool call as a short action line. Anthropic batches actions under
42-
// `computer_batch`; OpenAI/Gemini emit `goto`/`click`/... directly. Both flow
43-
// through here — batch sub-actions carry their kind in `type`.
44-
function formatAction(toolName: string, rawArgs: unknown): string {
45-
const a = (rawArgs ?? {}) as Record<string, any>;
46-
switch (toolName) {
47-
case 'computer_batch':
48-
return Array.isArray(a.actions)
49-
? a.actions.map((sub: any) => formatAction(sub?.type, sub)).join('; ')
50-
: 'batch';
51-
case 'computer_use_extra':
52-
// OpenAI's navigation helper wraps the real action under `action`.
53-
return typeof a.action === 'string' ? formatAction(a.action, a) : compact(a);
54-
case 'screenshot':
55-
return 'screenshot';
56-
case 'goto':
57-
return `goto ${a.url ?? ''}`.trim();
58-
case 'click':
59-
case 'left_click':
60-
case 'double_click':
61-
case 'right_click':
62-
return `${toolName} ${point(a)}`;
63-
case 'drag':
64-
return `drag ${dragPath(a.path)}`;
65-
case 'keypress':
66-
return `keypress ${Array.isArray(a.keys) ? a.keys.join('+') : compact(a)}`;
67-
case 'type':
68-
return `type ${quote(a.text)}`;
69-
case 'scroll':
70-
return `scroll ${point(a)}`.trim();
71-
case 'wait':
72-
return `wait ${a.ms ?? ''}ms`;
73-
default:
74-
return `${toolName ?? 'action'} ${compact(a)}`.trim();
16+
// Anthropic nests actions under `computer_batch`, OpenAI under
17+
// `computer_use_extra`; unwrap those, then print the action name and its args.
18+
function describe(name: string, args: any): string {
19+
if (name === 'computer_batch' && Array.isArray(args?.actions)) {
20+
return args.actions.map((a: any) => describe(a.type, a)).join('; ');
7521
}
22+
if (name === 'computer_use_extra' && typeof args?.action === 'string') {
23+
return describe(args.action, args);
24+
}
25+
const params = Object.entries(args ?? {})
26+
.filter(([key]) => key !== 'type' && key !== 'action')
27+
.map(([key, value]) => `${key}=${short(value)}`)
28+
.join(' ');
29+
return params ? `${name} ${params}` : name;
7630
}
7731

78-
function point(a: Record<string, any>): string {
79-
if (typeof a.x === 'number' && typeof a.y === 'number') return `(${a.x}, ${a.y})`;
80-
if (typeof a.x === 'number') return `(${a.x})`;
81-
return '';
82-
}
83-
84-
function dragPath(path: unknown): string {
85-
if (!Array.isArray(path) || path.length === 0) return '';
86-
const first = path[0];
87-
const last = path[path.length - 1];
88-
return `${point(first)}${point(last)}`;
89-
}
90-
91-
function quote(text: unknown): string {
92-
const s = typeof text === 'string' ? text : String(text ?? '');
93-
return `"${s.length > 80 ? `${s.slice(0, 77)}...` : s}"`;
32+
function narration(message: any): string {
33+
if (message?.role !== 'assistant') return '';
34+
return message.content
35+
.filter((block: any) => block.type === 'text')
36+
.map((block: any) => block.text)
37+
.join(' ')
38+
.replace(/\s+/g, ' ')
39+
.trim();
9440
}
9541

96-
function compact(value: unknown): string {
97-
const text = JSON.stringify(value) ?? '';
98-
return text === '{}' ? '' : text.length > 120 ? `${text.slice(0, 117)}...` : text;
42+
function short(value: unknown): string {
43+
const text = typeof value === 'string' ? value : JSON.stringify(value);
44+
return text.length > 80 ? `${text.slice(0, 77)}...` : text;
9945
}

0 commit comments

Comments
 (0)