feat: add event emitting + terminal task output #34

old-agentic-v1^2
Philipp Burckhardt 2023-07-05 16:49:12 -04:00 zatwierdzone przez GitHub
commit 3c71bc7e31
13 zmienionych plików z 744 dodań i 6 usunięć

Wyświetl plik

@ -0,0 +1,22 @@
import 'dotenv/config'
import { OpenAIClient } from 'openai-fetch'
import { z } from 'zod'
import { Agentic, TaskStatus } from '@/index'
async function main() {
const openai = new OpenAIClient({ apiKey: process.env.OPENAI_API_KEY! })
const $ = new Agentic({ openai })
const ai = $.gpt4(`generate fake data`).output(
z.object({ foo: z.string(), bar: z.number() })
)
ai.eventEmitter.on(TaskStatus.COMPLETED, (event) => {
console.log('Task completed successfully:', event)
})
ai.call()
}
main()

Wyświetl plik

@ -45,6 +45,7 @@
"@types/json-schema": "^7.0.12",
"colorette": "^2.0.20",
"debug": "^4.3.4",
"eventemitter3": "^5.0.1",
"expr-eval": "^2.0.2",
"handlebars": "^4.7.7",
"is-relative-url": "^4.0.0",
@ -62,6 +63,7 @@
"p-timeout": "^6.1.2",
"quick-lru": "^6.1.1",
"replicate": "^0.12.3",
"tree-model": "^1.0.7",
"ts-dedent": "^2.2.0",
"type-fest": "^3.12.0",
"zod": "^3.21.4",

Wyświetl plik

@ -1,17 +1,20 @@
import { EventEmitter } from 'eventemitter3'
import defaultKy from 'ky'
import { SetOptional } from 'type-fest'
import * as types from './types'
import { DEFAULT_OPENAI_MODEL } from './constants'
import { TerminalTaskTracker } from './events'
import { HumanFeedbackOptions, HumanFeedbackType } from './human-feedback'
import { HumanFeedbackMechanismCLI } from './human-feedback/cli'
import { OpenAIChatCompletion } from './llms/openai'
import { defaultLogger } from './logger'
import { defaultIDGeneratorFn, isFunction, isString } from './utils'
export class Agentic {
export class Agentic extends EventEmitter {
protected _ky: types.KyInstance
protected _logger: types.Logger
protected _taskTracker: TerminalTaskTracker
protected _openai?: types.openai.OpenAIClient
protected _anthropic?: types.anthropic.Client
@ -35,7 +38,10 @@ export class Agentic {
idGeneratorFn?: types.IDGeneratorFunction
logger?: types.Logger
ky?: types.KyInstance
taskTracker?: TerminalTaskTracker
}) {
super()
// TODO: This is a bit hacky, but we're doing it to have a slightly nicer API
// for the end developer when creating subclasses of `BaseTask` to use as
// tools.
@ -48,6 +54,7 @@ export class Agentic {
this._ky = opts.ky ?? defaultKy
this._logger = opts.logger ?? defaultLogger
this._taskTracker = opts.taskTracker ?? new TerminalTaskTracker()
this._openaiModelDefaults = {
provider: 'openai',
@ -98,6 +105,10 @@ export class Agentic {
return this._humanFeedbackDefaults
}
public get taskTracker(): TerminalTaskTracker {
return this._taskTracker
}
public get idGeneratorFn(): types.IDGeneratorFunction {
return this._idGeneratorFn
}

Wyświetl plik

@ -2,3 +2,4 @@ export const DEFAULT_OPENAI_MODEL = 'gpt-3.5-turbo'
export const DEFAULT_ANTHROPIC_MODEL = 'claude-instant-v1'
export const DEFAULT_BOT_NAME = 'Agentic Bot'
export const SKIP_HOOKS = Symbol('SKIP_HOOKS')
export const SPACE = ' '

Wyświetl plik

@ -0,0 +1,158 @@
import { defaultIDGeneratorFn } from '@/utils'
/**
* Payload of an event.
*/
export interface EventPayload {
[key: string]: unknown
}
/**
* Data required to create a new Event object.
*/
export interface EventData<T extends EventPayload> {
/**
* Event identifier
*/
id?: string
/**
* Event timestamp
*/
timestamp?: Date
/**
* Key-value pairs holding event data.
*/
payload?: T
/**
* Version of the event.
*/
version?: number
/**
* Event type.
*/
type?: string
}
/**
* Events that occur within the library (should be treated as immutable).
*/
export class Event<T extends EventPayload> {
public readonly id: string
public readonly timestamp: Date
public readonly payload?: T
public readonly version: number
constructor(data: EventData<T> = {}) {
this.id = defaultIDGeneratorFn()
this.timestamp = data.timestamp ?? new Date()
this.payload = data.payload
? JSON.parse(JSON.stringify(data.payload))
: undefined
this.version = data.version ?? 1
}
/**
* Converts a JSON string representation of an event back into an Event object.
*/
static fromJSON<T extends EventPayload>(json: string): Event<T> {
const data = JSON.parse(json)
data.timestamp = new Date(data.timestamp)
let Type
switch (data.type) {
case 'TaskEvent':
Type = TaskEvent<any, any>
break
case 'Event':
Type = Event
break
default:
throw new Error(`Unknown event type: ${data.type}`)
}
return new Type(data)
}
/**
* Serializes an event into a JSON string.
*/
toJSON(): string {
return JSON.stringify({
id: this.id,
timestamp: this.timestamp.toISOString(),
payload: this.payload,
version: this.version,
type: this.constructor.name
})
}
/**
* Returns a human-readable string representation of an event.
*/
toString(): string {
return `Event { id: ${
this.id
}, timestamp: ${this.timestamp.toISOString()}, payload: ${JSON.stringify(
this.payload
)} }`
}
}
/**
* Payload of a task event.
*/
export interface TaskEventPayload<TInput, TOutput> extends EventPayload {
taskName: string
taskId: string
taskStatus: TaskStatus
taskInputs?: TInput
taskOutput?: TOutput
parentTaskId?: string
}
/**
* Status of a task.
*/
export enum TaskStatus {
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
PENDING = 'PENDING',
RETRYING = 'RETRYING',
SKIPPED = 'SKIPPED',
RUNNING = 'RUNNING',
CANCELLED = 'CANCELLED'
}
/**
* Events that occur within the library related to tasks.
*/
export class TaskEvent<TInput, TOutput> extends Event<
TaskEventPayload<TInput, TOutput>
> {
get name(): string {
return this.payload?.taskName ?? ''
}
get taskId(): string {
return this.payload?.taskId ?? ''
}
get status(): TaskStatus {
return this.payload?.taskStatus ?? TaskStatus.RUNNING
}
get inputs(): any {
return this.payload?.taskInputs ?? ''
}
get output(): any {
return this.payload?.taskOutput ?? ''
}
get parentTaskId(): string {
return this.payload?.parentTaskId ?? 'root'
}
}

Wyświetl plik

@ -0,0 +1,2 @@
export * from './event'
export * from './tracker'

Wyświetl plik

@ -0,0 +1,32 @@
import isUnicodeSupported from 'is-unicode-supported'
const UNICODE_SYMBOLS = {
ARROW_RIGHT: '→',
CIRCLE: '●',
WARNING: '▲',
CROSS: '',
SQUARE_SMALL_FILLED: '◼',
SPINNER: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
BAR_START: '┌',
BAR: '│',
BAR_END: '└',
ACTIVE: '◆',
LEFT_ARROW: '←',
RIGHT_ARROW: '→'
}
const ASCII_SYMBOLS = {
ARROW_RIGHT: '→',
CIRCLE: '•',
WARNING: '‼',
CROSS: '×',
SQUARE_SMALL_FILLED: '■',
SPINNER: ['-', '\\', '|', '/'],
BAR_START: 'T',
BAR: '|',
BAR_END: '—',
ACTIVE: '*',
LEFT_ARROW: '<',
RIGHT_ARROW: '>'
}
export const SYMBOLS = isUnicodeSupported() ? UNICODE_SYMBOLS : ASCII_SYMBOLS

Wyświetl plik

@ -0,0 +1,422 @@
import process from 'node:process'
import readline from 'node:readline'
import { bgWhite, black, bold, cyan, gray, green, red, yellow } from 'colorette'
import TreeModel from 'tree-model'
import { SPACE } from '@/constants'
import { capitalize } from '@/utils'
import { TaskEvent, TaskStatus } from './event'
import { SYMBOLS } from './symbols'
export const MAGIC_STRING = '__INSIDE_TRACKER__' // a unique "magic" string that used to identify the output of the tracker
const TWO_SPACES = `${SPACE}${SPACE}`
// eslint-disable-next-line no-control-regex
const RE_ANSI_ESCAPES = /^(\x1b\[[0-9;]*[ABCDHJK]|[\r\n])+$/ // cursor movement, screen clearing, etc.
const originalStdoutWrite = process.stdout.write
const originalStderrWrite = process.stderr.write
export interface TerminalTaskTrackerOptions {
spinnerInterval?: number
inactivityInterval?: number
}
export class TerminalTaskTracker {
protected _tree = new TreeModel()
protected _root = this._tree.parse({ id: 'root' })
protected _interval: NodeJS.Timeout | null = null
protected _inactivityTimeout: NodeJS.Timeout | null = null
protected _truncateOutput = false
protected _viewMode = 'tasks'
protected _outputs: Array<string | Uint8Array> = []
protected _renderingPaused = false
protected _isClosed = false
protected _spinnerInterval: number
protected _inactivityInterval: number
private _stdoutBuffer: string[] = []
private _stderrBuffer: string[] = []
constructor({
spinnerInterval = 100,
inactivityInterval = 2_000
}: TerminalTaskTrackerOptions = {}) {
this._spinnerInterval = spinnerInterval
this._inactivityInterval = inactivityInterval
if (!process.stderr.isTTY) {
// If stderr is not a TTY, don't render any dynamic output...
return
}
process.stdout.write = (buffer: string | Uint8Array) => {
if (buffer instanceof Uint8Array) {
buffer = Buffer.from(buffer).toString('utf-8')
}
if (!this._renderingPaused) {
this._stdoutBuffer.push(buffer)
}
return originalStdoutWrite.call(process.stdout, buffer)
}
process.stderr.write = (buffer: string | Uint8Array) => {
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:
return originalStderrWrite.call(
process.stderr,
buffer.replace(MAGIC_STRING, '')
)
} else {
if (!this._renderingPaused && !RE_ANSI_ESCAPES.test(buffer)) {
// If an ANSI escape sequence is written to stderr, it will mess up the output, so we need to write it to stdout instead:
// 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()
}
handleKeyPress = (str, key) => {
if (key.ctrl && key.name === 'c') {
process.exit()
}
if (key.ctrl && key.name === 'e') {
this.toggleOutputTruncation()
}
if (key.ctrl && key.name === 'right') {
this.toggleView('next')
}
if (key.ctrl && key.name === 'left') {
this.toggleView('prev')
}
}
start() {
this._interval = setInterval(() => {
this.render()
}, this._spinnerInterval)
readline.emitKeypressEvents(process.stdin)
process.stdin.setRawMode(true)
process.stdin.on('keypress', this.handleKeyPress)
this.startInactivityTimeout()
this._isClosed = false
}
close() {
if (this._isClosed) {
return
}
if (this._interval) {
clearInterval(this._interval)
}
if (this._inactivityTimeout) {
clearTimeout(this._inactivityTimeout)
}
process.stdin.setRawMode(false)
// Remove the keypress listener:
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 = [
'',
'',
bgWhite(black(' Completed all tasks. ')),
'',
'',
bgWhite(black(' stderr: ')),
'',
this._stderrBuffer.join(''),
'',
bgWhite(black(' stdout: ')),
'',
this._stdoutBuffer.join(''),
''
]
process.stderr.write(finalLines.join('\n'))
// Pause the reading of stdin so that the Node.js process will exit once done:
process.stdin.pause()
this._isClosed = true
}
pause() {
this.clearAndSetCursorPosition()
this._renderingPaused = true
}
resume() {
this._renderingPaused = false
this.render()
}
stringify(value: any): string {
if (this._truncateOutput) {
const json = JSON.stringify(value)
if (json.length < 40) {
return json
}
return json.slice(0, 20) + '...' + json.slice(-20)
}
return JSON.stringify(value)
}
toggleOutputTruncation() {
this._truncateOutput = !this._truncateOutput
}
startInactivityTimeout() {
this._inactivityTimeout = setTimeout(() => {
const unfinishedTasks = this._root.all((node) => {
return (
node.model.status == TaskStatus.RUNNING ||
node.model.status == TaskStatus.RETRYING ||
node.model.status == TaskStatus.PENDING
)
})
if (unfinishedTasks.length === 0) {
this.close()
} else {
this.startInactivityTimeout()
}
}, this._inactivityInterval)
}
addEvent<TInput, TOutput>(event: TaskEvent<TInput, TOutput>) {
const {
parentTaskId = 'root',
taskId: id,
name,
status,
inputs,
output
} = event
const parentNode = this._root.first(
(node) => node.model.id === parentTaskId
)
const existingEventNode = parentNode
? parentNode.first((node) => node.model.id === id)
: null
if (existingEventNode) {
existingEventNode.model.status = status
existingEventNode.model.output = output
} else {
const node = this._tree.parse({ id, name, status, inputs, output: null })
if (parentNode) {
parentNode.addChild(node)
} else {
this._root.addChild(node)
}
}
}
private getStatusSymbolColor(
status: TaskStatus
): [string, (text: string) => string] {
switch (status) {
case TaskStatus.COMPLETED:
return [SYMBOLS.CIRCLE, green]
case TaskStatus.FAILED:
return [SYMBOLS.CROSS, red]
case TaskStatus.RETRYING:
return [this.getSpinnerSymbol(), yellow]
case TaskStatus.RUNNING:
default:
return [this.getSpinnerSymbol(), cyan]
}
}
renderTree(id?: string, level = 0): string[] {
const indent = SPACE.repeat(level * 2)
let lines: string[] = []
const root = id
? this._root.first((node) => node.model.id === id)
: this._root
if (root?.children) {
root.children.forEach(
({ model: { id, name, status, output, inputs } }) => {
const [statusSymbol, color] = this.getStatusSymbolColor(status)
lines.push(
indent +
color(statusSymbol) +
SPACE +
bold(name) +
gray('(' + this.stringify(inputs) + ')')
)
const hasChildren = root.hasChildren()
if (hasChildren) {
lines = lines.concat(
this.renderTree(id, level + 1).map((line, index, arr) => {
if (index === arr.length - 1) {
return indent + gray(SYMBOLS.BAR) + line
}
return indent + gray(SYMBOLS.BAR) + line
})
)
}
let line = ''
if (hasChildren) {
line = indent + gray(SYMBOLS.BAR_END)
}
const formattedOutput = this.stringify(output || '')
if (status === TaskStatus.COMPLETED) {
line +=
indent +
' ' +
gray(SYMBOLS.RIGHT_ARROW + SPACE + formattedOutput)
} else if (status === TaskStatus.FAILED) {
line +=
indent +
' ' +
gray(SYMBOLS.RIGHT_ARROW) +
SPACE +
red(formattedOutput)
} else if (status === TaskStatus.RETRYING) {
line +=
indent +
' ' +
yellow(SYMBOLS.WARNING) +
SPACE +
gray(formattedOutput)
}
lines.push(line)
}
)
}
return lines
}
clearAndSetCursorPosition() {
process.stderr.cursorTo(0, 0)
process.stderr.clearScreenDown()
}
clearPreviousRender(linesCount: number) {
for (let i = 0; i < linesCount; i++) {
process.stderr.moveCursor(0, -1)
process.stderr.clearLine(1)
}
}
private writeWithMagicString(content: string | string[]) {
let output
if (Array.isArray(content)) {
if (content.length === 0) {
return
}
output = content.join('\n')
} else {
output = content
}
process.stderr.write(MAGIC_STRING + output)
}
toggleView(direction: string) {
const viewModes = ['tasks', 'stdout', 'stderr']
const currentIdx = viewModes.indexOf(this._viewMode)
if (direction === 'next') {
this._viewMode = viewModes[(currentIdx + 1) % viewModes.length]
} else if (direction === 'prev') {
this._viewMode =
viewModes[(currentIdx - 1 + viewModes.length) % viewModes.length]
}
this.render()
}
getSpinnerSymbol(): string {
return SYMBOLS.SPINNER[
Math.floor(Date.now() / this._spinnerInterval) % SYMBOLS.SPINNER.length
]
}
renderHeader() {
const commands = [
'ctrl+c: exit',
`ctrl+e: ${
this._truncateOutput ? TWO_SPACES + 'expand' : 'truncate'
} output`,
'ctrl+left/right: switch view'
].join(' | ')
const header = [
` Agentic - ${capitalize(this._viewMode)} View`,
' ' + commands + ' ',
'',
''
].join('\n')
this.writeWithMagicString(bgWhite(black(header)))
}
render() {
if (this._renderingPaused) {
return // Do not render if paused
}
this.clearAndSetCursorPosition()
if (this._viewMode === 'tasks') {
const lines = this.renderTree('root')
this.clearPreviousRender(lines.length)
this.renderHeader()
this.writeWithMagicString(lines)
} else if (this._viewMode === 'stdout') {
this.clearPreviousRender(this._stdoutBuffer.length)
this.renderHeader()
this.writeWithMagicString(this._stdoutBuffer)
} else if (this._viewMode === 'stderr') {
this.clearPreviousRender(this._stderrBuffer.length)
this.renderHeader()
this.writeWithMagicString(this._stderrBuffer)
}
}
}

Wyświetl plik

@ -1,9 +1,10 @@
export * from './agentic'
export * from './task'
export * from './constants'
export * from './errors'
export * from './tokenizer'
export * from './events'
export * from './human-feedback'
export * from './task'
export * from './tokenizer'
export * from './llms'
export * from './services'

Wyświetl plik

@ -1,6 +1,7 @@
import { cyan, green, magenta, red, yellow } from 'colorette'
import logger from 'debug'
import { SPACE } from '@/constants'
import { identity } from '@/utils'
import { getEnv } from './env'
@ -57,7 +58,6 @@ if (LOG_LEVEL === undefined) {
const debug = logger('agentic')
const SPACE = ' '
const INDENT = SPACE.repeat(23)
// Override the default logger to add a timestamp and severity level to the logged arguments:

Wyświetl plik

@ -1,3 +1,4 @@
import EventEmitter from 'eventemitter3'
import pRetry, { FailedAttemptError } from 'p-retry'
import QuickLRU from 'quick-lru'
import { ZodType } from 'zod'
@ -6,6 +7,7 @@ import * as errors from './errors'
import * as types from './types'
import type { Agentic } from './agentic'
import { SKIP_HOOKS } from './constants'
import { TaskEvent, TaskStatus } from './events'
import {
HumanFeedbackMechanismCLI,
HumanFeedbackOptions,
@ -29,7 +31,7 @@ import { defaultIDGeneratorFn, isValidTaskIdentifier } from './utils'
export abstract class BaseTask<
TInput extends types.TaskInput = void,
TOutput extends types.TaskOutput = string
> {
> extends EventEmitter {
protected _agentic: Agentic
protected _id: string
@ -48,6 +50,8 @@ export abstract class BaseTask<
}> = []
constructor(options: types.BaseTaskOptions = {}) {
super()
this._agentic = options.agentic ?? globalThis.__agentic?.deref()
this._timeoutMs = options.timeoutMs
@ -190,7 +194,9 @@ export abstract class BaseTask<
})
this.addAfterCallHook(async (output, ctx) => {
this._agentic.taskTracker.pause()
const feedback = await feedbackMechanism.interact(output)
this._agentic.taskTracker.resume()
ctx.metadata = { ...ctx.metadata, feedback }
if (feedback.editedOutput) {
return feedback.editedOutput
@ -272,6 +278,11 @@ export abstract class BaseTask<
}
}
this.emit(TaskStatus.RUNNING, {
taskInputs: input,
...ctx.metadata
})
for (const { hook: preHook } of this._preHooks) {
const preHookResult = await preHook(ctx)
if (preHookResult === SKIP_HOOKS) {
@ -342,6 +353,12 @@ export abstract class BaseTask<
ctx.attemptNumber = err.attemptNumber + 1
ctx.metadata.error = err
this.emit(TaskStatus.RETRYING, {
taskInputs: input,
taskOutput: err,
...ctx.metadata
})
if (err instanceof errors.ZodOutputValidationError) {
ctx.retryMessage = err.message
return
@ -365,6 +382,12 @@ export abstract class BaseTask<
// task for now.
return
} else {
this.emit(TaskStatus.FAILED, {
taskInputs: input,
taskOutput: err,
...ctx.metadata
})
throw err
}
}
@ -381,6 +404,12 @@ export abstract class BaseTask<
// ctx.tracker.setOutput(stringifyForDebugging(result, { maxLength: 100 }))
this.emit(TaskStatus.COMPLETED, {
taskInputs: input,
taskOutput: result,
...ctx.metadata
})
return {
result,
metadata: ctx.metadata
@ -397,4 +426,33 @@ export abstract class BaseTask<
// input: TInput,
// onProgress: types.ProgressFunction
// }): Promise<TOutput>
on<T extends string | symbol>(
takStatus: T,
fn: (event: TaskEvent<TInput, TOutput>) => void,
context?: any
): this {
return super.on(takStatus, fn, context)
}
emit(taskStatus: string | symbol, payload: object = {}): boolean {
if (!Object.values(TaskStatus).includes(taskStatus as TaskStatus)) {
throw new Error(`Invalid task status: ${String(taskStatus)}`)
}
const { id, nameForModel } = this
const event = new TaskEvent<TInput, TOutput>({
payload: {
taskStatus: taskStatus as TaskStatus,
taskId: id,
taskName: nameForModel,
...payload
}
})
this._agentic.taskTracker.addEvent(event)
this._agentic.emit(taskStatus, event)
return super.emit(taskStatus, event)
}
}

Wyświetl plik

@ -254,3 +254,7 @@ export function isArray(value: any): value is any[] {
export function identity<T>(x: T): T {
return x
}
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}

Wyświetl plik

@ -1,4 +1,4 @@
lockfileVersion: '6.0'
lockfileVersion: '6.1'
settings:
autoInstallPeers: true
@ -125,6 +125,9 @@ importers:
debug:
specifier: ^4.3.4
version: 4.3.4
eventemitter3:
specifier: ^5.0.1
version: 5.0.1
expr-eval:
specifier: ^2.0.2
version: 2.0.2
@ -176,6 +179,9 @@ importers:
replicate:
specifier: ^0.12.3
version: 0.12.3
tree-model:
specifier: ^1.0.7
version: 1.0.7
ts-dedent:
specifier: ^2.2.0
version: 2.2.0
@ -3222,6 +3228,10 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
dev: false
/execa@0.8.0:
resolution: {integrity: sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==}
engines: {node: '>=4'}
@ -3359,6 +3369,10 @@ packages:
to-regex-range: 5.0.1
dev: true
/find-insert-index@0.0.1:
resolution: {integrity: sha512-eIqFuQzY7XwpAJ3sHWKFNGLx1nm3w/IhmFASETcx5sUuCaOUd3xDqRK/376SzXMVVJQaJUCPlS7L841T0xpFjQ==}
dev: false
/find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
@ -4834,6 +4848,10 @@ packages:
engines: {node: '>= 8'}
dev: true
/mergesort@0.0.1:
resolution: {integrity: sha512-WKghTBzqAvTt9rG5TWS78Dmk2kCCL9VkkX8Zi9kKfJ4iqYpvcGGpeYtkhPHa9NZAPLivZiZsdO/LBG3ENayDmQ==}
dev: false
/mermaid@10.2.3:
resolution: {integrity: sha512-cMVE5s9PlQvOwfORkyVpr5beMsLdInrycAosdr+tpZ0WFjG4RJ/bUHST7aTgHNJbujHkdBRAm+N50P3puQOfPw==}
dependencies:
@ -6928,6 +6946,13 @@ packages:
hasBin: true
dev: true
/tree-model@1.0.7:
resolution: {integrity: sha512-oP4LUbCVtD2gcjcRaeI4L5hY60tHzB+AK/bthIJ2Pq1EUUOio5/xFzPWnGoBZlhtqpqbOkhFDzKIwKLOn0kccQ==}
dependencies:
find-insert-index: 0.0.1
mergesort: 0.0.1
dev: false
/trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
dev: false