ethagent 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/package.json +2 -1
- package/src/app/FirstRun.tsx +3 -7
- package/src/app/FirstRunTimeline.tsx +1 -1
- package/src/chat/ChatBottomPane.tsx +29 -11
- package/src/chat/ChatScreen.tsx +169 -38
- package/src/chat/ConversationStack.tsx +1 -1
- package/src/chat/MessageList.tsx +185 -72
- package/src/chat/SessionStatus.tsx +3 -1
- package/src/chat/chatScreenUtils.ts +11 -15
- package/src/chat/chatSessionState.ts +5 -2
- package/src/chat/chatTurnOrchestrator.ts +7 -9
- package/src/chat/commands.ts +26 -26
- package/src/chat/display/DiffView.tsx +193 -0
- package/src/chat/display/SyntaxText.tsx +192 -0
- package/src/chat/display/toolCallDisplay.ts +103 -0
- package/src/chat/display/toolResultDisplay.ts +19 -0
- package/src/chat/{ChatInput.tsx → input/ChatInput.tsx} +61 -25
- package/src/chat/input/imageRefs.ts +30 -0
- package/src/chat/{TranscriptView.tsx → transcript/TranscriptView.tsx} +24 -50
- package/src/chat/{transcriptViewport.ts → transcript/transcriptViewport.ts} +12 -30
- package/src/chat/{ContextLimitView.tsx → views/ContextLimitView.tsx} +3 -3
- package/src/chat/{ContinuityEditReviewView.tsx → views/ContinuityEditReviewView.tsx} +11 -3
- package/src/chat/{CopyPicker.tsx → views/CopyPicker.tsx} +4 -5
- package/src/chat/{PermissionPrompt.tsx → views/PermissionPrompt.tsx} +16 -17
- package/src/chat/{PermissionsView.tsx → views/PermissionsView.tsx} +6 -6
- package/src/chat/{PlanApprovalView.tsx → views/PlanApprovalView.tsx} +4 -4
- package/src/chat/{ResumeView.tsx → views/ResumeView.tsx} +50 -41
- package/src/chat/views/RewindView.tsx +410 -0
- package/src/identity/continuity/privateEdit/diff.ts +2 -78
- package/src/identity/hub/OperationalRoutes.tsx +21 -21
- package/src/identity/hub/Routes.tsx +13 -13
- package/src/identity/hub/{flows/continuity → continuity}/ContinuityDashboardScreen.tsx +9 -9
- package/src/identity/hub/{flows/continuity → continuity}/RebackupStorageScreen.tsx +2 -2
- package/src/identity/hub/{flows/continuity → continuity}/RecoveryConfirmScreen.tsx +5 -5
- package/src/identity/hub/{flows/continuity → continuity}/SavePromptScreen.tsx +5 -5
- package/src/identity/hub/{effects/rebackup/runRebackup.ts → continuity/effects.ts} +17 -17
- package/src/identity/hub/{effects/rebackup → continuity}/index.ts +1 -1
- package/src/identity/hub/{effects/shared → continuity}/snapshot.ts +8 -8
- package/src/identity/hub/{effects/rebackup → continuity}/vault.ts +15 -15
- package/src/identity/hub/{flows/create → create}/CreateFlow.tsx +13 -13
- package/src/identity/hub/{effects/create.ts → create/effects.ts} +4 -4
- package/src/identity/hub/{flows/custody → custody}/CustodyEditFlow.tsx +9 -9
- package/src/identity/hub/{flows/custody/custodyFlowActions.ts → custody/actions.ts} +6 -6
- package/src/identity/hub/{flows/custody/custodyFlowHelpers.ts → custody/helpers.ts} +4 -4
- package/src/identity/hub/{effects/vault → custody}/preflight.ts +5 -5
- package/src/identity/hub/{flows/custody/custodyFlowRoutes.tsx → custody/routes.tsx} +8 -8
- package/src/identity/hub/{flows/custody/custodyEffects.ts → custody/transactions.ts} +9 -9
- package/src/identity/hub/{flows/custody/custodyFlowTypes.ts → custody/types.ts} +5 -5
- package/src/identity/hub/{flows/custody/custodyFlowEffects.ts → custody/useCustodyEffects.ts} +7 -7
- package/src/identity/hub/{flows/custody → custody}/useCustodyFlow.tsx +5 -5
- package/src/identity/hub/{flows/ens → ens}/EnsEditAdvancedScreens.tsx +13 -13
- package/src/identity/hub/{flows/ens → ens}/EnsEditFlow.tsx +7 -7
- package/src/identity/hub/{flows/ens → ens}/EnsEditMaintenanceScreens.tsx +10 -10
- package/src/identity/hub/{flows/ens → ens}/EnsEditReviewScreens.tsx +12 -12
- package/src/identity/hub/{flows/ens → ens}/EnsEditRunners.tsx +5 -5
- package/src/identity/hub/{flows/ens → ens}/EnsEditShared.tsx +10 -10
- package/src/identity/hub/{flows/ens → ens}/EnsEditSimpleScreens.tsx +14 -14
- package/src/identity/hub/{flows/ens/IdentityHubEnsFlow.tsx → ens/EnsFlow.tsx} +12 -12
- package/src/identity/hub/{flows/ens/OperatorWalletsScreen.tsx → ens/EnsOperatorWalletsScreen.tsx} +17 -17
- package/src/identity/hub/{advancedEnsValidation.ts → ens/advancedEnsValidation.ts} +2 -2
- package/src/identity/hub/{flows/ens/ensEditCopy.ts → ens/editCopy.ts} +3 -3
- package/src/identity/hub/{effects/ens/flows.ts → ens/effects.ts} +7 -7
- package/src/identity/hub/{effects/ens → ens}/index.ts +1 -1
- package/src/identity/hub/{model/ens.ts → ens/state.ts} +1 -1
- package/src/identity/hub/{effects/ens → ens}/transactions.ts +239 -239
- package/src/identity/hub/{flows/ens/ensEditTypes.ts → ens/types.ts} +7 -7
- package/src/identity/hub/identityHubReducer.ts +3 -3
- package/src/identity/hub/{flows/profile → profile}/EditProfileFlow.tsx +11 -11
- package/src/identity/hub/{effects/publicProfile/runPublicProfileSave.ts → profile/effects.ts} +18 -18
- package/src/identity/hub/{model → profile}/identity.ts +3 -3
- package/src/identity/hub/{effects/profile/profileState.ts → profile/state.ts} +181 -181
- package/src/identity/hub/{flows/restore → restore}/RestoreFlow.tsx +16 -16
- package/src/identity/hub/{effects/restore → restore}/apply.ts +10 -10
- package/src/identity/hub/{effects/restore → restore}/auth.ts +7 -7
- package/src/identity/hub/{effects/restore → restore}/discover.ts +6 -6
- package/src/identity/hub/{effects/restore → restore}/envelopes.ts +2 -2
- package/src/identity/hub/{effects/restore → restore}/fetch.ts +3 -3
- package/src/identity/hub/{effects/restore/shared.ts → restore/helpers.ts} +6 -6
- package/src/identity/hub/{effects/restore → restore}/recovery.ts +10 -10
- package/src/identity/hub/{effects/restore → restore}/resolve.ts +4 -4
- package/src/identity/hub/{effects → restore}/restoreAdmin.ts +1 -1
- package/src/identity/hub/{flows/restore/useRestoreFlowEffects.ts → restore/useRestoreEffects.ts} +5 -5
- package/src/identity/hub/{flows/settings → settings}/StorageCredentialScreen.tsx +5 -5
- package/src/identity/hub/{components → shared/components}/BusyScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/DetailsScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/ErrorScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/FlowTimeline.tsx +1 -1
- package/src/identity/hub/{components → shared/components}/IdentitySummary.tsx +8 -8
- package/src/identity/hub/{components → shared/components}/MenuScreen.tsx +7 -7
- package/src/identity/hub/{components → shared/components}/NetworkScreen.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/PinataJwtInput.tsx +4 -4
- package/src/identity/hub/{components → shared/components}/UnlinkedIdentityScreen.tsx +5 -5
- package/src/identity/hub/{components → shared/components}/WalletApprovalScreen.tsx +6 -6
- package/src/identity/hub/{components → shared/components}/menuFlagsFromReconciliation.ts +1 -1
- package/src/identity/hub/{effects/shared → shared/effects}/profilePrep.ts +1 -1
- package/src/identity/hub/{effects → shared/effects}/receipts.ts +2 -2
- package/src/identity/hub/{effects/shared → shared/effects}/sync.ts +4 -4
- package/src/identity/hub/{effects → shared/effects}/types.ts +3 -3
- package/src/identity/hub/{model → shared/model}/copy.ts +2 -2
- package/src/identity/hub/{model → shared/model}/errors.ts +5 -5
- package/src/identity/hub/{model → shared/model}/network.ts +3 -3
- package/src/identity/hub/{operatorWallets.ts → shared/operatorWallets.ts} +1 -1
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/hook.ts +1 -1
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/ownership.ts +2 -2
- package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/run.ts +6 -6
- package/src/identity/hub/{utils.ts → shared/utils.ts} +5 -5
- package/src/identity/hub/{flows/token-transfer/IdentityHubTokenTransferFlow.tsx → transfer/TokenTransferFlow.tsx} +8 -8
- package/src/identity/hub/{flows/token-transfer → transfer}/TokenTransferScreens.tsx +14 -14
- package/src/identity/hub/{effects/token-transfer/runTokenTransfer.ts → transfer/effects.ts} +16 -16
- package/src/identity/hub/{effects/token-transfer → transfer}/progress.ts +1 -1
- package/src/identity/hub/useIdentityHubController.ts +11 -11
- package/src/identity/hub/useIdentityHubSideEffects.ts +11 -11
- package/src/models/ModelPicker.tsx +143 -9
- package/src/models/catalog.ts +2 -1
- package/src/models/huggingface.ts +180 -2
- package/src/models/llamacpp.ts +110 -15
- package/src/models/llamacppPreflight.ts +30 -11
- package/src/models/modelPickerOptions.ts +16 -15
- package/src/models/providerDisplay.ts +16 -0
- package/src/providers/anthropic.ts +36 -5
- package/src/providers/contracts.ts +9 -1
- package/src/providers/errors.ts +6 -4
- package/src/providers/gemini.ts +29 -3
- package/src/providers/openai-chat.ts +83 -3
- package/src/providers/openai-responses-format.ts +29 -8
- package/src/providers/openai-responses.ts +22 -7
- package/src/providers/registry.ts +1 -0
- package/src/runtime/sessionMode.ts +1 -1
- package/src/runtime/systemPrompt.ts +3 -1
- package/src/runtime/toolExecution.ts +9 -6
- package/src/runtime/turn.ts +29 -0
- package/src/storage/config.ts +1 -0
- package/src/storage/rewind.ts +20 -0
- package/src/storage/sessions.ts +16 -3
- package/src/tools/bashSafety.ts +7 -3
- package/src/tools/bashTool.ts +1 -1
- package/src/tools/contracts.ts +3 -0
- package/src/tools/deleteFileTool.ts +8 -3
- package/src/tools/editTool.ts +10 -5
- package/src/tools/fileDiff.ts +261 -0
- package/src/tools/privateContinuityEditTool.ts +5 -1
- package/src/tools/writeFileTool.ts +8 -3
- package/src/ui/Spinner.tsx +39 -5
- package/src/ui/TextInput.tsx +2 -2
- package/src/ui/theme.ts +19 -0
- package/src/utils/clipboard.ts +10 -7
- package/src/utils/images.ts +140 -0
- package/src/utils/messages.ts +2 -0
- package/src/chat/RewindView.tsx +0 -386
- package/src/chat/toolResultDisplay.ts +0 -8
- package/src/identity/hub/effects/index.ts +0 -73
- package/src/identity/hub/effects/publicProfile/index.ts +0 -5
- package/src/identity/hub/effects/restore/restoreEffects.ts +0 -22
- package/src/identity/hub/effects/token-transfer/index.ts +0 -6
- /package/src/chat/{chatInputState.ts → input/chatInputState.ts} +0 -0
- /package/src/chat/{chatPaste.ts → input/chatPaste.ts} +0 -0
- /package/src/chat/{textCursor.ts → input/textCursor.ts} +0 -0
- /package/src/identity/hub/{model/continuity.ts → continuity/state.ts} +0 -0
- /package/src/identity/hub/{model/custody.ts → custody/state.ts} +0 -0
- /package/src/identity/hub/{effects/restore → restore}/index.ts +0 -0
- /package/src/identity/hub/{model → shared/model}/format.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/agentReconciliation/types.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/index.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/useAgentReconciliation.ts +0 -0
- /package/src/identity/hub/{reconciliation → shared/reconciliation}/walletSetup.ts +0 -0
- /package/src/identity/hub/{txGuard.ts → shared/txGuard.ts} +0 -0
- /package/src/identity/hub/{model/transfer.ts → transfer/state.ts} +0 -0
|
@@ -5,6 +5,8 @@ import { providerErrorFromResponse } from './errors.js'
|
|
|
5
5
|
import { fetchWithRetryStreamEvents } from './retry.js'
|
|
6
6
|
import { iterSseFrames } from './sse.js'
|
|
7
7
|
import { messageTextContent } from '../utils/messages.js'
|
|
8
|
+
import { hasImageBlocks, ImageLoadError, loadImageBlock } from '../utils/images.js'
|
|
9
|
+
import { providerDisplayName } from '../models/providerDisplay.js'
|
|
8
10
|
|
|
9
11
|
export type OpenAIToolDefinition = {
|
|
10
12
|
type: 'function'
|
|
@@ -27,6 +29,7 @@ type Options = {
|
|
|
27
29
|
loadApiKey?: () => Promise<string | null>
|
|
28
30
|
tools?: OpenAIToolDefinition[]
|
|
29
31
|
maxRetries?: number
|
|
32
|
+
hasVisionProjector?: boolean
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
type ChatChunk = {
|
|
@@ -75,6 +78,7 @@ export class OpenAIChatProvider implements Provider {
|
|
|
75
78
|
private readonly loadApiKey?: () => Promise<string | null>
|
|
76
79
|
private readonly tools: OpenAIToolDefinition[]
|
|
77
80
|
private readonly maxRetries?: number
|
|
81
|
+
private readonly hasVisionProjector: boolean
|
|
78
82
|
|
|
79
83
|
constructor(opts: Options) {
|
|
80
84
|
this.id = opts.id
|
|
@@ -85,6 +89,7 @@ export class OpenAIChatProvider implements Provider {
|
|
|
85
89
|
this.tools = opts.tools ?? []
|
|
86
90
|
this.maxRetries = opts.maxRetries
|
|
87
91
|
this.supportsTools = this.tools.length > 0
|
|
92
|
+
this.hasVisionProjector = opts.hasVisionProjector ?? false
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
async *complete(
|
|
@@ -98,6 +103,19 @@ export class OpenAIChatProvider implements Provider {
|
|
|
98
103
|
yield { type: 'error', message: error.message }
|
|
99
104
|
return
|
|
100
105
|
}
|
|
106
|
+
if (hasImageBlocks(messages)) {
|
|
107
|
+
if (this.id === 'llamacpp' && !this.hasVisionProjector) {
|
|
108
|
+
const hint = localModelNameHintsVision(this.model)
|
|
109
|
+
? '; open alt+p and run "Add Vision Encoder" on this model to enable image input'
|
|
110
|
+
: ''
|
|
111
|
+
yield { type: 'error', message: `image input is not enabled for local model "${this.model}" (no vision projector loaded)${hint}` }
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
if (this.id === 'openai' && !supportsOpenAIImages(this.model)) {
|
|
115
|
+
yield { type: 'error', message: `image input is not enabled for ${this.model}` }
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
}
|
|
101
119
|
|
|
102
120
|
const headers: Record<string, string> = {
|
|
103
121
|
'Content-Type': 'application/json',
|
|
@@ -105,6 +123,17 @@ export class OpenAIChatProvider implements Provider {
|
|
|
105
123
|
}
|
|
106
124
|
if (apiKey) headers.Authorization = `Bearer ${apiKey}`
|
|
107
125
|
|
|
126
|
+
let wireMessages: Array<Record<string, unknown>>
|
|
127
|
+
try {
|
|
128
|
+
wireMessages = await toWireMessages(messages)
|
|
129
|
+
} catch (err: unknown) {
|
|
130
|
+
if (err instanceof ImageLoadError) {
|
|
131
|
+
yield { type: 'error', message: err.message }
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
throw err
|
|
135
|
+
}
|
|
136
|
+
|
|
108
137
|
let response: Response
|
|
109
138
|
try {
|
|
110
139
|
response = yield* fetchWithRetryStreamEvents(`${this.baseUrl}/chat/completions`, {
|
|
@@ -112,7 +141,7 @@ export class OpenAIChatProvider implements Provider {
|
|
|
112
141
|
headers,
|
|
113
142
|
body: JSON.stringify({
|
|
114
143
|
model: this.model,
|
|
115
|
-
messages:
|
|
144
|
+
messages: wireMessages,
|
|
116
145
|
tools: this.tools.length > 0 ? this.tools : undefined,
|
|
117
146
|
tool_choice: this.tools.length > 0 ? 'auto' : undefined,
|
|
118
147
|
stream: true,
|
|
@@ -220,7 +249,7 @@ export class OpenAIChatProvider implements Provider {
|
|
|
220
249
|
|
|
221
250
|
}
|
|
222
251
|
|
|
223
|
-
export function toWireMessages(messages: Message[]): Array<Record<string, unknown
|
|
252
|
+
export async function toWireMessages(messages: Message[]): Promise<Array<Record<string, unknown>>> {
|
|
224
253
|
const out: Array<Record<string, unknown>> = []
|
|
225
254
|
|
|
226
255
|
for (const message of messages) {
|
|
@@ -229,6 +258,26 @@ export function toWireMessages(messages: Message[]): Array<Record<string, unknow
|
|
|
229
258
|
continue
|
|
230
259
|
}
|
|
231
260
|
|
|
261
|
+
if (message.role === 'user') {
|
|
262
|
+
const toolResults = message.content.filter(isToolResultBlock)
|
|
263
|
+
if (toolResults.length > 0) {
|
|
264
|
+
for (const block of toolResults) {
|
|
265
|
+
out.push({
|
|
266
|
+
role: 'tool',
|
|
267
|
+
tool_call_id: block.toolUseId,
|
|
268
|
+
content: block.content,
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
const nonToolBlocks = message.content.filter(block => block.type !== 'tool_result')
|
|
272
|
+
if (nonToolBlocks.length > 0) {
|
|
273
|
+
out.push({ role: 'user', content: await toOpenAIUserContent(nonToolBlocks) })
|
|
274
|
+
}
|
|
275
|
+
continue
|
|
276
|
+
}
|
|
277
|
+
out.push({ role: 'user', content: await toOpenAIUserContent(message.content) })
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
|
|
232
281
|
if (message.role === 'assistant') {
|
|
233
282
|
const textParts = message.content.filter(isTextBlock).map(block => block.text)
|
|
234
283
|
const toolCalls = message.content.filter(isToolUseBlock).map(block => ({
|
|
@@ -265,6 +314,37 @@ export function toWireMessages(messages: Message[]): Array<Record<string, unknow
|
|
|
265
314
|
return normalizeSystemMessages(out)
|
|
266
315
|
}
|
|
267
316
|
|
|
317
|
+
async function toOpenAIUserContent(blocks: MessageContentBlock[]): Promise<Array<Record<string, unknown>>> {
|
|
318
|
+
const parts: Array<Record<string, unknown>> = []
|
|
319
|
+
for (const block of blocks) {
|
|
320
|
+
if (block.type === 'text') {
|
|
321
|
+
if (block.text.length > 0) parts.push({ type: 'text', text: block.text })
|
|
322
|
+
continue
|
|
323
|
+
}
|
|
324
|
+
if (block.type === 'image') {
|
|
325
|
+
const loaded = await loadImageBlock(block)
|
|
326
|
+
if (loaded.url) {
|
|
327
|
+
parts.push({ type: 'image_url', image_url: { url: loaded.url } })
|
|
328
|
+
} else if (loaded.dataBase64 && loaded.mimeType) {
|
|
329
|
+
parts.push({ type: 'image_url', image_url: { url: `data:${loaded.mimeType};base64,${loaded.dataBase64}` } })
|
|
330
|
+
}
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return parts.length > 0 ? parts : [{ type: 'text', text: '' }]
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function supportsOpenAIImages(model: string): boolean {
|
|
338
|
+
const normalized = model.toLowerCase()
|
|
339
|
+
if (normalized.includes('gpt-3.5')) return false
|
|
340
|
+
return /gpt-4o|gpt-4\.1|gpt-4-turbo|gpt-4-vision|gpt-5|o1|o3|o4|chatgpt-4/.test(normalized)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function localModelNameHintsVision(model: string): boolean {
|
|
344
|
+
const normalized = model.toLowerCase()
|
|
345
|
+
return /llava|bakllava|qwen[-_.]?vl|qwen2[-_.]?vl|qwen2\.5[-_.]?vl|minicpm-?v|llama-3\.2.*vision|mllama|cogvlm|internvl|moondream|pixtral|phi-?3[\.-]?vision|phi-?3\.5[\.-]?vision|smolvlm/.test(normalized)
|
|
346
|
+
}
|
|
347
|
+
|
|
268
348
|
function normalizeSystemMessages(messages: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
|
|
269
349
|
const systemContents: string[] = []
|
|
270
350
|
const nonSystem: Array<Record<string, unknown>> = []
|
|
@@ -369,7 +449,7 @@ function providerNetworkErrorMessage(
|
|
|
369
449
|
): string {
|
|
370
450
|
const message = (err as Error).message || fallback
|
|
371
451
|
if (provider !== 'llamacpp') return message
|
|
372
|
-
return `${provider} request failed at ${baseUrl}: ${message}`
|
|
452
|
+
return `${providerDisplayName(provider)} request failed at ${baseUrl}: ${message}`
|
|
373
453
|
}
|
|
374
454
|
|
|
375
455
|
class ContentThinkingParser {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Message, MessageContentBlock } from './contracts.js'
|
|
2
2
|
import { messageTextContent } from '../utils/messages.js'
|
|
3
3
|
import type { OpenAIToolDefinition } from './openai-chat.js'
|
|
4
|
+
import { loadImageBlock } from '../utils/images.js'
|
|
4
5
|
|
|
5
6
|
export type ResponsesInputContent =
|
|
6
7
|
| { type: 'input_text'; text: string }
|
|
8
|
+
| { type: 'input_image'; image_url: string }
|
|
7
9
|
| { type: 'output_text'; text: string }
|
|
8
10
|
|
|
9
11
|
export type ResponsesInputItem =
|
|
@@ -30,13 +32,13 @@ export type ResponsesRequestBody = {
|
|
|
30
32
|
max_output_tokens?: number
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
export function buildResponsesBody(args: {
|
|
35
|
+
export async function buildResponsesBody(args: {
|
|
34
36
|
model: string
|
|
35
37
|
messages: Message[]
|
|
36
38
|
tools: OpenAIToolDefinition[]
|
|
37
39
|
maxOutputTokens?: number
|
|
38
|
-
}): ResponsesRequestBody {
|
|
39
|
-
const { instructions, items } = splitMessages(args.messages)
|
|
40
|
+
}): Promise<ResponsesRequestBody> {
|
|
41
|
+
const { instructions, items } = await splitMessages(args.messages)
|
|
40
42
|
const body: ResponsesRequestBody = {
|
|
41
43
|
model: args.model,
|
|
42
44
|
input: items,
|
|
@@ -60,10 +62,10 @@ export function buildResponsesBody(args: {
|
|
|
60
62
|
return body
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
function splitMessages(messages: Message[]): {
|
|
65
|
+
async function splitMessages(messages: Message[]): Promise<{
|
|
64
66
|
instructions?: string
|
|
65
67
|
items: ResponsesInputItem[]
|
|
66
|
-
} {
|
|
68
|
+
}> {
|
|
67
69
|
const instructions: string[] = []
|
|
68
70
|
const items: ResponsesInputItem[] = []
|
|
69
71
|
|
|
@@ -100,12 +102,12 @@ function splitMessages(messages: Message[]): {
|
|
|
100
102
|
}
|
|
101
103
|
continue
|
|
102
104
|
}
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
105
|
+
const content = await toOpenAIResponsesUserContent(blocks)
|
|
106
|
+
if (content.length > 0) {
|
|
105
107
|
items.push({
|
|
106
108
|
type: 'message',
|
|
107
109
|
role: 'user',
|
|
108
|
-
content
|
|
110
|
+
content,
|
|
109
111
|
})
|
|
110
112
|
}
|
|
111
113
|
continue
|
|
@@ -136,6 +138,25 @@ function splitMessages(messages: Message[]): {
|
|
|
136
138
|
}
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
async function toOpenAIResponsesUserContent(blocks: MessageContentBlock[]): Promise<ResponsesInputContent[]> {
|
|
142
|
+
const content: ResponsesInputContent[] = []
|
|
143
|
+
for (const block of blocks) {
|
|
144
|
+
if (block.type === 'text') {
|
|
145
|
+
if (block.text) content.push({ type: 'input_text', text: block.text })
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
if (block.type === 'image') {
|
|
149
|
+
const loaded = await loadImageBlock(block)
|
|
150
|
+
if (loaded.url) {
|
|
151
|
+
content.push({ type: 'input_image', image_url: loaded.url })
|
|
152
|
+
} else if (loaded.dataBase64 && loaded.mimeType) {
|
|
153
|
+
content.push({ type: 'input_image', image_url: `data:${loaded.mimeType};base64,${loaded.dataBase64}` })
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return content
|
|
158
|
+
}
|
|
159
|
+
|
|
139
160
|
function normalizeBlocks(content: Message['content']): MessageContentBlock[] {
|
|
140
161
|
if (typeof content === 'string') {
|
|
141
162
|
return content ? [{ type: 'text', text: content }] : []
|
|
@@ -5,7 +5,8 @@ import { providerErrorFromResponse } from './errors.js'
|
|
|
5
5
|
import { fetchWithRetryStreamEvents } from './retry.js'
|
|
6
6
|
import { iterSseEvents } from './sse.js'
|
|
7
7
|
import { buildResponsesBody } from './openai-responses-format.js'
|
|
8
|
-
import type
|
|
8
|
+
import { supportsOpenAIImages, type OpenAIToolDefinition } from './openai-chat.js'
|
|
9
|
+
import { hasImageBlocks, ImageLoadError } from '../utils/images.js'
|
|
9
10
|
|
|
10
11
|
const READ_TIMEOUT_MS = 45_000
|
|
11
12
|
|
|
@@ -64,15 +65,29 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
64
65
|
return
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
if (hasImageBlocks(messages) && !supportsOpenAIImages(this.model)) {
|
|
69
|
+
yield { type: 'error', message: `image input is not enabled for ${this.model}` }
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
67
73
|
let attempt = 0
|
|
68
74
|
while (true) {
|
|
69
75
|
attempt += 1
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
let body: string
|
|
77
|
+
try {
|
|
78
|
+
body = JSON.stringify(await buildResponsesBody({
|
|
79
|
+
model: this.model,
|
|
80
|
+
messages,
|
|
81
|
+
tools: this.tools,
|
|
82
|
+
maxOutputTokens: options.maxTokens,
|
|
83
|
+
}))
|
|
84
|
+
} catch (err: unknown) {
|
|
85
|
+
if (err instanceof ImageLoadError) {
|
|
86
|
+
yield { type: 'error', message: err.message }
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
throw err
|
|
90
|
+
}
|
|
76
91
|
|
|
77
92
|
let response: Response
|
|
78
93
|
try {
|
|
@@ -34,6 +34,7 @@ export function createProvider(config: EthagentConfig, options: { mode?: Session
|
|
|
34
34
|
baseUrl: localProviderBaseUrlFor('llamacpp', config.baseUrl),
|
|
35
35
|
apiKey: 'llamacpp',
|
|
36
36
|
tools: openAITools(mode, toolContext),
|
|
37
|
+
hasVisionProjector: Boolean(config.localMmprojPath),
|
|
37
38
|
})
|
|
38
39
|
case 'openai':
|
|
39
40
|
return createOpenAIProvider(config, openAITools(mode, toolContext))
|
|
@@ -24,7 +24,7 @@ export function nextSessionMode(mode: SessionMode): SessionMode {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function sessionModeLabel(mode: SessionMode): string {
|
|
27
|
-
return mode === 'plan' ? '
|
|
27
|
+
return mode === 'plan' ? 'plan mode' : mode === 'accept-edits' ? 'accept edits on' : ''
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function modePolicy(mode: PolicyMode): ModePolicy {
|
|
@@ -76,7 +76,7 @@ function buildToolEnabledPrompt(ctx: SystemPromptContext): string {
|
|
|
76
76
|
'**DIRECT REQUESTS**: If the user asks to change directory, list files, or read a file, respond with exactly one matching native tool call. Do not substitute prose or claim the action was taken.',
|
|
77
77
|
'**EVIDENCE REQUIRED**: Do not claim a path is missing, a directory does not exist, or a file is absent unless you have a `list_directory` or `read_file` result from this conversation that confirms it.',
|
|
78
78
|
'**TOOL TYPING**: Tool names are NOT shell commands. NEVER pass `list_directory`, `read_file`, `edit_file`, or `change_directory` directly to `run_bash`. Call the matching native tool.',
|
|
79
|
-
'
|
|
79
|
+
'**PREFER NATIVE TOOLS**: If a request can be answered by `list_directory`, `read_file`, `edit_file`, `write_file`, `delete_file`, or `change_directory`, use that tool. Treat reaching for `run_bash` as a flag that you may be doing it wrong — only proceed if the action genuinely needs a real shell.',
|
|
80
80
|
...(ctx.mode === 'plan'
|
|
81
81
|
? [
|
|
82
82
|
'Only read/list tools and permission-gated private continuity reads are available in plan mode.',
|
|
@@ -99,6 +99,8 @@ function buildToolEnabledPrompt(ctx: SystemPromptContext): string {
|
|
|
99
99
|
]
|
|
100
100
|
: ['No agent identity is linked in this session. Do not attempt private identity continuity edits; ask the user to create or load an agent first.']),
|
|
101
101
|
'Use `run_bash` **only** when true shell execution is necessary.',
|
|
102
|
+
'**NO BASH EXPLORATION**: Never use `run_bash` to inspect the workspace. That means no `node -e`, no heredocs (`<<EOF`, `<<\'NODE\'`), no `find`, `grep`, `cat`, `head`, `tail`, `ls`, `dir`, `type`, `tree`, or shell loops to read or walk files. Use `list_directory` for directories and `read_file` for files. `run_bash` is reserved for actions that require a real shell: running tests, builds, git operations, launching processes. If you catch yourself writing a one-liner to read or list something, stop and call the native tool instead.',
|
|
103
|
+
'**DISCOVERY BUDGET**: For exploratory or "tell me about" questions, target ≤5 tool calls total. If 5 calls of the same kind have not given you enough to answer, the question does not need more depth — answer from what you have. Do not recursively scan, walk every subdirectory, or write deeper scripts to be more thorough than the user asked for. Stop and reply.',
|
|
102
104
|
'Never use `run_bash` to produce conversational text. Do not call `echo`, `printf`, or similar to emit your reply — write it as your assistant text. Bash is for actions that need a real shell, not for generating words.',
|
|
103
105
|
'**CWD CONTINUITY**: The working directory below is authoritative. After `change_directory` succeeds, use the new path as the base for subsequent actions.',
|
|
104
106
|
'Do not lag behind the CWD. Edit/read relative to the *current* working directory.',
|
|
@@ -17,10 +17,7 @@ import type {
|
|
|
17
17
|
import { setCwd as setRuntimeCwd } from './cwd.js'
|
|
18
18
|
import type { EthagentConfig } from '../storage/config.js'
|
|
19
19
|
import type { SessionMessage } from '../storage/sessions.js'
|
|
20
|
-
import {
|
|
21
|
-
summarizeToolInput,
|
|
22
|
-
toolResultContentForRow,
|
|
23
|
-
} from '../chat/chatScreenUtils.js'
|
|
20
|
+
import { toolResultContentForRow, toolResultDiffForRow } from '../chat/chatScreenUtils.js'
|
|
24
21
|
import type { MessageRow } from '../chat/MessageList.js'
|
|
25
22
|
import { modePolicy, toPermissionMode, type SessionMode } from './sessionMode.js'
|
|
26
23
|
|
|
@@ -239,7 +236,7 @@ export async function runPendingToolUses(args: {
|
|
|
239
236
|
id: rowId,
|
|
240
237
|
name: toolUse.name,
|
|
241
238
|
summary: toolUse.name,
|
|
242
|
-
input:
|
|
239
|
+
input: toolUse.input,
|
|
243
240
|
},
|
|
244
241
|
])
|
|
245
242
|
await args.persistTurnMessage({
|
|
@@ -282,9 +279,15 @@ async function recordToolResult(
|
|
|
282
279
|
): Promise<void> {
|
|
283
280
|
const isError = !result.ok
|
|
284
281
|
const resultContent = toolResultContentForRow(toolUse.name, result.content, isError)
|
|
282
|
+
const diff = toolResultDiffForRow(result.content, isError)
|
|
285
283
|
args.updateRows(prev => prev.map(row =>
|
|
286
284
|
row.role === 'tool_call' && row.id === rowId
|
|
287
|
-
? {
|
|
285
|
+
? {
|
|
286
|
+
...row,
|
|
287
|
+
result: diff
|
|
288
|
+
? { content: resultContent, summary: result.summary, isError, diff }
|
|
289
|
+
: { content: resultContent, summary: result.summary, isError },
|
|
290
|
+
}
|
|
288
291
|
: row,
|
|
289
292
|
))
|
|
290
293
|
await args.persistTurnMessage({
|
package/src/runtime/turn.ts
CHANGED
|
@@ -56,6 +56,7 @@ function normalize(event: StreamEvent): ProviderTurnEvent {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export const MAX_CONTINUATION_NUDGES = 3
|
|
59
|
+
export const MAX_TOOL_USES_PER_TURN = 25
|
|
59
60
|
|
|
60
61
|
export type ContinuationNudgeReason =
|
|
61
62
|
| 'continuation'
|
|
@@ -63,6 +64,7 @@ export type ContinuationNudgeReason =
|
|
|
63
64
|
| 'tool_state_claim'
|
|
64
65
|
| 'tool_protocol_fake'
|
|
65
66
|
| 'tool_delegation'
|
|
67
|
+
| 'tool_budget'
|
|
66
68
|
| 'private_continuity_tool'
|
|
67
69
|
| 'private_continuity_tool_repair'
|
|
68
70
|
| 'reasoning_only'
|
|
@@ -82,6 +84,9 @@ const TOOL_PROTOCOL_FAKE_NUDGE_TEXT =
|
|
|
82
84
|
const TOOL_DELEGATION_NUDGE_TEXT =
|
|
83
85
|
'Do not ask the user to run native tools. You have access to the tools in this environment. Make exactly one native tool call now.'
|
|
84
86
|
|
|
87
|
+
const TOOL_BUDGET_NUDGE_TEXT =
|
|
88
|
+
'You have reached the tool-call budget for this turn. Do not call any more tools. Produce your final answer now using only what you already know from earlier tool results.'
|
|
89
|
+
|
|
85
90
|
const PRIVATE_CONTINUITY_NUDGE_TEXT =
|
|
86
91
|
'SOUL.md and MEMORY.md are existing private identity-vault scaffold files. Do not search workspace folders, read plans/, create files, or overwrite them. If exact private text is needed for a surgical removal or targeted replacement, call read_private_continuity_file with {"file":"MEMORY.md"} or {"file":"SOUL.md"}. If the user wants private continuity changed, call propose_private_continuity_edit. For memory/preferences use {"file":"MEMORY.md","appendToSection":"Durable User Preferences","appendText":"- User preference or memory note."}. For persona use {"file":"SOUL.md","appendToSection":"Persona","appendText":"- Persona or standing behavior note."}.'
|
|
87
92
|
|
|
@@ -164,6 +169,7 @@ export async function* runRuntimeTurn(
|
|
|
164
169
|
let continuationNudges = 0
|
|
165
170
|
let iterationIndex = 0
|
|
166
171
|
let priorIterationHadTools = false
|
|
172
|
+
let cumulativeToolUseCount = 0
|
|
167
173
|
const toolEvidenceThisTurn: ToolEvidence[] = []
|
|
168
174
|
|
|
169
175
|
// eslint-disable-next-line no-constant-condition
|
|
@@ -395,6 +401,29 @@ export async function* runRuntimeTurn(
|
|
|
395
401
|
return
|
|
396
402
|
}
|
|
397
403
|
|
|
404
|
+
if (cumulativeToolUseCount + pendingToolUses.length > MAX_TOOL_USES_PER_TURN) {
|
|
405
|
+
if (continuationNudges < maxContinuationNudges) {
|
|
406
|
+
continuationNudges += 1
|
|
407
|
+
yield {
|
|
408
|
+
type: 'continuation_nudge',
|
|
409
|
+
attempt: continuationNudges,
|
|
410
|
+
reason: 'tool_budget',
|
|
411
|
+
}
|
|
412
|
+
workingMessages = [
|
|
413
|
+
...await rebuildMessages(),
|
|
414
|
+
{ role: 'user', content: TOOL_BUDGET_NUDGE_TEXT },
|
|
415
|
+
]
|
|
416
|
+
continue
|
|
417
|
+
}
|
|
418
|
+
yield {
|
|
419
|
+
type: 'error',
|
|
420
|
+
message: `tool budget exceeded (${MAX_TOOL_USES_PER_TURN} max per turn); ask again with a narrower request`,
|
|
421
|
+
}
|
|
422
|
+
yield doneEvent(false, stopReason)
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
cumulativeToolUseCount += pendingToolUses.length
|
|
426
|
+
|
|
398
427
|
const batch = await runToolBatch(pendingToolUses)
|
|
399
428
|
for (const completed of batch.completedTools) {
|
|
400
429
|
toolEvidenceThisTurn.push({
|
package/src/storage/config.ts
CHANGED
|
@@ -80,6 +80,7 @@ const ConfigSchema = z.object({
|
|
|
80
80
|
provider: z.enum(PROVIDERS),
|
|
81
81
|
model: z.string().min(1),
|
|
82
82
|
baseUrl: z.string().url().optional(),
|
|
83
|
+
localMmprojPath: z.string().min(1).optional(),
|
|
83
84
|
firstRunAt: z.string(),
|
|
84
85
|
identity: IdentitySchema.optional(),
|
|
85
86
|
erc8004: z.object({
|
package/src/storage/rewind.ts
CHANGED
|
@@ -131,6 +131,26 @@ export async function listRewindEntries(
|
|
|
131
131
|
.slice(offset, offset + limit)
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
export async function groupRewindEntriesByTurn(
|
|
135
|
+
workspaceRoot: string,
|
|
136
|
+
sessionId: string,
|
|
137
|
+
): Promise<Map<string, RewindEntry[]>> {
|
|
138
|
+
const normalizedWorkspaceRoot = path.resolve(workspaceRoot)
|
|
139
|
+
const snapshots = await loadSnapshots()
|
|
140
|
+
const grouped = new Map<string, RewindEntry[]>()
|
|
141
|
+
for (const snapshot of snapshots) {
|
|
142
|
+
if (isIdentityMarkdownSnapshot(snapshot)) continue
|
|
143
|
+
if (!isSnapshotWithinScope(snapshot, normalizedWorkspaceRoot)) continue
|
|
144
|
+
if (snapshot.sessionId !== sessionId) continue
|
|
145
|
+
if (!snapshot.turnId) continue
|
|
146
|
+
const entry = toEntry(snapshot)
|
|
147
|
+
const bucket = grouped.get(snapshot.turnId)
|
|
148
|
+
if (bucket) bucket.push(entry)
|
|
149
|
+
else grouped.set(snapshot.turnId, [entry])
|
|
150
|
+
}
|
|
151
|
+
return grouped
|
|
152
|
+
}
|
|
153
|
+
|
|
134
154
|
export async function rewindWorkspaceEditsByEntryIds(
|
|
135
155
|
workspaceRoot: string,
|
|
136
156
|
entryIds: string[],
|
package/src/storage/sessions.ts
CHANGED
|
@@ -6,13 +6,15 @@ import type { Message } from '../providers/contracts.js'
|
|
|
6
6
|
import { getCwd } from '../runtime/cwd.js'
|
|
7
7
|
import type { SessionMode } from '../runtime/sessionMode.js'
|
|
8
8
|
import { atomicWriteText } from './atomicWrite.js'
|
|
9
|
+
import { stripFileChangeResultDiff } from '../tools/fileDiff.js'
|
|
9
10
|
import {
|
|
10
11
|
isUserCorrectionOfToolState,
|
|
11
12
|
looksLikeToolStateClaim,
|
|
12
13
|
} from '../runtime/toolClaimGuards.js'
|
|
14
|
+
import { userTextToContentBlocks } from '../utils/images.js'
|
|
13
15
|
|
|
14
16
|
export type SessionMessage =
|
|
15
|
-
| { version?: 2; role: 'user'; content: string; createdAt: string; turnId?: string; synthetic?: boolean }
|
|
17
|
+
| { version?: 2; role: 'user'; content: string; providerContent?: Message['content']; createdAt: string; turnId?: string; synthetic?: boolean }
|
|
16
18
|
| { version?: 2; role: 'assistant'; content: string; createdAt: string; model?: string; usage?: { in?: number; out?: number }; turnId?: string; synthetic?: boolean }
|
|
17
19
|
| { version?: 2; role: 'system'; content: string; createdAt: string; turnId?: string; synthetic?: boolean }
|
|
18
20
|
| { version: 2; role: 'tool_use'; toolUseId: string; name: string; input: Record<string, unknown>; createdAt: string; turnId?: string }
|
|
@@ -243,6 +245,17 @@ export type ProviderMessageProjectionOptions = {
|
|
|
243
245
|
export const TOOL_CORRECTION_CONTEXT_MESSAGE =
|
|
244
246
|
'The latest user message corrects a prior assistant claim about tool or filesystem state. Treat user correction and tool_result messages as authoritative. Ignore any recent assistant claim about files, directories, cwd, or tool execution unless it is backed by a tool_result, and retry with the appropriate tool.'
|
|
245
247
|
|
|
248
|
+
function resolveUserContent(
|
|
249
|
+
message: Extract<SessionMessage, { role: 'system' | 'user' | 'assistant' }>,
|
|
250
|
+
): Message['content'] {
|
|
251
|
+
if (message.role !== 'user') return message.content
|
|
252
|
+
if (message.providerContent) return message.providerContent
|
|
253
|
+
if (message.content.includes('[image:')) {
|
|
254
|
+
return userTextToContentBlocks(message.content)
|
|
255
|
+
}
|
|
256
|
+
return message.content
|
|
257
|
+
}
|
|
258
|
+
|
|
246
259
|
export function sessionMessagesToProviderMessages(
|
|
247
260
|
messages: SessionMessage[],
|
|
248
261
|
options: ProviderMessageProjectionOptions = {},
|
|
@@ -254,7 +267,7 @@ export function sessionMessagesToProviderMessages(
|
|
|
254
267
|
for (const [index, message] of messages.entries()) {
|
|
255
268
|
if (message.role === 'system' || message.role === 'user' || message.role === 'assistant') {
|
|
256
269
|
if (message.role === 'assistant' && invalidatedAssistantMessages.has(index)) continue
|
|
257
|
-
out.push({ role: message.role, content: message
|
|
270
|
+
out.push({ role: message.role, content: resolveUserContent(message) })
|
|
258
271
|
continue
|
|
259
272
|
}
|
|
260
273
|
if (message.role === 'tool_use') {
|
|
@@ -282,7 +295,7 @@ export function sessionMessagesToProviderMessages(
|
|
|
282
295
|
content: [{
|
|
283
296
|
type: 'tool_result',
|
|
284
297
|
toolUseId: message.toolUseId,
|
|
285
|
-
content: message.content,
|
|
298
|
+
content: stripFileChangeResultDiff(message.content),
|
|
286
299
|
isError: message.isError,
|
|
287
300
|
}],
|
|
288
301
|
})
|
package/src/tools/bashSafety.ts
CHANGED
|
@@ -98,16 +98,16 @@ export function assessBashCommand(command: string): BashSafetyAssessment {
|
|
|
98
98
|
const triggeredChecks = RISKY_PATTERN_CHECKS.filter(check => check.pattern.test(command)).map(check => check.message)
|
|
99
99
|
|
|
100
100
|
const warning = triggeredChecks.length > 0
|
|
101
|
-
? `
|
|
101
|
+
? `Warning: ${sentenceCase(triggeredChecks[0] ?? 'command is risky')}. Reusable approval is limited for this command.`
|
|
102
102
|
: highRisk
|
|
103
|
-
? `
|
|
103
|
+
? `Warning: ${sentenceCase(firstToken ?? '')} is a high-impact command. Reusable approval is limited for this command.`
|
|
104
104
|
: undefined
|
|
105
105
|
|
|
106
106
|
return {
|
|
107
107
|
warning,
|
|
108
108
|
canPersistExact: triggeredChecks.length === 0 && !nonPersistable,
|
|
109
109
|
canPersistPrefix: triggeredChecks.length === 0 && !highRisk && Boolean(firstToken),
|
|
110
|
-
commandPrefix: firstToken,
|
|
110
|
+
commandPrefix: firstToken ?? '',
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
@@ -180,3 +180,7 @@ function normalizeCommandToken(token: string): string {
|
|
|
180
180
|
.toLowerCase()
|
|
181
181
|
.replace(/[^a-z0-9_.:-]/g, '') ?? ''
|
|
182
182
|
}
|
|
183
|
+
|
|
184
|
+
function sentenceCase(value: string): string {
|
|
185
|
+
return value ? value[0]!.toUpperCase() + value.slice(1) : value
|
|
186
|
+
}
|
package/src/tools/bashTool.ts
CHANGED
|
@@ -39,7 +39,7 @@ export const bashTool: Tool<typeof schema> = {
|
|
|
39
39
|
command: input.command,
|
|
40
40
|
commandPrefix: safety.commandPrefix,
|
|
41
41
|
cwd,
|
|
42
|
-
title: '
|
|
42
|
+
title: 'Allow shell command?',
|
|
43
43
|
subtitle: `${input.command}\n${cwd}`,
|
|
44
44
|
warning: safety.warning,
|
|
45
45
|
canPersistExact: safety.canPersistExact,
|
package/src/tools/contracts.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type PermissionRequest =
|
|
|
23
23
|
subtitle: string
|
|
24
24
|
before: string
|
|
25
25
|
after: string
|
|
26
|
+
diff: string
|
|
26
27
|
changeSummary: string
|
|
27
28
|
}
|
|
28
29
|
| {
|
|
@@ -34,6 +35,7 @@ export type PermissionRequest =
|
|
|
34
35
|
subtitle: string
|
|
35
36
|
before: string
|
|
36
37
|
after: string
|
|
38
|
+
diff: string
|
|
37
39
|
changeSummary: string
|
|
38
40
|
}
|
|
39
41
|
| {
|
|
@@ -68,6 +70,7 @@ export type PermissionRequest =
|
|
|
68
70
|
subtitle: string
|
|
69
71
|
before: string
|
|
70
72
|
after: string
|
|
73
|
+
diff: string
|
|
71
74
|
changeSummary: string
|
|
72
75
|
}
|
|
73
76
|
| {
|
|
@@ -3,6 +3,7 @@ import path from 'node:path'
|
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
import { recordRewindSnapshot } from '../storage/rewind.js'
|
|
5
5
|
import type { Tool } from './contracts.js'
|
|
6
|
+
import { formatFileChangeResult, renderUnifiedFileDiff } from './fileDiff.js'
|
|
6
7
|
import { resolveWorkspacePath } from './readTool.js'
|
|
7
8
|
|
|
8
9
|
const schema = z.object({
|
|
@@ -35,6 +36,7 @@ export const deleteFileTool: Tool<typeof schema> = {
|
|
|
35
36
|
subtitle: prepared.fullPath,
|
|
36
37
|
before: preview(prepared.before),
|
|
37
38
|
after: '(deleted)',
|
|
39
|
+
diff: renderUnifiedFileDiff({ filePath: prepared.relativePath, before: prepared.before, after: '' }),
|
|
38
40
|
changeSummary: `delete ${prepared.relativePath}`,
|
|
39
41
|
}
|
|
40
42
|
},
|
|
@@ -58,9 +60,12 @@ export const deleteFileTool: Tool<typeof schema> = {
|
|
|
58
60
|
return {
|
|
59
61
|
ok: true,
|
|
60
62
|
summary: `deleted ${prepared.relativePath}`,
|
|
61
|
-
content:
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
content: formatFileChangeResult(
|
|
64
|
+
rewindWarning
|
|
65
|
+
? `deleted ${prepared.fullPath}\nwarning: ${rewindWarning}`
|
|
66
|
+
: `deleted ${prepared.fullPath}`,
|
|
67
|
+
renderUnifiedFileDiff({ filePath: prepared.relativePath, before: prepared.before, after: '' }),
|
|
68
|
+
),
|
|
64
69
|
}
|
|
65
70
|
},
|
|
66
71
|
}
|
package/src/tools/editTool.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { recordRewindSnapshot } from '../storage/rewind.js'
|
|
|
5
5
|
import type { EthagentConfig } from '../storage/config.js'
|
|
6
6
|
import type { Tool } from './contracts.js'
|
|
7
7
|
import { applyRequestedEdit } from './editUtils.js'
|
|
8
|
+
import { formatFileChangeResult, renderUnifiedFileDiff } from './fileDiff.js'
|
|
8
9
|
import { resolveWorkspacePath } from './readTool.js'
|
|
9
10
|
|
|
10
11
|
const schema = z.object({
|
|
@@ -44,15 +45,16 @@ export const editTool: Tool<typeof schema> = {
|
|
|
44
45
|
subtitle: fullPath,
|
|
45
46
|
before: applied.previewBefore,
|
|
46
47
|
after: applied.previewAfter,
|
|
48
|
+
diff: renderUnifiedFileDiff({ filePath: relativePath, before: applied.before, after: applied.after }),
|
|
47
49
|
changeSummary: applied.summary,
|
|
48
50
|
}
|
|
49
51
|
},
|
|
50
52
|
async execute(input, context) {
|
|
51
|
-
const { fullPath, applied, existedBefore, before } = await prepareEdit(input, context)
|
|
53
|
+
const { fullPath, relativePath, applied, existedBefore, before } = await prepareEdit(input, context)
|
|
52
54
|
const rewindWarning = await tryRecordRewindSnapshot({
|
|
53
55
|
workspaceRoot: context.workspaceRoot,
|
|
54
56
|
filePath: fullPath,
|
|
55
|
-
relativePath
|
|
57
|
+
relativePath,
|
|
56
58
|
existedBefore,
|
|
57
59
|
previousContent: before,
|
|
58
60
|
changeSummary: applied.summary,
|
|
@@ -68,9 +70,12 @@ export const editTool: Tool<typeof schema> = {
|
|
|
68
70
|
return {
|
|
69
71
|
ok: true,
|
|
70
72
|
summary: applied.summary,
|
|
71
|
-
content:
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
content: formatFileChangeResult(
|
|
74
|
+
rewindWarning
|
|
75
|
+
? `updated ${fullPath}\nwarning: ${rewindWarning}`
|
|
76
|
+
: `updated ${fullPath}`,
|
|
77
|
+
renderUnifiedFileDiff({ filePath: relativePath, before: applied.before, after: applied.after }),
|
|
78
|
+
),
|
|
74
79
|
}
|
|
75
80
|
},
|
|
76
81
|
}
|