ethagent 3.0.1 → 3.1.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 +6 -1
- package/package.json +3 -1
- package/src/app/FirstRun.tsx +1 -24
- package/src/app/firstRunConfig.ts +26 -0
- package/src/auth/openaiOAuth/landingPage.ts +2 -11
- package/src/chat/ChatScreen.tsx +32 -117
- package/src/chat/MessageList.tsx +18 -260
- package/src/chat/chatEnvironment.ts +16 -0
- package/src/chat/chatTurnContext.ts +50 -0
- package/src/chat/chatTurnOrchestrator.ts +5 -112
- package/src/chat/chatTurnRows.ts +64 -0
- package/src/chat/commands.ts +3 -178
- package/src/chat/continuityEditReview.ts +42 -0
- package/src/chat/input/ChatInput.tsx +10 -144
- package/src/chat/input/chatInputHelpers.ts +62 -0
- package/src/chat/input/inputRendering.tsx +93 -0
- package/src/chat/messageMarkdown.ts +220 -0
- package/src/chat/messageRows.ts +43 -0
- package/src/chat/planImplementation.ts +62 -0
- package/src/chat/slashCommandHandlers.ts +165 -0
- package/src/chat/slashCommandViews.ts +120 -0
- package/src/cli/main.tsx +7 -0
- package/src/identity/continuity/challenges.ts +123 -0
- package/src/identity/continuity/envelope.ts +49 -1484
- package/src/identity/continuity/envelopeCreate.ts +322 -0
- package/src/identity/continuity/envelopeCrypto.ts +182 -0
- package/src/identity/continuity/envelopeParse.ts +441 -0
- package/src/identity/continuity/envelopeTypes.ts +204 -0
- package/src/identity/continuity/envelopeVersion.ts +1 -0
- package/src/identity/continuity/payloadNormalization.ts +183 -0
- package/src/identity/continuity/publicSkills.ts +5 -5
- package/src/identity/continuity/skills/loadSkills.ts +12 -69
- package/src/identity/continuity/skills/skillPaths.ts +76 -0
- package/src/identity/continuity/skillsNormalization.ts +119 -0
- package/src/identity/continuity/snapshotToken.ts +28 -0
- package/src/identity/hub/continuity/completion.ts +67 -0
- package/src/identity/hub/continuity/effects.ts +5 -62
- package/src/identity/hub/profile/effects.ts +6 -170
- package/src/identity/hub/profile/operatorSave.ts +202 -0
- package/src/identity/registry/erc8004/metadata.ts +31 -23
- package/src/identity/wallet/browserWallet/html.ts +1 -57
- package/src/identity/wallet/browserWallet/walletPageSource.ts +85 -0
- package/src/identity/wallet/page/controller.ts +1 -1
- package/src/identity/wallet/page/errorView.ts +122 -0
- package/src/identity/wallet/page/view.ts +3 -114
- package/src/mcp/manager.ts +8 -66
- package/src/mcp/managerHelpers.ts +70 -0
- package/src/models/ModelPicker.tsx +69 -889
- package/src/models/huggingface.ts +20 -137
- package/src/models/huggingfaceStorage.ts +136 -0
- package/src/models/llamacpp.ts +37 -303
- package/src/models/llamacppCommands.ts +44 -0
- package/src/models/llamacppConfig.ts +34 -0
- package/src/models/llamacppDiscovery.ts +176 -0
- package/src/models/llamacppOutput.ts +65 -0
- package/src/models/modelPickerCatalogFlow.ts +56 -0
- package/src/models/modelPickerCredentials.ts +166 -0
- package/src/models/modelPickerData.ts +41 -0
- package/src/models/modelPickerDisplay.tsx +132 -0
- package/src/models/modelPickerHfFlow.ts +192 -0
- package/src/models/modelPickerLocalRunnerFlow.ts +115 -0
- package/src/models/modelPickerTypes.ts +69 -0
- package/src/models/modelPickerUninstallFlow.ts +48 -0
- package/src/models/modelPickerViewHelpers.ts +174 -0
- package/src/providers/openai-chat.ts +5 -124
- package/src/providers/openaiChatWire.ts +124 -0
- package/src/runtime/providerTurn.ts +38 -0
- package/src/runtime/textToolParser.ts +161 -0
- package/src/runtime/toolIntent.ts +1 -1
- package/src/runtime/turn.ts +43 -499
- package/src/runtime/turnNudges.ts +223 -0
- package/src/runtime/turnTypes.ts +86 -0
- package/src/ui/terminalTitle.ts +30 -0
package/src/runtime/turn.ts
CHANGED
|
@@ -1,165 +1,49 @@
|
|
|
1
|
-
import type { Message, Provider, ProviderRetryStreamEvent, StreamEvent } from '../providers/contracts.js'
|
|
2
|
-
import type { ToolResult } from '../tools/contracts.js'
|
|
3
|
-
import { getTool } from '../tools/registry.js'
|
|
4
1
|
import {
|
|
5
|
-
looksLikeToolStateClaim,
|
|
6
2
|
unsupportedToolStateClaims,
|
|
7
3
|
type ToolEvidence,
|
|
8
4
|
} from './toolClaimGuards.js'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
case 'retry': return event
|
|
52
|
-
case 'tool_use_start': return event
|
|
53
|
-
case 'tool_use_delta': return event
|
|
54
|
-
case 'tool_use_stop': return event
|
|
55
|
-
case 'done': return { type: 'done', stopReason: event.stopReason }
|
|
56
|
-
case 'error': return { type: 'error', message: event.message }
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export const MAX_CONTINUATION_NUDGES = 3
|
|
61
|
-
export const MAX_TOOL_USES_PER_TURN = 25
|
|
62
|
-
|
|
63
|
-
export type ContinuationNudgeReason =
|
|
64
|
-
| 'continuation'
|
|
65
|
-
| 'tool_capability'
|
|
66
|
-
| 'tool_state_claim'
|
|
67
|
-
| 'tool_protocol_fake'
|
|
68
|
-
| 'tool_delegation'
|
|
69
|
-
| 'tool_budget'
|
|
70
|
-
| 'private_continuity_tool'
|
|
71
|
-
| 'private_continuity_tool_repair'
|
|
72
|
-
| 'write_file_repair'
|
|
73
|
-
| 'reasoning_only'
|
|
74
|
-
|
|
75
|
-
const CONTINUATION_NUDGE_TEXT =
|
|
76
|
-
'Continue with the task. Use the appropriate tools to proceed.'
|
|
77
|
-
|
|
78
|
-
const TOOL_CAPABILITY_NUDGE_TEXT =
|
|
79
|
-
'You do have access to the provided tools in this environment. Continue by making the appropriate tool call; do not ask the user to run commands or paste command output.'
|
|
80
|
-
|
|
81
|
-
const TOOL_STATE_CLAIM_NUDGE_TEXT =
|
|
82
|
-
'Do not claim that files, directories, or workspace state changed unless you have executed the appropriate tool. Call the tool now.'
|
|
83
|
-
|
|
84
|
-
const TOOL_PROTOCOL_FAKE_NUDGE_TEXT =
|
|
85
|
-
'The previous response printed tool names or a tool menu instead of calling a tool. Tool names are not text output. Make exactly one native tool call now.'
|
|
86
|
-
|
|
87
|
-
const TOOL_DELEGATION_NUDGE_TEXT =
|
|
88
|
-
'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.'
|
|
89
|
-
|
|
90
|
-
const TOOL_BUDGET_NUDGE_TEXT =
|
|
91
|
-
'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.'
|
|
92
|
-
|
|
93
|
-
const PRIVATE_CONTINUITY_NUDGE_TEXT =
|
|
94
|
-
'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."}.'
|
|
95
|
-
|
|
96
|
-
const PRIVATE_CONTINUITY_REPAIR_NUDGE_TEXT =
|
|
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."}.'
|
|
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
|
-
|
|
102
|
-
const REASONING_ONLY_NUDGE_TEXT =
|
|
103
|
-
'You produced private reasoning but no user-visible answer. Answer the user now in visible text. Do not continue only with reasoning.'
|
|
104
|
-
|
|
105
|
-
export type TurnEvent =
|
|
106
|
-
| { type: 'iteration_start'; index: number }
|
|
107
|
-
| { type: 'text'; delta: string }
|
|
108
|
-
| { type: 'thinking'; delta: string }
|
|
109
|
-
| { type: 'thinking_end' }
|
|
110
|
-
| ProviderRetryStreamEvent
|
|
111
|
-
| { type: 'tool_use_start'; id: string; name: string }
|
|
112
|
-
| { type: 'tool_use_delta'; id: string; delta: string }
|
|
113
|
-
| {
|
|
114
|
-
type: 'tool_use_stop'
|
|
115
|
-
id: string
|
|
116
|
-
name: string
|
|
117
|
-
input: Record<string, unknown>
|
|
118
|
-
}
|
|
119
|
-
| { type: 'assistant_message_committed'; text: string }
|
|
120
|
-
| {
|
|
121
|
-
type: 'tool_executed'
|
|
122
|
-
id: string
|
|
123
|
-
name: string
|
|
124
|
-
input: Record<string, unknown>
|
|
125
|
-
result: ToolResult
|
|
126
|
-
cwd: string
|
|
127
|
-
}
|
|
128
|
-
| { type: 'continuation_nudge'; attempt: number; reason: ContinuationNudgeReason }
|
|
129
|
-
| { type: 'local_tool_recovery' }
|
|
130
|
-
| { type: 'error'; message: string; discardAssistant?: boolean }
|
|
131
|
-
| { type: 'cancelled' }
|
|
132
|
-
| { type: 'done'; finishedNormally: boolean; stopReason?: TurnStopReason }
|
|
133
|
-
|
|
134
|
-
export type PendingToolUse = {
|
|
135
|
-
id: string
|
|
136
|
-
name: string
|
|
137
|
-
input: Record<string, unknown>
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export type ExecutedToolUse = {
|
|
141
|
-
id: string
|
|
142
|
-
name: string
|
|
143
|
-
input: Record<string, unknown>
|
|
144
|
-
result: ToolResult
|
|
145
|
-
cwd: string
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export type ToolBatchRunner = (
|
|
149
|
-
pendingToolUses: PendingToolUse[],
|
|
150
|
-
) => Promise<{ cancelled: boolean; completedTools: ExecutedToolUse[] }>
|
|
151
|
-
|
|
152
|
-
export type RebuildMessages = () => Message[] | Promise<Message[]>
|
|
153
|
-
|
|
154
|
-
export type RuntimeTurnParams = {
|
|
155
|
-
provider: Provider
|
|
156
|
-
signal: AbortSignal
|
|
157
|
-
initialMessages: Message[]
|
|
158
|
-
rebuildMessages: RebuildMessages
|
|
159
|
-
runToolBatch: ToolBatchRunner
|
|
160
|
-
maxContinuationNudges?: number
|
|
161
|
-
}
|
|
162
|
-
|
|
5
|
+
import { parseLocalModelTextToolUses } from './textToolParser.js'
|
|
6
|
+
import { runProviderTurn } from './providerTurn.js'
|
|
7
|
+
import {
|
|
8
|
+
MAX_CONTINUATION_NUDGES,
|
|
9
|
+
MAX_TOOL_USES_PER_TURN,
|
|
10
|
+
REASONING_ONLY_NUDGE_TEXT,
|
|
11
|
+
TOOL_BUDGET_NUDGE_TEXT,
|
|
12
|
+
TOOL_DELEGATION_NUDGE_TEXT,
|
|
13
|
+
TOOL_PROTOCOL_FAKE_NUDGE_TEXT,
|
|
14
|
+
TOOL_STATE_CLAIM_REPAIR_NUDGE_TEXT,
|
|
15
|
+
looksLikeFakeToolProtocolText,
|
|
16
|
+
looksLikeToolDelegationText,
|
|
17
|
+
nextNudge,
|
|
18
|
+
nextToolResultRepairNudge,
|
|
19
|
+
} from './turnNudges.js'
|
|
20
|
+
import type {
|
|
21
|
+
PendingToolUse,
|
|
22
|
+
RuntimeTurnParams,
|
|
23
|
+
TurnEvent,
|
|
24
|
+
TurnStopReason,
|
|
25
|
+
} from './turnTypes.js'
|
|
26
|
+
|
|
27
|
+
export { parseLocalModelTextToolUse, parseLocalModelTextToolUses } from './textToolParser.js'
|
|
28
|
+
export {
|
|
29
|
+
MAX_CONTINUATION_NUDGES,
|
|
30
|
+
MAX_TOOL_USES_PER_TURN,
|
|
31
|
+
looksLikeContinuationIntent,
|
|
32
|
+
looksLikeFakeToolProtocolText,
|
|
33
|
+
looksLikePrivateContinuityWorkspaceCreationIntent,
|
|
34
|
+
looksLikeToolCapabilityConfusion,
|
|
35
|
+
looksLikeToolDelegationText,
|
|
36
|
+
looksLikeToolStateClaimWithoutTool,
|
|
37
|
+
} from './turnNudges.js'
|
|
38
|
+
export type {
|
|
39
|
+
ContinuationNudgeReason,
|
|
40
|
+
ExecutedToolUse,
|
|
41
|
+
PendingToolUse,
|
|
42
|
+
RebuildMessages,
|
|
43
|
+
RuntimeTurnParams,
|
|
44
|
+
ToolBatchRunner,
|
|
45
|
+
TurnEvent,
|
|
46
|
+
} from './turnTypes.js'
|
|
163
47
|
export async function* runRuntimeTurn(
|
|
164
48
|
params: RuntimeTurnParams,
|
|
165
49
|
): AsyncGenerator<TurnEvent, void, void> {
|
|
@@ -333,9 +217,7 @@ export async function* runRuntimeTurn(
|
|
|
333
217
|
{
|
|
334
218
|
role: 'user',
|
|
335
219
|
content:
|
|
336
|
-
|
|
337
|
-
+ 'Treat that claim as unreliable. '
|
|
338
|
-
+ TOOL_STATE_CLAIM_NUDGE_TEXT,
|
|
220
|
+
TOOL_STATE_CLAIM_REPAIR_NUDGE_TEXT,
|
|
339
221
|
},
|
|
340
222
|
]
|
|
341
223
|
continue
|
|
@@ -485,341 +367,3 @@ function doneEvent(finishedNormally: boolean, stopReason?: TurnStopReason): Extr
|
|
|
485
367
|
}
|
|
486
368
|
return { type: 'done', finishedNormally }
|
|
487
369
|
}
|
|
488
|
-
|
|
489
|
-
type RepairNudge = {
|
|
490
|
-
text: string
|
|
491
|
-
reason: ContinuationNudgeReason
|
|
492
|
-
failureMessage: string
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function nextToolResultRepairNudge(
|
|
496
|
-
provider: Pick<Provider, 'id' | 'supportsTools'>,
|
|
497
|
-
completedTools: ExecutedToolUse[],
|
|
498
|
-
): RepairNudge | null {
|
|
499
|
-
if (!provider.supportsTools) return null
|
|
500
|
-
const failedPrivateEdit = completedTools.some(completed =>
|
|
501
|
-
completed.name === 'propose_private_continuity_edit'
|
|
502
|
-
&& !completed.result.ok
|
|
503
|
-
&& completed.result.summary === 'propose_private_continuity_edit rejected input',
|
|
504
|
-
)
|
|
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
|
-
}
|
|
525
|
-
|
|
526
|
-
const failedWorkspacePrivateRead = completedTools.some(completed =>
|
|
527
|
-
completed.name === 'read_file'
|
|
528
|
-
&& !completed.result.ok
|
|
529
|
-
&& /read_private_continuity_file/.test(completed.result.content),
|
|
530
|
-
)
|
|
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
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
export function parseLocalModelTextToolUse(
|
|
542
|
-
provider: Pick<Provider, 'id'>,
|
|
543
|
-
assistantText: string,
|
|
544
|
-
iterationIndex = 0,
|
|
545
|
-
): PendingToolUse | null {
|
|
546
|
-
const parsed = parseLocalModelTextToolUses(provider, assistantText, iterationIndex)
|
|
547
|
-
return parsed.length === 1 ? parsed[0]! : null
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
export function parseLocalModelTextToolUses(
|
|
551
|
-
provider: Pick<Provider, 'id'>,
|
|
552
|
-
assistantText: string,
|
|
553
|
-
iterationIndex = 0,
|
|
554
|
-
): PendingToolUse[] {
|
|
555
|
-
if (provider.id !== 'llamacpp') return []
|
|
556
|
-
|
|
557
|
-
const calls = extractTextToolCalls(assistantText)
|
|
558
|
-
if (calls.length === 0) return []
|
|
559
|
-
|
|
560
|
-
return calls.map((call, index) => ({
|
|
561
|
-
id: calls.length === 1 ? `local-text-tool-${iterationIndex}` : `local-text-tool-${iterationIndex}-${index}`,
|
|
562
|
-
name: call.name,
|
|
563
|
-
input: call.input,
|
|
564
|
-
}))
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
function extractTextToolCalls(text: string): Array<{ name: string; input: Record<string, unknown> }> {
|
|
568
|
-
const payloads = extractToolPayloadCandidates(text)
|
|
569
|
-
const calls = payloads.flatMap(parseTextToolPayloads)
|
|
570
|
-
return calls.filter(call => typeof call.name === 'string' && isRecord(call.input) && Boolean(getTool(call.name)))
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
function extractToolPayloadCandidates(text: string): string[] {
|
|
574
|
-
const trimmed = text.trim()
|
|
575
|
-
if (!trimmed) return []
|
|
576
|
-
|
|
577
|
-
const exact = normalizeToolPayloadCandidate(trimmed)
|
|
578
|
-
if (exact.startsWith('{') && exact.endsWith('}')) return [exact]
|
|
579
|
-
if (exact.startsWith('[') && exact.endsWith(']')) return [exact]
|
|
580
|
-
|
|
581
|
-
const fencedOnlyMatch = trimmed.match(/^```[^\r\n]*\r?\n([\s\S]*?)\r?\n```$/i)
|
|
582
|
-
if (fencedOnlyMatch) return [normalizeToolPayloadCandidate(fencedOnlyMatch[1]!)]
|
|
583
|
-
|
|
584
|
-
const embedded = [
|
|
585
|
-
...[...trimmed.matchAll(/<tool_call>\s*([\s\S]*?)\s*<\/tool_call>/gi)].map(match => match[1]!),
|
|
586
|
-
...[...trimmed.matchAll(/```[^\r\n]*\r?\n([\s\S]*?)\r?\n```/g)].map(match => match[1]!),
|
|
587
|
-
...extractStandaloneJsonPayloads(trimmed),
|
|
588
|
-
].map(normalizeToolPayloadCandidate)
|
|
589
|
-
|
|
590
|
-
return [...new Set(embedded)]
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function extractStandaloneJsonPayloads(text: string): string[] {
|
|
594
|
-
const lines = text.split(/\r?\n/)
|
|
595
|
-
const out: string[] = []
|
|
596
|
-
|
|
597
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
598
|
-
const line = lines[i] ?? ''
|
|
599
|
-
const first = normalizeToolPayloadCandidate(line)
|
|
600
|
-
if (!first.startsWith('{') && !first.startsWith('[')) continue
|
|
601
|
-
|
|
602
|
-
let candidate = line
|
|
603
|
-
for (let j = i; j < lines.length; j += 1) {
|
|
604
|
-
if (j > i) candidate += `\n${lines[j] ?? ''}`
|
|
605
|
-
const normalized = normalizeToolPayloadCandidate(candidate)
|
|
606
|
-
if (canParseJson(normalized)) {
|
|
607
|
-
out.push(normalized)
|
|
608
|
-
i = j
|
|
609
|
-
break
|
|
610
|
-
}
|
|
611
|
-
if (candidate.length > 20_000) break
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
return out
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
function canParseJson(value: string): boolean {
|
|
619
|
-
try {
|
|
620
|
-
JSON.parse(value)
|
|
621
|
-
return true
|
|
622
|
-
} catch {
|
|
623
|
-
return false
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function normalizeToolPayloadCandidate(candidate: string): string {
|
|
628
|
-
let normalized = candidate
|
|
629
|
-
.trim()
|
|
630
|
-
.split(/\r?\n/)
|
|
631
|
-
.map(line => line.replace(/^\s*\d+\s+(?=[{\[<"])/, ''))
|
|
632
|
-
.join('\n')
|
|
633
|
-
.trim()
|
|
634
|
-
|
|
635
|
-
const toolCallMatch = normalized.match(/^<tool_call>\s*([\s\S]*?)\s*<\/tool_call>$/i)
|
|
636
|
-
if (toolCallMatch) normalized = toolCallMatch[1]!.trim()
|
|
637
|
-
return normalized
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
function parseTextToolPayloads(payload: string): Array<{ name: string; input: Record<string, unknown> }> {
|
|
641
|
-
let parsed: unknown
|
|
642
|
-
try {
|
|
643
|
-
parsed = JSON.parse(payload)
|
|
644
|
-
} catch {
|
|
645
|
-
return []
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
return normalizeParsedToolPayloads(parsed)
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
function normalizeParsedToolPayloads(value: unknown): Array<{ name: string; input: Record<string, unknown> }> {
|
|
652
|
-
if (Array.isArray(value)) {
|
|
653
|
-
return value.flatMap(normalizeParsedToolPayloads)
|
|
654
|
-
}
|
|
655
|
-
if (!isRecord(value)) return []
|
|
656
|
-
|
|
657
|
-
const toolCalls = value.tool_calls
|
|
658
|
-
if (Array.isArray(toolCalls)) {
|
|
659
|
-
return toolCalls.flatMap(normalizeParsedToolPayloads)
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const fn = value.function
|
|
663
|
-
if (isRecord(fn)) {
|
|
664
|
-
const call = normalizeNameAndInput(fn.name, fn.arguments)
|
|
665
|
-
return call ? [call] : []
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
const name = value.name ?? value.tool ?? value.tool_name ?? value.function_name
|
|
669
|
-
const rawInput = value.arguments ?? value.input ?? value.parameters ?? value.args ?? {}
|
|
670
|
-
const call = normalizeNameAndInput(name, rawInput)
|
|
671
|
-
return call ? [call] : []
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
function normalizeNameAndInput(
|
|
675
|
-
name: unknown,
|
|
676
|
-
rawInput: unknown,
|
|
677
|
-
): { name: string; input: Record<string, unknown> } | null {
|
|
678
|
-
if (typeof name !== 'string') return null
|
|
679
|
-
const input = parseToolInput(rawInput)
|
|
680
|
-
if (!input) return null
|
|
681
|
-
return { name, input }
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function parseToolInput(rawInput: unknown): Record<string, unknown> | null {
|
|
685
|
-
if (isRecord(rawInput)) return rawInput
|
|
686
|
-
if (typeof rawInput !== 'string') return null
|
|
687
|
-
try {
|
|
688
|
-
const parsed = JSON.parse(rawInput)
|
|
689
|
-
return isRecord(parsed) ? parsed : null
|
|
690
|
-
} catch {
|
|
691
|
-
return null
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
696
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
function nextNudge(
|
|
700
|
-
provider: Pick<Provider, 'supportsTools'>,
|
|
701
|
-
assistantText: string,
|
|
702
|
-
): { text: string; reason: ContinuationNudgeReason; keepAssistantContext: boolean } | null {
|
|
703
|
-
if (provider.supportsTools && looksLikePrivateContinuityWorkspaceCreationIntent(assistantText)) {
|
|
704
|
-
return {
|
|
705
|
-
text: PRIVATE_CONTINUITY_NUDGE_TEXT,
|
|
706
|
-
reason: 'private_continuity_tool',
|
|
707
|
-
keepAssistantContext: false,
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
if (provider.supportsTools && looksLikeToolCapabilityConfusion(assistantText)) {
|
|
711
|
-
return {
|
|
712
|
-
text: TOOL_CAPABILITY_NUDGE_TEXT,
|
|
713
|
-
reason: 'tool_capability',
|
|
714
|
-
keepAssistantContext: false,
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
if (looksLikeContinuationIntent(assistantText)) {
|
|
718
|
-
return {
|
|
719
|
-
text: CONTINUATION_NUDGE_TEXT,
|
|
720
|
-
reason: 'continuation',
|
|
721
|
-
keepAssistantContext: true,
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
return null
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
export function looksLikePrivateContinuityWorkspaceCreationIntent(text: string): boolean {
|
|
728
|
-
const lower = text.toLowerCase()
|
|
729
|
-
if (!/\b(soul|memory)\.md\b/.test(lower)) return false
|
|
730
|
-
return [
|
|
731
|
-
/\b(create|write|make|generate|scaffold|overwrite|replace|locate|find|search|read|check|inspect)\b.{0,100}\b(soul|memory)\.md\b/,
|
|
732
|
-
/\b(soul|memory)\.md\b.{0,100}\b(create|write|make|generate|scaffold|overwrite|replace|locate|find|search|read|check|inspect)\b/,
|
|
733
|
-
/\bplans?[\\/][^\s]*\b(soul|memory)\b/,
|
|
734
|
-
].some(pattern => pattern.test(lower))
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
export function looksLikeToolCapabilityConfusion(text: string): boolean {
|
|
738
|
-
const lower = text.toLowerCase()
|
|
739
|
-
const limitation =
|
|
740
|
-
/\b(i (do not|don't|cannot|can't) (have|access|run|execute|inspect|read|list|use)|no direct access|unable to|not able to|currently operating under|limitations and restrictions)\b/
|
|
741
|
-
const toolTask =
|
|
742
|
-
/\b(run|execute|shell command|command output|local machine|terminal|files?|directories|workspace|paste|share the contents)\b/
|
|
743
|
-
return limitation.test(lower) && toolTask.test(lower)
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
export function looksLikeToolStateClaimWithoutTool(text: string): boolean {
|
|
747
|
-
return looksLikeToolStateClaim(text)
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
export function looksLikeFakeToolProtocolText(text: string): boolean {
|
|
751
|
-
const lower = text.toLowerCase()
|
|
752
|
-
if (!lower.trim()) return false
|
|
753
|
-
|
|
754
|
-
const toolNames = new Set(
|
|
755
|
-
[...lower.matchAll(/\b(change_directory|edit_file|propose_private_continuity_edit|read_private_continuity_file|list_directory|read_file|run_bash|write_file|delete_file)\b/g)]
|
|
756
|
-
.map(match => match[1]),
|
|
757
|
-
)
|
|
758
|
-
if (toolNames.size < 2) return false
|
|
759
|
-
|
|
760
|
-
const codeBlock = /```|code\s*(?:-|:)?\s*block/.test(lower)
|
|
761
|
-
const toolMenu = /\b(available tools|tool functions|functions are|tools are|native tools)\b/.test(lower)
|
|
762
|
-
const actionIntent = /\b(let'?s|let me|i'?ll|i will|first|next)\b.{0,80}\b(list|read|inspect|execute|run|change|edit|write)\b/.test(lower)
|
|
763
|
-
const commaSeparatedTools = /(?:change_directory|edit_file|propose_private_continuity_edit|read_private_continuity_file|list_directory|read_file|run_bash|write_file|delete_file)(?:\s*,\s*|\s+){1,}/.test(lower)
|
|
764
|
-
|
|
765
|
-
return (codeBlock || toolMenu || actionIntent) && commaSeparatedTools
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
export function looksLikeToolDelegationText(text: string): boolean {
|
|
769
|
-
const lower = text.toLowerCase()
|
|
770
|
-
if (!lower.trim()) return false
|
|
771
|
-
|
|
772
|
-
const toolName = '(?:change_directory|edit_file|propose_private_continuity_edit|read_private_continuity_file|list_directory|read_file|run_bash|write_file|delete_file)'
|
|
773
|
-
if (!new RegExp(`\\b${toolName}\\b`).test(lower)) return false
|
|
774
|
-
|
|
775
|
-
const directToolRef = `(?:\`?${toolName}\`?|the\\s+\`?${toolName}\`?\\s+tool)`
|
|
776
|
-
const action = '(?:run|execute|call|use|invoke)'
|
|
777
|
-
const askPrefix = "(?:please|kindly|can you|could you|would you|you can|you should|you need to|you'll need to|try to|go ahead and)"
|
|
778
|
-
const selfPrefix = "(?:i'll|i will|let me|let's|we should|we need to|before proceeding|first|next|now)"
|
|
779
|
-
|
|
780
|
-
const askUser = new RegExp(`\\b${askPrefix}\\b.{0,100}\\b${action}\\b.{0,50}${directToolRef}`).test(lower)
|
|
781
|
-
const selfIntent = new RegExp(`\\b${selfPrefix}\\b.{0,100}\\b${action}\\b.{0,50}${directToolRef}`).test(lower)
|
|
782
|
-
const commandForm = new RegExp(`\\b${action}\\s+${directToolRef}\\b`).test(lower)
|
|
783
|
-
&& /\b(please|before proceeding|first|next|now|to proceed)\b/.test(lower)
|
|
784
|
-
const asksForOutput = new RegExp(`${directToolRef}.{0,120}\\b(output|result|files?|directory structure|working directory)\\b`).test(lower)
|
|
785
|
-
&& /\b(please|you|run|paste|share|provide)\b/.test(lower)
|
|
786
|
-
|
|
787
|
-
return askUser || selfIntent || commandForm || asksForOutput
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
export function looksLikeContinuationIntent(text: string): boolean {
|
|
791
|
-
const lower = text.toLowerCase()
|
|
792
|
-
|
|
793
|
-
const completionMarkers =
|
|
794
|
-
/\b(done|finished|completed|complete|summary|that's all|that is all|all set|hope this helps|let me know if)\b/
|
|
795
|
-
if (completionMarkers.test(lower)) return false
|
|
796
|
-
|
|
797
|
-
const actionVerbs =
|
|
798
|
-
'(do|create|write|edit|update|fix|implement|add|run|check|make|build|set up|go|proceed|begin)'
|
|
799
|
-
|
|
800
|
-
const shortMessage = lower.length < 80
|
|
801
|
-
|
|
802
|
-
const patterns: RegExp[] = [
|
|
803
|
-
new RegExp(
|
|
804
|
-
`\\bso now (i|let me|we) (need to|have to|should|must|will) ${actionVerbs}\\b`,
|
|
805
|
-
),
|
|
806
|
-
new RegExp(`\\bnow i('ll| will) ${actionVerbs}\\b`),
|
|
807
|
-
new RegExp(
|
|
808
|
-
`\\blet me (go ahead and |now )?${actionVerbs}\\b`,
|
|
809
|
-
),
|
|
810
|
-
new RegExp(`\\btime to ${actionVerbs}\\b`),
|
|
811
|
-
]
|
|
812
|
-
|
|
813
|
-
if (shortMessage) {
|
|
814
|
-
patterns.push(
|
|
815
|
-
new RegExp(
|
|
816
|
-
`\\bi('ll| will| need to| have to| must) (now )?${actionVerbs}\\b`,
|
|
817
|
-
),
|
|
818
|
-
new RegExp(
|
|
819
|
-
`\\bnext,?\\s+(i('ll| will)|let me|i need to) ${actionVerbs}\\b`,
|
|
820
|
-
),
|
|
821
|
-
)
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
return patterns.some(re => re.test(lower))
|
|
825
|
-
}
|