diff --git a/pkg/templates/typescript/anthropic-computer-use/index.ts b/pkg/templates/typescript/anthropic-computer-use/index.ts index f3c7e4d3..d501fbc8 100644 --- a/pkg/templates/typescript/anthropic-computer-use/index.ts +++ b/pkg/templates/typescript/anthropic-computer-use/index.ts @@ -2,6 +2,7 @@ import { Kernel, type KernelContext } from '@onkernel/sdk'; import { CuaAgent } from '@onkernel/cua-agent'; import type { AssistantMessage } from '@onkernel/cua-ai'; import { KernelBrowserSession } from './session'; +import { logAgentEvent } from './logging'; const kernel = new Kernel(); @@ -82,6 +83,7 @@ app.action( systemPrompt: SYSTEM_PROMPT, }, }); + agent.subscribe(logAgentEvent); await agent.prompt(payload.query); diff --git a/pkg/templates/typescript/anthropic-computer-use/logging.ts b/pkg/templates/typescript/anthropic-computer-use/logging.ts new file mode 100644 index 00000000..31f52dae --- /dev/null +++ b/pkg/templates/typescript/anthropic-computer-use/logging.ts @@ -0,0 +1,45 @@ +import type { AgentEvent } from '@onkernel/cua-agent'; + +// Logs the agent's narration (`agent>`) and each browser action (`→`) it takes. +// Pass to `agent.subscribe(logAgentEvent)`. +export function logAgentEvent(event: AgentEvent): void { + if (event.type === 'tool_execution_start') { + console.log(` → ${describe(event.toolName, event.args)}`); + } else if (event.type === 'tool_execution_end' && event.isError) { + console.log(` ✗ ${event.toolName} failed`); + } else if (event.type === 'message_end') { + const text = narration(event.message); + if (text) console.log(`\nagent> ${text}`); + } +} + +// Anthropic nests actions under `computer_batch`, OpenAI under +// `computer_use_extra`; unwrap those, then print the action name and its args. +function describe(name: string, args: any): string { + if (name === 'computer_batch' && Array.isArray(args?.actions)) { + return args.actions.map((a: any) => describe(a.type, a)).join('; '); + } + if (name === 'computer_use_extra' && typeof args?.action === 'string') { + return describe(args.action, args); + } + const params = Object.entries(args ?? {}) + .filter(([key]) => key !== 'type' && key !== 'action') + .map(([key, value]) => `${key}=${short(value)}`) + .join(' '); + return params ? `${name} ${params}` : name; +} + +function narration(message: any): string { + if (message?.role !== 'assistant') return ''; + return message.content + .filter((block: any) => block.type === 'text') + .map((block: any) => block.text) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); +} + +function short(value: unknown): string { + const text = typeof value === 'string' ? value : JSON.stringify(value); + return text.length > 80 ? `${text.slice(0, 77)}...` : text; +} diff --git a/pkg/templates/typescript/gemini-computer-use/index.ts b/pkg/templates/typescript/gemini-computer-use/index.ts index 9433d66c..a8a0f8c4 100644 --- a/pkg/templates/typescript/gemini-computer-use/index.ts +++ b/pkg/templates/typescript/gemini-computer-use/index.ts @@ -2,6 +2,7 @@ import { Kernel, type KernelContext } from '@onkernel/sdk'; import { CuaAgent } from '@onkernel/cua-agent'; import type { AssistantMessage } from '@onkernel/cua-ai'; import { KernelBrowserSession } from './session'; +import { logAgentEvent } from './logging'; const kernel = new Kernel(); @@ -67,6 +68,7 @@ The current date is ${currentDate}.`; systemPrompt, }, }); + agent.subscribe(logAgentEvent); await agent.prompt(payload.query); diff --git a/pkg/templates/typescript/gemini-computer-use/logging.ts b/pkg/templates/typescript/gemini-computer-use/logging.ts new file mode 100644 index 00000000..31f52dae --- /dev/null +++ b/pkg/templates/typescript/gemini-computer-use/logging.ts @@ -0,0 +1,45 @@ +import type { AgentEvent } from '@onkernel/cua-agent'; + +// Logs the agent's narration (`agent>`) and each browser action (`→`) it takes. +// Pass to `agent.subscribe(logAgentEvent)`. +export function logAgentEvent(event: AgentEvent): void { + if (event.type === 'tool_execution_start') { + console.log(` → ${describe(event.toolName, event.args)}`); + } else if (event.type === 'tool_execution_end' && event.isError) { + console.log(` ✗ ${event.toolName} failed`); + } else if (event.type === 'message_end') { + const text = narration(event.message); + if (text) console.log(`\nagent> ${text}`); + } +} + +// Anthropic nests actions under `computer_batch`, OpenAI under +// `computer_use_extra`; unwrap those, then print the action name and its args. +function describe(name: string, args: any): string { + if (name === 'computer_batch' && Array.isArray(args?.actions)) { + return args.actions.map((a: any) => describe(a.type, a)).join('; '); + } + if (name === 'computer_use_extra' && typeof args?.action === 'string') { + return describe(args.action, args); + } + const params = Object.entries(args ?? {}) + .filter(([key]) => key !== 'type' && key !== 'action') + .map(([key, value]) => `${key}=${short(value)}`) + .join(' '); + return params ? `${name} ${params}` : name; +} + +function narration(message: any): string { + if (message?.role !== 'assistant') return ''; + return message.content + .filter((block: any) => block.type === 'text') + .map((block: any) => block.text) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); +} + +function short(value: unknown): string { + const text = typeof value === 'string' ? value : JSON.stringify(value); + return text.length > 80 ? `${text.slice(0, 77)}...` : text; +} diff --git a/pkg/templates/typescript/openai-computer-use/index.ts b/pkg/templates/typescript/openai-computer-use/index.ts index d18e9537..04bdebc8 100644 --- a/pkg/templates/typescript/openai-computer-use/index.ts +++ b/pkg/templates/typescript/openai-computer-use/index.ts @@ -2,6 +2,7 @@ import { Kernel, type KernelContext } from '@onkernel/sdk'; import { CuaAgent } from '@onkernel/cua-agent'; import type { AssistantMessage } from '@onkernel/cua-ai'; import { maybeStartReplay, maybeStopReplay } from './lib/replay'; +import { logAgentEvent } from './lib/logging'; const kernel = new Kernel(); const app = kernel.app('ts-openai-cua'); @@ -52,6 +53,7 @@ app.action( systemPrompt: `You are operating a Chromium browser on a Kernel cloud VM. Use the navigation tool to open URLs directly, and review the screenshot after each action before continuing. The current date and time is ${new Date().toISOString()}.`, }, }); + agent.subscribe(logAgentEvent); await agent.prompt(payload.task); diff --git a/pkg/templates/typescript/openai-computer-use/lib/logging.ts b/pkg/templates/typescript/openai-computer-use/lib/logging.ts new file mode 100644 index 00000000..31f52dae --- /dev/null +++ b/pkg/templates/typescript/openai-computer-use/lib/logging.ts @@ -0,0 +1,45 @@ +import type { AgentEvent } from '@onkernel/cua-agent'; + +// Logs the agent's narration (`agent>`) and each browser action (`→`) it takes. +// Pass to `agent.subscribe(logAgentEvent)`. +export function logAgentEvent(event: AgentEvent): void { + if (event.type === 'tool_execution_start') { + console.log(` → ${describe(event.toolName, event.args)}`); + } else if (event.type === 'tool_execution_end' && event.isError) { + console.log(` ✗ ${event.toolName} failed`); + } else if (event.type === 'message_end') { + const text = narration(event.message); + if (text) console.log(`\nagent> ${text}`); + } +} + +// Anthropic nests actions under `computer_batch`, OpenAI under +// `computer_use_extra`; unwrap those, then print the action name and its args. +function describe(name: string, args: any): string { + if (name === 'computer_batch' && Array.isArray(args?.actions)) { + return args.actions.map((a: any) => describe(a.type, a)).join('; '); + } + if (name === 'computer_use_extra' && typeof args?.action === 'string') { + return describe(args.action, args); + } + const params = Object.entries(args ?? {}) + .filter(([key]) => key !== 'type' && key !== 'action') + .map(([key, value]) => `${key}=${short(value)}`) + .join(' '); + return params ? `${name} ${params}` : name; +} + +function narration(message: any): string { + if (message?.role !== 'assistant') return ''; + return message.content + .filter((block: any) => block.type === 'text') + .map((block: any) => block.text) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); +} + +function short(value: unknown): string { + const text = typeof value === 'string' ? value : JSON.stringify(value); + return text.length > 80 ? `${text.slice(0, 77)}...` : text; +}