Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/core/src/llm-core/platform/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,8 @@ export class ChatLunaChatModel extends BaseChatModel<ChatLunaModelCallOptions> {
) {
if (hasChunk) {
logger.debug(
'Stream failed after yielding chunks, cannot retry'
'Stream failed after yielding chunks, cannot retry',
error
)
}
if (reportUsage) {
Expand All @@ -322,7 +323,8 @@ export class ChatLunaChatModel extends BaseChatModel<ChatLunaModelCallOptions> {
}

logger.debug(
`Stream failed before first chunk (attempt ${attempt + 1}/${maxRetries}), retrying...`
`Stream failed before first chunk (attempt ${attempt + 1}/${maxRetries}), retrying...`,
error
)
await sleep(2000 * 2 ** attempt)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,8 @@ function ensureSkillEntry(cfg, key) {
for (const [id, item] of Object.entries(cfg.skills.items)) {
if (item.name === key) return { id, item }
}
const scanned = scanSkillDirs(cfg)
if (!scanned.includes(key)) throw new Error(`Skill not found: ${key}`)
cfg.skills.items[key] = { enabled: true, mode: 'description' }
return { id: key, item: cfg.skills.items[key] }
}
Expand Down
8 changes: 2 additions & 6 deletions packages/extension-agent/src/commands/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,9 @@ export function apply(ctx: Context) {

try {
const result = await service.syncAgentcliConfig()
const header = result.applied
? 'agentcli sync: applied'
: 'agentcli sync: no changes'
return `${header}\n${result.message}`
return `${result.applied ? 'agentcli sync: applied' : 'agentcli sync: no changes'}\n${result.message}`
} catch (err) {
const msg = getErrorMessage(err) || String(err) || 'unknown error'
return `agentcli sync failed: ${msg}`
return `agentcli sync failed: ${getErrorMessage(err)}`
}
})
}
Expand Down
4 changes: 1 addition & 3 deletions packages/extension-agent/src/commands/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,13 @@ export function apply(ctx: Context) {
const config = structuredClone(
ctx.chatluna_agent.getConsoleData().config
)
let count = 0

for (const [name, server] of Object.entries(servers)) {
config.mcp.mcpServers[name] = server as never
count += 1
}

await ctx.chatluna_agent.saveMcpConfig(config.mcp)
return `Added ${count} server(s)`
return `Added ${Object.keys(servers).length} server(s)`
}
})
)
Expand Down
216 changes: 46 additions & 170 deletions packages/extension-agent/src/computer/backends/e2b.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import mimeTypes from 'mime-types'
import { Context } from 'koishi'
import { buildPosixBackgroundCommand, quoteShell } from './types'
import { E2BBackendConfig } from '../../types'
import { ComputerCapability, E2BBackendConfig } from '../../types'
import { getErrorMessage } from '../../utils/shell'
import {
ComputerSessionApi,
Expand All @@ -40,13 +40,24 @@
setTimeout(timeoutMs: number): Promise<void>
pause(apiKey?: string): Promise<void>
kill(): Promise<void>
desktop?: never
}

export class E2BComputerSession implements ComputerSessionApi {
readonly backend = 'e2b' as const
readonly sessionId: string
readonly capabilities = [...CAPABILITIES]
readonly capabilities: ComputerCapability[] = [
'file_read',
'file_write',
'file_edit',
'file_publish',
'grep',
'glob',
'bash',
'terminal_pty',
'desktop_stream',
'desktop_screenshot',
'desktop_action'
]

private _connected = false
private _connecting?: Promise<void>
Expand Down Expand Up @@ -123,22 +134,20 @@
}

await sandbox.setTimeout(this.cfg.timeoutMs)
const current = await this.run(
'pwd',
{
timeoutMs: 5000
} as CommandStartOpts,
sandbox
)
this._home = current.stdout.trim() || '/'
this._home =
(
await this.run(
'pwd',
{ timeoutMs: 5000 } as CommandStartOpts,
sandbox
)
).stdout.trim() || '/'

if (this.options.cwd) {
const cwd = this.resolvePath(this.options.cwd)
const stat = await this.run(
`if [ -d ${quoteShell(cwd)} ]; then printf __dir__; fi`,
{
timeoutMs: 5000
} as CommandStartOpts,
{ timeoutMs: 5000 } as CommandStartOpts,
sandbox
)
if (stat.stdout.trim() === '__dir__') {
Expand Down Expand Up @@ -184,11 +193,6 @@
return
}

const desktop = this.ensureDesktopSandbox()
if (desktop) {
await desktop.stream.stop().catch(() => undefined)
}

try {
if (this.cfg.keepAlive) {
await this._sandbox.pause(this.resolveSecret(this.cfg.apiKey))
Expand Down Expand Up @@ -226,8 +230,9 @@
return result.stdout.trim()
}

const raw = await (await this.ensureSandbox()).files.read(target)
const text = String(raw)
const text = String(
await (await this.ensureSandbox()).files.read(target)
)
if (offset == null && limit == null) {
return text
}
Expand Down Expand Up @@ -256,10 +261,9 @@
return
}

const dir = posix.dirname(target)
const tmp = `${target}.${randomUUID()}.base64`

await this.execute(`mkdir -p ${quoteShell(dir)}`)
await this.execute(`mkdir -p ${quoteShell(posix.dirname(target))}`)
await sandbox.files.write(
tmp,
Buffer.from(content).toString('base64')
Expand Down Expand Up @@ -298,8 +302,7 @@

if (replaceCount === 1) {
const firstIdx = content.indexOf(oldString)
const secondIdx = content.indexOf(oldString, firstIdx + 1)
if (secondIdx !== -1) {
if (content.indexOf(oldString, firstIdx + 1) !== -1) {
throw new Error(
`Found multiple matches for oldString in ${filePath}. ` +
'Provide more surrounding lines in oldString to identify the correct match, or set replaceAll to change every instance.'
Expand Down Expand Up @@ -403,16 +406,15 @@
const sandbox = await this.ensureSandbox()
const target = this.resolvePath(filePath)
const info = await sandbox.files.getInfo(target)
const stream = await sandbox.files.read(target, {
format: 'stream'
})
const mimeType = mimeTypes.lookup(filePath)
const mime = mimeTypes.lookup(filePath)
return {
stream: Readable.fromWeb(
stream as unknown as globalThis.ReadableStream<Uint8Array>
(await sandbox.files.read(target, {
format: 'stream'
})) as unknown as globalThis.ReadableStream<Uint8Array>
),
size: info.size,
mimeType: mimeType === false ? undefined : mimeType
mimeType: mime === false ? undefined : mime
}
} catch (err) {
this.ctx.logger.error(err)
Expand All @@ -434,8 +436,8 @@
timeoutMs: this.cfg.timeoutMs,
onData: (data) => {
const text = Buffer.from(data).toString('utf8')
for (const callback of callbacks) {
callback(text)
for (const cb of callbacks) {
cb(text)
}
}
})
Expand Down Expand Up @@ -500,116 +502,20 @@
}

async getDesktopInfo(): Promise<DesktopInfo | undefined> {
// const desktop = this.ensureDesktopSandbox()
// if (!desktop) {
// return undefined
// }

// await desktop.stream.start().catch(() => undefined)
// const size = await desktop.getScreenSize()
// return {
// width: size.width,
// height: size.height,
// streamUrl: desktop.stream.getUrl({
// autoConnect: true,
// resize: 'scale'
// })
// }
return null
return undefined
}
Comment thread
dingyi222666 marked this conversation as resolved.
Comment thread
dingyi222666 marked this conversation as resolved.

async screenshot(): Promise<ScreenshotResult> {
// const desktop = this.ensureDesktopSandbox()
// if (!desktop) {
throw new Error('Desktop is not enabled for this E2B session.')
// }

// const bytes = await desktop.screenshot('bytes')
// const size = await desktop.getScreenSize()
// return {
// data: Buffer.from(bytes).toString('base64'),
// mimeType: 'image/png',
// width: size.width,
// height: size.height
// }
//
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async desktopAction(action: DesktopAction) {
const desktop = this.ensureDesktopSandbox()
if (!desktop) {
throw new Error('Desktop is not enabled for this E2B session.')
}

// if (action.type === 'click') {
// if (action.button === 'right') {
// await desktop.rightClick(action.x, action.y)
// return
// }
// if (action.button === 'middle') {
// await desktop.middleClick(action.x, action.y)
// return
// }
// await desktop.leftClick(action.x, action.y)
// return
// }

// if (action.type === 'type') {
// await desktop.write(action.text)
// return
// }

// if (action.type === 'key') {
// await desktop.press(action.key)
// return
// }

// if (action.type === 'scroll') {
// const direction = action.deltaY < 0 ? 'up' : 'down'
// await desktop.scroll(
// direction,
// Math.max(1, Math.abs(action.deltaY))
// )
// return
// }

// await desktop.drag(
// [action.startX, action.startY],
// [action.endX, action.endY]
// )
throw new Error('Desktop is not enabled for this E2B session.')
}

async getDesktopStream(): Promise<StreamHandle | undefined> {
const desktop = this.ensureDesktopSandbox()
if (!desktop) {
return undefined
}

try {
await desktop.stream.start()
const ctx = this.ctx
return {
url: desktop.stream.getUrl({
autoConnect: true,
resize: 'scale'
}),
async stop() {
try {
await desktop.stream.stop()
} catch (err) {
ctx.logger.error(err)
throw new Error(
`Failed to stop desktop stream: ${getErrorMessage(err)}`
)
}
}
}
} catch (err) {
this.ctx.logger.error(err)
throw new Error(
`Failed to start desktop stream: ${getErrorMessage(err)}`
)
}
return undefined
}

isInScope() {
Expand Down Expand Up @@ -640,67 +546,65 @@
return this._sandbox
}

private async run(
command: string,
options?: CommandStartOpts,
sandbox?: SandboxWrapper
): Promise<ExecuteResult> {
const current = sandbox ?? (await this.ensureSandbox())
let handle: CommandHandle | undefined
let result: CommandResult | undefined
let runErr: unknown
let timedOut = false

try {
handle = (await current.commands.run(command, {
...options,
background: true
})) as CommandHandle
result = await handle.wait()
} catch (err) {
if (err instanceof CommandExitError) {
result = err
} else if (
err instanceof TimeoutError ||
(err instanceof Error && err.name === 'TimeoutError')
) {
timedOut = true
result = {
exitCode: handle?.exitCode ?? 1,
stdout: handle?.stdout ?? '',
stderr: handle?.stderr ?? ''
}
} else {
runErr = err
}
}

try {
await current.setTimeout(this.cfg.timeoutMs)
} catch (err) {
if (!runErr && !isMissingSandboxError(err)) {
throw err
}
}

if (runErr) {
throw runErr
}

if (!result) {
throw new Error('Command finished without a result.')
}

return mapCommandResult(result, timedOut)
}

private ensureDesktopSandbox() {
return undefined
}

private usesDesktop() {
return this.cfg.desktopTemplate.length > 0
return {
exitCode: result.exitCode ?? 0,
stdout: result.stdout,
stderr: result.stderr,
signal: undefined,
timedOut
}
}

Check notice on line 607 in packages/extension-agent/src/computer/backends/e2b.ts

View check run for this annotation

codefactor.io / CodeFactor

packages/extension-agent/src/computer/backends/e2b.ts#L549-L607

Complex Method

private resolvePath(value: string) {
if (value === '~') {
Expand All @@ -727,19 +631,6 @@
}
}

function mapCommandResult(
result: CommandResult | CommandHandle,
timedOut = false
) {
return {
exitCode: result.exitCode ?? 0,
stdout: result.stdout,
stderr: result.stderr,
signal: undefined,
timedOut
}
}

function isMissingSandboxError(err: unknown) {
if (err instanceof NotFoundError) {
return true
Expand All @@ -764,20 +655,5 @@
},
kill: () => sandbox.kill(),
internal: sandbox
// desktop: sandbox instanceof DesktopSandbox ? sandbox : undefined
}
}

const CAPABILITIES = [
'file_read',
'file_write',
'file_edit',
'file_publish',
'grep',
'glob',
'bash',
'terminal_pty',
'desktop_stream',
'desktop_screenshot',
'desktop_action'
] as const
Loading
Loading