feat: add managed SSH execution spaces
Add managed SSH Hosts and projects with signed side-by-side GoodBuddy Agent and OpenCode Runtime installation, Ask and Execute workspace access, a Main-owned model bridge, detached Agent lifecycle, durable ACP output recovery, and project activation UI. Use protocol v2 with bounded FIFO frames over SSH instead of custom credit, priority, batching, or model-message chunking. Mismatched Host Agents now upgrade automatically, an exactly verified Node runtime is reused on-host, and local Portable builds can embed strictly verified Agent artifacts. Release note: 新增托管 SSH 远程项目,可自动安装或升级签名 Agent/OpenCode Runtime,并在 SSH 断线后继续 Agent 工作和同步未确认输出。
This commit is contained in:
@@ -10,6 +10,7 @@ import type {
|
||||
} from '../../shared/contracts'
|
||||
import type { ResolvedMcpServer } from '../capabilities/capability-service'
|
||||
import type { BrowserToolService } from '../browser/browser-model-tools'
|
||||
import type { WorkspaceAccess } from '../workspace'
|
||||
import {
|
||||
scopedReadToolNames,
|
||||
type KnowledgeMcpGateway
|
||||
@@ -173,13 +174,7 @@ const maxChatResponseBytes = 2 * 1024 * 1024
|
||||
const maxStreamBlockBytes = 1024 * 1024
|
||||
const maxToolArgumentBytes = 128 * 1024
|
||||
const maxToolContextBytes = 1024 * 1024
|
||||
const maxToolCallsPerRun = 40
|
||||
const maxToolRounds = 24
|
||||
const maxRepeatedIdenticalCalls = 3
|
||||
const maxIdenticalRoundsWithoutProgress = 2
|
||||
const defaultModelRequestTimeoutMs = 10 * 60_000
|
||||
const defaultModelOutputTokens = 4_096
|
||||
const summaryModelOutputTokens = contextSummaryTokenBudget
|
||||
|
||||
const noModelTools: ModelToolProviderLike = {
|
||||
listTools: async () => [],
|
||||
@@ -220,6 +215,7 @@ export type ModelRuntimeOptions = {
|
||||
imageGenerationQuality?: ImageGenerationQuality
|
||||
skillInstructions?: string
|
||||
defaultWorkspace?: string
|
||||
workspaceAccess?: WorkspaceAccess
|
||||
mcpServers?: ResolvedMcpServer[]
|
||||
browserService?: BrowserToolService
|
||||
knowledgeGateway?: KnowledgeMcpGateway
|
||||
@@ -227,7 +223,6 @@ export type ModelRuntimeOptions = {
|
||||
toolProvider?: ModelToolProviderLike
|
||||
fetcher?: typeof fetch
|
||||
requestTimeoutMs?: number
|
||||
maxOutputTokens?: number
|
||||
contextCompression?: {
|
||||
settings: ContextCompressionSettings
|
||||
contextWindowTokens?: number
|
||||
@@ -665,26 +660,6 @@ function parseToolArguments(value: unknown): Record<string, unknown> {
|
||||
return parsed as Record<string, unknown>
|
||||
}
|
||||
|
||||
function canonicalizeToolArguments(value: unknown): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(canonicalizeToolArguments)
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value as Record<string, unknown>)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, item]) => [key, canonicalizeToolArguments(item)])
|
||||
)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function getToolCallFingerprint(call: ModelToolCall): string {
|
||||
return `${call.name}:${JSON.stringify(
|
||||
canonicalizeToolArguments(call.arguments)
|
||||
)}`
|
||||
}
|
||||
|
||||
function validateToolResult(result: ModelToolResult): number {
|
||||
if (
|
||||
!Array.isArray(result.parts) ||
|
||||
@@ -1344,7 +1319,6 @@ async function* readAnthropicToolStream(
|
||||
let answer = ''
|
||||
let reasoning = ''
|
||||
let stopReason: unknown
|
||||
let toolCallCount = 0
|
||||
|
||||
for await (const block of readBoundedSseBlocks(response)) {
|
||||
const parsed = parseSseData(block)
|
||||
@@ -1421,10 +1395,6 @@ async function* readAnthropicToolStream(
|
||||
}
|
||||
}
|
||||
} else if (content.type === 'tool_use') {
|
||||
toolCallCount += 1
|
||||
if (toolCallCount > maxToolCallsPerRun) {
|
||||
throw new Error('模型单轮工具调用超过安全限制')
|
||||
}
|
||||
const identity = parseToolCallIdentity(content.id, content.name)
|
||||
next.kind = 'tool_use'
|
||||
next.initialInput = content.input
|
||||
@@ -1555,31 +1525,23 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
private readonly fetcher: typeof fetch
|
||||
private readonly toolProvider: ModelToolProviderLike
|
||||
private readonly requestTimeoutMs: number
|
||||
private readonly maxOutputTokens: number
|
||||
|
||||
constructor(private readonly options: ModelRuntimeOptions) {
|
||||
this.fetcher = options.fetcher ?? fetch
|
||||
this.requestTimeoutMs =
|
||||
options.requestTimeoutMs ?? defaultModelRequestTimeoutMs
|
||||
this.maxOutputTokens =
|
||||
options.maxOutputTokens ?? defaultModelOutputTokens
|
||||
if (
|
||||
!Number.isSafeInteger(this.requestTimeoutMs) ||
|
||||
this.requestTimeoutMs < 1
|
||||
) {
|
||||
throw new Error('模型接口请求超时设置无效')
|
||||
}
|
||||
if (
|
||||
!Number.isSafeInteger(this.maxOutputTokens) ||
|
||||
this.maxOutputTokens < 1 ||
|
||||
this.maxOutputTokens > summaryModelOutputTokens
|
||||
) {
|
||||
throw new Error('模型最大输出设置无效')
|
||||
}
|
||||
this.toolProvider =
|
||||
options.toolProvider ??
|
||||
new ModelToolProvider(
|
||||
options.defaultWorkspace ?? process.cwd(),
|
||||
options.workspaceAccess ??
|
||||
options.defaultWorkspace ??
|
||||
process.cwd(),
|
||||
options.mcpServers,
|
||||
options.browserService,
|
||||
options.knowledgeGateway,
|
||||
@@ -1831,6 +1793,14 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
)
|
||||
}
|
||||
|
||||
private get anthropicProtocolMaximumTokens(): number {
|
||||
return Math.max(
|
||||
this.options.contextCompression?.contextWindowTokens ??
|
||||
minimumModelContextWindowTokens,
|
||||
minimumModelContextWindowTokens
|
||||
)
|
||||
}
|
||||
|
||||
private getHardSafetyTriggerTokens(): number | undefined {
|
||||
const contextWindowTokens =
|
||||
this.options.contextCompression?.contextWindowTokens
|
||||
@@ -1842,8 +1812,7 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
contextWindowTokens,
|
||||
minimumModelContextWindowTokens
|
||||
) -
|
||||
this.maxOutputTokens -
|
||||
2_048
|
||||
12_000
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1915,8 +1884,7 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
supportsImageInput: false,
|
||||
toolProvider: noModelTools,
|
||||
fetcher: this.fetcher,
|
||||
requestTimeoutMs: this.requestTimeoutMs,
|
||||
maxOutputTokens: summaryModelOutputTokens
|
||||
requestTimeoutMs: this.requestTimeoutMs
|
||||
})
|
||||
const summaryRequest: AgentExecutionRequest = {
|
||||
requestId: request.requestId,
|
||||
@@ -2543,7 +2511,6 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
responses
|
||||
? {
|
||||
model: this.options.model,
|
||||
max_output_tokens: this.maxOutputTokens,
|
||||
stream: true,
|
||||
instructions: system,
|
||||
input: messages,
|
||||
@@ -2554,7 +2521,7 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
: anthropic
|
||||
? {
|
||||
model: this.options.model,
|
||||
max_tokens: this.maxOutputTokens,
|
||||
max_tokens: this.anthropicProtocolMaximumTokens,
|
||||
stream: true,
|
||||
system,
|
||||
messages,
|
||||
@@ -2564,7 +2531,6 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
}
|
||||
: {
|
||||
model: this.options.model,
|
||||
max_tokens: this.maxOutputTokens,
|
||||
stream: true,
|
||||
stream_options: {
|
||||
include_usage: true
|
||||
@@ -2690,8 +2656,7 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
const index = toolDelta?.index
|
||||
if (
|
||||
!Number.isSafeInteger(index) ||
|
||||
(index as number) < 0 ||
|
||||
(index as number) >= maxToolCallsPerRun
|
||||
(index as number) < 0
|
||||
) {
|
||||
throw new Error(
|
||||
'OpenAI 模型接口返回了无效流式工具调用序号'
|
||||
@@ -3011,14 +2976,9 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
rounds: [],
|
||||
compressionCount: 0
|
||||
}
|
||||
const seenCallIds = new Set<string>()
|
||||
let totalToolCalls = 0
|
||||
let answer = ''
|
||||
const identicalCallCounts = new Map<string, number>()
|
||||
let previousRoundSignature: string | undefined
|
||||
let identicalRoundsWithoutProgress = 0
|
||||
|
||||
for (let round = 0; round < maxToolRounds; round += 1) {
|
||||
for (let round = 0; ; round += 1) {
|
||||
signal.throwIfAborted()
|
||||
if (round > 0) {
|
||||
toolSnapshot = await loadToolSnapshot()
|
||||
@@ -3156,25 +3116,6 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
}
|
||||
return
|
||||
}
|
||||
const roundSignature = response.toolCalls
|
||||
.map(getToolCallFingerprint)
|
||||
.join('\n')
|
||||
if (roundSignature === previousRoundSignature) {
|
||||
identicalRoundsWithoutProgress += 1
|
||||
if (
|
||||
identicalRoundsWithoutProgress >=
|
||||
maxIdenticalRoundsWithoutProgress
|
||||
) {
|
||||
throw new Error('直连模型重复了相同工具调用且没有取得进展')
|
||||
}
|
||||
} else {
|
||||
previousRoundSignature = roundSignature
|
||||
identicalRoundsWithoutProgress = 0
|
||||
}
|
||||
totalToolCalls += response.toolCalls.length
|
||||
if (totalToolCalls > maxToolCallsPerRun) {
|
||||
throw new Error('直连模型单次运行的工具调用超过 40 个')
|
||||
}
|
||||
if (responses) {
|
||||
if (!response.responsesOutput) {
|
||||
throw new Error('OpenAI Responses 工具调用缺少 output')
|
||||
@@ -3205,19 +3146,13 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
const responsesResults: Array<Record<string, unknown>> = []
|
||||
const chatImageCarrierContent: Array<Record<string, unknown>> = []
|
||||
let roundContextBytes = 0
|
||||
const roundCallIds = new Set<string>()
|
||||
for (const call of response.toolCalls) {
|
||||
signal.throwIfAborted()
|
||||
const callFingerprint = getToolCallFingerprint(call)
|
||||
const identicalCallCount =
|
||||
(identicalCallCounts.get(callFingerprint) ?? 0) + 1
|
||||
identicalCallCounts.set(callFingerprint, identicalCallCount)
|
||||
if (identicalCallCount > maxRepeatedIdenticalCalls) {
|
||||
throw new Error('直连模型重复请求了完全相同的工具调用')
|
||||
}
|
||||
if (seenCallIds.has(call.id)) {
|
||||
if (roundCallIds.has(call.id)) {
|
||||
throw new Error('模型重复使用了工具调用 ID')
|
||||
}
|
||||
seenCallIds.add(call.id)
|
||||
roundCallIds.add(call.id)
|
||||
const tool = toolSnapshot.toolsByName.get(call.name)
|
||||
const displayName = tool?.displayName ?? call.name.slice(0, 128)
|
||||
const input = boundedToolDetail(call.arguments, 4_000)
|
||||
@@ -3421,7 +3356,6 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
})
|
||||
signal.throwIfAborted()
|
||||
}
|
||||
throw new Error('直连模型工具调用轮次超过 24 轮')
|
||||
}
|
||||
|
||||
async *run(
|
||||
@@ -3537,7 +3471,6 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
responses
|
||||
? {
|
||||
model: this.options.model,
|
||||
max_output_tokens: this.maxOutputTokens,
|
||||
stream: true,
|
||||
instructions: system,
|
||||
input: messages
|
||||
@@ -3545,14 +3478,13 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
: anthropic
|
||||
? {
|
||||
model: this.options.model,
|
||||
max_tokens: this.maxOutputTokens,
|
||||
max_tokens: this.anthropicProtocolMaximumTokens,
|
||||
stream: true,
|
||||
system,
|
||||
messages
|
||||
}
|
||||
: {
|
||||
model: this.options.model,
|
||||
max_tokens: this.maxOutputTokens,
|
||||
stream: true,
|
||||
stream_options: {
|
||||
include_usage: true
|
||||
|
||||
Reference in New Issue
Block a user