feat: improve runtime visibility and browser interaction
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
boundedToolDetail,
|
||||
safeToolArgumentSummary,
|
||||
safeToolErrorDetail
|
||||
} from './approval-summary'
|
||||
@@ -30,8 +31,29 @@ describe('safeToolArgumentSummary', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('boundedToolDetail', () => {
|
||||
it('preserves conversation details verbatim while bounding output', () => {
|
||||
expect(
|
||||
boundedToolDetail(
|
||||
{
|
||||
command: 'npm test',
|
||||
token: 'secret-token',
|
||||
output: 'Authorization: Bearer inline-secret'
|
||||
},
|
||||
1_000
|
||||
)
|
||||
).toBe(
|
||||
'{\n "command": "npm test",\n "token": "secret-token",\n "output": "Authorization: Bearer inline-secret"\n}'
|
||||
)
|
||||
expect(
|
||||
boundedToolDetail(' exact output\r\n', 1_000)
|
||||
).toBe(' exact output\r\n')
|
||||
expect(boundedToolDetail('x'.repeat(100), 20)).toHaveLength(20)
|
||||
})
|
||||
})
|
||||
|
||||
describe('safeToolErrorDetail', () => {
|
||||
it('extracts nested runtime errors while redacting secrets', () => {
|
||||
it('extracts nested runtime errors without rewriting their contents', () => {
|
||||
expect(
|
||||
safeToolErrorDetail([
|
||||
{
|
||||
@@ -39,14 +61,14 @@ describe('safeToolErrorDetail', () => {
|
||||
'exit code 1\nAuthorization: Bearer secret-token'
|
||||
}
|
||||
])
|
||||
).toBe('exit code 1\nAuthorization: [REDACTED]')
|
||||
).toBe('exit code 1\nAuthorization: Bearer secret-token')
|
||||
expect(
|
||||
safeToolErrorDetail({
|
||||
message:
|
||||
'{"token":"json-secret","authorization":"Basic abc123"}'
|
||||
})
|
||||
).toBe(
|
||||
'{"token":"[REDACTED]","authorization":"[REDACTED]"}'
|
||||
'{"token":"json-secret","authorization":"Basic abc123"}'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -66,4 +88,31 @@ describe('safeToolErrorDetail', () => {
|
||||
})
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('includes nested fetch causes and network diagnostics', () => {
|
||||
const cause = Object.assign(
|
||||
new Error('connect ECONNREFUSED 127.0.0.1:11434'),
|
||||
{
|
||||
code: 'ECONNREFUSED',
|
||||
errno: -4078,
|
||||
syscall: 'connect',
|
||||
address: '127.0.0.1',
|
||||
port: 11434
|
||||
}
|
||||
)
|
||||
const error = new TypeError('fetch failed', { cause })
|
||||
|
||||
expect(safeToolErrorDetail(error)).toBe(
|
||||
[
|
||||
'fetch failed',
|
||||
'cause:',
|
||||
'connect ECONNREFUSED 127.0.0.1:11434',
|
||||
'code: ECONNREFUSED',
|
||||
'errno: -4078',
|
||||
'syscall: connect',
|
||||
'address: 127.0.0.1',
|
||||
'port: 11434'
|
||||
].join('\n')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,9 @@ function redactValue(
|
||||
if (depth > 8) {
|
||||
return '[TRUNCATED]'
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return redactSensitiveText(value)
|
||||
}
|
||||
if (!value || typeof value !== 'object') {
|
||||
return value
|
||||
}
|
||||
@@ -64,6 +67,32 @@ export function safeToolErrorDetail(
|
||||
let remaining = maximum
|
||||
const seen = new WeakSet<object>()
|
||||
|
||||
const append = (value: string): void => {
|
||||
const text = [...value]
|
||||
.filter((character) => {
|
||||
const code = character.charCodeAt(0)
|
||||
return (
|
||||
code === 9 ||
|
||||
code === 10 ||
|
||||
code === 13 ||
|
||||
(code > 31 && code !== 127)
|
||||
)
|
||||
})
|
||||
.join('')
|
||||
.trim()
|
||||
if (!text || remaining <= 0) {
|
||||
return
|
||||
}
|
||||
const separator = parts.length > 0 ? '\n' : ''
|
||||
const available = Math.max(0, remaining - separator.length)
|
||||
if (available === 0) {
|
||||
return
|
||||
}
|
||||
const bounded = text.slice(0, available)
|
||||
parts.push(`${separator}${bounded}`)
|
||||
remaining -= separator.length + bounded.length
|
||||
}
|
||||
|
||||
const collect = (candidate: unknown, depth = 0): void => {
|
||||
if (remaining <= 0 || depth > 4 || candidate === undefined) {
|
||||
return
|
||||
@@ -73,30 +102,7 @@ export function safeToolErrorDetail(
|
||||
0,
|
||||
Math.min(candidate.length, remaining * 4)
|
||||
)
|
||||
const text = redactSensitiveText(
|
||||
[...boundedCandidate]
|
||||
.filter((character) => {
|
||||
const code = character.charCodeAt(0)
|
||||
return (
|
||||
code === 9 ||
|
||||
code === 10 ||
|
||||
code === 13 ||
|
||||
(code > 31 && code !== 127)
|
||||
)
|
||||
})
|
||||
.join('')
|
||||
).trim()
|
||||
if (!text) {
|
||||
return
|
||||
}
|
||||
const separator = parts.length > 0 ? '\n' : ''
|
||||
const available = Math.max(0, remaining - separator.length)
|
||||
if (available === 0) {
|
||||
return
|
||||
}
|
||||
const bounded = text.slice(0, available)
|
||||
parts.push(`${separator}${bounded}`)
|
||||
remaining -= separator.length + bounded.length
|
||||
append(boundedCandidate)
|
||||
return
|
||||
}
|
||||
if (!candidate || typeof candidate !== 'object') {
|
||||
@@ -113,10 +119,33 @@ export function safeToolErrorDetail(
|
||||
return
|
||||
}
|
||||
const record = candidate as Record<string, unknown>
|
||||
collect(record.message, depth + 1)
|
||||
for (const key of [
|
||||
'code',
|
||||
'errno',
|
||||
'syscall',
|
||||
'hostname',
|
||||
'address',
|
||||
'port',
|
||||
'status',
|
||||
'statusCode'
|
||||
]) {
|
||||
const metadata = record[key]
|
||||
if (
|
||||
typeof metadata === 'string' ||
|
||||
typeof metadata === 'number'
|
||||
) {
|
||||
append(`${key}: ${metadata}`)
|
||||
}
|
||||
}
|
||||
if (record.cause !== undefined) {
|
||||
append('cause:')
|
||||
collect(record.cause, depth + 1)
|
||||
}
|
||||
for (const key of [
|
||||
'content',
|
||||
'message',
|
||||
'error',
|
||||
'errors',
|
||||
'stderr',
|
||||
'detail',
|
||||
'data'
|
||||
@@ -152,3 +181,23 @@ export function safeToolArgumentSummary(
|
||||
redactValue(toolArguments, new WeakSet())
|
||||
).slice(0, maximum)
|
||||
}
|
||||
|
||||
export function boundedToolDetail(
|
||||
value: unknown,
|
||||
maximum: number
|
||||
): string | undefined {
|
||||
if (!Number.isSafeInteger(maximum) || maximum < 1 || value === undefined) {
|
||||
return undefined
|
||||
}
|
||||
let text: string | undefined
|
||||
if (typeof value === 'string') {
|
||||
text = value
|
||||
} else {
|
||||
try {
|
||||
text = JSON.stringify(value, null, 2)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
return text ? text.slice(0, maximum) : undefined
|
||||
}
|
||||
|
||||
@@ -876,7 +876,7 @@ describe('ContinueHostAdapter', () => {
|
||||
name: 'Bash',
|
||||
state: 'failed',
|
||||
error:
|
||||
'PowerShell parser failed Authorization: [REDACTED]'
|
||||
'PowerShell parser failed Authorization: Bearer secret-token'
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -940,7 +940,9 @@ describe('ContinueHostAdapter', () => {
|
||||
type: 'tool',
|
||||
callId: 'call-1',
|
||||
name: 'Bash',
|
||||
state: 'running'
|
||||
state: 'running',
|
||||
input:
|
||||
'{"command":"npm test","token":"secret-token"}'
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -973,7 +975,9 @@ describe('ContinueHostAdapter', () => {
|
||||
type: 'tool',
|
||||
callId: 'call-1',
|
||||
name: 'Bash',
|
||||
state: 'completed'
|
||||
state: 'completed',
|
||||
output:
|
||||
'Tests passed\nAuthorization: Bearer secret-token'
|
||||
},
|
||||
{ type: 'text', delta: 'TOOLS_OK' }
|
||||
]
|
||||
@@ -1012,7 +1016,11 @@ describe('ContinueHostAdapter', () => {
|
||||
{
|
||||
callId: 'call-1',
|
||||
name: 'Bash',
|
||||
state: 'completed'
|
||||
state: 'completed',
|
||||
input:
|
||||
'{"command":"npm test","token":"secret-token"}',
|
||||
output:
|
||||
'Tests passed\nAuthorization: Bearer secret-token'
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -1023,7 +1031,9 @@ describe('ContinueHostAdapter', () => {
|
||||
tool: {
|
||||
callId: 'call-1',
|
||||
name: 'Bash',
|
||||
state: 'running'
|
||||
state: 'running',
|
||||
input:
|
||||
'{"command":"npm test","token":"secret-token"}'
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1031,7 +1041,9 @@ describe('ContinueHostAdapter', () => {
|
||||
tool: {
|
||||
callId: 'call-1',
|
||||
name: 'Bash',
|
||||
state: 'completed'
|
||||
state: 'completed',
|
||||
output:
|
||||
'Tests passed\nAuthorization: Bearer secret-token'
|
||||
}
|
||||
},
|
||||
{ type: 'text', delta: 'TOOLS_OK' }
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
import { createAnthropicApiBaseUrl } from './anthropic-endpoint'
|
||||
import { createOpenAIApiBaseUrl } from './openai-endpoint'
|
||||
import {
|
||||
redactSensitiveText,
|
||||
boundedToolDetail,
|
||||
safeToolErrorDetail
|
||||
} from './approval-summary'
|
||||
|
||||
@@ -88,6 +88,8 @@ const continueHostStreamEventSchema = z.discriminatedUnion('type', [
|
||||
callId: z.string().min(1).max(256),
|
||||
name: z.string().min(1).max(200),
|
||||
state: z.enum(['running', 'completed', 'failed']),
|
||||
input: z.string().max(4_000).optional(),
|
||||
output: z.string().max(16_000).optional(),
|
||||
error: z.string().max(1_000).optional()
|
||||
})
|
||||
.strict()
|
||||
@@ -142,6 +144,8 @@ export type ContinueHostTool = {
|
||||
callId: string
|
||||
name: string
|
||||
state: 'pending' | 'running' | 'completed' | 'failed'
|
||||
input?: string
|
||||
output?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
@@ -383,7 +387,7 @@ function parseContinueFailure(text: string): string | undefined {
|
||||
: record.message
|
||||
const detail =
|
||||
typeof message === 'string' && message.trim()
|
||||
? `:${redactSensitiveText(message.trim()).slice(0, 500)}`
|
||||
? `:${message.trim().slice(0, 500)}`
|
||||
: ''
|
||||
return `Continue 模型请求失败${detail}`
|
||||
} catch {
|
||||
@@ -465,10 +469,23 @@ function extractContinueTools(
|
||||
normalizedState === 'failed'
|
||||
? normalizeContinueToolError(state.output)
|
||||
: undefined
|
||||
const input =
|
||||
toolFunction && typeof toolFunction === 'object'
|
||||
? boundedToolDetail(
|
||||
(toolFunction as Record<string, unknown>).arguments,
|
||||
4_000
|
||||
)
|
||||
: undefined
|
||||
const output =
|
||||
normalizedState === 'completed'
|
||||
? boundedToolDetail(state.output, 16_000)
|
||||
: undefined
|
||||
tools.set(callId, {
|
||||
callId,
|
||||
name: name.trim().slice(0, 200),
|
||||
state: normalizedState,
|
||||
...(input ? { input } : {}),
|
||||
...(output ? { output } : {}),
|
||||
...(error ? { error } : {})
|
||||
})
|
||||
}
|
||||
@@ -482,7 +499,14 @@ function mergeContinueTools(
|
||||
): ContinueHostTool[] {
|
||||
const tools = new Map(current.map((tool) => [tool.callId, tool]))
|
||||
for (const tool of updates) {
|
||||
tools.set(tool.callId, tool)
|
||||
const previous = tools.get(tool.callId)
|
||||
tools.set(tool.callId, {
|
||||
...previous,
|
||||
...tool,
|
||||
input: tool.input ?? previous?.input,
|
||||
output: tool.output ?? previous?.output,
|
||||
error: tool.error ?? previous?.error
|
||||
})
|
||||
}
|
||||
return [...tools.values()]
|
||||
}
|
||||
@@ -661,7 +685,7 @@ export class ContinueHostAdapter {
|
||||
patched = replaceExactly(
|
||||
patched,
|
||||
streamCallbacksMarker,
|
||||
'a={onContent:u=>{u&&e.goodbuddyEvents.length<5e3&&e.goodbuddyEvents.push({type:"text",delta:u})},onContentComplete:u=>{},onToolStart:(u,l,c)=>{c&&e.goodbuddyEvents.length<5e3&&e.goodbuddyEvents.push({type:"tool",callId:c,name:u,state:"running"})},onToolResult:(u,l,c,d)=>{d&&e.goodbuddyEvents.length<5e3&&e.goodbuddyEvents.push({type:"tool",callId:d,name:l,state:c==="done"?"completed":"failed"})},onToolError:(u,l,c)=>{c&&e.goodbuddyEvents.length<5e3&&e.goodbuddyEvents.push({type:"tool",callId:c,name:l??"unknown",state:"failed",error:String(u).slice(0,1e3)})},onToolPermissionRequest:'
|
||||
'a={onContent:u=>{u&&e.goodbuddyEvents.length<5e3&&e.goodbuddyEvents.push({type:"text",delta:u})},onContentComplete:u=>{},onToolStart:(u,l,c)=>{c&&e.goodbuddyEvents.length<5e3&&e.goodbuddyEvents.push({type:"tool",callId:c,name:u,state:"running",input:(()=>{try{return JSON.stringify(l).slice(0,4e3)}catch{return"[无法序列化]"}})()})},onToolResult:(u,l,c,d)=>{d&&e.goodbuddyEvents.length<5e3&&e.goodbuddyEvents.push({type:"tool",callId:d,name:l,state:c==="done"?"completed":"failed",output:String(u).slice(0,16e3)})},onToolError:(u,l,c)=>{c&&e.goodbuddyEvents.length<5e3&&e.goodbuddyEvents.push({type:"tool",callId:c,name:l??"unknown",state:"failed",error:String(u).slice(0,1e3)})},onToolPermissionRequest:'
|
||||
)
|
||||
patched = replaceExactly(
|
||||
patched,
|
||||
@@ -1112,6 +1136,12 @@ export class ContinueHostAdapter {
|
||||
callId: event.callId,
|
||||
name: event.name,
|
||||
state: event.state,
|
||||
...(event.input
|
||||
? { input: boundedToolDetail(event.input, 4_000) }
|
||||
: {}),
|
||||
...(event.output
|
||||
? { output: boundedToolDetail(event.output, 16_000) }
|
||||
: {}),
|
||||
...(event.error
|
||||
? { error: normalizeContinueToolError(event.error) }
|
||||
: {})
|
||||
@@ -1143,7 +1173,8 @@ export class ContinueHostAdapter {
|
||||
{
|
||||
callId: pendingCallId,
|
||||
name: pending.toolName,
|
||||
state: 'pending'
|
||||
state: 'pending',
|
||||
input: boundedToolDetail(pending.toolArgs, 4_000)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -384,7 +384,13 @@ describe('ContinueAgentRuntime', () => {
|
||||
mocks.runHost.mockResolvedValue({
|
||||
text: 'Continue response',
|
||||
tools: [
|
||||
{ callId: 'call-1', name: 'Bash', state: 'completed' },
|
||||
{
|
||||
callId: 'call-1',
|
||||
name: 'Bash',
|
||||
state: 'completed',
|
||||
input: '{"command":"npm test"}',
|
||||
output: 'Tests passed'
|
||||
},
|
||||
{ callId: 'call-2', name: 'Write', state: 'completed' }
|
||||
]
|
||||
})
|
||||
@@ -396,7 +402,9 @@ describe('ContinueAgentRuntime', () => {
|
||||
type: 'tool',
|
||||
name: 'Bash',
|
||||
state: 'completed',
|
||||
summary: 'Continue 工具:Bash'
|
||||
summary: 'Continue 工具:Bash',
|
||||
input: '{"command":"npm test"}',
|
||||
output: 'Tests passed'
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: 'tool',
|
||||
|
||||
@@ -72,6 +72,8 @@ function toContinueToolEvent(
|
||||
? 'failed'
|
||||
: tool.state,
|
||||
summary: `Continue 工具:${tool.name}`,
|
||||
...(tool.input ? { input: tool.input } : {}),
|
||||
...(tool.output ? { output: tool.output } : {}),
|
||||
...(tool.error ? { error: tool.error } : {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +286,7 @@ describe('ModelAgentRuntime', () => {
|
||||
await expect(consume()).rejects.toThrow('意外中断')
|
||||
})
|
||||
|
||||
it('redacts credentials from provider error messages', async () => {
|
||||
it('preserves bounded provider error messages', async () => {
|
||||
const runtime = new ModelAgentRuntime({
|
||||
apiKey: 'test-key',
|
||||
baseUrl: 'https://bigtoken.ai',
|
||||
@@ -319,7 +319,7 @@ describe('ModelAgentRuntime', () => {
|
||||
}
|
||||
|
||||
await expect(consume()).rejects.toThrow(
|
||||
'upstream failed Authorization: [REDACTED]'
|
||||
'upstream failed Authorization: Bearer secret-token'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -702,6 +702,15 @@ describe('ModelAgentRuntime', () => {
|
||||
.filter((event) => event.type === 'tool')
|
||||
.map((event) => event.state)
|
||||
).toEqual(['pending', 'running', 'completed'])
|
||||
expect(events).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: 'tool',
|
||||
state: 'completed',
|
||||
input: '{\n "path": "README.md"\n}',
|
||||
output:
|
||||
'tool result\n\n[图片结果 1:image/png]'
|
||||
})
|
||||
)
|
||||
expect(events).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: 'text',
|
||||
@@ -1693,7 +1702,7 @@ describe('ModelAgentRuntime', () => {
|
||||
'x-request-id': 'image-request-502'
|
||||
},
|
||||
expected:
|
||||
'upstream unavailable Authorization: [REDACTED](HTTP 502,请求 ID image-request-502)'
|
||||
'upstream unavailable Authorization: Bearer secret-token(HTTP 502,请求 ID image-request-502)'
|
||||
},
|
||||
{
|
||||
body: '<html>Bad Gateway</html>',
|
||||
|
||||
@@ -31,8 +31,8 @@ import type {
|
||||
RuntimeModelUsageEvent
|
||||
} from './runtime'
|
||||
import {
|
||||
redactSensitiveText,
|
||||
safeToolArgumentSummary
|
||||
boundedToolDetail,
|
||||
safeToolErrorDetail
|
||||
} from './approval-summary'
|
||||
|
||||
type ConversationMessage = {
|
||||
@@ -121,7 +121,7 @@ function getErrorMessage(value: unknown): string | undefined {
|
||||
}
|
||||
const error = 'error' in value ? value.error : undefined
|
||||
if (typeof error === 'string') {
|
||||
return redactSensitiveText(error).slice(0, 1_000)
|
||||
return error.slice(0, 1_000)
|
||||
}
|
||||
if (
|
||||
error &&
|
||||
@@ -129,13 +129,13 @@ function getErrorMessage(value: unknown): string | undefined {
|
||||
'message' in error &&
|
||||
typeof error.message === 'string'
|
||||
) {
|
||||
return redactSensitiveText(error.message).slice(0, 1_000)
|
||||
return error.message.slice(0, 1_000)
|
||||
}
|
||||
if (
|
||||
'message' in value &&
|
||||
typeof value.message === 'string'
|
||||
) {
|
||||
return redactSensitiveText(value.message).slice(0, 1_000)
|
||||
return value.message.slice(0, 1_000)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@@ -624,14 +624,29 @@ function getChatToolResultText(parts: ModelToolResultPart[]): string {
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
function getToolResultPreview(parts: ModelToolResultPart[]): string {
|
||||
let imageNumber = 0
|
||||
return parts
|
||||
.map((part) => {
|
||||
if (part.type === 'text') {
|
||||
return part.text
|
||||
}
|
||||
imageNumber += 1
|
||||
return `[图片结果 ${imageNumber}:${part.mimeType}]`
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
.slice(0, 16_000)
|
||||
}
|
||||
|
||||
function createRecoverableToolErrorResult(
|
||||
error: RecoverableModelToolError
|
||||
): ModelToolResult {
|
||||
const text = JSON.stringify({
|
||||
ok: false,
|
||||
recoverable: true,
|
||||
error: redactSensitiveText(error.message).slice(0, 1_000),
|
||||
nextAction: redactSensitiveText(error.nextAction).slice(0, 1_000)
|
||||
error: error.message.slice(0, 1_000),
|
||||
nextAction: error.nextAction.slice(0, 1_000)
|
||||
})
|
||||
return {
|
||||
parts: [{ type: 'text', text }],
|
||||
@@ -1250,7 +1265,7 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
providerMessage?.includes('模型接口请求失败')
|
||||
? '上游图像服务暂时不可用,请稍后重试或联系服务商'
|
||||
: providerMessage
|
||||
? redactSensitiveText(providerMessage).slice(0, 1_000)
|
||||
? providerMessage.slice(0, 1_000)
|
||||
: '图像生成请求失败'
|
||||
throw new Error(
|
||||
`${publicMessage}(HTTP ${response.status}${
|
||||
@@ -1546,13 +1561,15 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
seenCallIds.add(call.id)
|
||||
const tool = toolsByName.get(call.name)
|
||||
const displayName = tool?.displayName ?? call.name.slice(0, 128)
|
||||
const input = boundedToolDetail(call.arguments, 4_000)
|
||||
yield {
|
||||
requestId: request.requestId,
|
||||
type: 'tool',
|
||||
callId: call.id,
|
||||
name: displayName,
|
||||
state: 'pending',
|
||||
summary: `直连模型工具:${displayName}`
|
||||
summary: `直连模型工具:${displayName}`,
|
||||
input
|
||||
}
|
||||
if (!tool) {
|
||||
yield {
|
||||
@@ -1561,7 +1578,8 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
callId: call.id,
|
||||
name: displayName,
|
||||
state: 'failed',
|
||||
summary: `直连模型请求了未知工具:${displayName}`
|
||||
summary: `直连模型请求了未知工具:${displayName}`,
|
||||
input
|
||||
}
|
||||
throw new Error(`模型请求了未知工具「${displayName}」`)
|
||||
}
|
||||
@@ -1581,19 +1599,22 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
this.toolProvider.getApproval(
|
||||
tool,
|
||||
call.arguments,
|
||||
safeToolArgumentSummary(call.arguments),
|
||||
boundedToolDetail(call.arguments, 1_000) ?? '',
|
||||
toolContext
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
const detail = safeToolErrorDetail(error)
|
||||
yield {
|
||||
requestId: request.requestId,
|
||||
type: 'tool',
|
||||
callId: call.id,
|
||||
name: displayName,
|
||||
state: 'failed',
|
||||
summary: `直连模型工具审批失败:${displayName}`
|
||||
summary: `直连模型工具审批失败:${displayName}`,
|
||||
input,
|
||||
...(detail ? { error: detail } : {})
|
||||
}
|
||||
throw error
|
||||
}
|
||||
@@ -1604,7 +1625,8 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
callId: call.id,
|
||||
name: displayName,
|
||||
state: 'failed',
|
||||
summary: `用户拒绝了直连模型工具:${displayName}`
|
||||
summary: `用户拒绝了直连模型工具:${displayName}`,
|
||||
input
|
||||
}
|
||||
throw new Error(`用户拒绝了工具「${displayName}」`)
|
||||
}
|
||||
@@ -1615,7 +1637,8 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
callId: call.id,
|
||||
name: displayName,
|
||||
state: 'running',
|
||||
summary: `正在执行直连模型工具:${displayName}`
|
||||
summary: `正在执行直连模型工具:${displayName}`,
|
||||
input
|
||||
}
|
||||
|
||||
let result: ModelToolResult
|
||||
@@ -1629,6 +1652,7 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
)
|
||||
} catch (error) {
|
||||
const recoverable = error instanceof RecoverableModelToolError
|
||||
const detail = safeToolErrorDetail(error)
|
||||
yield {
|
||||
requestId: request.requestId,
|
||||
type: 'tool',
|
||||
@@ -1638,7 +1662,9 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
summary:
|
||||
recoverable
|
||||
? `直连模型工具需要刷新后重试:${displayName}`
|
||||
: `直连模型工具执行失败:${displayName}`
|
||||
: `直连模型工具执行失败:${displayName}`,
|
||||
input,
|
||||
...(detail ? { error: detail } : {})
|
||||
}
|
||||
if (recoverable) {
|
||||
result = createRecoverableToolErrorResult(error)
|
||||
@@ -1657,7 +1683,8 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
callId: call.id,
|
||||
name: displayName,
|
||||
state: 'failed',
|
||||
summary: `直连模型工具结果超过限制:${displayName}`
|
||||
summary: `直连模型工具结果超过限制:${displayName}`,
|
||||
input
|
||||
}
|
||||
throw new Error('直连模型工具结果总量超过 1MB 安全限制')
|
||||
}
|
||||
@@ -1691,7 +1718,9 @@ export class ModelAgentRuntime implements AgentRuntime {
|
||||
callId: call.id,
|
||||
name: displayName,
|
||||
state: 'completed',
|
||||
summary: `直连模型工具已完成:${displayName}`
|
||||
summary: `直连模型工具已完成:${displayName}`,
|
||||
input,
|
||||
output: getToolResultPreview(result.parts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,14 @@ function completedToolEvent(
|
||||
callID: callId,
|
||||
type: 'tool',
|
||||
tool,
|
||||
state: { status: 'completed' }
|
||||
state: {
|
||||
status: 'completed',
|
||||
input: {
|
||||
command: 'npm test',
|
||||
token: 'visible-token'
|
||||
},
|
||||
output: 'Tests passed\nAuthorization: Bearer secret-token'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1503,7 +1510,11 @@ describe('OpenCodeRuntime embedded permission mediation', () => {
|
||||
expect.objectContaining({
|
||||
type: 'tool',
|
||||
callId: 'call-1',
|
||||
state: 'completed'
|
||||
state: 'completed',
|
||||
input:
|
||||
'{\n "command": "npm test",\n "token": "visible-token"\n}',
|
||||
output:
|
||||
'Tests passed\nAuthorization: Bearer secret-token'
|
||||
})
|
||||
)
|
||||
expect(events).toContainEqual(
|
||||
@@ -1631,11 +1642,12 @@ describe('OpenCodeRuntime embedded permission mediation', () => {
|
||||
type: 'tool',
|
||||
callId: 'call-1',
|
||||
state: 'failed',
|
||||
error: 'write failed Authorization: [REDACTED]'
|
||||
error:
|
||||
'write failed Authorization: Bearer secret-token'
|
||||
}
|
||||
})
|
||||
await expect(stream.next()).rejects.toThrow(
|
||||
'write failed Authorization: [REDACTED]'
|
||||
'write failed Authorization: Bearer secret-token'
|
||||
)
|
||||
expect(session.abort).toHaveBeenCalledOnce()
|
||||
await runtime.dispose()
|
||||
@@ -1661,7 +1673,7 @@ describe('OpenCodeRuntime embedded permission mediation', () => {
|
||||
const runtime = embeddedRuntime(client)
|
||||
|
||||
await expect(collectRun(runtime)).rejects.toThrow(
|
||||
'prompt rejected Authorization: [REDACTED]'
|
||||
'prompt rejected Authorization: Bearer secret-token'
|
||||
)
|
||||
await runtime.dispose()
|
||||
})
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
type RuntimeSandboxResolution
|
||||
} from './runtime-sandbox'
|
||||
import {
|
||||
boundedToolDetail,
|
||||
safeToolErrorDetail
|
||||
} from './approval-summary'
|
||||
|
||||
@@ -978,6 +979,8 @@ export class OpenCodeRuntime implements AgentRuntime {
|
||||
{
|
||||
name: string
|
||||
state: 'pending' | 'running' | 'completed' | 'failed'
|
||||
input?: string
|
||||
output?: string
|
||||
error?: string
|
||||
}
|
||||
>()
|
||||
@@ -1077,9 +1080,19 @@ export class OpenCodeRuntime implements AgentRuntime {
|
||||
part.state.status === 'error'
|
||||
? safeToolErrorDetail(part.state.error)
|
||||
: undefined
|
||||
const input = isRecord(part.state.input)
|
||||
? boundedToolDetail(part.state.input, 4_000)
|
||||
: undefined
|
||||
const output =
|
||||
part.state.status === 'completed' &&
|
||||
typeof part.state.output === 'string'
|
||||
? part.state.output.slice(0, 16_000)
|
||||
: undefined
|
||||
toolStates.set(callId, {
|
||||
name: toolName,
|
||||
state,
|
||||
...(input ? { input } : {}),
|
||||
...(output ? { output } : {}),
|
||||
...(error ? { error } : {})
|
||||
})
|
||||
yield {
|
||||
@@ -1089,6 +1102,8 @@ export class OpenCodeRuntime implements AgentRuntime {
|
||||
name: toolName,
|
||||
state,
|
||||
summary: `OpenCode 工具:${toolName}`,
|
||||
...(input ? { input } : {}),
|
||||
...(output ? { output } : {}),
|
||||
...(error ? { error } : {})
|
||||
}
|
||||
}
|
||||
@@ -1309,6 +1324,8 @@ export class OpenCodeRuntime implements AgentRuntime {
|
||||
name: tool.name,
|
||||
state: 'failed',
|
||||
summary: `OpenCode 工具:${tool.name}`,
|
||||
...(tool.input ? { input: tool.input } : {}),
|
||||
...(tool.output ? { output: tool.output } : {}),
|
||||
...(tool.error ? { error: tool.error } : {})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user