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.
Files changed (73) hide show
  1. package/README.md +6 -1
  2. package/package.json +3 -1
  3. package/src/app/FirstRun.tsx +1 -24
  4. package/src/app/firstRunConfig.ts +26 -0
  5. package/src/auth/openaiOAuth/landingPage.ts +2 -11
  6. package/src/chat/ChatScreen.tsx +32 -117
  7. package/src/chat/MessageList.tsx +18 -260
  8. package/src/chat/chatEnvironment.ts +16 -0
  9. package/src/chat/chatTurnContext.ts +50 -0
  10. package/src/chat/chatTurnOrchestrator.ts +5 -112
  11. package/src/chat/chatTurnRows.ts +64 -0
  12. package/src/chat/commands.ts +3 -178
  13. package/src/chat/continuityEditReview.ts +42 -0
  14. package/src/chat/input/ChatInput.tsx +10 -144
  15. package/src/chat/input/chatInputHelpers.ts +62 -0
  16. package/src/chat/input/inputRendering.tsx +93 -0
  17. package/src/chat/messageMarkdown.ts +220 -0
  18. package/src/chat/messageRows.ts +43 -0
  19. package/src/chat/planImplementation.ts +62 -0
  20. package/src/chat/slashCommandHandlers.ts +165 -0
  21. package/src/chat/slashCommandViews.ts +120 -0
  22. package/src/cli/main.tsx +7 -0
  23. package/src/identity/continuity/challenges.ts +123 -0
  24. package/src/identity/continuity/envelope.ts +49 -1484
  25. package/src/identity/continuity/envelopeCreate.ts +322 -0
  26. package/src/identity/continuity/envelopeCrypto.ts +182 -0
  27. package/src/identity/continuity/envelopeParse.ts +441 -0
  28. package/src/identity/continuity/envelopeTypes.ts +204 -0
  29. package/src/identity/continuity/envelopeVersion.ts +1 -0
  30. package/src/identity/continuity/payloadNormalization.ts +183 -0
  31. package/src/identity/continuity/publicSkills.ts +5 -5
  32. package/src/identity/continuity/skills/loadSkills.ts +12 -69
  33. package/src/identity/continuity/skills/skillPaths.ts +76 -0
  34. package/src/identity/continuity/skillsNormalization.ts +119 -0
  35. package/src/identity/continuity/snapshotToken.ts +28 -0
  36. package/src/identity/hub/continuity/completion.ts +67 -0
  37. package/src/identity/hub/continuity/effects.ts +5 -62
  38. package/src/identity/hub/profile/effects.ts +6 -170
  39. package/src/identity/hub/profile/operatorSave.ts +202 -0
  40. package/src/identity/registry/erc8004/metadata.ts +31 -23
  41. package/src/identity/wallet/browserWallet/html.ts +1 -57
  42. package/src/identity/wallet/browserWallet/walletPageSource.ts +85 -0
  43. package/src/identity/wallet/page/controller.ts +1 -1
  44. package/src/identity/wallet/page/errorView.ts +122 -0
  45. package/src/identity/wallet/page/view.ts +3 -114
  46. package/src/mcp/manager.ts +8 -66
  47. package/src/mcp/managerHelpers.ts +70 -0
  48. package/src/models/ModelPicker.tsx +69 -889
  49. package/src/models/huggingface.ts +20 -137
  50. package/src/models/huggingfaceStorage.ts +136 -0
  51. package/src/models/llamacpp.ts +37 -303
  52. package/src/models/llamacppCommands.ts +44 -0
  53. package/src/models/llamacppConfig.ts +34 -0
  54. package/src/models/llamacppDiscovery.ts +176 -0
  55. package/src/models/llamacppOutput.ts +65 -0
  56. package/src/models/modelPickerCatalogFlow.ts +56 -0
  57. package/src/models/modelPickerCredentials.ts +166 -0
  58. package/src/models/modelPickerData.ts +41 -0
  59. package/src/models/modelPickerDisplay.tsx +132 -0
  60. package/src/models/modelPickerHfFlow.ts +192 -0
  61. package/src/models/modelPickerLocalRunnerFlow.ts +115 -0
  62. package/src/models/modelPickerTypes.ts +69 -0
  63. package/src/models/modelPickerUninstallFlow.ts +48 -0
  64. package/src/models/modelPickerViewHelpers.ts +174 -0
  65. package/src/providers/openai-chat.ts +5 -124
  66. package/src/providers/openaiChatWire.ts +124 -0
  67. package/src/runtime/providerTurn.ts +38 -0
  68. package/src/runtime/textToolParser.ts +161 -0
  69. package/src/runtime/toolIntent.ts +1 -1
  70. package/src/runtime/turn.ts +43 -499
  71. package/src/runtime/turnNudges.ts +223 -0
  72. package/src/runtime/turnTypes.ts +86 -0
  73. package/src/ui/terminalTitle.ts +30 -0
@@ -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
- type ProviderTurnEvent =
11
- | { type: 'text'; delta: string }
12
- | { type: 'thinking'; delta: string }
13
- | { type: 'thinking_end' }
14
- | ProviderRetryStreamEvent
15
- | { type: 'tool_use_start'; id: string; name: string }
16
- | { type: 'tool_use_delta'; id: string; delta: string }
17
- | { type: 'tool_use_stop'; id: string; name: string; input: Record<string, unknown> }
18
- | { type: 'done'; stopReason?: TurnStopReason }
19
- | { type: 'error'; message: string }
20
- | { type: 'cancelled' }
21
-
22
- type TurnStopReason = 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence' | 'unknown'
23
-
24
- async function* runProviderTurn(
25
- provider: Provider,
26
- messages: Message[],
27
- signal: AbortSignal,
28
- ): AsyncIterable<ProviderTurnEvent> {
29
- if (signal.aborted) {
30
- yield { type: 'cancelled' }
31
- return
32
- }
33
- for await (const ev of provider.complete(messages, signal)) {
34
- if (signal.aborted) {
35
- yield { type: 'cancelled' }
36
- return
37
- }
38
- yield normalize(ev)
39
- if (ev.type === 'done' || ev.type === 'error') return
40
- }
41
- if (signal.aborted) {
42
- yield { type: 'cancelled' }
43
- }
44
- }
45
-
46
- function normalize(event: StreamEvent): ProviderTurnEvent {
47
- switch (event.type) {
48
- case 'text': return { type: 'text', delta: event.delta }
49
- case 'thinking': return { type: 'thinking', delta: event.delta }
50
- case 'thinking_end': return { type: 'thinking_end' }
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
- 'The previous assistant response claimed workspace state without executing a tool. '
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
- }