feat(app): delegate chat to Archy's node-side assistant loop when embedded (Archy phase 13, D-01/D-17)
Adds archyBridge.sendChat(text) built on the existing postToParent + origin-validated listener pattern (same request-id correlation as requestContext), with a 180s timeout matching the node's ASSISTANT_HTTP_TIMEOUT. Adds useAI.ts's streamViaArchy, which branches all three existing send sites on the same __AIUI_EMBEDDED__ signal useArchy.ts already reads: embedded mode delegates the model call, the tool-calling loop and the model key to the node; standalone mode is untouched and keeps using streamClaude/streamOpenRouter with AIUI's own dev proxy (D-17). CLAUDE_PATH/OPENROUTER_PATH are not removed — 13-02 changes what those paths resolve to on a node, 13-09 retires them. Verified: vitest run 332/335 passing (3 pre-existing failures confirmed via a scratch worktree at the prior HEAD, unrelated to this change — seed extraction count assertions and a web-search-integration body.webSearch assertion); vue-tsc --noEmit clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import type { ImageAttachment } from '@aiui/core/types/message'
|
|||||||
import { usePersonaStore } from '@/stores/personas'
|
import { usePersonaStore } from '@/stores/personas'
|
||||||
import { useMemoryStore } from '@/stores/memory'
|
import { useMemoryStore } from '@/stores/memory'
|
||||||
import { useArchy } from '@/composables/useArchy'
|
import { useArchy } from '@/composables/useArchy'
|
||||||
|
import { archyBridge } from '@/services/archyBridge'
|
||||||
import { useCodeContext } from '@/composables/useCodeContext'
|
import { useCodeContext } from '@/composables/useCodeContext'
|
||||||
import { apiFetch } from '@/utils/api-fetch'
|
import { apiFetch } from '@/utils/api-fetch'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
@@ -374,6 +375,33 @@ async function streamOpenRouter(
|
|||||||
}, onError, signal)
|
}, onError, signal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Embedded-mode chat delegation (D-01/D-17): when AIUI is running inside
|
||||||
|
* Archy, the model call, the tool-calling loop, and the model key all live
|
||||||
|
* node-side. This sends only the latest user turn over the existing
|
||||||
|
* origin-checked postMessage bridge (`archyBridge.sendChat`) and emits the
|
||||||
|
* node's final answer as a single token — there is no streaming across the
|
||||||
|
* bridge in this tracer, only a one-shot `chat:request`/`chat:response`
|
||||||
|
* round trip.
|
||||||
|
*/
|
||||||
|
async function streamViaArchy(
|
||||||
|
messages: ChatMessage[],
|
||||||
|
onToken: (text: string) => void,
|
||||||
|
onError: (err: string) => void,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<void> {
|
||||||
|
const lastUser = [...messages].reverse().find((m) => m.role === 'user')
|
||||||
|
const text = lastUser?.content ?? ''
|
||||||
|
try {
|
||||||
|
const result = await archyBridge.sendChat(text)
|
||||||
|
if (signal?.aborted) return
|
||||||
|
onToken(result.text)
|
||||||
|
} catch (err) {
|
||||||
|
if (signal?.aborted) return
|
||||||
|
onError(err instanceof Error ? err.message : 'Archy chat request failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function readSSE(
|
async function readSSE(
|
||||||
res: Response,
|
res: Response,
|
||||||
onData: (data: string) => void,
|
onData: (data: string) => void,
|
||||||
@@ -560,7 +588,11 @@ export async function streamWithModel(
|
|||||||
activeModel.value = model
|
activeModel.value = model
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (provider === 'claude') {
|
if (useArchy().isEmbedded.value) {
|
||||||
|
// D-17: embedded mode delegates the loop, the tools and the key to
|
||||||
|
// Archy — provider/model selection here doesn't apply node-side.
|
||||||
|
await streamViaArchy(history, onToken, onError, signal)
|
||||||
|
} else if (provider === 'claude') {
|
||||||
await streamClaude(history, onToken, onError, 'You are a helpful assistant.', false, signal)
|
await streamClaude(history, onToken, onError, 'You are a helpful assistant.', false, signal)
|
||||||
} else if (provider === 'openrouter') {
|
} else if (provider === 'openrouter') {
|
||||||
await streamOpenRouter(history, onToken, onError, 'You are a helpful assistant.', signal)
|
await streamOpenRouter(history, onToken, onError, 'You are a helpful assistant.', signal)
|
||||||
@@ -643,7 +675,11 @@ export function useAI() {
|
|||||||
const genParams = getConversationParams(chatStore)
|
const genParams = getConversationParams(chatStore)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (provider === 'claude') {
|
if (useArchy().isEmbedded.value) {
|
||||||
|
// D-17: embedded mode delegates the loop, the tools and the key to
|
||||||
|
// Archy — provider/model selection here doesn't apply node-side.
|
||||||
|
await streamViaArchy(history, onToken, onError, signal)
|
||||||
|
} else if (provider === 'claude') {
|
||||||
await streamClaude(history, onToken, onError, systemPrompt, proxyWebSearch, signal, genParams)
|
await streamClaude(history, onToken, onError, systemPrompt, proxyWebSearch, signal, genParams)
|
||||||
} else if (provider === 'openrouter') {
|
} else if (provider === 'openrouter') {
|
||||||
await streamOpenRouter(history, onToken, onError, systemPrompt, signal)
|
await streamOpenRouter(history, onToken, onError, systemPrompt, signal)
|
||||||
@@ -755,7 +791,11 @@ export function useAI() {
|
|||||||
const genParams = getConversationParams(chatStore)
|
const genParams = getConversationParams(chatStore)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (provider === 'claude') {
|
if (useArchy().isEmbedded.value) {
|
||||||
|
// D-17: embedded mode delegates the loop, the tools and the key to
|
||||||
|
// Archy — provider/model selection here doesn't apply node-side.
|
||||||
|
await streamViaArchy(history, onToken, onError, signal)
|
||||||
|
} else if (provider === 'claude') {
|
||||||
await streamClaude(history, onToken, onError, systemPrompt, chatStore.webSearchEnabled, signal, genParams)
|
await streamClaude(history, onToken, onError, systemPrompt, chatStore.webSearchEnabled, signal, genParams)
|
||||||
} else if (provider === 'openrouter') {
|
} else if (provider === 'openrouter') {
|
||||||
await streamOpenRouter(history, onToken, onError, systemPrompt, signal)
|
await streamOpenRouter(history, onToken, onError, systemPrompt, signal)
|
||||||
|
|||||||
@@ -74,6 +74,19 @@ function handleMessage(event: MessageEvent) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'chat:response': {
|
||||||
|
const pending = pendingRequests.get(msg.id)
|
||||||
|
if (pending) {
|
||||||
|
pendingRequests.delete(msg.id)
|
||||||
|
if (msg.success) {
|
||||||
|
pending.resolve({ text: msg.text ?? '' })
|
||||||
|
} else {
|
||||||
|
pending.reject(new Error(msg.error || 'Chat request failed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'permissions:update': {
|
case 'permissions:update': {
|
||||||
currentPermissions = msg.categories || []
|
currentPermissions = msg.categories || []
|
||||||
for (const cb of permissionsCallbacks) cb(currentPermissions)
|
for (const cb of permissionsCallbacks) cb(currentPermissions)
|
||||||
@@ -189,6 +202,32 @@ export const archyBridge = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a chat turn to Archy's node-side assistant loop (D-01: the model
|
||||||
|
* key and the tool-calling loop live on the node, never in this bundle).
|
||||||
|
* Returns the assistant's final text answer for this turn.
|
||||||
|
*/
|
||||||
|
sendChat(text: string): Promise<{ text: string }> {
|
||||||
|
const id = generateId()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
pendingRequests.set(id, { resolve: resolve as (v: unknown) => void, reject })
|
||||||
|
postToParent({
|
||||||
|
type: 'chat:request',
|
||||||
|
id,
|
||||||
|
text,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Matches the node's ASSISTANT_HTTP_TIMEOUT (180s) — the assistant
|
||||||
|
// loop's tool-calling round trip can legitimately take that long.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pendingRequests.has(id)) {
|
||||||
|
pendingRequests.delete(id)
|
||||||
|
reject(new Error('Chat request timed out'))
|
||||||
|
}
|
||||||
|
}, 180000)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
/** Request Archy's theme info */
|
/** Request Archy's theme info */
|
||||||
requestTheme() {
|
requestTheme() {
|
||||||
postToParent({ type: 'theme:request' })
|
postToParent({ type: 'theme:request' })
|
||||||
|
|||||||
Reference in New Issue
Block a user