ethagent 2.2.0 → 2.3.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/package.json +2 -1
- package/src/app/FirstRun.tsx +1 -7
- package/src/app/FirstRunTimeline.tsx +1 -1
- package/src/chat/ChatBottomPane.tsx +20 -11
- package/src/chat/ChatScreen.tsx +160 -35
- 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 +1 -1
- package/src/chat/chatTurnOrchestrator.ts +1 -7
- 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} +36 -23
- 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} +35 -35
- 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 +5 -3
- package/src/models/catalog.ts +2 -1
- package/src/models/modelPickerOptions.ts +2 -14
- package/src/models/providerDisplay.ts +16 -0
- package/src/providers/errors.ts +6 -4
- package/src/providers/openai-chat.ts +2 -1
- 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/rewind.ts +20 -0
- package/src/storage/sessions.ts +2 -1
- 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 +25 -3
- package/src/ui/TextInput.tsx +2 -2
- package/src/ui/theme.ts +17 -0
- package/src/utils/clipboard.ts +10 -7
- 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
|
@@ -612,9 +612,11 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
612
612
|
) : (
|
|
613
613
|
<Select
|
|
614
614
|
options={[
|
|
615
|
-
{ value: '
|
|
616
|
-
{ value: '
|
|
617
|
-
{ value: '
|
|
615
|
+
{ value: 'hdr:account', label: 'Account', disabled: true, role: 'section', bold: true },
|
|
616
|
+
{ value: 'signin', label: 'Sign in Again', indent: 2 },
|
|
617
|
+
{ value: 'signout', label: 'Sign Out', indent: 2 },
|
|
618
|
+
{ value: 'hdr:nav', label: 'Navigation', disabled: true, role: 'section', bold: true },
|
|
619
|
+
{ value: 'cancel', label: 'Back', indent: 2 },
|
|
618
620
|
]}
|
|
619
621
|
onSubmit={(value) => {
|
|
620
622
|
if (value === 'signin') {
|
package/src/models/catalog.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { defaultModelFor, type EthagentConfig, type ProviderId } from '../storag
|
|
|
2
2
|
import { getKey } from '../storage/secrets.js'
|
|
3
3
|
import { loadLocalHfModels } from './huggingface.js'
|
|
4
4
|
import { hasOpenAIOAuthCredentials } from '../auth/openaiOAuth/credentials.js'
|
|
5
|
+
import { providerDisplayName } from './providerDisplay.js'
|
|
5
6
|
|
|
6
7
|
const OPENAI_OAUTH_MODEL_IDS = ['gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', 'gpt-5.2'] as const
|
|
7
8
|
|
|
@@ -85,7 +86,7 @@ export async function discoverProviderModels(
|
|
|
85
86
|
if (provider === 'openai' && await hasOpenAIOAuthCredentials()) {
|
|
86
87
|
return openAIOAuthCatalog()
|
|
87
88
|
}
|
|
88
|
-
return fallbackResult(config, `missing ${provider} API key`)
|
|
89
|
+
return fallbackResult(config, `missing ${providerDisplayName(provider)} API key`)
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
const baseUrl = provider === 'openai' ? openAIBaseUrlFor(config) : ''
|
|
@@ -7,26 +7,14 @@ import { type SelectOption } from '../ui/Select.js'
|
|
|
7
7
|
import { formatLocalHfModelDisplayName, formatModelDisplayName } from './modelDisplay.js'
|
|
8
8
|
import { localModelId, quantizationFromFilename } from './huggingface.js'
|
|
9
9
|
import type { UncensoredCatalogEntry } from './uncensoredCatalog.js'
|
|
10
|
+
import { cloudProviderDisplayName, providerDisplayName, type CloudProviderId } from './providerDisplay.js'
|
|
10
11
|
|
|
11
|
-
export type CloudProviderId
|
|
12
|
+
export { cloudProviderDisplayName, providerDisplayName, type CloudProviderId }
|
|
12
13
|
|
|
13
14
|
export const MODEL_PICKER_CLOUD_PROVIDERS: CloudProviderId[] = ['openai', 'anthropic', 'gemini']
|
|
14
15
|
export const LOCAL_MODEL_LINK_HINT = 'Paste a GGUF link'
|
|
15
16
|
export const LOCAL_MODEL_LINK_EXAMPLE = 'e.g. https://huggingface.co/Qwen/Qwen3-8B-GGUF'
|
|
16
17
|
|
|
17
|
-
export function cloudProviderDisplayName(provider: CloudProviderId): string {
|
|
18
|
-
switch (provider) {
|
|
19
|
-
case 'openai': return 'OpenAI'
|
|
20
|
-
case 'anthropic': return 'Anthropic'
|
|
21
|
-
case 'gemini': return 'Gemini'
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function providerDisplayName(provider: ProviderId): string {
|
|
26
|
-
if (provider === 'llamacpp') return 'llama.cpp'
|
|
27
|
-
return cloudProviderDisplayName(provider)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
18
|
export type LocalHfPickerModel = {
|
|
31
19
|
id: string
|
|
32
20
|
displayName: string
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ProviderId } from '../storage/config.js'
|
|
2
|
+
|
|
3
|
+
export type CloudProviderId = Exclude<ProviderId, 'llamacpp'>
|
|
4
|
+
|
|
5
|
+
export function cloudProviderDisplayName(provider: CloudProviderId): string {
|
|
6
|
+
switch (provider) {
|
|
7
|
+
case 'openai': return 'OpenAI'
|
|
8
|
+
case 'anthropic': return 'Anthropic'
|
|
9
|
+
case 'gemini': return 'Gemini'
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function providerDisplayName(provider: ProviderId): string {
|
|
14
|
+
if (provider === 'llamacpp') return 'llama.cpp'
|
|
15
|
+
return cloudProviderDisplayName(provider)
|
|
16
|
+
}
|
package/src/providers/errors.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ProviderId } from '../storage/config.js'
|
|
2
2
|
import { ProviderError } from './contracts.js'
|
|
3
3
|
import { formatGeminiRateLimitMessage } from './gemini.js'
|
|
4
|
+
import { providerDisplayName } from '../models/providerDisplay.js'
|
|
4
5
|
|
|
5
6
|
type ErrorBody =
|
|
6
7
|
| string
|
|
@@ -30,19 +31,20 @@ export async function providerErrorFromResponse(
|
|
|
30
31
|
&& /API[_ ]?key( not valid| not found|_invalid)|invalid api key/i.test(detail)
|
|
31
32
|
) {
|
|
32
33
|
return new ProviderError(
|
|
33
|
-
'
|
|
34
|
+
'Gemini: API key rejected — verify your key at https://aistudio.google.com/app/apikey, then run /key gemini to set it again',
|
|
34
35
|
)
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
if (provider !== 'llamacpp') {
|
|
39
|
+
const name = providerDisplayName(provider)
|
|
38
40
|
if (response.status === 401 || response.status === 403) {
|
|
39
|
-
return new ProviderError(`auth failed: check your ${
|
|
41
|
+
return new ProviderError(`auth failed: check your ${name} key (/doctor to verify)`)
|
|
40
42
|
}
|
|
41
43
|
if (response.status === 429) {
|
|
42
|
-
return new ProviderError(detail || `${
|
|
44
|
+
return new ProviderError(detail || `${name} rate limit exceeded`, { transient: true })
|
|
43
45
|
}
|
|
44
46
|
if (response.status >= 500) {
|
|
45
|
-
return new ProviderError(detail || `${
|
|
47
|
+
return new ProviderError(detail || `${name} server error (${response.status})`, { transient: true })
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -5,6 +5,7 @@ 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 { providerDisplayName } from '../models/providerDisplay.js'
|
|
8
9
|
|
|
9
10
|
export type OpenAIToolDefinition = {
|
|
10
11
|
type: 'function'
|
|
@@ -369,7 +370,7 @@ function providerNetworkErrorMessage(
|
|
|
369
370
|
): string {
|
|
370
371
|
const message = (err as Error).message || fallback
|
|
371
372
|
if (provider !== 'llamacpp') return message
|
|
372
|
-
return `${provider} request failed at ${baseUrl}: ${message}`
|
|
373
|
+
return `${providerDisplayName(provider)} request failed at ${baseUrl}: ${message}`
|
|
373
374
|
}
|
|
374
375
|
|
|
375
376
|
class ContentThinkingParser {
|
|
@@ -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/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,6 +6,7 @@ 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,
|
|
@@ -282,7 +283,7 @@ export function sessionMessagesToProviderMessages(
|
|
|
282
283
|
content: [{
|
|
283
284
|
type: 'tool_result',
|
|
284
285
|
toolUseId: message.toolUseId,
|
|
285
|
-
content: message.content,
|
|
286
|
+
content: stripFileChangeResultDiff(message.content),
|
|
286
287
|
isError: message.isError,
|
|
287
288
|
}],
|
|
288
289
|
})
|
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
|
}
|