kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
fix: handle ANSI escapes and uint8 utf-8 writes
rodzic
c4ecea1ff8
commit
ac5eba853a
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
Ładowanie…
Reference in New Issue