ethagent 2.4.0 → 3.0.1
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 +7 -4
- package/package.json +2 -1
- package/src/app/FirstRun.tsx +155 -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 +3 -1
- package/src/chat/ChatScreen.tsx +7 -1
- package/src/chat/ConversationStack.tsx +25 -19
- package/src/chat/MessageList.tsx +194 -53
- package/src/chat/chatSessionState.ts +1 -1
- package/src/chat/chatTurnOrchestrator.ts +59 -0
- package/src/chat/input/ChatInput.tsx +3 -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/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 +134 -9
- package/src/identity/continuity/publicSkills.ts +54 -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 +79 -5
- package/src/identity/hub/Routes.tsx +5 -3
- package/src/identity/hub/continuity/ContinuityDashboardScreen.tsx +7 -73
- package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +6 -6
- package/src/identity/hub/continuity/SavePromptScreen.tsx +1 -2
- 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/NewSkillScreen.tsx +57 -0
- package/src/identity/hub/continuity/skills/NewSkillVisibilityScreen.tsx +52 -0
- package/src/identity/hub/continuity/skills/SkillActionsScreen.tsx +151 -0
- package/src/identity/hub/continuity/skills/SkillsTreeScreen.tsx +181 -0
- package/src/identity/hub/continuity/snapshot.ts +3 -0
- package/src/identity/hub/continuity/state.ts +9 -8
- package/src/identity/hub/continuity/vault.ts +42 -10
- package/src/identity/hub/create/CreateFlow.tsx +1 -1
- package/src/identity/hub/custody/CustodyEditFlow.tsx +3 -3
- package/src/identity/hub/custody/routes.tsx +1 -1
- package/src/identity/hub/ens/EnsEditAdvancedScreens.tsx +0 -1
- package/src/identity/hub/ens/EnsEditMaintenanceScreens.tsx +0 -1
- package/src/identity/hub/identityHubReducer.ts +15 -0
- package/src/identity/hub/profile/EditProfileFlow.tsx +5 -5
- 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 +14 -4
- 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 +118 -54
- package/src/identity/hub/shared/components/MenuScreen.tsx +21 -18
- package/src/identity/hub/shared/components/UnlinkedIdentityScreen.tsx +4 -4
- 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/identity/wallet/page/copy.ts +43 -43
- package/src/mcp/manager.ts +1 -1
- package/src/models/ModelPicker.tsx +89 -84
- package/src/models/llamacpp.ts +160 -11
- package/src/models/llamacppPreflight.ts +1 -16
- package/src/models/modelPickerOptions.ts +45 -37
- package/src/providers/contracts.ts +1 -0
- package/src/providers/openai-chat.ts +50 -9
- package/src/providers/openai-responses.ts +19 -4
- package/src/runtime/toolExecution.ts +4 -3
- package/src/runtime/turn.ts +61 -30
- 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 +1 -1
- 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
|
@@ -26,9 +26,6 @@ export type LlamaCppPreflightDeps = {
|
|
|
26
26
|
timeoutMs?: number
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const UNTRACKED_VISION_DETAIL =
|
|
30
|
-
'A llama-server is already serving this alias but ethagent did not launch it, so we cannot apply the vision projector. Stop the external process and reopen ethagent.'
|
|
31
|
-
|
|
32
29
|
type ModelsProbe =
|
|
33
30
|
| { up: true; models: string[] }
|
|
34
31
|
| { up: false; models: [] }
|
|
@@ -68,19 +65,7 @@ export async function ensureLlamaCppRunnerReady(
|
|
|
68
65
|
}
|
|
69
66
|
}
|
|
70
67
|
if (!local.mmprojPath) return { ok: true, alreadyRunning: true }
|
|
71
|
-
|
|
72
|
-
if (stopped && stopped.ok && stopped.reason === 'untracked-server') {
|
|
73
|
-
return withPreflightMessage(
|
|
74
|
-
{
|
|
75
|
-
ok: false,
|
|
76
|
-
code: 'untracked-server',
|
|
77
|
-
message: UNTRACKED_VISION_DETAIL,
|
|
78
|
-
detail: UNTRACKED_VISION_DETAIL,
|
|
79
|
-
servedModels: stopped.servedModels,
|
|
80
|
-
},
|
|
81
|
-
local,
|
|
82
|
-
)
|
|
83
|
-
}
|
|
68
|
+
await (deps.stopServer ?? stopLlamaCppServer)().catch(() => null)
|
|
84
69
|
}
|
|
85
70
|
|
|
86
71
|
const result = await (deps.startServer ?? startLlamaCppServer)({
|
|
@@ -61,60 +61,68 @@ const CHILD_INDENT = 4
|
|
|
61
61
|
export function buildModelPickerOptions(
|
|
62
62
|
data: ModelPickerOptionsData,
|
|
63
63
|
context: ModelPickerOptionsContext,
|
|
64
|
+
options_: { localOnly?: boolean } = {},
|
|
64
65
|
): SelectOption<string>[] {
|
|
66
|
+
const localOnly = options_.localOnly === true
|
|
65
67
|
const options: SelectOption<string>[] = []
|
|
66
68
|
|
|
67
69
|
options.push(sectionOption('hdr:local', 'Local Models'))
|
|
68
70
|
appendHfModelOptions(options, data, context, 'Added From Links', 46)
|
|
71
|
+
options.push(groupOption('hdr:local:manage', 'Manage'))
|
|
69
72
|
options.push(utilityOption('hf:download', 'Add Local Model File', LOCAL_MODEL_LINK_HINT))
|
|
70
73
|
options.push(utilityOption('local:catalog', 'View Full Catalog', 'Curated local GGUF files'))
|
|
71
74
|
if (data.hfModels.length > 0) {
|
|
72
75
|
options.push(utilityOption('local:uninstall', 'Uninstall Downloaded GGUF'))
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
78
|
+
if (!localOnly) {
|
|
79
|
+
options.push(sectionOption('hdr:cloud', 'Cloud'))
|
|
80
|
+
for (const provider of MODEL_PICKER_CLOUD_PROVIDERS) {
|
|
81
|
+
options.push(groupOption(`hdr:cloud:${provider}`, cloudProviderDisplayName(provider)))
|
|
82
|
+
const keySet = data.cloudKeys[provider] === true
|
|
83
|
+
if (!keySet) {
|
|
84
|
+
if (provider === 'openai') {
|
|
85
|
+
options.push(utilityOption('oauth:openai', 'Sign in with ChatGPT', 'Use your ChatGPT subscription'))
|
|
86
|
+
}
|
|
87
|
+
options.push(utilityOption(`key:set:${provider}`, 'Add API Key'))
|
|
88
|
+
continue
|
|
82
89
|
}
|
|
83
|
-
options.push(utilityOption(`key:set:${provider}`, 'Add API Key'))
|
|
84
|
-
continue
|
|
85
|
-
}
|
|
86
90
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
const catalog = data.cloudCatalogs[provider]
|
|
92
|
+
if (catalog?.status === 'fallback') {
|
|
93
|
+
const reason = catalog.error ? ` · ${catalog.error}` : ''
|
|
94
|
+
options.push(noticeOption(
|
|
95
|
+
`hdr:cloud-fallback:${provider}`,
|
|
96
|
+
`Catalog unavailable${reason} · showing configured model`,
|
|
97
|
+
CHILD_INDENT,
|
|
98
|
+
))
|
|
99
|
+
}
|
|
96
100
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
101
|
+
const models = orderModelsForContextFit(provider, cloudPickerModels(provider, catalog, context), context.contextFit)
|
|
102
|
+
if (models.length === 0) {
|
|
103
|
+
options.push(noticeOption(`hdr:cloud-empty:${provider}`, 'No selectable models', CHILD_INDENT))
|
|
104
|
+
}
|
|
105
|
+
for (const model of models) {
|
|
106
|
+
const active = context.currentProvider === provider && context.currentModel === model
|
|
107
|
+
const displayName = formatModelDisplayName(provider, model, { maxLength: 58 })
|
|
108
|
+
options.push(rowOption(
|
|
109
|
+
`c:${provider}:${model}`,
|
|
110
|
+
contextFitLabel(provider, model, `${displayName}${active ? ' *' : ''}`, context.contextFit),
|
|
111
|
+
))
|
|
112
|
+
}
|
|
113
|
+
options.push(groupOption(`hdr:cloud:${provider}:manage`, 'Manage'))
|
|
114
|
+
options.push(utilityOption(`catalog:${provider}`, 'Full Catalog'))
|
|
115
|
+
const manageLabel = provider === 'openai' && data.cloudCredentialKinds?.openai === 'oauth'
|
|
116
|
+
? 'Manage ChatGPT Sign-in'
|
|
117
|
+
: 'Manage API Key'
|
|
118
|
+
options.push(utilityOption(`key:manage:${provider}`, manageLabel))
|
|
108
119
|
}
|
|
109
|
-
options.push(utilityOption(`catalog:${provider}`, 'Full Catalog'))
|
|
110
|
-
const manageLabel = provider === 'openai' && data.cloudCredentialKinds?.openai === 'oauth'
|
|
111
|
-
? 'Manage ChatGPT Sign-in'
|
|
112
|
-
: 'Manage API Key'
|
|
113
|
-
options.push(utilityOption(`key:manage:${provider}`, manageLabel))
|
|
114
120
|
}
|
|
115
121
|
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
if (!localOnly) {
|
|
123
|
+
options.push(sectionOption('hdr:exit', 'Exit'))
|
|
124
|
+
options.push(utilityOption('cancel', 'Close Model Picker', 'Return to chat'))
|
|
125
|
+
}
|
|
118
126
|
|
|
119
127
|
return options
|
|
120
128
|
}
|
|
@@ -42,6 +42,7 @@ export type ProviderRetryStreamEvent = { type: 'retry' } & RetryEvent
|
|
|
42
42
|
export type StreamEvent =
|
|
43
43
|
| { type: 'text'; delta: string }
|
|
44
44
|
| { type: 'thinking'; delta: string }
|
|
45
|
+
| { type: 'thinking_end' }
|
|
45
46
|
| ProviderRetryStreamEvent
|
|
46
47
|
| { type: 'tool_use_start'; id: string; name: string }
|
|
47
48
|
| { type: 'tool_use_delta'; id: string; delta: string }
|
|
@@ -171,6 +171,7 @@ export class OpenAIChatProvider implements Provider {
|
|
|
171
171
|
let stopReason: DoneStopReason = 'unknown'
|
|
172
172
|
const toolCalls = new Map<number, StreamingToolCall>()
|
|
173
173
|
const contentThinkingParser = new ContentThinkingParser(this.id)
|
|
174
|
+
let reasoningPending = false
|
|
174
175
|
|
|
175
176
|
try {
|
|
176
177
|
for await (const frame of iterSseFrames(response.body, signal, READ_TIMEOUT_MS)) {
|
|
@@ -194,18 +195,34 @@ export class OpenAIChatProvider implements Provider {
|
|
|
194
195
|
? delta.thinking
|
|
195
196
|
: ''
|
|
196
197
|
|
|
197
|
-
if (reasoning.length > 0)
|
|
198
|
+
if (reasoning.length > 0) {
|
|
199
|
+
yield { type: 'thinking', delta: reasoning }
|
|
200
|
+
reasoningPending = true
|
|
201
|
+
}
|
|
198
202
|
if (text.length > 0) {
|
|
203
|
+
if (reasoningPending) {
|
|
204
|
+
yield { type: 'thinking_end' }
|
|
205
|
+
reasoningPending = false
|
|
206
|
+
}
|
|
199
207
|
for (const event of contentThinkingParser.push(text)) {
|
|
200
208
|
yield event
|
|
201
209
|
}
|
|
202
210
|
}
|
|
203
211
|
|
|
204
|
-
|
|
212
|
+
const toolCallDeltas = delta?.tool_calls ?? []
|
|
213
|
+
if (toolCallDeltas.length > 0 && reasoningPending) {
|
|
214
|
+
yield { type: 'thinking_end' }
|
|
215
|
+
reasoningPending = false
|
|
216
|
+
}
|
|
217
|
+
for (const event of applyStreamingToolCallDelta(toolCalls, toolCallDeltas)) {
|
|
205
218
|
yield event
|
|
206
219
|
}
|
|
207
220
|
|
|
208
221
|
if (choice?.finish_reason) {
|
|
222
|
+
if (reasoningPending) {
|
|
223
|
+
yield { type: 'thinking_end' }
|
|
224
|
+
reasoningPending = false
|
|
225
|
+
}
|
|
209
226
|
stopReason = normalizeFinishReason(choice.finish_reason)
|
|
210
227
|
}
|
|
211
228
|
if (parsed.usage) {
|
|
@@ -223,6 +240,10 @@ export class OpenAIChatProvider implements Provider {
|
|
|
223
240
|
for (const event of contentThinkingParser.flush()) {
|
|
224
241
|
yield event
|
|
225
242
|
}
|
|
243
|
+
if (reasoningPending) {
|
|
244
|
+
yield { type: 'thinking_end' }
|
|
245
|
+
reasoningPending = false
|
|
246
|
+
}
|
|
226
247
|
|
|
227
248
|
let streamEmittedToolUses = 0
|
|
228
249
|
if (stopReason === 'tool_use' || toolCalls.size > 0) {
|
|
@@ -383,17 +404,35 @@ function isToolResultBlock(block: MessageContentBlock): block is Extract<Message
|
|
|
383
404
|
|
|
384
405
|
function parseToolArguments(inputJson: string): Record<string, unknown> {
|
|
385
406
|
if (!inputJson.trim()) return {}
|
|
407
|
+
const direct = tryParseJsonOnce(inputJson)
|
|
408
|
+
if (direct !== undefined) return coerceToToolArguments(direct)
|
|
409
|
+
const repaired = repairJsonObject(inputJson)
|
|
410
|
+
if (!repaired) return {}
|
|
411
|
+
const parsedRepaired = tryParseJsonOnce(repaired)
|
|
412
|
+
return parsedRepaired === undefined ? {} : coerceToToolArguments(parsedRepaired)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function tryParseJsonOnce(value: string): unknown {
|
|
386
416
|
try {
|
|
387
|
-
return JSON.parse(
|
|
417
|
+
return JSON.parse(value)
|
|
388
418
|
} catch {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
419
|
+
return undefined
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function coerceToToolArguments(value: unknown): Record<string, unknown> {
|
|
424
|
+
if (typeof value === 'string') {
|
|
425
|
+
const trimmed = value.trim()
|
|
426
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
427
|
+
const inner = tryParseJsonOnce(trimmed)
|
|
428
|
+
if (inner !== undefined) return coerceToToolArguments(inner)
|
|
395
429
|
}
|
|
430
|
+
return {}
|
|
431
|
+
}
|
|
432
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
433
|
+
return value as Record<string, unknown>
|
|
396
434
|
}
|
|
435
|
+
return {}
|
|
397
436
|
}
|
|
398
437
|
|
|
399
438
|
function* applyStreamingToolCallDelta(
|
|
@@ -486,7 +525,9 @@ class ContentThinkingParser {
|
|
|
486
525
|
yield { type: this.state === 'thinking' ? 'thinking' : 'text', delta: before }
|
|
487
526
|
}
|
|
488
527
|
this.buffer = this.buffer.slice(tagIndex + tag.length)
|
|
528
|
+
const wasThinking = this.state === 'thinking'
|
|
489
529
|
this.state = this.state === 'text' ? 'thinking' : 'text'
|
|
530
|
+
if (wasThinking) yield { type: 'thinking_end' }
|
|
490
531
|
continue
|
|
491
532
|
}
|
|
492
533
|
|
|
@@ -276,15 +276,30 @@ function parseToolArguments(input: string): Record<string, unknown> {
|
|
|
276
276
|
const trimmed = input.trim()
|
|
277
277
|
if (!trimmed) return {}
|
|
278
278
|
try {
|
|
279
|
-
|
|
280
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
281
|
-
? (parsed as Record<string, unknown>)
|
|
282
|
-
: {}
|
|
279
|
+
return coerceToToolArguments(JSON.parse(trimmed))
|
|
283
280
|
} catch {
|
|
284
281
|
return {}
|
|
285
282
|
}
|
|
286
283
|
}
|
|
287
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
|
+
|
|
288
303
|
function networkErrorMessage(baseUrl: string, err: unknown, fallback = 'network error'): string {
|
|
289
304
|
const message = (err as Error).message || fallback
|
|
290
305
|
return `openai request failed at ${baseUrl}: ${message}`
|
|
@@ -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(
|
|
@@ -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
|
+
}
|