@ouraihub/aether-ui 0.0.0-stage → 0.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/LICENSE +789 -0
- package/fixtures/model-classify.json +42 -0
- package/fixtures/pricing-cost.json +56 -0
- package/fixtures/reasoning-infer.json +44 -0
- package/fixtures/sse/anthropic-messages/chinese.sse +24 -0
- package/fixtures/sse/anthropic-messages/multi-tool.sse +33 -0
- package/fixtures/sse/anthropic-messages/single-tool.sse +39 -0
- package/fixtures/sse/anthropic-messages/text.sse +27 -0
- package/fixtures/sse/anthropic-messages/truncated.sse +21 -0
- package/fixtures/sse/anthropic-messages/upstream-error.sse +12 -0
- package/fixtures/sse/expect.json +20 -0
- package/fixtures/sse/openai-chat/chinese.sse +14 -0
- package/fixtures/sse/openai-chat/multi-tool.sse +18 -0
- package/fixtures/sse/openai-chat/single-tool.sse +22 -0
- package/fixtures/sse/openai-chat/text.sse +14 -0
- package/fixtures/sse/openai-chat/truncated.sse +12 -0
- package/fixtures/sse/openai-chat/upstream-error.sse +6 -0
- package/fixtures/sse/openai-responses/chinese.sse +33 -0
- package/fixtures/sse/openai-responses/multi-tool.sse +36 -0
- package/fixtures/sse/openai-responses/single-tool.sse +27 -0
- package/fixtures/sse/openai-responses/text.sse +33 -0
- package/fixtures/sse/openai-responses/truncated.sse +30 -0
- package/fixtures/sse/openai-responses/upstream-error.sse +9 -0
- package/package.json +71 -3
- package/src/CopyInput.svelte +33 -0
- package/src/KitNotifier.svelte +33 -0
- package/src/chat/ApprovalCard.svelte +23 -0
- package/src/chat/ChatPanel.svelte +2097 -0
- package/src/chat/MessageItem.svelte +583 -0
- package/src/chat/SkillPicker.svelte +126 -0
- package/src/chat/ToolCallItem.svelte +23 -0
- package/src/chat/UsageDetails.svelte +138 -0
- package/src/chat/index.ts +2 -0
- package/src/chat/run-session.svelte.ts +556 -0
- package/src/components/ui/alert/alert-action.svelte +20 -0
- package/src/components/ui/alert/alert-description.svelte +23 -0
- package/src/components/ui/alert/alert-title.svelte +23 -0
- package/src/components/ui/alert/alert.svelte +43 -0
- package/src/components/ui/alert/index.ts +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-action.svelte +27 -0
- package/src/components/ui/alert-dialog/alert-dialog-cancel.svelte +27 -0
- package/src/components/ui/alert-dialog/alert-dialog-content.svelte +32 -0
- package/src/components/ui/alert-dialog/alert-dialog-description.svelte +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +23 -0
- package/src/components/ui/alert-dialog/alert-dialog-header.svelte +20 -0
- package/src/components/ui/alert-dialog/alert-dialog-media.svelte +20 -0
- package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +7 -0
- package/src/components/ui/alert-dialog/alert-dialog-title.svelte +17 -0
- package/src/components/ui/alert-dialog/alert-dialog-trigger.svelte +7 -0
- package/src/components/ui/alert-dialog/alert-dialog.svelte +7 -0
- package/src/components/ui/alert-dialog/index.ts +40 -0
- package/src/components/ui/button/button.svelte +81 -0
- package/src/components/ui/button/index.ts +17 -0
- package/src/components/ui/dialog/dialog-close.svelte +11 -0
- package/src/components/ui/dialog/dialog-content.svelte +48 -0
- package/src/components/ui/dialog/dialog-description.svelte +17 -0
- package/src/components/ui/dialog/dialog-footer.svelte +32 -0
- package/src/components/ui/dialog/dialog-header.svelte +20 -0
- package/src/components/ui/dialog/dialog-overlay.svelte +17 -0
- package/src/components/ui/dialog/dialog-portal.svelte +7 -0
- package/src/components/ui/dialog/dialog-title.svelte +17 -0
- package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/src/components/ui/dialog/dialog.svelte +7 -0
- package/src/components/ui/dialog/index.ts +34 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +46 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +17 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +36 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +31 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
- package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/src/components/ui/dropdown-menu/index.ts +54 -0
- package/src/components/ui/empty/empty-content.svelte +23 -0
- package/src/components/ui/empty/empty-description.svelte +23 -0
- package/src/components/ui/empty/empty-header.svelte +20 -0
- package/src/components/ui/empty/empty-media.svelte +41 -0
- package/src/components/ui/empty/empty-title.svelte +20 -0
- package/src/components/ui/empty/empty.svelte +23 -0
- package/src/components/ui/empty/index.ts +22 -0
- package/src/components/ui/field/field-content.svelte +20 -0
- package/src/components/ui/field/field-description.svelte +25 -0
- package/src/components/ui/field/field-error.svelte +58 -0
- package/src/components/ui/field/field-group.svelte +23 -0
- package/src/components/ui/field/field-label.svelte +25 -0
- package/src/components/ui/field/field-legend.svelte +24 -0
- package/src/components/ui/field/field-separator.svelte +35 -0
- package/src/components/ui/field/field-set.svelte +20 -0
- package/src/components/ui/field/field-title.svelte +20 -0
- package/src/components/ui/field/field.svelte +47 -0
- package/src/components/ui/field/index.ts +33 -0
- package/src/components/ui/input/index.ts +7 -0
- package/src/components/ui/input/input.svelte +48 -0
- package/src/components/ui/label/index.ts +7 -0
- package/src/components/ui/label/label.svelte +20 -0
- package/src/components/ui/select/index.ts +40 -0
- package/src/components/ui/select/select-content.svelte +45 -0
- package/src/components/ui/select/select-group-heading.svelte +21 -0
- package/src/components/ui/select/select-group.svelte +17 -0
- package/src/components/ui/select/select-item.svelte +43 -0
- package/src/components/ui/select/select-label.svelte +20 -0
- package/src/components/ui/select/select-portal.svelte +7 -0
- package/src/components/ui/select/select-scroll-down-button.svelte +20 -0
- package/src/components/ui/select/select-scroll-up-button.svelte +20 -0
- package/src/components/ui/select/select-separator.svelte +18 -0
- package/src/components/ui/select/select-trigger.svelte +29 -0
- package/src/components/ui/select/select-value.svelte +7 -0
- package/src/components/ui/select/select.svelte +11 -0
- package/src/components/ui/separator/index.ts +7 -0
- package/src/components/ui/separator/separator.svelte +21 -0
- package/src/components/ui/sonner/index.ts +1 -0
- package/src/components/ui/sonner/sonner.svelte +40 -0
- package/src/components/ui/switch/index.ts +7 -0
- package/src/components/ui/switch/switch.svelte +34 -0
- package/src/components/ui/textarea/index.ts +7 -0
- package/src/components/ui/textarea/textarea.svelte +22 -0
- package/src/context.ts +34 -0
- package/src/dev/Demo.svelte +45 -0
- package/src/dev/main.ts +7 -0
- package/src/i18n/en.ts +617 -0
- package/src/i18n/index.ts +23 -0
- package/src/i18n/zh-CN.ts +599 -0
- package/src/index.ts +10 -0
- package/src/logs/LogPanel.svelte +97 -0
- package/src/logs/LogSettings.svelte +56 -0
- package/src/logs/index.ts +5 -0
- package/src/logs/logger.ts +96 -0
- package/src/markdown.ts +8 -0
- package/src/mcp/McpClientSettings.svelte +491 -0
- package/src/mcp/McpServerSettings.svelte +327 -0
- package/src/mcp/config.ts +26 -0
- package/src/mcp/index.ts +3 -0
- package/src/mcp/input.ts +73 -0
- package/src/notify.svelte.ts +43 -0
- package/src/ports/contract.ts +165 -0
- package/src/ports/fake.ts +484 -0
- package/src/ports/index.ts +7 -0
- package/src/ports/search.ts +47 -0
- package/src/ports/skill-sources.ts +45 -0
- package/src/providers/EndpointKeysDialog.svelte +343 -0
- package/src/providers/EndpointPanel.svelte +491 -0
- package/src/providers/ModelPickerDialog.svelte +187 -0
- package/src/providers/ModelSelect.svelte +434 -0
- package/src/providers/ModelSettingsDialog.svelte +453 -0
- package/src/providers/ProvidersSettings.svelte +631 -0
- package/src/providers/RequestConfigDialog.svelte +250 -0
- package/src/providers/SecretInput.svelte +276 -0
- package/src/providers/draft.ts +329 -0
- package/src/providers/edit-session.ts +146 -0
- package/src/providers/index.ts +5 -0
- package/src/providers/reasoning.ts +41 -0
- package/src/providers/revision.svelte.ts +4 -0
- package/src/search/SearchSettings.svelte +703 -0
- package/src/search/index.ts +13 -0
- package/src/search/settings-flow.ts +62 -0
- package/src/session.ts +20 -0
- package/src/skills/SkillAdd.svelte +347 -0
- package/src/skills/SkillAuthoring.svelte +381 -0
- package/src/skills/SkillEditor.svelte +94 -0
- package/src/skills/SkillPreviewDialog.svelte +122 -0
- package/src/skills/SkillsSettings.svelte +140 -0
- package/src/skills/error.ts +32 -0
- package/src/skills/index.ts +4 -0
- package/src/styles/kit.css +137 -0
- package/src/styles/tokens.css +22 -0
- package/src/tauri/index.ts +282 -0
- package/src/utils.ts +22 -0
- package/src/web/index.ts +376 -0
- package/README.md +0 -3
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
// In-memory RunClient, ConversationClient and desktop actions for tests and demos (docs/16, docs/22).
|
|
2
|
+
// Seed storage with initialConversations; override clients independently or omit a capability.
|
|
3
|
+
import { KitClientError } from './contract'
|
|
4
|
+
import type {
|
|
5
|
+
ConversationClient,
|
|
6
|
+
DesktopActions,
|
|
7
|
+
RunClient,
|
|
8
|
+
KitPorts,
|
|
9
|
+
LogsPort,
|
|
10
|
+
McpClientPort,
|
|
11
|
+
McpServerPort,
|
|
12
|
+
ProvidersPort,
|
|
13
|
+
SkillsPort,
|
|
14
|
+
} from './contract'
|
|
15
|
+
import type { Conversation, LogLevel, RunEvent, RunSnapshot, Turn } from '@ouraihub/aether-contracts'
|
|
16
|
+
import type { SearchPort } from './search'
|
|
17
|
+
import type { SkillSourcesPort } from './skill-sources'
|
|
18
|
+
|
|
19
|
+
type PortName = keyof KitPorts
|
|
20
|
+
|
|
21
|
+
export interface FakePortsOptions {
|
|
22
|
+
/** Initial stored conversations, copied to isolate the caller from fake storage. */
|
|
23
|
+
initialConversations?: readonly Conversation[]
|
|
24
|
+
conversations?: Partial<ConversationClient>
|
|
25
|
+
desktop?: Partial<DesktopActions>
|
|
26
|
+
logs?: Partial<LogsPort>
|
|
27
|
+
providers?: Partial<ProvidersPort>
|
|
28
|
+
chat?: Partial<RunClient>
|
|
29
|
+
search?: Partial<SearchPort>
|
|
30
|
+
skills?: Partial<SkillsPort>
|
|
31
|
+
skillSources?: Partial<SkillSourcesPort>
|
|
32
|
+
mcpClient?: Partial<McpClientPort>
|
|
33
|
+
mcpServer?: Partial<McpServerPort>
|
|
34
|
+
/** Ports to leave out, as a host without that feature would. */
|
|
35
|
+
omit?: readonly PortName[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const STOPPED = { running: false, port: 0, url: '', toolCount: 0 }
|
|
39
|
+
const clientError = (
|
|
40
|
+
code: ConstructorParameters<typeof KitClientError>[0]['code'],
|
|
41
|
+
message: string,
|
|
42
|
+
details: Record<string, string> = {},
|
|
43
|
+
) => new KitClientError({ code, message, details })
|
|
44
|
+
|
|
45
|
+
export function createFakePorts(options: FakePortsOptions = {}): KitPorts {
|
|
46
|
+
let level: LogLevel = 'INFO'
|
|
47
|
+
let sequence = 0
|
|
48
|
+
let conversationSequence = 0
|
|
49
|
+
const conversations = new Map<string, Conversation>(
|
|
50
|
+
(options.initialConversations ?? []).map((conversation) => [
|
|
51
|
+
conversation.summary.id,
|
|
52
|
+
structuredClone(conversation),
|
|
53
|
+
]),
|
|
54
|
+
)
|
|
55
|
+
const nextId = (prefix: string): string => {
|
|
56
|
+
let id: string
|
|
57
|
+
do {
|
|
58
|
+
id = `${prefix}-${++sequence}`
|
|
59
|
+
} while (
|
|
60
|
+
[...conversations.values()].some((conversation) =>
|
|
61
|
+
conversation.turns.some((turn) => turn.id === id || turn.questionId === id),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
return id
|
|
65
|
+
}
|
|
66
|
+
const storesConversations = !options.omit?.includes('conversations')
|
|
67
|
+
const runs = new Map<string, RunSnapshot>()
|
|
68
|
+
const terminals: string[] = []
|
|
69
|
+
const acceptRun = (
|
|
70
|
+
conversation: Conversation | null,
|
|
71
|
+
turn: Turn | null,
|
|
72
|
+
select: boolean,
|
|
73
|
+
onEvent: (event: RunEvent) => void,
|
|
74
|
+
signal?: AbortSignal,
|
|
75
|
+
) => {
|
|
76
|
+
const run: RunSnapshot = {
|
|
77
|
+
runId: nextId('fake-run'),
|
|
78
|
+
conversationId: conversation?.summary.id ?? null,
|
|
79
|
+
turnId: turn?.id ?? null,
|
|
80
|
+
status: 'running',
|
|
81
|
+
cancelRequested: false,
|
|
82
|
+
}
|
|
83
|
+
runs.set(run.runId, run)
|
|
84
|
+
const cancel = () => {
|
|
85
|
+
run.cancelRequested = true
|
|
86
|
+
}
|
|
87
|
+
signal?.addEventListener('abort', cancel, { once: true })
|
|
88
|
+
// Leave an acceptance turn for callers to request cancellation or inspect the active run.
|
|
89
|
+
queueMicrotask(() =>
|
|
90
|
+
queueMicrotask(() => {
|
|
91
|
+
if (signal?.aborted) cancel()
|
|
92
|
+
if (turn && !run.cancelRequested) onEvent({ kind: 'text_delta', text: turn.text })
|
|
93
|
+
run.status = run.cancelRequested ? 'cancelled' : 'completed'
|
|
94
|
+
if (turn && conversation) {
|
|
95
|
+
turn.status = run.status
|
|
96
|
+
if (run.status === 'cancelled') turn.text = ''
|
|
97
|
+
if (select && run.status === 'completed')
|
|
98
|
+
conversation.summary = { ...conversation.summary, selectedTurnId: turn.id, updatedAt: Date.now() }
|
|
99
|
+
}
|
|
100
|
+
signal?.removeEventListener('abort', cancel)
|
|
101
|
+
terminals.push(run.runId)
|
|
102
|
+
if (terminals.length > 128) {
|
|
103
|
+
const expired = terminals.shift()
|
|
104
|
+
if (expired !== undefined) runs.delete(expired)
|
|
105
|
+
}
|
|
106
|
+
onEvent({ kind: 'end', status: run.status })
|
|
107
|
+
}),
|
|
108
|
+
)
|
|
109
|
+
return { runId: run.runId, conversationId: run.conversationId, turnId: run.turnId }
|
|
110
|
+
}
|
|
111
|
+
const snapshot = (conversation: Conversation): Conversation => structuredClone(conversation)
|
|
112
|
+
const getConversation = (id: string) => {
|
|
113
|
+
const conversation = conversations.get(id)
|
|
114
|
+
if (!conversation) throw clientError('not_found', `conversation ${id}`, { entity: 'conversation' })
|
|
115
|
+
if (conversation.turns.some((turn) => turn.status === 'running'))
|
|
116
|
+
throw clientError('conflict', 'conversation is active', { reason: 'active_run' })
|
|
117
|
+
return conversation
|
|
118
|
+
}
|
|
119
|
+
const getTurn = (conversationId: string, turnId: string) => {
|
|
120
|
+
const turn = getConversation(conversationId).turns.find((candidate) => candidate.id === turnId)
|
|
121
|
+
if (!turn) throw clientError('not_found', `turn ${turnId}`, { entity: 'turn' })
|
|
122
|
+
return turn
|
|
123
|
+
}
|
|
124
|
+
const conversationClient: ConversationClient = {
|
|
125
|
+
list: (scope?: string) =>
|
|
126
|
+
Promise.resolve(
|
|
127
|
+
[...conversations.values()]
|
|
128
|
+
.filter(({ summary }) => !scope || summary.scope === scope)
|
|
129
|
+
.map(({ summary }) => ({ ...summary })),
|
|
130
|
+
),
|
|
131
|
+
get: async (id: string) => {
|
|
132
|
+
const conversation = conversations.get(id)
|
|
133
|
+
if (!conversation) throw clientError('not_found', `conversation ${id}`, { entity: 'conversation' })
|
|
134
|
+
return snapshot(conversation)
|
|
135
|
+
},
|
|
136
|
+
remove: async (id: string) => {
|
|
137
|
+
getConversation(id)
|
|
138
|
+
conversations.delete(id)
|
|
139
|
+
},
|
|
140
|
+
rename: async (id: string, title: string) => {
|
|
141
|
+
const conversation = getConversation(id)
|
|
142
|
+
conversation.summary = { ...conversation.summary, title, updatedAt: Date.now() }
|
|
143
|
+
return { ...conversation.summary }
|
|
144
|
+
},
|
|
145
|
+
editTurn: async (conversationId: string, turnId: string, text: string, expectedText: string) => {
|
|
146
|
+
const conversation = getConversation(conversationId)
|
|
147
|
+
const turn = getTurn(conversationId, turnId)
|
|
148
|
+
if (turn.text !== expectedText) throw clientError('conflict', 'turn changed', { reason: 'text_changed' })
|
|
149
|
+
if (!text.trim() || Array.from(text).length > 24_000)
|
|
150
|
+
throw clientError('bad_request', 'invalid text', { field: 'text' })
|
|
151
|
+
if (conversation.turns.some((candidate) => candidate.parentId === turnId || candidate.supersedesId === turnId))
|
|
152
|
+
throw clientError('conflict', 'turn has children', { reason: 'has_children' })
|
|
153
|
+
turn.text = text
|
|
154
|
+
turn.reasoningText = null
|
|
155
|
+
turn.contextUsage = null
|
|
156
|
+
conversation.summary.updatedAt = Date.now()
|
|
157
|
+
return snapshot(conversation)
|
|
158
|
+
},
|
|
159
|
+
removeTurn: async (conversationId: string, turnId: string) => {
|
|
160
|
+
// Same rule as both hosts (AutoNote docs/35 §3.3): reparent onto the successor, then delete.
|
|
161
|
+
const conversation = getConversation(conversationId)
|
|
162
|
+
const turn = getTurn(conversationId, turnId)
|
|
163
|
+
if (turn.status === 'running') throw clientError('conflict', 'turn running', { reason: 'turn_running' })
|
|
164
|
+
// Turns are stored in creation order, which stands in for `seq`.
|
|
165
|
+
const at = conversation.turns.indexOf(turn)
|
|
166
|
+
const group = conversation.turns.filter(
|
|
167
|
+
(t) => t.kind === 'reply' && t.questionId === turn.questionId && t !== turn,
|
|
168
|
+
)
|
|
169
|
+
const sibling = group.find((t) => conversation.turns.indexOf(t) > at) ?? group.at(-1)
|
|
170
|
+
const successor = sibling?.id ?? turn.parentId
|
|
171
|
+
const removed = new Set([turnId])
|
|
172
|
+
for (const t of conversation.turns)
|
|
173
|
+
if (t.kind === 'translation' && removed.has(t.parentId ?? '')) removed.add(t.id)
|
|
174
|
+
conversation.turns = conversation.turns
|
|
175
|
+
.filter((t) => !removed.has(t.id))
|
|
176
|
+
.map((t) => ({
|
|
177
|
+
...t,
|
|
178
|
+
parentId: removed.has(t.parentId ?? '') ? successor : t.parentId,
|
|
179
|
+
supersedesId: removed.has(t.supersedesId ?? '')
|
|
180
|
+
? sibling && sibling.id !== t.id
|
|
181
|
+
? sibling.id
|
|
182
|
+
: null
|
|
183
|
+
: t.supersedesId,
|
|
184
|
+
}))
|
|
185
|
+
if (removed.has(conversation.summary.selectedTurnId ?? '')) {
|
|
186
|
+
const rest = conversation.turns.filter((t) => t.kind === 'reply')
|
|
187
|
+
let head = successor ?? rest.filter((t) => t.parentId === null).at(-1)?.id ?? null
|
|
188
|
+
for (;;) {
|
|
189
|
+
const child = head === null ? undefined : rest.filter((t) => t.parentId === head).at(-1)
|
|
190
|
+
if (!child) break
|
|
191
|
+
head = child.id
|
|
192
|
+
}
|
|
193
|
+
conversation.summary.selectedTurnId = head
|
|
194
|
+
}
|
|
195
|
+
conversation.summary.updatedAt = Date.now()
|
|
196
|
+
return snapshot(conversation)
|
|
197
|
+
},
|
|
198
|
+
fork: async (conversationId: string, turnId: string) => {
|
|
199
|
+
const source = getConversation(conversationId)
|
|
200
|
+
const target = getTurn(conversationId, turnId)
|
|
201
|
+
const chain: Turn[] = []
|
|
202
|
+
const seen = new Set<string>()
|
|
203
|
+
let cursor: Turn | undefined = target
|
|
204
|
+
while (cursor) {
|
|
205
|
+
if (seen.has(cursor.id)) throw clientError('conflict', 'history cycle', { reason: 'history_cycle' })
|
|
206
|
+
seen.add(cursor.id)
|
|
207
|
+
chain.unshift(cursor)
|
|
208
|
+
if (!cursor.parentId) break
|
|
209
|
+
cursor = source.turns.find((turn) => turn.id === cursor?.parentId)
|
|
210
|
+
if (!cursor) throw clientError('not_found', 'history parent', { entity: 'turn' })
|
|
211
|
+
}
|
|
212
|
+
const ids = new Map<string, string>()
|
|
213
|
+
const questionIds = new Map<string, string>()
|
|
214
|
+
const turns = chain.map((turn) => {
|
|
215
|
+
const id = nextId('fake-turn')
|
|
216
|
+
ids.set(turn.id, id)
|
|
217
|
+
let questionId = questionIds.get(turn.questionId)
|
|
218
|
+
if (questionId === undefined) {
|
|
219
|
+
questionId = id
|
|
220
|
+
questionIds.set(turn.questionId, questionId)
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
...turn,
|
|
224
|
+
id,
|
|
225
|
+
questionId,
|
|
226
|
+
parentId: turn.parentId ? (ids.get(turn.parentId) ?? null) : null,
|
|
227
|
+
supersedesId: turn.supersedesId ? (ids.get(turn.supersedesId) ?? null) : null,
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
let id: string
|
|
231
|
+
do {
|
|
232
|
+
id = `fake-conversation-${++conversationSequence}`
|
|
233
|
+
} while (conversations.has(id))
|
|
234
|
+
const copy: Conversation = {
|
|
235
|
+
summary: {
|
|
236
|
+
...source.summary,
|
|
237
|
+
id,
|
|
238
|
+
createdAt: Date.now(),
|
|
239
|
+
updatedAt: Date.now(),
|
|
240
|
+
selectedTurnId: ids.get(turnId) ?? null,
|
|
241
|
+
},
|
|
242
|
+
turns: structuredClone(turns),
|
|
243
|
+
}
|
|
244
|
+
conversations.set(id, copy)
|
|
245
|
+
return snapshot(copy)
|
|
246
|
+
},
|
|
247
|
+
selectTurn: async (conversationId: string, turnId: string) => {
|
|
248
|
+
const conversation = getConversation(conversationId)
|
|
249
|
+
getTurn(conversationId, turnId)
|
|
250
|
+
conversation.summary = { ...conversation.summary, selectedTurnId: turnId, updatedAt: Date.now() }
|
|
251
|
+
return snapshot(conversation)
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
const ports: Required<Omit<KitPorts, 'search' | 'skillSources'>> = {
|
|
255
|
+
logs: {
|
|
256
|
+
tail: () => Promise.resolve([]),
|
|
257
|
+
ingest: (batch) => Promise.resolve({ accepted: batch.entries.length, rejected: 0 }),
|
|
258
|
+
getLevel: () => Promise.resolve({ level, available: true }),
|
|
259
|
+
setLevel: (next) => {
|
|
260
|
+
level = next
|
|
261
|
+
return Promise.resolve({ level, available: true })
|
|
262
|
+
},
|
|
263
|
+
...options.logs,
|
|
264
|
+
},
|
|
265
|
+
desktop: {
|
|
266
|
+
openLogsDirectory: () => Promise.resolve(),
|
|
267
|
+
...options.desktop,
|
|
268
|
+
},
|
|
269
|
+
conversations: { ...conversationClient, ...options.conversations },
|
|
270
|
+
providers: {
|
|
271
|
+
revealNeedsPassword: false,
|
|
272
|
+
keyStrategies: false,
|
|
273
|
+
revealKey: () => Promise.reject(clientError('internal', 'Configure the fake provider reveal method')),
|
|
274
|
+
list: () => Promise.resolve([]),
|
|
275
|
+
setEnabled: () => Promise.resolve([]),
|
|
276
|
+
save: () => Promise.resolve([]),
|
|
277
|
+
saveKey: () => Promise.resolve([]),
|
|
278
|
+
removeKey: () => Promise.resolve([]),
|
|
279
|
+
remove: () => Promise.resolve([]),
|
|
280
|
+
modelOptions: () => Promise.resolve([]),
|
|
281
|
+
probe: () => Promise.resolve({ probeUrl: '', models: [], error: null, suggestedProtocol: null }),
|
|
282
|
+
...options.providers,
|
|
283
|
+
},
|
|
284
|
+
chat: {
|
|
285
|
+
start: async (request, onEvent, callOptions) => {
|
|
286
|
+
if (callOptions?.signal?.aborted) throw clientError('cancelled', 'cancelled')
|
|
287
|
+
const retry = request.retryTurnId != null || request.regenerate
|
|
288
|
+
const translation = request.translateTurnId != null
|
|
289
|
+
const action = request.retryTurnId != null || translation
|
|
290
|
+
if (
|
|
291
|
+
(retry && translation) ||
|
|
292
|
+
(action && (request.regenerate || request.webSearch === true)) ||
|
|
293
|
+
(request.replyToId != null && (retry || translation)) ||
|
|
294
|
+
(request.targetLanguage != null && !translation)
|
|
295
|
+
)
|
|
296
|
+
throw clientError('bad_request', 'invalid graph request', { field: 'action' })
|
|
297
|
+
if (request.images?.length && (retry || translation))
|
|
298
|
+
throw clientError('bad_request', 'images on action', { field: 'images', reason: 'action_conflict' })
|
|
299
|
+
if (request.purpose === 'writing' && (request.replyToId != null || action || request.targetLanguage != null))
|
|
300
|
+
throw clientError('bad_request', 'writing graph fields', { field: 'action' })
|
|
301
|
+
if ((retry || translation || request.replyToId != null) && (!request.conversationId || !storesConversations))
|
|
302
|
+
throw clientError('bad_request', 'conversation required', { field: 'conversationId' })
|
|
303
|
+
const language = request.targetLanguage?.trim() ?? ''
|
|
304
|
+
if (translation && (!language || Array.from(language).length > 80))
|
|
305
|
+
throw clientError('bad_request', 'invalid language', { field: 'targetLanguage' })
|
|
306
|
+
if (!retry && !translation && !request.input.trim())
|
|
307
|
+
throw clientError('bad_request', 'empty input', { field: 'input' })
|
|
308
|
+
if (!storesConversations || request.purpose === 'writing')
|
|
309
|
+
return acceptRun(null, null, false, onEvent, callOptions?.signal)
|
|
310
|
+
let conversationId = request.conversationId
|
|
311
|
+
if (!conversationId) {
|
|
312
|
+
do {
|
|
313
|
+
conversationId = `fake-conversation-${++conversationSequence}`
|
|
314
|
+
} while (conversations.has(conversationId))
|
|
315
|
+
}
|
|
316
|
+
const conversation: Conversation = request.conversationId
|
|
317
|
+
? getConversation(conversationId)
|
|
318
|
+
: {
|
|
319
|
+
summary: {
|
|
320
|
+
id: conversationId,
|
|
321
|
+
title: request.input.slice(0, 80),
|
|
322
|
+
scope: request.scope ?? null,
|
|
323
|
+
createdAt: Date.now(),
|
|
324
|
+
updatedAt: Date.now(),
|
|
325
|
+
selectedTurnId: null,
|
|
326
|
+
},
|
|
327
|
+
turns: [],
|
|
328
|
+
}
|
|
329
|
+
const selectedId =
|
|
330
|
+
conversation.summary.selectedTurnId ??
|
|
331
|
+
[...conversation.turns].reverse().find((turn) => turn.status === 'completed')?.id ??
|
|
332
|
+
null
|
|
333
|
+
const sourceId = request.retryTurnId ?? request.translateTurnId ?? (retry ? selectedId : null)
|
|
334
|
+
const source = sourceId ? getTurn(conversationId, sourceId) : undefined
|
|
335
|
+
if ((retry || translation) && !source) throw clientError('not_found', 'source turn', { entity: 'turn' })
|
|
336
|
+
if (request.replyToId) getTurn(conversationId, request.replyToId)
|
|
337
|
+
if (!retry && !translation && selectedId && !request.replyToId) getTurn(conversationId, selectedId)
|
|
338
|
+
const model =
|
|
339
|
+
request.model ??
|
|
340
|
+
(source?.endpointId && source.modelId ? { endpointId: source.endpointId, modelId: source.modelId } : null)
|
|
341
|
+
if (source && !model) throw clientError('needs_config', 'source model unavailable')
|
|
342
|
+
if (source && model && options.providers?.modelOptions) {
|
|
343
|
+
const models = await ports.providers.modelOptions()
|
|
344
|
+
if (!models.some((option) => option.endpointId === model.endpointId && option.modelId === model.modelId))
|
|
345
|
+
throw clientError('needs_config', 'source model unavailable')
|
|
346
|
+
}
|
|
347
|
+
if (callOptions?.signal?.aborted) throw clientError('cancelled', 'cancelled')
|
|
348
|
+
if (request.conversationId) getConversation(conversationId)
|
|
349
|
+
conversations.set(conversationId, conversation)
|
|
350
|
+
const turnId = nextId('fake-turn')
|
|
351
|
+
const translationSource = translation ? source : undefined
|
|
352
|
+
const retrySource = retry ? source : undefined
|
|
353
|
+
const input = translationSource
|
|
354
|
+
? `Translate the following reply into ${language}. Return only the translation, preserving Markdown formatting.\n\n${translationSource.text}`
|
|
355
|
+
: retrySource
|
|
356
|
+
? retrySource.input
|
|
357
|
+
: request.input
|
|
358
|
+
const turn: Turn = {
|
|
359
|
+
id: turnId,
|
|
360
|
+
parentId: translationSource
|
|
361
|
+
? translationSource.id
|
|
362
|
+
: retrySource
|
|
363
|
+
? retrySource.parentId
|
|
364
|
+
: (request.replyToId ?? selectedId),
|
|
365
|
+
questionId: retrySource ? retrySource.questionId : turnId,
|
|
366
|
+
supersedesId: retrySource ? retrySource.id : null,
|
|
367
|
+
kind: translationSource ? 'translation' : retrySource ? retrySource.kind : 'reply',
|
|
368
|
+
endpointId: model?.endpointId ?? null,
|
|
369
|
+
modelId: model?.modelId ?? null,
|
|
370
|
+
input,
|
|
371
|
+
images: translationSource
|
|
372
|
+
? []
|
|
373
|
+
: retrySource
|
|
374
|
+
? retrySource.images
|
|
375
|
+
: (request.images ?? []).map((image) => ({
|
|
376
|
+
mime: image.mime,
|
|
377
|
+
url: `data:${image.mime};base64,${image.data}`,
|
|
378
|
+
})),
|
|
379
|
+
generated: [],
|
|
380
|
+
text: `Fake reply: ${input}`,
|
|
381
|
+
status: 'running',
|
|
382
|
+
errorCode: null,
|
|
383
|
+
reasoningText: null,
|
|
384
|
+
reasoningEffort: request.reasoningEffort ?? source?.reasoningEffort ?? 'default',
|
|
385
|
+
usage: null,
|
|
386
|
+
contextUsage: {
|
|
387
|
+
tokens: input.length * 2 + 12,
|
|
388
|
+
contextWindow: null,
|
|
389
|
+
source: 'estimated',
|
|
390
|
+
endpointId: model?.endpointId ?? null,
|
|
391
|
+
modelId: model?.modelId ?? null,
|
|
392
|
+
},
|
|
393
|
+
}
|
|
394
|
+
conversation.turns.push(turn)
|
|
395
|
+
return acceptRun(conversation, turn, !retry, onEvent, callOptions?.signal)
|
|
396
|
+
},
|
|
397
|
+
cancel: async (runId) => {
|
|
398
|
+
const run = runs.get(runId)
|
|
399
|
+
if (run?.status === 'running') run.cancelRequested = true
|
|
400
|
+
},
|
|
401
|
+
approve: async (runId) => {
|
|
402
|
+
if (!runs.has(runId)) throw clientError('not_found', `run ${runId}`, { entity: 'run' })
|
|
403
|
+
throw clientError('conflict', 'no pending approval', { reason: 'approval_expired' })
|
|
404
|
+
},
|
|
405
|
+
getSnapshot: async (runId) => {
|
|
406
|
+
const run = runs.get(runId)
|
|
407
|
+
if (!run) throw clientError('not_found', `run ${runId}`, { entity: 'run' })
|
|
408
|
+
return { ...run }
|
|
409
|
+
},
|
|
410
|
+
...options.chat,
|
|
411
|
+
},
|
|
412
|
+
skills: {
|
|
413
|
+
list: () => Promise.resolve([]),
|
|
414
|
+
get: (id) => Promise.reject(clientError('not_found', `skill ${id}`, { entity: 'skill' })),
|
|
415
|
+
preview: () => Promise.reject(clientError('bad_request', 'no package', { field: 'SKILL.md' })),
|
|
416
|
+
install: () => Promise.resolve([]),
|
|
417
|
+
save: () => Promise.resolve([]),
|
|
418
|
+
duplicate: () => Promise.resolve([]),
|
|
419
|
+
reorder: () => Promise.resolve([]),
|
|
420
|
+
remove: () => Promise.resolve([]),
|
|
421
|
+
...options.skills,
|
|
422
|
+
},
|
|
423
|
+
mcpClient: {
|
|
424
|
+
list: () => Promise.resolve([]),
|
|
425
|
+
save: () => Promise.resolve([]),
|
|
426
|
+
remove: () => Promise.resolve([]),
|
|
427
|
+
refresh: () => Promise.resolve([]),
|
|
428
|
+
setToolApproval: () => Promise.resolve(),
|
|
429
|
+
...options.mcpClient,
|
|
430
|
+
},
|
|
431
|
+
mcpServer: {
|
|
432
|
+
status: () => Promise.resolve(STOPPED),
|
|
433
|
+
tokens: () => Promise.resolve([]),
|
|
434
|
+
start: () => Promise.resolve(STOPPED),
|
|
435
|
+
stop: () => Promise.resolve(STOPPED),
|
|
436
|
+
setPort: (port) => Promise.resolve({ ...STOPPED, port }),
|
|
437
|
+
issueToken: (name, scopes) =>
|
|
438
|
+
Promise.resolve({
|
|
439
|
+
token: 'kit_fake',
|
|
440
|
+
id: 'fake-token',
|
|
441
|
+
name,
|
|
442
|
+
scopes: [...scopes],
|
|
443
|
+
createdAt: 0,
|
|
444
|
+
lastUsedAt: null,
|
|
445
|
+
revokedAt: null,
|
|
446
|
+
}),
|
|
447
|
+
revokeToken: () => Promise.resolve(),
|
|
448
|
+
scopes: () => Promise.resolve([]),
|
|
449
|
+
setScopes: () => Promise.resolve([]),
|
|
450
|
+
...options.mcpServer,
|
|
451
|
+
},
|
|
452
|
+
}
|
|
453
|
+
const result: KitPorts = { ...ports }
|
|
454
|
+
if (options.search) {
|
|
455
|
+
const unavailable = () => Promise.reject(clientError('internal', 'Configure the fake search method'))
|
|
456
|
+
result.search = {
|
|
457
|
+
settings: unavailable,
|
|
458
|
+
save: unavailable,
|
|
459
|
+
setDefault: unavailable,
|
|
460
|
+
savePreferences: unavailable,
|
|
461
|
+
saveKey: unavailable,
|
|
462
|
+
removeKey: unavailable,
|
|
463
|
+
reveal: unavailable,
|
|
464
|
+
check: unavailable,
|
|
465
|
+
search: unavailable,
|
|
466
|
+
fetch: unavailable,
|
|
467
|
+
...options.search,
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (options.skillSources) {
|
|
471
|
+
const unavailable = () => Promise.reject(clientError('internal', 'Configure the fake skill source method'))
|
|
472
|
+
result.skillSources = {
|
|
473
|
+
search: unavailable,
|
|
474
|
+
prepare: unavailable,
|
|
475
|
+
discover: unavailable,
|
|
476
|
+
prepareSystem: unavailable,
|
|
477
|
+
pickDirectory: unavailable,
|
|
478
|
+
pickSearchDirectory: unavailable,
|
|
479
|
+
...options.skillSources,
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
for (const name of options.omit ?? []) delete result[name]
|
|
483
|
+
return result
|
|
484
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Public contracts, client ports and test fakes (docs/16 §2, §6).
|
|
2
|
+
export type * from '@ouraihub/aether-contracts'
|
|
3
|
+
export * from './contract'
|
|
4
|
+
export type * from './search'
|
|
5
|
+
export type * from './skill-sources'
|
|
6
|
+
export { createFakePorts } from './fake'
|
|
7
|
+
export type { FakePortsOptions } from './fake'
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CallOptions } from './contract'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
SearchCap,
|
|
5
|
+
SearchPreferences,
|
|
6
|
+
SearchSettingsDto,
|
|
7
|
+
SearchSave,
|
|
8
|
+
SearchResult,
|
|
9
|
+
SearchPage,
|
|
10
|
+
} from '@ouraihub/aether-contracts'
|
|
11
|
+
export type {
|
|
12
|
+
SearchCap,
|
|
13
|
+
SearchKey,
|
|
14
|
+
SearchPreferences,
|
|
15
|
+
SearchProvider,
|
|
16
|
+
SearchSettingsDto,
|
|
17
|
+
SearchSave,
|
|
18
|
+
SearchResult,
|
|
19
|
+
SearchHit,
|
|
20
|
+
SearchPage,
|
|
21
|
+
} from '@ouraihub/aether-contracts'
|
|
22
|
+
|
|
23
|
+
export interface SearchPort {
|
|
24
|
+
revealNeedsPassword?: boolean
|
|
25
|
+
settings(capability: SearchCap, options?: CallOptions): Promise<SearchSettingsDto>
|
|
26
|
+
save(input: SearchSave, options?: CallOptions): Promise<SearchSettingsDto>
|
|
27
|
+
setDefault(capability: SearchCap, provider: string, options?: CallOptions): Promise<SearchSettingsDto>
|
|
28
|
+
savePreferences(input: SearchPreferences, options?: CallOptions): Promise<SearchPreferences>
|
|
29
|
+
saveKey(
|
|
30
|
+
capability: SearchCap,
|
|
31
|
+
provider: string,
|
|
32
|
+
value: string,
|
|
33
|
+
keyId?: string,
|
|
34
|
+
options?: CallOptions,
|
|
35
|
+
): Promise<SearchSettingsDto>
|
|
36
|
+
removeKey(capability: SearchCap, provider: string, keyId: string, options?: CallOptions): Promise<SearchSettingsDto>
|
|
37
|
+
reveal(
|
|
38
|
+
capability: SearchCap,
|
|
39
|
+
provider: string,
|
|
40
|
+
keyId: string,
|
|
41
|
+
currentPassword?: string,
|
|
42
|
+
options?: CallOptions,
|
|
43
|
+
): Promise<string>
|
|
44
|
+
check(capability: SearchCap, provider: string, options?: CallOptions): Promise<{ count: number }>
|
|
45
|
+
search(query: string, options?: CallOptions): Promise<SearchResult>
|
|
46
|
+
fetch(url: string, options?: CallOptions): Promise<SearchPage>
|
|
47
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ModelRef, RunAccepted, RunEvent, Source } from '@ouraihub/aether-contracts'
|
|
2
|
+
import type { CallOptions } from './contract'
|
|
3
|
+
|
|
4
|
+
export interface SkillCandidate {
|
|
5
|
+
name: string
|
|
6
|
+
description: string
|
|
7
|
+
url: string
|
|
8
|
+
origin: string
|
|
9
|
+
}
|
|
10
|
+
export interface SystemSkillCandidate {
|
|
11
|
+
name: string
|
|
12
|
+
path: string
|
|
13
|
+
origin: string
|
|
14
|
+
}
|
|
15
|
+
export interface PreparedSkill {
|
|
16
|
+
source: Source
|
|
17
|
+
origin: string
|
|
18
|
+
ignored?: string[]
|
|
19
|
+
}
|
|
20
|
+
/** A multi-turn skill-creator workspace; its package is installed only through preview + digest. */
|
|
21
|
+
export interface SkillAuthoringPort {
|
|
22
|
+
create(options?: CallOptions): Promise<string>
|
|
23
|
+
send(
|
|
24
|
+
sessionId: string,
|
|
25
|
+
input: string,
|
|
26
|
+
model: ModelRef,
|
|
27
|
+
onEvent: (event: RunEvent) => void,
|
|
28
|
+
options?: CallOptions,
|
|
29
|
+
): Promise<RunAccepted>
|
|
30
|
+
approve(sessionId: string, runId: string, callId: string, allow: boolean): Promise<void>
|
|
31
|
+
cancel(sessionId: string, runId: string): Promise<void>
|
|
32
|
+
prepare(sessionId: string, options?: CallOptions): Promise<PreparedSkill>
|
|
33
|
+
close(sessionId: string): Promise<void>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Optional acquisition capabilities; missing desktop actions never appear in Web hosts. */
|
|
37
|
+
export interface SkillSourcesPort {
|
|
38
|
+
search(query: string, options?: CallOptions): Promise<SkillCandidate[]>
|
|
39
|
+
prepare(url: string, options?: CallOptions): Promise<PreparedSkill>
|
|
40
|
+
discover(options?: CallOptions): Promise<SystemSkillCandidate[]>
|
|
41
|
+
prepareSystem(path: string, options?: CallOptions): Promise<PreparedSkill>
|
|
42
|
+
pickDirectory(options?: CallOptions): Promise<PreparedSkill | null>
|
|
43
|
+
pickSearchDirectory(options?: CallOptions): Promise<SystemSkillCandidate[] | null>
|
|
44
|
+
authoring?: SkillAuthoringPort
|
|
45
|
+
}
|