ethagent 2.3.0 → 3.0.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 +18 -4
- package/package.json +2 -1
- package/src/app/FirstRun.tsx +157 -15
- package/src/app/FirstRunTimeline.tsx +4 -0
- package/src/app/input/AppInputProvider.tsx +19 -0
- package/src/app/input/appInputParser.ts +19 -4
- package/src/chat/ChatBottomPane.tsx +12 -1
- package/src/chat/ChatScreen.tsx +17 -5
- package/src/chat/ConversationStack.tsx +25 -19
- package/src/chat/MessageList.tsx +194 -53
- package/src/chat/chatSessionState.ts +4 -1
- package/src/chat/chatTurnOrchestrator.ts +65 -2
- package/src/chat/input/ChatInput.tsx +28 -2
- package/src/chat/input/imageRefs.ts +30 -0
- package/src/chat/input/textCursor.ts +13 -3
- package/src/chat/transcript/TranscriptView.tsx +7 -5
- package/src/chat/transcript/transcriptViewport.ts +88 -17
- package/src/chat/views/PermissionPrompt.tsx +26 -26
- package/src/chat/views/PermissionsView.tsx +18 -12
- package/src/chat/views/ResumeView.tsx +16 -7
- package/src/chat/views/RewindView.tsx +3 -1
- package/src/cli/ResetConfirmView.tsx +24 -9
- package/src/identity/continuity/editor.ts +27 -2
- package/src/identity/continuity/envelope.ts +125 -0
- package/src/identity/continuity/publicSkills.ts +37 -1
- package/src/identity/continuity/skills/frontmatter.ts +183 -0
- package/src/identity/continuity/skills/loadSkills.ts +609 -0
- package/src/identity/continuity/skills/publicSkillsSync.ts +32 -0
- package/src/identity/continuity/skills/scaffold.ts +52 -0
- package/src/identity/continuity/skills/types.ts +30 -0
- package/src/identity/continuity/storage/defaults.ts +28 -47
- package/src/identity/continuity/storage/files.ts +1 -0
- package/src/identity/continuity/storage/paths.ts +1 -0
- package/src/identity/continuity/storage/scaffold.ts +25 -23
- package/src/identity/continuity/storage/status.ts +34 -5
- package/src/identity/continuity/storage/types.ts +3 -2
- package/src/identity/continuity/storage.ts +3 -0
- package/src/identity/hub/OperationalRoutes.tsx +105 -3
- package/src/identity/hub/Routes.tsx +5 -3
- package/src/identity/hub/continuity/ContinuityDashboardScreen.tsx +5 -51
- package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +1 -1
- package/src/identity/hub/continuity/SavePromptScreen.tsx +1 -0
- package/src/identity/hub/continuity/effects.ts +36 -5
- package/src/identity/hub/continuity/skills/DeleteSkillConfirmScreen.tsx +112 -0
- package/src/identity/hub/continuity/skills/DeleteSkillScreen.tsx +123 -0
- package/src/identity/hub/continuity/skills/NewSkillScreen.tsx +57 -0
- package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +52 -0
- package/src/identity/hub/continuity/skills/SkillVisibilityScreen.tsx +171 -0
- package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +213 -0
- package/src/identity/hub/continuity/snapshot.ts +3 -0
- package/src/identity/hub/continuity/state.ts +3 -2
- package/src/identity/hub/continuity/vault.ts +42 -10
- package/src/identity/hub/custody/CustodyEditFlow.tsx +3 -3
- package/src/identity/hub/identityHubReducer.ts +21 -0
- package/src/identity/hub/profile/effects.ts +16 -3
- package/src/identity/hub/restore/RestoreFlow.tsx +43 -6
- package/src/identity/hub/restore/apply.ts +12 -1
- package/src/identity/hub/restore/recovery.ts +11 -1
- package/src/identity/hub/restore/resolve.ts +1 -1
- package/src/identity/hub/restore/useRestoreEffects.ts +4 -6
- package/src/identity/hub/shared/components/DetailsScreen.tsx +4 -1
- package/src/identity/hub/shared/components/IdentitySummary.tsx +97 -53
- package/src/identity/hub/shared/components/MenuScreen.tsx +18 -15
- package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +1 -1
- package/src/identity/hub/shared/components/menuFlagsFromReconciliation.ts +8 -12
- package/src/identity/hub/shared/effects/sync.ts +16 -3
- package/src/identity/hub/shared/model/copy.ts +2 -4
- package/src/identity/hub/transfer/effects.ts +15 -2
- package/src/identity/hub/useIdentityHubContinuity.ts +145 -23
- package/src/identity/hub/useIdentityHubController.ts +5 -1
- package/src/identity/hub/useIdentityHubSideEffects.ts +2 -4
- package/src/mcp/manager.ts +1 -1
- package/src/models/ModelPicker.tsx +211 -74
- package/src/models/huggingface.ts +180 -2
- package/src/models/llamacpp.ts +261 -17
- package/src/models/llamacppPreflight.ts +16 -12
- package/src/models/modelPickerOptions.ts +57 -38
- package/src/providers/anthropic.ts +36 -5
- package/src/providers/contracts.ts +10 -1
- package/src/providers/gemini.ts +29 -3
- package/src/providers/openai-chat.ts +131 -11
- package/src/providers/openai-responses-format.ts +29 -8
- package/src/providers/openai-responses.ts +41 -11
- package/src/providers/registry.ts +1 -0
- package/src/runtime/toolExecution.ts +4 -3
- package/src/runtime/turn.ts +61 -30
- package/src/storage/config.ts +1 -0
- package/src/storage/sessions.ts +14 -2
- package/src/tools/changeDirectoryTool.ts +1 -1
- package/src/tools/contracts.ts +10 -0
- package/src/tools/deleteFileTool.ts +1 -1
- package/src/tools/editTool.ts +1 -1
- package/src/tools/listDirectoryTool.ts +1 -1
- package/src/tools/listSkillFilesTool.ts +77 -0
- package/src/tools/listSkillsTool.ts +68 -0
- package/src/tools/mcpResourceTools.ts +2 -2
- package/src/tools/privateContinuityReadTool.ts +1 -1
- package/src/tools/readSkillTool.ts +107 -0
- package/src/tools/readTool.ts +1 -1
- package/src/tools/registry.ts +6 -0
- package/src/tools/writeFileTool.ts +22 -2
- package/src/ui/Spinner.tsx +15 -3
- package/src/ui/theme.ts +2 -0
- package/src/utils/images.ts +140 -0
- package/src/utils/messages.ts +2 -0
- package/src/identity/continuity/localBackup.ts +0 -249
- package/src/identity/continuity/zipWriter.ts +0 -95
- package/src/identity/hub/continuity/index.ts +0 -7
- package/src/identity/hub/ens/index.ts +0 -11
- package/src/identity/hub/restore/index.ts +0 -22
|
@@ -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 {
|
|
@@ -261,15 +276,30 @@ function parseToolArguments(input: string): Record<string, unknown> {
|
|
|
261
276
|
const trimmed = input.trim()
|
|
262
277
|
if (!trimmed) return {}
|
|
263
278
|
try {
|
|
264
|
-
|
|
265
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
266
|
-
? (parsed as Record<string, unknown>)
|
|
267
|
-
: {}
|
|
279
|
+
return coerceToToolArguments(JSON.parse(trimmed))
|
|
268
280
|
} catch {
|
|
269
281
|
return {}
|
|
270
282
|
}
|
|
271
283
|
}
|
|
272
284
|
|
|
285
|
+
function coerceToToolArguments(value: unknown): Record<string, unknown> {
|
|
286
|
+
if (typeof value === 'string') {
|
|
287
|
+
const trimmed = value.trim()
|
|
288
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
289
|
+
try {
|
|
290
|
+
return coerceToToolArguments(JSON.parse(trimmed))
|
|
291
|
+
} catch {
|
|
292
|
+
return {}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return {}
|
|
296
|
+
}
|
|
297
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
298
|
+
return value as Record<string, unknown>
|
|
299
|
+
}
|
|
300
|
+
return {}
|
|
301
|
+
}
|
|
302
|
+
|
|
273
303
|
function networkErrorMessage(baseUrl: string, err: unknown, fallback = 'network error'): string {
|
|
274
304
|
const message = (err as Error).message || fallback
|
|
275
305
|
return `openai request failed at ${baseUrl}: ${message}`
|
|
@@ -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))
|
|
@@ -50,8 +50,8 @@ export async function executeToolWithPermissions(
|
|
|
50
50
|
return {
|
|
51
51
|
result: {
|
|
52
52
|
ok: false,
|
|
53
|
-
summary: `
|
|
54
|
-
content: `
|
|
53
|
+
summary: `Unknown tool ${options.name}`,
|
|
54
|
+
content: `Tool '${options.name}' is not registered`,
|
|
55
55
|
},
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -98,6 +98,7 @@ export async function executeToolWithPermissions(
|
|
|
98
98
|
options.permissionMode === 'plan' &&
|
|
99
99
|
request.kind !== 'read' &&
|
|
100
100
|
request.kind !== 'private-continuity-read' &&
|
|
101
|
+
request.kind !== 'private-skill-read' &&
|
|
101
102
|
!(request.kind === 'mcp' && request.readOnly)
|
|
102
103
|
) {
|
|
103
104
|
return {
|
|
@@ -111,7 +112,7 @@ export async function executeToolWithPermissions(
|
|
|
111
112
|
|
|
112
113
|
const matchedRule = matchPermissionRule(options.getPermissionRules(), request)
|
|
113
114
|
const decision: PermissionDecision =
|
|
114
|
-
modePolicy(options.permissionMode).autoAllowToolKind(
|
|
115
|
+
modePolicy(options.permissionMode).autoAllowToolKind(tool.kind)
|
|
115
116
|
? 'allow-once'
|
|
116
117
|
: matchedRule
|
|
117
118
|
? 'allow-once'
|
package/src/runtime/turn.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type ProviderTurnEvent =
|
|
11
11
|
| { type: 'text'; delta: string }
|
|
12
12
|
| { type: 'thinking'; delta: string }
|
|
13
|
+
| { type: 'thinking_end' }
|
|
13
14
|
| ProviderRetryStreamEvent
|
|
14
15
|
| { type: 'tool_use_start'; id: string; name: string }
|
|
15
16
|
| { type: 'tool_use_delta'; id: string; delta: string }
|
|
@@ -46,6 +47,7 @@ function normalize(event: StreamEvent): ProviderTurnEvent {
|
|
|
46
47
|
switch (event.type) {
|
|
47
48
|
case 'text': return { type: 'text', delta: event.delta }
|
|
48
49
|
case 'thinking': return { type: 'thinking', delta: event.delta }
|
|
50
|
+
case 'thinking_end': return { type: 'thinking_end' }
|
|
49
51
|
case 'retry': return event
|
|
50
52
|
case 'tool_use_start': return event
|
|
51
53
|
case 'tool_use_delta': return event
|
|
@@ -67,6 +69,7 @@ export type ContinuationNudgeReason =
|
|
|
67
69
|
| 'tool_budget'
|
|
68
70
|
| 'private_continuity_tool'
|
|
69
71
|
| 'private_continuity_tool_repair'
|
|
72
|
+
| 'write_file_repair'
|
|
70
73
|
| 'reasoning_only'
|
|
71
74
|
|
|
72
75
|
const CONTINUATION_NUDGE_TEXT =
|
|
@@ -93,6 +96,9 @@ const PRIVATE_CONTINUITY_NUDGE_TEXT =
|
|
|
93
96
|
const PRIVATE_CONTINUITY_REPAIR_NUDGE_TEXT =
|
|
94
97
|
'The previous propose_private_continuity_edit call had invalid or missing input. Retry the same native tool now with complete arguments. Do not answer in prose and do not search for markdown files. 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."}.'
|
|
95
98
|
|
|
99
|
+
const WRITE_FILE_REPAIR_NUDGE_TEXT =
|
|
100
|
+
'The previous write_file call was rejected because the arguments were missing or malformed. Retry the same native tool now with a JSON object (not a JSON string) shaped exactly like {"path":"relative/path.ext","content":"...complete file contents..."}. Both fields are required and must be non-empty. Do not answer in prose.'
|
|
101
|
+
|
|
96
102
|
const REASONING_ONLY_NUDGE_TEXT =
|
|
97
103
|
'You produced private reasoning but no user-visible answer. Answer the user now in visible text. Do not continue only with reasoning.'
|
|
98
104
|
|
|
@@ -100,6 +106,7 @@ export type TurnEvent =
|
|
|
100
106
|
| { type: 'iteration_start'; index: number }
|
|
101
107
|
| { type: 'text'; delta: string }
|
|
102
108
|
| { type: 'thinking'; delta: string }
|
|
109
|
+
| { type: 'thinking_end' }
|
|
103
110
|
| ProviderRetryStreamEvent
|
|
104
111
|
| { type: 'tool_use_start'; id: string; name: string }
|
|
105
112
|
| { type: 'tool_use_delta'; id: string; delta: string }
|
|
@@ -203,6 +210,8 @@ export async function* runRuntimeTurn(
|
|
|
203
210
|
} else if (ev.type === 'thinking') {
|
|
204
211
|
thinkingSeen = true
|
|
205
212
|
yield { type: 'thinking', delta: ev.delta }
|
|
213
|
+
} else if (ev.type === 'thinking_end') {
|
|
214
|
+
yield { type: 'thinking_end' }
|
|
206
215
|
} else if (ev.type === 'tool_use_start') {
|
|
207
216
|
yield { type: 'tool_use_start', id: ev.id, name: ev.name }
|
|
208
217
|
} else if (ev.type === 'tool_use_delta') {
|
|
@@ -279,7 +288,7 @@ export async function* runRuntimeTurn(
|
|
|
279
288
|
}
|
|
280
289
|
yield {
|
|
281
290
|
type: 'error',
|
|
282
|
-
message: '
|
|
291
|
+
message: 'Model printed tool names instead of making a tool call',
|
|
283
292
|
discardAssistant: true,
|
|
284
293
|
}
|
|
285
294
|
yield doneEvent(false, stopReason)
|
|
@@ -302,7 +311,7 @@ export async function* runRuntimeTurn(
|
|
|
302
311
|
}
|
|
303
312
|
yield {
|
|
304
313
|
type: 'error',
|
|
305
|
-
message: '
|
|
314
|
+
message: 'Model asked the user to run a tool instead of making a tool call',
|
|
306
315
|
discardAssistant: true,
|
|
307
316
|
}
|
|
308
317
|
yield doneEvent(false, stopReason)
|
|
@@ -333,7 +342,7 @@ export async function* runRuntimeTurn(
|
|
|
333
342
|
}
|
|
334
343
|
yield {
|
|
335
344
|
type: 'error',
|
|
336
|
-
message: '
|
|
345
|
+
message: 'Model claimed workspace state without matching tool evidence',
|
|
337
346
|
discardAssistant: true,
|
|
338
347
|
}
|
|
339
348
|
yield doneEvent(false, stopReason)
|
|
@@ -342,26 +351,18 @@ export async function* runRuntimeTurn(
|
|
|
342
351
|
}
|
|
343
352
|
|
|
344
353
|
if (pendingToolUses.length === 0) {
|
|
345
|
-
if (!assistantText && thinkingSeen) {
|
|
346
|
-
|
|
347
|
-
continuationNudges += 1
|
|
348
|
-
yield {
|
|
349
|
-
type: 'continuation_nudge',
|
|
350
|
-
attempt: continuationNudges,
|
|
351
|
-
reason: 'reasoning_only',
|
|
352
|
-
}
|
|
353
|
-
workingMessages = [
|
|
354
|
-
...await rebuildMessages(),
|
|
355
|
-
{ role: 'user', content: REASONING_ONLY_NUDGE_TEXT },
|
|
356
|
-
]
|
|
357
|
-
continue
|
|
358
|
-
}
|
|
354
|
+
if (!assistantText && thinkingSeen && continuationNudges < maxContinuationNudges) {
|
|
355
|
+
continuationNudges += 1
|
|
359
356
|
yield {
|
|
360
|
-
type: '
|
|
361
|
-
|
|
357
|
+
type: 'continuation_nudge',
|
|
358
|
+
attempt: continuationNudges,
|
|
359
|
+
reason: 'reasoning_only',
|
|
362
360
|
}
|
|
363
|
-
|
|
364
|
-
|
|
361
|
+
workingMessages = [
|
|
362
|
+
...await rebuildMessages(),
|
|
363
|
+
{ role: 'user', content: REASONING_ONLY_NUDGE_TEXT },
|
|
364
|
+
]
|
|
365
|
+
continue
|
|
365
366
|
}
|
|
366
367
|
|
|
367
368
|
const nudge = nextNudge(provider, assistantText)
|
|
@@ -387,7 +388,7 @@ export async function* runRuntimeTurn(
|
|
|
387
388
|
if (assistantText && nudge?.reason === 'tool_capability') {
|
|
388
389
|
yield {
|
|
389
390
|
type: 'error',
|
|
390
|
-
message: '
|
|
391
|
+
message: 'Model refused available tools after corrective nudges',
|
|
391
392
|
}
|
|
392
393
|
yield doneEvent(false, stopReason)
|
|
393
394
|
return
|
|
@@ -456,17 +457,17 @@ export async function* runRuntimeTurn(
|
|
|
456
457
|
yield {
|
|
457
458
|
type: 'continuation_nudge',
|
|
458
459
|
attempt: continuationNudges,
|
|
459
|
-
reason:
|
|
460
|
+
reason: repairNudge.reason,
|
|
460
461
|
}
|
|
461
462
|
workingMessages = [
|
|
462
463
|
...await rebuildMessages(),
|
|
463
|
-
{ role: 'user', content: repairNudge },
|
|
464
|
+
{ role: 'user', content: repairNudge.text },
|
|
464
465
|
]
|
|
465
466
|
continue
|
|
466
467
|
}
|
|
467
468
|
yield {
|
|
468
469
|
type: 'error',
|
|
469
|
-
message:
|
|
470
|
+
message: repairNudge.failureMessage,
|
|
470
471
|
discardAssistant: true,
|
|
471
472
|
}
|
|
472
473
|
yield doneEvent(false, stopReason)
|
|
@@ -485,26 +486,56 @@ function doneEvent(finishedNormally: boolean, stopReason?: TurnStopReason): Extr
|
|
|
485
486
|
return { type: 'done', finishedNormally }
|
|
486
487
|
}
|
|
487
488
|
|
|
489
|
+
type RepairNudge = {
|
|
490
|
+
text: string
|
|
491
|
+
reason: ContinuationNudgeReason
|
|
492
|
+
failureMessage: string
|
|
493
|
+
}
|
|
494
|
+
|
|
488
495
|
function nextToolResultRepairNudge(
|
|
489
496
|
provider: Pick<Provider, 'id' | 'supportsTools'>,
|
|
490
497
|
completedTools: ExecutedToolUse[],
|
|
491
|
-
):
|
|
498
|
+
): RepairNudge | null {
|
|
492
499
|
if (!provider.supportsTools) return null
|
|
493
500
|
const failedPrivateEdit = completedTools.some(completed =>
|
|
494
501
|
completed.name === 'propose_private_continuity_edit'
|
|
495
502
|
&& !completed.result.ok
|
|
496
503
|
&& completed.result.summary === 'propose_private_continuity_edit rejected input',
|
|
497
504
|
)
|
|
498
|
-
if (failedPrivateEdit)
|
|
505
|
+
if (failedPrivateEdit) {
|
|
506
|
+
return {
|
|
507
|
+
text: PRIVATE_CONTINUITY_REPAIR_NUDGE_TEXT,
|
|
508
|
+
reason: 'private_continuity_tool_repair',
|
|
509
|
+
failureMessage: 'Model called propose_private_continuity_edit with invalid input after corrective nudges',
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const failedWriteFile = completedTools.some(completed =>
|
|
514
|
+
completed.name === 'write_file'
|
|
515
|
+
&& !completed.result.ok
|
|
516
|
+
&& completed.result.summary === 'write_file rejected input',
|
|
517
|
+
)
|
|
518
|
+
if (failedWriteFile) {
|
|
519
|
+
return {
|
|
520
|
+
text: WRITE_FILE_REPAIR_NUDGE_TEXT,
|
|
521
|
+
reason: 'write_file_repair',
|
|
522
|
+
failureMessage: 'Model called write_file with invalid input after corrective nudges',
|
|
523
|
+
}
|
|
524
|
+
}
|
|
499
525
|
|
|
500
526
|
const failedWorkspacePrivateRead = completedTools.some(completed =>
|
|
501
527
|
completed.name === 'read_file'
|
|
502
528
|
&& !completed.result.ok
|
|
503
529
|
&& /read_private_continuity_file/.test(completed.result.content),
|
|
504
530
|
)
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
531
|
+
if (failedWorkspacePrivateRead) {
|
|
532
|
+
return {
|
|
533
|
+
text: 'The previous read_file call targeted private identity continuity markdown. Retry now with read_private_continuity_file and complete input such as {"file":"MEMORY.md"} or {"file":"SOUL.md"}. Do not search workspace folders.',
|
|
534
|
+
reason: 'private_continuity_tool_repair',
|
|
535
|
+
failureMessage: 'Model kept reading private continuity files via read_file after corrective nudges',
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return null
|
|
508
539
|
}
|
|
509
540
|
|
|
510
541
|
export function parseLocalModelTextToolUse(
|
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/sessions.ts
CHANGED
|
@@ -11,9 +11,10 @@ import {
|
|
|
11
11
|
isUserCorrectionOfToolState,
|
|
12
12
|
looksLikeToolStateClaim,
|
|
13
13
|
} from '../runtime/toolClaimGuards.js'
|
|
14
|
+
import { userTextToContentBlocks } from '../utils/images.js'
|
|
14
15
|
|
|
15
16
|
export type SessionMessage =
|
|
16
|
-
| { 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 }
|
|
17
18
|
| { version?: 2; role: 'assistant'; content: string; createdAt: string; model?: string; usage?: { in?: number; out?: number }; turnId?: string; synthetic?: boolean }
|
|
18
19
|
| { version?: 2; role: 'system'; content: string; createdAt: string; turnId?: string; synthetic?: boolean }
|
|
19
20
|
| { version: 2; role: 'tool_use'; toolUseId: string; name: string; input: Record<string, unknown>; createdAt: string; turnId?: string }
|
|
@@ -244,6 +245,17 @@ export type ProviderMessageProjectionOptions = {
|
|
|
244
245
|
export const TOOL_CORRECTION_CONTEXT_MESSAGE =
|
|
245
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.'
|
|
246
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
|
+
|
|
247
259
|
export function sessionMessagesToProviderMessages(
|
|
248
260
|
messages: SessionMessage[],
|
|
249
261
|
options: ProviderMessageProjectionOptions = {},
|
|
@@ -255,7 +267,7 @@ export function sessionMessagesToProviderMessages(
|
|
|
255
267
|
for (const [index, message] of messages.entries()) {
|
|
256
268
|
if (message.role === 'system' || message.role === 'user' || message.role === 'assistant') {
|
|
257
269
|
if (message.role === 'assistant' && invalidatedAssistantMessages.has(index)) continue
|
|
258
|
-
out.push({ role: message.role, content: message
|
|
270
|
+
out.push({ role: message.role, content: resolveUserContent(message) })
|
|
259
271
|
continue
|
|
260
272
|
}
|
|
261
273
|
if (message.role === 'tool_use') {
|
|
@@ -32,7 +32,7 @@ export const changeDirectoryTool: Tool<typeof schema> = {
|
|
|
32
32
|
path: fullPath,
|
|
33
33
|
relativePath: path.relative(context.workspaceRoot, fullPath) || path.basename(fullPath),
|
|
34
34
|
directoryPath: path.dirname(fullPath),
|
|
35
|
-
title: '
|
|
35
|
+
title: 'Allow directory change?',
|
|
36
36
|
subtitle: fullPath,
|
|
37
37
|
}
|
|
38
38
|
},
|
package/src/tools/contracts.ts
CHANGED
|
@@ -48,6 +48,16 @@ export type PermissionRequest =
|
|
|
48
48
|
file: 'SOUL.md' | 'MEMORY.md'
|
|
49
49
|
range: string
|
|
50
50
|
}
|
|
51
|
+
| {
|
|
52
|
+
kind: 'private-skill-read'
|
|
53
|
+
path: string
|
|
54
|
+
relativePath: string
|
|
55
|
+
directoryPath: string
|
|
56
|
+
title: string
|
|
57
|
+
subtitle: string
|
|
58
|
+
skillName: string
|
|
59
|
+
mode: 'list' | 'read'
|
|
60
|
+
}
|
|
51
61
|
| {
|
|
52
62
|
kind: 'private-continuity-edit'
|
|
53
63
|
path: string
|
|
@@ -32,7 +32,7 @@ export const deleteFileTool: Tool<typeof schema> = {
|
|
|
32
32
|
path: prepared.fullPath,
|
|
33
33
|
relativePath: prepared.relativePath,
|
|
34
34
|
directoryPath: path.dirname(prepared.fullPath),
|
|
35
|
-
title: '
|
|
35
|
+
title: 'Allow file delete?',
|
|
36
36
|
subtitle: prepared.fullPath,
|
|
37
37
|
before: preview(prepared.before),
|
|
38
38
|
after: '(deleted)',
|
package/src/tools/editTool.ts
CHANGED
|
@@ -41,7 +41,7 @@ export const editTool: Tool<typeof schema> = {
|
|
|
41
41
|
path: fullPath,
|
|
42
42
|
relativePath,
|
|
43
43
|
directoryPath: path.dirname(fullPath),
|
|
44
|
-
title: '
|
|
44
|
+
title: 'Allow file edit?',
|
|
45
45
|
subtitle: fullPath,
|
|
46
46
|
before: applied.previewBefore,
|
|
47
47
|
after: applied.previewAfter,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import {
|
|
4
|
+
continuityVaultRef,
|
|
5
|
+
} from '../identity/continuity/storage.js'
|
|
6
|
+
import { listSkillFiles } from '../identity/continuity/skills/loadSkills.js'
|
|
7
|
+
import type { Tool } from './contracts.js'
|
|
8
|
+
|
|
9
|
+
const schema = z.object({
|
|
10
|
+
name: z.string().min(1),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const listSkillFilesTool: Tool<typeof schema> = {
|
|
14
|
+
name: 'list_private_skill_files',
|
|
15
|
+
kind: 'private-continuity-read',
|
|
16
|
+
readOnly: true,
|
|
17
|
+
description: [
|
|
18
|
+
'List every file inside a private skill folder for the active identity.',
|
|
19
|
+
'Returns each file as `- <relativePath> (<bytes>) — <absolutePath>`, including SKILL.md and any supporting files (references, examples, scripts).',
|
|
20
|
+
'Use this after list_private_skills shows a skill with `(+N supporting files)` to discover what is available, then call read_private_skill with `file` to load text content.',
|
|
21
|
+
'Pass the absolute path directly to run_bash to execute supporting scripts (e.g. `python "<absolutePath>" <args>`).',
|
|
22
|
+
].join(' '),
|
|
23
|
+
inputSchema: schema,
|
|
24
|
+
inputSchemaJson: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
name: { type: 'string', description: 'Skill folder name from the private skills index.' },
|
|
28
|
+
},
|
|
29
|
+
required: ['name'],
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
},
|
|
32
|
+
parse(input) {
|
|
33
|
+
return schema.parse(input)
|
|
34
|
+
},
|
|
35
|
+
async buildPermissionRequest(input, context) {
|
|
36
|
+
const identity = context.config?.identity
|
|
37
|
+
if (!identity) throw new Error('No active identity; create or load an identity before listing private skill files')
|
|
38
|
+
const ref = continuityVaultRef(identity)
|
|
39
|
+
const folder = input.name.replace(/^.*:/, '')
|
|
40
|
+
const skillDir = path.join(ref.skillsDir, folder)
|
|
41
|
+
return {
|
|
42
|
+
kind: 'private-skill-read',
|
|
43
|
+
path: skillDir,
|
|
44
|
+
relativePath: `identity-vault/skills/${folder}`,
|
|
45
|
+
directoryPath: skillDir,
|
|
46
|
+
title: 'Allow private skill folder list?',
|
|
47
|
+
subtitle: `List files in ${folder}/`,
|
|
48
|
+
skillName: input.name,
|
|
49
|
+
mode: 'list',
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
async execute(input, context) {
|
|
53
|
+
const identity = context.config?.identity
|
|
54
|
+
if (!identity) {
|
|
55
|
+
return { ok: false, summary: 'no active identity', content: 'No active identity; cannot list private skill files.' }
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const folder = input.name.replace(/^.*:/, '')
|
|
59
|
+
const files = await listSkillFiles(identity, folder)
|
|
60
|
+
if (files.length === 0) {
|
|
61
|
+
return { ok: true, summary: `no files in ${folder}`, content: `Skill folder ${folder}/ is empty or does not exist.` }
|
|
62
|
+
}
|
|
63
|
+
const lines = files.map(f => `- ${f.relativePath} (${f.sizeBytes} bytes) — ${f.absolutePath}`)
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
summary: `listed ${files.length} file${files.length === 1 ? '' : 's'} in ${folder}`,
|
|
67
|
+
content: lines.join('\n'),
|
|
68
|
+
}
|
|
69
|
+
} catch (err: unknown) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
summary: 'list failed',
|
|
73
|
+
content: (err as Error).message,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import {
|
|
3
|
+
continuityVaultRef,
|
|
4
|
+
} from '../identity/continuity/storage.js'
|
|
5
|
+
import { listSkillsTree } from '../identity/continuity/skills/loadSkills.js'
|
|
6
|
+
import type { Tool } from './contracts.js'
|
|
7
|
+
|
|
8
|
+
const schema = z.object({})
|
|
9
|
+
|
|
10
|
+
export const listSkillsTool: Tool<typeof schema> = {
|
|
11
|
+
name: 'list_private_skills',
|
|
12
|
+
kind: 'private-continuity-read',
|
|
13
|
+
readOnly: true,
|
|
14
|
+
description: [
|
|
15
|
+
'List private skills available in the owner-authored skills tree for the active identity.',
|
|
16
|
+
'Returns each skill folder name with its one-line description; bodies are loaded separately via read_private_skill.',
|
|
17
|
+
'When a skill has supporting files beyond SKILL.md, the entry is annotated with (+N supporting files) so you know to call list_private_skill_files.',
|
|
18
|
+
'Use this when the user mentions a skill not in the injected skill index, or to discover what skills exist.',
|
|
19
|
+
].join(' '),
|
|
20
|
+
inputSchema: schema,
|
|
21
|
+
inputSchemaJson: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {},
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
},
|
|
26
|
+
parse(input) {
|
|
27
|
+
return schema.parse(input ?? {})
|
|
28
|
+
},
|
|
29
|
+
async buildPermissionRequest(_input, context) {
|
|
30
|
+
const identity = context.config?.identity
|
|
31
|
+
if (!identity) throw new Error('No active identity; create or load an identity before listing private skills')
|
|
32
|
+
const ref = continuityVaultRef(identity)
|
|
33
|
+
return {
|
|
34
|
+
kind: 'private-skill-read',
|
|
35
|
+
path: ref.skillsDir,
|
|
36
|
+
relativePath: 'identity-vault/skills',
|
|
37
|
+
directoryPath: ref.skillsDir,
|
|
38
|
+
title: 'Allow private skills index read?',
|
|
39
|
+
subtitle: 'List skill names and descriptions from the private skills tree',
|
|
40
|
+
skillName: '*',
|
|
41
|
+
mode: 'list',
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
async execute(_input, context) {
|
|
45
|
+
const identity = context.config?.identity
|
|
46
|
+
if (!identity) {
|
|
47
|
+
return { ok: false, summary: 'no active identity', content: 'No active identity; cannot list private skills.' }
|
|
48
|
+
}
|
|
49
|
+
const { skills, supportingCounts } = await listSkillsTree(identity)
|
|
50
|
+
if (skills.length === 0) {
|
|
51
|
+
return { ok: true, summary: 'no private skills', content: 'The private skills tree is empty.' }
|
|
52
|
+
}
|
|
53
|
+
const lines = skills.map(entry => {
|
|
54
|
+
const display = entry.displayName ?? entry.name
|
|
55
|
+
const desc = entry.description ? ` — ${entry.description}` : ''
|
|
56
|
+
const when = entry.whenToUse ? ` (when: ${entry.whenToUse})` : ''
|
|
57
|
+
const vis = entry.visibility !== 'private' ? ` [visibility: ${entry.visibility}]` : ''
|
|
58
|
+
const supporting = supportingCounts[entry.name] ?? 0
|
|
59
|
+
const trailer = supporting > 0 ? ` (+${supporting} supporting file${supporting === 1 ? '' : 's'})` : ''
|
|
60
|
+
return `- ${display}${desc}${when}${vis}${trailer}`
|
|
61
|
+
})
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
summary: `listed ${skills.length} private skill${skills.length === 1 ? '' : 's'}`,
|
|
65
|
+
content: lines.join('\n'),
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
}
|
|
@@ -30,7 +30,7 @@ export const listMcpResourcesTool: Tool<typeof ListMcpResourcesInput> = {
|
|
|
30
30
|
const serverName = input.server ?? '*'
|
|
31
31
|
return {
|
|
32
32
|
kind: 'mcp',
|
|
33
|
-
title: '
|
|
33
|
+
title: 'Allow MCP resource listing?',
|
|
34
34
|
subtitle: input.server ? `list resources from ${input.server}` : 'list resources from all connected MCP servers',
|
|
35
35
|
serverName,
|
|
36
36
|
normalizedServerName: normalizeNameForMcp(serverName),
|
|
@@ -72,7 +72,7 @@ export const readMcpResourceTool: Tool<typeof ReadMcpResourceInput> = {
|
|
|
72
72
|
async buildPermissionRequest(input) {
|
|
73
73
|
return {
|
|
74
74
|
kind: 'mcp',
|
|
75
|
-
title: '
|
|
75
|
+
title: 'Allow MCP resource read?',
|
|
76
76
|
subtitle: `${input.server} / ${input.uri}`,
|
|
77
77
|
serverName: input.server,
|
|
78
78
|
normalizedServerName: normalizeNameForMcp(input.server),
|
|
@@ -43,7 +43,7 @@ export const privateContinuityReadTool: Tool<typeof schema> = {
|
|
|
43
43
|
path: prepared.fullPath,
|
|
44
44
|
relativePath: prepared.relativePath,
|
|
45
45
|
directoryPath: prepared.directoryPath,
|
|
46
|
-
title: '
|
|
46
|
+
title: 'Allow private continuity read?',
|
|
47
47
|
subtitle: input.startLine || input.endLine
|
|
48
48
|
? `${prepared.fullPath} · lines ${input.startLine ?? 1}-${input.endLine ?? 'end'}`
|
|
49
49
|
: prepared.fullPath,
|