mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 08:03:39 +00:00
Add session export to HTML, improve tool error handling, and enhance RPC mode documentation
This commit is contained in:
parent
68092ccf01
commit
9e3e319f1a
9 changed files with 638 additions and 63 deletions
|
|
@ -166,9 +166,6 @@ export class Agent {
|
|||
this._state.streamMessage = null;
|
||||
this._state.error = undefined;
|
||||
|
||||
// Emit agent_start
|
||||
this.emit({ type: "agent_start" });
|
||||
|
||||
const reasoning =
|
||||
this._state.thinkingLevel === "off"
|
||||
? undefined
|
||||
|
|
@ -291,9 +288,6 @@ export class Agent {
|
|||
this._state.streamMessage = null;
|
||||
this._state.pendingToolCalls = new Set<string>();
|
||||
this.abortController = undefined;
|
||||
|
||||
// Emit agent_end with all generated messages
|
||||
this.emit({ type: "agent_end", messages: generatedMessages });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,9 +18,6 @@ export ANTHROPIC_API_KEY=sk-ant-...
|
|||
|
||||
# Start the interactive CLI
|
||||
pi
|
||||
|
||||
# Or use the full command name
|
||||
coding-agent
|
||||
```
|
||||
|
||||
Once in the CLI, you can chat with the AI:
|
||||
|
|
@ -29,7 +26,7 @@ Once in the CLI, you can chat with the AI:
|
|||
You: Create a simple Express server in src/server.ts
|
||||
```
|
||||
|
||||
The agent will use its tools to read, write, and edit files as needed.
|
||||
The agent will use its tools to read, write, and edit files as needed, and execute commands via Bash.
|
||||
|
||||
## API Keys
|
||||
|
||||
|
|
@ -88,6 +85,51 @@ Export the current session to a self-contained HTML file:
|
|||
|
||||
The HTML file includes the full conversation with syntax highlighting and is viewable in any browser.
|
||||
|
||||
## Editor Features
|
||||
|
||||
The interactive input editor includes several productivity features:
|
||||
|
||||
### Path Completion
|
||||
|
||||
Press **Tab** to autocomplete file and directory paths:
|
||||
- Works with relative paths: `./src/` + Tab → complete files in src/
|
||||
- Works with parent directories: `../../` + Tab → navigate up and complete
|
||||
- Works with home directory: `~/Des` + Tab → `~/Desktop/`
|
||||
- Use **Up/Down arrows** to navigate completion suggestions
|
||||
- Press **Enter** to select a completion
|
||||
- Shows matching files and directories as you type
|
||||
|
||||
### File Drag & Drop
|
||||
|
||||
Drag files from your OS file explorer (Finder on macOS, Explorer on Windows) directly onto the terminal. The file path will be automatically inserted into the editor. Works great with screenshots from macOS screenshot tool.
|
||||
|
||||
### Multi-line Paste
|
||||
|
||||
Paste multiple lines of text (e.g., code snippets, logs) and they'll be automatically coalesced into a compact `[paste #123 <N> lines]` reference in the editor. The full content is still sent to the model.
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
- **Ctrl+K**: Delete current line
|
||||
- **Ctrl+C**: Clear editor (first press) / Exit pi (second press)
|
||||
- **Tab**: Path completion
|
||||
- **Enter**: Send message
|
||||
- **Shift+Enter**: Insert new line (multi-line input)
|
||||
- **Arrow keys**: Move cursor
|
||||
- **Ctrl+A** / **Home** / **Cmd+Left** (macOS): Jump to start of line
|
||||
- **Ctrl+E** / **End** / **Cmd+Right** (macOS): Jump to end of line
|
||||
|
||||
## Project Context Files
|
||||
|
||||
Place an `AGENT.md` or `CLAUDE.md` file in your project root to provide context to the AI. The contents will be automatically included at the start of new sessions (not when continuing/resuming sessions).
|
||||
|
||||
This is useful for:
|
||||
- Project-specific instructions and guidelines
|
||||
- Architecture documentation
|
||||
- Coding conventions and style guides
|
||||
- Dependencies and setup information
|
||||
|
||||
The file is injected as a user message at the beginning of each new session, ensuring the AI has project context without modifying the system prompt.
|
||||
|
||||
## Image Support
|
||||
|
||||
Send images to vision-capable models by providing file paths:
|
||||
|
|
@ -96,12 +138,9 @@ Send images to vision-capable models by providing file paths:
|
|||
You: What is in this screenshot? /path/to/image.png
|
||||
```
|
||||
|
||||
Supported formats: `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.bmp`, `.svg`
|
||||
Supported formats: `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`
|
||||
|
||||
The image will be automatically encoded and sent with your message. Vision-capable models include:
|
||||
- GPT-4o, GPT-4o-mini (OpenAI)
|
||||
- Claude 3.5 Sonnet, Claude 3.5 Haiku (Anthropic)
|
||||
- Gemini 2.5 Flash, Gemini 2.5 Pro (Google)
|
||||
The image will be automatically encoded and sent with your message. JPEG and PNG are supported across all vision models. Other formats may only be supported by some models.
|
||||
|
||||
## Available Tools
|
||||
|
||||
|
|
@ -109,7 +148,7 @@ The agent has access to four core tools for working with your codebase:
|
|||
|
||||
### read
|
||||
|
||||
Read file contents. Supports text files and images (jpg, png, gif, webp, bmp, svg). Images are sent as attachments. For text files, defaults to first 2000 lines. Use offset/limit parameters for large files. Lines longer than 2000 characters are truncated.
|
||||
Read file contents. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, defaults to first 2000 lines. Use offset/limit parameters for large files. Lines longer than 2000 characters are truncated.
|
||||
|
||||
### write
|
||||
|
||||
|
|
@ -151,6 +190,18 @@ This opens an interactive session selector where you can:
|
|||
|
||||
Sessions include all conversation messages, tool calls and results, model switches, and thinking level changes.
|
||||
|
||||
To run without saving a session (ephemeral mode):
|
||||
|
||||
```bash
|
||||
pi --no-session
|
||||
```
|
||||
|
||||
To use a specific session file instead of auto-generating one:
|
||||
|
||||
```bash
|
||||
pi --session /path/to/my-session.jsonl
|
||||
```
|
||||
|
||||
## CLI Options
|
||||
|
||||
```bash
|
||||
|
|
@ -159,25 +210,37 @@ pi [options] [messages...]
|
|||
|
||||
### Options
|
||||
|
||||
**--provider <name>**
|
||||
**--provider <name>**
|
||||
Provider name. Available: `anthropic`, `openai`, `google`, `xai`, `groq`, `cerebras`, `openrouter`, `zai`. Default: `anthropic`
|
||||
|
||||
**--model <id>**
|
||||
**--model <id>**
|
||||
Model ID. Default: `claude-sonnet-4-5`
|
||||
|
||||
**--api-key <key>**
|
||||
**--api-key <key>**
|
||||
API key (overrides environment variables)
|
||||
|
||||
**--system-prompt <text>**
|
||||
**--system-prompt <text>**
|
||||
Custom system prompt (overrides default coding assistant prompt)
|
||||
|
||||
**--continue, -c**
|
||||
**--mode <mode>**
|
||||
Output mode for non-interactive usage. Options:
|
||||
- `text` (default): Output only the final assistant message text
|
||||
- `json`: Stream all agent events as JSON (one event per line). Events are emitted by `@mariozechner/pi-agent` and include message updates, tool executions, and completions
|
||||
- `rpc`: JSON mode plus stdin listener for headless operation. Send JSON commands on stdin: `{"type":"prompt","message":"..."}` or `{"type":"abort"}`. See [test/rpc-example.ts](test/rpc-example.ts) for a complete example
|
||||
|
||||
**--no-session**
|
||||
Don't save session (ephemeral mode)
|
||||
|
||||
**--session <path>**
|
||||
Use specific session file path instead of auto-generating one
|
||||
|
||||
**--continue, -c**
|
||||
Continue the most recent session
|
||||
|
||||
**--resume, -r**
|
||||
**--resume, -r**
|
||||
Select a session to resume (opens interactive selector)
|
||||
|
||||
**--help, -h**
|
||||
**--help, -h**
|
||||
Show help message
|
||||
|
||||
### Examples
|
||||
|
|
@ -186,9 +249,18 @@ Show help message
|
|||
# Start interactive mode
|
||||
pi
|
||||
|
||||
# Single message mode
|
||||
# Single message mode (text output)
|
||||
pi "List all .ts files in src/"
|
||||
|
||||
# JSON mode - stream all agent events
|
||||
pi --mode json "List all .ts files in src/"
|
||||
|
||||
# RPC mode - headless operation (see test/rpc-example.ts)
|
||||
pi --mode rpc --no-session
|
||||
# Then send JSON on stdin:
|
||||
# {"type":"prompt","message":"List all .ts files"}
|
||||
# {"type":"abort"}
|
||||
|
||||
# Continue previous session
|
||||
pi -c "What did we discuss?"
|
||||
|
||||
|
|
|
|||
345
packages/coding-agent/out.html
Normal file
345
packages/coding-agent/out.html
Normal file
File diff suppressed because one or more lines are too long
29
packages/coding-agent/poem.txt
Normal file
29
packages/coding-agent/poem.txt
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
Through morning mist the sunlight breaks,
|
||||
A golden path on silver lakes,
|
||||
The world awakens, soft and new,
|
||||
With diamond drops of morning dew,
|
||||
And birds sing songs that daylight makes.
|
||||
|
||||
The forest stands in ancient green,
|
||||
Where shadows dance and light is seen,
|
||||
Through branches high the breezes play,
|
||||
And lead the wandering heart away,
|
||||
To places only dreams have been.
|
||||
|
||||
The river flows with endless grace,
|
||||
Its waters mirror time and space,
|
||||
It carries stories, old and worn,
|
||||
Of every soul that's ever born,
|
||||
And every smile on every face.
|
||||
|
||||
When evening falls with purple hue,
|
||||
The stars emerge in cosmic view,
|
||||
They whisper secrets from afar,
|
||||
Each distant, shimmering, silent star,
|
||||
A reminder what we thought we knew.
|
||||
|
||||
So let us walk this earthly ground,
|
||||
Where beauty, pain, and love are found,
|
||||
Embrace the journey, come what may,
|
||||
For every night will yield to day,
|
||||
And silence always turns to sound.
|
||||
|
|
@ -2,7 +2,7 @@ import { Agent, ProviderTransport, type ThinkingLevel } from "@mariozechner/pi-a
|
|||
import { getModel, type KnownProvider } from "@mariozechner/pi-ai";
|
||||
import { ProcessTerminal, TUI } from "@mariozechner/pi-tui";
|
||||
import chalk from "chalk";
|
||||
import { readFileSync } from "fs";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { SessionManager } from "./session-manager.js";
|
||||
|
|
@ -38,6 +38,8 @@ interface Args {
|
|||
resume?: boolean;
|
||||
help?: boolean;
|
||||
mode?: Mode;
|
||||
noSession?: boolean;
|
||||
session?: string;
|
||||
messages: string[];
|
||||
}
|
||||
|
||||
|
|
@ -68,6 +70,10 @@ function parseArgs(args: string[]): Args {
|
|||
result.apiKey = args[++i];
|
||||
} else if (arg === "--system-prompt" && i + 1 < args.length) {
|
||||
result.systemPrompt = args[++i];
|
||||
} else if (arg === "--no-session") {
|
||||
result.noSession = true;
|
||||
} else if (arg === "--session" && i + 1 < args.length) {
|
||||
result.session = args[++i];
|
||||
} else if (!arg.startsWith("-")) {
|
||||
result.messages.push(arg);
|
||||
}
|
||||
|
|
@ -90,6 +96,8 @@ ${chalk.bold("Options:")}
|
|||
--mode <mode> Output mode: text (default), json, or rpc
|
||||
--continue, -c Continue previous session
|
||||
--resume, -r Select a session to resume
|
||||
--session <path> Use specific session file
|
||||
--no-session Don't save session (ephemeral)
|
||||
--help, -h Show this help
|
||||
|
||||
${chalk.bold("Examples:")}
|
||||
|
|
@ -140,6 +148,23 @@ Guidelines:
|
|||
|
||||
Current directory: ${process.cwd()}`;
|
||||
|
||||
/**
|
||||
* Look for AGENT.md or CLAUDE.md in the current directory and return its contents
|
||||
*/
|
||||
function loadProjectContext(): string | null {
|
||||
const candidates = ["AGENT.md", "CLAUDE.md"];
|
||||
for (const filename of candidates) {
|
||||
if (existsSync(filename)) {
|
||||
try {
|
||||
return readFileSync(filename, "utf-8");
|
||||
} catch (error) {
|
||||
console.error(chalk.yellow(`Warning: Could not read ${filename}: ${error}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function selectSession(sessionManager: SessionManager): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
const ui = new TUI(new ProcessTerminal());
|
||||
|
|
@ -241,7 +266,7 @@ async function runRpcMode(agent: Agent, _sessionManager: SessionManager): Promis
|
|||
});
|
||||
|
||||
// Listen for JSON input on stdin
|
||||
const readline = require("readline");
|
||||
const readline = await import("readline");
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
|
|
@ -277,7 +302,12 @@ export async function main(args: string[]) {
|
|||
}
|
||||
|
||||
// Setup session manager
|
||||
const sessionManager = new SessionManager(parsed.continue && !parsed.resume);
|
||||
const sessionManager = new SessionManager(parsed.continue && !parsed.resume, parsed.session);
|
||||
|
||||
// Disable session saving if --no-session flag is set
|
||||
if (parsed.noSession) {
|
||||
sessionManager.disable();
|
||||
}
|
||||
|
||||
// Handle --resume flag: show session selector
|
||||
if (parsed.resume) {
|
||||
|
|
@ -398,6 +428,27 @@ export async function main(args: string[]) {
|
|||
// Start session
|
||||
sessionManager.startSession(agent.state);
|
||||
|
||||
// Inject project context (AGENT.md/CLAUDE.md) if not continuing/resuming
|
||||
if (!parsed.continue && !parsed.resume) {
|
||||
const projectContext = loadProjectContext();
|
||||
if (projectContext) {
|
||||
// Queue the context as a message that will be injected at the start
|
||||
await agent.queueMessage({
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `[Project Context from ${existsSync("AGENT.md") ? "AGENT.md" : "CLAUDE.md"}]\n\n${projectContext}`,
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
if (shouldPrintMessages) {
|
||||
console.log(chalk.dim(`Loaded project context from ${existsSync("AGENT.md") ? "AGENT.md" : "CLAUDE.md"}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to agent events to save messages and log events
|
||||
agent.subscribe((event) => {
|
||||
// Save messages on completion
|
||||
|
|
@ -412,15 +463,14 @@ export async function main(args: string[]) {
|
|||
});
|
||||
|
||||
// Route to appropriate mode
|
||||
if (isInteractive) {
|
||||
// No mode flag in interactive - always use TUI
|
||||
if (mode === "rpc") {
|
||||
// RPC mode - headless operation
|
||||
await runRpcMode(agent, sessionManager);
|
||||
} else if (isInteractive) {
|
||||
// No messages and not RPC - use TUI
|
||||
await runInteractiveMode(agent, sessionManager, VERSION);
|
||||
} else {
|
||||
// CLI mode with messages
|
||||
if (mode === "rpc") {
|
||||
await runRpcMode(agent, sessionManager);
|
||||
} else {
|
||||
await runSingleShotMode(agent, sessionManager, parsed.messages, mode);
|
||||
}
|
||||
await runSingleShotMode(agent, sessionManager, parsed.messages, mode);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ export interface SessionHeader {
|
|||
id: string;
|
||||
timestamp: string;
|
||||
cwd: string;
|
||||
systemPrompt: string;
|
||||
model: string;
|
||||
thinkingLevel: string;
|
||||
}
|
||||
|
|
@ -50,11 +49,16 @@ export class SessionManager {
|
|||
private sessionId!: string;
|
||||
private sessionFile!: string;
|
||||
private sessionDir: string;
|
||||
private enabled: boolean = true;
|
||||
|
||||
constructor(continueSession: boolean = false) {
|
||||
constructor(continueSession: boolean = false, customSessionPath?: string) {
|
||||
this.sessionDir = this.getSessionDirectory();
|
||||
|
||||
if (continueSession) {
|
||||
if (customSessionPath) {
|
||||
// Use custom session file path
|
||||
this.sessionFile = resolve(customSessionPath);
|
||||
this.loadSessionId();
|
||||
} else if (continueSession) {
|
||||
const mostRecent = this.findMostRecentlyModifiedSession();
|
||||
if (mostRecent) {
|
||||
this.sessionFile = mostRecent;
|
||||
|
|
@ -67,6 +71,11 @@ export class SessionManager {
|
|||
}
|
||||
}
|
||||
|
||||
/** Disable session saving (for --no-session mode) */
|
||||
disable() {
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
private getSessionDirectory(): string {
|
||||
const cwd = process.cwd();
|
||||
const safePath = "--" + cwd.replace(/^\//, "").replace(/\//g, "-") + "--";
|
||||
|
|
@ -121,12 +130,12 @@ export class SessionManager {
|
|||
}
|
||||
|
||||
startSession(state: AgentState): void {
|
||||
if (!this.enabled) return;
|
||||
const entry: SessionHeader = {
|
||||
type: "session",
|
||||
id: this.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
cwd: process.cwd(),
|
||||
systemPrompt: state.systemPrompt,
|
||||
model: `${state.model.provider}/${state.model.id}`,
|
||||
thinkingLevel: state.thinkingLevel,
|
||||
};
|
||||
|
|
@ -134,6 +143,7 @@ export class SessionManager {
|
|||
}
|
||||
|
||||
saveMessage(message: any): void {
|
||||
if (!this.enabled) return;
|
||||
const entry: SessionMessageEntry = {
|
||||
type: "message",
|
||||
timestamp: new Date().toISOString(),
|
||||
|
|
@ -143,6 +153,7 @@ export class SessionManager {
|
|||
}
|
||||
|
||||
saveEvent(event: AgentEvent): void {
|
||||
if (!this.enabled) return;
|
||||
const entry: SessionEventEntry = {
|
||||
type: "event",
|
||||
timestamp: new Date().toISOString(),
|
||||
|
|
@ -152,6 +163,7 @@ export class SessionManager {
|
|||
}
|
||||
|
||||
saveThinkingLevelChange(thinkingLevel: string): void {
|
||||
if (!this.enabled) return;
|
||||
const entry: ThinkingLevelChangeEntry = {
|
||||
type: "thinking_level_change",
|
||||
timestamp: new Date().toISOString(),
|
||||
|
|
@ -161,6 +173,7 @@ export class SessionManager {
|
|||
}
|
||||
|
||||
saveModelChange(model: string): void {
|
||||
if (!this.enabled) return;
|
||||
const entry: ModelChangeEntry = {
|
||||
type: "model_change",
|
||||
timestamp: new Date().toISOString(),
|
||||
|
|
|
|||
|
|
@ -163,10 +163,7 @@ export const editTool: AgentTool<typeof editSchema> = {
|
|||
if (signal) {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
}
|
||||
resolve({
|
||||
content: [{ type: "text", text: `Error: File not found: ${path}` }],
|
||||
details: undefined,
|
||||
});
|
||||
reject(new Error(`File not found: ${path}`));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -188,15 +185,11 @@ export const editTool: AgentTool<typeof editSchema> = {
|
|||
if (signal) {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
}
|
||||
resolve({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,
|
||||
},
|
||||
],
|
||||
details: undefined,
|
||||
});
|
||||
reject(
|
||||
new Error(
|
||||
`Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -207,15 +200,11 @@ export const editTool: AgentTool<typeof editSchema> = {
|
|||
if (signal) {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
}
|
||||
resolve({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,
|
||||
},
|
||||
],
|
||||
details: undefined,
|
||||
});
|
||||
reject(
|
||||
new Error(
|
||||
`Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,6 @@ const IMAGE_MIME_TYPES: Record<string, string> = {
|
|||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".bmp": "image/bmp",
|
||||
".svg": "image/svg+xml",
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -52,7 +50,7 @@ export const readTool: AgentTool<typeof readSchema> = {
|
|||
name: "read",
|
||||
label: "read",
|
||||
description:
|
||||
"Read the contents of a file. Supports text files and images (jpg, png, gif, webp, bmp, svg). Images are sent as attachments. For text files, defaults to first 2000 lines. Use offset/limit for large files.",
|
||||
"Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, defaults to first 2000 lines. Use offset/limit for large files.",
|
||||
parameters: readSchema,
|
||||
execute: async (
|
||||
_toolCallId: string,
|
||||
|
|
|
|||
85
packages/coding-agent/test/rpc-example.ts
Normal file
85
packages/coding-agent/test/rpc-example.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { spawn } from "node:child_process";
|
||||
import { dirname, join } from "path";
|
||||
import * as readline from "readline";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
/**
|
||||
* Interactive example of using coding-agent in RPC mode
|
||||
* Usage: npx tsx test/rpc-example.ts
|
||||
*/
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Spawn agent in RPC mode
|
||||
const agent = spawn("node", ["dist/cli.js", "--mode", "rpc", "--no-session"], {
|
||||
cwd: join(__dirname, ".."),
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
let isWaiting = false;
|
||||
|
||||
// Parse agent events
|
||||
readline.createInterface({ input: agent.stdout, terminal: false }).on("line", (line: string) => {
|
||||
try {
|
||||
const event = JSON.parse(line);
|
||||
|
||||
if (event.type === "agent_start") isWaiting = true;
|
||||
|
||||
if (event.type === "message_update") {
|
||||
const { assistantMessageEvent } = event;
|
||||
if (assistantMessageEvent.type === "text_delta" || assistantMessageEvent.type === "thinking_delta") {
|
||||
process.stdout.write(assistantMessageEvent.delta);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.type === "tool_execution_start") {
|
||||
console.log(`\n[Tool: ${event.toolName}]`);
|
||||
}
|
||||
|
||||
if (event.type === "tool_execution_end") {
|
||||
console.log(`[Result: ${JSON.stringify(event.result, null, 2)}]\n`);
|
||||
}
|
||||
|
||||
if (event.type === "agent_end") {
|
||||
console.log("\n");
|
||||
isWaiting = false;
|
||||
process.stdout.write("You: ");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Parse error:", line);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle user input
|
||||
const stdinReader = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
terminal: true,
|
||||
});
|
||||
|
||||
stdinReader.on("line", (line: string) => {
|
||||
if (isWaiting) return;
|
||||
isWaiting = true;
|
||||
agent.stdin.write(JSON.stringify({ type: "prompt", message: line }) + "\n");
|
||||
});
|
||||
|
||||
// Capture readline's SIGINT and handle it ourselves
|
||||
stdinReader.on("SIGINT", () => {
|
||||
process.emit("SIGINT", "SIGINT");
|
||||
});
|
||||
|
||||
// Handle Ctrl+C
|
||||
process.on("SIGINT", () => {
|
||||
if (isWaiting) {
|
||||
console.log("\n[Aborting...]");
|
||||
agent.stdin.write(JSON.stringify({ type: "abort" }) + "\n");
|
||||
} else {
|
||||
agent.kill();
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
agent.stderr.on("data", (data) => console.error("Error:", data.toString()));
|
||||
|
||||
console.log("Interactive RPC mode example. Type 'exit' to quit.\n");
|
||||
process.stdout.write("You: ");
|
||||
Loading…
Add table
Add a link
Reference in a new issue