fix: handle ANSI escapes and uint8 utf-8 writes

old-agentic-v1^2
Philipp Burckhardt 2023-06-29 22:14:08 -04:00
rodzic c4ecea1ff8
commit ac5eba853a
2 zmienionych plików z 57 dodań i 27 usunięć

Wyświetl plik

@ -10,7 +10,10 @@ import { SYMBOLS } from './symbols'
const MAGIC_STRING = '__INSIDE_TRACKER__' // Define a unique "magic" string const MAGIC_STRING = '__INSIDE_TRACKER__' // Define a unique "magic" string
const SPINNER_INTERVAL = 1000 // eslint-disable-next-line no-control-regex
const RE_ANSI_ESCAPES = /\x1b\[[0-9;]*[A-Za-z]/ // cursor movement, screen clearing, etc.
const SPINNER_INTERVAL = 100 // 100ms
const INACTIVITY_THRESHOLD = 2000 // 2 seconds const INACTIVITY_THRESHOLD = 2000 // 2 seconds
function getSpinnerSymbol() { function getSpinnerSymbol() {
@ -19,6 +22,9 @@ function getSpinnerSymbol() {
] ]
} }
const originalStdoutWrite = process.stdout.write
const originalStderrWrite = process.stderr.write
export class TerminalTaskTracker { export class TerminalTaskTracker {
protected events: Record<string, any[]> = { root: [] } protected events: Record<string, any[]> = { root: [] }
protected interval: NodeJS.Timeout | null = null protected interval: NodeJS.Timeout | null = null
@ -26,11 +32,10 @@ export class TerminalTaskTracker {
protected truncateOutput = false protected truncateOutput = false
protected renderTasks = true protected renderTasks = true
protected outputs: Array<string | Uint8Array> = [] protected outputs: Array<string | Uint8Array> = []
protected renderingPaused = false
private stdoutBuffer: any[] = [] private stdoutBuffer: string[] = []
private stderrBuffer: any[] = [] private stderrBuffer: string[] = []
private originalStdoutWrite = process.stdout.write
private originalStderrWrite = process.stderr.write
constructor() { constructor() {
if (!process.stderr.isTTY) { if (!process.stderr.isTTY) {
@ -38,33 +43,40 @@ export class TerminalTaskTracker {
return return
} }
process.stdout.write = (str: any) => { process.stdout.write = (buffer: string | Uint8Array) => {
this.stdoutBuffer.push(str) if (buffer instanceof Uint8Array) {
return this.originalStdoutWrite.call(process.stdout, str) buffer = Buffer.from(buffer).toString('utf-8')
}
this.stdoutBuffer.push(buffer)
return originalStdoutWrite.call(process.stdout, buffer)
} }
process.stderr.write = (str: any) => { process.stderr.write = (buffer: string | Uint8Array) => {
if (str.startsWith(MAGIC_STRING)) { if (buffer instanceof Uint8Array) {
buffer = Buffer.from(buffer).toString('utf-8')
}
if (typeof buffer === 'string' && buffer.startsWith(MAGIC_STRING)) {
// This write is from inside the tracker, remove the magic string and write to stderr: // This write is from inside the tracker, remove the magic string and write to stderr:
return this.originalStderrWrite.call( return originalStderrWrite.call(
process.stderr, process.stderr,
str.replace(MAGIC_STRING, '') buffer.replace(MAGIC_STRING, '')
) )
} else { } else {
// This write is from outside the tracker, add it to stderrBuffer and write to stderr: if (!RE_ANSI_ESCAPES.test(buffer)) {
this.stderrBuffer.push(str) // If an ANSI escape sequence is written to stderr, it will mess up the output, so we need to write it to stdout instead:
return this.originalStderrWrite.call(process.stderr, str) // This write is from outside the tracker, add it to stderrBuffer and write to stderr:
this.stderrBuffer.push(buffer)
}
return originalStderrWrite.call(process.stderr, buffer)
} }
} }
this.start() this.start()
} }
renderOutput() {
const output = this.outputs.join('')
process.stderr.write(output)
}
handleKeyPress = (str, key) => { handleKeyPress = (str, key) => {
if (key.ctrl && key.name === 'c') { if (key.ctrl && key.name === 'c') {
process.exit() process.exit()
@ -107,6 +119,10 @@ export class TerminalTaskTracker {
// Remove the keypress listener: // Remove the keypress listener:
process.stdin.off('keypress', this.handleKeyPress) process.stdin.off('keypress', this.handleKeyPress)
// Restore the original `process.stdout.write()` and `process.stderr.write()` functions:
process.stdout.write = originalStdoutWrite
process.stderr.write = originalStderrWrite
const finalLines = [ const finalLines = [
'', '',
'', '',
@ -123,16 +139,22 @@ export class TerminalTaskTracker {
'', '',
'' ''
] ]
this.writeWithMagicString(finalLines)
// Restore the original `process.stdout.write()` and `process.stderr.write()` functions: process.stderr.write(finalLines.join('\n'))
process.stdout.write = this.originalStdoutWrite
process.stderr.write = this.originalStderrWrite
// Pause the reading of stdin so that the Node.js process will exit once done: // Pause the reading of stdin so that the Node.js process will exit once done:
process.stdin.pause() process.stdin.pause()
} }
pause() {
this.renderingPaused = true
}
resume() {
this.renderingPaused = false
this.render()
}
stringify(value: any) { stringify(value: any) {
if (this.truncateOutput) { if (this.truncateOutput) {
const json = JSON.stringify(value) const json = JSON.stringify(value)
@ -271,6 +293,7 @@ export class TerminalTaskTracker {
private writeWithMagicString(content: string | string[]) { private writeWithMagicString(content: string | string[]) {
let output let output
if (Array.isArray(content)) { if (Array.isArray(content)) {
if (content.length === 0) { if (content.length === 0) {
return return
@ -285,6 +308,10 @@ export class TerminalTaskTracker {
} }
render() { render() {
if (this.renderingPaused) {
return // Do not render if paused
}
this.clearAndSetCursorPosition() this.clearAndSetCursorPosition()
const lines = this.renderTree('root') const lines = this.renderTree('root')
if (this.renderTasks) { if (this.renderTasks) {

Wyświetl plik

@ -2,16 +2,16 @@ import pRetry, { FailedAttemptError } from 'p-retry'
import QuickLRU from 'quick-lru' import QuickLRU from 'quick-lru'
import { ZodType } from 'zod' import { ZodType } from 'zod'
import * as errors from './errors'
import * as types from './types'
import type { Agentic } from './agentic' import type { Agentic } from './agentic'
import { SKIP_HOOKS } from './constants' import { SKIP_HOOKS } from './constants'
import * as errors from './errors'
import { TaskEventEmitter, TaskStatus } from './events' import { TaskEventEmitter, TaskStatus } from './events'
import { import {
HumanFeedbackMechanismCLI, HumanFeedbackMechanismCLI,
HumanFeedbackOptions, HumanFeedbackOptions,
HumanFeedbackType HumanFeedbackType
} from './human-feedback' } from './human-feedback'
import * as types from './types'
import { defaultIDGeneratorFn, isValidTaskIdentifier } from './utils' import { defaultIDGeneratorFn, isValidTaskIdentifier } from './utils'
/** /**
@ -201,7 +201,9 @@ export abstract class BaseTask<
}) })
this.addAfterCallHook(async (output, ctx) => { this.addAfterCallHook(async (output, ctx) => {
this._agentic.taskTracker.pause()
const feedback = await feedbackMechanism.interact(output) const feedback = await feedbackMechanism.interact(output)
this._agentic.taskTracker.resume()
ctx.metadata = { ...ctx.metadata, feedback } ctx.metadata = { ...ctx.metadata, feedback }
if (feedback.editedOutput) { if (feedback.editedOutput) {
return feedback.editedOutput return feedback.editedOutput
@ -344,7 +346,8 @@ export abstract class BaseTask<
...this._retryConfig, ...this._retryConfig,
onFailedAttempt: async (err: FailedAttemptError) => { onFailedAttempt: async (err: FailedAttemptError) => {
this._logger.warn( this._logger.warn(
`Task error "${this.nameForHuman}" failed attempt ${err.attemptNumber `Task error "${this.nameForHuman}" failed attempt ${
err.attemptNumber
}${input ? ': ' + JSON.stringify(input) : ''}`, }${input ? ': ' + JSON.stringify(input) : ''}`,
err err
) )